diff --git a/apis/v1alpha1/gameserver_types.go b/apis/v1alpha1/gameserver_types.go index b9c93b18..2befbf26 100644 --- a/apis/v1alpha1/gameserver_types.go +++ b/apis/v1alpha1/gameserver_types.go @@ -41,6 +41,21 @@ 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. + // When Image updated, pod.spec.containers[*].image will be updated immediately. + Image string `json:"image,omitempty"` + // Resources indicates the resources of the container to update. + // When Resources updated, pod.spec.containers[*].Resources will be not updated immediately, + // which will be updated when pod recreate. + 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 00902561..dda2db3b 100644 --- a/config/crd/bases/game.kruise.io_gameservers.yaml +++ b/config/crd/bases/game.kruise.io_gameservers.yaml @@ -58,6 +58,54 @@ 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. + When Image updated, pod.spec.containers[*].image will be updated + immediately. + 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. When Resources updated, pod.spec.containers[*].Resources + will be not updated immediately, which will be updated when + pod recreate. + 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 60ae5be9..030e8eda 100644 --- a/config/crd/bases/game.kruise.io_gameserversets.yaml +++ b/config/crd/bases/game.kruise.io_gameserversets.yaml @@ -535,6 +535,56 @@ 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. When Image updated, pod.spec.containers[*].image + will be updated immediately. + 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. When Resources updated, + pod.spec.containers[*].Resources will be not updated + immediately, which will be updated when pod recreate. + 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/docs/en/user_manuals/CRD_field_description.md b/docs/en/user_manuals/CRD_field_description.md index 8f05b14b..d00fd75e 100644 --- a/docs/en/user_manuals/CRD_field_description.md +++ b/docs/en/user_manuals/CRD_field_description.md @@ -241,6 +241,24 @@ type GameServerSpec struct { // Whether to perform network isolation and cut off the access layer network // Default is false 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. + // When Image updated, pod.spec.containers[*].image will be updated immediately. + Image string `json:"image,omitempty"` + + // Resources indicates the resources of the container to update. + // When Resources updated, pod.spec.containers[*].Resources will be not updated immediately, + // which will be updated when pod recreate. + Resources corev1.ResourceRequirements `json:"resources,omitempty"` } ``` diff --git "a/docs/\344\270\255\346\226\207/\347\224\250\346\210\267\346\211\213\345\206\214/CRD\345\255\227\346\256\265\350\257\264\346\230\216.md" "b/docs/\344\270\255\346\226\207/\347\224\250\346\210\267\346\211\213\345\206\214/CRD\345\255\227\346\256\265\350\257\264\346\230\216.md" index fae99331..7c38cad1 100644 --- "a/docs/\344\270\255\346\226\207/\347\224\250\346\210\267\346\211\213\345\206\214/CRD\345\255\227\346\256\265\350\257\264\346\230\216.md" +++ "b/docs/\344\270\255\346\226\207/\347\224\250\346\210\267\346\211\213\345\206\214/CRD\345\255\227\346\256\265\350\257\264\346\230\216.md" @@ -202,7 +202,25 @@ type GameServerSpec struct { // 是否进行网络隔离、切断接入层网络,默认为false NetworkDisabled bool `json:"networkDisabled,omitempty"` + + // 使对应的GameServer Containers字段与GameServerSetSpec中GameServerTemplate定义的字段不同,意味着该GameServer可以拥有独立的参数配置。 + // 当前支持更改 Image 与 Resources + Containers []GameServerContainer `json:"containers,omitempty"` } + +type GameServerContainer struct { + // Name 表示要更新的容器的名称。 + Name string `json:"name"` + + // Image 表示要更新的容器的镜像。 + // 当Image更新时,pod.spec.containers[*].image会立即更新。 + Image string `json:"image,omitempty"` + + // Resources 表示要更新的容器的资源。 + // 当Resources更新时,pod.spec.containers[*].Resources不会立即更新,它会在pod重建时更新。 + Resources corev1.ResourceRequirements `json:"resources,omitempty"` +} + ``` ### GameServerStatus diff --git a/pkg/controllers/gameserver/gameserver_manager.go b/pkg/controllers/gameserver/gameserver_manager.go index 3f9f25d8..9df060f9 100644 --- a/pkg/controllers/gameserver/gameserver_manager.go +++ b/pkg/controllers/gameserver/gameserver_manager.go @@ -174,8 +174,17 @@ func (manager GameServerManager) SyncGsToPod() error { } } - if len(newLabels) != 0 || len(newAnnotations) != 0 { - patchPod := map[string]interface{}{"metadata": map[string]map[string]string{"labels": newLabels, "annotations": newAnnotations}} + // sync pod containers when the containers(images) in GameServer are different from that in pod. + containers := manager.syncPodContainers(gs.Spec.Containers, pod.DeepCopy().Spec.Containers) + + if len(newLabels) != 0 || len(newAnnotations) != 0 || containers != nil { + patchPod := make(map[string]interface{}) + if len(newLabels) != 0 || len(newAnnotations) != 0 { + patchPod["metadata"] = map[string]map[string]string{"labels": newLabels, "annotations": newAnnotations} + } + if containers != nil { + patchPod["spec"] = map[string]interface{}{"containers": containers} + } patchPodBytes, err := json.Marshal(patchPod) if err != nil { return err @@ -359,6 +368,29 @@ func syncServiceQualities(serviceQualities []gameKruiseV1alpha1.ServiceQuality, return spec, newGsConditions } +func (manager GameServerManager) syncPodContainers(gsContainers []gameKruiseV1alpha1.GameServerContainer, podContainers []corev1.Container) []corev1.Container { + var newContainers []corev1.Container + for _, podContainer := range podContainers { + for _, gsContainer := range gsContainers { + if gsContainer.Name == podContainer.Name { + var newContainer corev1.Container + newContainer.Name = podContainer.Name + changed := false + if gsContainer.Image != "" && gsContainer.Image != podContainer.Image { + newContainer.Image = gsContainer.Image + changed = true + } + + if changed { + newContainers = append(newContainers, newContainer) + } + } + } + } + + return newContainers +} + func NewGameServerManager(gs *gameKruiseV1alpha1.GameServer, pod *corev1.Pod, c client.Client, recorder record.EventRecorder) Control { return &GameServerManager{ gameServer: gs, diff --git a/pkg/controllers/gameserver/gameserver_manager_test.go b/pkg/controllers/gameserver/gameserver_manager_test.go index f62a2e7a..aa34ff0d 100644 --- a/pkg/controllers/gameserver/gameserver_manager_test.go +++ b/pkg/controllers/gameserver/gameserver_manager_test.go @@ -688,3 +688,58 @@ func TestSyncNetworkStatus(t *testing.T) { } } } + +func TestSyncPodContainers(t *testing.T) { + tests := []struct { + gsContainers []gameKruiseV1alpha1.GameServerContainer + podContainers []corev1.Container + newContainers []corev1.Container + }{ + // case 0 + { + gsContainers: nil, + podContainers: []corev1.Container{ + { + Name: "A", + Image: "A-v1", + }, + }, + newContainers: nil, + }, + + // case 1 + { + gsContainers: []gameKruiseV1alpha1.GameServerContainer{ + { + Name: "A", + Image: "A-v2", + }, + }, + podContainers: []corev1.Container{ + { + Name: "A", + Image: "A-v1", + }, + { + Name: "B", + Image: "B-v1", + }, + }, + newContainers: []corev1.Container{ + { + Name: "A", + Image: "A-v2", + }, + }, + }, + } + + for i, test := range tests { + expect := test.newContainers + manager := &GameServerManager{} + actual := manager.syncPodContainers(test.gsContainers, test.podContainers) + if !reflect.DeepEqual(expect, actual) { + t.Errorf("case %d: expect newContainers %v, but actually got %v", i, expect, actual) + } + } +} 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..c2bf3553 --- /dev/null +++ b/pkg/webhook/mutating_pod_test.go @@ -0,0 +1,282 @@ +package webhook + +import ( + "context" + gameKruiseV1alpha1 "github.com/openkruise/kruise-game/apis/v1alpha1" + admissionv1 "k8s.io/api/admission/v1" + 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" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "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) + } + } +} + +func TestGetPodFromRequest(t *testing.T) { + tests := []struct { + req admission.Request + pod *corev1.Pod + }{ + // case 0 + { + req: admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: admissionv1.Delete, + Object: runtime.RawExtension{ + Raw: []byte(`{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "foo", + "namespace": "default" + }, + "spec": { + "containers": [ + { + "image": "bar:v2", + "name": "bar" + } + ] + } +}`), + }, + OldObject: runtime.RawExtension{ + Raw: []byte(`{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "foo", + "namespace": "default" + }, + "spec": { + "containers": [ + { + "image": "bar:v1", + "name": "bar" + } + ] + } +}`), + }, + }, + }, + pod: &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Pod", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Image: "bar:v1", Name: "bar"}, + }, + }, + }, + }, + + // case 1 + { + req: admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: admissionv1.Update, + Object: runtime.RawExtension{ + Raw: []byte(`{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "foo", + "namespace": "default" + }, + "spec": { + "containers": [ + { + "image": "bar:v2", + "name": "bar" + } + ] + } +}`), + }, + OldObject: runtime.RawExtension{ + Raw: []byte(`{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "foo", + "namespace": "default" + }, + "spec": { + "containers": [ + { + "image": "bar:v1", + "name": "bar" + } + ] + } +}`), + }, + }, + }, + pod: &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Pod", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Image: "bar:v2", Name: "bar"}, + }, + }, + }, + }, + } + + decoder, err := admission.NewDecoder(runtime.NewScheme()) + if err != nil { + t.Error(err) + } + + for i, test := range tests { + actual, err := getPodFromRequest(test.req, decoder) + if err != nil { + t.Error(err) + } + expect := test.pod + if !reflect.DeepEqual(actual, expect) { + t.Errorf("case %d: expect pod %v, but actually got %v", i, expect, actual) + } + } +} diff --git a/pkg/webhook/webhook_test.go b/pkg/webhook/webhook_test.go new file mode 100644 index 00000000..539f687a --- /dev/null +++ b/pkg/webhook/webhook_test.go @@ -0,0 +1,160 @@ +package webhook + +import ( + "context" + v1 "k8s.io/api/admissionregistration/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + "reflect" + "testing" +) + +func TestCheckValidatingConfiguration(t *testing.T) { + tests := []struct { + vwcNow *v1.ValidatingWebhookConfiguration + dnsName string + caBundle []byte + vwcNew *v1.ValidatingWebhookConfiguration + }{ + { + vwcNow: nil, + dnsName: "dnsName", + caBundle: []byte(`xxx`), + vwcNew: &v1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: validatingWebhookConfigurationName, + }, + Webhooks: getValidatingWebhookConf("dnsName", []byte(`xxx`)), + }, + }, + { + vwcNow: &v1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: validatingWebhookConfigurationName, + }, + Webhooks: getValidatingWebhookConf("dnsName", []byte(`old`)), + }, + dnsName: "dnsName", + caBundle: []byte(`new`), + vwcNew: &v1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: validatingWebhookConfigurationName, + }, + Webhooks: getValidatingWebhookConf("dnsName", []byte(`new`)), + }, + }, + { + vwcNow: &v1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: validatingWebhookConfigurationName, + }, + }, + dnsName: "dnsName", + caBundle: []byte(`new`), + vwcNew: &v1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: validatingWebhookConfigurationName, + }, + Webhooks: getValidatingWebhookConf("dnsName", []byte(`new`)), + }, + }, + } + + for i, test := range tests { + clientSet := fake.NewSimpleClientset() + if test.vwcNow != nil { + _, err := clientSet.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), test.vwcNow, metav1.CreateOptions{}) + if err != nil { + t.Error(err) + } + } + + if err := checkValidatingConfiguration(test.dnsName, clientSet, test.caBundle); err != nil { + t.Error(err) + } + + expect := test.vwcNew + actual, err := clientSet.AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(context.TODO(), validatingWebhookConfigurationName, metav1.GetOptions{}) + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(expect.Webhooks, actual.Webhooks) { + t.Errorf("case %d: expect validatingWebhookConfiguration webhooks %v, but actually got %v", i, expect.Webhooks, actual.Webhooks) + } + } +} + +func TestCheckMutatingConfiguration(t *testing.T) { + tests := []struct { + mwcNow *v1.MutatingWebhookConfiguration + dnsName string + caBundle []byte + mwcNew *v1.MutatingWebhookConfiguration + }{ + { + mwcNow: nil, + dnsName: "dnsName", + caBundle: []byte(`xxx`), + mwcNew: &v1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: mutatingWebhookConfigurationName, + }, + Webhooks: getMutatingWebhookConf("dnsName", []byte(`xxx`)), + }, + }, + { + mwcNow: &v1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: mutatingWebhookConfigurationName, + }, + Webhooks: getMutatingWebhookConf("dnsName", []byte(`old`)), + }, + dnsName: "dnsName", + caBundle: []byte(`new`), + mwcNew: &v1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: mutatingWebhookConfigurationName, + }, + Webhooks: getMutatingWebhookConf("dnsName", []byte(`new`)), + }, + }, + { + mwcNow: &v1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: mutatingWebhookConfigurationName, + }, + }, + dnsName: "dnsName", + caBundle: []byte(`new`), + mwcNew: &v1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: mutatingWebhookConfigurationName, + }, + Webhooks: getMutatingWebhookConf("dnsName", []byte(`new`)), + }, + }, + } + + for i, test := range tests { + clientSet := fake.NewSimpleClientset() + if test.mwcNow != nil { + _, err := clientSet.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), test.mwcNow, metav1.CreateOptions{}) + if err != nil { + t.Error(err) + } + } + + if err := checkMutatingConfiguration(test.dnsName, clientSet, test.caBundle); err != nil { + t.Error(err) + } + + expect := test.mwcNew + actual, err := clientSet.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(context.TODO(), mutatingWebhookConfigurationName, metav1.GetOptions{}) + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(expect.Webhooks, actual.Webhooks) { + t.Errorf("case %d: expect validatingWebhookConfiguration webhooks %v, but actually got %v", i, expect.Webhooks, actual.Webhooks) + } + } +}