From 39c82bc168d9e335886a8c49865f6f234f0fca6d Mon Sep 17 00:00:00 2001 From: Simon Tien Date: Fri, 10 Jan 2025 14:15:36 +1100 Subject: [PATCH] feat: upgrade nodepool crd to v1beta2 --- .../crds/apps.openyurt.io_nodepools.yaml | 189 ++++++++++ .../yurt-manager-auto-generated.yaml | 42 +++ pkg/apis/addtoscheme_apps_v1beta2.go | 26 ++ pkg/apis/apps/v1beta2/default.go | 26 ++ pkg/apis/apps/v1beta2/doc.go | 17 + pkg/apis/apps/v1beta2/groupversion_info.go | 44 +++ pkg/apis/apps/v1beta2/nodepool_conversion.go | 26 ++ pkg/apis/apps/v1beta2/nodepool_types.go | 164 +++++++++ .../apps/v1beta2/zz_generated.deepcopy.go | 182 ++++++++++ .../nodepool/v1beta2/nodepool_default.go | 81 +++++ .../nodepool/v1beta2/nodepool_default_test.go | 329 ++++++++++++++++++ .../nodepool/v1beta2/nodepool_handler.go | 47 +++ .../nodepool/v1beta2/nodepool_validation.go | 194 +++++++++++ .../v1beta2/nodepool_validation_test.go | 329 ++++++++++++++++++ pkg/yurtmanager/webhook/server.go | 8 +- 15 files changed, 1703 insertions(+), 1 deletion(-) create mode 100644 pkg/apis/addtoscheme_apps_v1beta2.go create mode 100644 pkg/apis/apps/v1beta2/default.go create mode 100644 pkg/apis/apps/v1beta2/doc.go create mode 100644 pkg/apis/apps/v1beta2/groupversion_info.go create mode 100644 pkg/apis/apps/v1beta2/nodepool_conversion.go create mode 100644 pkg/apis/apps/v1beta2/nodepool_types.go create mode 100644 pkg/apis/apps/v1beta2/zz_generated.deepcopy.go create mode 100644 pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_default.go create mode 100644 pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_default_test.go create mode 100644 pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_handler.go create mode 100644 pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_validation.go create mode 100644 pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_validation_test.go diff --git a/charts/yurt-manager/crds/apps.openyurt.io_nodepools.yaml b/charts/yurt-manager/crds/apps.openyurt.io_nodepools.yaml index 9f108009bd8..d4ec39f4505 100644 --- a/charts/yurt-manager/crds/apps.openyurt.io_nodepools.yaml +++ b/charts/yurt-manager/crds/apps.openyurt.io_nodepools.yaml @@ -290,6 +290,195 @@ spec: storage: true subresources: status: {} + - additionalPrinterColumns: + - description: The type of nodepool + jsonPath: .spec.type + name: Type + type: string + - description: The number of ready nodes in the pool + jsonPath: .status.readyNodeNum + name: ReadyNodes + type: integer + - jsonPath: .status.unreadyNodeNum + name: NotReadyNodes + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: NodePool is the Schema for the nodepools API + 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: NodePoolSpec defines the desired state of NodePool + properties: + annotations: + additionalProperties: + type: string + description: |- + If specified, the Annotations will be added to all nodes. + NOTE: existing labels with samy keys on the nodes will be overwritten. + type: object + hostNetwork: + description: |- + HostNetwork is used to specify that cni components(like flannel) + will not be installed on the nodes of this NodePool. + This means all pods on the nodes of this NodePool will use + HostNetwork and share network namespace with host machine. + type: boolean + interConnectivity: + description: |- + InterConnectivity represents all nodes in the NodePool can access with each other + through Layer 2 or Layer 3 network or not. If the field is true, + nodepool-level list/watch requests reuse can be applied for this nodepool. + otherwise, only node-level list/watch requests reuse can be applied for the nodepool. + This field cannot be changed after creation. + type: boolean + labels: + additionalProperties: + type: string + description: |- + If specified, the Labels will be added to all nodes. + NOTE: existing labels with samy keys on the nodes will be overwritten. + type: object + leaderElectionStrategy: + description: |- + LeaderElectionStrategy represents the policy how to elect a leader Yurthub in a nodepool. + random: select one ready node as leader at random. + mark: select one ready node as leader from nodes that are specified by labelselector. + More strategies will be supported according to user's new requirements. + type: string + leaderNodeLabelSelector: + additionalProperties: + type: string + description: |- + LeaderNodeLabelSelector is used only when LeaderElectionStrategy is mark. leader Yurhub will be + elected from nodes that filtered by this label selector. + type: object + poolScopeMetadata: + description: |- + PoolScopeMetadata is used for specifying resources which will be shared in the nodepool. + And it is supported to modify dynamically. and the default value is v1.Service and discovery.Endpointslice. + items: + description: |- + GroupVersionKind unambiguously identifies a kind. It doesn't anonymously include GroupVersion + to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling + properties: + group: + type: string + kind: + type: string + version: + type: string + required: + - group + - kind + - version + type: object + type: array + taints: + description: If specified, the Taints will be added to all nodes. + items: + description: |- + The node this Taint is attached to has the "effect" on + any pod that does not tolerate the Taint. + properties: + effect: + description: |- + Required. The effect of the taint on pods + that do not tolerate the taint. + Valid effects are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: Required. The taint key to be applied to a node. + type: string + timeAdded: + description: |- + TimeAdded represents the time at which the taint was added. + It is only written for NoExecute taints. + format: date-time + type: string + value: + description: The taint value corresponding to the taint key. + type: string + required: + - effect + - key + type: object + type: array + type: + description: The type of the NodePool + type: string + type: object + status: + description: NodePoolStatus defines the observed state of NodePool + properties: + conditions: + description: |- + Conditions represents the latest available observations of a NodePool's + current state that includes LeaderHubElection status. + items: + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + format: date-time + type: string + message: + description: A human readable message indicating details about the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of NodePool condition. + type: string + type: object + type: array + leaderEndpoints: + description: LeaderEndpoints is used for storing the address of Leader Yurthub. + items: + type: string + type: array + nodes: + description: The list of nodes' names in the pool + items: + type: string + type: array + readyNodeNum: + description: Total number of ready nodes in the pool. + format: int32 + type: integer + unreadyNodeNum: + description: Total number of unready nodes in the pool. + format: int32 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} conversion: strategy: Webhook webhook: diff --git a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml index e0d5685314f..18dd2626901 100644 --- a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml +++ b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml @@ -1518,6 +1518,26 @@ webhooks: resources: - nodepools sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta2 + clientConfig: + service: + name: yurt-manager-webhook-service + namespace: {{ .Release.Namespace }} + path: /mutate-apps-openyurt-io-v1beta2-nodepool + failurePolicy: Fail + name: m.v1beta2.nodepool.kb.io + rules: + - apiGroups: + - apps.openyurt.io + apiVersions: + - v1beta2 + operations: + - CREATE + resources: + - nodepools + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 @@ -1710,6 +1730,28 @@ webhooks: resources: - nodepools sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta2 + clientConfig: + service: + name: yurt-manager-webhook-service + namespace: {{ .Release.Namespace }} + path: /validate-apps-openyurt-io-v1beta2-nodepool + failurePolicy: Fail + name: v.v1beta2.nodepool.kb.io + rules: + - apiGroups: + - apps.openyurt.io + apiVersions: + - v1beta2 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - nodepools + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 diff --git a/pkg/apis/addtoscheme_apps_v1beta2.go b/pkg/apis/addtoscheme_apps_v1beta2.go new file mode 100644 index 00000000000..facf9ad1a8a --- /dev/null +++ b/pkg/apis/addtoscheme_apps_v1beta2.go @@ -0,0 +1,26 @@ +/* +Copyright 2023 The OpenYurt 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 apis + +import ( + version "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta2" +) + +func init() { + // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back + AddToSchemes = append(AddToSchemes, version.SchemeBuilder.AddToScheme) +} diff --git a/pkg/apis/apps/v1beta2/default.go b/pkg/apis/apps/v1beta2/default.go new file mode 100644 index 00000000000..7d9b75cdc7b --- /dev/null +++ b/pkg/apis/apps/v1beta2/default.go @@ -0,0 +1,26 @@ +/* +Copyright 2024 The OpenYurt 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 v1beta2 + +// SetDefaultsNodePool set default values for NodePool. +func SetDefaultsNodePool(obj *NodePool) { + // example for set default value for NodePool + if obj.Annotations == nil { + obj.Annotations = make(map[string]string) + } + +} diff --git a/pkg/apis/apps/v1beta2/doc.go b/pkg/apis/apps/v1beta2/doc.go new file mode 100644 index 00000000000..82bb85a822a --- /dev/null +++ b/pkg/apis/apps/v1beta2/doc.go @@ -0,0 +1,17 @@ +/* +Copyright 2024 The OpenYurt 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 v1beta2 diff --git a/pkg/apis/apps/v1beta2/groupversion_info.go b/pkg/apis/apps/v1beta2/groupversion_info.go new file mode 100644 index 00000000000..5ace81d7609 --- /dev/null +++ b/pkg/apis/apps/v1beta2/groupversion_info.go @@ -0,0 +1,44 @@ +/* +Copyright 2024 The OpenYurt 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 v1beta2 + +// Package v1beta2 contains API Schema definitions for the apps v1beta2API group +// +kubebuilder:object:generate=true +// +groupName=apps.openyurt.io + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "apps.openyurt.io", Version: "v1beta2"} + + SchemeGroupVersion = GroupVersion + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +// Resource is required by pkg/client/listers/... +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/pkg/apis/apps/v1beta2/nodepool_conversion.go b/pkg/apis/apps/v1beta2/nodepool_conversion.go new file mode 100644 index 00000000000..34a152ebfc9 --- /dev/null +++ b/pkg/apis/apps/v1beta2/nodepool_conversion.go @@ -0,0 +1,26 @@ +/* +Copyright 2024 The OpenYurt 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 v1beta2 + +/* +Implementing the hub method is pretty easy -- we just have to add an empty +method called `Hub()` to serve as a +[marker](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Hub). +*/ + +// Hub marks this type as a conversion hub. +func (*NodePool) Hub() {} diff --git a/pkg/apis/apps/v1beta2/nodepool_types.go b/pkg/apis/apps/v1beta2/nodepool_types.go new file mode 100644 index 00000000000..b722104d2fe --- /dev/null +++ b/pkg/apis/apps/v1beta2/nodepool_types.go @@ -0,0 +1,164 @@ +/* +Copyright 2024 The OpenYurt 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 v1beta2 + +import ( + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type NodePoolType string + +// LeaderElectionStrategy represents the policy how to elect a leader Yurthub in a nodepool. +type LeaderElectionStrategy string + +const ( + Edge NodePoolType = "Edge" + Cloud NodePoolType = "Cloud" + + ElectionStrategyMark LeaderElectionStrategy = "mark" + ElectionStrategyRandom LeaderElectionStrategy = "random" + + // LeaderStatus means the status of leader yurthub election. + // If it's ready the leader elected, otherwise no leader is elected. + LeaderStatus NodePoolConditionType = "LeaderReady" +) + +// NodePoolSpec defines the desired state of NodePool +type NodePoolSpec struct { + // The type of the NodePool + // +optional + Type NodePoolType `json:"type,omitempty"` + + // HostNetwork is used to specify that cni components(like flannel) + // will not be installed on the nodes of this NodePool. + // This means all pods on the nodes of this NodePool will use + // HostNetwork and share network namespace with host machine. + HostNetwork bool `json:"hostNetwork,omitempty"` + + // If specified, the Labels will be added to all nodes. + // NOTE: existing labels with samy keys on the nodes will be overwritten. + // +optional + Labels map[string]string `json:"labels,omitempty"` + + // If specified, the Annotations will be added to all nodes. + // NOTE: existing labels with samy keys on the nodes will be overwritten. + // +optional + Annotations map[string]string `json:"annotations,omitempty"` + + // If specified, the Taints will be added to all nodes. + // +optional + Taints []v1.Taint `json:"taints,omitempty"` + + // InterConnectivity represents all nodes in the NodePool can access with each other + // through Layer 2 or Layer 3 network or not. If the field is true, + // nodepool-level list/watch requests reuse can be applied for this nodepool. + // otherwise, only node-level list/watch requests reuse can be applied for the nodepool. + // This field cannot be changed after creation. + InterConnectivity bool `json:"interConnectivity,omitempty"` + + // LeaderElectionStrategy represents the policy how to elect a leader Yurthub in a nodepool. + // random: select one ready node as leader at random. + // mark: select one ready node as leader from nodes that are specified by labelselector. + // More strategies will be supported according to user's new requirements. + LeaderElectionStrategy string `json:"leaderElectionStrategy,omitempty"` + + // LeaderNodeLabelSelector is used only when LeaderElectionStrategy is mark. leader Yurhub will be + // elected from nodes that filtered by this label selector. + LeaderNodeLabelSelector map[string]string `json:"leaderNodeLabelSelector,omitempty"` + + // PoolScopeMetadata is used for specifying resources which will be shared in the nodepool. + // And it is supported to modify dynamically. and the default value is v1.Service and discovery.Endpointslice. + PoolScopeMetadata []metav1.GroupVersionKind `json:"poolScopeMetadata,omitempty"` +} + +// NodePoolStatus defines the observed state of NodePool +type NodePoolStatus struct { + // Total number of ready nodes in the pool. + // +optional + ReadyNodeNum int32 `json:"readyNodeNum"` + + // Total number of unready nodes in the pool. + // +optional + UnreadyNodeNum int32 `json:"unreadyNodeNum"` + + // The list of nodes' names in the pool + // +optional + Nodes []string `json:"nodes,omitempty"` + + // LeaderEndpoints is used for storing the address of Leader Yurthub. + // +optional + LeaderEndpoints []string `json:"leaderEndpoints,omitempty"` + + // Conditions represents the latest available observations of a NodePool's + // current state that includes LeaderHubElection status. + // +optional + Conditions []NodePoolCondition `json:"conditions,omitempty"` +} + +// NodePoolConditionType represents a NodePool condition value. +type NodePoolConditionType string + +type NodePoolCondition struct { + // Type of NodePool condition. + Type NodePoolConditionType `json:"type,omitempty"` + + // Status of the condition, one of True, False, Unknown. + Status v1.ConditionStatus `json:"status,omitempty"` + + // Last time the condition transitioned from one status to another. + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` + + // The reason for the condition's last transition. + Reason string `json:"reason,omitempty"` + + // A human readable message indicating details about the transition. + Message string `json:"message,omitempty"` +} + +// +genclient +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Cluster,path=nodepools,shortName=np,categories=all +// +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".spec.type",description="The type of nodepool" +// +kubebuilder:printcolumn:name="ReadyNodes",type="integer",JSONPath=".status.readyNodeNum",description="The number of ready nodes in the pool" +// +kubebuilder:printcolumn:name="NotReadyNodes",type="integer",JSONPath=".status.unreadyNodeNum" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:subresource:status +// +genclient:nonNamespaced +// +kubebuilder:storageversion + +// NodePool is the Schema for the nodepools API +type NodePool struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec NodePoolSpec `json:"spec,omitempty"` + Status NodePoolStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// NodePoolList contains a list of NodePool +type NodePoolList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []NodePool `json:"items"` +} + +func init() { + SchemeBuilder.Register(&NodePool{}, &NodePoolList{}) +} diff --git a/pkg/apis/apps/v1beta2/zz_generated.deepcopy.go b/pkg/apis/apps/v1beta2/zz_generated.deepcopy.go new file mode 100644 index 00000000000..b4b76e08dc8 --- /dev/null +++ b/pkg/apis/apps/v1beta2/zz_generated.deepcopy.go @@ -0,0 +1,182 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2023 The OpenYurt 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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta2 + +import ( + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodePool) DeepCopyInto(out *NodePool) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePool. +func (in *NodePool) DeepCopy() *NodePool { + if in == nil { + return nil + } + out := new(NodePool) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NodePool) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodePoolCondition) DeepCopyInto(out *NodePoolCondition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePoolCondition. +func (in *NodePoolCondition) DeepCopy() *NodePoolCondition { + if in == nil { + return nil + } + out := new(NodePoolCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodePoolList) DeepCopyInto(out *NodePoolList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NodePool, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePoolList. +func (in *NodePoolList) DeepCopy() *NodePoolList { + if in == nil { + return nil + } + out := new(NodePoolList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NodePoolList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodePoolSpec) DeepCopyInto(out *NodePoolSpec) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Taints != nil { + in, out := &in.Taints, &out.Taints + *out = make([]v1.Taint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.LeaderNodeLabelSelector != nil { + in, out := &in.LeaderNodeLabelSelector, &out.LeaderNodeLabelSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.PoolScopeMetadata != nil { + in, out := &in.PoolScopeMetadata, &out.PoolScopeMetadata + *out = make([]metav1.GroupVersionKind, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePoolSpec. +func (in *NodePoolSpec) DeepCopy() *NodePoolSpec { + if in == nil { + return nil + } + out := new(NodePoolSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodePoolStatus) DeepCopyInto(out *NodePoolStatus) { + *out = *in + if in.Nodes != nil { + in, out := &in.Nodes, &out.Nodes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.LeaderEndpoints != nil { + in, out := &in.LeaderEndpoints, &out.LeaderEndpoints + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]NodePoolCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodePoolStatus. +func (in *NodePoolStatus) DeepCopy() *NodePoolStatus { + if in == nil { + return nil + } + out := new(NodePoolStatus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_default.go b/pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_default.go new file mode 100644 index 00000000000..2aae30174d1 --- /dev/null +++ b/pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_default.go @@ -0,0 +1,81 @@ +/* +Copyright 2023 The OpenYurt 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 v1beta2 + +import ( + "context" + "fmt" + "strings" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/openyurtio/openyurt/pkg/apis/apps" + "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta2" +) + +// Default satisfies the defaulting webhook interface. +func (webhook *NodePoolHandler) Default(ctx context.Context, obj runtime.Object) error { + np, ok := obj.(*v1beta2.NodePool) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a NodePool but got a %T", obj)) + } + + // specify default type as Edge + if len(np.Spec.Type) == 0 { + np.Spec.Type = v1beta2.Edge + } + + if np.Labels == nil { + np.Labels = map[string]string{ + apps.NodePoolTypeLabel: strings.ToLower(string(np.Spec.Type)), + } + } else { + np.Labels[apps.NodePoolTypeLabel] = strings.ToLower(string(np.Spec.Type)) + } + + // init node pool status + np.Status = v1beta2.NodePoolStatus{ + ReadyNodeNum: 0, + UnreadyNodeNum: 0, + Nodes: make([]string, 0), + } + + // Set default election strategy + if np.Spec.LeaderElectionStrategy == "" { + np.Spec.LeaderElectionStrategy = string(v1beta2.ElectionStrategyRandom) + } + + // Set default PoolScopeMetadata + if np.Spec.PoolScopeMetadata == nil { + np.Spec.PoolScopeMetadata = []v1.GroupVersionKind{ + { + Group: "core", + Version: "v1", + Kind: "Service", + }, + { + Group: "discovery.k8s.io", + Version: "v1", + Kind: "EndpointSlice", + }, + } + } + + return nil +} diff --git a/pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_default_test.go b/pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_default_test.go new file mode 100644 index 00000000000..41b271ad124 --- /dev/null +++ b/pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_default_test.go @@ -0,0 +1,329 @@ +/* +Copyright 2024 The OpenYurt 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 v1beta2 + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta2" +) + +func TestDefault(t *testing.T) { + testcases := map[string]struct { + obj runtime.Object + expectErr bool + wantedNodePool *v1beta2.NodePool + }{ + "it is not a nodepool": { + obj: &corev1.Pod{}, + expectErr: true, + }, + "nodepool has no type": { + obj: &v1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: v1beta2.NodePoolSpec{ + HostNetwork: true, + }, + }, + wantedNodePool: &v1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "nodepool.openyurt.io/type": "edge", + }, + }, + Spec: v1beta2.NodePoolSpec{ + HostNetwork: true, + Type: v1beta2.Edge, + LeaderElectionStrategy: string(v1beta2.ElectionStrategyRandom), + PoolScopeMetadata: []metav1.GroupVersionKind{ + { + Group: "core", + Version: "v1", + Kind: "Service", + }, + { + Group: "discovery.k8s.io", + Version: "v1", + Kind: "EndpointSlice", + }, + }, + }, + Status: v1beta2.NodePoolStatus{ + ReadyNodeNum: 0, + UnreadyNodeNum: 0, + Nodes: []string{}, + }, + }, + }, + "nodepool has pool type": { + obj: &v1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + }, + }, + Spec: v1beta2.NodePoolSpec{ + HostNetwork: true, + Type: v1beta2.Cloud, + }, + }, + wantedNodePool: &v1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + "nodepool.openyurt.io/type": "cloud", + }, + }, + Spec: v1beta2.NodePoolSpec{ + HostNetwork: true, + Type: v1beta2.Cloud, + LeaderElectionStrategy: string(v1beta2.ElectionStrategyRandom), + PoolScopeMetadata: []metav1.GroupVersionKind{ + { + Group: "core", + Version: "v1", + Kind: "Service", + }, + { + Group: "discovery.k8s.io", + Version: "v1", + Kind: "EndpointSlice", + }, + }, + }, + Status: v1beta2.NodePoolStatus{ + ReadyNodeNum: 0, + UnreadyNodeNum: 0, + Nodes: []string{}, + }, + }, + }, + "nodepool has no leader election strategy": { + obj: &v1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + }, + }, + Spec: v1beta2.NodePoolSpec{ + HostNetwork: true, + Type: v1beta2.Cloud, + LeaderElectionStrategy: "", + }, + }, + wantedNodePool: &v1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + "nodepool.openyurt.io/type": "cloud", + }, + }, + Spec: v1beta2.NodePoolSpec{ + HostNetwork: true, + Type: v1beta2.Cloud, + LeaderElectionStrategy: string(v1beta2.ElectionStrategyRandom), + PoolScopeMetadata: []metav1.GroupVersionKind{ + { + Group: "core", + Version: "v1", + Kind: "Service", + }, + { + Group: "discovery.k8s.io", + Version: "v1", + Kind: "EndpointSlice", + }, + }, + }, + Status: v1beta2.NodePoolStatus{ + ReadyNodeNum: 0, + UnreadyNodeNum: 0, + Nodes: []string{}, + }, + }, + }, + "nodepool has no mark election strategy": { + obj: &v1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + }, + }, + Spec: v1beta2.NodePoolSpec{ + HostNetwork: true, + Type: v1beta2.Cloud, + LeaderElectionStrategy: string(v1beta2.ElectionStrategyMark), + }, + }, + wantedNodePool: &v1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + "nodepool.openyurt.io/type": "cloud", + }, + }, + Spec: v1beta2.NodePoolSpec{ + HostNetwork: true, + Type: v1beta2.Cloud, + LeaderElectionStrategy: string(v1beta2.ElectionStrategyMark), + PoolScopeMetadata: []metav1.GroupVersionKind{ + { + Group: "core", + Version: "v1", + Kind: "Service", + }, + { + Group: "discovery.k8s.io", + Version: "v1", + Kind: "EndpointSlice", + }, + }, + }, + Status: v1beta2.NodePoolStatus{ + ReadyNodeNum: 0, + UnreadyNodeNum: 0, + Nodes: []string{}, + }, + }, + }, + "nodepool has no pool scope metadata": { + obj: &v1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + }, + }, + Spec: v1beta2.NodePoolSpec{ + HostNetwork: true, + Type: v1beta2.Cloud, + LeaderElectionStrategy: string(v1beta2.ElectionStrategyMark), + }, + }, + wantedNodePool: &v1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + "nodepool.openyurt.io/type": "cloud", + }, + }, + Spec: v1beta2.NodePoolSpec{ + HostNetwork: true, + Type: v1beta2.Cloud, + LeaderElectionStrategy: string(v1beta2.ElectionStrategyMark), + PoolScopeMetadata: []metav1.GroupVersionKind{ + { + Group: "core", + Version: "v1", + Kind: "Service", + }, + { + Group: "discovery.k8s.io", + Version: "v1", + Kind: "EndpointSlice", + }, + }, + }, + Status: v1beta2.NodePoolStatus{ + ReadyNodeNum: 0, + UnreadyNodeNum: 0, + Nodes: []string{}, + }, + }, + }, + "nodepool has pool scope metadata": { + obj: &v1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + }, + }, + Spec: v1beta2.NodePoolSpec{ + HostNetwork: true, + Type: v1beta2.Cloud, + LeaderElectionStrategy: string(v1beta2.ElectionStrategyMark), + PoolScopeMetadata: []metav1.GroupVersionKind{ + { + Group: "discovery.k8s.io", + Version: "v1", + Kind: "Endpoints", + }, + }, + }, + }, + wantedNodePool: &v1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + "nodepool.openyurt.io/type": "cloud", + }, + }, + Spec: v1beta2.NodePoolSpec{ + HostNetwork: true, + Type: v1beta2.Cloud, + LeaderElectionStrategy: string(v1beta2.ElectionStrategyMark), + PoolScopeMetadata: []metav1.GroupVersionKind{ + { + Group: "discovery.k8s.io", + Version: "v1", + Kind: "Endpoints", + }, + }, + }, + Status: v1beta2.NodePoolStatus{ + ReadyNodeNum: 0, + UnreadyNodeNum: 0, + Nodes: []string{}, + }, + }, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + h := NodePoolHandler{} + err := h.Default(context.TODO(), tc.obj) + if tc.expectErr { + require.Error(t, err, "expected no error") + return + } + require.NoError(t, err, "expected error") + + currentNp := tc.obj.(*v1beta2.NodePool) + assert.Equal(t, tc.wantedNodePool, currentNp) + }) + } +} diff --git a/pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_handler.go b/pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_handler.go new file mode 100644 index 00000000000..f05eb6de22b --- /dev/null +++ b/pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_handler.go @@ -0,0 +1,47 @@ +/* +Copyright 2023 The OpenYurt 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 v1beta2 + +import ( + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + yurtClient "github.com/openyurtio/openyurt/cmd/yurt-manager/app/client" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" + "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta2" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" +) + +// SetupWebhookWithManager sets up Cluster webhooks. mutate path, validatepath, error +func (webhook *NodePoolHandler) SetupWebhookWithManager(mgr ctrl.Manager) (string, string, error) { + // init + webhook.Client = yurtClient.GetClientByControllerNameOrDie(mgr, names.NodePoolController) + + return util.RegisterWebhook(mgr, &v1beta2.NodePool{}, webhook) +} + +// +kubebuilder:webhook:path=/validate-apps-openyurt-io-v1beta2-nodepool,mutating=false,failurePolicy=fail,groups=apps.openyurt.io,resources=nodepools,verbs=create;update;delete,versions=v1beta2,name=v.v1beta2.nodepool.kb.io,sideEffects=None,admissionReviewVersions=v1;v1beta2 +// +kubebuilder:webhook:path=/mutate-apps-openyurt-io-v1beta2-nodepool,mutating=true,failurePolicy=fail,groups=apps.openyurt.io,resources=nodepools,verbs=create,versions=v1beta2,name=m.v1beta2.nodepool.kb.io,sideEffects=None,admissionReviewVersions=v1;v1beta2 + +// NodePoolHandler implements a validating and defaulting webhook for Cluster. +type NodePoolHandler struct { + Client client.Client +} + +var _ webhook.CustomDefaulter = &NodePoolHandler{} +var _ webhook.CustomValidator = &NodePoolHandler{} diff --git a/pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_validation.go b/pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_validation.go new file mode 100644 index 00000000000..feeaa4e16cb --- /dev/null +++ b/pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_validation.go @@ -0,0 +1,194 @@ +/* +Copyright 2023 The OpenYurt 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 v1beta2 + +import ( + "context" + "errors" + "fmt" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apivalidation "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + appsv1beta2 "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta2" + "github.com/openyurtio/openyurt/pkg/projectinfo" +) + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type. +func (webhook *NodePoolHandler) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + np, ok := obj.(*appsv1beta2.NodePool) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a NodePool but got a %T", obj)) + } + + if allErrs := validateNodePoolSpec(&np.Spec); len(allErrs) > 0 { + return nil, apierrors.NewInvalid(appsv1beta2.GroupVersion.WithKind("NodePool").GroupKind(), np.Name, allErrs) + } + + return nil, nil +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type. +func (webhook *NodePoolHandler) ValidateUpdate( + ctx context.Context, + oldObj, newObj runtime.Object, +) (admission.Warnings, error) { + newNp, ok := newObj.(*appsv1beta2.NodePool) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a NodePool but got a %T", newObj)) + } + oldNp, ok := oldObj.(*appsv1beta2.NodePool) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a NodePool but got a %T", oldObj)) + } + + if allErrs := validateNodePoolSpecUpdate(&newNp.Spec, &oldNp.Spec); len(allErrs) > 0 { + return nil, apierrors.NewForbidden( + appsv1beta2.GroupVersion.WithResource("nodepools").GroupResource(), + newNp.Name, + allErrs[0], + ) + } + + return nil, nil +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type. +func (webhook *NodePoolHandler) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + np, ok := obj.(*appsv1beta2.NodePool) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a NodePool but got a %T", obj)) + } + if allErrs := validateNodePoolDeletion(webhook.Client, np); len(allErrs) > 0 { + return nil, apierrors.NewForbidden( + appsv1beta2.GroupVersion.WithResource("nodepools").GroupResource(), + np.Name, + allErrs[0], + ) + } + + return nil, nil +} + +// annotationValidator validates the NodePool.Spec.Annotations +var annotationValidator = func(annos map[string]string) error { + errs := apivalidation.ValidateAnnotations(annos, field.NewPath("field")) + if len(errs) > 0 { + return errors.New(errs.ToAggregate().Error()) + } + return nil +} + +func validateNodePoolSpecAnnotations(annotations map[string]string) field.ErrorList { + if err := annotationValidator(annotations); err != nil { + return field.ErrorList([]*field.Error{ + field.Invalid(field.NewPath("spec").Child("annotations"), + annotations, "invalid annotations")}) + } + return nil +} + +// validateNodePoolSpec validates the nodepool spec. +func validateNodePoolSpec(spec *appsv1beta2.NodePoolSpec) field.ErrorList { + if allErrs := validateNodePoolSpecAnnotations(spec.Annotations); allErrs != nil { + return allErrs + } + + // NodePool type should be Edge or Cloud + if spec.Type != appsv1beta2.Edge && spec.Type != appsv1beta2.Cloud { + return []*field.Error{ + field.Invalid(field.NewPath("spec").Child("type"), spec.Type, "pool type should be Edge or Cloud"), + } + } + + // Cloud NodePool can not set HostNetwork=true + if spec.Type == appsv1beta2.Cloud && spec.HostNetwork { + return []*field.Error{ + field.Invalid( + field.NewPath("spec").Child("hostNetwork"), + spec.HostNetwork, + "Cloud NodePool cloud not support hostNetwork", + ), + } + } + + // Check leader election strategy has been set to Random or Mark + switch spec.LeaderElectionStrategy { + case string(appsv1beta2.ElectionStrategyRandom), string(appsv1beta2.ElectionStrategyMark): + return nil + default: + return []*field.Error{ + field.Invalid( + field.NewPath("spec").Child("leaderElectionStrategy"), + spec.LeaderElectionStrategy, + "leaderElectionStrategy should be Random or Mark", + ), + } + } +} + +// validateNodePoolSpecUpdate tests if required fields in the NodePool spec are set. +func validateNodePoolSpecUpdate(spec, oldSpec *appsv1beta2.NodePoolSpec) field.ErrorList { + if allErrs := validateNodePoolSpec(spec); allErrs != nil { + return allErrs + } + + if spec.Type != oldSpec.Type { + return field.ErrorList([]*field.Error{ + field.Forbidden(field.NewPath("spec").Child("type"), "pool type can't be changed")}) + } + + if spec.HostNetwork != oldSpec.HostNetwork { + return field.ErrorList([]*field.Error{ + field.Forbidden(field.NewPath("spec").Child("hostNetwork"), "pool hostNetwork can't be changed"), + }) + } + + if spec.InterConnectivity != oldSpec.InterConnectivity { + return field.ErrorList([]*field.Error{ + field.Forbidden( + field.NewPath("spec").Child("interConnectivity"), + "pool interConnectivity can't be changed", + ), + }) + } + + return nil +} + +// validateNodePoolDeletion validate the nodepool deletion event, which prevents +// the default-nodepool from being deleted +func validateNodePoolDeletion(cli client.Client, np *appsv1beta2.NodePool) field.ErrorList { + nodes := corev1.NodeList{} + + if err := cli.List(context.TODO(), &nodes, client.MatchingLabels(map[string]string{projectinfo.GetNodePoolLabel(): np.Name})); err != nil { + return field.ErrorList([]*field.Error{ + field.Forbidden(field.NewPath("metadata").Child("name"), + "could not get nodes associated to the pool")}) + } + if len(nodes.Items) != 0 { + return field.ErrorList([]*field.Error{ + field.Forbidden(field.NewPath("metadata").Child("name"), + "cannot remove nonempty pool, please drain the pool before deleting")}) + } + return nil +} diff --git a/pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_validation_test.go b/pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_validation_test.go new file mode 100644 index 00000000000..0be9d6e12b0 --- /dev/null +++ b/pkg/yurtmanager/webhook/nodepool/v1beta2/nodepool_validation_test.go @@ -0,0 +1,329 @@ +/* +Copyright 2023 The OpenYurt 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 v1beta2 + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/openyurtio/openyurt/pkg/apis" + appsv1beta2 "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta2" + "github.com/openyurtio/openyurt/pkg/projectinfo" +) + +func TestValidateCreate(t *testing.T) { + testcases := map[string]struct { + pool runtime.Object + errcode int + }{ + "it is a normal nodepool": { + pool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Edge, + LeaderElectionStrategy: string(appsv1beta2.ElectionStrategyRandom), + }, + }, + errcode: 0, + }, + "it is not a nodepool": { + pool: &corev1.Node{}, + errcode: http.StatusBadRequest, + }, + "invalid annotation": { + pool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Edge, + Annotations: map[string]string{ + "-&#foo": "invalid annotation", + }, + }, + }, + errcode: http.StatusUnprocessableEntity, + }, + "invalid pool type": { + pool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: "invalid type", + }, + }, + errcode: http.StatusUnprocessableEntity, + }, + "invalid leader election strategy": { + pool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Edge, + LeaderElectionStrategy: "invalid strategy", + }, + }, + errcode: http.StatusUnprocessableEntity, + }, + } + + handler := &NodePoolHandler{} + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + _, err := handler.ValidateCreate(context.TODO(), tc.pool) + if tc.errcode == 0 { + require.NoError(t, err, "Expected error code %d, got %v", tc.errcode, err) + return + } + require.Error(t, err, "Expected error code %d, got %v", tc.errcode, err) + + statusErr := err.(*errors.StatusError) + assert.Equal(t, tc.errcode, int(statusErr.Status().Code), "Expected error code %d, got %v", tc.errcode, err) + }) + } +} + +func TestValidateUpdate(t *testing.T) { + testcases := map[string]struct { + oldPool runtime.Object + newPool runtime.Object + errcode int + }{ + "update a normal nodepool": { + oldPool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Edge, + LeaderElectionStrategy: string(appsv1beta2.ElectionStrategyRandom), + }, + }, + newPool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Edge, + Labels: map[string]string{ + "foo": "bar", + }, + LeaderElectionStrategy: string(appsv1beta2.ElectionStrategyMark), + }, + }, + errcode: 0, + }, + "oldPool is not a nodepool": { + oldPool: &corev1.Node{}, + newPool: &appsv1beta2.NodePool{}, + errcode: http.StatusBadRequest, + }, + "newPool is not a nodepool": { + oldPool: &appsv1beta2.NodePool{}, + newPool: &corev1.Node{}, + errcode: http.StatusBadRequest, + }, + "invalid pool type": { + oldPool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Edge, + }, + }, + newPool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: "invalid type", + }, + }, + errcode: http.StatusForbidden, + }, + "type is changed": { + oldPool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Edge, + }, + }, + newPool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Cloud, + }, + }, + errcode: http.StatusForbidden, + }, + "host network is changed": { + oldPool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Edge, + HostNetwork: false, + }, + }, + newPool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Edge, + HostNetwork: true, + }, + }, + errcode: http.StatusForbidden, + }, + "interConnectivity is changed": { + oldPool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Edge, + HostNetwork: false, + }, + }, + newPool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Edge, + HostNetwork: true, + }, + }, + errcode: http.StatusForbidden, + }, + "leaderElectionStrategy is changed": { + oldPool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Edge, + HostNetwork: false, + LeaderElectionStrategy: "mark", + }, + }, + newPool: &appsv1beta2.NodePool{ + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Edge, + HostNetwork: false, + LeaderElectionStrategy: "random", + }, + }, + errcode: 0, + }, + } + + handler := &NodePoolHandler{} + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + _, err := handler.ValidateUpdate(context.TODO(), tc.oldPool, tc.newPool) + if tc.errcode == 0 { + require.NoError(t, err, "Expected error code %d, got %v", tc.errcode, err) + return + } + require.Error(t, err) + statusErr := err.(*errors.StatusError) + assert.Equal(t, tc.errcode, int(statusErr.Status().Code), "Expected error code %d, got %v", tc.errcode, err) + }) + } +} + +func prepareNodes() []client.Object { + nodes := []client.Object{ + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Labels: map[string]string{ + projectinfo.GetNodePoolLabel(): "hangzhou", + }, + }, + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + } + return nodes +} + +func prepareNodePools() []client.Object { + pools := []client.Object{ + &appsv1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hangzhou", + }, + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Edge, + Labels: map[string]string{ + "region": "hangzhou", + }, + }, + }, + &appsv1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "beijing", + }, + Spec: appsv1beta2.NodePoolSpec{ + Type: appsv1beta2.Edge, + Labels: map[string]string{ + "region": "beijing", + }, + }, + }, + } + return pools +} + +func TestValidateDelete(t *testing.T) { + nodes := prepareNodes() + pools := prepareNodePools() + scheme := runtime.NewScheme() + if err := clientgoscheme.AddToScheme(scheme); err != nil { + t.Fatal("Fail to add kubernetes clint-go custom resource") + } + apis.AddToScheme(scheme) + + c := fakeclient.NewClientBuilder().WithScheme(scheme).WithObjects(pools...).WithObjects(nodes...).Build() + + testcases := map[string]struct { + pool runtime.Object + errcode int + }{ + "delete a empty nodepool": { + pool: &appsv1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "beijing", + }, + }, + errcode: 0, + }, + "delete a nodepool with node in it": { + pool: &appsv1beta2.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hangzhou", + }, + }, + errcode: http.StatusForbidden, + }, + "it is not a nodepool": { + pool: &corev1.Node{}, + errcode: http.StatusBadRequest, + }, + } + + handler := &NodePoolHandler{ + Client: c, + } + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + _, err := handler.ValidateDelete(context.TODO(), tc.pool) + if tc.errcode == 0 { + require.NoError(t, err, "Expected error code %d, got %v", tc.errcode, err) + return + } + require.Error(t, err) + statusErr := err.(*errors.StatusError) + assert.Equal(t, tc.errcode, int(statusErr.Status().Code), "Expected error code %d, got %v", tc.errcode, err) + }) + } +} diff --git a/pkg/yurtmanager/webhook/server.go b/pkg/yurtmanager/webhook/server.go index 80d24479e75..1e85db68168 100644 --- a/pkg/yurtmanager/webhook/server.go +++ b/pkg/yurtmanager/webhook/server.go @@ -36,6 +36,7 @@ import ( v1beta1gateway "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/gateway/v1beta1" v1node "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/node/v1" v1beta1nodepool "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/nodepool/v1beta1" + v1beta2nodepool "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/nodepool/v1beta2" v1beta1platformadmin "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/platformadmin/v1beta1" v1alpha1pod "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/pod/v1alpha1" "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" @@ -74,6 +75,7 @@ func addControllerWebhook(name string, handler SetupWebhookWithManager) { func init() { addControllerWebhook(names.GatewayPickupController, &v1beta1gateway.GatewayHandler{}) addControllerWebhook(names.NodePoolController, &v1beta1nodepool.NodePoolHandler{}) + addControllerWebhook(names.NodePoolController, &v1beta2nodepool.NodePoolHandler{}) addControllerWebhook(names.YurtStaticSetController, &v1alpha1yurtstaticset.YurtStaticSetHandler{}) addControllerWebhook(names.YurtAppSetController, &v1beta1yurtappset.YurtAppSetHandler{}) addControllerWebhook(names.YurtAppDaemonController, &v1alpha1yurtappdaemon.YurtAppDaemonHandler{}) @@ -133,7 +135,11 @@ func SetupWithManager(c *config.CompletedConfig, mgr manager.Manager) error { // set up controller webhooks for controllerName, list := range controllerWebhooks { - if !app.IsControllerEnabled(controllerName, controller.ControllersDisabledByDefault, c.ComponentConfig.Generic.Controllers) { + if !app.IsControllerEnabled( + controllerName, + controller.ControllersDisabledByDefault, + c.ComponentConfig.Generic.Controllers, + ) { klog.Warningf("Webhook for %v is disabled", controllerName) continue }