diff --git a/bootstrap/kubeadm/api/v1alpha2/conversion.go b/bootstrap/kubeadm/api/v1alpha2/conversion.go index 98a98c0e390d..9524f2cc6623 100644 --- a/bootstrap/kubeadm/api/v1alpha2/conversion.go +++ b/bootstrap/kubeadm/api/v1alpha2/conversion.go @@ -40,10 +40,23 @@ func (src *KubeadmConfig) ConvertTo(dstRaw conversion.Hub) error { 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) + + // Track files successfully up-converted. We need this to dedupe + // restored files from user-updated files on up-conversion. We store + // them as pointers for later modification without paying for second + // lookup. + dstPaths := make(map[string]*kubeadmbootstrapv1alpha3.File, len(dst.Spec.Files)) + for i := range dst.Spec.Files { + file := dst.Spec.Files[i] + dstPaths[file.Path] = &file + } + + // If we find a restored file matching the file path of a v1alpha2 + // file with no content, we should restore contentFrom to that file. + for _, restoredFile := range restored.Spec.Files { + dstFile, exists := dstPaths[restoredFile.Path] + if exists && dstFile.Content == "" { + dstFile.ContentFrom = restoredFile.ContentFrom } } diff --git a/bootstrap/kubeadm/api/v1alpha2/conversion_test.go b/bootstrap/kubeadm/api/v1alpha2/conversion_test.go index 76940f494dad..22f2129916ac 100644 --- a/bootstrap/kubeadm/api/v1alpha2/conversion_test.go +++ b/bootstrap/kubeadm/api/v1alpha2/conversion_test.go @@ -19,6 +19,8 @@ package v1alpha2 import ( "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -33,11 +35,16 @@ func TestConvertKubeadmConfig(t *testing.T) { src := &v1alpha3.KubeadmConfig{ ObjectMeta: metav1.ObjectMeta{ - Name: "hub", + Name: "hub", + Annotations: map[string]string{}, }, Spec: v1alpha3.KubeadmConfigSpec{ Files: []v1alpha3.File{ { + Path: "/etc/another/file", + Owner: "ubuntu:ubuntu", + Encoding: v1alpha3.GzipBase64, + Permissions: "0600", ContentFrom: &v1alpha3.FileSource{ Secret: v1alpha3.SecretFileSource{ Name: "foo", @@ -46,7 +53,11 @@ func TestConvertKubeadmConfig(t *testing.T) { }, }, { - Content: "baz", + Path: "/etc/kubernetes/azure.json", + Owner: "root:root", + Encoding: v1alpha3.Base64, + Permissions: "0644", + Content: "baz", }, }, }, @@ -55,9 +66,9 @@ func TestConvertKubeadmConfig(t *testing.T) { DataSecretName: pointer.StringPtr("secret-data"), }, } - dst := &KubeadmConfig{} - g.Expect(dst.ConvertFrom(src)).To(Succeed()) + dst := &KubeadmConfig{} + g.Expect(dst.ConvertFrom(src.DeepCopy())).To(Succeed()) restored := &v1alpha3.KubeadmConfig{} g.Expect(dst.ConvertTo(restored)).To(Succeed()) @@ -65,6 +76,13 @@ func TestConvertKubeadmConfig(t *testing.T) { g.Expect(restored.Name).To(Equal(src.Name)) g.Expect(restored.Status.Ready).To(Equal(src.Status.Ready)) g.Expect(restored.Status.DataSecretName).To(Equal(src.Status.DataSecretName)) + + diff := cmp.Diff(src.Spec.Files, restored.Spec.Files, cmpopts.SortSlices(func(i, j v1alpha3.File) bool { + return i.Path < j.Path + })) + if diff != "" { + t.Fatalf(diff) + } }) }) } diff --git a/bootstrap/kubeadm/api/v1alpha3/kubeadmbootstrapconfig_types_test.go b/bootstrap/kubeadm/api/v1alpha3/kubeadmbootstrapconfig_types_test.go index 3d72ff76bb95..156324019d77 100644 --- a/bootstrap/kubeadm/api/v1alpha3/kubeadmbootstrapconfig_types_test.go +++ b/bootstrap/kubeadm/api/v1alpha3/kubeadmbootstrapconfig_types_test.go @@ -126,6 +126,25 @@ func TestClusterValidate(t *testing.T) { }, expectErr: true, }, + "invalid with duplicate file path": { + in: &KubeadmConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + Namespace: "default", + }, + Spec: KubeadmConfigSpec{ + Files: []File{ + { + Content: "foo", + }, + { + Content: "bar", + }, + }, + }, + }, + expectErr: true, + }, } for name, tt := range cases { diff --git a/bootstrap/kubeadm/api/v1alpha3/kubeadmconfig_webhook.go b/bootstrap/kubeadm/api/v1alpha3/kubeadmconfig_webhook.go index c8e95a9063a5..a845d55c8782 100644 --- a/bootstrap/kubeadm/api/v1alpha3/kubeadmconfig_webhook.go +++ b/bootstrap/kubeadm/api/v1alpha3/kubeadmconfig_webhook.go @@ -31,6 +31,7 @@ var ( 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" + PathConflictMsg = "path property must be unique among all files" ) func (c *KubeadmConfig) SetupWebhookWithManager(mgr ctrl.Manager) error { @@ -61,6 +62,8 @@ func (c *KubeadmConfig) ValidateDelete() error { func (c *KubeadmConfigSpec) validate(name string) error { var allErrs field.ErrorList + knownPaths := map[string]struct{}{} + for i := range c.Files { file := c.Files[i] if file.Content != "" && file.ContentFrom != nil { @@ -98,6 +101,18 @@ func (c *KubeadmConfigSpec) validate(name string) error { ) } } + _, conflict := knownPaths[file.Path] + if conflict { + allErrs = append( + allErrs, + field.Invalid( + field.NewPath("spec", "files", fmt.Sprintf("%d", i), "path"), + file, + PathConflictMsg, + ), + ) + } + knownPaths[file.Path] = struct{}{} } if len(allErrs) == 0 { diff --git a/go.mod b/go.mod index 3cacb81226e5..f2ab225e1498 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/evanphx/json-patch v4.5.0+incompatible github.com/go-logr/logr v0.1.0 github.com/gogo/protobuf v1.3.1 + github.com/google/go-cmp v0.4.0 github.com/google/go-github v17.0.0+incompatible github.com/google/go-querystring v1.0.0 // indirect github.com/google/gofuzz v1.1.0