diff --git a/charts/karpenter-crd/templates/karpenter.k8s.aws_ec2nodeclasses.yaml b/charts/karpenter-crd/templates/karpenter.k8s.aws_ec2nodeclasses.yaml index ff1bc9da2b40..210bdeca1421 100644 --- a/charts/karpenter-crd/templates/karpenter.k8s.aws_ec2nodeclasses.yaml +++ b/charts/karpenter-crd/templates/karpenter.k8s.aws_ec2nodeclasses.yaml @@ -592,6 +592,9 @@ spec: items: description: AMI contains resolved AMI selector values utilized for node launch properties: + deprecated: + description: Deprecation status of the AMI + type: boolean id: description: ID of the AMI type: string diff --git a/pkg/apis/crds/karpenter.k8s.aws_ec2nodeclasses.yaml b/pkg/apis/crds/karpenter.k8s.aws_ec2nodeclasses.yaml index ff1bc9da2b40..210bdeca1421 100644 --- a/pkg/apis/crds/karpenter.k8s.aws_ec2nodeclasses.yaml +++ b/pkg/apis/crds/karpenter.k8s.aws_ec2nodeclasses.yaml @@ -592,6 +592,9 @@ spec: items: description: AMI contains resolved AMI selector values utilized for node launch properties: + deprecated: + description: Deprecation status of the AMI + type: boolean id: description: ID of the AMI type: string diff --git a/pkg/apis/v1/ec2nodeclass_status.go b/pkg/apis/v1/ec2nodeclass_status.go index 89afe2370e8c..f064e879fc97 100644 --- a/pkg/apis/v1/ec2nodeclass_status.go +++ b/pkg/apis/v1/ec2nodeclass_status.go @@ -23,6 +23,7 @@ const ( ConditionTypeSubnetsReady = "SubnetsReady" ConditionTypeSecurityGroupsReady = "SecurityGroupsReady" ConditionTypeAMIsReady = "AMIsReady" + ConditionTypeAMIsDeprecated = "AMIsDeprecated" ConditionTypeInstanceProfileReady = "InstanceProfileReady" ) @@ -54,6 +55,9 @@ type AMI struct { // ID of the AMI // +required ID string `json:"id"` + // Deprecation status of the AMI + // +optional + Deprecated bool `json:"deprecated,omitempty"` // Name of the AMI // +optional Name string `json:"name,omitempty"` diff --git a/pkg/controllers/nodeclass/status/ami.go b/pkg/controllers/nodeclass/status/ami.go index faf94d2dac08..97011237e6c3 100644 --- a/pkg/controllers/nodeclass/status/ami.go +++ b/pkg/controllers/nodeclass/status/ami.go @@ -44,6 +44,10 @@ func (a *AMI) Reconcile(ctx context.Context, nodeClass *v1.EC2NodeClass) (reconc nodeClass.StatusConditions().SetFalse(v1.ConditionTypeAMIsReady, "AMINotFound", "AMISelector did not match any AMIs") return reconcile.Result{}, nil } + // Find if any discovered AMI is deprecated + containsDeprecatedAMIs := lo.SomeBy(amis, func(ami amifamily.AMI) bool { + return ami.Deprecated + }) nodeClass.Status.AMIs = lo.Map(amis, func(ami amifamily.AMI, _ int) v1.AMI { reqs := lo.Map(ami.Requirements.NodeSelectorRequirements(), func(item karpv1.NodeSelectorRequirementWithMinValues, _ int) corev1.NodeSelectorRequirement { return item.NodeSelectorRequirement @@ -58,9 +62,15 @@ func (a *AMI) Reconcile(ctx context.Context, nodeClass *v1.EC2NodeClass) (reconc return v1.AMI{ Name: ami.Name, ID: ami.AmiID, + Deprecated: ami.Deprecated, Requirements: reqs, } }) - nodeClass.StatusConditions().SetTrue(v1.ConditionTypeAMIsReady) + // Set the status condition on the nodeclass if any discovered AMI for the selector terms are deprecated or not + if containsDeprecatedAMIs { + nodeClass.StatusConditions().SetTrue(v1.ConditionTypeAMIsDeprecated) + } else { + nodeClass.StatusConditions().SetTrue(v1.ConditionTypeAMIsReady) + } return reconcile.Result{RequeueAfter: 5 * time.Minute}, nil } diff --git a/pkg/controllers/nodeclass/status/ami_test.go b/pkg/controllers/nodeclass/status/ami_test.go index 74de453016de..960f2c0b78a8 100644 --- a/pkg/controllers/nodeclass/status/ami_test.go +++ b/pkg/controllers/nodeclass/status/ami_test.go @@ -534,4 +534,111 @@ var _ = Describe("NodeClass AMI Status Controller", func() { nodeClass = ExpectExists(ctx, env.Client, nodeClass) Expect(nodeClass.StatusConditions().IsTrue(v1.ConditionTypeAMIsReady)).To(BeFalse()) }) + Context("NodeClass AMI Status", func() { + BeforeEach(func() { + // Set time using the injectable/fake clock to now + awsEnv.Clock.SetTime(time.Now()) + nodeClass.Spec.AMISelectorTerms = []v1.AMISelectorTerm{ + { + Tags: map[string]string{"*": "*"}, + }, + } + awsEnv.EC2API.DescribeImagesOutput.Set(&ec2.DescribeImagesOutput{ + Images: []ec2types.Image{ + { + Name: aws.String("test-ami-3"), + ImageId: aws.String("ami-id-789"), + CreationDate: aws.String("2021-08-31T00:12:42.000Z"), + DeprecationTime: aws.String(awsEnv.Clock.Now().Add(30 * time.Minute).Format(time.RFC3339)), + Architecture: "x86_64", + Tags: []ec2types.Tag{ + {Key: aws.String("Name"), Value: aws.String("test-ami-3")}, + {Key: aws.String("foo"), Value: aws.String("bar")}, + }, + }, + { + Name: aws.String("test-ami-2"), + ImageId: aws.String("ami-id-456"), + CreationDate: aws.String("2021-08-31T00:12:42.000Z"), + Architecture: "arm64", + Tags: []ec2types.Tag{ + {Key: aws.String("Name"), Value: aws.String("test-ami-2")}, + {Key: aws.String("foo"), Value: aws.String("bar")}, + }, + }, + }, + }) + }) + It("should update nodeclass AMI status with correct deprecation value and conditions", func() { + ExpectApplied(ctx, env.Client, nodeClass) + ExpectObjectReconciled(ctx, env.Client, statusController, nodeClass) + nodeClass = ExpectExists(ctx, env.Client, nodeClass) + + Expect(len(nodeClass.Status.AMIs)).To(Equal(2)) + Expect(nodeClass.Status.AMIs).To(Equal( + []v1.AMI{ + { + Name: "test-ami-2", + ID: "ami-id-456", + Requirements: []corev1.NodeSelectorRequirement{ + { + Key: corev1.LabelArchStable, + Operator: corev1.NodeSelectorOpIn, + Values: []string{karpv1.ArchitectureArm64}, + }, + }, + }, + { + Name: "test-ami-3", + ID: "ami-id-789", + Requirements: []corev1.NodeSelectorRequirement{ + { + Key: corev1.LabelArchStable, + Operator: corev1.NodeSelectorOpIn, + Values: []string{karpv1.ArchitectureAmd64}, + }, + }, + }, + }, + )) + Expect(nodeClass.StatusConditions().IsTrue(v1.ConditionTypeAMIsReady)).To(BeTrue()) + + // Increment clock to simulate status updates on deprecated AMIs + awsEnv.Clock.Step(40 * time.Minute) + + // Flush Cache + awsEnv.EC2Cache.Flush() + + ExpectObjectReconciled(ctx, env.Client, statusController, nodeClass) + nodeClass = ExpectExists(ctx, env.Client, nodeClass) + Expect(len(nodeClass.Status.AMIs)).To(Equal(2)) + Expect(nodeClass.Status.AMIs).To(Equal( + []v1.AMI{ + { + Name: "test-ami-2", + ID: "ami-id-456", + Requirements: []corev1.NodeSelectorRequirement{{ + Key: corev1.LabelArchStable, + Operator: corev1.NodeSelectorOpIn, + Values: []string{karpv1.ArchitectureArm64}, + }, + }, + }, + { + Name: "test-ami-3", + ID: "ami-id-789", + // Adds deprecated field to the AMI status on the NodeClass + Deprecated: true, + Requirements: []corev1.NodeSelectorRequirement{{ + Key: corev1.LabelArchStable, + Operator: corev1.NodeSelectorOpIn, + Values: []string{karpv1.ArchitectureAmd64}, + }, + }, + }, + }, + )) + Expect(nodeClass.StatusConditions().IsTrue(v1.ConditionTypeAMIsDeprecated)).To(BeTrue()) + }) + }) }) diff --git a/website/content/en/preview/concepts/nodeclasses.md b/website/content/en/preview/concepts/nodeclasses.md index ce271720d024..c3741f3a50f1 100644 --- a/website/content/en/preview/concepts/nodeclasses.md +++ b/website/content/en/preview/concepts/nodeclasses.md @@ -1464,7 +1464,7 @@ status: ## status.amis -[`status.amis`]({{< ref "#statusamis" >}}) contains the resolved `id`, `name`, and `requirements` of either the default AMIs for the [`spec.amiFamily`]({{< ref "#specamifamily" >}}) or the AMIs selected by the [`spec.amiSelectorTerms`]({{< ref "#specamiselectorterms" >}}) if this field is specified. +[`status.amis`]({{< ref "#statusamis" >}}) contains the resolved `id`, `name`, `requirements`, and the `deprecated` status of either the default AMIs for the [`spec.amiFamily`]({{< ref "#specamifamily" >}}) or the AMIs selected by the [`spec.amiSelectorTerms`]({{< ref "#specamiselectorterms" >}}) if this field is specified. The `deprecated` status will be shown for resolved AMIs that are deprecated. #### Examples @@ -1529,6 +1529,7 @@ status: amis: - id: ami-01234567890123456 name: custom-ami-amd64 + deprecated: true requirements: - key: kubernetes.io/arch operator: In @@ -1565,7 +1566,8 @@ NodeClasses have the following status conditions: | SubnetsReady | Subnets are discovered. | | SecurityGroupsReady | Security Groups are discovered. | | InstanceProfileReady | Instance Profile is discovered. | -| AMIsReady | AMIs are discovered | +| AMIsReady | AMIs are discovered. | +| AMIsDeprecated | AMIs are discovered, but they are deprecated. Individual deprecated AMIs can be identified by reviewing the `status.amis`. | | Ready | Top level condition that indicates if the nodeClass is ready. If any of the underlying conditions is `False` then this condition is set to `False` and `Message` on the condition indicates the dependency that was not resolved. | If a NodeClass is not ready, NodePools that reference it through their `nodeClassRef` will not be considered for scheduling.