Skip to content

Commit eb53e9b

Browse files
feat: AL2023 AMI family support (#5604)
Co-authored-by: Jonathan Innis <[email protected]>
1 parent 1a8359c commit eb53e9b

34 files changed

+1137
-11
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ vulncheck: ## Verify code vulnerabilities
119119
@govulncheck ./pkg/...
120120

121121
licenses: download ## Verifies dependency licenses
122-
! go-licenses csv ./... | grep -v -e 'MIT' -e 'Apache-2.0' -e 'BSD-3-Clause' -e 'BSD-2-Clause' -e 'ISC' -e 'MPL-2.0'
122+
# TODO: remove nodeadm check once license is updated
123+
! go-licenses csv ./... | grep -v -e 'MIT' -e 'Apache-2.0' -e 'BSD-3-Clause' -e 'BSD-2-Clause' -e 'ISC' -e 'MPL-2.0' -e 'github.com/awslabs/amazon-eks-ami/nodeadm'
123124

124125
image: ## Build the Karpenter controller images using ko build
125126
$(eval CONTROLLER_IMG=$(shell $(WITH_GOFLAGS) KOCACHE=$(KOCACHE) KO_DOCKER_REPO="$(KO_DOCKER_REPO)" ko build --bare github.com/aws/karpenter-provider-aws/cmd/controller))

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/PuerkitoBio/goquery v1.9.0
88
github.com/aws/aws-sdk-go v1.50.25
99
github.com/aws/karpenter-provider-aws/tools/kompat v0.0.0-20231207011214-752356948623
10+
github.com/awslabs/amazon-eks-ami/nodeadm v0.0.0-20240226194241-7da3d23779cf
1011
github.com/go-logr/zapr v1.3.0
1112
github.com/imdario/mergo v0.3.16
1213
github.com/mitchellh/hashstructure/v2 v2.0.2
@@ -24,10 +25,11 @@ require (
2425
k8s.io/apiextensions-apiserver v0.29.2
2526
k8s.io/apimachinery v0.29.2
2627
k8s.io/client-go v0.29.2
27-
k8s.io/utils v0.0.0-20230726121419-3b25d923346b
28+
k8s.io/utils v0.0.0-20240102154912-e7106e64919e
2829
knative.dev/pkg v0.0.0-20231010144348-ca8c009405dd
2930
sigs.k8s.io/controller-runtime v0.17.2
3031
sigs.k8s.io/karpenter v0.34.1-0.20240227070119-01b8127e7068
32+
sigs.k8s.io/yaml v1.4.0
3133
)
3234

3335
require (
@@ -113,5 +115,4 @@ require (
113115
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
114116
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
115117
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
116-
sigs.k8s.io/yaml v1.4.0 // indirect
117118
)

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ github.com/aws/aws-sdk-go v1.50.25 h1:vhiHtLYybv1Nhx3Kv18BBC6L0aPJHaG9aeEsr92W99
5858
github.com/aws/aws-sdk-go v1.50.25/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
5959
github.com/aws/karpenter-provider-aws/tools/kompat v0.0.0-20231207011214-752356948623 h1:DQEFtmPyyMVHOyqva+DaWR6iAQG4h0KJbpSJAYlsnEo=
6060
github.com/aws/karpenter-provider-aws/tools/kompat v0.0.0-20231207011214-752356948623/go.mod h1:fpKKbSoh7nKrbAw8V44Ov1sgosfUvR1ZtyN9k44zHfY=
61+
github.com/awslabs/amazon-eks-ami/nodeadm v0.0.0-20240226194241-7da3d23779cf h1:RH5DuK+nbRBD4S2c0kSXUhNiH4wjkDlOWosra3pBq3k=
62+
github.com/awslabs/amazon-eks-ami/nodeadm v0.0.0-20240226194241-7da3d23779cf/go.mod h1:9NafTAUHL0FlMeL6Cu5PXnMZ1q/LnC9X2emLXHsVbM8=
6163
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
6264
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
6365
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -746,8 +748,8 @@ k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
746748
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
747749
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
748750
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
749-
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
750-
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
751+
k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ=
752+
k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
751753
knative.dev/pkg v0.0.0-20231010144348-ca8c009405dd h1:KJXBX9dOmRTUWduHg1gnWtPGIEl+GMh8UHdrBEZgOXE=
752754
knative.dev/pkg v0.0.0-20231010144348-ca8c009405dd/go.mod h1:36cYnaOVHkzmhgybmYX6zDaTl3PakFeJQJl7wi6/RLE=
753755
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

pkg/apis/crds/karpenter.k8s.aws_ec2nodeclasses.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ spec:
5050
description: AMIFamily is the AMI family that instances use.
5151
enum:
5252
- AL2
53+
- AL2023
5354
- Bottlerocket
5455
- Ubuntu
5556
- Custom

pkg/apis/v1beta1/ec2nodeclass.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ type EC2NodeClassSpec struct {
5151
// +optional
5252
AMISelectorTerms []AMISelectorTerm `json:"amiSelectorTerms,omitempty" hash:"ignore"`
5353
// AMIFamily is the AMI family that instances use.
54-
// +kubebuilder:validation:Enum:={AL2,Bottlerocket,Ubuntu,Custom,Windows2019,Windows2022}
54+
// +kubebuilder:validation:Enum:={AL2,AL2023,Bottlerocket,Ubuntu,Custom,Windows2019,Windows2022}
5555
// +required
5656
AMIFamily *string `json:"amiFamily"`
5757
// UserData to be applied to the provisioned nodes.

pkg/apis/v1beta1/labels.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ var (
7575
}
7676
AMIFamilyBottlerocket = "Bottlerocket"
7777
AMIFamilyAL2 = "AL2"
78+
AMIFamilyAL2023 = "AL2023"
7879
AMIFamilyUbuntu = "Ubuntu"
7980
AMIFamilyWindows2019 = "Windows2019"
8081
AMIFamilyWindows2022 = "Windows2022"

pkg/controllers/nodeclass/controller.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ func (c *Controller) Reconcile(ctx context.Context, nodeClass *v1beta1.EC2NodeCl
8787
c.resolveAMIs(ctx, nodeClass),
8888
c.resolveInstanceProfile(ctx, nodeClass),
8989
)
90+
if lo.FromPtr(nodeClass.Spec.AMIFamily) == v1beta1.AMIFamilyAL2023 {
91+
if cidrErr := c.launchTemplateProvider.ResolveClusterCIDR(ctx); err != nil {
92+
err = multierr.Append(err, fmt.Errorf("resolving cluster CIDR, %w", cidrErr))
93+
}
94+
}
9095
if !equality.Semantic.DeepEqual(stored, nodeClass) {
9196
statusCopy := nodeClass.DeepCopy()
9297
if patchErr := c.kubeClient.Patch(ctx, nodeClass, client.MergeFrom(stored)); err != nil {

pkg/controllers/nodeclass/suite_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929

3030
"github.com/aws/aws-sdk-go/aws"
3131
"github.com/aws/aws-sdk-go/service/ec2"
32+
"github.com/aws/aws-sdk-go/service/eks"
3233
"github.com/aws/aws-sdk-go/service/iam"
3334
corev1beta1 "sigs.k8s.io/karpenter/pkg/apis/v1beta1"
3435
"sigs.k8s.io/karpenter/pkg/events"
@@ -107,6 +108,46 @@ var _ = Describe("NodeClassController", func() {
107108
},
108109
})
109110
})
111+
Context("Cluster CIDR Resolution", func() {
112+
BeforeEach(func() {
113+
// Cluster CIDR will only be resolved once per lifetime of the launch template provider, reset to nil between tests
114+
awsEnv.LaunchTemplateProvider.ClusterCIDR.Store(nil)
115+
})
116+
It("shouldn't resolve cluster CIDR for non-AL2023 NodeClasses", func() {
117+
for _, family := range []string{
118+
v1beta1.AMIFamilyAL2,
119+
v1beta1.AMIFamilyBottlerocket,
120+
v1beta1.AMIFamilyUbuntu,
121+
v1beta1.AMIFamilyWindows2019,
122+
v1beta1.AMIFamilyWindows2022,
123+
v1beta1.AMIFamilyCustom,
124+
} {
125+
nodeClass.Spec.AMIFamily = lo.ToPtr(family)
126+
ExpectApplied(ctx, env.Client, nodeClass)
127+
ExpectReconcileSucceeded(ctx, nodeClassController, client.ObjectKeyFromObject(nodeClass))
128+
Expect(awsEnv.LaunchTemplateProvider.ClusterCIDR.Load()).To(BeNil())
129+
}
130+
})
131+
It("should resolve cluster CIDR for IPv4 clusters", func() {
132+
nodeClass.Spec.AMIFamily = lo.ToPtr(v1beta1.AMIFamilyAL2023)
133+
ExpectApplied(ctx, env.Client, nodeClass)
134+
ExpectReconcileSucceeded(ctx, nodeClassController, client.ObjectKeyFromObject(nodeClass))
135+
Expect(lo.FromPtr(awsEnv.LaunchTemplateProvider.ClusterCIDR.Load())).To(Equal("10.100.0.0/16"))
136+
})
137+
It("should resolve cluster CIDR for IPv6 clusters", func() {
138+
awsEnv.EKSAPI.DescribeClusterBehavior.Output.Set(&eks.DescribeClusterOutput{
139+
Cluster: &eks.Cluster{
140+
KubernetesNetworkConfig: &eks.KubernetesNetworkConfigResponse{
141+
ServiceIpv6Cidr: lo.ToPtr("2001:db8::/64"),
142+
},
143+
},
144+
})
145+
nodeClass.Spec.AMIFamily = lo.ToPtr(v1beta1.AMIFamilyAL2023)
146+
ExpectApplied(ctx, env.Client, nodeClass)
147+
ExpectReconcileSucceeded(ctx, nodeClassController, client.ObjectKeyFromObject(nodeClass))
148+
Expect(lo.FromPtr(awsEnv.LaunchTemplateProvider.ClusterCIDR.Load())).To(Equal("2001:db8::/64"))
149+
})
150+
})
110151
Context("Subnet Status", func() {
111152
It("Should update EC2NodeClass status for Subnets", func() {
112153
ExpectApplied(ctx, env.Client, nodeClass)

pkg/fake/eksapi.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/aws/aws-sdk-go/aws/request"
2121
"github.com/aws/aws-sdk-go/service/eks"
2222
"github.com/aws/aws-sdk-go/service/eks/eksiface"
23+
"github.com/samber/lo"
2324
)
2425

2526
const ()
@@ -35,6 +36,10 @@ type EKSAPI struct {
3536
EKSAPIBehavior
3637
}
3738

39+
func NewEKSAPI() *EKSAPI {
40+
return &EKSAPI{}
41+
}
42+
3843
// Reset must be called between tests otherwise tests will pollute
3944
// each other.
4045
func (s *EKSAPI) Reset() {
@@ -43,6 +48,12 @@ func (s *EKSAPI) Reset() {
4348

4449
func (s *EKSAPI) DescribeClusterWithContext(_ context.Context, input *eks.DescribeClusterInput, _ ...request.Option) (*eks.DescribeClusterOutput, error) {
4550
return s.DescribeClusterBehavior.Invoke(input, func(*eks.DescribeClusterInput) (*eks.DescribeClusterOutput, error) {
46-
return nil, nil
51+
return &eks.DescribeClusterOutput{
52+
Cluster: &eks.Cluster{
53+
KubernetesNetworkConfig: &eks.KubernetesNetworkConfigResponse{
54+
ServiceIpv4Cidr: lo.ToPtr("10.100.0.0/16"),
55+
},
56+
},
57+
}, nil
4758
})
4859
}

pkg/operator/operator.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ func NewOperator(ctx context.Context, operator *operator.Operator) (context.Cont
149149
ctx,
150150
cache.New(awscache.DefaultTTL, awscache.DefaultCleanupInterval),
151151
ec2api,
152+
eks.New(sess),
152153
amiResolver,
153154
securityGroupProvider,
154155
subnetProvider,

pkg/providers/amifamily/al2023.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
Licensed under the Apache License, Version 2.0 (the "License");
3+
you may not use this file except in compliance with the License.
4+
You may obtain a copy of the License at
5+
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License.
13+
*/
14+
15+
package amifamily
16+
17+
import (
18+
"fmt"
19+
20+
"github.com/samber/lo"
21+
v1 "k8s.io/api/core/v1"
22+
corev1beta1 "sigs.k8s.io/karpenter/pkg/apis/v1beta1"
23+
"sigs.k8s.io/karpenter/pkg/cloudprovider"
24+
"sigs.k8s.io/karpenter/pkg/scheduling"
25+
26+
"github.com/aws/karpenter-provider-aws/pkg/apis/v1beta1"
27+
"github.com/aws/karpenter-provider-aws/pkg/providers/amifamily/bootstrap"
28+
)
29+
30+
type AL2023 struct {
31+
DefaultFamily
32+
*Options
33+
}
34+
35+
func (a AL2023) DefaultAMIs(version string) []DefaultAMIOutput {
36+
return []DefaultAMIOutput{
37+
{
38+
Query: fmt.Sprintf("/aws/service/eks/optimized-ami/%s/amazon-linux-2023/x86_64/standard/recommended/image_id", version),
39+
Requirements: scheduling.NewRequirements(
40+
scheduling.NewRequirement(v1.LabelArchStable, v1.NodeSelectorOpIn, corev1beta1.ArchitectureAmd64),
41+
),
42+
},
43+
{
44+
Query: fmt.Sprintf("/aws/service/eks/optimized-ami/%s/amazon-linux-2023/arm64/standard/recommended/image_id", version),
45+
Requirements: scheduling.NewRequirements(
46+
scheduling.NewRequirement(v1.LabelArchStable, v1.NodeSelectorOpIn, corev1beta1.ArchitectureArm64),
47+
),
48+
},
49+
}
50+
}
51+
52+
func (a AL2023) UserData(kubeletConfig *corev1beta1.KubeletConfiguration, taints []v1.Taint, labels map[string]string, caBundle *string, _ []*cloudprovider.InstanceType, customUserData *string, instanceStorePolicy *v1beta1.InstanceStorePolicy) bootstrap.Bootstrapper {
53+
return bootstrap.Nodeadm{
54+
Options: bootstrap.Options{
55+
ClusterName: a.Options.ClusterName,
56+
ClusterEndpoint: a.Options.ClusterEndpoint,
57+
ClusterCIDR: a.Options.ClusterCIDR,
58+
KubeletConfig: kubeletConfig,
59+
Taints: taints,
60+
Labels: labels,
61+
CABundle: caBundle,
62+
AWSENILimitedPodDensity: false,
63+
CustomUserData: customUserData,
64+
InstanceStorePolicy: instanceStorePolicy,
65+
},
66+
}
67+
}
68+
69+
// DefaultBlockDeviceMappings returns the default block device mappings for the AMI Family
70+
func (a AL2023) DefaultBlockDeviceMappings() []*v1beta1.BlockDeviceMapping {
71+
return []*v1beta1.BlockDeviceMapping{{
72+
DeviceName: a.EphemeralBlockDevice(),
73+
EBS: &DefaultEBS,
74+
}}
75+
}
76+
77+
func (a AL2023) EphemeralBlockDevice() *string {
78+
return lo.ToPtr("/dev/xvda")
79+
}

pkg/providers/amifamily/bootstrap/bootstrap.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
type Options struct {
3535
ClusterName string
3636
ClusterEndpoint string
37+
ClusterCIDR *string
3738
KubeletConfig *corev1beta1.KubeletConfiguration
3839
Taints []core.Taint `hash:"set"`
3940
Labels map[string]string `hash:"set"`
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
Licensed under the Apache License, Version 2.0 (the "License");
3+
you may not use this file except in compliance with the License.
4+
You may obtain a copy of the License at
5+
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License.
13+
*/
14+
15+
package mime
16+
17+
import (
18+
"bytes"
19+
"encoding/base64"
20+
"errors"
21+
"fmt"
22+
"io"
23+
"mime"
24+
"mime/multipart"
25+
"net/mail"
26+
"net/textproto"
27+
"strings"
28+
29+
admapi "github.com/awslabs/amazon-eks-ami/nodeadm/api"
30+
)
31+
32+
type ContentType string
33+
34+
const (
35+
boundary = "//"
36+
versionHeader = "MIME-Version: 1.0"
37+
38+
ContentTypeShellScript ContentType = `text/x-shellscript; charset="us-ascii"`
39+
ContentTypeNodeConfig ContentType = "application/" + admapi.GroupName
40+
ContentTypeMultipart ContentType = `multipart/mixed; boundary="` + boundary + `"`
41+
)
42+
43+
type Entry struct {
44+
ContentType ContentType
45+
Content string
46+
}
47+
48+
type Archive []Entry
49+
50+
func NewArchive(content string) (Archive, error) {
51+
archive := Archive{}
52+
if content == "" {
53+
return archive, nil
54+
}
55+
reader, err := archive.getReader(content)
56+
if err != nil {
57+
return nil, err
58+
}
59+
for {
60+
p, err := reader.NextPart()
61+
if err != nil {
62+
if errors.Is(err, io.EOF) {
63+
break
64+
}
65+
return nil, fmt.Errorf("parsing content, %w", err)
66+
}
67+
slurp, err := io.ReadAll(p)
68+
if err != nil {
69+
return nil, fmt.Errorf("parsing content, %s, %w", string(slurp), err)
70+
}
71+
archive = append(archive, Entry{
72+
ContentType: ContentType(p.Header.Get("Content-Type")),
73+
Content: string(slurp),
74+
})
75+
}
76+
return archive, nil
77+
}
78+
79+
// Serialize returns a base64 encoded serialized MIME multi-part archive
80+
func (ma Archive) Serialize() (string, error) {
81+
buffer := bytes.Buffer{}
82+
writer := multipart.NewWriter(&buffer)
83+
if err := writer.SetBoundary(boundary); err != nil {
84+
return "", err
85+
}
86+
buffer.WriteString(versionHeader + "\n")
87+
buffer.WriteString(fmt.Sprintf("Content-Type: %s\n\n", ContentTypeMultipart))
88+
for _, entry := range ma {
89+
partWriter, err := writer.CreatePart(textproto.MIMEHeader{
90+
"Content-Type": []string{string(entry.ContentType)},
91+
})
92+
if err != nil {
93+
return "", fmt.Errorf("creating multi-part section for entry, %w", err)
94+
}
95+
_, err = partWriter.Write([]byte(entry.Content))
96+
if err != nil {
97+
return "", fmt.Errorf("writing entry, %w", err)
98+
}
99+
}
100+
if err := writer.Close(); err != nil {
101+
return "", fmt.Errorf("terminating multi-part archive, %w", err)
102+
}
103+
// The mime/multipart package adds carriage returns, while the rest of our logic does not. Remove all
104+
// carriage returns for consistency.
105+
return base64.StdEncoding.EncodeToString([]byte(strings.ReplaceAll(buffer.String(), "\r", ""))), nil
106+
}
107+
108+
func (Archive) getReader(content string) (*multipart.Reader, error) {
109+
mailMsg, err := mail.ReadMessage(strings.NewReader(content))
110+
if err != nil {
111+
return nil, err
112+
}
113+
mediaType, params, err := mime.ParseMediaType(mailMsg.Header.Get("Content-Type"))
114+
if err != nil {
115+
return nil, fmt.Errorf("archive doesn't have Content-Type header, %w", err)
116+
}
117+
if !strings.HasPrefix(mediaType, "multipart/") {
118+
return nil, fmt.Errorf("archive is not in multipart format, %w", err)
119+
}
120+
return multipart.NewReader(mailMsg.Body, params["boundary"]), nil
121+
}

0 commit comments

Comments
 (0)