Skip to content

Commit 1d2d15e

Browse files
committed
kubeadm upgrade: Allow supplying hand migrated component configs
Currently, kubeadm would refuse to perfom an upgrade (or even planing for one) if it detects a user supplied unsupported component config version. Hence, users are required to manually upgrade their component configs and store them in the config maps prior to executing `kubeadm upgrade plan` or `kubeadm upgrade apply`. This change introduces the ability to use the `--config` option of the `kubeadm upgrade plan` and `kubeadm upgrade apply` commands to supply a YAML file containing component configs to be used in place of the existing ones in the cluster upon upgrade. The old behavior where `--config` is used to reconfigure a cluster is still supported. kubeadm automatically detects which behavior to use based on the presence (or absense) of kubeadm config types (API group `kubeadm.kubernetes.io`). Signed-off-by: Rostislav M. Georgiev <[email protected]>
1 parent 5d01274 commit 1d2d15e

File tree

5 files changed

+283
-15
lines changed

5 files changed

+283
-15
lines changed

cmd/kubeadm/app/cmd/upgrade/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ go_library(
2020
"//cmd/kubeadm/app/cmd/phases/upgrade/node:go_default_library",
2121
"//cmd/kubeadm/app/cmd/phases/workflow:go_default_library",
2222
"//cmd/kubeadm/app/cmd/util:go_default_library",
23+
"//cmd/kubeadm/app/componentconfigs:go_default_library",
2324
"//cmd/kubeadm/app/constants:go_default_library",
2425
"//cmd/kubeadm/app/features:go_default_library",
2526
"//cmd/kubeadm/app/phases/controlplane:go_default_library",

cmd/kubeadm/app/cmd/upgrade/common.go

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"bytes"
2222
"fmt"
2323
"io"
24+
"io/ioutil"
2425
"os"
2526
"strings"
2627
"time"
@@ -36,16 +37,89 @@ import (
3637
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
3738
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/validation"
3839
cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util"
40+
"k8s.io/kubernetes/cmd/kubeadm/app/componentconfigs"
3941
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
4042
"k8s.io/kubernetes/cmd/kubeadm/app/features"
4143
"k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade"
4244
"k8s.io/kubernetes/cmd/kubeadm/app/preflight"
45+
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
4346
"k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient"
4447
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
4548
dryrunutil "k8s.io/kubernetes/cmd/kubeadm/app/util/dryrun"
4649
kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig"
4750
)
4851

52+
// isKubeadmConfigPresent checks if a kubeadm config type is found in the provided document map
53+
func isKubeadmConfigPresent(docmap kubeadmapi.DocumentMap) bool {
54+
for gvk := range docmap {
55+
if gvk.Group == kubeadmapi.GroupName {
56+
return true
57+
}
58+
}
59+
return false
60+
}
61+
62+
// loadConfig loads configuration from a file and/or the cluster. InitConfiguration, ClusterConfiguration and (optionally) component configs
63+
// are loaded. This function allows the component configs to be loaded from a file that contains only them. If the file contains any kubeadm types
64+
// in it (API group "kubeadm.kubernetes.io" present), then the supplied file is treaded as a legacy reconfiguration style "--config" use and the
65+
// returned bool value is set to true (the only case to be done so).
66+
func loadConfig(cfgPath string, client clientset.Interface, skipComponentConfigs bool) (*kubeadmapi.InitConfiguration, bool, error) {
67+
// Used for info logs here
68+
const logPrefix = "upgrade/config"
69+
70+
// The usual case here is to not have a config file, but rather load the config from the cluster.
71+
// This is probably 90% of the time. So we handle it first.
72+
if cfgPath == "" {
73+
cfg, err := configutil.FetchInitConfigurationFromCluster(client, os.Stdout, logPrefix, false, skipComponentConfigs)
74+
return cfg, false, err
75+
}
76+
77+
// Otherwise, we have a config file. Let's load it.
78+
configBytes, err := ioutil.ReadFile(cfgPath)
79+
if err != nil {
80+
return nil, false, errors.Wrapf(err, "unable to load config from file %q", cfgPath)
81+
}
82+
83+
// Split the YAML documents in the file into a DocumentMap
84+
docmap, err := kubeadmutil.SplitYAMLDocuments(configBytes)
85+
if err != nil {
86+
return nil, false, err
87+
}
88+
89+
// If there are kubeadm types (API group kubeadm.kubernetes.io) present, we need to keep the existing behavior
90+
// here. Basically, we have to load all of the configs from the file and none from the cluster. Configs that are
91+
// missing from the file will be automatically regenerated by kubeadm even if they are present in the cluster.
92+
// The resulting configs overwrite the existing cluster ones at the end of a successful upgrade apply operation.
93+
if isKubeadmConfigPresent(docmap) {
94+
klog.Warning("WARNING: Usage of the --config flag with kubeadm config types for reconfiguring the cluster during upgrade is not recommended!")
95+
cfg, err := configutil.BytesToInitConfiguration(configBytes)
96+
return cfg, true, err
97+
}
98+
99+
// If no kubeadm config types are present, we assume that there are manually upgraded component configs in the file.
100+
// Hence, we load the kubeadm types from the cluster.
101+
initCfg, err := configutil.FetchInitConfigurationFromCluster(client, os.Stdout, logPrefix, false, true)
102+
if err != nil {
103+
return nil, false, err
104+
}
105+
106+
// Stop here if the caller does not want us to load the component configs
107+
if !skipComponentConfigs {
108+
// Load the component configs with upgrades
109+
if err := componentconfigs.FetchFromClusterWithLocalOverwrites(&initCfg.ClusterConfiguration, client, docmap); err != nil {
110+
return nil, false, err
111+
}
112+
113+
// Now default and validate the configs
114+
componentconfigs.Default(&initCfg.ClusterConfiguration, &initCfg.LocalAPIEndpoint, &initCfg.NodeRegistration)
115+
if errs := componentconfigs.Validate(&initCfg.ClusterConfiguration); len(errs) != 0 {
116+
return nil, false, errs.ToAggregate()
117+
}
118+
}
119+
120+
return initCfg, false, nil
121+
}
122+
49123
// enforceRequirements verifies that it's okay to upgrade and then returns the variables needed for the rest of the procedure
50124
func enforceRequirements(flags *applyPlanFlags, args []string, dryRun bool, upgradeApply bool) (clientset.Interface, upgrade.VersionGetter, *kubeadmapi.InitConfiguration, error) {
51125
client, err := getClient(flags.kubeConfigPath, dryRun)
@@ -62,21 +136,7 @@ func enforceRequirements(flags *applyPlanFlags, args []string, dryRun bool, upgr
62136
fmt.Println("[upgrade/config] Making sure the configuration is correct:")
63137

64138
var newK8sVersion string
65-
var cfg *kubeadmapi.InitConfiguration
66-
67-
if flags.cfgPath != "" {
68-
klog.Warning("WARNING: Usage of the --config flag for reconfiguring the cluster during upgrade is not recommended!")
69-
cfg, err = configutil.LoadInitConfigurationFromFile(flags.cfgPath)
70-
71-
// Initialize newK8sVersion to the value in the ClusterConfiguration. This is done, so that users who use the --config option
72-
// don't have to specify the Kubernetes version twice if they don't want to upgrade, but just change a setting.
73-
if err != nil {
74-
newK8sVersion = cfg.KubernetesVersion
75-
}
76-
} else {
77-
cfg, err = configutil.FetchInitConfigurationFromCluster(client, os.Stdout, "upgrade/config", false, !upgradeApply)
78-
}
79-
139+
cfg, legacyReconfigure, err := loadConfig(flags.cfgPath, client, !upgradeApply)
80140
if err != nil {
81141
if apierrors.IsNotFound(err) {
82142
fmt.Printf("[upgrade/config] In order to upgrade, a ConfigMap called %q in the %s namespace must exist.\n", constants.KubeadmConfigConfigMap, metav1.NamespaceSystem)
@@ -90,6 +150,11 @@ func enforceRequirements(flags *applyPlanFlags, args []string, dryRun bool, upgr
90150
err = errors.Errorf("the ConfigMap %q in the %s namespace used for getting configuration information was not found", constants.KubeadmConfigConfigMap, metav1.NamespaceSystem)
91151
}
92152
return nil, nil, nil, errors.Wrap(err, "[upgrade/config] FATAL")
153+
} else if legacyReconfigure {
154+
// Set the newK8sVersion to the value in the ClusterConfiguration. This is done, so that users who use the --config option
155+
// to supply a new ClusterConfiguration don't have to specify the Kubernetes version twice,
156+
// if they don't want to upgrade but just change a setting.
157+
newK8sVersion = cfg.KubernetesVersion
93158
}
94159

95160
ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(flags.ignorePreflightErrors, cfg.NodeRegistration.IgnorePreflightErrors)

cmd/kubeadm/app/componentconfigs/configset.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,49 @@ func FetchFromDocumentMap(clusterCfg *kubeadmapi.ClusterConfiguration, docmap ku
239239
return nil
240240
}
241241

242+
// FetchFromClusterWithLocalOverwrites fetches component configs from a cluster and overwrites them locally with
243+
// the ones present in the supplied document map. If any UnsupportedConfigVersionError are not handled by the configs
244+
// in the document map, the function returns them all as a single UnsupportedConfigVersionsErrorMap.
245+
// This function is normally called only in some specific cases during upgrade.
246+
func FetchFromClusterWithLocalOverwrites(clusterCfg *kubeadmapi.ClusterConfiguration, client clientset.Interface, docmap kubeadmapi.DocumentMap) error {
247+
ensureInitializedComponentConfigs(clusterCfg)
248+
249+
oldVersionErrs := UnsupportedConfigVersionsErrorMap{}
250+
251+
for _, handler := range known {
252+
componentCfg, err := handler.FromCluster(client, clusterCfg)
253+
if err != nil {
254+
if vererr, ok := err.(*UnsupportedConfigVersionError); ok {
255+
oldVersionErrs[handler.GroupVersion.Group] = vererr
256+
} else {
257+
return err
258+
}
259+
} else if componentCfg != nil {
260+
clusterCfg.ComponentConfigs[handler.GroupVersion.Group] = componentCfg
261+
}
262+
}
263+
264+
for _, handler := range known {
265+
componentCfg, err := handler.FromDocumentMap(docmap)
266+
if err != nil {
267+
if vererr, ok := err.(*UnsupportedConfigVersionError); ok {
268+
oldVersionErrs[handler.GroupVersion.Group] = vererr
269+
} else {
270+
return err
271+
}
272+
} else if componentCfg != nil {
273+
clusterCfg.ComponentConfigs[handler.GroupVersion.Group] = componentCfg
274+
delete(oldVersionErrs, handler.GroupVersion.Group)
275+
}
276+
}
277+
278+
if len(oldVersionErrs) != 0 {
279+
return oldVersionErrs
280+
}
281+
282+
return nil
283+
}
284+
242285
// Validate is a placeholder for performing a validation on an already loaded component configs in a ClusterConfiguration
243286
// Currently it prints a warning that no validation was performed
244287
func Validate(clusterCfg *kubeadmapi.ClusterConfiguration) field.ErrorList {

cmd/kubeadm/app/componentconfigs/configset_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,139 @@ func TestFetchFromDocumentMap(t *testing.T) {
110110
t.Fatalf("missmatch between supplied and loaded type numbers:\n\tgot: %d\n\texpected: %d", len(clusterCfg.ComponentConfigs), len(gvkmap))
111111
}
112112
}
113+
114+
func kubeproxyConfigMap(contents string) *v1.ConfigMap {
115+
return &v1.ConfigMap{
116+
ObjectMeta: metav1.ObjectMeta{
117+
Name: constants.KubeProxyConfigMap,
118+
Namespace: metav1.NamespaceSystem,
119+
},
120+
Data: map[string]string{
121+
constants.KubeProxyConfigMapKey: dedent.Dedent(contents),
122+
},
123+
}
124+
}
125+
126+
func TestFetchFromClusterWithLocalUpgrades(t *testing.T) {
127+
cases := []struct {
128+
desc string
129+
obj runtime.Object
130+
config string
131+
expectedValue string
132+
expectedErr bool
133+
}{
134+
{
135+
desc: "reconginzed cluster object without overwrite is used",
136+
obj: kubeproxyConfigMap(`
137+
apiVersion: kubeproxy.config.k8s.io/v1alpha1
138+
kind: KubeProxyConfiguration
139+
hostnameOverride: foo
140+
`),
141+
expectedValue: "foo",
142+
},
143+
{
144+
desc: "reconginzed cluster object with overwrite is not used",
145+
obj: kubeproxyConfigMap(`
146+
apiVersion: kubeproxy.config.k8s.io/v1alpha1
147+
kind: KubeProxyConfiguration
148+
hostnameOverride: foo
149+
`),
150+
config: dedent.Dedent(`
151+
apiVersion: kubeproxy.config.k8s.io/v1alpha1
152+
kind: KubeProxyConfiguration
153+
hostnameOverride: bar
154+
`),
155+
expectedValue: "bar",
156+
},
157+
{
158+
desc: "old config without overwrite returns an error",
159+
obj: kubeproxyConfigMap(`
160+
apiVersion: kubeproxy.config.k8s.io/v1alpha0
161+
kind: KubeProxyConfiguration
162+
hostnameOverride: foo
163+
`),
164+
expectedErr: true,
165+
},
166+
{
167+
desc: "old config with recognized overwrite returns success",
168+
obj: kubeproxyConfigMap(`
169+
apiVersion: kubeproxy.config.k8s.io/v1alpha0
170+
kind: KubeProxyConfiguration
171+
hostnameOverride: foo
172+
`),
173+
config: dedent.Dedent(`
174+
apiVersion: kubeproxy.config.k8s.io/v1alpha1
175+
kind: KubeProxyConfiguration
176+
hostnameOverride: bar
177+
`),
178+
expectedValue: "bar",
179+
},
180+
{
181+
desc: "old config with old overwrite returns an error",
182+
obj: kubeproxyConfigMap(`
183+
apiVersion: kubeproxy.config.k8s.io/v1alpha0
184+
kind: KubeProxyConfiguration
185+
hostnameOverride: foo
186+
`),
187+
config: dedent.Dedent(`
188+
apiVersion: kubeproxy.config.k8s.io/v1alpha0
189+
kind: KubeProxyConfiguration
190+
hostnameOverride: bar
191+
`),
192+
expectedErr: true,
193+
},
194+
}
195+
for _, test := range cases {
196+
t.Run(test.desc, func(t *testing.T) {
197+
clusterCfg := &kubeadmapi.ClusterConfiguration{
198+
KubernetesVersion: constants.CurrentKubernetesVersion.String(),
199+
}
200+
201+
k8sVersion := version.MustParseGeneric(clusterCfg.KubernetesVersion)
202+
203+
client := clientsetfake.NewSimpleClientset(
204+
test.obj,
205+
&v1.ConfigMap{
206+
ObjectMeta: metav1.ObjectMeta{
207+
Name: constants.GetKubeletConfigMapName(k8sVersion),
208+
Namespace: metav1.NamespaceSystem,
209+
},
210+
Data: map[string]string{
211+
constants.KubeletBaseConfigurationConfigMapKey: dedent.Dedent(`
212+
apiVersion: kubelet.config.k8s.io/v1beta1
213+
kind: KubeletConfiguration
214+
`),
215+
},
216+
},
217+
)
218+
219+
docmap, err := kubeadmutil.SplitYAMLDocuments([]byte(test.config))
220+
if err != nil {
221+
t.Fatalf("unexpected failure of SplitYAMLDocuments: %v", err)
222+
}
223+
224+
err = FetchFromClusterWithLocalOverwrites(clusterCfg, client, docmap)
225+
if err != nil {
226+
if !test.expectedErr {
227+
t.Errorf("unexpected failure: %v", err)
228+
}
229+
} else {
230+
if test.expectedErr {
231+
t.Error("unexpected success")
232+
} else {
233+
kubeproxyCfg, ok := clusterCfg.ComponentConfigs[KubeProxyGroup]
234+
if !ok {
235+
t.Error("the config was reported as loaded, but was not in reality")
236+
} else {
237+
actualConfig, ok := kubeproxyCfg.(*kubeProxyConfig)
238+
if !ok {
239+
t.Error("the config is not of the expected type")
240+
} else if actualConfig.config.HostnameOverride != test.expectedValue {
241+
t.Errorf("unexpected value:\n\tgot: %q\n\texpected: %q", actualConfig.config.HostnameOverride, test.expectedValue)
242+
}
243+
}
244+
}
245+
}
246+
})
247+
}
248+
}

cmd/kubeadm/app/componentconfigs/utils.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package componentconfigs
1818

1919
import (
2020
"fmt"
21+
"sort"
22+
"strings"
2123

2224
"k8s.io/apimachinery/pkg/runtime/schema"
2325
"k8s.io/klog/v2"
@@ -40,6 +42,27 @@ func (err *UnsupportedConfigVersionError) Error() string {
4042
return fmt.Sprintf("unsupported apiVersion %q, you may have to do manual conversion to %q and run kubeadm again", err.OldVersion, err.CurrentVersion)
4143
}
4244

45+
// UnsupportedConfigVersionsErrorMap is a cumulative version of the UnsupportedConfigVersionError type
46+
type UnsupportedConfigVersionsErrorMap map[string]*UnsupportedConfigVersionError
47+
48+
// Error implements the standard Golang error interface for UnsupportedConfigVersionsErrorMap
49+
func (errs UnsupportedConfigVersionsErrorMap) Error() string {
50+
// Make sure the error messages we print are predictable by sorting them by the group names involved
51+
groups := make([]string, 0, len(errs))
52+
for group := range errs {
53+
groups = append(groups, group)
54+
}
55+
sort.Strings(groups)
56+
57+
msgs := make([]string, 1, 1+len(errs))
58+
msgs[0] = "multiple unsupported config version errors encountered:"
59+
for _, group := range groups {
60+
msgs = append(msgs, errs[group].Error())
61+
}
62+
63+
return strings.Join(msgs, "\n\t- ")
64+
}
65+
4366
// warnDefaultComponentConfigValue prints a warning if the user modified a field in a certain
4467
// CompomentConfig from the default recommended value in kubeadm.
4568
func warnDefaultComponentConfigValue(componentConfigKind, paramName string, defaultValue, userValue interface{}) {

0 commit comments

Comments
 (0)