Skip to content

Commit d944190

Browse files
committed
kubeadm: make the scheduler and KCM connect to local endpoint
Pinning the kube-controller-manager and kube-scheduler kubeconfig files to point to the control-plane-endpoint can be problematic during immutable upgrades if one of these components ends up contacting an N-1 kube-apiserver: https://kubernetes.io/docs/setup/release/version-skew-policy/#kube-controller-manager-kube-scheduler-and-cloud-controller-manager For example, the components can send a request for a non-existing API version. Instead of using the CPE for these components, use the LocalAPIEndpoint. This guarantees that the components would talk to the local kube-apiserver, which should be the same version, unless the user explicitly patched manifests.
1 parent d159ae3 commit d944190

File tree

3 files changed

+73
-30
lines changed

3 files changed

+73
-30
lines changed

cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -362,32 +362,37 @@ func ValidateKubeconfigsForExternalCA(outDir string, cfg *kubeadmapi.InitConfigu
362362
}
363363

364364
func getKubeConfigSpecsBase(cfg *kubeadmapi.InitConfiguration) (map[string]*kubeConfigSpec, error) {
365-
apiServer, err := kubeadmutil.GetControlPlaneEndpoint(cfg.ControlPlaneEndpoint, &cfg.LocalAPIEndpoint)
365+
controlPlaneEndpoint, err := kubeadmutil.GetControlPlaneEndpoint(cfg.ControlPlaneEndpoint, &cfg.LocalAPIEndpoint)
366366
if err != nil {
367367
return nil, err
368368
}
369+
localAPIEndpoint, err := kubeadmutil.GetLocalAPIEndpoint(&cfg.LocalAPIEndpoint)
370+
if err != nil {
371+
return nil, err
372+
}
373+
369374
return map[string]*kubeConfigSpec{
370375
kubeadmconstants.AdminKubeConfigFileName: {
371-
APIServer: apiServer,
376+
APIServer: controlPlaneEndpoint,
372377
ClientName: "kubernetes-admin",
373378
ClientCertAuth: &clientCertAuth{
374379
Organizations: []string{kubeadmconstants.SystemPrivilegedGroup},
375380
},
376381
},
377382
kubeadmconstants.KubeletKubeConfigFileName: {
378-
APIServer: apiServer,
383+
APIServer: controlPlaneEndpoint,
379384
ClientName: fmt.Sprintf("%s%s", kubeadmconstants.NodesUserPrefix, cfg.NodeRegistration.Name),
380385
ClientCertAuth: &clientCertAuth{
381386
Organizations: []string{kubeadmconstants.NodesGroup},
382387
},
383388
},
384389
kubeadmconstants.ControllerManagerKubeConfigFileName: {
385-
APIServer: apiServer,
390+
APIServer: localAPIEndpoint,
386391
ClientName: kubeadmconstants.ControllerManagerUser,
387392
ClientCertAuth: &clientCertAuth{},
388393
},
389394
kubeadmconstants.SchedulerKubeConfigFileName: {
390-
APIServer: apiServer,
395+
APIServer: localAPIEndpoint,
391396
ClientName: kubeadmconstants.SchedulerUser,
392397
ClientCertAuth: &clientCertAuth{},
393398
},

cmd/kubeadm/app/phases/kubeconfig/kubeconfig_test.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,22 @@ func TestGetKubeConfigSpecs(t *testing.T) {
167167
if err != nil {
168168
t.Error(err)
169169
}
170-
if spec.APIServer != controlPlaneEndpoint {
171-
t.Errorf("getKubeConfigSpecs didn't injected cfg.APIServer endpoint into spec for %s", assertion.kubeConfigFile)
170+
localAPIEndpoint, err := kubeadmutil.GetLocalAPIEndpoint(&cfg.LocalAPIEndpoint)
171+
if err != nil {
172+
t.Error(err)
173+
}
174+
175+
switch assertion.kubeConfigFile {
176+
case kubeadmconstants.AdminKubeConfigFileName, kubeadmconstants.KubeletKubeConfigFileName:
177+
if spec.APIServer != controlPlaneEndpoint {
178+
t.Errorf("expected getKubeConfigSpecs for %s to set cfg.APIServer to %s, got %s",
179+
assertion.kubeConfigFile, controlPlaneEndpoint, spec.APIServer)
180+
}
181+
case kubeadmconstants.ControllerManagerKubeConfigFileName, kubeadmconstants.SchedulerKubeConfigFileName:
182+
if spec.APIServer != localAPIEndpoint {
183+
t.Errorf("expected getKubeConfigSpecs for %s to set cfg.APIServer to %s, got %s",
184+
assertion.kubeConfigFile, localAPIEndpoint, spec.APIServer)
185+
}
172186
}
173187

174188
// Asserts CA certs and CA keys loaded into specs

cmd/kubeadm/app/util/endpoint.go

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,10 @@ import (
3434
// - if the controlPlaneEndpoint is defined but without a port number, use the controlPlaneEndpoint + localEndpoint.BindPort is used.
3535
// - Otherwise, in case the controlPlaneEndpoint is not defined, use the localEndpoint.AdvertiseAddress + the localEndpoint.BindPort.
3636
func GetControlPlaneEndpoint(controlPlaneEndpoint string, localEndpoint *kubeadmapi.APIEndpoint) (string, error) {
37-
// parse the bind port
38-
bindPortString := strconv.Itoa(int(localEndpoint.BindPort))
39-
if _, err := ParsePort(bindPortString); err != nil {
40-
return "", errors.Wrapf(err, "invalid value %q given for api.bindPort", localEndpoint.BindPort)
41-
}
42-
43-
// parse the AdvertiseAddress
44-
var ip = net.ParseIP(localEndpoint.AdvertiseAddress)
45-
if ip == nil {
46-
return "", errors.Errorf("invalid value `%s` given for api.advertiseAddress", localEndpoint.AdvertiseAddress)
47-
}
48-
49-
// set the control-plane url using localEndpoint.AdvertiseAddress + the localEndpoint.BindPort
50-
controlPlaneURL := &url.URL{
51-
Scheme: "https",
52-
Host: net.JoinHostPort(ip.String(), bindPortString),
37+
// get the URL of the local endpoint
38+
localAPIEndpoint, err := GetLocalAPIEndpoint(localEndpoint)
39+
if err != nil {
40+
return "", err
5341
}
5442

5543
// if the controlplane endpoint is defined
@@ -62,22 +50,32 @@ func GetControlPlaneEndpoint(controlPlaneEndpoint string, localEndpoint *kubeadm
6250
}
6351

6452
// if a port is provided within the controlPlaneAddress warn the users we are using it, else use the bindport
53+
localEndpointPort := strconv.Itoa(int(localEndpoint.BindPort))
6554
if port != "" {
66-
if port != bindPortString {
55+
if port != localEndpointPort {
6756
fmt.Println("[endpoint] WARNING: port specified in controlPlaneEndpoint overrides bindPort in the controlplane address")
6857
}
6958
} else {
70-
port = bindPortString
59+
port = localEndpointPort
7160
}
7261

7362
// overrides the control-plane url using the controlPlaneAddress (and eventually the bindport)
74-
controlPlaneURL = &url.URL{
75-
Scheme: "https",
76-
Host: net.JoinHostPort(host, port),
77-
}
63+
return formatURL(host, port).String(), nil
7864
}
7965

80-
return controlPlaneURL.String(), nil
66+
return localAPIEndpoint, nil
67+
}
68+
69+
// GetLocalAPIEndpoint parses an APIEndpoint and returns it as a string,
70+
// or returns and error in case it cannot be parsed.
71+
func GetLocalAPIEndpoint(localEndpoint *kubeadmapi.APIEndpoint) (string, error) {
72+
// get the URL of the local endpoint
73+
localEndpointIP, localEndpointPort, err := parseAPIEndpoint(localEndpoint)
74+
if err != nil {
75+
return "", err
76+
}
77+
url := formatURL(localEndpointIP.String(), localEndpointPort)
78+
return url.String(), nil
8179
}
8280

8381
// ParseHostPort parses a network address of the form "host:port", "ipv4:port", "[ipv6]:port" into host and port;
@@ -123,3 +121,29 @@ func ParsePort(port string) (int, error) {
123121

124122
return 0, errors.New("port must be a valid number between 1 and 65535, inclusive")
125123
}
124+
125+
// parseAPIEndpoint parses an APIEndpoint and returns the AdvertiseAddress as net.IP and the BindPort as string.
126+
// If the BindPort or AdvertiseAddress are invalid it returns an error.
127+
func parseAPIEndpoint(localEndpoint *kubeadmapi.APIEndpoint) (net.IP, string, error) {
128+
// parse the bind port
129+
bindPortString := strconv.Itoa(int(localEndpoint.BindPort))
130+
if _, err := ParsePort(bindPortString); err != nil {
131+
return nil, "", errors.Wrapf(err, "invalid value %q given for api.bindPort", localEndpoint.BindPort)
132+
}
133+
134+
// parse the AdvertiseAddress
135+
var ip = net.ParseIP(localEndpoint.AdvertiseAddress)
136+
if ip == nil {
137+
return nil, "", errors.Errorf("invalid value `%s` given for api.advertiseAddress", localEndpoint.AdvertiseAddress)
138+
}
139+
140+
return ip, bindPortString, nil
141+
}
142+
143+
// formatURL takes a host and a port string and creates a net.URL using https scheme
144+
func formatURL(host, port string) *url.URL {
145+
return &url.URL{
146+
Scheme: "https",
147+
Host: net.JoinHostPort(host, port),
148+
}
149+
}

0 commit comments

Comments
 (0)