diff --git a/cmd/kubeadm/app/cmd/BUILD b/cmd/kubeadm/app/cmd/BUILD index a49e27ddb5f..cc986152d05 100644 --- a/cmd/kubeadm/app/cmd/BUILD +++ b/cmd/kubeadm/app/cmd/BUILD @@ -57,6 +57,7 @@ go_library( "//pkg/version:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/duration:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", diff --git a/cmd/kubeadm/app/cmd/config.go b/cmd/kubeadm/app/cmd/config.go index 5cf848ad6b6..9f9f888f6eb 100644 --- a/cmd/kubeadm/app/cmd/config.go +++ b/cmd/kubeadm/app/cmd/config.go @@ -17,6 +17,7 @@ limitations under the License. package cmd import ( + "bytes" "fmt" "io" "io/ioutil" @@ -28,6 +29,7 @@ import ( flag "github.com/spf13/pflag" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" clientset "k8s.io/client-go/kubernetes" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" @@ -44,14 +46,8 @@ import ( utilsexec "k8s.io/utils/exec" ) -const ( - // TODO: Figure out how to get these constants from the API machinery - masterConfig = "MasterConfiguration" - nodeConfig = "NodeConfiguration" -) - var ( - availableAPIObjects = []string{masterConfig, nodeConfig} + availableAPIObjects = []string{constants.MasterConfigurationKind, constants.NodeConfigurationKind} // sillyToken is only set statically to make kubeadm not randomize the token on every run sillyToken = kubeadmapiv1alpha3.BootstrapToken{ Token: &kubeadmapiv1alpha3.BootstrapTokenString{ @@ -110,16 +106,13 @@ func NewCmdConfigPrintDefault(out io.Writer) *cobra.Command { if len(apiObjects) == 0 { apiObjects = availableAPIObjects } - for i, apiObject := range apiObjects { - if i > 0 { - fmt.Fprintln(out, "---") - } - + allBytes := [][]byte{} + for _, apiObject := range apiObjects { cfgBytes, err := getDefaultAPIObjectBytes(apiObject) kubeadmutil.CheckErr(err) - // Print the API object byte array - fmt.Fprintf(out, "%s", cfgBytes) + allBytes = append(allBytes, cfgBytes) } + fmt.Fprint(out, string(bytes.Join(allBytes, []byte(constants.YAMLDocumentSeparator)))) }, } cmd.Flags().StringSliceVar(&apiObjects, "api-objects", apiObjects, @@ -128,26 +121,29 @@ func NewCmdConfigPrintDefault(out io.Writer) *cobra.Command { } func getDefaultAPIObjectBytes(apiObject string) ([]byte, error) { - if apiObject == masterConfig { - - internalcfg, err := configutil.ConfigFileAndDefaultsToInternalConfig("", &kubeadmapiv1alpha3.MasterConfiguration{ - BootstrapTokens: []kubeadmapiv1alpha3.BootstrapToken{sillyToken}, + var internalcfg runtime.Object + var err error + switch apiObject { + case constants.MasterConfigurationKind: + internalcfg, err = configutil.ConfigFileAndDefaultsToInternalConfig("", &kubeadmapiv1alpha3.MasterConfiguration{ + API: kubeadmapiv1alpha3.API{AdvertiseAddress: "1.2.3.4"}, + BootstrapTokens: []kubeadmapiv1alpha3.BootstrapToken{sillyToken}, + KubernetesVersion: fmt.Sprintf("v1.%d.0", constants.MinimumControlPlaneVersion.Minor()+1), }) - kubeadmutil.CheckErr(err) - - return kubeadmutil.MarshalToYamlForCodecs(internalcfg, kubeadmapiv1alpha3.SchemeGroupVersion, kubeadmscheme.Codecs) - } - if apiObject == nodeConfig { - internalcfg, err := configutil.NodeConfigFileAndDefaultsToInternalConfig("", &kubeadmapiv1alpha3.NodeConfiguration{ + case constants.NodeConfigurationKind: + internalcfg, err = configutil.NodeConfigFileAndDefaultsToInternalConfig("", &kubeadmapiv1alpha3.NodeConfiguration{ Token: sillyToken.Token.String(), DiscoveryTokenAPIServers: []string{"kube-apiserver:6443"}, DiscoveryTokenUnsafeSkipCAVerification: true, }) - kubeadmutil.CheckErr(err) - - return kubeadmutil.MarshalToYamlForCodecs(internalcfg, kubeadmapiv1alpha3.SchemeGroupVersion, kubeadmscheme.Codecs) + // TODO: DiscoveryTokenUnsafeSkipCAVerification: true needs to be set for validation to pass, but shouldn't be recommended as the default + default: + err = fmt.Errorf("--api-object needs to be one of %v", availableAPIObjects) } - return []byte{}, fmt.Errorf("--api-object needs to be one of %v", availableAPIObjects) + if err != nil { + return []byte{}, err + } + return kubeadmutil.MarshalToYamlForCodecs(internalcfg, kubeadmapiv1alpha3.SchemeGroupVersion, kubeadmscheme.Codecs) } // NewCmdConfigMigrate returns cobra.Command for "kubeadm config migrate" command @@ -176,31 +172,12 @@ func NewCmdConfigMigrate(out io.Writer) *cobra.Command { kubeadmutil.CheckErr(fmt.Errorf("The --old-config flag is mandatory")) } - b, err := ioutil.ReadFile(oldCfgPath) + internalcfg, err := configutil.AnyConfigFileAndDefaultsToInternal(oldCfgPath) kubeadmutil.CheckErr(err) - var outputBytes []byte - gvk, err := kubeadmutil.GroupVersionKindFromBytes(b, kubeadmscheme.Codecs) + outputBytes, err := kubeadmutil.MarshalToYamlForCodecs(internalcfg, kubeadmapiv1alpha3.SchemeGroupVersion, kubeadmscheme.Codecs) kubeadmutil.CheckErr(err) - switch gvk.Kind { - case masterConfig: - internalcfg, err := configutil.ConfigFileAndDefaultsToInternalConfig(oldCfgPath, &kubeadmapiv1alpha3.MasterConfiguration{}) - kubeadmutil.CheckErr(err) - - outputBytes, err = kubeadmutil.MarshalToYamlForCodecs(internalcfg, kubeadmapiv1alpha3.SchemeGroupVersion, kubeadmscheme.Codecs) - kubeadmutil.CheckErr(err) - case nodeConfig: - internalcfg, err := configutil.NodeConfigFileAndDefaultsToInternalConfig(oldCfgPath, &kubeadmapiv1alpha3.NodeConfiguration{}) - kubeadmutil.CheckErr(err) - - // TODO: In the future we might not want to duplicate these two lines of code for every case here. - outputBytes, err = kubeadmutil.MarshalToYamlForCodecs(internalcfg, kubeadmapiv1alpha3.SchemeGroupVersion, kubeadmscheme.Codecs) - kubeadmutil.CheckErr(err) - default: - kubeadmutil.CheckErr(fmt.Errorf("Didn't recognize type with GroupVersionKind: %v", gvk)) - } - if newCfgPath == "" { fmt.Fprint(out, string(outputBytes)) } else { diff --git a/cmd/kubeadm/app/cmd/phases/kubelet.go b/cmd/kubeadm/app/cmd/phases/kubelet.go index bfdf60dcfb0..4da1c7120c9 100644 --- a/cmd/kubeadm/app/cmd/phases/kubelet.go +++ b/cmd/kubeadm/app/cmd/phases/kubelet.go @@ -18,12 +18,10 @@ package phases import ( "fmt" - "io/ioutil" "github.com/spf13/cobra" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" - kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" kubeadmapiv1alpha3 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha3" "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" @@ -38,12 +36,6 @@ import ( utilsexec "k8s.io/utils/exec" ) -const ( - // TODO: Figure out how to get these constants from the API machinery - masterConfig = "MasterConfiguration" - nodeConfig = "NodeConfiguration" -) - var ( kubeletWriteEnvFileLongDesc = normalizer.LongDesc(` Writes an environment file with flags that should be passed to the kubelet executing on the master or node. @@ -56,7 +48,7 @@ var ( kubeadm alpha phase kubelet write-env-file --config masterconfig.yaml # Writes a dynamic environment file with kubelet flags from a NodeConfiguration file. - kubeadm alpha phase kubelet write-env-file --config nodeConfig.yaml + kubeadm alpha phase kubelet write-env-file --config nodeconfig.yaml `) kubeletConfigUploadLongDesc = normalizer.LongDesc(` @@ -144,12 +136,7 @@ func NewCmdKubeletWriteEnvFile() *cobra.Command { // RunKubeletWriteEnvFile is the function that is run when "kubeadm phase kubelet write-env-file" is executed func RunKubeletWriteEnvFile(cfgPath string) error { - b, err := ioutil.ReadFile(cfgPath) - if err != nil { - return err - } - - gvk, err := kubeadmutil.GroupVersionKindFromBytes(b, kubeadmscheme.Codecs) + internalcfg, err := configutil.AnyConfigFileAndDefaultsToInternal(cfgPath) if err != nil { return err } @@ -157,30 +144,18 @@ func RunKubeletWriteEnvFile(cfgPath string) error { var nodeRegistrationObj *kubeadmapi.NodeRegistrationOptions var featureGates map[string]bool var registerWithTaints bool - switch gvk.Kind { - case masterConfig: - internalcfg, err := configutil.ConfigFileAndDefaultsToInternalConfig(cfgPath, &kubeadmapiv1alpha3.MasterConfiguration{}) - if err != nil { - return err - } - nodeRegistrationObj = &internalcfg.NodeRegistration - featureGates = internalcfg.FeatureGates + + switch cfg := internalcfg.(type) { + case *kubeadmapi.MasterConfiguration: + nodeRegistrationObj = &cfg.NodeRegistration + featureGates = cfg.FeatureGates registerWithTaints = false - case nodeConfig: - internalcfg, err := configutil.NodeConfigFileAndDefaultsToInternalConfig(cfgPath, &kubeadmapiv1alpha3.NodeConfiguration{}) - if err != nil { - return err - } - nodeRegistrationObj = &internalcfg.NodeRegistration - featureGates = internalcfg.FeatureGates + case *kubeadmapi.NodeConfiguration: + nodeRegistrationObj = &cfg.NodeRegistration + featureGates = cfg.FeatureGates registerWithTaints = true default: - if err != nil { - return fmt.Errorf("Didn't recognize type with GroupVersionKind: %v", gvk) - } - } - if nodeRegistrationObj == nil { - return fmt.Errorf("couldn't load nodeRegistration field from config file") + return fmt.Errorf("couldn't read config file, no matching kind found") } if err := kubeletphase.WriteKubeletDynamicEnvFile(nodeRegistrationObj, featureGates, registerWithTaints, constants.KubeletRunDirectory); err != nil { diff --git a/cmd/kubeadm/app/constants/constants.go b/cmd/kubeadm/app/constants/constants.go index dcffdd069c1..f23ae929c32 100644 --- a/cmd/kubeadm/app/constants/constants.go +++ b/cmd/kubeadm/app/constants/constants.go @@ -285,6 +285,16 @@ const ( // CoreDNSVersion is the version of CoreDNS to be deployed if it is used CoreDNSVersion = "1.1.3" + + // MasterConfigurationKind is the string kind value for the MasterConfiguration struct + MasterConfigurationKind = "MasterConfiguration" + + // NodeConfigurationKind is the string kind value for the MasterConfiguration struct + NodeConfigurationKind = "NodeConfiguration" + + // YAMLDocumentSeparator is the separator for YAML documents + // TODO: Find a better place for this constant + YAMLDocumentSeparator = "---\n" ) var ( diff --git a/cmd/kubeadm/app/util/BUILD b/cmd/kubeadm/app/util/BUILD index 78f3b794181..93c51705859 100644 --- a/cmd/kubeadm/app/util/BUILD +++ b/cmd/kubeadm/app/util/BUILD @@ -21,14 +21,16 @@ go_library( importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util", deps = [ "//cmd/kubeadm/app/apis/kubeadm:go_default_library", + "//cmd/kubeadm/app/constants:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/net:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/yaml:go_default_library", "//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library", - "//vendor/gopkg.in/yaml.v2:go_default_library", + "//vendor/github.com/ghodss/yaml:go_default_library", "//vendor/k8s.io/utils/exec:go_default_library", ], ) @@ -49,8 +51,10 @@ go_test( "//cmd/kubeadm/app/apis/kubeadm:go_default_library", "//cmd/kubeadm/app/apis/kubeadm/scheme:go_default_library", "//cmd/kubeadm/app/apis/kubeadm/v1alpha3:go_default_library", + "//cmd/kubeadm/app/constants:go_default_library", "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", ], ) diff --git a/cmd/kubeadm/app/util/config/BUILD b/cmd/kubeadm/app/util/config/BUILD index e78aca8bb50..5a9f4a35966 100644 --- a/cmd/kubeadm/app/util/config/BUILD +++ b/cmd/kubeadm/app/util/config/BUILD @@ -10,6 +10,7 @@ go_library( name = "go_default_library", srcs = [ "cluster.go", + "common.go", "masterconfig.go", "nodeconfig.go", ], @@ -37,6 +38,7 @@ go_library( go_test( name = "go_default_test", srcs = [ + "common_test.go", "masterconfig_test.go", "nodeconfig_test.go", ], @@ -46,6 +48,7 @@ go_test( "//cmd/kubeadm/app/apis/kubeadm:go_default_library", "//cmd/kubeadm/app/apis/kubeadm/scheme:go_default_library", "//cmd/kubeadm/app/apis/kubeadm/v1alpha3:go_default_library", + "//cmd/kubeadm/app/constants:go_default_library", "//cmd/kubeadm/app/util:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/github.com/pmezard/go-difflib/difflib:go_default_library", diff --git a/cmd/kubeadm/app/util/config/common.go b/cmd/kubeadm/app/util/config/common.go new file mode 100644 index 00000000000..6c0e20b7470 --- /dev/null +++ b/cmd/kubeadm/app/util/config/common.go @@ -0,0 +1,118 @@ +/* +Copyright 2018 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 config + +import ( + "fmt" + "io/ioutil" + "strings" + + "github.com/golang/glog" + + "k8s.io/apimachinery/pkg/runtime" + kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + kubeadmapiv1alpha3 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha3" + "k8s.io/kubernetes/cmd/kubeadm/app/constants" + kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" + "k8s.io/kubernetes/pkg/util/version" +) + +// AnyConfigFileAndDefaultsToInternal reads either a MasterConfiguration or NodeConfiguration and unmarshals it +func AnyConfigFileAndDefaultsToInternal(cfgPath string) (runtime.Object, error) { + b, err := ioutil.ReadFile(cfgPath) + if err != nil { + return nil, err + } + + gvks, err := kubeadmutil.GroupVersionKindsFromBytes(b) + if err != nil { + return nil, err + } + + // First, check if the gvk list has MasterConfiguration and in that case try to unmarshal it + if kubeadmutil.GroupVersionKindsHasMasterConfiguration(gvks) { + return ConfigFileAndDefaultsToInternalConfig(cfgPath, &kubeadmapiv1alpha3.MasterConfiguration{}) + } + if kubeadmutil.GroupVersionKindsHasNodeConfiguration(gvks) { + return NodeConfigFileAndDefaultsToInternalConfig(cfgPath, &kubeadmapiv1alpha3.NodeConfiguration{}) + } + return nil, fmt.Errorf("didn't recognize types with GroupVersionKind: %v", gvks) +} + +// DetectUnsupportedVersion reads YAML bytes, extracts the TypeMeta information and errors out with an user-friendly message if the API spec is too old for this kubeadm version +func DetectUnsupportedVersion(b []byte) error { + gvks, err := kubeadmutil.GroupVersionKindsFromBytes(b) + if err != nil { + return err + } + + // TODO: On our way to making the kubeadm API beta and higher, give good user output in case they use an old config file with a new kubeadm version, and + // tell them how to upgrade. The support matrix will look something like this now and in the future: + // v1.10 and earlier: v1alpha1 + // v1.11: v1alpha1 read-only, writes only v1alpha2 config + // v1.12: v1alpha2 read-only, writes only v1beta1 config. Warns if the user tries to use v1alpha1 + // v1.13 and v1.14: v1beta1 read-only, writes only v1 config. Warns if the user tries to use v1alpha1 or v1alpha2. + // v1.15: v1 is the only supported format. + oldKnownAPIVersions := map[string]string{ + "kubeadm.k8s.io/v1alpha1": "v1.11", + } + // If we find an old API version in this gvk list, error out and tell the user why this doesn't work + for _, gvk := range gvks { + if useKubeadmVersion := oldKnownAPIVersions[gvk.GroupVersion().String()]; len(useKubeadmVersion) != 0 { + return fmt.Errorf("your configuration file uses an old API spec: %q. Please use kubeadm %s instead and run 'kubeadm config migrate --old-config old.yaml --new-config new.yaml', which will write the new, similar spec using a newer API version.", gvk.GroupVersion().String(), useKubeadmVersion) + } + } + return nil +} + +// NormalizeKubernetesVersion resolves version labels, sets alternative +// image registry if requested for CI builds, and validates minimal +// version that kubeadm SetInitDynamicDefaultssupports. +func NormalizeKubernetesVersion(cfg *kubeadmapi.MasterConfiguration) error { + // Requested version is automatic CI build, thus use KubernetesCI Image Repository for core images + if kubeadmutil.KubernetesIsCIVersion(cfg.KubernetesVersion) { + cfg.CIImageRepository = constants.DefaultCIImageRepository + } + + // Parse and validate the version argument and resolve possible CI version labels + ver, err := kubeadmutil.KubernetesReleaseVersion(cfg.KubernetesVersion) + if err != nil { + return err + } + cfg.KubernetesVersion = ver + + // Parse the given kubernetes version and make sure it's higher than the lowest supported + k8sVersion, err := version.ParseSemantic(cfg.KubernetesVersion) + if err != nil { + return fmt.Errorf("couldn't parse kubernetes version %q: %v", cfg.KubernetesVersion, err) + } + if k8sVersion.LessThan(constants.MinimumControlPlaneVersion) { + return fmt.Errorf("this version of kubeadm only supports deploying clusters with the control plane version >= %s. Current version: %s", constants.MinimumControlPlaneVersion.String(), cfg.KubernetesVersion) + } + return nil +} + +// LowercaseSANs can be used to force all SANs to be lowercase so it passes IsDNS1123Subdomain +func LowercaseSANs(sans []string) { + for i, san := range sans { + lowercase := strings.ToLower(san) + if lowercase != san { + glog.V(1).Infof("lowercasing SAN %q to %q", san, lowercase) + sans[i] = lowercase + } + } +} diff --git a/cmd/kubeadm/app/util/config/common_test.go b/cmd/kubeadm/app/util/config/common_test.go new file mode 100644 index 00000000000..e7029e3f8de --- /dev/null +++ b/cmd/kubeadm/app/util/config/common_test.go @@ -0,0 +1,158 @@ +/* +Copyright 2018 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 config + +import ( + "bytes" + "testing" + + kubeadmapiv1alpha3 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha3" + "k8s.io/kubernetes/cmd/kubeadm/app/constants" +) + +var files = map[string][]byte{ + "Master_v1alpha1": []byte(` +apiVersion: kubeadm.k8s.io/v1alpha1 +kind: MasterConfiguration +`), + "Node_v1alpha1": []byte(` +apiVersion: kubeadm.k8s.io/v1alpha1 +kind: NodeConfiguration +`), + "Master_v1alpha3": []byte(` +apiVersion: kubeadm.k8s.io/v1alpha3 +kind: MasterConfiguration +`), + "Node_v1alpha3": []byte(` +apiVersion: kubeadm.k8s.io/v1alpha3 +kind: NodeConfiguration +`), + "NoKind": []byte(` +apiVersion: baz.k8s.io/v1 +foo: foo +bar: bar +`), + "NoAPIVersion": []byte(` +kind: Bar +foo: foo +bar: bar +`), +} + +func TestDetectUnsupportedVersion(t *testing.T) { + var tests = []struct { + name string + fileContents []byte + expectedErr bool + }{ + { + name: "Master_v1alpha1", + fileContents: files["Master_v1alpha1"], + expectedErr: true, + }, + { + name: "Node_v1alpha1", + fileContents: files["Node_v1alpha1"], + expectedErr: true, + }, + { + name: "Master_v1alpha3", + fileContents: files["Master_v1alpha3"], + }, + { + name: "Node_v1alpha3", + fileContents: files["Node_v1alpha3"], + }, + { + name: "DuplicateMaster", + fileContents: bytes.Join([][]byte{files["Master_v1alpha3"], files["Master_v1alpha3"]}, []byte(constants.YAMLDocumentSeparator)), + expectedErr: true, + }, + { + name: "NoKind", + fileContents: files["NoKind"], + expectedErr: true, + }, + { + name: "NoAPIVersion", + fileContents: files["NoAPIVersion"], + expectedErr: true, + }, + { + name: "v1alpha1InMultiple", + fileContents: bytes.Join([][]byte{files["Master_v1alpha3"], files["Master_v1alpha1"]}, []byte(constants.YAMLDocumentSeparator)), + expectedErr: true, + }, + } + + for _, rt := range tests { + t.Run(rt.name, func(t2 *testing.T) { + + err := DetectUnsupportedVersion(rt.fileContents) + if (err != nil) != rt.expectedErr { + t2.Errorf("expected error: %t, actual: %t", rt.expectedErr, err != nil) + } + }) + } +} + +func TestLowercaseSANs(t *testing.T) { + tests := []struct { + name string + in []string + out []string + }{ + { + name: "empty struct", + }, + { + name: "already lowercase", + in: []string{"example.k8s.io"}, + out: []string{"example.k8s.io"}, + }, + { + name: "ip addresses and uppercase", + in: []string{"EXAMPLE.k8s.io", "10.100.0.1"}, + out: []string{"example.k8s.io", "10.100.0.1"}, + }, + { + name: "punycode and uppercase", + in: []string{"xn--7gq663byk9a.xn--fiqz9s", "ANOTHEREXAMPLE.k8s.io"}, + out: []string{"xn--7gq663byk9a.xn--fiqz9s", "anotherexample.k8s.io"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cfg := &kubeadmapiv1alpha3.MasterConfiguration{ + APIServerCertSANs: test.in, + } + + LowercaseSANs(cfg.APIServerCertSANs) + + if len(cfg.APIServerCertSANs) != len(test.out) { + t.Fatalf("expected %d elements, got %d", len(test.out), len(cfg.APIServerCertSANs)) + } + + for i, expected := range test.out { + if cfg.APIServerCertSANs[i] != expected { + t.Errorf("expected element %d to be %q, got %q", i, expected, cfg.APIServerCertSANs[i]) + } + } + }) + } +} diff --git a/cmd/kubeadm/app/util/config/masterconfig.go b/cmd/kubeadm/app/util/config/masterconfig.go index e39037c0b07..a1a27fe0cc7 100644 --- a/cmd/kubeadm/app/util/config/masterconfig.go +++ b/cmd/kubeadm/app/util/config/masterconfig.go @@ -20,7 +20,6 @@ import ( "fmt" "io/ioutil" "net" - "strings" "github.com/golang/glog" @@ -33,9 +32,7 @@ import ( kubeadmapiv1alpha3 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha3" "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/validation" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" - kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" - "k8s.io/kubernetes/pkg/util/node" - "k8s.io/kubernetes/pkg/util/version" + nodeutil "k8s.io/kubernetes/pkg/util/node" ) // SetInitDynamicDefaults checks and sets configuration values for the MasterConfiguration object @@ -88,7 +85,7 @@ func SetInitDynamicDefaults(cfg *kubeadmapi.MasterConfiguration) error { cfg.BootstrapTokens[i].Token = token } - cfg.NodeRegistration.Name = node.GetHostname(cfg.NodeRegistration.Name) + cfg.NodeRegistration.Name = nodeutil.GetHostname(cfg.NodeRegistration.Name) // Only if the slice is nil, we should append the master taint. This allows the user to specify an empty slice for no default master taint if cfg.NodeRegistration.Taints == nil { @@ -152,64 +149,3 @@ func defaultAndValidate(cfg *kubeadmapi.MasterConfiguration) (*kubeadmapi.Master } return cfg, nil } - -// DetectUnsupportedVersion reads YAML bytes, extracts the TypeMeta information and errors out with an user-friendly message if the API spec is too old for this kubeadm version -func DetectUnsupportedVersion(b []byte) error { - apiVersionStr, _, err := kubeadmutil.ExtractAPIVersionAndKindFromYAML(b) - if err != nil { - return err - } - - // TODO: On our way to making the kubeadm API beta and higher, give good user output in case they use an old config file with a new kubeadm version, and - // tell them how to upgrade. The support matrix will look something like this now and in the future: - // v1.10 and earlier: v1alpha1 - // v1.11: v1alpha1 read-only, writes only v1alpha2 config - // v1.12: v1alpha2 read-only, writes only v1beta1 config. Warns if the user tries to use v1alpha1 - // v1.13 and v1.14: v1beta1 read-only, writes only v1 config. Warns if the user tries to use v1alpha1 or v1alpha2. - // v1.15: v1 is the only supported format. - oldKnownAPIVersions := map[string]string{ - "kubeadm.k8s.io/v1alpha1": "v1.11", - } - if useKubeadmVersion := oldKnownAPIVersions[apiVersionStr]; len(useKubeadmVersion) != 0 { - return fmt.Errorf("your configuration file seem to use an old API spec. Please use kubeadm %s instead and run 'kubeadm config migrate --old-config old.yaml --new-config new.yaml', which will write the new, similar spec using a newer API version.", useKubeadmVersion) - } - return nil -} - -// NormalizeKubernetesVersion resolves version labels, sets alternative -// image registry if requested for CI builds, and validates minimal -// version that kubeadm supports. -func NormalizeKubernetesVersion(cfg *kubeadmapi.MasterConfiguration) error { - // Requested version is automatic CI build, thus use KubernetesCI Image Repository for core images - if kubeadmutil.KubernetesIsCIVersion(cfg.KubernetesVersion) { - cfg.CIImageRepository = kubeadmconstants.DefaultCIImageRepository - } - - // Parse and validate the version argument and resolve possible CI version labels - ver, err := kubeadmutil.KubernetesReleaseVersion(cfg.KubernetesVersion) - if err != nil { - return err - } - cfg.KubernetesVersion = ver - - // Parse the given kubernetes version and make sure it's higher than the lowest supported - k8sVersion, err := version.ParseSemantic(cfg.KubernetesVersion) - if err != nil { - return fmt.Errorf("couldn't parse kubernetes version %q: %v", cfg.KubernetesVersion, err) - } - if k8sVersion.LessThan(kubeadmconstants.MinimumControlPlaneVersion) { - return fmt.Errorf("this version of kubeadm only supports deploying clusters with the control plane version >= %s. Current version: %s", kubeadmconstants.MinimumControlPlaneVersion.String(), cfg.KubernetesVersion) - } - return nil -} - -// LowercaseSANs can be used to force all SANs to be lowercase so it passes IsDNS1123Subdomain -func LowercaseSANs(sans []string) { - for i, san := range sans { - lowercase := strings.ToLower(san) - if lowercase != san { - glog.V(1).Infof("lowercasing SAN %q to %q", san, lowercase) - sans[i] = lowercase - } - } -} diff --git a/cmd/kubeadm/app/util/config/masterconfig_test.go b/cmd/kubeadm/app/util/config/masterconfig_test.go index 572989f466b..291ffe0d0cf 100644 --- a/cmd/kubeadm/app/util/config/masterconfig_test.go +++ b/cmd/kubeadm/app/util/config/masterconfig_test.go @@ -127,50 +127,3 @@ func TestConfigFileAndDefaultsToInternalConfig(t *testing.T) { }) } } - -func TestLowercaseSANs(t *testing.T) { - tests := []struct { - name string - in []string - out []string - }{ - { - name: "empty struct", - }, - { - name: "already lowercase", - in: []string{"example.k8s.io"}, - out: []string{"example.k8s.io"}, - }, - { - name: "ip addresses and uppercase", - in: []string{"EXAMPLE.k8s.io", "10.100.0.1"}, - out: []string{"example.k8s.io", "10.100.0.1"}, - }, - { - name: "punycode and uppercase", - in: []string{"xn--7gq663byk9a.xn--fiqz9s", "ANOTHEREXAMPLE.k8s.io"}, - out: []string{"xn--7gq663byk9a.xn--fiqz9s", "anotherexample.k8s.io"}, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - cfg := &kubeadmapiv1alpha3.MasterConfiguration{ - APIServerCertSANs: test.in, - } - - LowercaseSANs(cfg.APIServerCertSANs) - - if len(cfg.APIServerCertSANs) != len(test.out) { - t.Fatalf("expected %d elements, got %d", len(test.out), len(cfg.APIServerCertSANs)) - } - - for i, expected := range test.out { - if cfg.APIServerCertSANs[i] != expected { - t.Errorf("expected element %d to be %q, got %q", i, expected, cfg.APIServerCertSANs[i]) - } - } - }) - } -} diff --git a/cmd/kubeadm/app/util/marshal.go b/cmd/kubeadm/app/util/marshal.go index c5a1b01611c..94c80416e23 100644 --- a/cmd/kubeadm/app/util/marshal.go +++ b/cmd/kubeadm/app/util/marshal.go @@ -17,15 +17,20 @@ limitations under the License. package util import ( - "errors" + "bufio" + "bytes" "fmt" + "io" - yaml "gopkg.in/yaml.v2" + "github.com/ghodss/yaml" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/util/errors" + utilyaml "k8s.io/apimachinery/pkg/util/yaml" clientsetscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/kubernetes/cmd/kubeadm/app/constants" ) // MarshalToYaml marshals an object into yaml. @@ -66,67 +71,88 @@ func UnmarshalFromYamlForCodecs(buffer []byte, gv schema.GroupVersion, codecs se return runtime.Decode(decoder, buffer) } -// ExtractAPIVersionAndKindFromYAML extracts the APIVersion and Kind fields from YAML bytes -func ExtractAPIVersionAndKindFromYAML(b []byte) (string, string, error) { - decoded, err := LoadYAML(b) - if err != nil { - return "", "", fmt.Errorf("unable to decode config from bytes: %v", err) - } - - kindStr, ok := decoded["kind"].(string) - if !ok || len(kindStr) == 0 { - return "", "", fmt.Errorf("any config file must have the kind field set") - } - apiVersionStr, ok := decoded["apiVersion"].(string) - if !ok || len(apiVersionStr) == 0 { - return "", "", fmt.Errorf("any config file must have the apiVersion field set") - } - return apiVersionStr, kindStr, nil -} - -// GroupVersionKindFromBytes parses the bytes and returns the gvk -// TODO: Find a better way to do this, invoking the API machinery directly without first loading the yaml manually -func GroupVersionKindFromBytes(b []byte, codecs serializer.CodecFactory) (schema.GroupVersionKind, error) { - apiVersionStr, kindStr, err := ExtractAPIVersionAndKindFromYAML(b) - if err != nil { - return schema.EmptyObjectKind.GroupVersionKind(), err - } - - gv, err := schema.ParseGroupVersion(apiVersionStr) - if err != nil { - return schema.EmptyObjectKind.GroupVersionKind(), fmt.Errorf("unable to parse apiVersion: %v", err) - } - return gv.WithKind(kindStr), nil -} - -// LoadYAML is a small wrapper around go-yaml that ensures all nested structs are map[string]interface{} instead of map[interface{}]interface{}. -func LoadYAML(bytes []byte) (map[string]interface{}, error) { - var decoded map[interface{}]interface{} - if err := yaml.Unmarshal(bytes, &decoded); err != nil { - return map[string]interface{}{}, fmt.Errorf("couldn't unmarshal YAML: %v", err) - } - - converted, ok := convert(decoded).(map[string]interface{}) - if !ok { - return map[string]interface{}{}, errors.New("yaml is not a map") - } - - return converted, nil -} - -// https://stackoverflow.com/questions/40737122/convert-yaml-to-json-without-struct-golang -func convert(i interface{}) interface{} { - switch x := i.(type) { - case map[interface{}]interface{}: - m2 := map[string]interface{}{} - for k, v := range x { - m2[k.(string)] = convert(v) +// SplitYAMLDocuments reads the YAML bytes per-document, unmarshals the TypeMeta information from each document +// and returns a map between the GroupVersionKind of the document and the document bytes +func SplitYAMLDocuments(yamlBytes []byte) (map[schema.GroupVersionKind][]byte, error) { + gvkmap := map[schema.GroupVersionKind][]byte{} + knownKinds := map[string]bool{} + errs := []error{} + buf := bytes.NewBuffer(yamlBytes) + reader := utilyaml.NewYAMLReader(bufio.NewReader(buf)) + for { + typeMetaInfo := runtime.TypeMeta{} + // Read one YAML document at a time, until io.EOF is returned + b, err := reader.Read() + if err == io.EOF { + break + } else if err != nil { + return nil, err } - return m2 - case []interface{}: - for i, v := range x { - x[i] = convert(v) + if len(b) == 0 { + break + } + // Deserialize the TypeMeta information of this byte slice + if err := yaml.Unmarshal(b, &typeMetaInfo); err != nil { + return nil, err + } + // Require TypeMeta information to be present + if len(typeMetaInfo.APIVersion) == 0 || len(typeMetaInfo.Kind) == 0 { + errs = append(errs, fmt.Errorf("invalid configuration: kind and apiVersion is mandatory information that needs to be specified in all YAML documents")) + continue + } + // Check whether the kind has been registered before. If it has, throw an error + if known := knownKinds[typeMetaInfo.Kind]; known { + errs = append(errs, fmt.Errorf("invalid configuration: kind %q is specified twice in YAML file", typeMetaInfo.Kind)) + continue + } + knownKinds[typeMetaInfo.Kind] = true + + // Build a GroupVersionKind object from the deserialized TypeMeta object + gv, err := schema.ParseGroupVersion(typeMetaInfo.APIVersion) + if err != nil { + errs = append(errs, fmt.Errorf("unable to parse apiVersion: %v", err)) + continue + } + gvk := gv.WithKind(typeMetaInfo.Kind) + + // Save the mapping between the gvk and the bytes that object consists of + gvkmap[gvk] = b + } + if err := errors.NewAggregate(errs); err != nil { + return nil, err + } + return gvkmap, nil +} + +// GroupVersionKindsFromBytes parses the bytes and returns a gvk slice +func GroupVersionKindsFromBytes(b []byte) ([]schema.GroupVersionKind, error) { + gvkmap, err := SplitYAMLDocuments(b) + if err != nil { + return nil, err + } + gvks := []schema.GroupVersionKind{} + for gvk := range gvkmap { + gvks = append(gvks, gvk) + } + return gvks, nil +} + +// GroupVersionKindsHasKind returns whether the following gvk slice contains the kind given as a parameter +func GroupVersionKindsHasKind(gvks []schema.GroupVersionKind, kind string) bool { + for _, gvk := range gvks { + if gvk.Kind == kind { + return true } } - return i + return false +} + +// GroupVersionKindsHasMasterConfiguration returns whether the following gvk slice contains a MasterConfiguration object +func GroupVersionKindsHasMasterConfiguration(gvks []schema.GroupVersionKind) bool { + return GroupVersionKindsHasKind(gvks, constants.MasterConfigurationKind) +} + +// GroupVersionKindsHasNodeConfiguration returns whether the following gvk slice contains a NodeConfiguration object +func GroupVersionKindsHasNodeConfiguration(gvks []schema.GroupVersionKind) bool { + return GroupVersionKindsHasKind(gvks, constants.NodeConfigurationKind) } diff --git a/cmd/kubeadm/app/util/marshal_test.go b/cmd/kubeadm/app/util/marshal_test.go index e0357baf24b..d92b4cd0d6d 100644 --- a/cmd/kubeadm/app/util/marshal_test.go +++ b/cmd/kubeadm/app/util/marshal_test.go @@ -17,15 +17,48 @@ limitations under the License. package util import ( + "bytes" "reflect" + "sort" "testing" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" kubeadmapiv1alpha3 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha3" + "k8s.io/kubernetes/cmd/kubeadm/app/constants" ) +var files = map[string][]byte{ + "foo": []byte(` +kind: Foo +apiVersion: foo.k8s.io/v1 +fooField: foo +`), + "bar": []byte(` +apiVersion: bar.k8s.io/v2 +barField: bar +kind: Bar +`), + "baz": []byte(` +apiVersion: baz.k8s.io/v1 +kind: Baz +baz: + foo: bar +`), + "nokind": []byte(` +apiVersion: baz.k8s.io/v1 +foo: foo +bar: bar +`), + "noapiversion": []byte(` +kind: Bar +foo: foo +bar: bar +`), +} + func TestMarshalUnmarshalYaml(t *testing.T) { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -114,3 +147,248 @@ func TestMarshalUnmarshalToYamlForCodecs(t *testing.T) { t.Errorf("expected %v, got %v", *cfg, *cfg2) } } + +func TestSplitYAMLDocuments(t *testing.T) { + var tests = []struct { + name string + fileContents []byte + gvkmap map[schema.GroupVersionKind][]byte + expectedErr bool + }{ + { + name: "FooOnly", + fileContents: files["foo"], + gvkmap: map[schema.GroupVersionKind][]byte{ + {Group: "foo.k8s.io", Version: "v1", Kind: "Foo"}: files["foo"], + }, + }, + { + name: "FooBar", + fileContents: bytes.Join([][]byte{files["foo"], files["bar"]}, []byte(constants.YAMLDocumentSeparator)), + gvkmap: map[schema.GroupVersionKind][]byte{ + {Group: "foo.k8s.io", Version: "v1", Kind: "Foo"}: files["foo"], + {Group: "bar.k8s.io", Version: "v2", Kind: "Bar"}: files["bar"], + }, + }, + { + name: "FooTwiceInvalid", + fileContents: bytes.Join([][]byte{files["foo"], files["bar"], files["foo"]}, []byte(constants.YAMLDocumentSeparator)), + expectedErr: true, + }, + { + name: "InvalidBaz", + fileContents: bytes.Join([][]byte{files["foo"], files["baz"]}, []byte(constants.YAMLDocumentSeparator)), + expectedErr: true, + }, + { + name: "InvalidNoKind", + fileContents: files["nokind"], + expectedErr: true, + }, + { + name: "InvalidNoAPIVersion", + fileContents: files["noapiversion"], + expectedErr: true, + }, + } + + for _, rt := range tests { + t.Run(rt.name, func(t2 *testing.T) { + + gvkmap, err := SplitYAMLDocuments(rt.fileContents) + if (err != nil) != rt.expectedErr { + t2.Errorf("expected error: %t, actual: %t", rt.expectedErr, err != nil) + } + + if !reflect.DeepEqual(gvkmap, rt.gvkmap) { + t2.Errorf("expected gvkmap: %s\n\tactual: %s\n", rt.gvkmap, gvkmap) + } + }) + } +} + +func TestGroupVersionKindsFromBytes(t *testing.T) { + var tests = []struct { + name string + fileContents []byte + gvks []string + expectedErr bool + }{ + { + name: "FooOnly", + fileContents: files["foo"], + gvks: []string{ + "foo.k8s.io/v1, Kind=Foo", + }, + }, + { + name: "FooBar", + fileContents: bytes.Join([][]byte{files["foo"], files["bar"]}, []byte(constants.YAMLDocumentSeparator)), + gvks: []string{ + "foo.k8s.io/v1, Kind=Foo", + "bar.k8s.io/v2, Kind=Bar", + }, + }, + { + name: "FooTwiceInvalid", + fileContents: bytes.Join([][]byte{files["foo"], files["bar"], files["foo"]}, []byte(constants.YAMLDocumentSeparator)), + gvks: []string{}, + expectedErr: true, + }, + { + name: "InvalidBaz", + fileContents: bytes.Join([][]byte{files["foo"], files["baz"]}, []byte(constants.YAMLDocumentSeparator)), + gvks: []string{}, + expectedErr: true, + }, + { + name: "InvalidNoKind", + fileContents: files["nokind"], + gvks: []string{}, + expectedErr: true, + }, + { + name: "InvalidNoAPIVersion", + fileContents: files["noapiversion"], + gvks: []string{}, + expectedErr: true, + }, + } + + for _, rt := range tests { + t.Run(rt.name, func(t2 *testing.T) { + + gvks, err := GroupVersionKindsFromBytes(rt.fileContents) + if (err != nil) != rt.expectedErr { + t2.Errorf("expected error: %t, actual: %t", rt.expectedErr, err != nil) + } + + strgvks := []string{} + for _, gvk := range gvks { + strgvks = append(strgvks, gvk.String()) + } + sort.Strings(strgvks) + sort.Strings(rt.gvks) + + if !reflect.DeepEqual(strgvks, rt.gvks) { + t2.Errorf("expected gvks: %s\n\tactual: %s\n", rt.gvks, strgvks) + } + }) + } +} + +func TestGroupVersionKindsHasKind(t *testing.T) { + var tests = []struct { + name string + gvks []schema.GroupVersionKind + kind string + expected bool + }{ + { + name: "FooOnly", + gvks: []schema.GroupVersionKind{ + {Group: "foo.k8s.io", Version: "v1", Kind: "Foo"}, + }, + kind: "Foo", + expected: true, + }, + { + name: "FooBar", + gvks: []schema.GroupVersionKind{ + {Group: "foo.k8s.io", Version: "v1", Kind: "Foo"}, + {Group: "bar.k8s.io", Version: "v2", Kind: "Bar"}, + }, + kind: "Bar", + expected: true, + }, + { + name: "FooBazNoBaz", + gvks: []schema.GroupVersionKind{ + {Group: "foo.k8s.io", Version: "v1", Kind: "Foo"}, + {Group: "bar.k8s.io", Version: "v2", Kind: "Bar"}, + }, + kind: "Baz", + expected: false, + }, + } + + for _, rt := range tests { + t.Run(rt.name, func(t2 *testing.T) { + + actual := GroupVersionKindsHasKind(rt.gvks, rt.kind) + if rt.expected != actual { + t2.Errorf("expected gvks has kind: %t\n\tactual: %t\n", rt.expected, actual) + } + }) + } +} + +func TestGroupVersionKindsHasMasterConfiguration(t *testing.T) { + var tests = []struct { + name string + gvks []schema.GroupVersionKind + kind string + expected bool + }{ + { + name: "NoMasterConfiguration", + gvks: []schema.GroupVersionKind{ + {Group: "foo.k8s.io", Version: "v1", Kind: "Foo"}, + }, + expected: false, + }, + { + name: "MasterConfigurationFound", + gvks: []schema.GroupVersionKind{ + {Group: "foo.k8s.io", Version: "v1", Kind: "Foo"}, + {Group: "bar.k8s.io", Version: "v2", Kind: "MasterConfiguration"}, + }, + expected: true, + }, + } + + for _, rt := range tests { + t.Run(rt.name, func(t2 *testing.T) { + + actual := GroupVersionKindsHasMasterConfiguration(rt.gvks) + if rt.expected != actual { + t2.Errorf("expected gvks has MasterConfiguration: %t\n\tactual: %t\n", rt.expected, actual) + } + }) + } +} + +func TestGroupVersionKindsHasNodeConfiguration(t *testing.T) { + var tests = []struct { + name string + gvks []schema.GroupVersionKind + kind string + expected bool + }{ + { + name: "NoNodeConfiguration", + gvks: []schema.GroupVersionKind{ + {Group: "foo.k8s.io", Version: "v1", Kind: "Foo"}, + }, + expected: false, + }, + { + name: "NodeConfigurationFound", + gvks: []schema.GroupVersionKind{ + {Group: "foo.k8s.io", Version: "v1", Kind: "Foo"}, + {Group: "bar.k8s.io", Version: "v2", Kind: "NodeConfiguration"}, + }, + expected: true, + }, + } + + for _, rt := range tests { + t.Run(rt.name, func(t2 *testing.T) { + + actual := GroupVersionKindsHasNodeConfiguration(rt.gvks) + if rt.expected != actual { + t2.Errorf("expected gvks has NodeConfiguration: %t\n\tactual: %t\n", rt.expected, actual) + } + }) + } +}