Merge pull request #65631 from luxas/kubeadm_support_yaml_documents

Automatic merge from submit-queue (batch tested with PRs 65822, 65834, 65859, 65631). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

kubeadm: Add support for reading multiple YAML documents

**What this PR does / why we need it**:
In preparation for splitting the kubelet and kube-proxy componentconfigs out of the MasterConfiguration API struct, add support for reading multiple YAML documents

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
ref: kubernetes/kubeadm#911
Depends on:
 - [x] https://github.com/kubernetes/kubernetes/pull/65776
 - [x] https://github.com/kubernetes/kubernetes/pull/65628
 - [x] https://github.com/kubernetes/kubernetes/pull/65629

**Special notes for your reviewer**:
Please only review the `Refactor a bit of the config YAML loading code, and support loading multiple YAML documents` commit

**Release note**:

```release-note
NONE
```
@kubernetes/sig-cluster-lifecycle-pr-reviews
This commit is contained in:
Kubernetes Submit Queue 2018-07-05 10:16:12 -07:00 committed by GitHub
commit d10ff1a205
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 700 additions and 261 deletions

View File

@ -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",

View File

@ -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 {

View File

@ -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 {

View File

@ -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 (

View File

@ -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",
],
)

View File

@ -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",

View File

@ -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
}
}
}

View File

@ -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])
}
}
})
}
}

View File

@ -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
}
}
}

View File

@ -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])
}
}
})
}
}

View File

@ -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)
}

View File

@ -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)
}
})
}
}