diff --git a/bootstrap/kubeadm/api/v1alpha2/conversion.go b/bootstrap/kubeadm/api/v1alpha2/conversion.go index 33e121058dc4..98a98c0e390d 100644 --- a/bootstrap/kubeadm/api/v1alpha2/conversion.go +++ b/bootstrap/kubeadm/api/v1alpha2/conversion.go @@ -39,6 +39,13 @@ func (src *KubeadmConfig) ConvertTo(dstRaw conversion.Hub) error { dst.Status.DataSecretName = restored.Status.DataSecretName dst.Spec.Verbosity = restored.Spec.Verbosity dst.Spec.UseExperimentalRetryJoin = restored.Spec.UseExperimentalRetryJoin + dst.Spec.Files = restored.Spec.Files + for i := range restored.Spec.Files { + file := restored.Spec.Files[i] + if file.ContentFrom != nil { + dst.Spec.Files = append(dst.Spec.Files, file) + } + } return nil } @@ -129,3 +136,8 @@ func Convert_v1alpha2_KubeadmConfigSpec_To_v1alpha3_KubeadmConfigSpec(in *Kubead func Convert_v1alpha3_KubeadmConfigSpec_To_v1alpha2_KubeadmConfigSpec(in *kubeadmbootstrapv1alpha3.KubeadmConfigSpec, out *KubeadmConfigSpec, s apiconversion.Scope) error { return autoConvert_v1alpha3_KubeadmConfigSpec_To_v1alpha2_KubeadmConfigSpec(in, out, s) } + +// Convert_v1alpha3_File_To_v1alpha2_File converts from the Hub version (v1alpha3) of the File to this version. +func Convert_v1alpha3_File_To_v1alpha2_File(in *kubeadmbootstrapv1alpha3.File, out *File, s apiconversion.Scope) error { + return autoConvert_v1alpha3_File_To_v1alpha2_File(in, out, s) +} diff --git a/bootstrap/kubeadm/api/v1alpha2/conversion_test.go b/bootstrap/kubeadm/api/v1alpha2/conversion_test.go index 6e9c93837f06..76940f494dad 100644 --- a/bootstrap/kubeadm/api/v1alpha2/conversion_test.go +++ b/bootstrap/kubeadm/api/v1alpha2/conversion_test.go @@ -35,7 +35,21 @@ func TestConvertKubeadmConfig(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "hub", }, - Spec: v1alpha3.KubeadmConfigSpec{}, + Spec: v1alpha3.KubeadmConfigSpec{ + Files: []v1alpha3.File{ + { + ContentFrom: &v1alpha3.FileSource{ + Secret: v1alpha3.SecretFileSource{ + Name: "foo", + Key: "bar", + }, + }, + }, + { + Content: "baz", + }, + }, + }, Status: v1alpha3.KubeadmConfigStatus{ Ready: true, DataSecretName: pointer.StringPtr("secret-data"), diff --git a/bootstrap/kubeadm/api/v1alpha2/zz_generated.conversion.go b/bootstrap/kubeadm/api/v1alpha2/zz_generated.conversion.go index 147e2f07ddb8..a828996deb0d 100644 --- a/bootstrap/kubeadm/api/v1alpha2/zz_generated.conversion.go +++ b/bootstrap/kubeadm/api/v1alpha2/zz_generated.conversion.go @@ -41,11 +41,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1alpha3.File)(nil), (*File)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha3_File_To_v1alpha2_File(a.(*v1alpha3.File), b.(*File), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*KubeadmConfig)(nil), (*v1alpha3.KubeadmConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha2_KubeadmConfig_To_v1alpha3_KubeadmConfig(a.(*KubeadmConfig), b.(*v1alpha3.KubeadmConfig), scope) }); err != nil { @@ -136,6 +131,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1alpha3.File)(nil), (*File)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha3_File_To_v1alpha2_File(a.(*v1alpha3.File), b.(*File), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1alpha3.KubeadmConfigSpec)(nil), (*KubeadmConfigSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha3_KubeadmConfigSpec_To_v1alpha2_KubeadmConfigSpec(a.(*v1alpha3.KubeadmConfigSpec), b.(*KubeadmConfigSpec), scope) }); err != nil { @@ -169,14 +169,10 @@ func autoConvert_v1alpha3_File_To_v1alpha2_File(in *v1alpha3.File, out *File, s out.Permissions = in.Permissions out.Encoding = Encoding(in.Encoding) out.Content = in.Content + // WARNING: in.ContentFrom requires manual conversion: does not exist in peer-type return nil } -// Convert_v1alpha3_File_To_v1alpha2_File is an autogenerated conversion function. -func Convert_v1alpha3_File_To_v1alpha2_File(in *v1alpha3.File, out *File, s conversion.Scope) error { - return autoConvert_v1alpha3_File_To_v1alpha2_File(in, out, s) -} - func autoConvert_v1alpha2_KubeadmConfig_To_v1alpha3_KubeadmConfig(in *KubeadmConfig, out *v1alpha3.KubeadmConfig, s conversion.Scope) error { out.ObjectMeta = in.ObjectMeta if err := Convert_v1alpha2_KubeadmConfigSpec_To_v1alpha3_KubeadmConfigSpec(&in.Spec, &out.Spec, s); err != nil { @@ -255,7 +251,17 @@ func autoConvert_v1alpha2_KubeadmConfigSpec_To_v1alpha3_KubeadmConfigSpec(in *Ku out.ClusterConfiguration = (*v1beta1.ClusterConfiguration)(unsafe.Pointer(in.ClusterConfiguration)) out.InitConfiguration = (*v1beta1.InitConfiguration)(unsafe.Pointer(in.InitConfiguration)) out.JoinConfiguration = (*v1beta1.JoinConfiguration)(unsafe.Pointer(in.JoinConfiguration)) - out.Files = *(*[]v1alpha3.File)(unsafe.Pointer(&in.Files)) + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = make([]v1alpha3.File, len(*in)) + for i := range *in { + if err := Convert_v1alpha2_File_To_v1alpha3_File(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Files = nil + } out.PreKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PreKubeadmCommands)) out.PostKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PostKubeadmCommands)) out.Users = *(*[]v1alpha3.User)(unsafe.Pointer(&in.Users)) @@ -268,7 +274,17 @@ func autoConvert_v1alpha3_KubeadmConfigSpec_To_v1alpha2_KubeadmConfigSpec(in *v1 out.ClusterConfiguration = (*v1beta1.ClusterConfiguration)(unsafe.Pointer(in.ClusterConfiguration)) out.InitConfiguration = (*v1beta1.InitConfiguration)(unsafe.Pointer(in.InitConfiguration)) out.JoinConfiguration = (*v1beta1.JoinConfiguration)(unsafe.Pointer(in.JoinConfiguration)) - out.Files = *(*[]File)(unsafe.Pointer(&in.Files)) + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = make([]File, len(*in)) + for i := range *in { + if err := Convert_v1alpha3_File_To_v1alpha2_File(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Files = nil + } out.PreKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PreKubeadmCommands)) out.PostKubeadmCommands = *(*[]string)(unsafe.Pointer(&in.PostKubeadmCommands)) out.Users = *(*[]User)(unsafe.Pointer(&in.Users)) diff --git a/bootstrap/kubeadm/api/v1alpha3/kubeadmbootstrapconfig_types.go b/bootstrap/kubeadm/api/v1alpha3/kubeadmbootstrapconfig_types.go index 5a99cca6150c..5fcf4d200c9e 100644 --- a/bootstrap/kubeadm/api/v1alpha3/kubeadmbootstrapconfig_types.go +++ b/bootstrap/kubeadm/api/v1alpha3/kubeadmbootstrapconfig_types.go @@ -172,7 +172,32 @@ type File struct { Encoding Encoding `json:"encoding,omitempty"` // Content is the actual content of the file. - Content string `json:"content"` + // +optional + Content string `json:"content,omitempty"` + + // ContentFrom is a referenced source of content to populate the file. + // +optional + ContentFrom *FileSource `json:"contentFrom,omitempty"` +} + +// FileSource is a union of all possible external source types for file 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 FileSource struct { + // Secret represents a secret that should populate this file. + Secret SecretFileSource `json:"secret"` +} + +// Adapts a Secret into a FileSource. +// +// The contents of the target Secret's Data field will be presented +// as files using the keys in the Data field as the file names. +type SecretFileSource 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. diff --git a/bootstrap/kubeadm/api/v1alpha3/kubeadmbootstrapconfig_types_test.go b/bootstrap/kubeadm/api/v1alpha3/kubeadmbootstrapconfig_types_test.go index 2fa9d415f902..3d72ff76bb95 100644 --- a/bootstrap/kubeadm/api/v1alpha3/kubeadmbootstrapconfig_types_test.go +++ b/bootstrap/kubeadm/api/v1alpha3/kubeadmbootstrapconfig_types_test.go @@ -17,62 +17,127 @@ limitations under the License. package v1alpha3 import ( - . "github.com/onsi/ginkgo" + "testing" + . "github.com/onsi/gomega" - "golang.org/x/net/context" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" ) // These tests are written in BDD-style using Ginkgo framework. Refer to // http://onsi.github.io/ginkgo to learn more. -var _ = Describe("KubeadmConfig", func() { - var ( - key types.NamespacedName - created, fetched *KubeadmConfig - ) - - BeforeEach(func() { - // Add any setup steps that needs to be executed before each test - }) - - AfterEach(func() { - // Add any teardown steps that needs to be executed after each test - }) - - // Add Tests for OpenAPI validation (or additional CRD features) specified in - // your API definition. - // Avoid adding tests for vanilla CRUD operations because they would - // test Kubernetes API server, which isn't the goal here. - Context("Create API", func() { - - It("should create an object successfully", func() { - - key = types.NamespacedName{ - Name: "foo", - Namespace: "default", - } - created = &KubeadmConfig{ +func TestClusterValidate(t *testing.T) { + cases := map[string]struct { + in *KubeadmConfig + expectErr bool + }{ + "valid content": { + in: &KubeadmConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + Namespace: "default", + }, + Spec: KubeadmConfigSpec{ + Files: []File{ + { + Content: "foo", + }, + }, + }, + }, + }, + "valid contentFrom": { + in: &KubeadmConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + Namespace: "default", + }, + Spec: KubeadmConfigSpec{ + Files: []File{ + { + ContentFrom: &FileSource{ + Secret: SecretFileSource{ + Name: "foo", + Key: "bar", + }, + }, + }, + }, + }, + }, + }, + "invalid content and contentFrom": { + in: &KubeadmConfig{ ObjectMeta: metav1.ObjectMeta{ - Name: "foo", + Name: "baz", Namespace: "default", }, + Spec: KubeadmConfigSpec{ + Files: []File{ + { + ContentFrom: &FileSource{}, + Content: "foo", + }, + }, + }, + }, + expectErr: true, + }, + "invalid contentFrom without name": { + in: &KubeadmConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + Namespace: "default", + }, + Spec: KubeadmConfigSpec{ + Files: []File{ + { + ContentFrom: &FileSource{ + Secret: SecretFileSource{ + Key: "bar", + }, + }, + Content: "foo", + }, + }, + }, + }, + expectErr: true, + }, + "invalid contentFrom without key": { + in: &KubeadmConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + Namespace: "default", + }, + Spec: KubeadmConfigSpec{ + Files: []File{ + { + ContentFrom: &FileSource{ + Secret: SecretFileSource{ + Name: "foo", + }, + }, + Content: "foo", + }, + }, + }, + }, + expectErr: true, + }, + } + + for name, tt := range cases { + t.Run(name, func(t *testing.T) { + g := NewWithT(t) + if tt.expectErr { + g.Expect(tt.in.ValidateCreate()).NotTo(Succeed()) + g.Expect(tt.in.ValidateUpdate(nil)).NotTo(Succeed()) + } else { + g.Expect(tt.in.ValidateCreate()).To(Succeed()) + g.Expect(tt.in.ValidateUpdate(nil)).To(Succeed()) } - - By("creating an API obj") - Expect(k8sClient.Create(context.TODO(), created)).To(Succeed()) - - fetched = &KubeadmConfig{} - Expect(k8sClient.Get(context.TODO(), key, fetched)).To(Succeed()) - Expect(fetched).To(Equal(created)) - - By("deleting the created object") - Expect(k8sClient.Delete(context.TODO(), created)).To(Succeed()) - Expect(k8sClient.Get(context.TODO(), key, created)).ToNot(Succeed()) }) - - }) - -}) + } +} diff --git a/bootstrap/kubeadm/api/v1alpha3/kubeadmconfig_webhook.go b/bootstrap/kubeadm/api/v1alpha3/kubeadmconfig_webhook.go index d08b3e1598d8..c8e95a9063a5 100644 --- a/bootstrap/kubeadm/api/v1alpha3/kubeadmconfig_webhook.go +++ b/bootstrap/kubeadm/api/v1alpha3/kubeadmconfig_webhook.go @@ -17,11 +17,91 @@ limitations under the License. package v1alpha3 import ( + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +var ( + ConflictingFileSourceMsg = "only one of content of contentFrom may be specified for a single file" + MissingFileSourceMsg = "source for file content must be specified if contenFrom is non-nil" + MissingSecretNameMsg = "secret file source must specify non-empty secret name" + MissingSecretKeyMsg = "secret file source must specify non-empty secret key" ) -func (r *KubeadmConfig) SetupWebhookWithManager(mgr ctrl.Manager) error { +func (c *KubeadmConfig) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). - For(r). + For(c). Complete() } + +// +kubebuilder:webhook:verbs=create;update,path=/validate-bootstrap-cluster-x-k8s-io-v1alpha3-kubeadmconfig,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=bootstrap.cluster.x-k8s.io,resources=kubeadmconfigs,versions=v1alpha3,name=validation.kubeadmconfig.bootstrap.cluster.x-k8s.io,sideEffects=None + +var _ webhook.Validator = &KubeadmConfig{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (c *KubeadmConfig) ValidateCreate() error { + return c.Spec.validate(c.Name) +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (c *KubeadmConfig) ValidateUpdate(old runtime.Object) error { + return c.Spec.validate(c.Name) +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (c *KubeadmConfig) ValidateDelete() error { + return nil +} + +func (c *KubeadmConfigSpec) validate(name string) error { + var allErrs field.ErrorList + + for i := range c.Files { + file := c.Files[i] + if file.Content != "" && file.ContentFrom != nil { + allErrs = append( + allErrs, + field.Invalid( + field.NewPath("spec", "files", fmt.Sprintf("%d", i)), + file, + ConflictingFileSourceMsg, + ), + ) + } + // n.b.: if we ever add types besides Secret as a ContentFrom + // Source, we must add webhook validation here for one of the + // sources being non-nil. + if file.ContentFrom != nil { + if file.ContentFrom.Secret.Name == "" { + allErrs = append( + allErrs, + field.Invalid( + field.NewPath("spec", "files", fmt.Sprintf("%d", i), "contentFrom", "secret", "name"), + file, + MissingSecretNameMsg, + ), + ) + } + if file.ContentFrom.Secret.Key == "" { + allErrs = append( + allErrs, + field.Invalid( + field.NewPath("spec", "files", fmt.Sprintf("%d", i), "contentFrom", "secret", "key"), + file, + MissingSecretKeyMsg, + ), + ) + } + } + } + + if len(allErrs) == 0 { + return nil + } + return apierrors.NewInvalid(GroupVersion.WithKind("KubeadmConfig").GroupKind(), name, allErrs) +} diff --git a/bootstrap/kubeadm/api/v1alpha3/zz_generated.deepcopy.go b/bootstrap/kubeadm/api/v1alpha3/zz_generated.deepcopy.go index ec9804d5aab5..72c1e952f200 100644 --- a/bootstrap/kubeadm/api/v1alpha3/zz_generated.deepcopy.go +++ b/bootstrap/kubeadm/api/v1alpha3/zz_generated.deepcopy.go @@ -21,13 +21,18 @@ limitations under the License. package v1alpha3 import ( - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/cluster-api/bootstrap/kubeadm/types/v1beta1" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *File) DeepCopyInto(out *File) { *out = *in + if in.ContentFrom != nil { + in, out := &in.ContentFrom, &out.ContentFrom + *out = new(FileSource) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new File. @@ -40,6 +45,22 @@ func (in *File) DeepCopy() *File { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSource) DeepCopyInto(out *FileSource) { + *out = *in + out.Secret = in.Secret +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSource. +func (in *FileSource) DeepCopy() *FileSource { + if in == nil { + return nil + } + out := new(FileSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KubeadmConfig) DeepCopyInto(out *KubeadmConfig) { *out = *in @@ -120,7 +141,9 @@ func (in *KubeadmConfigSpec) DeepCopyInto(out *KubeadmConfigSpec) { if in.Files != nil { in, out := &in.Files, &out.Files *out = make([]File, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.PreKubeadmCommands != nil { in, out := &in.PreKubeadmCommands, &out.PreKubeadmCommands @@ -301,6 +324,21 @@ func (in *NTP) DeepCopy() *NTP { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretFileSource) DeepCopyInto(out *SecretFileSource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretFileSource. +func (in *SecretFileSource) DeepCopy() *SecretFileSource { + if in == nil { + return nil + } + out := new(SecretFileSource) + 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 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 556d7edf2572..082bad5f48f3 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 @@ -1169,6 +1169,29 @@ spec: content: description: Content is the actual content of the file. type: string + contentFrom: + description: ContentFrom is a referenced source of content to + populate the file. + properties: + secret: + description: Secret represents a secret that should populate + this file. + 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 encoding: description: Encoding specifies the encoding of the file contents. enum: @@ -1189,7 +1212,6 @@ spec: to the file, e.g. "0640". type: string required: - - content - path type: object 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 a812c7388a0e..3fa17261f61e 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 @@ -1227,6 +1227,29 @@ spec: content: description: Content is the actual content of the file. type: string + contentFrom: + description: ContentFrom is a referenced source of content + to populate the file. + properties: + secret: + description: Secret represents a secret that should + populate this file. + 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 encoding: description: Encoding specifies the encoding of the file contents. @@ -1248,7 +1271,6 @@ spec: assign to the file, e.g. "0640". type: string required: - - content - path type: object type: array diff --git a/bootstrap/kubeadm/config/webhook/kustomization.yaml b/bootstrap/kubeadm/config/webhook/kustomization.yaml index cf9bcbdd1d04..23314b7710e3 100644 --- a/bootstrap/kubeadm/config/webhook/kustomization.yaml +++ b/bootstrap/kubeadm/config/webhook/kustomization.yaml @@ -11,7 +11,7 @@ configurations: patchesStrategicMerge: - manager_webhook_patch.yaml -# - webhookcainjection_patch.yaml +- webhookcainjection_patch.yaml vars: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. diff --git a/bootstrap/kubeadm/config/webhook/manifests.yaml b/bootstrap/kubeadm/config/webhook/manifests.yaml index e69de29bb2d1..a343c138a9eb 100644 --- a/bootstrap/kubeadm/config/webhook/manifests.yaml +++ b/bootstrap/kubeadm/config/webhook/manifests.yaml @@ -0,0 +1,28 @@ + +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + creationTimestamp: null + name: validating-webhook-configuration +webhooks: +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-bootstrap-cluster-x-k8s-io-v1alpha3-kubeadmconfig + failurePolicy: Fail + matchPolicy: Equivalent + name: validation.kubeadmconfig.bootstrap.cluster.x-k8s.io + rules: + - apiGroups: + - bootstrap.cluster.x-k8s.io + apiVersions: + - v1alpha3 + operations: + - CREATE + - UPDATE + resources: + - kubeadmconfigs + sideEffects: None diff --git a/bootstrap/kubeadm/config/webhook/webhookcainjection_patch.yaml b/bootstrap/kubeadm/config/webhook/webhookcainjection_patch.yaml index 02ab515d4281..e838a4bf1d25 100644 --- a/bootstrap/kubeadm/config/webhook/webhookcainjection_patch.yaml +++ b/bootstrap/kubeadm/config/webhook/webhookcainjection_patch.yaml @@ -1,13 +1,5 @@ -# This patch add annotation to admission webhook config and -# the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. -apiVersion: admissionregistration.k8s.io/v1 -kind: MutatingWebhookConfiguration -metadata: - name: mutating-webhook-configuration - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) --- -apiVersion: admissionregistration.k8s.io/v1 +apiVersion: admissionregistration.k8s.io/v1beta1 kind: ValidatingWebhookConfiguration metadata: name: validating-webhook-configuration diff --git a/bootstrap/kubeadm/controllers/kubeadmconfig_controller.go b/bootstrap/kubeadm/controllers/kubeadmconfig_controller.go index 8001b364e6bf..2c1f670657d6 100644 --- a/bootstrap/kubeadm/controllers/kubeadmconfig_controller.go +++ b/bootstrap/kubeadm/controllers/kubeadmconfig_controller.go @@ -29,6 +29,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" kerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/utils/pointer" clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" @@ -365,9 +366,15 @@ func (r *KubeadmConfigReconciler) handleClusterNotInitialized(ctx context.Contex verbosityFlag = fmt.Sprintf("--v %s", strconv.Itoa(int(*scope.Config.Spec.Verbosity))) } + files, err := r.resolveFiles(ctx, scope.Config, certificates.AsFiles()...) + if err != nil { + scope.Error(err, "Failed to resolve files") + return ctrl.Result{}, err + } + cloudInitData, err := cloudinit.NewInitControlPlane(&cloudinit.ControlPlaneInput{ BaseUserData: cloudinit.BaseUserData{ - AdditionalFiles: scope.Config.Spec.Files, + AdditionalFiles: files, NTP: scope.Config.Spec.NTP, PreKubeadmCommands: scope.Config.Spec.PreKubeadmCommands, PostKubeadmCommands: scope.Config.Spec.PostKubeadmCommands, @@ -433,9 +440,15 @@ func (r *KubeadmConfigReconciler) joinWorker(ctx context.Context, scope *Scope) verbosityFlag = fmt.Sprintf("--v %s", strconv.Itoa(int(*scope.Config.Spec.Verbosity))) } + files, err := r.resolveFiles(ctx, scope.Config) + if err != nil { + scope.Error(err, "Failed to resolve files") + return ctrl.Result{}, err + } + cloudJoinData, err := cloudinit.NewNode(&cloudinit.NodeInput{ BaseUserData: cloudinit.BaseUserData{ - AdditionalFiles: scope.Config.Spec.Files, + AdditionalFiles: files, NTP: scope.Config.Spec.NTP, PreKubeadmCommands: scope.Config.Spec.PreKubeadmCommands, PostKubeadmCommands: scope.Config.Spec.PostKubeadmCommands, @@ -502,11 +515,17 @@ func (r *KubeadmConfigReconciler) joinControlplane(ctx context.Context, scope *S verbosityFlag = fmt.Sprintf("--v %s", strconv.Itoa(int(*scope.Config.Spec.Verbosity))) } + files, err := r.resolveFiles(ctx, scope.Config, certificates.AsFiles()...) + if err != nil { + scope.Error(err, "Failed to resolve files") + return ctrl.Result{}, err + } + cloudJoinData, err := cloudinit.NewJoinControlPlane(&cloudinit.ControlPlaneJoinInput{ JoinConfiguration: joinData, Certificates: certificates, BaseUserData: cloudinit.BaseUserData{ - AdditionalFiles: scope.Config.Spec.Files, + AdditionalFiles: files, NTP: scope.Config.Spec.NTP, PreKubeadmCommands: scope.Config.Spec.PreKubeadmCommands, PostKubeadmCommands: scope.Config.Spec.PostKubeadmCommands, @@ -528,6 +547,45 @@ func (r *KubeadmConfigReconciler) joinControlplane(ctx context.Context, scope *S return ctrl.Result{}, nil } +// resolveFiles maps .Spec.Files into cloudinit.Files, resolving any object references +// along the way. +func (r *KubeadmConfigReconciler) resolveFiles(ctx context.Context, cfg *bootstrapv1.KubeadmConfig, merge ...bootstrapv1.File) ([]bootstrapv1.File, error) { + collected := append(cfg.Spec.Files, merge...) + + for i := range collected { + in := collected[i] + if in.ContentFrom != nil { + data, err := r.resolveSecretFileContent(ctx, cfg.Namespace, in) + if err != nil { + return nil, errors.Wrapf(err, "failed to resolve file source") + } + in.ContentFrom = nil + in.Content = string(data) + collected[i] = in + } + + } + + return collected, nil +} + +// resolveSecretFileContent returns file content fetched from a referenced secret object. +func (r *KubeadmConfigReconciler) resolveSecretFileContent(ctx context.Context, ns string, source bootstrapv1.File) ([]byte, error) { + secret := &corev1.Secret{} + key := types.NamespacedName{Namespace: ns, Name: source.ContentFrom.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.ContentFrom.Secret.Key] + if !ok { + return nil, errors.Errorf("secret references non-existent secret key: %q", source.ContentFrom.Secret.Key) + } + return data, nil +} + // ClusterToKubeadmConfigs is a handler.ToRequestsFunc to be used to enqeue // requests for reconciliation of KubeadmConfigs. func (r *KubeadmConfigReconciler) ClusterToKubeadmConfigs(o handler.MapObject) []ctrl.Request { 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 164e6a3d6982..b4510a484897 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 @@ -443,6 +443,29 @@ spec: content: description: Content is the actual content of the file. type: string + contentFrom: + description: ContentFrom is a referenced source of content + to populate the file. + properties: + secret: + description: Secret represents a secret that should + populate this file. + 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 encoding: description: Encoding specifies the encoding of the file contents. @@ -464,7 +487,6 @@ spec: to the file, e.g. "0640". type: string required: - - content - path type: object type: array