diff --git a/pkg/cloudprovider/cloudprovider.go b/pkg/cloudprovider/cloudprovider.go index 8ba4de9..b22738a 100644 --- a/pkg/cloudprovider/cloudprovider.go +++ b/pkg/cloudprovider/cloudprovider.go @@ -23,6 +23,7 @@ import ( "fmt" "log" "slices" + "strconv" "strings" "sync" "time" @@ -611,7 +612,8 @@ func filterCompatibleInstanceTypes(instanceTypes []*ClusterAPIInstanceType, node filteredInstances := lo.Filter(instanceTypes, func(i *ClusterAPIInstanceType, _ int) bool { // TODO (elmiko) if/when we have offering availability, this is a good place to filter out unavailable instance types return reqs.Compatible(i.Requirements, scheduling.AllowUndefinedWellKnownLabels) == nil && - resources.Fits(nodeClaim.Spec.Resources.Requests, i.Allocatable()) + resources.Fits(nodeClaim.Spec.Resources.Requests, i.Allocatable()) && + i.Offerings.Available().HasCompatible(reqs) }) return filteredInstances @@ -631,6 +633,25 @@ func labelsFromScaleFromZeroAnnotation(annotation string) map[string]string { return labels } +// isBelowMaxSize checks if the MachineDeployment's current replicas are below the max size +// defined by the Cluster Autoscaler annotation. If the annotation is not present or cannot +// be parsed, it returns true (no limit enforced). +func isBelowMaxSize(machineDeployment *capiv1beta1.MachineDeployment) bool { + if machineDeployment == nil { + return false + } + maxSizeStr, ok := machineDeployment.Annotations[capiv1beta1.AutoscalerMaxSizeAnnotation] + if !ok { + return true + } + maxSize, err := strconv.Atoi(maxSizeStr) + if err != nil { + return true + } + replicas := int(ptr.Deref(machineDeployment.Spec.Replicas, 0)) + return replicas < maxSize +} + func machineDeploymentToInstanceType(machineDeployment *capiv1beta1.MachineDeployment) *ClusterAPIInstanceType { instanceType := &ClusterAPIInstanceType{} @@ -657,7 +678,7 @@ func machineDeploymentToInstanceType(machineDeployment *capiv1beta1.MachineDeplo &cloudprovider.Offering{ Requirements: scheduling.NewRequirements(requirements...), Price: 0.0, - Available: true, + Available: isBelowMaxSize(machineDeployment), }, } diff --git a/pkg/cloudprovider/cloudprovider_test.go b/pkg/cloudprovider/cloudprovider_test.go index a4fd99d..779221d 100644 --- a/pkg/cloudprovider/cloudprovider_test.go +++ b/pkg/cloudprovider/cloudprovider_test.go @@ -720,6 +720,96 @@ var _ = Describe("machineDeploymentToInstanceType function", func() { }) }) +var _ = Describe("isBelowMaxSize function", func() { + It("returns false when MachineDeployment is nil", func() { + Expect(isBelowMaxSize(nil)).To(BeFalse()) + }) + + It("returns true when no max size annotation is present", func() { + md := newMachineDeployment("md-1", "test-cluster", true) + md.Spec.Replicas = ptr.To(int32(5)) + Expect(isBelowMaxSize(md)).To(BeTrue()) + }) + + It("returns true when replicas are below max size", func() { + md := newMachineDeployment("md-1", "test-cluster", true) + md.Spec.Replicas = ptr.To(int32(3)) + md.Annotations = map[string]string{ + capiv1beta1.AutoscalerMaxSizeAnnotation: "10", + } + Expect(isBelowMaxSize(md)).To(BeTrue()) + }) + + It("returns false when replicas equal max size", func() { + md := newMachineDeployment("md-1", "test-cluster", true) + md.Spec.Replicas = ptr.To(int32(10)) + md.Annotations = map[string]string{ + capiv1beta1.AutoscalerMaxSizeAnnotation: "10", + } + Expect(isBelowMaxSize(md)).To(BeFalse()) + }) + + It("returns false when replicas exceed max size", func() { + md := newMachineDeployment("md-1", "test-cluster", true) + md.Spec.Replicas = ptr.To(int32(12)) + md.Annotations = map[string]string{ + capiv1beta1.AutoscalerMaxSizeAnnotation: "10", + } + Expect(isBelowMaxSize(md)).To(BeFalse()) + }) + + It("returns true when max size annotation is not a valid integer", func() { + md := newMachineDeployment("md-1", "test-cluster", true) + md.Spec.Replicas = ptr.To(int32(5)) + md.Annotations = map[string]string{ + capiv1beta1.AutoscalerMaxSizeAnnotation: "not-a-number", + } + Expect(isBelowMaxSize(md)).To(BeTrue()) + }) + + It("returns true when replicas is nil (defaults to 0)", func() { + md := newMachineDeployment("md-1", "test-cluster", true) + md.Spec.Replicas = nil + md.Annotations = map[string]string{ + capiv1beta1.AutoscalerMaxSizeAnnotation: "10", + } + Expect(isBelowMaxSize(md)).To(BeTrue()) + }) + + It("returns false when max size is 0", func() { + md := newMachineDeployment("md-1", "test-cluster", true) + md.Spec.Replicas = ptr.To(int32(0)) + md.Annotations = map[string]string{ + capiv1beta1.AutoscalerMaxSizeAnnotation: "0", + } + Expect(isBelowMaxSize(md)).To(BeFalse()) + }) +}) + +var _ = Describe("machineDeploymentToInstanceType max size behavior", func() { + It("sets offering Available to false when replicas are at max size", func() { + md := newMachineDeployment("md-1", "test-cluster", true) + md.Spec.Replicas = ptr.To(int32(10)) + md.Annotations = map[string]string{ + capiv1beta1.AutoscalerMaxSizeAnnotation: "10", + } + instanceType := machineDeploymentToInstanceType(md) + Expect(instanceType.Offerings).To(HaveLen(1)) + Expect(instanceType.Offerings[0]).To(HaveField("Available", false)) + }) + + It("sets offering Available to true when replicas are below max size", func() { + md := newMachineDeployment("md-1", "test-cluster", true) + md.Spec.Replicas = ptr.To(int32(3)) + md.Annotations = map[string]string{ + capiv1beta1.AutoscalerMaxSizeAnnotation: "10", + } + instanceType := machineDeploymentToInstanceType(md) + Expect(instanceType.Offerings).To(HaveLen(1)) + Expect(instanceType.Offerings[0]).To(HaveField("Available", true)) + }) +}) + func newMachine(machineName string, clusterName string, karpenterMember bool) *capiv1beta1.Machine { machine := &capiv1beta1.Machine{} machine.SetName(machineName)