Skip to content

Commit b4c28ca

Browse files
authored
Merge pull request #221 from daimaxiaxie/enhance-userdata
Enhance userdata to support mime
2 parents e92dee8 + d5fa475 commit b4c28ca

File tree

283 files changed

+202494
-49
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

283 files changed

+202494
-49
lines changed

charts/karpenter/crds/karpenter.k8s.alibabacloud_ecsnodeclasses.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ spec:
9090
x-kubernetes-validations:
9191
- message: '''alias'' is improperly formatted, must match the
9292
format ''family'''
93-
rule: self.matches('^[a-zA-Z0-9]*$')
93+
rule: self.matches('^[a-zA-Z0-9]+@.+$')
9494
- message: 'family is not supported, must be one of the following:
9595
''AlibabaCloudLinux3,ContainerOS'''
9696
rule: self.find('^[^@]+') in ['AlibabaCloudLinux3', 'ContainerOS']

go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ require (
99
github.com/alibabacloud-go/tea v1.2.2
1010
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
1111
github.com/alibabacloud-go/vpc-20160428/v6 v6.11.2
12+
github.com/aws/karpenter-provider-aws v1.2.1
1213
github.com/awslabs/operatorpkg v0.0.0-20250121140423-041752c305f4
1314
github.com/cloudpilot-ai/priceserver v0.0.0-20241011010411-15ac0e19a857
1415
github.com/mitchellh/hashstructure/v2 v2.0.2
16+
github.com/onsi/ginkgo/v2 v2.22.2
17+
github.com/onsi/gomega v1.36.2
1518
github.com/patrickmn/go-cache v2.1.0+incompatible
1619
github.com/samber/lo v1.49.1
1720
github.com/stretchr/testify v1.10.0
@@ -27,10 +30,14 @@ require (
2730
)
2831

2932
require (
33+
github.com/awslabs/amazon-eks-ami/nodeadm v0.0.0-20240229193347-cfab22a10647 // indirect
3034
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
35+
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
3136
github.com/google/btree v1.1.3 // indirect
37+
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
3238
github.com/klauspost/compress v1.17.11 // indirect
3339
github.com/x448/float16 v0.8.4 // indirect
40+
golang.org/x/tools v0.29.0 // indirect
3441
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
3542
)
3643

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ github.com/aliyun/credentials-go v1.4.3 h1:N3iHyvHRMyOwY1+0qBLSf3hb5JFiOujVSVuEp
5858
github.com/aliyun/credentials-go v1.4.3/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
5959
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
6060
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
61+
github.com/aws/karpenter-provider-aws v1.2.1 h1:JPZ4EPM9iJ+lga6zDpsrHZuhojgKbrgWPqnwTFE3gNw=
62+
github.com/aws/karpenter-provider-aws v1.2.1/go.mod h1:ojjedpNzBJ0uKJ0Ge8/spDLbXWHIKGDbk8MUWh6pklE=
63+
github.com/awslabs/amazon-eks-ami/nodeadm v0.0.0-20240229193347-cfab22a10647 h1:8yRBVsjGmI7qQsPWtIrbWP+XfwHO9Wq7gdLVzjqiZFs=
64+
github.com/awslabs/amazon-eks-ami/nodeadm v0.0.0-20240229193347-cfab22a10647/go.mod h1:9NafTAUHL0FlMeL6Cu5PXnMZ1q/LnC9X2emLXHsVbM8=
6165
github.com/awslabs/operatorpkg v0.0.0-20250121140423-041752c305f4 h1:o5eK/Sh2Ndkv1xOw0Y3Wd9pIoFum8dvNY8MBYbFD9Rw=
6266
github.com/awslabs/operatorpkg v0.0.0-20250121140423-041752c305f4/go.mod h1:ajddvYQzaxpfjeVxLa02iedZnExetLxx45m3S86Dhj0=
6367
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=

pkg/apis/v1alpha1/ecsnodeclass.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ type ImageSelectorTerm struct {
153153
// Valid families include: AlibabaCloudLinux3,ContainerOS
154154
// Currently only supports version pinning to the latest image release, with that images version format (ex: "aliyun3@latest").
155155
// Setting the version to latest will result in drift when a new Image is released. This is **not** recommended for production environments.
156-
// +kubebuilder:validation:XValidation:message="'alias' is improperly formatted, must match the format 'family'",rule="self.matches('^[a-zA-Z0-9]*$')"
156+
// +kubebuilder:validation:XValidation:message="'alias' is improperly formatted, must match the format 'family'",rule="self.matches('^[a-zA-Z0-9]+@.+$')"
157157
// +kubebuilder:validation:XValidation:message="family is not supported, must be one of the following: 'AlibabaCloudLinux3,ContainerOS'",rule="self.find('^[^@]+') in ['AlibabaCloudLinux3', 'ContainerOS']"
158158
// +kubebuilder:validation:MaxLength=30
159159
// +optional

pkg/providers/cluster/ackmanaged.go

Lines changed: 37 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,40 @@ func (a *ACKManaged) UserData(ctx context.Context,
143143
taints []corev1.Taint,
144144
kubeletCfg *v1alpha1.KubeletConfiguration,
145145
userData *string) (string, error) {
146+
147+
attach, err := a.getClusterAttachScripts(ctx)
148+
if err != nil {
149+
return "", err
150+
}
151+
ackScript := a.ackBootstrap(attach, labels, taints, kubeletCfg)
152+
cloudInit := NewCloudInit()
153+
154+
if err := cloudInit.Merge(&ackScript); err != nil {
155+
return "", err
156+
}
157+
if err := cloudInit.Merge(userData); err != nil {
158+
return "", err
159+
}
160+
161+
return cloudInit.Script()
162+
}
163+
164+
func (a *ACKManaged) FeatureFlags() FeatureFlags {
165+
if cni, err := a.GetClusterCNI(context.TODO()); err == nil && cni == ClusterCNITypeFlannel {
166+
return FeatureFlags{
167+
PodsPerCoreEnabled: false,
168+
SupportsENILimitedPodDensity: false,
169+
}
170+
}
171+
return FeatureFlags{
172+
PodsPerCoreEnabled: true,
173+
SupportsENILimitedPodDensity: true,
174+
}
175+
}
176+
177+
func (a *ACKManaged) getClusterAttachScripts(ctx context.Context) (string, error) {
146178
if cachedScript, ok := a.cache.Get(a.clusterID); ok {
147-
return a.resolveUserData(cachedScript.(string), labels, taints, kubeletCfg, userData), nil
179+
return cachedScript.(string), nil
148180
}
149181

150182
reqPara := &ackclient.DescribeClusterAttachScriptsRequest{
@@ -174,20 +206,7 @@ func (a *ACKManaged) UserData(ctx context.Context,
174206
}
175207

176208
a.cache.SetDefault(a.clusterID, respStr)
177-
return a.resolveUserData(respStr, labels, taints, kubeletCfg, userData), nil
178-
}
179-
180-
func (a *ACKManaged) FeatureFlags() FeatureFlags {
181-
if cni, err := a.GetClusterCNI(context.TODO()); err == nil && cni == ClusterCNITypeFlannel {
182-
return FeatureFlags{
183-
PodsPerCoreEnabled: false,
184-
SupportsENILimitedPodDensity: false,
185-
}
186-
}
187-
return FeatureFlags{
188-
PodsPerCoreEnabled: true,
189-
SupportsENILimitedPodDensity: true,
190-
}
209+
return respStr, nil
191210
}
192211

193212
// We need to manually retrieve the runtime configuration of the nodepool, with the default node pool prioritized.
@@ -239,21 +258,13 @@ func (a *ACKManaged) getClusterAttachRuntimeConfiguration(ctx context.Context) (
239258
tea.StringValue(targetNodepool.KubernetesConfig.RuntimeVersion), nil
240259
}
241260

242-
func (a *ACKManaged) resolveUserData(respStr string, labels map[string]string, taints []corev1.Taint,
243-
kubeletCfg *v1alpha1.KubeletConfiguration, userData *string) string {
244-
preUserData, postUserData := parseCustomUserData(userData)
261+
func (a *ACKManaged) ackBootstrap(respStr string, labels map[string]string, taints []corev1.Taint,
262+
kubeletCfg *v1alpha1.KubeletConfiguration) string {
245263

246264
var script bytes.Buffer
247265
// Add bash script header
248266
script.WriteString("#!/bin/bash\n\n")
249267

250-
// Insert preUserData if available
251-
if preUserData != "" {
252-
// Pre-userData: scripts to be executed before node registration
253-
script.WriteString("echo \"Executing preUserData...\"\n")
254-
script.WriteString(preUserData + "\n\n")
255-
}
256-
257268
// Clean up the input string
258269
script.WriteString(respStr + " ")
259270
// Add labels
@@ -264,15 +275,7 @@ func (a *ACKManaged) resolveUserData(respStr string, labels map[string]string, t
264275
// Add taints
265276
script.WriteString(fmt.Sprintf("--taints %s\n\n", a.formatTaints(taints)))
266277

267-
// Insert postUserData if available
268-
if postUserData != "" {
269-
// Post-userData: scripts to be executed after node registration
270-
script.WriteString("echo \"Executing postUserData...\"\n")
271-
script.WriteString(postUserData + "\n")
272-
}
273-
274-
// Encode to base64
275-
return base64.StdEncoding.EncodeToString(script.Bytes())
278+
return script.String()
276279
}
277280

278281
func (a *ACKManaged) formatLabels(labels map[string]string) string {
@@ -310,16 +313,3 @@ func convertNodeClassKubeletConfigToACKNodeConfig(kubeletCfg *v1alpha1.KubeletCo
310313
}
311314
return base64.StdEncoding.EncodeToString(data)
312315
}
313-
314-
const userDataSeparator = "#===USERDATA_SEPARATOR==="
315-
316-
// By default, the UserData is executed after the node registration is completed.
317-
// If a user requires tasks to be executed both before and after node registration,
318-
// they must split the userdata into preUserData and postUserData using a SEPARATOR.
319-
func parseCustomUserData(userData *string) (string, string) {
320-
parts := strings.Split(tea.StringValue(userData), userDataSeparator)
321-
if len(parts) == 2 {
322-
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
323-
}
324-
return "", tea.StringValue(userData)
325-
}

pkg/providers/cluster/cloudinit.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
Copyright 2024 The CloudPilot AI Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cluster
18+
19+
import (
20+
"mime"
21+
"strings"
22+
23+
awsmime "github.com/aws/karpenter-provider-aws/pkg/providers/amifamily/bootstrap/mime"
24+
"github.com/samber/lo"
25+
)
26+
27+
const (
28+
contentTypeStage string = `stage`
29+
contentTypeShellScriptMediaType string = `text/x-shellscript`
30+
)
31+
32+
type CloudInit struct {
33+
entries []awsmime.Entry
34+
}
35+
36+
func NewCloudInit() *CloudInit {
37+
return &CloudInit{
38+
entries: make([]awsmime.Entry, 0),
39+
}
40+
}
41+
42+
func (c *CloudInit) Script() (string, error) {
43+
c.sort()
44+
mimeArchive := awsmime.Archive(c.entries)
45+
userData, err := mimeArchive.Serialize()
46+
if err != nil {
47+
return "", err
48+
}
49+
return userData, nil
50+
}
51+
52+
func (c *CloudInit) Merge(userdata *string) error {
53+
userData := lo.FromPtr(userdata)
54+
if userData == "" {
55+
return nil
56+
}
57+
if strings.HasPrefix(strings.TrimSpace(userData), "MIME-Version:") ||
58+
strings.HasPrefix(strings.TrimSpace(userData), "Content-Type:") {
59+
archive, err := awsmime.NewArchive(userData)
60+
if err != nil {
61+
return err
62+
}
63+
c.entries = append(c.entries, archive...)
64+
return nil
65+
}
66+
// Fallback to YAML or shall script if UserData is not in MIME format. Determine the content type for the
67+
// generated MIME header depending on the type of the custom UserData.
68+
c.entries = append(c.entries, awsmime.Entry{
69+
ContentType: awsmime.ContentTypeShellScript,
70+
Content: userData,
71+
})
72+
return nil
73+
}
74+
75+
func (c *CloudInit) sort() {
76+
var pre, non []awsmime.Entry
77+
for _, entry := range c.entries {
78+
mediaType, params, err := mime.ParseMediaType(string(entry.ContentType))
79+
if err != nil {
80+
non = append(non, entry)
81+
continue
82+
}
83+
if stage, ok := params[contentTypeStage]; ok && mediaType == contentTypeShellScriptMediaType && stage == "pre" {
84+
pre = append(pre, entry)
85+
} else {
86+
non = append(non, entry)
87+
}
88+
}
89+
c.entries = append(pre, non...)
90+
}

0 commit comments

Comments
 (0)