Skip to content

Commit 3e0a5c1

Browse files
committed
Add bootCommands cloud-init file generation
1 parent afd68cd commit 3e0a5c1

23 files changed

+286
-34
lines changed

bootstrap/kubeadm/api/v1beta1/kubeadmconfig_types.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,27 @@ type KubeadmConfigSpec struct {
7777
// +kubebuilder:validation:MaxItems=100
7878
Mounts []MountPoints `json:"mounts,omitempty"`
7979

80-
// preKubeadmCommands specifies extra commands to run before kubeadm runs
80+
// bootCommands specifies extra commands to run very early in the boot process via the cloud-init bootcmd
81+
// module. bootcmd will run on every boot, 'cloud-init-per' command can be used to make bootcmd run exactly
82+
// once. This is typically run in the cloud-init.service systemd unit. This has no effect in Ignition.
83+
// +optional
84+
// +kubebuilder:validation:MaxItems=1000
85+
// +kubebuilder:validation:items:MinLength=1
86+
// +kubebuilder:validation:items:MaxLength=10240
87+
BootCommands []string `json:"bootCommands,omitempty"`
88+
89+
// preKubeadmCommands specifies extra commands to run before kubeadm runs.
90+
// With cloud-init, this is prepended to the runcmd module configuration, and is typically executed in
91+
// the cloud-final.service systemd unit. In Ignition, this is prepended to /etc/kubeadm.sh.
8192
// +optional
8293
// +kubebuilder:validation:MaxItems=1000
8394
// +kubebuilder:validation:items:MinLength=1
8495
// +kubebuilder:validation:items:MaxLength=10240
8596
PreKubeadmCommands []string `json:"preKubeadmCommands,omitempty"`
8697

87-
// postKubeadmCommands specifies extra commands to run after kubeadm runs
98+
// postKubeadmCommands specifies extra commands to run after kubeadm runs.
99+
// With cloud-init, this is appended to the runcmd module configuration, and is typically executed in
100+
// the cloud-final.service systemd unit. In Ignition, this is appended to /etc/kubeadm.sh.
88101
// +optional
89102
// +kubebuilder:validation:MaxItems=1000
90103
// +kubebuilder:validation:items:MinLength=1
@@ -356,6 +369,16 @@ func (c *KubeadmConfigSpec) validateIgnition(pathPrefix *field.Path) field.Error
356369
}
357370
}
358371

372+
if c.BootCommands != nil {
373+
allErrs = append(
374+
allErrs,
375+
field.Forbidden(
376+
pathPrefix.Child("bootCommands"),
377+
cannotUseWithIgnition,
378+
),
379+
)
380+
}
381+
359382
if c.DiskSetup == nil {
360383
return allErrs
361384
}

bootstrap/kubeadm/api/v1beta1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigs.yaml

Lines changed: 19 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigtemplates.yaml

Lines changed: 19 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
Copyright 2025 The Kubernetes 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 cloudinit
18+
19+
const (
20+
bootCommandsTemplate = `{{ define "boot_commands" -}}
21+
{{- if . }}
22+
bootcmd:{{ range . }}
23+
- {{printf "%q" .}}
24+
{{- end -}}
25+
{{- end -}}
26+
{{- end -}}
27+
`
28+
)

