diff --git a/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_types.go b/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_types.go index 344d6b51334b..ed0b7365a2f9 100644 --- a/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_types.go +++ b/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_types.go @@ -77,14 +77,27 @@ type KubeadmConfigSpec struct { // +kubebuilder:validation:MaxItems=100 Mounts []MountPoints `json:"mounts,omitempty"` - // preKubeadmCommands specifies extra commands to run before kubeadm runs + // bootCommands specifies extra commands to run very early in the boot process via the cloud-init bootcmd + // module. bootcmd will run on every boot, 'cloud-init-per' command can be used to make bootcmd run exactly + // once. This is typically run in the cloud-init.service systemd unit. This has no effect in Ignition. + // +optional + // +kubebuilder:validation:MaxItems=1000 + // +kubebuilder:validation:items:MinLength=1 + // +kubebuilder:validation:items:MaxLength=10240 + BootCommands []string `json:"bootCommands,omitempty"` + + // preKubeadmCommands specifies extra commands to run before kubeadm runs. + // With cloud-init, this is prepended to the runcmd module configuration, and is typically executed in + // the cloud-final.service systemd unit. In Ignition, this is prepended to /etc/kubeadm.sh. // +optional // +kubebuilder:validation:MaxItems=1000 // +kubebuilder:validation:items:MinLength=1 // +kubebuilder:validation:items:MaxLength=10240 PreKubeadmCommands []string `json:"preKubeadmCommands,omitempty"` - // postKubeadmCommands specifies extra commands to run after kubeadm runs + // postKubeadmCommands specifies extra commands to run after kubeadm runs. + // With cloud-init, this is appended to the runcmd module configuration, and is typically executed in + // the cloud-final.service systemd unit. In Ignition, this is appended to /etc/kubeadm.sh. // +optional // +kubebuilder:validation:MaxItems=1000 // +kubebuilder:validation:items:MinLength=1 @@ -356,6 +369,16 @@ func (c *KubeadmConfigSpec) validateIgnition(pathPrefix *field.Path) field.Error } } + if c.BootCommands != nil { + allErrs = append( + allErrs, + field.Forbidden( + pathPrefix.Child("bootCommands"), + cannotUseWithIgnition, + ), + ) + } + if c.DiskSetup == nil { return allErrs } diff --git a/bootstrap/kubeadm/api/v1beta1/zz_generated.deepcopy.go b/bootstrap/kubeadm/api/v1beta1/zz_generated.deepcopy.go index 3f508065e68c..e1bdbac23f9e 100644 --- a/bootstrap/kubeadm/api/v1beta1/zz_generated.deepcopy.go +++ b/bootstrap/kubeadm/api/v1beta1/zz_generated.deepcopy.go @@ -864,6 +864,11 @@ func (in *KubeadmConfigSpec) DeepCopyInto(out *KubeadmConfigSpec) { } } } + if in.BootCommands != nil { + in, out := &in.BootCommands, &out.BootCommands + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.PreKubeadmCommands != nil { in, out := &in.PreKubeadmCommands, &out.PreKubeadmCommands *out = make([]string, len(*in)) diff --git a/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigs.yaml b/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigs.yaml index 9e6fa96975bf..51598b7739f4 100644 --- a/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigs.yaml +++ b/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigs.yaml @@ -2039,6 +2039,17 @@ spec: spec: description: spec is the desired state of KubeadmConfig. properties: + bootCommands: + description: |- + bootCommands specifies extra commands to run very early in the boot process via the cloud-init bootcmd + module. bootcmd will run on every boot, 'cloud-init-per' command can be used to make bootcmd run exactly + once. This is typically run in the cloud-init.service systemd unit. This has no effect in Ignition. + items: + maxLength: 10240 + minLength: 1 + type: string + maxItems: 1000 + type: array clusterConfiguration: description: clusterConfiguration along with InitConfiguration are the configurations necessary for the init command @@ -3773,8 +3784,10 @@ spec: type: array type: object postKubeadmCommands: - description: postKubeadmCommands specifies extra commands to run after - kubeadm runs + description: |- + postKubeadmCommands specifies extra commands to run after kubeadm runs. + With cloud-init, this is appended to the runcmd module configuration, and is typically executed in + the cloud-final.service systemd unit. In Ignition, this is appended to /etc/kubeadm.sh. items: maxLength: 10240 minLength: 1 @@ -3782,8 +3795,10 @@ spec: maxItems: 1000 type: array preKubeadmCommands: - description: preKubeadmCommands specifies extra commands to run before - kubeadm runs + description: |- + preKubeadmCommands specifies extra commands to run before kubeadm runs. + With cloud-init, this is prepended to the runcmd module configuration, and is typically executed in + the cloud-final.service systemd unit. In Ignition, this is prepended to /etc/kubeadm.sh. items: maxLength: 10240 minLength: 1 diff --git a/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigtemplates.yaml b/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigtemplates.yaml index 99d7af35e683..400b6ec24201 100644 --- a/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigtemplates.yaml +++ b/bootstrap/kubeadm/config/crd/bases/bootstrap.cluster.x-k8s.io_kubeadmconfigtemplates.yaml @@ -1986,6 +1986,17 @@ spec: spec: description: spec is the desired state of KubeadmConfig. properties: + bootCommands: + description: |- + bootCommands specifies extra commands to run very early in the boot process via the cloud-init bootcmd + module. bootcmd will run on every boot, 'cloud-init-per' command can be used to make bootcmd run exactly + once. This is typically run in the cloud-init.service systemd unit. This has no effect in Ignition. + items: + maxLength: 10240 + minLength: 1 + type: string + maxItems: 1000 + type: array clusterConfiguration: description: clusterConfiguration along with InitConfiguration are the configurations necessary for the init command @@ -3764,8 +3775,10 @@ spec: type: array type: object postKubeadmCommands: - description: postKubeadmCommands specifies extra commands - to run after kubeadm runs + description: |- + postKubeadmCommands specifies extra commands to run after kubeadm runs. + With cloud-init, this is appended to the runcmd module configuration, and is typically executed in + the cloud-final.service systemd unit. In Ignition, this is appended to /etc/kubeadm.sh. items: maxLength: 10240 minLength: 1 @@ -3773,8 +3786,10 @@ spec: maxItems: 1000 type: array preKubeadmCommands: - description: preKubeadmCommands specifies extra commands to - run before kubeadm runs + description: |- + preKubeadmCommands specifies extra commands to run before kubeadm runs. + With cloud-init, this is prepended to the runcmd module configuration, and is typically executed in + the cloud-final.service systemd unit. In Ignition, this is prepended to /etc/kubeadm.sh. items: maxLength: 10240 minLength: 1 diff --git a/bootstrap/kubeadm/internal/cloudinit/boot_commands.go b/bootstrap/kubeadm/internal/cloudinit/boot_commands.go new file mode 100644 index 000000000000..f03715442f15 --- /dev/null +++ b/bootstrap/kubeadm/internal/cloudinit/boot_commands.go @@ -0,0 +1,28 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cloudinit + +const ( + bootCommandsTemplate = `{{ define "boot_commands" -}} +{{- if . }} +bootcmd:{{ range . }} + - {{printf "%q" .}} +{{- end -}} +{{- end -}} +{{- end -}} +` +) diff --git a/bootstrap/kubeadm/internal/cloudinit/cloudinit.go b/bootstrap/kubeadm/internal/cloudinit/cloudinit.go index 048ad085728e..4aedf0e2c364 100644 --- a/bootstrap/kubeadm/internal/cloudinit/cloudinit.go +++ b/bootstrap/kubeadm/internal/cloudinit/cloudinit.go @@ -45,6 +45,7 @@ const ( // BaseUserData is shared across all the various types of files written to disk. type BaseUserData struct { Header string + BootCommands []string PreKubeadmCommands []string PostKubeadmCommands []string AdditionalFiles []bootstrapv1.File @@ -83,6 +84,10 @@ func generate(kind string, tpl string, data interface{}) ([]byte, error) { return nil, errors.Wrap(err, "failed to parse files template") } + if _, err := tm.Parse(bootCommandsTemplate); err != nil { + return nil, errors.Wrap(err, "failed to parse boot commands template") + } + if _, err := tm.Parse(commandsTemplate); err != nil { return nil, errors.Wrap(err, "failed to parse commands template") } diff --git a/bootstrap/kubeadm/internal/cloudinit/cloudinit_test.go b/bootstrap/kubeadm/internal/cloudinit/cloudinit_test.go index 789e9b7e8bcd..8219d2a53c8f 100644 --- a/bootstrap/kubeadm/internal/cloudinit/cloudinit_test.go +++ b/bootstrap/kubeadm/internal/cloudinit/cloudinit_test.go @@ -34,6 +34,7 @@ func TestNewInitControlPlaneAdditionalFileEncodings(t *testing.T) { cpinput := &ControlPlaneInput{ BaseUserData: BaseUserData{ Header: "test", + BootCommands: nil, PreKubeadmCommands: nil, PostKubeadmCommands: nil, AdditionalFiles: []bootstrapv1.File{ @@ -95,8 +96,9 @@ func TestNewInitControlPlaneCommands(t *testing.T) { cpinput := &ControlPlaneInput{ BaseUserData: BaseUserData{ Header: "test", - PreKubeadmCommands: []string{`"echo $(date) ': hello world!'"`}, - PostKubeadmCommands: []string{"echo $(date) ': hello world!'"}, + BootCommands: []string{"echo $(date)", "echo 'hello BootCommands!'"}, + PreKubeadmCommands: []string{`"echo $(date) ': hello PreKubeadmCommands!'"`}, + PostKubeadmCommands: []string{"echo $(date) ': hello PostKubeadmCommands!'"}, AdditionalFiles: nil, WriteFiles: nil, Users: nil, @@ -117,13 +119,18 @@ func TestNewInitControlPlaneCommands(t *testing.T) { out, err := NewInitControlPlane(cpinput) g.Expect(err).ToNot(HaveOccurred()) - expectedCommands := []string{ - `"\"echo $(date) ': hello world!'\""`, - `"echo $(date) ': hello world!'"`, - } - for _, f := range expectedCommands { - g.Expect(out).To(ContainSubstring(f)) - } + expectedBootCmd := `bootcmd: + - "echo $(date)" + - "echo 'hello BootCommands!'"` + + g.Expect(out).To(ContainSubstring(expectedBootCmd)) + + expectedRunCmd := `runcmd: + - "\"echo $(date) ': hello PreKubeadmCommands!'\"" + - 'kubeadm init --config /run/kubeadm/kubeadm.yaml && echo success > /run/cluster-api/bootstrap-success.complete' + - "echo $(date) ': hello PostKubeadmCommands!'"` + + g.Expect(out).To(ContainSubstring(expectedRunCmd)) } func TestNewInitControlPlaneDiskMounts(t *testing.T) { @@ -132,6 +139,7 @@ func TestNewInitControlPlaneDiskMounts(t *testing.T) { cpinput := &ControlPlaneInput{ BaseUserData: BaseUserData{ Header: "test", + BootCommands: nil, PreKubeadmCommands: nil, PostKubeadmCommands: nil, WriteFiles: nil, @@ -194,6 +202,7 @@ func TestNewJoinControlPlaneAdditionalFileEncodings(t *testing.T) { cpinput := &ControlPlaneJoinInput{ BaseUserData: BaseUserData{ + BootCommands: nil, Header: "test", PreKubeadmCommands: nil, PostKubeadmCommands: nil, @@ -247,6 +256,7 @@ func TestNewJoinControlPlaneExperimentalRetry(t *testing.T) { cpinput := &ControlPlaneJoinInput{ BaseUserData: BaseUserData{ Header: "test", + BootCommands: nil, PreKubeadmCommands: nil, PostKubeadmCommands: nil, UseExperimentalRetry: true, @@ -315,3 +325,79 @@ func Test_useKubeadmBootstrapScriptPre1_31(t *testing.T) { }) } } + +func TestNewJoinControlPlaneCommands(t *testing.T) { + g := NewWithT(t) + + cpinput := &ControlPlaneJoinInput{ + BaseUserData: BaseUserData{ + Header: "test", + BootCommands: []string{"echo $(date)", "echo 'hello BootCommands!'"}, + PreKubeadmCommands: []string{`"echo $(date) ': hello PreKubeadmCommands!'"`}, + PostKubeadmCommands: []string{"echo $(date) ': hello PostKubeadmCommands!'"}, + AdditionalFiles: nil, + WriteFiles: nil, + Users: nil, + NTP: nil, + }, + Certificates: secret.Certificates{}, + JoinConfiguration: "my-join-config", + } + + for _, certificate := range cpinput.Certificates { + certificate.KeyPair = &certs.KeyPair{ + Cert: []byte("some certificate"), + Key: []byte("some key"), + } + } + + out, err := NewJoinControlPlane(cpinput) + g.Expect(err).ToNot(HaveOccurred()) + + expectedBootCmd := `bootcmd: + - "echo $(date)" + - "echo 'hello BootCommands!'"` + + g.Expect(out).To(ContainSubstring(expectedBootCmd)) + + expectedRunCmd := `runcmd: + - "\"echo $(date) ': hello PreKubeadmCommands!'\"" + - kubeadm join --config /run/kubeadm/kubeadm-join-config.yaml && echo success > /run/cluster-api/bootstrap-success.complete + - "echo $(date) ': hello PostKubeadmCommands!'"` + + g.Expect(out).To(ContainSubstring(expectedRunCmd)) +} + +func TestNewJoinNodeCommands(t *testing.T) { + g := NewWithT(t) + + nodeinput := &NodeInput{ + BaseUserData: BaseUserData{ + Header: "test", + BootCommands: []string{"echo $(date)", "echo 'hello BootCommands!'"}, + PreKubeadmCommands: []string{`"echo $(date) ': hello PreKubeadmCommands!'"`}, + PostKubeadmCommands: []string{"echo $(date) ': hello PostKubeadmCommands!'"}, + AdditionalFiles: nil, + WriteFiles: nil, + Users: nil, + NTP: nil, + }, + JoinConfiguration: "my-join-config", + } + + out, err := NewNode(nodeinput) + g.Expect(err).ToNot(HaveOccurred()) + + expectedBootCmd := `bootcmd: + - "echo $(date)" + - "echo 'hello BootCommands!'"` + + g.Expect(out).To(ContainSubstring(expectedBootCmd)) + + expectedRunCmd := `runcmd: + - "\"echo $(date) ': hello PreKubeadmCommands!'\"" + - kubeadm join --config /run/kubeadm/kubeadm-join-config.yaml && echo success > /run/cluster-api/bootstrap-success.complete + - "echo $(date) ': hello PostKubeadmCommands!'"` + + g.Expect(out).To(ContainSubstring(expectedRunCmd)) +} diff --git a/bootstrap/kubeadm/internal/cloudinit/controlplane_init.go b/bootstrap/kubeadm/internal/cloudinit/controlplane_init.go index c6cd12d5665e..a7412a7a8a54 100644 --- a/bootstrap/kubeadm/internal/cloudinit/controlplane_init.go +++ b/bootstrap/kubeadm/internal/cloudinit/controlplane_init.go @@ -35,6 +35,7 @@ const ( owner: root:root permissions: '0640' 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)" +{{- template "boot_commands" .BootCommands }} runcmd: {{- template "commands" .PreKubeadmCommands }} - 'kubeadm init --config /run/kubeadm/kubeadm.yaml {{.KubeadmVerbosity}} && {{ .SentinelFileCommand }}' diff --git a/bootstrap/kubeadm/internal/cloudinit/controlplane_join.go b/bootstrap/kubeadm/internal/cloudinit/controlplane_join.go index b0366330b5bd..7cbd5fd00952 100644 --- a/bootstrap/kubeadm/internal/cloudinit/controlplane_join.go +++ b/bootstrap/kubeadm/internal/cloudinit/controlplane_join.go @@ -34,6 +34,7 @@ const ( owner: root:root permissions: '0640' 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)" +{{- template "boot_commands" .BootCommands }} runcmd: {{- template "commands" .PreKubeadmCommands }} - {{ .KubeadmCommand }} && {{ .SentinelFileCommand }} diff --git a/bootstrap/kubeadm/internal/cloudinit/node.go b/bootstrap/kubeadm/internal/cloudinit/node.go index 80a143242161..81a9e0a4966d 100644 --- a/bootstrap/kubeadm/internal/cloudinit/node.go +++ b/bootstrap/kubeadm/internal/cloudinit/node.go @@ -29,6 +29,7 @@ const ( owner: root:root permissions: '0640' 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)" +{{- template "boot_commands" .BootCommands }} runcmd: {{- template "commands" .PreKubeadmCommands }} - {{ .KubeadmCommand }} && {{ .SentinelFileCommand }} diff --git a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go index d5ccd667efb4..42de91c1cc28 100644 --- a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go +++ b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go @@ -638,6 +638,7 @@ func (r *KubeadmConfigReconciler) handleClusterNotInitialized(ctx context.Contex BaseUserData: cloudinit.BaseUserData{ AdditionalFiles: files, NTP: scope.Config.Spec.NTP, + BootCommands: scope.Config.Spec.BootCommands, PreKubeadmCommands: scope.Config.Spec.PreKubeadmCommands, PostKubeadmCommands: scope.Config.Spec.PostKubeadmCommands, Users: users, @@ -794,6 +795,7 @@ func (r *KubeadmConfigReconciler) joinWorker(ctx context.Context, scope *Scope) BaseUserData: cloudinit.BaseUserData{ AdditionalFiles: files, NTP: scope.Config.Spec.NTP, + BootCommands: scope.Config.Spec.BootCommands, PreKubeadmCommands: scope.Config.Spec.PreKubeadmCommands, PostKubeadmCommands: scope.Config.Spec.PostKubeadmCommands, Users: users, @@ -948,6 +950,7 @@ func (r *KubeadmConfigReconciler) joinControlplane(ctx context.Context, scope *S BaseUserData: cloudinit.BaseUserData{ AdditionalFiles: files, NTP: scope.Config.Spec.NTP, + BootCommands: scope.Config.Spec.BootCommands, PreKubeadmCommands: scope.Config.Spec.PreKubeadmCommands, PostKubeadmCommands: scope.Config.Spec.PostKubeadmCommands, Users: users, diff --git a/bootstrap/kubeadm/internal/ignition/clc/clc_test.go b/bootstrap/kubeadm/internal/ignition/clc/clc_test.go index 906df32a39af..3d357a53e31f 100644 --- a/bootstrap/kubeadm/internal/ignition/clc/clc_test.go +++ b/bootstrap/kubeadm/internal/ignition/clc/clc_test.go @@ -64,7 +64,6 @@ func TestRender(t *testing.T) { // Test multi-line commands as well. "cat < /etc/modules-load.d/containerd.conf\noverlay\nbr_netfilter\nEOF\n", } - tc := []struct { desc string input *cloudinit.BaseUserData diff --git a/bootstrap/kubeadm/internal/webhooks/kubeadmconfig_test.go b/bootstrap/kubeadm/internal/webhooks/kubeadmconfig_test.go index 9beb34e15381..a041c33c1a3c 100644 --- a/bootstrap/kubeadm/internal/webhooks/kubeadmconfig_test.go +++ b/bootstrap/kubeadm/internal/webhooks/kubeadmconfig_test.go @@ -457,6 +457,32 @@ func TestKubeadmConfigValidate(t *testing.T) { }, expectErr: true, }, + "bootCommands configured with Ignition format": { + enableIgnitionFeature: true, + in: &bootstrapv1.KubeadmConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + Namespace: metav1.NamespaceDefault, + }, + Spec: bootstrapv1.KubeadmConfigSpec{ + Format: bootstrapv1.Ignition, + BootCommands: []string{"echo $(date)", "echo 'hello BootCommands!'"}, + }, + }, + expectErr: true, + }, + "bootCommands configured with CloudConfig format": { + in: &bootstrapv1.KubeadmConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + Namespace: metav1.NamespaceDefault, + }, + Spec: bootstrapv1.KubeadmConfigSpec{ + Format: bootstrapv1.CloudConfig, + BootCommands: []string{"echo $(date)", "echo 'hello BootCommands!'"}, + }, + }, + }, } for name, tt := range cases { diff --git a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml index e197980312e2..bd85072c3866 100644 --- a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml +++ b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml @@ -2510,6 +2510,17 @@ spec: kubeadmConfigSpec is a KubeadmConfigSpec to use for initializing and joining machines to the control plane. properties: + bootCommands: + description: |- + bootCommands specifies extra commands to run very early in the boot process via the cloud-init bootcmd + module. bootcmd will run on every boot, 'cloud-init-per' command can be used to make bootcmd run exactly + once. This is typically run in the cloud-init.service systemd unit. This has no effect in Ignition. + items: + maxLength: 10240 + minLength: 1 + type: string + maxItems: 1000 + type: array clusterConfiguration: description: clusterConfiguration along with InitConfiguration are the configurations necessary for the init command @@ -4264,8 +4275,10 @@ spec: type: array type: object postKubeadmCommands: - description: postKubeadmCommands specifies extra commands to run - after kubeadm runs + description: |- + postKubeadmCommands specifies extra commands to run after kubeadm runs. + With cloud-init, this is appended to the runcmd module configuration, and is typically executed in + the cloud-final.service systemd unit. In Ignition, this is appended to /etc/kubeadm.sh. items: maxLength: 10240 minLength: 1 @@ -4273,8 +4286,10 @@ spec: maxItems: 1000 type: array preKubeadmCommands: - description: preKubeadmCommands specifies extra commands to run - before kubeadm runs + description: |- + preKubeadmCommands specifies extra commands to run before kubeadm runs. + With cloud-init, this is prepended to the runcmd module configuration, and is typically executed in + the cloud-final.service systemd unit. In Ignition, this is prepended to /etc/kubeadm.sh. items: maxLength: 10240 minLength: 1 diff --git a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml index 010700e16e76..8a69632bba3b 100644 --- a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml +++ b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml @@ -1195,6 +1195,17 @@ spec: kubeadmConfigSpec is a KubeadmConfigSpec to use for initializing and joining machines to the control plane. properties: + bootCommands: + description: |- + bootCommands specifies extra commands to run very early in the boot process via the cloud-init bootcmd + module. bootcmd will run on every boot, 'cloud-init-per' command can be used to make bootcmd run exactly + once. This is typically run in the cloud-init.service systemd unit. This has no effect in Ignition. + items: + maxLength: 10240 + minLength: 1 + type: string + maxItems: 1000 + type: array clusterConfiguration: description: clusterConfiguration along with InitConfiguration are the configurations necessary for the init command @@ -2993,8 +3004,10 @@ spec: type: array type: object postKubeadmCommands: - description: postKubeadmCommands specifies extra commands - to run after kubeadm runs + description: |- + postKubeadmCommands specifies extra commands to run after kubeadm runs. + With cloud-init, this is appended to the runcmd module configuration, and is typically executed in + the cloud-final.service systemd unit. In Ignition, this is appended to /etc/kubeadm.sh. items: maxLength: 10240 minLength: 1 @@ -3002,8 +3015,10 @@ spec: maxItems: 1000 type: array preKubeadmCommands: - description: preKubeadmCommands specifies extra commands - to run before kubeadm runs + description: |- + preKubeadmCommands specifies extra commands to run before kubeadm runs. + With cloud-init, this is prepended to the runcmd module configuration, and is typically executed in + the cloud-final.service systemd unit. In Ignition, this is prepended to /etc/kubeadm.sh. items: maxLength: 10240 minLength: 1 diff --git a/controlplane/kubeadm/internal/filters_test.go b/controlplane/kubeadm/internal/filters_test.go index 1d6ffde3c029..8d22ffa3b1d1 100644 --- a/controlplane/kubeadm/internal/filters_test.go +++ b/controlplane/kubeadm/internal/filters_test.go @@ -550,7 +550,7 @@ func TestMatchInitOrJoinConfiguration(t *testing.T) { }, JoinConfiguration: nil, Files: nil, - ... // 10 identical fields + ... // 11 identical fields }`)) }) t.Run("returns true if JoinConfiguration is equal", func(t *testing.T) { @@ -675,7 +675,7 @@ func TestMatchInitOrJoinConfiguration(t *testing.T) { }, Files: nil, DiskSetup: nil, - ... // 9 identical fields + ... // 10 identical fields }`)) }) t.Run("returns false if some other configurations are not equal", func(t *testing.T) { @@ -736,7 +736,7 @@ func TestMatchInitOrJoinConfiguration(t *testing.T) { + Files: []v1beta1.File{}, DiskSetup: nil, Mounts: nil, - ... // 8 identical fields + ... // 9 identical fields }`)) }) } @@ -922,7 +922,7 @@ func TestMatchesKubeadmBootstrapConfig(t *testing.T) { }, JoinConfiguration: nil, Files: nil, - ... // 10 identical fields + ... // 11 identical fields }`)) }) t.Run("returns true if JoinConfiguration is equal", func(t *testing.T) { @@ -1047,7 +1047,7 @@ func TestMatchesKubeadmBootstrapConfig(t *testing.T) { }, Files: nil, DiskSetup: nil, - ... // 9 identical fields + ... // 10 identical fields }`)) }) t.Run("returns false if some other configurations are not equal", func(t *testing.T) { @@ -1108,7 +1108,7 @@ func TestMatchesKubeadmBootstrapConfig(t *testing.T) { + Files: []v1beta1.File{}, DiskSetup: nil, Mounts: nil, - ... // 8 identical fields + ... // 9 identical fields }`)) }) t.Run("should match on labels and annotations", func(t *testing.T) { diff --git a/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane.go b/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane.go index 2e16fecb871b..9fcf55b319ad 100644 --- a/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane.go +++ b/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane.go @@ -142,6 +142,7 @@ const ( skipPhases = "skipPhases" patches = "patches" directory = "directory" + bootCommands = "bootCommands" preKubeadmCommands = "preKubeadmCommands" postKubeadmCommands = "postKubeadmCommands" files = "files" @@ -208,6 +209,7 @@ func (webhook *KubeadmControlPlane) ValidateUpdate(_ context.Context, oldObj, ne {spec, kubeadmConfigSpec, joinConfiguration, "discovery"}, {spec, kubeadmConfigSpec, joinConfiguration, "discovery", "*"}, // spec.kubeadmConfigSpec + {spec, kubeadmConfigSpec, bootCommands}, {spec, kubeadmConfigSpec, preKubeadmCommands}, {spec, kubeadmConfigSpec, postKubeadmCommands}, {spec, kubeadmConfigSpec, files}, diff --git a/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane_test.go b/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane_test.go index c8df406d3bc3..a008272e7629 100644 --- a/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane_test.go +++ b/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane_test.go @@ -396,6 +396,7 @@ func TestKubeadmControlPlaneValidateUpdate(t *testing.T) { validUpdate := before.DeepCopy() validUpdate.Labels = map[string]string{"blue": "green"} + validUpdate.Spec.KubeadmConfigSpec.BootCommands = []string{"ab", "abc"} validUpdate.Spec.KubeadmConfigSpec.PreKubeadmCommands = []string{"ab", "abc"} validUpdate.Spec.KubeadmConfigSpec.PostKubeadmCommands = []string{"ab", "abc"} validUpdate.Spec.KubeadmConfigSpec.Files = []bootstrapv1.File{ diff --git a/docs/book/src/tasks/bootstrap/kubeadm-bootstrap/index.md b/docs/book/src/tasks/bootstrap/kubeadm-bootstrap/index.md index 71215b547e9d..cdf3074d6379 100644 --- a/docs/book/src/tasks/bootstrap/kubeadm-bootstrap/index.md +++ b/docs/book/src/tasks/bootstrap/kubeadm-bootstrap/index.md @@ -182,6 +182,13 @@ The `KubeadmConfig` object supports customizing the content of the config-data. } ``` +- `KubeadmConfig.BootCommands` specifies a list of commands to be executed very early in the boot process + + ```yaml + bootCommands: + - cloud-init-per once mymkfs mkfs /dev/vdb + ``` + - `KubeadmConfig.PreKubeadmCommands` specifies a list of commands to be executed before `kubeadm init/join` ```yaml diff --git a/internal/apis/bootstrap/kubeadm/v1alpha3/conversion.go b/internal/apis/bootstrap/kubeadm/v1alpha3/conversion.go index 4d02942935a3..569065e9d69a 100644 --- a/internal/apis/bootstrap/kubeadm/v1alpha3/conversion.go +++ b/internal/apis/bootstrap/kubeadm/v1alpha3/conversion.go @@ -55,6 +55,7 @@ func MergeRestoredKubeadmConfigSpec(dst *bootstrapv1.KubeadmConfigSpec, restored } } + dst.BootCommands = restored.BootCommands dst.Ignition = restored.Ignition if restored.ClusterConfiguration != nil { diff --git a/internal/apis/bootstrap/kubeadm/v1alpha3/zz_generated.conversion.go b/internal/apis/bootstrap/kubeadm/v1alpha3/zz_generated.conversion.go index 8134a0dd27bf..48a378086ddd 100644 --- a/internal/apis/bootstrap/kubeadm/v1alpha3/zz_generated.conversion.go +++ b/internal/apis/bootstrap/kubeadm/v1alpha3/zz_generated.conversion.go @@ -517,6 +517,7 @@ func autoConvert_v1beta1_KubeadmConfigSpec_To_v1alpha3_KubeadmConfigSpec(in *v1b } out.DiskSetup = (*DiskSetup)(unsafe.Pointer(in.DiskSetup)) out.Mounts = *(*[]MountPoints)(unsafe.Pointer(&in.Mounts)) + // WARNING: in.BootCommands requires manual conversion: does not exist in peer-type out.PreKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PreKubeadmCommands)) out.PostKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PostKubeadmCommands)) if in.Users != nil { diff --git a/internal/apis/bootstrap/kubeadm/v1alpha4/conversion.go b/internal/apis/bootstrap/kubeadm/v1alpha4/conversion.go index 3f9597aa6784..4447e8143f37 100644 --- a/internal/apis/bootstrap/kubeadm/v1alpha4/conversion.go +++ b/internal/apis/bootstrap/kubeadm/v1alpha4/conversion.go @@ -54,6 +54,7 @@ func MergeRestoredKubeadmConfigSpec(dst *bootstrapv1.KubeadmConfigSpec, restored } } + dst.BootCommands = restored.BootCommands dst.Ignition = restored.Ignition if restored.ClusterConfiguration != nil { diff --git a/internal/apis/bootstrap/kubeadm/v1alpha4/zz_generated.conversion.go b/internal/apis/bootstrap/kubeadm/v1alpha4/zz_generated.conversion.go index 0e1ff11cb46e..ecddbbb13496 100644 --- a/internal/apis/bootstrap/kubeadm/v1alpha4/zz_generated.conversion.go +++ b/internal/apis/bootstrap/kubeadm/v1alpha4/zz_generated.conversion.go @@ -1210,6 +1210,7 @@ func autoConvert_v1beta1_KubeadmConfigSpec_To_v1alpha4_KubeadmConfigSpec(in *v1b } out.DiskSetup = (*DiskSetup)(unsafe.Pointer(in.DiskSetup)) out.Mounts = *(*[]MountPoints)(unsafe.Pointer(&in.Mounts)) + // WARNING: in.BootCommands requires manual conversion: does not exist in peer-type out.PreKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PreKubeadmCommands)) out.PostKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PostKubeadmCommands)) if in.Users != nil {