diff --git a/kwok/main.go b/kwok/main.go index 4943b17da8..761f9d0563 100644 --- a/kwok/main.go +++ b/kwok/main.go @@ -38,5 +38,5 @@ func main() { op.GetClient(), op.EventRecorder, cloudProvider, - )...).Start(ctx) + )...).Start(ctx, cloudProvider) } diff --git a/pkg/apis/v1/nodeclaim_conversion.go b/pkg/apis/v1/nodeclaim_conversion.go new file mode 100644 index 0000000000..fd784cd6e5 --- /dev/null +++ b/pkg/apis/v1/nodeclaim_conversion.go @@ -0,0 +1,123 @@ +/* +Copyright The Kubernetes 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 v1 + +import ( + "context" + + "github.com/samber/lo" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "knative.dev/pkg/apis" + + "sigs.k8s.io/karpenter/pkg/apis/v1beta1" + "sigs.k8s.io/karpenter/pkg/operator/injection" +) + +func (in *NodeClaim) ConvertTo(ctx context.Context, to apis.Convertible) error { + v1beta1NC := to.(*v1beta1.NodeClaim) + v1beta1NC.ObjectMeta = in.ObjectMeta + + in.Spec.convertTo(ctx, &v1beta1NC.Spec) + in.Status.convertTo((&v1beta1NC.Status)) + return nil +} + +func (in *NodeClaimSpec) convertTo(ctx context.Context, v1beta1nc *v1beta1.NodeClaimSpec) { + v1beta1nc.Taints = in.Taints + v1beta1nc.StartupTaints = in.StartupTaints + v1beta1nc.Resources = v1beta1.ResourceRequirements(in.Resources) + v1beta1nc.Requirements = lo.Map(in.Requirements, func(v1Requirements NodeSelectorRequirementWithMinValues, _ int) v1beta1.NodeSelectorRequirementWithMinValues { + return v1beta1.NodeSelectorRequirementWithMinValues{ + NodeSelectorRequirement: v1.NodeSelectorRequirement{ + Key: v1Requirements.Key, + Operator: v1Requirements.Operator, + Values: v1Requirements.Values, + }, + MinValues: v1Requirements.MinValues, + } + }) + + if in.NodeClassRef != nil { + nodeclass, found := lo.Find(injection.GetNodeClasses(ctx), func(nc schema.GroupVersionKind) bool { + return nc.Kind == in.NodeClassRef.Kind && nc.Group == in.NodeClassRef.Group + }) + v1beta1nc.NodeClassRef = &v1beta1.NodeClassReference{ + Kind: in.NodeClassRef.Kind, + Name: in.NodeClassRef.Name, + APIVersion: lo.Ternary(found, nodeclass.GroupVersion().String(), ""), + } + } + + // Need to implement Kubelet Conversion +} + +func (in *NodeClaimStatus) convertTo(v1beta1nc *v1beta1.NodeClaimStatus) { + v1beta1nc.NodeName = in.NodeName + v1beta1nc.ProviderID = in.ProviderID + v1beta1nc.ImageID = in.ImageID + v1beta1nc.Capacity = in.Capacity + v1beta1nc.Allocatable = in.Allocatable + v1beta1nc.Conditions = in.Conditions +} + +func (in *NodeClaim) ConvertFrom(ctx context.Context, from apis.Convertible) error { + v1beta1NC := from.(*v1beta1.NodeClaim) + in.ObjectMeta = v1beta1NC.ObjectMeta + + in.Status.convertFrom((&v1beta1NC.Status)) + return in.Spec.convertFrom(ctx, &v1beta1NC.Spec) +} + +func (in *NodeClaimSpec) convertFrom(ctx context.Context, v1beta1nc *v1beta1.NodeClaimSpec) error { + in.Taints = v1beta1nc.Taints + in.StartupTaints = v1beta1nc.StartupTaints + in.Resources = ResourceRequirements(v1beta1nc.Resources) + in.Requirements = lo.Map(v1beta1nc.Requirements, func(v1beta1Requirements v1beta1.NodeSelectorRequirementWithMinValues, _ int) NodeSelectorRequirementWithMinValues { + return NodeSelectorRequirementWithMinValues{ + NodeSelectorRequirement: v1.NodeSelectorRequirement{ + Key: v1beta1Requirements.Key, + Operator: v1beta1Requirements.Operator, + Values: v1beta1Requirements.Values, + }, + MinValues: v1beta1Requirements.MinValues, + } + }) + + defaultNodeClassGVK := injection.GetNodeClasses(ctx)[0] + nodeclassGroupVersion, err := schema.ParseGroupVersion(v1beta1nc.NodeClassRef.APIVersion) + if err != nil { + return err + } + in.NodeClassRef = &NodeClassReference{ + Name: v1beta1nc.NodeClassRef.Name, + Kind: lo.Ternary(v1beta1nc.NodeClassRef.Kind == "", defaultNodeClassGVK.Kind, v1beta1nc.NodeClassRef.Kind), + Group: lo.Ternary(v1beta1nc.NodeClassRef.APIVersion == "", defaultNodeClassGVK.Group, nodeclassGroupVersion.Group), + } + + // Need to implement Kubelet Conversion + return nil +} + +func (in *NodeClaimStatus) convertFrom(v1beta1nc *v1beta1.NodeClaimStatus) { + in.NodeName = v1beta1nc.NodeName + in.ProviderID = v1beta1nc.ProviderID + in.ImageID = v1beta1nc.ImageID + in.Capacity = v1beta1nc.Capacity + in.Allocatable = v1beta1nc.Allocatable + in.Conditions = v1beta1nc.Conditions +} diff --git a/pkg/apis/v1/nodeclaim_conversion_test.go b/pkg/apis/v1/nodeclaim_conversion_test.go new file mode 100644 index 0000000000..5b754018de --- /dev/null +++ b/pkg/apis/v1/nodeclaim_conversion_test.go @@ -0,0 +1,515 @@ +/* +Copyright The Kubernetes 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 v1_test + +import ( + "github.com/awslabs/operatorpkg/status" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/samber/lo" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + . "sigs.k8s.io/karpenter/pkg/apis/v1" + "sigs.k8s.io/karpenter/pkg/apis/v1beta1" + "sigs.k8s.io/karpenter/pkg/operator/injection" + "sigs.k8s.io/karpenter/pkg/test" +) + +var _ = Describe("Convert v1 to v1beta1 NodeClaim API", func() { + var ( + v1nodepool *NodePool + v1beta1nodepool *v1beta1.NodePool + v1nodeclaim *NodeClaim + v1beta1nodeclaim *v1beta1.NodeClaim + ) + + BeforeEach(func() { + v1beta1nodepool = test.NodePool() + v1nodepool = &NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-nodepool", + }, + Spec: NodePoolSpec{ + Template: NodeClaimTemplate{ + Spec: NodeClaimSpec{ + NodeClassRef: &NodeClassReference{ + Name: "test", + Kind: "test-kind", + Group: "test-group", + }, + Requirements: []NodeSelectorRequirementWithMinValues{}, + }, + }, + }, + } + v1nodeclaim = &NodeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + NodePoolLabelKey: v1nodepool.Name, + }, + }, + Spec: NodeClaimSpec{ + NodeClassRef: &NodeClassReference{ + Name: "test", + Kind: "test-kind", + Group: "test-group", + }, + }, + } + v1beta1nodeclaim = &v1beta1.NodeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + NodePoolLabelKey: v1beta1nodepool.Name, + }, + }, + Spec: v1beta1.NodeClaimSpec{ + NodeClassRef: &v1beta1.NodeClassReference{ + Name: "test", + Kind: "test-kind", + APIVersion: "test-group/test-version", + }, + }, + } + Expect(env.Client.Create(ctx, v1nodepool)).To(Succeed()) + cloudProvider.NodeClassGroupVersionKind = []schema.GroupVersionKind{ + { + Group: "fake-cloudprovider-group", + Version: "fake-cloudprovider-version", + Kind: "fake-cloudprovider-kind", + }, + } + ctx = injection.WithNodeClasses(ctx, cloudProvider.GetSupportedNodeClasses()) + }) + + It("should convert v1 nodeclaim metadata", func() { + v1nodeclaim.ObjectMeta = test.ObjectMeta() + Expect(v1nodeclaim.ConvertFrom(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1beta1nodeclaim.ObjectMeta).To(BeEquivalentTo(v1nodeclaim.ObjectMeta)) + }) + Context("NodeClaim Spec", func() { + It("should convert v1 nodeclaim taints", func() { + v1nodeclaim.Spec.Taints = []v1.Taint{ + { + Key: "test-key-1", + Value: "test-value-1", + Effect: v1.TaintEffectNoExecute, + }, + { + Key: "test-key-2", + Value: "test-value-2", + Effect: v1.TaintEffectNoSchedule, + }, + } + Expect(v1nodeclaim.ConvertTo(ctx, v1beta1nodeclaim)).To(Succeed()) + for i := range v1nodeclaim.Spec.Taints { + Expect(v1beta1nodeclaim.Spec.Taints[i].Key).To(Equal(v1nodeclaim.Spec.Taints[i].Key)) + Expect(v1beta1nodeclaim.Spec.Taints[i].Value).To(Equal(v1nodeclaim.Spec.Taints[i].Value)) + Expect(v1beta1nodeclaim.Spec.Taints[i].Effect).To(Equal(v1nodeclaim.Spec.Taints[i].Effect)) + } + }) + It("should convert v1 nodeclaim startup taints", func() { + v1nodeclaim.Spec.StartupTaints = []v1.Taint{ + { + Key: "test-key-startup-1", + Value: "test-value-startup-1", + Effect: v1.TaintEffectNoExecute, + }, + { + Key: "test-key-startup-2", + Value: "test-value-startup-2", + Effect: v1.TaintEffectNoSchedule, + }, + } + Expect(v1nodeclaim.ConvertTo(ctx, v1beta1nodeclaim)).To(Succeed()) + for i := range v1nodeclaim.Spec.StartupTaints { + Expect(v1beta1nodeclaim.Spec.StartupTaints[i].Key).To(Equal(v1nodeclaim.Spec.StartupTaints[i].Key)) + Expect(v1beta1nodeclaim.Spec.StartupTaints[i].Value).To(Equal(v1nodeclaim.Spec.StartupTaints[i].Value)) + Expect(v1beta1nodeclaim.Spec.StartupTaints[i].Effect).To(Equal(v1nodeclaim.Spec.StartupTaints[i].Effect)) + } + }) + It("should convert v1 nodeclaim requirements", func() { + v1nodeclaim.Spec.Requirements = []NodeSelectorRequirementWithMinValues{ + { + NodeSelectorRequirement: v1.NodeSelectorRequirement{ + Key: v1.LabelArchStable, + Operator: v1.NodeSelectorOpExists, + }, + MinValues: lo.ToPtr(451613), + }, + { + NodeSelectorRequirement: v1.NodeSelectorRequirement{ + Key: CapacityTypeLabelKey, + Operator: v1.NodeSelectorOpIn, + Values: []string{CapacityTypeSpot}, + }, + MinValues: lo.ToPtr(9787513), + }, + } + Expect(v1nodeclaim.ConvertTo(ctx, v1beta1nodeclaim)).To(Succeed()) + for i := range v1nodeclaim.Spec.Requirements { + Expect(v1beta1nodeclaim.Spec.Requirements[i].Key).To(Equal(v1nodeclaim.Spec.Requirements[i].Key)) + Expect(v1beta1nodeclaim.Spec.Requirements[i].Operator).To(Equal(v1nodeclaim.Spec.Requirements[i].Operator)) + Expect(v1beta1nodeclaim.Spec.Requirements[i].Values).To(Equal(v1nodeclaim.Spec.Requirements[i].Values)) + Expect(v1beta1nodeclaim.Spec.Requirements[i].MinValues).To(Equal(v1nodeclaim.Spec.Requirements[i].MinValues)) + } + }) + It("should convert v1 nodeclaim resources", func() { + v1nodeclaim.Spec.Resources = ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("1"), + v1.ResourceMemory: resource.MustParse("134G"), + }, + } + Expect(v1nodeclaim.ConvertTo(ctx, v1beta1nodeclaim)).To(Succeed()) + for key := range v1nodeclaim.Spec.Resources.Requests { + Expect(v1nodeclaim.Spec.Resources.Requests[key]).To(Equal(v1beta1nodeclaim.Spec.Resources.Requests[key])) + } + }) + Context("NodeClassRef", func() { + It("should convert v1 nodeclaim template nodeClassRef", func() { + v1nodeclaim.Spec.NodeClassRef = &NodeClassReference{ + Kind: "fake-cloudprovider-kind", + Name: "nodeclass-test", + Group: "fake-cloudprovider-group", + } + Expect(v1nodeclaim.ConvertTo(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1beta1nodeclaim.Spec.NodeClassRef.Kind).To(Equal(v1nodeclaim.Spec.NodeClassRef.Kind)) + Expect(v1beta1nodeclaim.Spec.NodeClassRef.Name).To(Equal(v1nodeclaim.Spec.NodeClassRef.Name)) + Expect(v1beta1nodeclaim.Spec.NodeClassRef.APIVersion).To(Equal(cloudProvider.NodeClassGroupVersionKind[0].GroupVersion().String())) + }) + It("should not include APIVersion for v1beta1 if Group and Kind is not in the supported nodeclass", func() { + v1nodeclaim.Spec.NodeClassRef = &NodeClassReference{ + Kind: "test-kind", + Name: "nodeclass-test", + Group: "testgroup.sh", + } + Expect(v1nodeclaim.ConvertTo(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1beta1nodeclaim.Spec.NodeClassRef.Kind).To(Equal(v1nodeclaim.Spec.NodeClassRef.Kind)) + Expect(v1beta1nodeclaim.Spec.NodeClassRef.Name).To(Equal(v1nodeclaim.Spec.NodeClassRef.Name)) + Expect(v1beta1nodeclaim.Spec.NodeClassRef.APIVersion).To(Equal("")) + }) + }) + }) + Context("NodeClaim Status", func() { + It("should convert v1 nodeclaim nodename", func() { + v1nodeclaim.Status.NodeName = "test-node-name" + Expect(v1nodeclaim.ConvertTo(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1nodeclaim.Status.NodeName).To(Equal(v1beta1nodeclaim.Status.NodeName)) + }) + It("should convert v1 nodeclaim provider id", func() { + v1nodeclaim.Status.ProviderID = "test-provider-id" + Expect(v1nodeclaim.ConvertTo(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1nodeclaim.Status.ProviderID).To(Equal(v1beta1nodeclaim.Status.ProviderID)) + }) + It("should convert v1 nodeclaim image id", func() { + v1nodeclaim.Status.ImageID = "test-image-id" + Expect(v1nodeclaim.ConvertTo(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1nodeclaim.Status.ImageID).To(Equal(v1beta1nodeclaim.Status.ImageID)) + }) + It("should convert v1 nodeclaim capacity", func() { + v1nodeclaim.Status.Capacity = v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("13432"), + v1.ResourceMemory: resource.MustParse("1332G"), + } + Expect(v1nodeclaim.ConvertTo(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1nodeclaim.Status.Capacity).To(Equal(v1beta1nodeclaim.Status.Capacity)) + }) + It("should convert v1 nodeclaim allocatable", func() { + v1nodeclaim.Status.Allocatable = v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("13432"), + v1.ResourceMemory: resource.MustParse("1332G"), + } + Expect(v1nodeclaim.ConvertTo(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1nodeclaim.Status.Allocatable).To(Equal(v1beta1nodeclaim.Status.Allocatable)) + }) + It("should convert v1 nodeclaim conditions", func() { + v1nodeclaim.Status.Conditions = []status.Condition{ + { + Status: status.ConditionReady, + Reason: "test-reason", + }, + { + Status: ConditionTypeDrifted, + Reason: "test-reason", + }, + } + Expect(v1nodeclaim.ConvertTo(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1nodeclaim.Status.Conditions).To(Equal(v1beta1nodeclaim.Status.Conditions)) + }) + }) +}) + +var _ = Describe("Convert V1beta1 to V1 NodeClaim API", func() { + var ( + v1nodepool *NodePool + v1beta1nodepool *v1beta1.NodePool + v1nodeclaim *NodeClaim + v1beta1nodeclaim *v1beta1.NodeClaim + ) + + BeforeEach(func() { + v1nodepool = &NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-nodepool", + }, + Spec: NodePoolSpec{ + Template: NodeClaimTemplate{ + Spec: NodeClaimSpec{ + NodeClassRef: &NodeClassReference{ + Name: "test", + Kind: "test-kind", + Group: "test-group", + }, + Requirements: []NodeSelectorRequirementWithMinValues{}, + }, + }, + }, + } + v1beta1nodepool = test.NodePool() + v1nodeclaim = &NodeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + NodePoolLabelKey: v1nodepool.Name, + }, + }, + Spec: NodeClaimSpec{ + NodeClassRef: &NodeClassReference{ + Name: "test", + Kind: "test-kind", + Group: "test-group", + }, + }, + } + v1beta1nodeclaim = &v1beta1.NodeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + NodePoolLabelKey: v1beta1nodepool.Name, + }, + }, + Spec: v1beta1.NodeClaimSpec{ + NodeClassRef: &v1beta1.NodeClassReference{ + Name: "test", + Kind: "test-kind", + APIVersion: "test-group/test-version", + }, + }, + } + Expect(env.Client.Create(ctx, v1beta1nodepool)).To(Succeed()) + cloudProvider.NodeClassGroupVersionKind = []schema.GroupVersionKind{ + { + Group: "fake-cloudprovider-group", + Version: "fake-cloudprovider-version", + Kind: "fake-cloudprovider-kind", + }, + } + ctx = injection.WithNodeClasses(ctx, cloudProvider.GetSupportedNodeClasses()) + }) + + It("should convert v1beta1 nodeclaim metadata", func() { + v1beta1nodeclaim.ObjectMeta = test.ObjectMeta() + Expect(v1nodeclaim.ConvertFrom(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1nodeclaim.ObjectMeta).To(BeEquivalentTo(v1beta1nodeclaim.ObjectMeta)) + }) + Context("NodeClaim Spec", func() { + It("should convert v1beta1 nodeclaim taints", func() { + v1beta1nodeclaim.Spec.Taints = []v1.Taint{ + { + Key: "test-key-1", + Value: "test-value-1", + Effect: v1.TaintEffectNoExecute, + }, + { + Key: "test-key-2", + Value: "test-value-2", + Effect: v1.TaintEffectNoSchedule, + }, + } + Expect(v1nodeclaim.ConvertFrom(ctx, v1beta1nodeclaim)).To(Succeed()) + for i := range v1beta1nodeclaim.Spec.Taints { + Expect(v1nodeclaim.Spec.Taints[i].Key).To(Equal(v1beta1nodeclaim.Spec.Taints[i].Key)) + Expect(v1nodeclaim.Spec.Taints[i].Value).To(Equal(v1beta1nodeclaim.Spec.Taints[i].Value)) + Expect(v1nodeclaim.Spec.Taints[i].Effect).To(Equal(v1beta1nodeclaim.Spec.Taints[i].Effect)) + } + }) + It("should convert v1beta1 nodeclaim startup taints", func() { + v1beta1nodeclaim.Spec.StartupTaints = []v1.Taint{ + { + Key: "test-key-startup-1", + Value: "test-value-startup-1", + Effect: v1.TaintEffectNoExecute, + }, + { + Key: "test-key-startup-2", + Value: "test-value-startup-2", + Effect: v1.TaintEffectNoSchedule, + }, + } + Expect(v1nodeclaim.ConvertFrom(ctx, v1beta1nodeclaim)).To(Succeed()) + for i := range v1beta1nodeclaim.Spec.StartupTaints { + Expect(v1nodeclaim.Spec.StartupTaints[i].Key).To(Equal(v1beta1nodeclaim.Spec.StartupTaints[i].Key)) + Expect(v1nodeclaim.Spec.StartupTaints[i].Value).To(Equal(v1beta1nodeclaim.Spec.StartupTaints[i].Value)) + Expect(v1nodeclaim.Spec.StartupTaints[i].Effect).To(Equal(v1beta1nodeclaim.Spec.StartupTaints[i].Effect)) + } + }) + It("should convert v1beta1 nodeclaim requirements", func() { + v1beta1nodeclaim.Spec.Requirements = []v1beta1.NodeSelectorRequirementWithMinValues{ + { + NodeSelectorRequirement: v1.NodeSelectorRequirement{ + Key: v1.LabelArchStable, + Operator: v1.NodeSelectorOpExists, + }, + MinValues: lo.ToPtr(4189133), + }, + { + NodeSelectorRequirement: v1.NodeSelectorRequirement{ + Key: CapacityTypeLabelKey, + Operator: v1.NodeSelectorOpIn, + Values: []string{CapacityTypeSpot}, + }, + MinValues: lo.ToPtr(7716191), + }, + } + Expect(v1nodeclaim.ConvertFrom(ctx, v1beta1nodeclaim)).To(Succeed()) + for i := range v1beta1nodeclaim.Spec.Requirements { + Expect(v1nodeclaim.Spec.Requirements[i].Key).To(Equal(v1beta1nodeclaim.Spec.Requirements[i].Key)) + Expect(v1nodeclaim.Spec.Requirements[i].Operator).To(Equal(v1beta1nodeclaim.Spec.Requirements[i].Operator)) + Expect(v1nodeclaim.Spec.Requirements[i].Values).To(Equal(v1beta1nodeclaim.Spec.Requirements[i].Values)) + Expect(v1nodeclaim.Spec.Requirements[i].MinValues).To(Equal(v1beta1nodeclaim.Spec.Requirements[i].MinValues)) + } + }) + It("should convert v1beta1 nodeclaim resources", func() { + v1beta1nodeclaim.Spec.Resources = v1beta1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("1"), + v1.ResourceMemory: resource.MustParse("134G"), + }, + } + Expect(v1nodeclaim.ConvertFrom(ctx, v1beta1nodeclaim)).To(Succeed()) + for key := range v1beta1nodeclaim.Spec.Resources.Requests { + Expect(v1beta1nodeclaim.Spec.Resources.Requests[key]).To(Equal(v1nodeclaim.Spec.Resources.Requests[key])) + } + }) + // It("should convert v1 nodeclaim template kubelet", func() { + // v1beta1nodeclaim.Spec.Kubelet = &v1beta1.KubeletConfiguration{ + // ClusterDNS: []string{"test-cluster-dns"}, + // MaxPods: lo.ToPtr(int32(9383)), + // PodsPerCore: lo.ToPtr(int32(9334283)), + // SystemReserved: map[string]string{"system-key": "reserved"}, + // KubeReserved: map[string]string{"kube-key": "reserved"}, + // EvictionHard: map[string]string{"eviction-key": "eviction"}, + // EvictionSoft: map[string]string{"eviction-key": "eviction"}, + // EvictionSoftGracePeriod: map[string]metav1.Duration{"test-soft-grace": {Duration: time.Hour}}, + // EvictionMaxPodGracePeriod: lo.ToPtr(int32(382902)), + // ImageGCHighThresholdPercent: lo.ToPtr(int32(382902)), + // CPUCFSQuota: lo.ToPtr(false), + // } + // Expect(v1nodeclaim.Annotations).To(BeNil()) + // Expect(v1nodeclaim.ConvertFrom(ctx, v1beta1nodeclaim)).To(Succeed()) + // kubelet := &v1beta1.KubeletConfiguration{} + // kubeletString, found := v1nodeclaim.Annotations[V1Beta1KubeletConfiguration] + // Expect(found).To(BeTrue()) + // err := json.Unmarshal([]byte(kubeletString), kubelet) + // Expect(err).To(BeNil()) + // Expect(kubelet.ClusterDNS).To(Equal(v1beta1nodeclaim.Spec.Kubelet.ClusterDNS)) + // Expect(lo.FromPtr(kubelet.MaxPods)).To(Equal(lo.FromPtr(v1beta1nodeclaim.Spec.Kubelet.MaxPods))) + // Expect(lo.FromPtr(kubelet.PodsPerCore)).To(Equal(lo.FromPtr(v1beta1nodeclaim.Spec.Kubelet.PodsPerCore))) + // Expect(lo.FromPtr(kubelet.EvictionMaxPodGracePeriod)).To(Equal(lo.FromPtr(v1beta1nodeclaim.Spec.Kubelet.EvictionMaxPodGracePeriod))) + // Expect(lo.FromPtr(kubelet.ImageGCHighThresholdPercent)).To(Equal(lo.FromPtr(v1beta1nodeclaim.Spec.Kubelet.ImageGCHighThresholdPercent))) + // Expect(lo.FromPtr(kubelet.ImageGCHighThresholdPercent)).To(Equal(lo.FromPtr(v1beta1nodeclaim.Spec.Kubelet.ImageGCHighThresholdPercent))) + // Expect(lo.FromPtr(kubelet.ImageGCHighThresholdPercent)).To(Equal(lo.FromPtr(v1beta1nodeclaim.Spec.Kubelet.ImageGCHighThresholdPercent))) + // Expect(lo.FromPtr(kubelet.ImageGCHighThresholdPercent)).To(Equal(lo.FromPtr(v1beta1nodeclaim.Spec.Kubelet.ImageGCHighThresholdPercent))) + // Expect(kubelet.SystemReserved).To(Equal(v1beta1nodeclaim.Spec.Kubelet.SystemReserved)) + // Expect(kubelet.KubeReserved).To(Equal(v1beta1nodeclaim.Spec.Kubelet.KubeReserved)) + // Expect(kubelet.EvictionHard).To(Equal(v1beta1nodeclaim.Spec.Kubelet.EvictionHard)) + // Expect(kubelet.EvictionSoft).To(Equal(v1beta1nodeclaim.Spec.Kubelet.EvictionSoft)) + // Expect(kubelet.EvictionSoftGracePeriod).To(Equal(v1beta1nodeclaim.Spec.Kubelet.EvictionSoftGracePeriod)) + // Expect(lo.FromPtr(kubelet.CPUCFSQuota)).To(Equal(lo.FromPtr(v1beta1nodeclaim.Spec.Kubelet.CPUCFSQuota))) + // }) + Context("NodeClassRef", func() { + It("should convert v1beta1 nodeclaim template nodeClassRef", func() { + v1beta1nodeclaim.Spec.NodeClassRef = &v1beta1.NodeClassReference{ + Kind: "test-kind", + Name: "nodeclass-test", + APIVersion: "testgroup.sh/testversion", + } + Expect(v1nodeclaim.ConvertFrom(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1nodeclaim.Spec.NodeClassRef.Kind).To(Equal(v1beta1nodeclaim.Spec.NodeClassRef.Kind)) + Expect(v1nodeclaim.Spec.NodeClassRef.Name).To(Equal(v1beta1nodeclaim.Spec.NodeClassRef.Name)) + Expect(v1nodeclaim.Spec.NodeClassRef.Group).To(Equal("testgroup.sh")) + }) + It("should set default nodeclass group and kind on v1beta1 nodeclassRef", func() { + v1beta1nodeclaim.Spec.NodeClassRef = &v1beta1.NodeClassReference{ + Name: "nodeclass-test", + } + Expect(v1nodeclaim.ConvertFrom(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1nodeclaim.Spec.NodeClassRef.Kind).To(Equal(cloudProvider.NodeClassGroupVersionKind[0].Kind)) + Expect(v1nodeclaim.Spec.NodeClassRef.Name).To(Equal(v1beta1nodeclaim.Spec.NodeClassRef.Name)) + Expect(v1nodeclaim.Spec.NodeClassRef.Group).To(Equal(cloudProvider.NodeClassGroupVersionKind[0].Group)) + }) + }) + }) + Context("NodeClaim Status", func() { + It("should convert v1beta1 nodeclaim nodename", func() { + v1beta1nodeclaim.Status.NodeName = "test-node-name" + Expect(v1nodeclaim.ConvertFrom(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1beta1nodeclaim.Status.NodeName).To(Equal(v1nodeclaim.Status.NodeName)) + }) + It("should convert v1beta1 nodeclaim provider id", func() { + v1beta1nodeclaim.Status.ProviderID = "test-provider-id" + Expect(v1nodeclaim.ConvertFrom(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1beta1nodeclaim.Status.ProviderID).To(Equal(v1nodeclaim.Status.ProviderID)) + }) + It("should convert v1beta1 nodeclaim image id", func() { + v1beta1nodeclaim.Status.ImageID = "test-image-id" + Expect(v1nodeclaim.ConvertFrom(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1beta1nodeclaim.Status.ImageID).To(Equal(v1nodeclaim.Status.ImageID)) + }) + It("should convert v1beta1 nodeclaim capacity", func() { + v1beta1nodeclaim.Status.Capacity = v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("13432"), + v1.ResourceMemory: resource.MustParse("1332G"), + } + Expect(v1nodeclaim.ConvertFrom(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1beta1nodeclaim.Status.Capacity).To(Equal(v1nodeclaim.Status.Capacity)) + }) + It("should convert v1beta1 nodeclaim allocatable", func() { + v1beta1nodeclaim.Status.Allocatable = v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("13432"), + v1.ResourceMemory: resource.MustParse("1332G"), + } + Expect(v1nodeclaim.ConvertFrom(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1beta1nodeclaim.Status.Allocatable).To(Equal(v1nodeclaim.Status.Allocatable)) + }) + It("should convert v1beta1 nodeclaim conditions", func() { + v1beta1nodeclaim.Status.Conditions = []status.Condition{ + { + Status: status.ConditionReady, + Reason: "test-reason", + }, + { + Status: ConditionTypeDrifted, + Reason: "test-reason", + }, + } + Expect(v1nodeclaim.ConvertFrom(ctx, v1beta1nodeclaim)).To(Succeed()) + Expect(v1beta1nodeclaim.Status.Conditions).To(Equal(v1nodeclaim.Status.Conditions)) + }) + }) +}) diff --git a/pkg/apis/v1/nodepool_conversion.go b/pkg/apis/v1/nodepool_conversion.go new file mode 100644 index 0000000000..eebffe65c4 --- /dev/null +++ b/pkg/apis/v1/nodepool_conversion.go @@ -0,0 +1,174 @@ +/* +Copyright The Kubernetes 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 v1 + +import ( + "context" + "sort" + "strings" + + "github.com/samber/lo" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "knative.dev/pkg/apis" + + "sigs.k8s.io/karpenter/pkg/apis/v1beta1" + "sigs.k8s.io/karpenter/pkg/operator/injection" +) + +// Convert v1 NodePool to v1beta1 NodePool +func (in *NodePool) ConvertTo(ctx context.Context, to apis.Convertible) error { + v1beta1NP := to.(*v1beta1.NodePool) + v1beta1NP.ObjectMeta = in.ObjectMeta + in.Spec.convertTo(ctx, &v1beta1NP.Spec) + + // Convert v1 status + v1beta1NP.Status.Resources = in.Status.Resources + return nil +} + +func (in *NodePoolSpec) convertTo(ctx context.Context, v1beta1np *v1beta1.NodePoolSpec) { + v1beta1np.Weight = in.Weight + v1beta1np.Limits = v1beta1.Limits(in.Limits) + in.Disruption.convertTo(&v1beta1np.Disruption) + in.Template.convertTo(ctx, &v1beta1np.Template) +} + +func (in *Disruption) convertTo(v1beta1np *v1beta1.Disruption) { + v1beta1np.ConsolidateAfter = (*v1beta1.NillableDuration)(in.ConsolidateAfter) + v1beta1np.ConsolidationPolicy = v1beta1.ConsolidationPolicy(in.ConsolidationPolicy) + v1beta1np.ExpireAfter = v1beta1.NillableDuration(in.ExpireAfter) + v1beta1np.Budgets = lo.Map(in.Budgets, func(v1budget Budget, _ int) v1beta1.Budget { + return v1beta1.Budget{ + Reasons: lo.Map(v1budget.Reasons, func(reason DisruptionReason, _ int) v1beta1.DisruptionReason { + return v1beta1.DisruptionReason(reason) + }), + Nodes: v1budget.Nodes, + Schedule: v1budget.Schedule, + Duration: v1budget.Duration, + } + }) +} + +func (in *NodeClaimTemplate) convertTo(ctx context.Context, v1beta1np *v1beta1.NodeClaimTemplate) { + v1beta1np.ObjectMeta = v1beta1.ObjectMeta(in.ObjectMeta) + v1beta1np.Spec.Taints = in.Spec.Taints + v1beta1np.Spec.StartupTaints = in.Spec.StartupTaints + v1beta1np.Spec.Requirements = lo.Map(in.Spec.Requirements, func(v1Requirements NodeSelectorRequirementWithMinValues, _ int) v1beta1.NodeSelectorRequirementWithMinValues { + return v1beta1.NodeSelectorRequirementWithMinValues{ + NodeSelectorRequirement: v1.NodeSelectorRequirement{ + Key: v1Requirements.Key, + Values: v1Requirements.Values, + Operator: v1Requirements.Operator, + }, + MinValues: v1Requirements.MinValues, + } + }) + + nodeClasses := injection.GetNodeClasses(ctx) + // We are sorting the supported nodeclass, so that we are able to consistently find the same GVK, + // if multiple version of a nodeclass are supported + sort.Slice(nodeClasses, func(i int, j int) bool { + if nodeClasses[i].Group != nodeClasses[j].Group { + return nodeClasses[i].Group < nodeClasses[j].Group + } + if nodeClasses[i].Version != nodeClasses[j].Version { + return nodeClasses[i].Version < nodeClasses[j].Version + } + return nodeClasses[i].Kind < nodeClasses[j].Kind + }) + matchingNodeClass, found := lo.Find(nodeClasses, func(nc schema.GroupVersionKind) bool { + return nc.Kind == in.Spec.NodeClassRef.Kind && nc.Group == in.Spec.NodeClassRef.Group + }) + v1beta1np.Spec.NodeClassRef = &v1beta1.NodeClassReference{ + Kind: in.Spec.NodeClassRef.Kind, + Name: in.Spec.NodeClassRef.Name, + APIVersion: lo.Ternary(found, matchingNodeClass.GroupVersion().String(), ""), + } + + // Need to implement Kubelet Conversion +} + +// Convert v1beta1 NodePool to V1 NodePool +func (in *NodePool) ConvertFrom(ctx context.Context, v1beta1np apis.Convertible) error { + v1beta1NP := v1beta1np.(*v1beta1.NodePool) + in.ObjectMeta = v1beta1NP.ObjectMeta + + // Convert v1beta1 status + in.Status.Resources = v1beta1NP.Status.Resources + + return in.Spec.convertFrom(ctx, &v1beta1NP.Spec) +} + +func (in *NodePoolSpec) convertFrom(ctx context.Context, v1beta1np *v1beta1.NodePoolSpec) error { + in.Weight = v1beta1np.Weight + in.Limits = Limits(v1beta1np.Limits) + in.Disruption.convertFrom(&v1beta1np.Disruption) + return in.Template.convertFrom(ctx, &v1beta1np.Template) +} + +func (in *Disruption) convertFrom(v1beta1np *v1beta1.Disruption) { + in.ConsolidateAfter = (*NillableDuration)(v1beta1np.ConsolidateAfter) + in.ConsolidationPolicy = ConsolidationPolicy(v1beta1np.ConsolidationPolicy) + in.ExpireAfter = NillableDuration(v1beta1np.ExpireAfter) + in.Budgets = lo.Map(v1beta1np.Budgets, func(v1beta1budget v1beta1.Budget, _ int) Budget { + return Budget{ + Reasons: lo.Map(v1beta1budget.Reasons, func(reason v1beta1.DisruptionReason, _ int) DisruptionReason { + return DisruptionReason(reason) + }), + Nodes: v1beta1budget.Nodes, + Schedule: v1beta1budget.Schedule, + Duration: v1beta1budget.Duration, + } + }) +} + +func (in *NodeClaimTemplate) convertFrom(ctx context.Context, v1beta1np *v1beta1.NodeClaimTemplate) error { + in.ObjectMeta = ObjectMeta(v1beta1np.ObjectMeta) + in.Spec.Taints = v1beta1np.Spec.Taints + in.Spec.StartupTaints = v1beta1np.Spec.StartupTaints + in.Spec.Requirements = lo.Map(v1beta1np.Spec.Requirements, func(v1beta1Requirements v1beta1.NodeSelectorRequirementWithMinValues, _ int) NodeSelectorRequirementWithMinValues { + return NodeSelectorRequirementWithMinValues{ + NodeSelectorRequirement: v1.NodeSelectorRequirement{ + Key: v1beta1Requirements.Key, + Values: v1beta1Requirements.Values, + Operator: v1beta1Requirements.Operator, + }, + MinValues: v1beta1Requirements.MinValues, + } + }) + + nodeclasses := injection.GetNodeClasses(ctx) + in.Spec.NodeClassRef = &NodeClassReference{ + Name: v1beta1np.Spec.NodeClassRef.Name, + Kind: lo.Ternary(v1beta1np.Spec.NodeClassRef.Kind == "", nodeclasses[0].Kind, v1beta1np.Spec.NodeClassRef.Kind), + Group: lo.Ternary(v1beta1np.Spec.NodeClassRef.APIVersion == "", nodeclasses[0].Group, strings.Split(v1beta1np.Spec.NodeClassRef.APIVersion, "/")[0]), + } + + defaultNodeClassGVK := injection.GetNodeClasses(ctx)[0] + nodeclassGroupVersion, err := schema.ParseGroupVersion(v1beta1np.Spec.NodeClassRef.APIVersion) + if err != nil { + return err + } + in.Spec.NodeClassRef = &NodeClassReference{ + Name: v1beta1np.Spec.NodeClassRef.Name, + Kind: lo.Ternary(v1beta1np.Spec.NodeClassRef.Kind == "", defaultNodeClassGVK.Kind, v1beta1np.Spec.NodeClassRef.Kind), + Group: lo.Ternary(v1beta1np.Spec.NodeClassRef.APIVersion == "", defaultNodeClassGVK.Group, nodeclassGroupVersion.Group), + } + // Need to implement Kubelet Conversion + return nil +} diff --git a/pkg/apis/v1/nodepool_conversion_test.go b/pkg/apis/v1/nodepool_conversion_test.go new file mode 100644 index 0000000000..c10a9c3c18 --- /dev/null +++ b/pkg/apis/v1/nodepool_conversion_test.go @@ -0,0 +1,549 @@ +/* +Copyright The Kubernetes 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 v1_test + +import ( + "time" + + "sigs.k8s.io/karpenter/pkg/test" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/samber/lo" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + . "sigs.k8s.io/karpenter/pkg/apis/v1" + "sigs.k8s.io/karpenter/pkg/apis/v1beta1" + "sigs.k8s.io/karpenter/pkg/operator/injection" +) + +var _ = Describe("Convert V1 to V1beta1 NodePool API", func() { + var v1nodepool *NodePool + var v1beta1nodepool *v1beta1.NodePool + + BeforeEach(func() { + v1nodepool = &NodePool{ + Spec: NodePoolSpec{ + Template: NodeClaimTemplate{ + Spec: NodeClaimSpec{ + NodeClassRef: &NodeClassReference{ + Name: "test", + Kind: "test", + Group: "test", + }, + }, + }, + }, + } + v1beta1nodepool = &v1beta1.NodePool{ + Spec: v1beta1.NodePoolSpec{ + Template: v1beta1.NodeClaimTemplate{ + Spec: v1beta1.NodeClaimSpec{ + NodeClassRef: &v1beta1.NodeClassReference{ + Name: "test", + Kind: "test", + APIVersion: "group/test", + }, + }, + }, + }, + } + cloudProvider.NodeClassGroupVersionKind = []schema.GroupVersionKind{ + { + Group: "fake-cloudprovider-group", + Version: "fake-cloudprovider-version", + Kind: "fake-cloudprovider-kind", + }, + } + ctx = injection.WithNodeClasses(ctx, cloudProvider.GetSupportedNodeClasses()) + }) + + It("should convert v1 nodepool metadata", func() { + v1nodepool.ObjectMeta = test.ObjectMeta() + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + Expect(v1beta1nodepool.ObjectMeta).To(BeEquivalentTo(v1nodepool.ObjectMeta)) + }) + Context("NodePool Spec", func() { + It("should convert v1 nodepool weights", func() { + v1nodepool.Spec.Weight = lo.ToPtr(int32(62)) + Expect(v1nodepool.ConvertTo(ctx, v1beta1nodepool)).To(Succeed()) + Expect(lo.FromPtr(v1beta1nodepool.Spec.Weight)).To(Equal(int32(62))) + }) + It("should convert v1 nodepool limits", func() { + v1nodepool.Spec.Limits = Limits{ + v1.ResourceCPU: resource.MustParse("5"), + v1.ResourceMemory: resource.MustParse("14145G"), + } + Expect(v1nodepool.ConvertTo(ctx, v1beta1nodepool)).To(Succeed()) + for _, resource := range lo.Keys(v1nodepool.Spec.Limits) { + Expect(v1beta1nodepool.Spec.Limits[resource]).To(Equal(v1nodepool.Spec.Limits[resource])) + } + }) + Context("NodeClaimTemplate", func() { + It("should convert v1 nodepool metadata", func() { + v1nodepool.Spec.Template.ObjectMeta = ObjectMeta{ + Labels: map[string]string{ + "test-key-1": "test-value-1", + "test-key-2": "test-value-2", + }, + Annotations: map[string]string{ + "test-key-1": "test-value-1", + "test-key-2": "test-value-2", + }, + } + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + Expect(v1beta1nodepool.Spec.Template.ObjectMeta).To(BeEquivalentTo(v1nodepool.Spec.Template.ObjectMeta)) + }) + It("should convert v1 nodepool template taints", func() { + v1nodepool.Spec.Template.Spec.Taints = []v1.Taint{ + { + Key: "test-key-1", + Value: "test-value-1", + Effect: v1.TaintEffectNoExecute, + }, + { + Key: "test-key-2", + Value: "test-value-2", + Effect: v1.TaintEffectNoSchedule, + }, + } + Expect(v1nodepool.ConvertTo(ctx, v1beta1nodepool)).To(Succeed()) + for i := range v1nodepool.Spec.Template.Spec.Taints { + Expect(v1beta1nodepool.Spec.Template.Spec.Taints[i].Key).To(Equal(v1nodepool.Spec.Template.Spec.Taints[i].Key)) + Expect(v1beta1nodepool.Spec.Template.Spec.Taints[i].Value).To(Equal(v1nodepool.Spec.Template.Spec.Taints[i].Value)) + Expect(v1beta1nodepool.Spec.Template.Spec.Taints[i].Effect).To(Equal(v1nodepool.Spec.Template.Spec.Taints[i].Effect)) + } + }) + It("should convert v1 nodepool template startup taints", func() { + v1nodepool.Spec.Template.Spec.StartupTaints = []v1.Taint{ + { + Key: "test-key-startup-1", + Value: "test-value-startup-1", + Effect: v1.TaintEffectNoExecute, + }, + { + Key: "test-key-startup-2", + Value: "test-value-startup-2", + Effect: v1.TaintEffectNoSchedule, + }, + } + Expect(v1nodepool.ConvertTo(ctx, v1beta1nodepool)).To(Succeed()) + for i := range v1nodepool.Spec.Template.Spec.StartupTaints { + Expect(v1beta1nodepool.Spec.Template.Spec.StartupTaints[i].Key).To(Equal(v1nodepool.Spec.Template.Spec.StartupTaints[i].Key)) + Expect(v1beta1nodepool.Spec.Template.Spec.StartupTaints[i].Value).To(Equal(v1nodepool.Spec.Template.Spec.StartupTaints[i].Value)) + Expect(v1beta1nodepool.Spec.Template.Spec.StartupTaints[i].Effect).To(Equal(v1nodepool.Spec.Template.Spec.StartupTaints[i].Effect)) + } + }) + It("should convert v1 nodepool template requirements", func() { + v1nodepool.Spec.Template.Spec.Requirements = []NodeSelectorRequirementWithMinValues{ + { + NodeSelectorRequirement: v1.NodeSelectorRequirement{ + Key: v1.LabelArchStable, + Operator: v1.NodeSelectorOpExists, + }, + MinValues: lo.ToPtr(433234), + }, + { + NodeSelectorRequirement: v1.NodeSelectorRequirement{ + Key: CapacityTypeLabelKey, + Operator: v1.NodeSelectorOpIn, + Values: []string{CapacityTypeSpot}, + }, + MinValues: lo.ToPtr(65765), + }, + } + Expect(v1nodepool.ConvertTo(ctx, v1beta1nodepool)).To(Succeed()) + for i := range v1nodepool.Spec.Template.Spec.Requirements { + Expect(v1beta1nodepool.Spec.Template.Spec.Requirements[i].Key).To(Equal(v1nodepool.Spec.Template.Spec.Requirements[i].Key)) + Expect(v1beta1nodepool.Spec.Template.Spec.Requirements[i].Operator).To(Equal(v1nodepool.Spec.Template.Spec.Requirements[i].Operator)) + Expect(v1beta1nodepool.Spec.Template.Spec.Requirements[i].Values).To(Equal(v1nodepool.Spec.Template.Spec.Requirements[i].Values)) + Expect(v1beta1nodepool.Spec.Template.Spec.Requirements[i].MinValues).To(Equal(v1nodepool.Spec.Template.Spec.Requirements[i].MinValues)) + } + }) + Context("NodeClassRef", func() { + It("should convert v1 nodepool template nodeClassRef", func() { + v1nodepool.Spec.Template.Spec.NodeClassRef = &NodeClassReference{ + Kind: "fake-cloudprovider-kind", + Name: "nodeclass-test", + Group: "fake-cloudprovider-group", + } + Expect(v1nodepool.ConvertTo(ctx, v1beta1nodepool)).To(Succeed()) + Expect(v1beta1nodepool.Spec.Template.Spec.NodeClassRef.Kind).To(Equal(v1nodepool.Spec.Template.Spec.NodeClassRef.Kind)) + Expect(v1beta1nodepool.Spec.Template.Spec.NodeClassRef.Name).To(Equal(v1nodepool.Spec.Template.Spec.NodeClassRef.Name)) + Expect(v1beta1nodepool.Spec.Template.Spec.NodeClassRef.APIVersion).To(Equal(cloudProvider.NodeClassGroupVersionKind[0].GroupVersion().String())) + }) + It("should not include APIVersion for v1beta1 if Group and Kind is not in the supported nodeclass", func() { + v1nodepool.Spec.Template.Spec.NodeClassRef = &NodeClassReference{ + Kind: "test-kind", + Name: "nodeclass-test", + Group: "testgroup.sh", + } + Expect(v1nodepool.ConvertTo(ctx, v1beta1nodepool)).To(Succeed()) + Expect(v1beta1nodepool.Spec.Template.Spec.NodeClassRef.Kind).To(Equal(v1nodepool.Spec.Template.Spec.NodeClassRef.Kind)) + Expect(v1beta1nodepool.Spec.Template.Spec.NodeClassRef.Name).To(Equal(v1nodepool.Spec.Template.Spec.NodeClassRef.Name)) + Expect(v1beta1nodepool.Spec.Template.Spec.NodeClassRef.APIVersion).To(Equal("")) + }) + }) + }) + Context("Disruption", func() { + It("should convert v1 nodepool consolidateAfter", func() { + v1nodepool.Spec.Disruption.ConsolidateAfter = &NillableDuration{Duration: lo.ToPtr(time.Second * 2121)} + Expect(v1nodepool.ConvertTo(ctx, v1beta1nodepool)).To(Succeed()) + Expect(lo.FromPtr(v1beta1nodepool.Spec.Disruption.ConsolidateAfter.Duration)).To(Equal(lo.FromPtr(v1nodepool.Spec.Disruption.ConsolidateAfter.Duration))) + }) + It("should convert v1 nodepool consolidatePolicy", func() { + v1nodepool.Spec.Disruption.ConsolidationPolicy = ConsolidationPolicyWhenEmpty + Expect(v1nodepool.ConvertTo(ctx, v1beta1nodepool)).To(Succeed()) + Expect(string(v1beta1nodepool.Spec.Disruption.ConsolidationPolicy)).To(Equal(string(v1nodepool.Spec.Disruption.ConsolidationPolicy))) + }) + It("should convert v1 nodepool ExpireAfter", func() { + v1nodepool.Spec.Disruption.ExpireAfter = NillableDuration{Duration: lo.ToPtr(time.Second * 2121)} + Expect(v1nodepool.ConvertTo(ctx, v1beta1nodepool)).To(Succeed()) + Expect(v1beta1nodepool.Spec.Disruption.ExpireAfter.Duration).To(Equal(v1nodepool.Spec.Disruption.ExpireAfter.Duration)) + }) + Context("Budgets", func() { + It("should convert v1 nodepool nodes", func() { + v1nodepool.Spec.Disruption.Budgets = append(v1nodepool.Spec.Disruption.Budgets, Budget{ + Nodes: "1545", + }) + Expect(v1nodepool.ConvertTo(ctx, v1beta1nodepool)).To(Succeed()) + for i := range v1nodepool.Spec.Disruption.Budgets { + Expect(v1beta1nodepool.Spec.Disruption.Budgets[i].Nodes).To(Equal(v1nodepool.Spec.Disruption.Budgets[i].Nodes)) + } + }) + It("should convert v1 nodepool schedule", func() { + v1nodepool.Spec.Disruption.Budgets = append(v1nodepool.Spec.Disruption.Budgets, Budget{ + Schedule: lo.ToPtr("1545"), + }) + Expect(v1nodepool.ConvertTo(ctx, v1beta1nodepool)).To(Succeed()) + for i := range v1nodepool.Spec.Disruption.Budgets { + Expect(v1beta1nodepool.Spec.Disruption.Budgets[i].Schedule).To(Equal(v1nodepool.Spec.Disruption.Budgets[i].Schedule)) + } + }) + It("should convert v1 nodepool duration", func() { + v1nodepool.Spec.Disruption.Budgets = append(v1nodepool.Spec.Disruption.Budgets, Budget{ + Duration: &metav1.Duration{Duration: time.Second * 2121}, + }) + Expect(v1nodepool.ConvertTo(ctx, v1beta1nodepool)).To(Succeed()) + for i := range v1nodepool.Spec.Disruption.Budgets { + Expect(v1beta1nodepool.Spec.Disruption.Budgets[i].Duration.Duration).To(Equal(v1nodepool.Spec.Disruption.Budgets[i].Duration.Duration)) + } + }) + It("should convert v1 nodepool reason", func() { + v1nodepool.Spec.Disruption.Budgets = append(v1nodepool.Spec.Disruption.Budgets, Budget{ + Reasons: []DisruptionReason{DisruptionReasonDrifted, DisruptionReasonUnderutilized, DisruptionReasonEmpty}, + }) + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + for i := range v1nodepool.Spec.Disruption.Budgets { + expected := lo.Map(v1nodepool.Spec.Disruption.Budgets[i].Reasons, func(reason DisruptionReason, _ int) v1beta1.DisruptionReason { + return v1beta1.DisruptionReason(reason) + }) + Expect(v1beta1nodepool.Spec.Disruption.Budgets[i].Reasons).To(BeEquivalentTo(expected)) + } + }) + }) + }) + }) + It("should convert v1 nodepool status", func() { + v1nodepool.Status.Resources = v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("5"), + v1.ResourceMemory: resource.MustParse("14145G"), + } + Expect(v1nodepool.ConvertTo(ctx, v1beta1nodepool)).To(Succeed()) + for _, resource := range lo.Keys(v1nodepool.Status.Resources) { + Expect(v1beta1nodepool.Status.Resources[resource]).To(Equal(v1nodepool.Status.Resources[resource])) + } + }) +}) + +var _ = Describe("Convert V1beta1 to V1 NodePool API", func() { + var ( + v1nodepool *NodePool + v1beta1nodepool *v1beta1.NodePool + ) + + BeforeEach(func() { + v1nodepool = &NodePool{ + Spec: NodePoolSpec{ + Template: NodeClaimTemplate{ + Spec: NodeClaimSpec{ + NodeClassRef: &NodeClassReference{ + Name: "test", + Kind: "test", + Group: "test", + }, + }, + }, + }, + } + v1beta1nodepool = &v1beta1.NodePool{ + Spec: v1beta1.NodePoolSpec{ + Template: v1beta1.NodeClaimTemplate{ + Spec: v1beta1.NodeClaimSpec{ + NodeClassRef: &v1beta1.NodeClassReference{ + Name: "test", + Kind: "test", + APIVersion: "group/test", + }, + }, + }, + }, + } + cloudProvider.NodeClassGroupVersionKind = []schema.GroupVersionKind{ + { + Group: "fake-cloudprovider-group", + Version: "fake-cloudprovider-version", + Kind: "fake-cloudprovider-kind", + }, + } + ctx = injection.WithNodeClasses(ctx, cloudProvider.GetSupportedNodeClasses()) + }) + + It("should convert v1beta1 nodepool metadata", func() { + v1beta1nodepool.ObjectMeta = test.ObjectMeta() + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + Expect(v1nodepool.ObjectMeta).To(BeEquivalentTo(v1beta1nodepool.ObjectMeta)) + }) + Context("NodePool Spec", func() { + It("should convert v1beta1 nodepool weights", func() { + v1beta1nodepool.Spec.Weight = lo.ToPtr(int32(62)) + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + Expect(v1nodepool.Spec.Weight).To(Equal(v1beta1nodepool.Spec.Weight)) + }) + It("should convert v1beta1 nodepool limits", func() { + v1beta1nodepool.Spec.Limits = v1beta1.Limits{ + v1.ResourceCPU: resource.MustParse("5"), + v1.ResourceMemory: resource.MustParse("14145G"), + } + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + for _, resource := range lo.Keys(v1beta1nodepool.Spec.Limits) { + Expect(v1nodepool.Spec.Limits[resource]).To(Equal(v1beta1nodepool.Spec.Limits[resource])) + } + }) + Context("NodeClaimTemplate", func() { + It("should convert v1beta1 nodepool metadata", func() { + v1beta1nodepool.Spec.Template.ObjectMeta = v1beta1.ObjectMeta{ + Labels: map[string]string{ + "test-key-1": "test-value-1", + "test-key-2": "test-value-2", + }, + Annotations: map[string]string{ + "test-key-1": "test-value-1", + "test-key-2": "test-value-2", + }, + } + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + Expect(v1nodepool.Spec.Template.ObjectMeta).To(BeEquivalentTo(v1beta1nodepool.Spec.Template.ObjectMeta)) + }) + It("should convert v1beta1 nodepool template taints", func() { + v1beta1nodepool.Spec.Template.Spec.Taints = []v1.Taint{ + { + Key: "test-key-1", + Value: "test-value-1", + Effect: v1.TaintEffectNoExecute, + }, + { + Key: "test-key-2", + Value: "test-value-2", + Effect: v1.TaintEffectNoSchedule, + }, + } + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + for i := range v1beta1nodepool.Spec.Template.Spec.Taints { + Expect(v1nodepool.Spec.Template.Spec.Taints[i].Key).To(Equal(v1beta1nodepool.Spec.Template.Spec.Taints[i].Key)) + Expect(v1nodepool.Spec.Template.Spec.Taints[i].Value).To(Equal(v1beta1nodepool.Spec.Template.Spec.Taints[i].Value)) + Expect(v1nodepool.Spec.Template.Spec.Taints[i].Effect).To(Equal(v1beta1nodepool.Spec.Template.Spec.Taints[i].Effect)) + } + }) + It("should convert v1beta1 nodepool template startup taints", func() { + v1beta1nodepool.Spec.Template.Spec.StartupTaints = []v1.Taint{ + { + Key: "test-key-startup-1", + Value: "test-value-startup-1", + Effect: v1.TaintEffectNoExecute, + }, + { + Key: "test-key-startup-2", + Value: "test-value-startup-2", + Effect: v1.TaintEffectNoSchedule, + }, + } + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + for i := range v1beta1nodepool.Spec.Template.Spec.StartupTaints { + Expect(v1nodepool.Spec.Template.Spec.StartupTaints[i].Key).To(Equal(v1beta1nodepool.Spec.Template.Spec.StartupTaints[i].Key)) + Expect(v1nodepool.Spec.Template.Spec.StartupTaints[i].Value).To(Equal(v1beta1nodepool.Spec.Template.Spec.StartupTaints[i].Value)) + Expect(v1nodepool.Spec.Template.Spec.StartupTaints[i].Effect).To(Equal(v1beta1nodepool.Spec.Template.Spec.StartupTaints[i].Effect)) + } + }) + It("should convert v1beta1 nodepool template requirements", func() { + v1beta1nodepool.Spec.Template.Spec.Requirements = []v1beta1.NodeSelectorRequirementWithMinValues{ + { + NodeSelectorRequirement: v1.NodeSelectorRequirement{ + Key: v1.LabelArchStable, + Operator: v1.NodeSelectorOpExists, + }, + MinValues: lo.ToPtr(98946513), + }, + { + NodeSelectorRequirement: v1.NodeSelectorRequirement{ + Key: CapacityTypeLabelKey, + Operator: v1.NodeSelectorOpIn, + Values: []string{CapacityTypeSpot}, + }, + MinValues: lo.ToPtr(513164), + }, + } + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + for i := range v1beta1nodepool.Spec.Template.Spec.Requirements { + Expect(v1nodepool.Spec.Template.Spec.Requirements[i].Key).To(Equal(v1beta1nodepool.Spec.Template.Spec.Requirements[i].Key)) + Expect(v1nodepool.Spec.Template.Spec.Requirements[i].Operator).To(Equal(v1beta1nodepool.Spec.Template.Spec.Requirements[i].Operator)) + Expect(v1nodepool.Spec.Template.Spec.Requirements[i].Values).To(Equal(v1beta1nodepool.Spec.Template.Spec.Requirements[i].Values)) + Expect(v1nodepool.Spec.Template.Spec.Requirements[i].MinValues).To(Equal(v1beta1nodepool.Spec.Template.Spec.Requirements[i].MinValues)) + } + }) + // It("should convert v1 nodepool template kubelet", func() { + // v1beta1nodepool.Spec.Template.Spec.Kubelet = &v1beta1.KubeletConfiguration{ + // ClusterDNS: []string{"test-cluster-dns"}, + // MaxPods: lo.ToPtr(int32(9383)), + // PodsPerCore: lo.ToPtr(int32(9334283)), + // SystemReserved: map[string]string{"system-key": "reserved"}, + // KubeReserved: map[string]string{"kube-key": "reserved"}, + // EvictionHard: map[string]string{"eviction-key": "eviction"}, + // EvictionSoft: map[string]string{"eviction-key": "eviction"}, + // EvictionSoftGracePeriod: map[string]metav1.Duration{"test-soft-grace": {Duration: time.Hour}}, + // EvictionMaxPodGracePeriod: lo.ToPtr(int32(382902)), + // ImageGCHighThresholdPercent: lo.ToPtr(int32(382902)), + // CPUCFSQuota: lo.ToPtr(false), + // } + // Expect(v1nodepool.Annotations).To(BeNil()) + // Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + // kubelet := &v1beta1.KubeletConfiguration{} + // kubeletString, found := v1nodepool.Annotations[V1Beta1KubeletConfiguration] + // Expect(found).To(BeTrue()) + // err := json.Unmarshal([]byte(kubeletString), kubelet) + // Expect(err).To(BeNil()) + // Expect(kubelet.ClusterDNS).To(Equal(v1beta1nodepool.Spec.Template.Spec.Kubelet.ClusterDNS)) + // Expect(lo.FromPtr(kubelet.MaxPods)).To(Equal(lo.FromPtr(v1beta1nodepool.Spec.Template.Spec.Kubelet.MaxPods))) + // Expect(lo.FromPtr(kubelet.PodsPerCore)).To(Equal(lo.FromPtr(v1beta1nodepool.Spec.Template.Spec.Kubelet.PodsPerCore))) + // Expect(lo.FromPtr(kubelet.EvictionMaxPodGracePeriod)).To(Equal(lo.FromPtr(v1beta1nodepool.Spec.Template.Spec.Kubelet.EvictionMaxPodGracePeriod))) + // Expect(lo.FromPtr(kubelet.ImageGCHighThresholdPercent)).To(Equal(lo.FromPtr(v1beta1nodepool.Spec.Template.Spec.Kubelet.ImageGCHighThresholdPercent))) + // Expect(lo.FromPtr(kubelet.ImageGCHighThresholdPercent)).To(Equal(lo.FromPtr(v1beta1nodepool.Spec.Template.Spec.Kubelet.ImageGCHighThresholdPercent))) + // Expect(lo.FromPtr(kubelet.ImageGCHighThresholdPercent)).To(Equal(lo.FromPtr(v1beta1nodepool.Spec.Template.Spec.Kubelet.ImageGCHighThresholdPercent))) + // Expect(lo.FromPtr(kubelet.ImageGCHighThresholdPercent)).To(Equal(lo.FromPtr(v1beta1nodepool.Spec.Template.Spec.Kubelet.ImageGCHighThresholdPercent))) + // Expect(kubelet.SystemReserved).To(Equal(v1beta1nodepool.Spec.Template.Spec.Kubelet.SystemReserved)) + // Expect(kubelet.KubeReserved).To(Equal(v1beta1nodepool.Spec.Template.Spec.Kubelet.KubeReserved)) + // Expect(kubelet.EvictionHard).To(Equal(v1beta1nodepool.Spec.Template.Spec.Kubelet.EvictionHard)) + // Expect(kubelet.EvictionSoft).To(Equal(v1beta1nodepool.Spec.Template.Spec.Kubelet.EvictionSoft)) + // Expect(kubelet.EvictionSoftGracePeriod).To(Equal(v1beta1nodepool.Spec.Template.Spec.Kubelet.EvictionSoftGracePeriod)) + // Expect(lo.FromPtr(kubelet.CPUCFSQuota)).To(Equal(lo.FromPtr(v1beta1nodepool.Spec.Template.Spec.Kubelet.CPUCFSQuota))) + // }) + Context("NodeClassRef", func() { + It("should convert v1beta1 nodepool template nodeClassRef", func() { + v1beta1nodepool.Spec.Template.Spec.NodeClassRef = &v1beta1.NodeClassReference{ + Kind: "test-kind", + Name: "nodeclass-test", + APIVersion: "testgroup.sh/testversion", + } + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + Expect(v1nodepool.Spec.Template.Spec.NodeClassRef.Kind).To(Equal(v1beta1nodepool.Spec.Template.Spec.NodeClassRef.Kind)) + Expect(v1nodepool.Spec.Template.Spec.NodeClassRef.Name).To(Equal(v1beta1nodepool.Spec.Template.Spec.NodeClassRef.Name)) + Expect(v1nodepool.Spec.Template.Spec.NodeClassRef.Group).To(Equal("testgroup.sh")) + }) + It("should set default nodeclass group and kind on v1beta1 nodeclassRef", func() { + v1beta1nodepool.Spec.Template.Spec.NodeClassRef = &v1beta1.NodeClassReference{ + Name: "nodeclass-test", + } + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + Expect(v1nodepool.Spec.Template.Spec.NodeClassRef.Kind).To(Equal(cloudProvider.NodeClassGroupVersionKind[0].Kind)) + Expect(v1nodepool.Spec.Template.Spec.NodeClassRef.Name).To(Equal(v1beta1nodepool.Spec.Template.Spec.NodeClassRef.Name)) + Expect(v1nodepool.Spec.Template.Spec.NodeClassRef.Group).To(Equal(cloudProvider.NodeClassGroupVersionKind[0].Group)) + }) + }) + }) + Context("Disruption", func() { + It("should convert v1beta1 nodepool consolidateAfter", func() { + v1beta1nodepool.Spec.Disruption.ConsolidateAfter = &v1beta1.NillableDuration{Duration: lo.ToPtr(time.Second * 2121)} + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + Expect(v1nodepool.Spec.Disruption.ConsolidateAfter.Duration).To(Equal(v1beta1nodepool.Spec.Disruption.ConsolidateAfter.Duration)) + }) + It("should convert v1beta1 nodepool consolidatePolicy", func() { + v1beta1nodepool.Spec.Disruption.ConsolidationPolicy = v1beta1.ConsolidationPolicyWhenEmpty + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + Expect(string(v1nodepool.Spec.Disruption.ConsolidationPolicy)).To(Equal(string(v1beta1nodepool.Spec.Disruption.ConsolidationPolicy))) + }) + It("should convert v1beta1 nodepool ExpireAfter", func() { + v1beta1nodepool.Spec.Disruption.ExpireAfter = v1beta1.NillableDuration{Duration: lo.ToPtr(time.Second * 2121)} + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + Expect(v1nodepool.Spec.Disruption.ExpireAfter.Duration).To(Equal(v1beta1nodepool.Spec.Disruption.ExpireAfter.Duration)) + }) + Context("Budgets", func() { + It("should convert v1beta1 nodepool nodes", func() { + v1beta1nodepool.Spec.Disruption.Budgets = append(v1beta1nodepool.Spec.Disruption.Budgets, v1beta1.Budget{ + Nodes: "1545", + }) + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + for i := range v1beta1nodepool.Spec.Disruption.Budgets { + Expect(v1nodepool.Spec.Disruption.Budgets[i].Nodes).To(Equal(v1beta1nodepool.Spec.Disruption.Budgets[i].Nodes)) + } + }) + It("should convert v1beta1 nodepool schedule", func() { + v1beta1nodepool.Spec.Disruption.Budgets = append(v1beta1nodepool.Spec.Disruption.Budgets, v1beta1.Budget{ + Schedule: lo.ToPtr("1545"), + }) + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + for i := range v1beta1nodepool.Spec.Disruption.Budgets { + Expect(v1nodepool.Spec.Disruption.Budgets[i].Schedule).To(Equal(v1beta1nodepool.Spec.Disruption.Budgets[i].Schedule)) + } + }) + It("should convert v1beta1 nodepool duration", func() { + v1beta1nodepool.Spec.Disruption.Budgets = append(v1beta1nodepool.Spec.Disruption.Budgets, v1beta1.Budget{ + Duration: &metav1.Duration{Duration: time.Second * 2121}, + }) + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + for i := range v1beta1nodepool.Spec.Disruption.Budgets { + Expect(v1nodepool.Spec.Disruption.Budgets[i].Duration.Duration).To(Equal(v1beta1nodepool.Spec.Disruption.Budgets[i].Duration.Duration)) + } + }) + It("should convert v1beta1 nodepool reason", func() { + v1beta1nodepool.Spec.Disruption.Budgets = append(v1beta1nodepool.Spec.Disruption.Budgets, v1beta1.Budget{ + Reasons: []v1beta1.DisruptionReason{v1beta1.DisruptionReasonDrifted, v1beta1.DisruptionReasonUnderutilized, v1beta1.DisruptionReasonEmpty}, + }) + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + for i := range v1beta1nodepool.Spec.Disruption.Budgets { + expected := lo.Map(v1beta1nodepool.Spec.Disruption.Budgets[i].Reasons, func(reason v1beta1.DisruptionReason, _ int) DisruptionReason { + return DisruptionReason(reason) + }) + Expect(v1nodepool.Spec.Disruption.Budgets[i].Reasons).To(BeEquivalentTo(expected)) + } + }) + }) + }) + }) + It("should convert v1beta1 nodepool status", func() { + v1beta1nodepool.Status.Resources = v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("5"), + v1.ResourceMemory: resource.MustParse("14145G"), + } + Expect(v1nodepool.ConvertFrom(ctx, v1beta1nodepool)).To(Succeed()) + for _, resource := range lo.Keys(v1beta1nodepool.Status.Resources) { + Expect(v1beta1nodepool.Status.Resources[resource]).To(Equal(v1nodepool.Status.Resources[resource])) + } + }) +}) diff --git a/pkg/apis/v1/suite_test.go b/pkg/apis/v1/suite_test.go index 1af493d333..ece0bf92d3 100644 --- a/pkg/apis/v1/suite_test.go +++ b/pkg/apis/v1/suite_test.go @@ -29,21 +29,24 @@ import ( "sigs.k8s.io/karpenter/pkg/apis" "sigs.k8s.io/karpenter/pkg/apis/v1beta1" + "sigs.k8s.io/karpenter/pkg/cloudprovider/fake" "sigs.k8s.io/karpenter/pkg/test" . "sigs.k8s.io/karpenter/pkg/test/expectations" ) var ctx context.Context var env *test.Environment +var cloudProvider *fake.CloudProvider func TestAPIs(t *testing.T) { ctx = TestContextWithLogger(t) RegisterFailHandler(Fail) - RunSpecs(t, "v1beta1") + RunSpecs(t, "v1") } var _ = BeforeSuite(func() { env = test.NewEnvironment(test.WithCRDs(apis.CRDs...)) + cloudProvider = fake.NewCloudProvider() }) var _ = AfterEach(func() { diff --git a/pkg/apis/v1beta1/nodeclaim_conversion.go b/pkg/apis/v1beta1/nodeclaim_conversion.go new file mode 100644 index 0000000000..d69c557055 --- /dev/null +++ b/pkg/apis/v1beta1/nodeclaim_conversion.go @@ -0,0 +1,29 @@ +/* +Copyright The Kubernetes 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 v1beta1 + +import ( + "context" + + "knative.dev/pkg/apis" +) + +// Since v1 is the hub conversion version, We will only need to implement conversion webhooks for v1 + +func (in *NodeClaim) ConvertTo(_ context.Context, _ apis.Convertible) error { return nil } + +func (in *NodeClaim) ConvertFrom(_ context.Context, _ apis.Convertible) error { return nil } diff --git a/pkg/apis/v1beta1/nodepool_conversion.go b/pkg/apis/v1beta1/nodepool_conversion.go new file mode 100644 index 0000000000..ed50b62f56 --- /dev/null +++ b/pkg/apis/v1beta1/nodepool_conversion.go @@ -0,0 +1,29 @@ +/* +Copyright The Kubernetes 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 v1beta1 + +import ( + "context" + + "knative.dev/pkg/apis" +) + +// Since v1 is the hub conversion version, We will only need to implement conversion webhooks for v1 + +func (in *NodePool) ConvertTo(_ context.Context, _ apis.Convertible) error { return nil } + +func (in *NodePool) ConvertFrom(_ context.Context, _ apis.Convertible) error { return nil } diff --git a/pkg/operator/injection/injection.go b/pkg/operator/injection/injection.go index 43eec1e1b6..5b2215acd4 100644 --- a/pkg/operator/injection/injection.go +++ b/pkg/operator/injection/injection.go @@ -21,14 +21,19 @@ import ( "flag" "os" + "knative.dev/pkg/logging" + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/karpenter/pkg/operator/options" ) type controllerNameKeyType struct{} +type nodeClassType struct{} var controllerNameKey = controllerNameKeyType{} +var nodeClassKey = nodeClassType{} func WithControllerName(ctx context.Context, name string) context.Context { return context.WithValue(ctx, controllerNameKey, name) @@ -57,3 +62,16 @@ func WithOptionsOrDie(ctx context.Context, opts ...options.Injectable) context.C } return ctx } + +func WithNodeClasses(ctx context.Context, opts []schema.GroupVersionKind) context.Context { + return context.WithValue(ctx, nodeClassKey, opts) +} + +func GetNodeClasses(ctx context.Context) []schema.GroupVersionKind { + retval := ctx.Value(nodeClassKey) + if retval == nil { + // This is a developer error if this happens, so we should panic + logging.FromContext(ctx).Fatal("nodeclass doesn't exist in context") + } + return retval.([]schema.GroupVersionKind) +} diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index acde5ab8b6..10e696f283 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -36,6 +36,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" crmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" + "sigs.k8s.io/karpenter/pkg/cloudprovider" "sigs.k8s.io/karpenter/pkg/metrics" "github.com/samber/lo" @@ -234,7 +235,7 @@ func (o *Operator) WithWebhooks(ctx context.Context, ctors ...knativeinjection.C return o } -func (o *Operator) Start(ctx context.Context) { +func (o *Operator) Start(ctx context.Context, cp cloudprovider.CloudProvider) { wg := &sync.WaitGroup{} wg.Add(1) go func() { @@ -247,6 +248,8 @@ func (o *Operator) Start(ctx context.Context) { wg.Add(1) go func() { defer wg.Done() + // Taking the first supported NodeClass to be the default NodeClass + ctx = injection.WithNodeClasses(ctx, cp.GetSupportedNodeClasses()) webhooks.Start(ctx, o.GetConfig(), o.webhooks...) }() } diff --git a/pkg/webhooks/webhooks.go b/pkg/webhooks/webhooks.go index 9bc919f060..7fbf2f424d 100644 --- a/pkg/webhooks/webhooks.go +++ b/pkg/webhooks/webhooks.go @@ -24,6 +24,7 @@ import ( "net/http" "strings" + "github.com/awslabs/operatorpkg/object" "github.com/samber/lo" "go.uber.org/zap" "golang.org/x/sync/errgroup" @@ -39,10 +40,13 @@ import ( "knative.dev/pkg/webhook/certificates" "knative.dev/pkg/webhook/configmaps" "knative.dev/pkg/webhook/resourcesemantics" + "knative.dev/pkg/webhook/resourcesemantics/conversion" "knative.dev/pkg/webhook/resourcesemantics/validation" "sigs.k8s.io/controller-runtime/pkg/healthz" + v1 "sigs.k8s.io/karpenter/pkg/apis/v1" "sigs.k8s.io/karpenter/pkg/apis/v1beta1" + "sigs.k8s.io/karpenter/pkg/operator/injection" "sigs.k8s.io/karpenter/pkg/operator/logging" "sigs.k8s.io/karpenter/pkg/operator/options" ) @@ -51,14 +55,35 @@ const component = "webhook" var ( Resources = map[schema.GroupVersionKind]resourcesemantics.GenericCRD{ - {Group: "karpenter.sh", Version: "v1beta1", Kind: "NodePool"}: &v1beta1.NodePool{}, - {Group: "karpenter.sh", Version: "v1beta1", Kind: "NodeClaim"}: &v1beta1.NodeClaim{}, + object.GVK(&v1beta1.NodePool{}): &v1beta1.NodePool{}, + object.GVK(&v1beta1.NodeClaim{}): &v1beta1.NodeClaim{}, + } + // Remove conversion webhooks once v1.1.0, and v1beta1 APIs are dropped + ConversionResource = map[schema.GroupKind]conversion.GroupKindConversion{ + object.GVK(&v1beta1.NodePool{}).GroupKind(): { + DefinitionName: "nodepools.karpenter.sh", + HubVersion: "v1", + Zygotes: map[string]conversion.ConvertibleObject{ + "v1": &v1.NodePool{}, + "v1beta1": &v1beta1.NodePool{}, + }, + }, + object.GVK(&v1beta1.NodeClaim{}).GroupKind(): { + DefinitionName: "nodeclaims.karpenter.sh", + HubVersion: "v1", + Zygotes: map[string]conversion.ConvertibleObject{ + "v1": &v1.NodeClaim{}, + "v1beta1": &v1beta1.NodeClaim{}, + }, + }, } ) func NewWebhooks() []knativeinjection.ControllerConstructor { return []knativeinjection.ControllerConstructor{ certificates.NewController, + NewCRDConversionWebhook, + // Webhook validation is only supported for v1beta1 APIs NewCRDValidationWebhook, NewConfigValidationWebhook, } @@ -74,6 +99,17 @@ func NewCRDValidationWebhook(ctx context.Context, _ configmap.Watcher) *controll ) } +func NewCRDConversionWebhook(ctx context.Context, _ configmap.Watcher) *controller.Impl { + nodeclassCtx := injection.GetNodeClasses(ctx) + return conversion.NewConversionController(ctx, + "/conversion/karpenter.sh", + ConversionResource, + func(ctx context.Context) context.Context { + return injection.WithNodeClasses(ctx, nodeclassCtx) + }, + ) +} + func NewConfigValidationWebhook(ctx context.Context, _ configmap.Watcher) *controller.Impl { return configmaps.NewAdmissionController(ctx, "validation.webhook.config.karpenter.sh",