bootstrap/kubeadm/internal/cloudinit/cloudinit.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const (
4545
// BaseUserData is shared across all the various types of files written to disk.
4646
type BaseUserData struct {
4747
Header string
48+
BootCommands []string
4849
PreKubeadmCommands []string
4950
PostKubeadmCommands []string
5051
AdditionalFiles []bootstrapv1.File
@@ -83,6 +84,10 @@ func generate(kind string, tpl string, data interface{}) ([]byte, error) {
8384
return nil, errors.Wrap(err, "failed to parse files template")
8485
}
8586

87+
if _, err := tm.Parse(bootCommandsTemplate); err != nil {
88+
return nil, errors.Wrap(err, "failed to parse boot commands template")
89+
}
90+
8691
if _, err := tm.Parse(commandsTemplate); err != nil {
8792
return nil, errors.Wrap(err, "failed to parse commands template")
8893
}

bootstrap/kubeadm/internal/cloudinit/cloudinit_test.go

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func TestNewInitControlPlaneAdditionalFileEncodings(t *testing.T) {
3434
cpinput := &ControlPlaneInput{
3535
BaseUserData: BaseUserData{
3636
Header: "test",
37+
BootCommands: nil,
3738
PreKubeadmCommands: nil,
3839
PostKubeadmCommands: nil,
3940
AdditionalFiles: []bootstrapv1.File{
@@ -95,8 +96,9 @@ func TestNewInitControlPlaneCommands(t *testing.T) {
9596
cpinput := &ControlPlaneInput{
9697
BaseUserData: BaseUserData{
9798
Header: "test",
98-
PreKubeadmCommands: []string{`"echo $(date) ': hello world!'"`},
99-
PostKubeadmCommands: []string{"echo $(date) ': hello world!'"},
99+
BootCommands: []string{"echo $(date)", "echo 'hello BootCommands!'"},
100+
PreKubeadmCommands: []string{`"echo $(date) ': hello PreKubeadmCommands!'"`},
101+
PostKubeadmCommands: []string{"echo $(date) ': hello PostKubeadmCommands!'"},
100102
AdditionalFiles: nil,
101103
WriteFiles: nil,
102104
Users: nil,
@@ -117,13 +119,18 @@ func TestNewInitControlPlaneCommands(t *testing.T) {
117119
out, err := NewInitControlPlane(cpinput)
118120
g.Expect(err).ToNot(HaveOccurred())
119121

120-
expectedCommands := []string{
121-
`"\"echo $(date) ': hello world!'\""`,
122-
`"echo $(date) ': hello world!'"`,
123-
}
124-
for _, f := range expectedCommands {
125-
g.Expect(out).To(ContainSubstring(f))
126-
}
122+
expectedBootCmd := `bootcmd:
123+
- "echo $(date)"
124+
- "echo 'hello BootCommands!'"`
125+
126+
g.Expect(out).To(ContainSubstring(expectedBootCmd))
127+
128+
expectedRunCmd := `runcmd:
129+
- "\"echo $(date) ': hello PreKubeadmCommands!'\""
130+
- 'kubeadm init --config /run/kubeadm/kubeadm.yaml && echo success > /run/cluster-api/bootstrap-success.complete'
131+
- "echo $(date) ': hello PostKubeadmCommands!'"`
132+
133+
g.Expect(out).To(ContainSubstring(expectedRunCmd))
127134
}
128135

129136
func TestNewInitControlPlaneDiskMounts(t *testing.T) {
@@ -132,6 +139,7 @@ func TestNewInitControlPlaneDiskMounts(t *testing.T) {
132139
cpinput := &ControlPlaneInput{
133140
BaseUserData: BaseUserData{
134141
Header: "test",
142+
BootCommands: nil,
135143
PreKubeadmCommands: nil,
136144
PostKubeadmCommands: nil,
137145
WriteFiles: nil,
@@ -194,6 +202,7 @@ func TestNewJoinControlPlaneAdditionalFileEncodings(t *testing.T) {
194202

195203
cpinput := &ControlPlaneJoinInput{
196204
BaseUserData: BaseUserData{
205+
BootCommands: nil,
197206
Header: "test",
198207
PreKubeadmCommands: nil,
199208
PostKubeadmCommands: nil,
@@ -247,6 +256,7 @@ func TestNewJoinControlPlaneExperimentalRetry(t *testing.T) {
247256
cpinput := &ControlPlaneJoinInput{
248257
BaseUserData: BaseUserData{
249258
Header: "test",
259+
BootCommands: nil,
250260
PreKubeadmCommands: nil,
251261
PostKubeadmCommands: nil,
252262
UseExperimentalRetry: true,
@@ -315,3 +325,79 @@ func Test_useKubeadmBootstrapScriptPre1_31(t *testing.T) {
315325
})
316326
}
317327
}
328+
329+
func TestNewJoinControlPlaneCommands(t *testing.T) {
330+
g := NewWithT(t)
331+
332+
cpinput := &ControlPlaneJoinInput{
333+
BaseUserData: BaseUserData{
334+
Header: "test",
335+
BootCommands: []string{"echo $(date)", "echo 'hello BootCommands!'"},
336+
PreKubeadmCommands: []string{`"echo $(date) ': hello PreKubeadmCommands!'"`},
337+
PostKubeadmCommands: []string{"echo $(date) ': hello PostKubeadmCommands!'"},
338+
AdditionalFiles: nil,
339+
WriteFiles: nil,
340+
Users: nil,
341+
NTP: nil,
342+
},
343+
Certificates: secret.Certificates{},
344+
JoinConfiguration: "my-join-config",
345+
}
346+
347+
for _, certificate := range cpinput.Certificates {
348+
certificate.KeyPair = &certs.KeyPair{
349+
Cert: []byte("some certificate"),
350+
Key: []byte("some key"),
351+
}
352+
}
353+
354+
out, err := NewJoinControlPlane(cpinput)
355+
g.Expect(err).ToNot(HaveOccurred())
356+
357+
expectedBootCmd := `bootcmd:
358+
- "echo $(date)"
359+
- "echo 'hello BootCommands!'"`
360+
361+
g.Expect(out).To(ContainSubstring(expectedBootCmd))
362+
363+
expectedRunCmd := `runcmd:
364+
- "\"echo $(date) ': hello PreKubeadmCommands!'\""
365+
- kubeadm join --config /run/kubeadm/kubeadm-join-config.yaml && echo success > /run/cluster-api/bootstrap-success.complete
366+
- "echo $(date) ': hello PostKubeadmCommands!'"`
367+
368+
g.Expect(out).To(ContainSubstring(expectedRunCmd))
369+
}
370+
371+
func TestNewJoinNodeCommands(t *testing.T) {
372+
g := NewWithT(t)
373+
374+
nodeinput := &NodeInput{
375+
BaseUserData: BaseUserData{
376+
Header: "test",
377+
BootCommands: []string{"echo $(date)", "echo 'hello BootCommands!'"},
378+
PreKubeadmCommands: []string{`"echo $(date) ': hello PreKubeadmCommands!'"`},
379+
PostKubeadmCommands: []string{"echo $(date) ': hello PostKubeadmCommands!'"},
380+
AdditionalFiles: nil,
381+
WriteFiles: nil,
382+
Users: nil,
383+
NTP: nil,
384+
},
385+
JoinConfiguration: "my-join-config",
386+
}
387+
388+
out, err := NewNode(nodeinput)
389+
g.Expect(err).ToNot(HaveOccurred())
390+
391+
expectedBootCmd := `bootcmd:
392+
- "echo $(date)"
393+
- "echo 'hello BootCommands!'"`
394+
395+
g.Expect(out).To(ContainSubstring(expectedBootCmd))
396+
397+
expectedRunCmd := `runcmd:
398+
- "\"echo $(date) ': hello PreKubeadmCommands!'\""
399+
- kubeadm join --config /run/kubeadm/kubeadm-join-config.yaml && echo success > /run/cluster-api/bootstrap-success.complete
400+
- "echo $(date) ': hello PostKubeadmCommands!'"`
401+
402+
g.Expect(out).To(ContainSubstring(expectedRunCmd))
403+
}

bootstrap/kubeadm/internal/cloudinit/controlplane_init.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const (
3535
owner: root:root
3636
permissions: '0640'
3737
content: "This placeholder file is used to create the /run/cluster-api sub directory in a way that is compatible with both Linux and Windows (mkdir -p /run/cluster-api does not work with Windows)"
38+
{{- template "boot_commands" .BootCommands }}
3839
runcmd:
3940
{{- template "commands" .PreKubeadmCommands }}
4041
- 'kubeadm init --config /run/kubeadm/kubeadm.yaml {{.KubeadmVerbosity}} && {{ .SentinelFileCommand }}'

bootstrap/kubeadm/internal/cloudinit/controlplane_join.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const (
3434
owner: root:root
3535
permissions: '0640'
3636
content: "This placeholder file is used to create the /run/cluster-api sub directory in a way that is compatible with both Linux and Windows (mkdir -p /run/cluster-api does not work with Windows)"
37+
{{- template "boot_commands" .BootCommands }}
3738
runcmd:
3839
{{- template "commands" .PreKubeadmCommands }}
3940
- {{ .KubeadmCommand }} && {{ .SentinelFileCommand }}

bootstrap/kubeadm/internal/cloudinit/node.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
owner: root:root
3030
permissions: '0640'
3131
content: "This placeholder file is used to create the /run/cluster-api sub directory in a way that is compatible with both Linux and Windows (mkdir -p /run/cluster-api does not work with Windows)"
32+
{{- template "boot_commands" .BootCommands }}
3233
runcmd:
3334
{{- template "commands" .PreKubeadmCommands }}
3435
- {{ .KubeadmCommand }} && {{ .SentinelFileCommand }}

0 commit comments

Comments
 (0)