Skip to content

Commit

Permalink
chore: Enforce fields are required for v1beta1/EC2NodeClass (aws#4638)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathan-innis authored Sep 19, 2023
1 parent 007e5da commit 7465361
Show file tree
Hide file tree
Showing 8 changed files with 52 additions and 49 deletions.
25 changes: 23 additions & 2 deletions pkg/apis/crds/karpenter.k8s.aws_ec2nodeclasses.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ spec:
properties:
amiFamily:
description: AMIFamily is the AMI family that instances use.
enum:
- AL2
- Bottlerocket
- Ubuntu
- Custom
- Windows2019
- Windows2022
type: string
amiSelectorTerms:
description: AMISelectorTerms is a list of or ami selector terms.
Expand Down Expand Up @@ -152,6 +159,11 @@ spec:
enabled for instances that are launched
type: boolean
metadataOptions:
default:
httpEndpoint: enabled
httpProtocolIPv6: disabled
httpPutResponseHopLimit: 2
httpTokens: required
description: "MetadataOptions for the generated launch template of
provisioned nodes. \n This specifies the exposure of the Instance
Metadata Service to provisioned EC2 nodes. For more information,
Expand All @@ -163,30 +175,34 @@ spec:
disabled, with httpPutResponseLimit of 2, and with httpTokens required."
properties:
httpEndpoint:
default: enabled
description: "HTTPEndpoint enables or disables the HTTP metadata
endpoint on provisioned nodes. If metadata options is non-nil,
but this parameter is not specified, the default state is \"enabled\".
\n If you specify a value of \"disabled\", instance metadata
will not be accessible on the node."
type: string
httpProtocolIPv6:
default: disabled
description: HTTPProtocolIPv6 enables or disables the IPv6 endpoint
for the instance metadata service on provisioned nodes. If metadata
options is non-nil, but this parameter is not specified, the
default state is "disabled".
type: string
httpPutResponseHopLimit:
default: 2
description: HTTPPutResponseHopLimit is the desired HTTP PUT response
hop limit for instance metadata requests. The larger the number,
the further instance metadata requests can travel. Possible
values are integers from 1 to 64. If metadata options is non-nil,
but this parameter is not specified, the default value is 1.
but this parameter is not specified, the default value is 2.
format: int64
type: integer
httpTokens:
default: required
description: "HTTPTokens determines the state of token usage for
instance metadata requests. If metadata options is non-nil,
but this parameter is not specified, the default state is \"optional\".
but this parameter is not specified, the default state is \"required\".
\n If the state is optional, one can choose to retrieve instance
metadata with or without a signed token header on the request.
If one retrieves the IAM role credentials without a token, the
Expand Down Expand Up @@ -260,6 +276,11 @@ spec:
will merge certain fields into this UserData to ensure nodes are
being provisioned with the correct configuration.
type: string
required:
- amiFamily
- role
- securityGroupSelectorTerms
- subnetSelectorTerms
type: object
status:
description: EC2NodeClassStatus contains the resolved state of the EC2NodeClass
Expand Down
22 changes: 14 additions & 8 deletions pkg/apis/v1beta1/ec2nodeclass.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,26 @@ import (
// This will contain configuration necessary to launch instances in AWS.
type EC2NodeClassSpec struct {
// SubnetSelectorTerms is a list of or subnet selector terms. The terms are ORed.
// +optional
// +required
SubnetSelectorTerms []SubnetSelectorTerm `json:"subnetSelectorTerms" hash:"ignore"`
// SecurityGroupSelectorTerms is a list of or security group selector terms. The terms are ORed.
// +optional
// +required
SecurityGroupSelectorTerms []SecurityGroupSelectorTerm `json:"securityGroupSelectorTerms" hash:"ignore"`
// AMISelectorTerms is a list of or ami selector terms. The terms are ORed.
// +optional
AMISelectorTerms []AMISelectorTerm `json:"amiSelectorTerms,omitempty" hash:"ignore"`
// AMIFamily is the AMI family that instances use.
// +optional
AMIFamily *string `json:"amiFamily,omitempty"`
// +kubebuilder:validation:Enum:={AL2,Bottlerocket,Ubuntu,Custom,Windows2019,Windows2022}
// +required
AMIFamily *string `json:"amiFamily"`
// UserData to be applied to the provisioned nodes.
// It must be in the appropriate format based on the AMIFamily in use. Karpenter will merge certain fields into
// this UserData to ensure nodes are being provisioned with the correct configuration.
// +optional
UserData *string `json:"userData,omitempty"`
// Role is the AWS identity that nodes use.
// +optional
Role *string `json:"role,omitempty"`
// +required
Role string `json:"role"`
// Tags to be applied on ec2 resources like instances and launch templates.
// +optional
Tags map[string]string `json:"tags,omitempty"`
Expand All @@ -69,6 +70,7 @@ type EC2NodeClassSpec struct {
// If omitted, defaults to httpEndpoint enabled, with httpProtocolIPv6
// disabled, with httpPutResponseLimit of 2, and with httpTokens
// required.
// +kubebuilder:default={"httpEndpoint":"enabled","httpProtocolIPv6":"disabled","httpPutResponseHopLimit":2,"httpTokens":"required"}
// +optional
MetadataOptions *MetadataOptions `json:"metadataOptions,omitempty"`
// Context is a Reserved field in EC2 APIs
Expand Down Expand Up @@ -161,23 +163,26 @@ type MetadataOptions struct {
//
// If you specify a value of "disabled", instance metadata will not be accessible
// on the node.
// +kubebuilder:default=enabled
// +optional
HTTPEndpoint *string `json:"httpEndpoint,omitempty"`
// HTTPProtocolIPv6 enables or disables the IPv6 endpoint for the instance metadata
// service on provisioned nodes. If metadata options is non-nil, but this parameter
// is not specified, the default state is "disabled".
// +kubebuilder:default=disabled
// +optional
HTTPProtocolIPv6 *string `json:"httpProtocolIPv6,omitempty"`
// HTTPPutResponseHopLimit is the desired HTTP PUT response hop limit for
// instance metadata requests. The larger the number, the further instance
// metadata requests can travel. Possible values are integers from 1 to 64.
// If metadata options is non-nil, but this parameter is not specified, the
// default value is 1.
// default value is 2.
// +kubebuilder:default=2
// +optional
HTTPPutResponseHopLimit *int64 `json:"httpPutResponseHopLimit,omitempty"`
// HTTPTokens determines the state of token usage for instance metadata
// requests. If metadata options is non-nil, but this parameter is not
// specified, the default state is "optional".
// specified, the default state is "required".
//
// If the state is optional, one can choose to retrieve instance metadata with
// or without a signed token header on the request. If one retrieves the IAM
Expand All @@ -189,6 +194,7 @@ type MetadataOptions struct {
// instance metadata retrieval requests. In this state, retrieving the IAM
// role credentials always returns the version 2.0 credentials; the version
// 1.0 credentials are not available.
// +kubebuilder:default=required
// +optional
HTTPTokens *string `json:"httpTokens,omitempty"`
}
Expand Down
14 changes: 1 addition & 13 deletions pkg/apis/v1beta1/ec2nodeclass_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"strings"

"github.com/aws/aws-sdk-go/service/ec2"
"github.com/samber/lo"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/api/resource"
"knative.dev/pkg/apis"
Expand Down Expand Up @@ -64,7 +63,6 @@ func (in *EC2NodeClassSpec) validate(_ context.Context) (errs *apis.FieldError)
in.validateMetadataOptions().ViaField(metadataOptionsPath),
in.validateAMIFamily().ViaField(amiFamilyPath),
in.validateBlockDeviceMappings().ViaField(blockDeviceMappingsPath),
in.validateUserData().ViaField(userDataPath),
in.validateTags().ViaField(tagsPath),
)
}
Expand Down Expand Up @@ -249,24 +247,14 @@ func (in *EC2NodeClassSpec) validateVolumeSize(blockDeviceMapping *BlockDeviceMa
return nil
}

func (in *EC2NodeClassSpec) validateUserData() (errs *apis.FieldError) {
if in.UserData == nil {
return nil
}
if lo.FromPtr(in.AMIFamily) == AMIFamilyWindows2019 || lo.FromPtr(in.AMIFamily) == AMIFamilyWindows2022 {
errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("%s AMIFamily is not currently supported with custom userData", lo.FromPtr(in.AMIFamily)), userDataPath))
}
return errs
}

func (in *EC2NodeClassSpec) validateAMIFamily() (errs *apis.FieldError) {
if in.AMIFamily == nil {
return nil
}
if *in.AMIFamily == AMIFamilyCustom && len(in.AMISelectorTerms) == 0 {
errs = errs.Also(apis.ErrMissingField(amiSelectorTermsPath))
}
return errs.Also(in.validateStringEnum(*in.AMIFamily, amiFamilyPath, SupportedAMIFamilies))
return errs
}

func (in *EC2NodeClassSpec) validateTags() (errs *apis.FieldError) {
Expand Down
18 changes: 3 additions & 15 deletions pkg/apis/v1beta1/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ import (
"github.com/Pallinder/go-randomdata"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "knative.dev/pkg/logging/testing"
"knative.dev/pkg/ptr"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
. "knative.dev/pkg/logging/testing"

"github.com/aws/aws-sdk-go/aws"

Expand Down Expand Up @@ -71,16 +69,6 @@ var _ = Describe("Validation", func() {
It("should succeed if user data is empty", func() {
Expect(nc.Validate(ctx)).To(Succeed())
})
It("should fail if Windows2019 AMIFamily is specified", func() {
nc.Spec.AMIFamily = &v1alpha1.AMIFamilyWindows2019
nc.Spec.UserData = ptr.String("someUserData")
Expect(nc.Validate(ctx)).To(Not(Succeed()))
})
It("should fail if Windows2022 AMIFamily is specified", func() {
nc.Spec.AMIFamily = &v1alpha1.AMIFamilyWindows2022
nc.Spec.UserData = ptr.String("someUserData")
Expect(nc.Validate(ctx)).To(Not(Succeed()))
})
})
Context("Tags", func() {
It("should succeed when tags are empty", func() {
Expand Down Expand Up @@ -471,7 +459,7 @@ var _ = Describe("Validation", func() {
Spec: v1beta1.EC2NodeClassSpec{
AMIFamily: aws.String(v1alpha1.AMIFamilyAL2),
Context: aws.String("context-1"),
Role: aws.String("role-1"),
Role: "role-1",
Tags: map[string]string{
"keyTag-1": "valueTag-1",
"keyTag-2": "valueTag-2",
Expand All @@ -498,7 +486,7 @@ var _ = Describe("Validation", func() {
updatedHash := nodeClass.Hash()
Expect(hash).ToNot(Equal(updatedHash))
},
Entry("InstanceProfile Drift", v1beta1.EC2NodeClassSpec{Role: aws.String("role-2")}),
Entry("InstanceProfile Drift", v1beta1.EC2NodeClassSpec{Role: "role-2"}),
Entry("UserData Drift", v1beta1.EC2NodeClassSpec{UserData: aws.String("userdata-test-2")}),
Entry("Tags Drift", v1beta1.EC2NodeClassSpec{Tags: map[string]string{"keyTag-test-3": "valueTag-test-3"}}),
Entry("MetadataOptions Drift", v1beta1.EC2NodeClassSpec{MetadataOptions: &v1beta1.MetadataOptions{HTTPEndpoint: aws.String("test-metadata-2")}}),
Expand Down
5 changes: 0 additions & 5 deletions pkg/apis/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/controllers/nodeclass/nodeclass_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@ var _ = Describe("NodeClassController", func() {
},
Entry("AMIFamily Drift", &v1beta1.EC2NodeClass{Spec: v1beta1.EC2NodeClassSpec{AMIFamily: aws.String(v1beta1.AMIFamilyBottlerocket)}}),
Entry("UserData Drift", &v1beta1.EC2NodeClass{Spec: v1beta1.EC2NodeClassSpec{UserData: aws.String("userdata-test-2")}}),
Entry("Role Drift", &v1beta1.EC2NodeClass{Spec: v1beta1.EC2NodeClassSpec{Role: aws.String("new-role")}}),
Entry("Role Drift", &v1beta1.EC2NodeClass{Spec: v1beta1.EC2NodeClassSpec{Role: "new-role"}}),
Entry("Tags Drift", &v1beta1.EC2NodeClass{Spec: v1beta1.EC2NodeClassSpec{Tags: map[string]string{"keyTag-test-3": "valueTag-test-3"}}}),
Entry("BlockDeviceMappings Drift", &v1beta1.EC2NodeClass{Spec: v1beta1.EC2NodeClassSpec{BlockDeviceMappings: []*v1beta1.BlockDeviceMapping{{DeviceName: aws.String("map-device-test-3")}}}}),
Entry("DetailedMonitoring Drift", &v1beta1.EC2NodeClass{Spec: v1beta1.EC2NodeClassSpec{DetailedMonitoring: aws.Bool(true)}}),
Expand Down
7 changes: 6 additions & 1 deletion pkg/test/nodeclass.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ func EC2NodeClass(overrides ...v1beta1.EC2NodeClass) *v1beta1.EC2NodeClass {
panic(fmt.Sprintf("Failed to merge settings: %s", err))
}
}
if options.Spec.AMIFamily == nil {
options.Spec.AMIFamily = &v1beta1.AMIFamilyAL2
}
if options.Spec.Role == "" {
options.Spec.Role = "test-role"
}
if len(options.Spec.SecurityGroupSelectorTerms) == 0 {
options.Spec.SecurityGroupSelectorTerms = []v1beta1.SecurityGroupSelectorTerm{
{
Expand All @@ -39,7 +45,6 @@ func EC2NodeClass(overrides ...v1beta1.EC2NodeClass) *v1beta1.EC2NodeClass {
},
}
}

if len(options.Spec.SubnetSelectorTerms) == 0 {
options.Spec.SubnetSelectorTerms = []v1beta1.SubnetSelectorTerm{
{
Expand Down
8 changes: 4 additions & 4 deletions pkg/utils/nodeclass/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ var _ = Describe("NodeClassUtils", func() {
Expect(nodeClass.Spec.AMISelectorTerms[0].Tags).To(Equal(nodeTemplate.Spec.AMISelector))
Expect(nodeClass.Spec.AMIFamily).To(Equal(nodeTemplate.Spec.AMIFamily))
Expect(nodeClass.Spec.UserData).To(Equal(nodeTemplate.Spec.UserData))
Expect(nodeClass.Spec.Role).To(BeNil())
Expect(nodeClass.Spec.Role).To(BeEmpty())
Expect(nodeClass.Spec.Tags).To(Equal(nodeTemplate.Spec.Tags))
ExpectBlockDeviceMappingsEqual(nodeTemplate.Spec.BlockDeviceMappings, nodeClass.Spec.BlockDeviceMappings)
Expect(nodeClass.Spec.DetailedMonitoring).To(Equal(nodeTemplate.Spec.DetailedMonitoring))
Expand Down Expand Up @@ -234,7 +234,7 @@ var _ = Describe("NodeClassUtils", func() {

Expect(nodeClass.Spec.AMIFamily).To(Equal(nodeTemplate.Spec.AMIFamily))
Expect(nodeClass.Spec.UserData).To(Equal(nodeTemplate.Spec.UserData))
Expect(nodeClass.Spec.Role).To(BeNil())
Expect(nodeClass.Spec.Role).To(BeEmpty())
Expect(nodeClass.Spec.Tags).To(Equal(nodeTemplate.Spec.Tags))
ExpectBlockDeviceMappingsEqual(nodeTemplate.Spec.BlockDeviceMappings, nodeClass.Spec.BlockDeviceMappings)
Expect(nodeClass.Spec.DetailedMonitoring).To(Equal(nodeTemplate.Spec.DetailedMonitoring))
Expand Down Expand Up @@ -283,7 +283,7 @@ var _ = Describe("NodeClassUtils", func() {

Expect(nodeClass.Spec.AMIFamily).To(Equal(nodeTemplate.Spec.AMIFamily))
Expect(nodeClass.Spec.UserData).To(Equal(nodeTemplate.Spec.UserData))
Expect(nodeClass.Spec.Role).To(BeNil())
Expect(nodeClass.Spec.Role).To(BeEmpty())
Expect(nodeClass.Spec.Tags).To(Equal(nodeTemplate.Spec.Tags))
ExpectBlockDeviceMappingsEqual(nodeTemplate.Spec.BlockDeviceMappings, nodeClass.Spec.BlockDeviceMappings)
Expect(nodeClass.Spec.DetailedMonitoring).To(Equal(nodeTemplate.Spec.DetailedMonitoring))
Expand Down Expand Up @@ -397,7 +397,7 @@ var _ = Describe("NodeClassUtils", func() {

Expect(nodeClass.Spec.AMIFamily).To(Equal(nodeTemplate.Spec.AMIFamily))
Expect(nodeClass.Spec.UserData).To(Equal(nodeTemplate.Spec.UserData))
Expect(nodeClass.Spec.Role).To(BeNil())
Expect(nodeClass.Spec.Role).To(BeEmpty())
Expect(nodeClass.Spec.Tags).To(Equal(nodeTemplate.Spec.Tags))
ExpectBlockDeviceMappingsEqual(nodeTemplate.Spec.BlockDeviceMappings, nodeClass.Spec.BlockDeviceMappings)
Expect(nodeClass.Spec.DetailedMonitoring).To(Equal(nodeTemplate.Spec.DetailedMonitoring))
Expand Down

0 comments on commit 7465361

Please sign in to comment.