diff --git a/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_types.go b/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_types.go index ea2956d30857..208e06a68c8a 100644 --- a/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_types.go +++ b/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_types.go @@ -75,11 +75,20 @@ type KubeadmConfigSpec struct { // +optional 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. This is typically run in the cloud-init.service systemd unit. This has no effect in Ignition. + // +optional + BootCommands []BootCommand `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 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 PostKubeadmCommands []string `json:"postKubeadmCommands,omitempty"` @@ -720,3 +729,6 @@ type Filesystem struct { // MountPoints defines input for generated mounts in cloud-init. type MountPoints []string + +// BootCommand defines input for each bootcmd command in cloud-init. +type BootCommand []string diff --git a/bootstrap/kubeadm/api/v1beta1/zz_generated.deepcopy.go b/bootstrap/kubeadm/api/v1beta1/zz_generated.deepcopy.go index 3f508065e68c..11710d2eaa4f 100644 --- a/bootstrap/kubeadm/api/v1beta1/zz_generated.deepcopy.go +++ b/bootstrap/kubeadm/api/v1beta1/zz_generated.deepcopy.go @@ -68,6 +68,25 @@ func (in *APIServer) DeepCopy() *APIServer { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in BootCommand) DeepCopyInto(out *BootCommand) { + { + in := &in + *out = make(BootCommand, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootCommand. +func (in BootCommand) DeepCopy() BootCommand { + if in == nil { + return nil + } + out := new(BootCommand) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BootstrapToken) DeepCopyInto(out *BootstrapToken) { *out = *in @@ -864,6 +883,17 @@ func (in *KubeadmConfigSpec) DeepCopyInto(out *KubeadmConfigSpec) { } } } + if in.BootCommands != nil { + in, out := &in.BootCommands, &out.BootCommands + *out = make([]BootCommand, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = make(BootCommand, 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 3d29c546acc4..72bb8af89fff 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 @@ -2045,6 +2045,17 @@ spec: KubeadmConfigSpec defines the desired state of KubeadmConfig. Either ClusterConfiguration and InitConfiguration should be defined or the JoinConfiguration should be defined. properties: + bootCommands: + description: |- + bootCommands specifies extra commands to run very early in the boot process via the cloud-init bootcmd + module. This is typically run in the cloud-init.service systemd unit. This has no effect in Ignition. + items: + description: BootCommand defines input for each bootcmd command + in cloud-init. + items: + type: string + type: array + type: array clusterConfiguration: description: clusterConfiguration along with InitConfiguration are the configurations necessary for the init command @@ -3591,14 +3602,18 @@ 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: type: string 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: type: string type: array 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 23cfa5bbd1f0..2bee123a354f 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 @@ -1992,6 +1992,17 @@ spec: KubeadmConfigSpec defines the desired state of KubeadmConfig. Either ClusterConfiguration and InitConfiguration should be defined or the JoinConfiguration should be defined. properties: + bootCommands: + description: |- + bootCommands specifies extra commands to run very early in the boot process via the cloud-init bootcmd + module. This is typically run in the cloud-init.service systemd unit. This has no effect in Ignition. + items: + description: BootCommand defines input for each bootcmd + command in cloud-init. + items: + type: string + type: array + type: array clusterConfiguration: description: clusterConfiguration along with InitConfiguration are the configurations necessary for the init command @@ -3581,14 +3592,18 @@ 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: type: string 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: type: string type: array diff --git a/bootstrap/kubeadm/internal/cloudinit/boot_commands.go b/bootstrap/kubeadm/internal/cloudinit/boot_commands.go new file mode 100644 index 000000000000..9ecc1c8361e7 --- /dev/null +++ b/bootstrap/kubeadm/internal/cloudinit/boot_commands.go @@ -0,0 +1,29 @@ +/* +Copyright 2019 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 . }} + - {{ range . }}- {{ . }} + {{ end -}} +{{- end -}} +{{- end -}} +{{- end -}} +` +) diff --git a/bootstrap/kubeadm/internal/cloudinit/cloudinit.go b/bootstrap/kubeadm/internal/cloudinit/cloudinit.go index 048ad085728e..3e2fc3e4e92f 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 []bootstrapv1.BootCommand 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..78e6b55a4010 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{ @@ -94,9 +95,12 @@ func TestNewInitControlPlaneCommands(t *testing.T) { cpinput := &ControlPlaneInput{ BaseUserData: BaseUserData{ + BootCommands: []bootstrapv1.BootCommand{ + {"echo", "$(date) hello BootCommands!"}, + }, Header: "test", - PreKubeadmCommands: []string{`"echo $(date) ': hello world!'"`}, - PostKubeadmCommands: []string{"echo $(date) ': hello world!'"}, + PreKubeadmCommands: []string{`"echo $(date) ': hello PreKubeadmCommands!'"`}, + PostKubeadmCommands: []string{"echo $(date) ': hello PostKubeadmCommands!'"}, AdditionalFiles: nil, WriteFiles: nil, Users: nil, @@ -117,13 +121,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) 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 +141,7 @@ func TestNewInitControlPlaneDiskMounts(t *testing.T) { cpinput := &ControlPlaneInput{ BaseUserData: BaseUserData{ Header: "test", + BootCommands: nil, PreKubeadmCommands: nil, PostKubeadmCommands: nil, WriteFiles: nil, @@ -194,6 +204,7 @@ func TestNewJoinControlPlaneAdditionalFileEncodings(t *testing.T) { cpinput := &ControlPlaneJoinInput{ BaseUserData: BaseUserData{ + BootCommands: nil, Header: "test", PreKubeadmCommands: nil, PostKubeadmCommands: nil, @@ -247,6 +258,7 @@ func TestNewJoinControlPlaneExperimentalRetry(t *testing.T) { cpinput := &ControlPlaneJoinInput{ BaseUserData: BaseUserData{ Header: "test", + BootCommands: nil, PreKubeadmCommands: nil, PostKubeadmCommands: nil, UseExperimentalRetry: true, @@ -315,3 +327,83 @@ func Test_useKubeadmBootstrapScriptPre1_31(t *testing.T) { }) } } + +func TestNewJoinControlPlaneCommands(t *testing.T) { + g := NewWithT(t) + + cpinput := &ControlPlaneJoinInput{ + BaseUserData: BaseUserData{ + BootCommands: []bootstrapv1.BootCommand{ + {"echo", "$(date) hello BootCommands!"}, + }, + Header: "test", + 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) 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{ + BootCommands: []bootstrapv1.BootCommand{ + {"echo", "$(date) hello BootCommands!"}, + }, + Header: "test", + 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) 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 b9a6ea250e7b..130a3e56e6ae 100644 --- a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go +++ b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go @@ -531,6 +531,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, @@ -652,6 +653,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, @@ -771,6 +773,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..9349d8eb1277 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 @@ -73,6 +72,11 @@ func TestRender(t *testing.T) { { desc: "renders valid Ignition JSON", input: &cloudinit.BaseUserData{ + BootCommands: []bootstrapv1.BootCommand{ + { + "boot-command", "another-boot-command", + }, + }, PreKubeadmCommands: preKubeadmCommands, PostKubeadmCommands: postKubeadmCommands, KubeadmCommand: "kubeadm join", @@ -269,6 +273,11 @@ func TestRender(t *testing.T) { { desc: "multiple users with password auth", input: &cloudinit.BaseUserData{ + BootCommands: []bootstrapv1.BootCommand{ + { + "boot-command", "another-boot-command", + }, + }, PreKubeadmCommands: preKubeadmCommands, PostKubeadmCommands: postKubeadmCommands, KubeadmCommand: "kubeadm join", @@ -351,6 +360,11 @@ func TestRender(t *testing.T) { { desc: "base64 encoded content", input: &cloudinit.BaseUserData{ + BootCommands: []bootstrapv1.BootCommand{ + { + "boot-command", "another-boot-command", + }, + }, PreKubeadmCommands: preKubeadmCommands, PostKubeadmCommands: postKubeadmCommands, KubeadmCommand: "kubeadm join", @@ -434,6 +448,11 @@ func TestRender(t *testing.T) { { desc: "all file ownership combinations", input: &cloudinit.BaseUserData{ + BootCommands: []bootstrapv1.BootCommand{ + { + "boot-command", "another-boot-command", + }, + }, PreKubeadmCommands: preKubeadmCommands, PostKubeadmCommands: postKubeadmCommands, KubeadmCommand: "kubeadm join", 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 0e4c0a820168..1d592b829bab 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. This is typically run in the cloud-init.service systemd unit. This has no effect in Ignition. + items: + description: BootCommand defines input for each bootcmd command + in cloud-init. + items: + type: string + type: array + type: array clusterConfiguration: description: clusterConfiguration along with InitConfiguration are the configurations necessary for the init command @@ -4076,14 +4087,18 @@ 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: type: string 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: type: string type: array 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 332634f0bfa1..a8415f605c12 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 @@ -1205,6 +1205,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. This is typically run in the cloud-init.service systemd unit. This has no effect in Ignition. + items: + description: BootCommand defines input for each bootcmd + command in cloud-init. + items: + type: string + type: array + type: array clusterConfiguration: description: clusterConfiguration along with InitConfiguration are the configurations necessary for the init command @@ -2815,14 +2826,18 @@ 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: type: string 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: type: string type: array diff --git a/controlplane/kubeadm/internal/filters_test.go b/controlplane/kubeadm/internal/filters_test.go index db78d4b06a4f..56b208d21aee 100644 --- a/controlplane/kubeadm/internal/filters_test.go +++ b/controlplane/kubeadm/internal/filters_test.go @@ -548,7 +548,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) { @@ -673,7 +673,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) { @@ -734,7 +734,7 @@ func TestMatchInitOrJoinConfiguration(t *testing.T) { + Files: []v1beta1.File{}, DiskSetup: nil, Mounts: nil, - ... // 8 identical fields + ... // 9 identical fields }`)) }) } @@ -920,7 +920,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) { @@ -1045,7 +1045,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) { @@ -1106,7 +1106,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 df85bb946bee..b5ea4c6d8d66 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..39b062ea2a19 100644 --- a/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane_test.go +++ b/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane_test.go @@ -396,6 +396,9 @@ func TestKubeadmControlPlaneValidateUpdate(t *testing.T) { validUpdate := before.DeepCopy() validUpdate.Labels = map[string]string{"blue": "green"} + validUpdate.Spec.KubeadmConfigSpec.BootCommands = []bootstrapv1.BootCommand{ + {"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..afd9c5c25e54 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 {