From ffa4348abc6ebc87924219bafffce9fa30981bc5 Mon Sep 17 00:00:00 2001 From: pweikai Date: Tue, 5 Apr 2022 12:13:30 -0700 Subject: [PATCH] add passwdFrom to kubeadm v1beta1 --- bootstrap/kubeadm/api/v1alpha3/conversion.go | 23 ++++ .../api/v1alpha3/zz_generated.conversion.go | 40 ++++-- bootstrap/kubeadm/api/v1alpha4/conversion.go | 23 ++++ .../api/v1alpha4/zz_generated.conversion.go | 40 ++++-- .../api/v1beta1/kubeadmconfig_types.go | 24 ++++ .../api/v1beta1/kubeadmconfig_webhook.go | 45 +++++++ .../api/v1beta1/kubeadmconfig_webhook_test.go | 94 +++++++++++++ .../api/v1beta1/zz_generated.deepcopy.go | 36 +++++ ...strap.cluster.x-k8s.io_kubeadmconfigs.yaml | 23 ++++ ...uster.x-k8s.io_kubeadmconfigtemplates.yaml | 23 ++++ .../controllers/kubeadmconfig_controller.go | 63 ++++++++- .../kubeadmconfig_controller_test.go | 126 ++++++++++++++++++ .../kubeadm/api/v1alpha3/conversion.go | 9 ++ .../kubeadm/api/v1alpha4/conversion.go | 19 +++ ...cluster.x-k8s.io_kubeadmcontrolplanes.yaml | 23 ++++ ...x-k8s.io_kubeadmcontrolplanetemplates.yaml | 23 ++++ 16 files changed, 607 insertions(+), 27 deletions(-) diff --git a/bootstrap/kubeadm/api/v1alpha3/conversion.go b/bootstrap/kubeadm/api/v1alpha3/conversion.go index 349e48d6f5d1..a467fc6ccc97 100644 --- a/bootstrap/kubeadm/api/v1alpha3/conversion.go +++ b/bootstrap/kubeadm/api/v1alpha3/conversion.go @@ -38,6 +38,15 @@ func (src *KubeadmConfig) ConvertTo(dstRaw conversion.Hub) error { return err } + dst.Spec.Users = restored.Spec.Users + if restored.Spec.Users != nil { + for i := range restored.Spec.Users { + if restored.Spec.Users[i].PasswdFrom != nil { + dst.Spec.Users[i].PasswdFrom = restored.Spec.Users[i].PasswdFrom + } + } + } + if restored.Spec.JoinConfiguration != nil && restored.Spec.JoinConfiguration.NodeRegistration.IgnorePreflightErrors != nil { if dst.Spec.JoinConfiguration == nil { dst.Spec.JoinConfiguration = &bootstrapv1.JoinConfiguration{} @@ -111,6 +120,15 @@ func (src *KubeadmConfigTemplate) ConvertTo(dstRaw conversion.Hub) error { return err } + dst.Spec.Template.Spec.Users = restored.Spec.Template.Spec.Users + if restored.Spec.Template.Spec.Users != nil { + for i := range restored.Spec.Template.Spec.Users { + if restored.Spec.Template.Spec.Users[i].PasswdFrom != nil { + dst.Spec.Template.Spec.Users[i].PasswdFrom = restored.Spec.Template.Spec.Users[i].PasswdFrom + } + } + } + if restored.Spec.Template.Spec.JoinConfiguration != nil && restored.Spec.Template.Spec.JoinConfiguration.NodeRegistration.IgnorePreflightErrors != nil { if dst.Spec.Template.Spec.JoinConfiguration == nil { dst.Spec.Template.Spec.JoinConfiguration = &bootstrapv1.JoinConfiguration{} @@ -215,3 +233,8 @@ func Convert_v1beta1_KubeadmConfigSpec_To_v1alpha3_KubeadmConfigSpec(in *bootstr // KubeadmConfigSpec.Ignition does not exist in kubeadm v1alpha3 API. return autoConvert_v1beta1_KubeadmConfigSpec_To_v1alpha3_KubeadmConfigSpec(in, out, s) } + +func Convert_v1beta1_User_To_v1alpha3_User(in *bootstrapv1.User, out *User, s apiconversion.Scope) error { + // User.PasswdFrom does not exist in kubeadm v1alpha3 API. + return autoConvert_v1beta1_User_To_v1alpha3_User(in, out, s) +} diff --git a/bootstrap/kubeadm/api/v1alpha3/zz_generated.conversion.go b/bootstrap/kubeadm/api/v1alpha3/zz_generated.conversion.go index ea239590f8c0..bf88f9a66cdf 100644 --- a/bootstrap/kubeadm/api/v1alpha3/zz_generated.conversion.go +++ b/bootstrap/kubeadm/api/v1alpha3/zz_generated.conversion.go @@ -184,11 +184,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta1.User)(nil), (*User)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_User_To_v1alpha3_User(a.(*v1beta1.User), b.(*User), scope) - }); err != nil { - return err - } if err := s.AddConversionFunc((*upstreamv1beta1.ClusterConfiguration)(nil), (*v1beta1.ClusterConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_upstreamv1beta1_ClusterConfiguration_To_v1beta1_ClusterConfiguration(a.(*upstreamv1beta1.ClusterConfiguration), b.(*v1beta1.ClusterConfiguration), scope) }); err != nil { @@ -229,6 +224,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta1.User)(nil), (*User)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_User_To_v1alpha3_User(a.(*v1beta1.User), b.(*User), scope) + }); err != nil { + return err + } return nil } @@ -447,7 +447,17 @@ func autoConvert_v1alpha3_KubeadmConfigSpec_To_v1beta1_KubeadmConfigSpec(in *Kub out.Mounts = *(*[]v1beta1.MountPoints)(unsafe.Pointer(&in.Mounts)) out.PreKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PreKubeadmCommands)) out.PostKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PostKubeadmCommands)) - out.Users = *(*[]v1beta1.User)(unsafe.Pointer(&in.Users)) + if in.Users != nil { + in, out := &in.Users, &out.Users + *out = make([]v1beta1.User, len(*in)) + for i := range *in { + if err := Convert_v1alpha3_User_To_v1beta1_User(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Users = nil + } out.NTP = (*v1beta1.NTP)(unsafe.Pointer(in.NTP)) out.Format = v1beta1.Format(in.Format) out.Verbosity = (*int32)(unsafe.Pointer(in.Verbosity)) @@ -493,7 +503,17 @@ func autoConvert_v1beta1_KubeadmConfigSpec_To_v1alpha3_KubeadmConfigSpec(in *v1b out.Mounts = *(*[]MountPoints)(unsafe.Pointer(&in.Mounts)) out.PreKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PreKubeadmCommands)) out.PostKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PostKubeadmCommands)) - out.Users = *(*[]User)(unsafe.Pointer(&in.Users)) + if in.Users != nil { + in, out := &in.Users, &out.Users + *out = make([]User, len(*in)) + for i := range *in { + if err := Convert_v1beta1_User_To_v1alpha3_User(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Users = nil + } out.NTP = (*NTP)(unsafe.Pointer(in.NTP)) out.Format = Format(in.Format) out.Verbosity = (*int32)(unsafe.Pointer(in.Verbosity)) @@ -762,14 +782,10 @@ func autoConvert_v1beta1_User_To_v1alpha3_User(in *v1beta1.User, out *User, s co out.Inactive = (*bool)(unsafe.Pointer(in.Inactive)) out.Shell = (*string)(unsafe.Pointer(in.Shell)) out.Passwd = (*string)(unsafe.Pointer(in.Passwd)) + // WARNING: in.PasswdFrom requires manual conversion: does not exist in peer-type out.PrimaryGroup = (*string)(unsafe.Pointer(in.PrimaryGroup)) out.LockPassword = (*bool)(unsafe.Pointer(in.LockPassword)) out.Sudo = (*string)(unsafe.Pointer(in.Sudo)) out.SSHAuthorizedKeys = *(*[]string)(unsafe.Pointer(&in.SSHAuthorizedKeys)) return nil } - -// Convert_v1beta1_User_To_v1alpha3_User is an autogenerated conversion function. -func Convert_v1beta1_User_To_v1alpha3_User(in *v1beta1.User, out *User, s conversion.Scope) error { - return autoConvert_v1beta1_User_To_v1alpha3_User(in, out, s) -} diff --git a/bootstrap/kubeadm/api/v1alpha4/conversion.go b/bootstrap/kubeadm/api/v1alpha4/conversion.go index 6da4b2da8286..b2bbcb5c50e4 100644 --- a/bootstrap/kubeadm/api/v1alpha4/conversion.go +++ b/bootstrap/kubeadm/api/v1alpha4/conversion.go @@ -37,6 +37,15 @@ func (src *KubeadmConfig) ConvertTo(dstRaw conversion.Hub) error { return err } + dst.Spec.Users = restored.Spec.Users + if restored.Spec.Users != nil { + for i := range restored.Spec.Users { + if restored.Spec.Users[i].PasswdFrom != nil { + dst.Spec.Users[i].PasswdFrom = restored.Spec.Users[i].PasswdFrom + } + } + } + dst.Spec.Ignition = restored.Spec.Ignition if restored.Spec.InitConfiguration != nil { if dst.Spec.InitConfiguration == nil { @@ -91,6 +100,15 @@ func (src *KubeadmConfigTemplate) ConvertTo(dstRaw conversion.Hub) error { return err } + dst.Spec.Template.Spec.Users = restored.Spec.Template.Spec.Users + if restored.Spec.Template.Spec.Users != nil { + for i := range restored.Spec.Template.Spec.Users { + if restored.Spec.Template.Spec.Users[i].PasswdFrom != nil { + dst.Spec.Template.Spec.Users[i].PasswdFrom = restored.Spec.Template.Spec.Users[i].PasswdFrom + } + } + } + dst.Spec.Template.Spec.Ignition = restored.Spec.Template.Spec.Ignition if restored.Spec.Template.Spec.InitConfiguration != nil { if dst.Spec.Template.Spec.InitConfiguration == nil { @@ -147,3 +165,8 @@ func Convert_v1beta1_JoinConfiguration_To_v1alpha4_JoinConfiguration(in *bootstr // InitConfiguration.Patches does not exist in kubeadm v1alpha4 API. return autoConvert_v1beta1_JoinConfiguration_To_v1alpha4_JoinConfiguration(in, out, s) } + +func Convert_v1beta1_User_To_v1alpha4_User(in *bootstrapv1.User, out *User, s apiconversion.Scope) error { + // User.PasswdFrom does not exist in kubeadm v1alpha4 API. + return autoConvert_v1beta1_User_To_v1alpha4_User(in, out, s) +} diff --git a/bootstrap/kubeadm/api/v1alpha4/zz_generated.conversion.go b/bootstrap/kubeadm/api/v1alpha4/zz_generated.conversion.go index cd0bff198def..187a97828352 100644 --- a/bootstrap/kubeadm/api/v1alpha4/zz_generated.conversion.go +++ b/bootstrap/kubeadm/api/v1alpha4/zz_generated.conversion.go @@ -390,11 +390,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta1.User)(nil), (*User)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_User_To_v1alpha4_User(a.(*v1beta1.User), b.(*User), scope) - }); err != nil { - return err - } if err := s.AddConversionFunc((*v1beta1.InitConfiguration)(nil), (*InitConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_InitConfiguration_To_v1alpha4_InitConfiguration(a.(*v1beta1.InitConfiguration), b.(*InitConfiguration), scope) }); err != nil { @@ -410,6 +405,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta1.User)(nil), (*User)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_User_To_v1alpha4_User(a.(*v1beta1.User), b.(*User), scope) + }); err != nil { + return err + } return nil } @@ -1108,7 +1108,17 @@ func autoConvert_v1alpha4_KubeadmConfigSpec_To_v1beta1_KubeadmConfigSpec(in *Kub out.Mounts = *(*[]v1beta1.MountPoints)(unsafe.Pointer(&in.Mounts)) out.PreKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PreKubeadmCommands)) out.PostKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PostKubeadmCommands)) - out.Users = *(*[]v1beta1.User)(unsafe.Pointer(&in.Users)) + if in.Users != nil { + in, out := &in.Users, &out.Users + *out = make([]v1beta1.User, len(*in)) + for i := range *in { + if err := Convert_v1alpha4_User_To_v1beta1_User(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Users = nil + } out.NTP = (*v1beta1.NTP)(unsafe.Pointer(in.NTP)) out.Format = v1beta1.Format(in.Format) out.Verbosity = (*int32)(unsafe.Pointer(in.Verbosity)) @@ -1146,7 +1156,17 @@ func autoConvert_v1beta1_KubeadmConfigSpec_To_v1alpha4_KubeadmConfigSpec(in *v1b out.Mounts = *(*[]MountPoints)(unsafe.Pointer(&in.Mounts)) out.PreKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PreKubeadmCommands)) out.PostKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PostKubeadmCommands)) - out.Users = *(*[]User)(unsafe.Pointer(&in.Users)) + if in.Users != nil { + in, out := &in.Users, &out.Users + *out = make([]User, len(*in)) + for i := range *in { + if err := Convert_v1beta1_User_To_v1alpha4_User(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Users = nil + } out.NTP = (*NTP)(unsafe.Pointer(in.NTP)) out.Format = Format(in.Format) out.Verbosity = (*int32)(unsafe.Pointer(in.Verbosity)) @@ -1503,14 +1523,10 @@ func autoConvert_v1beta1_User_To_v1alpha4_User(in *v1beta1.User, out *User, s co out.Inactive = (*bool)(unsafe.Pointer(in.Inactive)) out.Shell = (*string)(unsafe.Pointer(in.Shell)) out.Passwd = (*string)(unsafe.Pointer(in.Passwd)) + // WARNING: in.PasswdFrom requires manual conversion: does not exist in peer-type out.PrimaryGroup = (*string)(unsafe.Pointer(in.PrimaryGroup)) out.LockPassword = (*bool)(unsafe.Pointer(in.LockPassword)) out.Sudo = (*string)(unsafe.Pointer(in.Sudo)) out.SSHAuthorizedKeys = *(*[]string)(unsafe.Pointer(&in.SSHAuthorizedKeys)) return nil } - -// Convert_v1beta1_User_To_v1alpha4_User is an autogenerated conversion function. -func Convert_v1beta1_User_To_v1alpha4_User(in *v1beta1.User, out *User, s conversion.Scope) error { - return autoConvert_v1beta1_User_To_v1alpha4_User(in, out, s) -} diff --git a/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_types.go b/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_types.go index 40bbe36a5505..7d2c6704dcf8 100644 --- a/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_types.go +++ b/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_types.go @@ -252,6 +252,26 @@ type SecretFileSource struct { Key string `json:"key"` } +// PasswdSource is a union of all possible external source types for passwd data. +// Only one field may be populated in any given instance. Developers adding new +// sources of data for target systems should add them here. +type PasswdSource struct { + // Secret represents a secret that should populate this password. + Secret SecretPasswdSource `json:"secret"` +} + +// SecretPasswdSource adapts a Secret into a PasswdSource. +// +// The contents of the target Secret's Data field will be presented +// as passwd using the keys in the Data field as the file names. +type SecretPasswdSource struct { + // Name of the secret in the KubeadmBootstrapConfig's namespace to use. + Name string `json:"name"` + + // Key is the key in the secret's data map for this value. + Key string `json:"key"` +} + // User defines the input for a generated user in cloud-init. type User struct { // Name specifies the user name @@ -281,6 +301,10 @@ type User struct { // +optional Passwd *string `json:"passwd,omitempty"` + // PasswdFrom is a referenced source of passwd to populate the passwd. + // +optional + PasswdFrom *PasswdSource `json:"passwdFrom,omitempty"` + // PrimaryGroup specifies the primary group for the user // +optional PrimaryGroup *string `json:"primaryGroup,omitempty"` diff --git a/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_webhook.go b/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_webhook.go index bd5ce1d8aebc..a1284256ac9b 100644 --- a/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_webhook.go +++ b/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_webhook.go @@ -31,6 +31,7 @@ import ( var ( cannotUseWithIgnition = fmt.Sprintf("not supported when spec.format is set to %q", Ignition) conflictingFileSourceMsg = "only one of content or contentFrom may be specified for a single file" + conflictingUserSourceMsg = "only one of passwd or passwdFrom may be specified for a single user" kubeadmBootstrapFormatIgnitionFeatureDisabledMsg = "can be set only if the KubeadmBootstrapFormatIgnition feature gate is enabled" missingSecretNameMsg = "secret file source must specify non-empty secret name" missingSecretKeyMsg = "secret file source must specify non-empty secret key" @@ -93,6 +94,7 @@ func (c *KubeadmConfigSpec) Validate(pathPrefix *field.Path) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, c.validateFiles(pathPrefix)...) + allErrs = append(allErrs, c.validateUsers(pathPrefix)...) allErrs = append(allErrs, c.validateIgnition(pathPrefix)...) return allErrs @@ -155,6 +157,49 @@ func (c *KubeadmConfigSpec) validateFiles(pathPrefix *field.Path) field.ErrorLis return allErrs } +func (c *KubeadmConfigSpec) validateUsers(pathPrefix *field.Path) field.ErrorList { + var allErrs field.ErrorList + + for i := range c.Users { + user := c.Users[i] + if user.Passwd != nil && user.PasswdFrom != nil { + allErrs = append( + allErrs, + field.Invalid( + pathPrefix.Child("users").Index(i), + user, + conflictingUserSourceMsg, + ), + ) + } + // n.b.: if we ever add types besides Secret as a PasswdFrom + // Source, we must add webhook validation here for one of the + // sources being non-nil. + if user.PasswdFrom != nil { + if user.PasswdFrom.Secret.Name == "" { + allErrs = append( + allErrs, + field.Required( + pathPrefix.Child("users").Index(i).Child("passwdFrom", "secret", "name"), + missingSecretNameMsg, + ), + ) + } + if user.PasswdFrom.Secret.Key == "" { + allErrs = append( + allErrs, + field.Required( + pathPrefix.Child("users").Index(i).Child("passwdFrom", "secret", "key"), + missingSecretKeyMsg, + ), + ) + } + } + } + + return allErrs +} + func (c *KubeadmConfigSpec) validateIgnition(pathPrefix *field.Path) field.ErrorList { var allErrs field.ErrorList diff --git a/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_webhook_test.go b/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_webhook_test.go index e8ab19f455ff..fb842d45f9b2 100644 --- a/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_webhook_test.go +++ b/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_webhook_test.go @@ -178,6 +178,100 @@ func TestKubeadmConfigValidate(t *testing.T) { }, expectErr: true, }, + "valid passwd": { + in: &KubeadmConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + Namespace: metav1.NamespaceDefault, + }, + Spec: KubeadmConfigSpec{ + Users: []User{ + { + Passwd: pointer.StringPtr("foo"), + }, + }, + }, + }, + }, + "valid passwdFrom": { + in: &KubeadmConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + Namespace: metav1.NamespaceDefault, + }, + Spec: KubeadmConfigSpec{ + Users: []User{ + { + PasswdFrom: &PasswdSource{ + Secret: SecretPasswdSource{ + Name: "foo", + Key: "bar", + }, + }, + }, + }, + }, + }, + }, + "invalid passwd and passwdFrom": { + in: &KubeadmConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + Namespace: metav1.NamespaceDefault, + }, + Spec: KubeadmConfigSpec{ + Users: []User{ + { + PasswdFrom: &PasswdSource{}, + Passwd: pointer.StringPtr("foo"), + }, + }, + }, + }, + expectErr: true, + }, + "invalid passwdFrom without name": { + in: &KubeadmConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + Namespace: metav1.NamespaceDefault, + }, + Spec: KubeadmConfigSpec{ + Users: []User{ + { + PasswdFrom: &PasswdSource{ + Secret: SecretPasswdSource{ + Key: "bar", + }, + }, + Passwd: pointer.StringPtr("foo"), + }, + }, + }, + }, + expectErr: true, + }, + "invalid passwdFrom without key": { + in: &KubeadmConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + Namespace: metav1.NamespaceDefault, + }, + Spec: KubeadmConfigSpec{ + Users: []User{ + { + PasswdFrom: &PasswdSource{ + Secret: SecretPasswdSource{ + Name: "foo", + }, + }, + Passwd: pointer.StringPtr("foo"), + }, + }, + }, + }, + expectErr: true, + }, "Ignition field is set, format is not Ignition": { enableIgnitionFeature: true, in: &KubeadmConfig{ diff --git a/bootstrap/kubeadm/api/v1beta1/zz_generated.deepcopy.go b/bootstrap/kubeadm/api/v1beta1/zz_generated.deepcopy.go index b3caa08cc5fc..403c76830611 100644 --- a/bootstrap/kubeadm/api/v1beta1/zz_generated.deepcopy.go +++ b/bootstrap/kubeadm/api/v1beta1/zz_generated.deepcopy.go @@ -1021,6 +1021,22 @@ func (in *Partition) DeepCopy() *Partition { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PasswdSource) DeepCopyInto(out *PasswdSource) { + *out = *in + out.Secret = in.Secret +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PasswdSource. +func (in *PasswdSource) DeepCopy() *PasswdSource { + if in == nil { + return nil + } + out := new(PasswdSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Patches) DeepCopyInto(out *Patches) { *out = *in @@ -1051,6 +1067,21 @@ func (in *SecretFileSource) DeepCopy() *SecretFileSource { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretPasswdSource) DeepCopyInto(out *SecretPasswdSource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretPasswdSource. +func (in *SecretPasswdSource) DeepCopy() *SecretPasswdSource { + if in == nil { + return nil + } + out := new(SecretPasswdSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *User) DeepCopyInto(out *User) { *out = *in @@ -1084,6 +1115,11 @@ func (in *User) DeepCopyInto(out *User) { *out = new(string) **out = **in } + if in.PasswdFrom != nil { + in, out := &in.PasswdFrom, &out.PasswdFrom + *out = new(PasswdSource) + **out = **in + } if in.PrimaryGroup != nil { in, out := &in.PrimaryGroup, &out.PrimaryGroup *out = new(string) 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 9aa0b56ea8bc..87fd625ee325 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 @@ -2956,6 +2956,29 @@ spec: passwd: description: Passwd specifies a hashed password for the user type: string + passwdFrom: + description: PasswdFrom is a referenced source of passwd to + populate the passwd. + properties: + secret: + description: Secret represents a secret that should populate + this password. + properties: + key: + description: Key is the key in the secret's data map + for this value. + type: string + name: + description: Name of the secret in the KubeadmBootstrapConfig's + namespace to use. + type: string + required: + - key + - name + type: object + required: + - secret + type: object primaryGroup: description: PrimaryGroup specifies the primary group for the user 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 fde62870a7c6..f6a8ffaf6d47 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 @@ -3008,6 +3008,29 @@ spec: description: Passwd specifies a hashed password for the user type: string + passwdFrom: + description: PasswdFrom is a referenced source of passwd + to populate the passwd. + properties: + secret: + description: Secret represents a secret that should + populate this password. + properties: + key: + description: Key is the key in the secret's + data map for this value. + type: string + name: + description: Name of the secret in the KubeadmBootstrapConfig's + namespace to use. + type: string + required: + - key + - name + type: object + required: + - secret + type: object primaryGroup: description: PrimaryGroup specifies the primary group for the user diff --git a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go index 7bb951f6fd20..8f6c0e6e381d 100644 --- a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go +++ b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go @@ -447,13 +447,19 @@ func (r *KubeadmConfigReconciler) handleClusterNotInitialized(ctx context.Contex return ctrl.Result{}, err } + users, err := r.resolveUsers(ctx, scope.Config) + if err != nil { + conditions.MarkFalse(scope.Config, bootstrapv1.DataSecretAvailableCondition, bootstrapv1.DataSecretGenerationFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + return ctrl.Result{}, err + } + controlPlaneInput := &cloudinit.ControlPlaneInput{ BaseUserData: cloudinit.BaseUserData{ AdditionalFiles: files, NTP: scope.Config.Spec.NTP, PreKubeadmCommands: scope.Config.Spec.PreKubeadmCommands, PostKubeadmCommands: scope.Config.Spec.PostKubeadmCommands, - Users: scope.Config.Spec.Users, + Users: users, Mounts: scope.Config.Spec.Mounts, DiskSetup: scope.Config.Spec.DiskSetup, KubeadmVerbosity: verbosityFlag, @@ -540,13 +546,19 @@ func (r *KubeadmConfigReconciler) joinWorker(ctx context.Context, scope *Scope) return ctrl.Result{}, err } + users, err := r.resolveUsers(ctx, scope.Config) + if err != nil { + conditions.MarkFalse(scope.Config, bootstrapv1.DataSecretAvailableCondition, bootstrapv1.DataSecretGenerationFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + return ctrl.Result{}, err + } + nodeInput := &cloudinit.NodeInput{ BaseUserData: cloudinit.BaseUserData{ AdditionalFiles: files, NTP: scope.Config.Spec.NTP, PreKubeadmCommands: scope.Config.Spec.PreKubeadmCommands, PostKubeadmCommands: scope.Config.Spec.PostKubeadmCommands, - Users: scope.Config.Spec.Users, + Users: users, Mounts: scope.Config.Spec.Mounts, DiskSetup: scope.Config.Spec.DiskSetup, KubeadmVerbosity: verbosityFlag, @@ -635,6 +647,12 @@ func (r *KubeadmConfigReconciler) joinControlplane(ctx context.Context, scope *S return ctrl.Result{}, err } + users, err := r.resolveUsers(ctx, scope.Config) + if err != nil { + conditions.MarkFalse(scope.Config, bootstrapv1.DataSecretAvailableCondition, bootstrapv1.DataSecretGenerationFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + return ctrl.Result{}, err + } + controlPlaneJoinInput := &cloudinit.ControlPlaneJoinInput{ JoinConfiguration: joinData, Certificates: certificates, @@ -643,7 +661,7 @@ func (r *KubeadmConfigReconciler) joinControlplane(ctx context.Context, scope *S NTP: scope.Config.Spec.NTP, PreKubeadmCommands: scope.Config.Spec.PreKubeadmCommands, PostKubeadmCommands: scope.Config.Spec.PostKubeadmCommands, - Users: scope.Config.Spec.Users, + Users: users, Mounts: scope.Config.Spec.Mounts, DiskSetup: scope.Config.Spec.DiskSetup, KubeadmVerbosity: verbosityFlag, @@ -713,6 +731,45 @@ func (r *KubeadmConfigReconciler) resolveSecretFileContent(ctx context.Context, return data, nil } +// resolveUsers maps .Spec.Users into cloudinit.Users, resolving any object references +// along the way. +func (r *KubeadmConfigReconciler) resolveUsers(ctx context.Context, cfg *bootstrapv1.KubeadmConfig) ([]bootstrapv1.User, error) { + collected := make([]bootstrapv1.User, 0, len(cfg.Spec.Users)) + + for i := range cfg.Spec.Users { + in := cfg.Spec.Users[i] + if in.PasswdFrom != nil { + data, err := r.resolveSecretPasswordContent(ctx, cfg.Namespace, in) + if err != nil { + return nil, errors.Wrapf(err, "failed to resolve passwd source") + } + in.PasswdFrom = nil + passwdContent := string(data) + in.Passwd = &passwdContent + } + collected = append(collected, in) + } + + return collected, nil +} + +// resolveSecretUserContent returns passwd fetched from a referenced secret object. +func (r *KubeadmConfigReconciler) resolveSecretPasswordContent(ctx context.Context, ns string, source bootstrapv1.User) ([]byte, error) { + secret := &corev1.Secret{} + key := types.NamespacedName{Namespace: ns, Name: source.PasswdFrom.Secret.Name} + if err := r.Client.Get(ctx, key, secret); err != nil { + if apierrors.IsNotFound(err) { + return nil, errors.Wrapf(err, "secret not found: %s", key) + } + return nil, errors.Wrapf(err, "failed to retrieve Secret %q", key) + } + data, ok := secret.Data[source.PasswdFrom.Secret.Key] + if !ok { + return nil, errors.Errorf("secret references non-existent secret key: %q", source.PasswdFrom.Secret.Key) + } + return data, nil +} + // ClusterToKubeadmConfigs is a handler.ToRequestsFunc to be used to enqueue // requests for reconciliation of KubeadmConfigs. func (r *KubeadmConfigReconciler) ClusterToKubeadmConfigs(o client.Object) []ctrl.Request { diff --git a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller_test.go b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller_test.go index 7d9109d93550..cbc17189d996 100644 --- a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller_test.go +++ b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller_test.go @@ -1861,6 +1861,132 @@ func TestKubeadmConfigReconciler_ResolveFiles(t *testing.T) { } } +func TestKubeadmConfigReconciler_ResolveUsers(t *testing.T) { + fakePasswd := "bar" + testSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "source", + }, + Data: map[string][]byte{ + "key": []byte(fakePasswd), + }, + } + + cases := map[string]struct { + cfg *bootstrapv1.KubeadmConfig + objects []client.Object + expect []bootstrapv1.User + }{ + "password should pass through": { + cfg: &bootstrapv1.KubeadmConfig{ + Spec: bootstrapv1.KubeadmConfigSpec{ + Users: []bootstrapv1.User{ + { + Name: "foo", + Passwd: &fakePasswd, + }, + }, + }, + }, + expect: []bootstrapv1.User{ + { + Name: "foo", + Passwd: &fakePasswd, + }, + }, + }, + "passwdFrom should convert correctly": { + cfg: &bootstrapv1.KubeadmConfig{ + Spec: bootstrapv1.KubeadmConfigSpec{ + Users: []bootstrapv1.User{ + { + Name: "foo", + PasswdFrom: &bootstrapv1.PasswdSource{ + Secret: bootstrapv1.SecretPasswdSource{ + Name: "source", + Key: "key", + }, + }, + }, + }, + }, + }, + expect: []bootstrapv1.User{ + { + Name: "foo", + Passwd: &fakePasswd, + }, + }, + objects: []client.Object{testSecret}, + }, + "multiple users should work correctly": { + cfg: &bootstrapv1.KubeadmConfig{ + Spec: bootstrapv1.KubeadmConfigSpec{ + Users: []bootstrapv1.User{ + { + Name: "foo", + Passwd: &fakePasswd, + }, + { + Name: "bar", + PasswdFrom: &bootstrapv1.PasswdSource{ + Secret: bootstrapv1.SecretPasswdSource{ + Name: "source", + Key: "key", + }, + }, + }, + }, + }, + }, + expect: []bootstrapv1.User{ + { + Name: "foo", + Passwd: &fakePasswd, + }, + { + Name: "bar", + Passwd: &fakePasswd, + }, + }, + objects: []client.Object{testSecret}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + g := NewWithT(t) + + myclient := fake.NewClientBuilder().WithObjects(tc.objects...).Build() + k := &KubeadmConfigReconciler{ + Client: myclient, + KubeadmInitLock: &myInitLocker{}, + } + + // make a list of password we expect to be sourced from secrets + // after we resolve users, assert that the original spec has + // not been mutated and all password we expected to be sourced + // from secret still are. + passwdFrom := map[string]bool{} + for _, user := range tc.cfg.Spec.Users { + if user.PasswdFrom != nil { + passwdFrom[user.Name] = true + } + } + + users, err := k.resolveUsers(ctx, tc.cfg) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(users).To(Equal(tc.expect)) + for _, user := range tc.cfg.Spec.Users { + if passwdFrom[user.Name] { + g.Expect(user.PasswdFrom).NotTo(BeNil()) + g.Expect(user.Passwd).To(BeNil()) + } + } + }) + } +} + // test utils. // newWorkerMachineForCluster returns a Machine with the passed Cluster's information and a pre-configured name. diff --git a/controlplane/kubeadm/api/v1alpha3/conversion.go b/controlplane/kubeadm/api/v1alpha3/conversion.go index 41047ace4280..113e7a0641b4 100644 --- a/controlplane/kubeadm/api/v1alpha3/conversion.go +++ b/controlplane/kubeadm/api/v1alpha3/conversion.go @@ -40,8 +40,17 @@ func (src *KubeadmControlPlane) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.MachineTemplate.ObjectMeta = restored.Spec.MachineTemplate.ObjectMeta dst.Spec.MachineTemplate.NodeDeletionTimeout = restored.Spec.MachineTemplate.NodeDeletionTimeout + dst.Spec.KubeadmConfigSpec.Users = restored.Spec.KubeadmConfigSpec.Users dst.Status.Version = restored.Status.Version + if restored.Spec.KubeadmConfigSpec.Users != nil { + for i := range restored.Spec.KubeadmConfigSpec.Users { + if restored.Spec.KubeadmConfigSpec.Users[i].PasswdFrom != nil { + dst.Spec.KubeadmConfigSpec.Users[i].PasswdFrom = restored.Spec.KubeadmConfigSpec.Users[i].PasswdFrom + } + } + } + if restored.Spec.KubeadmConfigSpec.JoinConfiguration != nil && restored.Spec.KubeadmConfigSpec.JoinConfiguration.NodeRegistration.IgnorePreflightErrors != nil { if dst.Spec.KubeadmConfigSpec.JoinConfiguration == nil { dst.Spec.KubeadmConfigSpec.JoinConfiguration = &bootstrapv1.JoinConfiguration{} diff --git a/controlplane/kubeadm/api/v1alpha4/conversion.go b/controlplane/kubeadm/api/v1alpha4/conversion.go index f54388b1d196..664d1b4cc98b 100644 --- a/controlplane/kubeadm/api/v1alpha4/conversion.go +++ b/controlplane/kubeadm/api/v1alpha4/conversion.go @@ -39,6 +39,15 @@ func (src *KubeadmControlPlane) ConvertTo(dstRaw conversion.Hub) error { return err } + dst.Spec.KubeadmConfigSpec.Users = restored.Spec.KubeadmConfigSpec.Users + if restored.Spec.KubeadmConfigSpec.Users != nil { + for i := range restored.Spec.KubeadmConfigSpec.Users { + if restored.Spec.KubeadmConfigSpec.Users[i].PasswdFrom != nil { + dst.Spec.KubeadmConfigSpec.Users[i].PasswdFrom = restored.Spec.KubeadmConfigSpec.Users[i].PasswdFrom + } + } + } + dst.Spec.KubeadmConfigSpec.Ignition = restored.Spec.KubeadmConfigSpec.Ignition if restored.Spec.KubeadmConfigSpec.InitConfiguration != nil { if dst.Spec.KubeadmConfigSpec.InitConfiguration == nil { @@ -96,8 +105,18 @@ func (src *KubeadmControlPlaneTemplate) ConvertTo(dstRaw conversion.Hub) error { return err } + dst.Spec.Template.Spec.KubeadmConfigSpec.Users = restored.Spec.Template.Spec.KubeadmConfigSpec.Users dst.Spec.Template.Spec.KubeadmConfigSpec.Ignition = restored.Spec.Template.Spec.KubeadmConfigSpec.Ignition dst.Spec.Template.Spec.MachineTemplate = restored.Spec.Template.Spec.MachineTemplate + + if restored.Spec.Template.Spec.KubeadmConfigSpec.Users != nil { + for i := range restored.Spec.Template.Spec.KubeadmConfigSpec.Users { + if restored.Spec.Template.Spec.KubeadmConfigSpec.Users[i].PasswdFrom != nil { + dst.Spec.Template.Spec.KubeadmConfigSpec.Users[i].PasswdFrom = restored.Spec.Template.Spec.KubeadmConfigSpec.Users[i].PasswdFrom + } + } + } + if restored.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration != nil { if dst.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration == nil { dst.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration = &bootstrapv1.InitConfiguration{} 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 3dcbf7a79b89..5211771abf7b 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 @@ -3442,6 +3442,29 @@ spec: description: Passwd specifies a hashed password for the user type: string + passwdFrom: + description: PasswdFrom is a referenced source of passwd + to populate the passwd. + properties: + secret: + description: Secret represents a secret that should + populate this password. + properties: + key: + description: Key is the key in the secret's data + map for this value. + type: string + name: + description: Name of the secret in the KubeadmBootstrapConfig's + namespace to use. + type: string + required: + - key + - name + type: object + required: + - secret + type: object primaryGroup: description: PrimaryGroup specifies the primary group for the user 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 ae33ad81fd5b..1c12482c7781 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 @@ -2259,6 +2259,29 @@ spec: description: Passwd specifies a hashed password for the user type: string + passwdFrom: + description: PasswdFrom is a referenced source of + passwd to populate the passwd. + properties: + secret: + description: Secret represents a secret that + should populate this password. + properties: + key: + description: Key is the key in the secret's + data map for this value. + type: string + name: + description: Name of the secret in the KubeadmBootstrapConfig's + namespace to use. + type: string + required: + - key + - name + type: object + required: + - secret + type: object primaryGroup: description: PrimaryGroup specifies the primary group for the user