Updates Kubeadm Master Endpoint for IPv6

Previously, kubeadm would use <ip>:<port> to construct a master
endpoint. This works fine for IPv4 addresses, but not for IPv6.
IPv6 requires the ip to be encased in brackets when being joined
to a port with a colon.

This patch updates kubeadm to support wrapping a v6 address with
[] to form the master endpoint url. Since this functionality is
needed in multiple areas, a dedicated util function was created.

Fixes: https://github.com/kubernetes/kubernetes/issues/48227
pull/6/head
Daneyon Hansen 2017-06-28 10:20:13 -07:00
parent 858d9d4857
commit 3390bc3cbc
12 changed files with 422 additions and 20 deletions

View File

@ -17,7 +17,6 @@ limitations under the License.
package kubeadm
import (
"fmt"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -116,7 +115,3 @@ type NodeConfiguration struct {
// the security of kubeadm since other nodes can impersonate the master.
DiscoveryTokenUnsafeSkipCAVerification bool
}
func (cfg *MasterConfiguration) GetMasterEndpoint() string {
return fmt.Sprintf("https://%s:%d", cfg.API.AdvertiseAddress, cfg.API.BindPort)
}

View File

@ -24,6 +24,7 @@ go_library(
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/cmd/features:go_default_library",
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/util:go_default_library",
"//cmd/kubeadm/app/util/token:go_default_library",
"//pkg/api/validation:go_default_library",
"//pkg/kubeapiserver/authorizer/modes:go_default_library",

View File

@ -31,6 +31,7 @@ import (
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/features"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
tokenutil "k8s.io/kubernetes/cmd/kubeadm/app/util/token"
apivalidation "k8s.io/kubernetes/pkg/api/validation"
authzmodes "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
@ -68,6 +69,7 @@ func ValidateMasterConfiguration(c *kubeadm.MasterConfiguration) field.ErrorList
allErrs = append(allErrs, ValidateNodeName(c.NodeName, field.NewPath("node-name"))...)
allErrs = append(allErrs, ValidateToken(c.Token, field.NewPath("token"))...)
allErrs = append(allErrs, ValidateFeatureFlags(c.FeatureFlags, field.NewPath("feature-flags"))...)
allErrs = append(allErrs, ValidateAPIEndpoint(c, field.NewPath("api-endpoint"))...)
return allErrs
}
@ -309,3 +311,13 @@ func ValidateFeatureFlags(featureFlags map[string]bool, fldPath *field.Path) fie
return allErrs
}
func ValidateAPIEndpoint(c *kubeadm.MasterConfiguration, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
endpoint, err := kubeadmutil.GetMasterEndpoint(c)
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath, endpoint, "Invalid API Endpoint"))
}
return allErrs
}

View File

@ -209,6 +209,71 @@ func TestValidateIPNetFromString(t *testing.T) {
}
}
func TestValidateAPIEndpoint(t *testing.T) {
var tests = []struct {
name string
s *kubeadm.MasterConfiguration
expected bool
}{
{
name: "Missing configuration",
s: &kubeadm.MasterConfiguration{},
expected: false,
},
{
name: "Valid IPv4 address and default port",
s: &kubeadm.MasterConfiguration{
API: kubeadm.API{
AdvertiseAddress: "1.2.3.4",
BindPort: 6443,
},
},
expected: true,
},
{
name: "Valid IPv6 address and port",
s: &kubeadm.MasterConfiguration{
API: kubeadm.API{
AdvertiseAddress: "2001:db7::1",
BindPort: 3446,
},
},
expected: true,
},
{
name: "Invalid IPv4 address",
s: &kubeadm.MasterConfiguration{
API: kubeadm.API{
AdvertiseAddress: "1.2.34",
BindPort: 6443,
},
},
expected: false,
},
{
name: "Invalid IPv6 address",
s: &kubeadm.MasterConfiguration{
API: kubeadm.API{
AdvertiseAddress: "2001:db7:1",
BindPort: 3446,
},
},
expected: false,
},
}
for _, rt := range tests {
actual := ValidateAPIEndpoint(rt.s, nil)
if (len(actual) == 0) != rt.expected {
t.Errorf(
"%s test case failed:\n\texpected: %t\n\t actual: %t",
rt.name,
rt.expected,
(len(actual) == 0),
)
}
}
}
func TestValidateMasterConfiguration(t *testing.T) {
nodename := "valid-nodename"
var tests = []struct {
@ -220,6 +285,10 @@ func TestValidateMasterConfiguration(t *testing.T) {
&kubeadm.MasterConfiguration{}, false},
{"invalid missing token with IPv4 service subnet",
&kubeadm.MasterConfiguration{
API: kubeadm.API{
AdvertiseAddress: "1.2.3.4",
BindPort: 6443,
},
AuthorizationModes: []string{"Node", "RBAC"},
Networking: kubeadm.Networking{
ServiceSubnet: "10.96.0.1/12",
@ -230,6 +299,10 @@ func TestValidateMasterConfiguration(t *testing.T) {
}, false},
{"invalid missing token with IPv6 service subnet",
&kubeadm.MasterConfiguration{
API: kubeadm.API{
AdvertiseAddress: "1.2.3.4",
BindPort: 6443,
},
AuthorizationModes: []string{"Node", "RBAC"},
Networking: kubeadm.Networking{
ServiceSubnet: "2001:db8::1/98",
@ -240,6 +313,10 @@ func TestValidateMasterConfiguration(t *testing.T) {
}, false},
{"invalid missing node name",
&kubeadm.MasterConfiguration{
API: kubeadm.API{
AdvertiseAddress: "1.2.3.4",
BindPort: 6443,
},
AuthorizationModes: []string{"Node", "RBAC"},
Networking: kubeadm.Networking{
ServiceSubnet: "10.96.0.1/12",
@ -250,6 +327,10 @@ func TestValidateMasterConfiguration(t *testing.T) {
}, false},
{"valid master configuration with IPv4 service subnet",
&kubeadm.MasterConfiguration{
API: kubeadm.API{
AdvertiseAddress: "1.2.3.4",
BindPort: 6443,
},
AuthorizationModes: []string{"Node", "RBAC"},
Networking: kubeadm.Networking{
ServiceSubnet: "10.96.0.1/12",
@ -261,6 +342,10 @@ func TestValidateMasterConfiguration(t *testing.T) {
}, true},
{"valid master configuration using IPv6 service subnet",
&kubeadm.MasterConfiguration{
API: kubeadm.API{
AdvertiseAddress: "1:2:3::4",
BindPort: 3446,
},
AuthorizationModes: []string{"Node", "RBAC"},
Networking: kubeadm.Networking{
ServiceSubnet: "2001:db8::1/98",

View File

@ -20,7 +20,6 @@ import (
"fmt"
"io"
"io/ioutil"
"strconv"
"text/template"
"time"
@ -73,7 +72,7 @@ var (
You can now join any number of machines by running the following on each node
as root:
kubeadm join --token {{.Token}} {{.MasterIP}}:{{.MasterPort}} --discovery-token-ca-cert-hash {{.CAPubKeyPin}}
kubeadm join --token {{.Token}} {{.MasterHostPort}} --discovery-token-ca-cert-hash {{.CAPubKeyPin}}
`)))
)
@ -329,6 +328,9 @@ func (i *Init) Run(out io.Writer) error {
// Load the CA certificate from so we can pin its public key
caCert, err := pkiutil.TryLoadCertFromDisk(i.cfg.CertificatesDir, kubeadmconstants.CACertAndKeyBaseName)
// Generate the Master host/port pair used by initDoneTempl
masterHostPort, err := kubeadmutil.GetMasterHostPort(i.cfg)
if err != nil {
return err
}
@ -338,8 +340,7 @@ func (i *Init) Run(out io.Writer) error {
"KubeConfigName": kubeadmconstants.AdminKubeConfigFileName,
"Token": i.cfg.Token,
"CAPubKeyPin": pubkeypin.Hash(caCert),
"MasterIP": i.cfg.API.AdvertiseAddress,
"MasterPort": strconv.Itoa(int(i.cfg.API.BindPort)),
"MasterHostPort": masterHostPort,
}
if i.skipTokenPrint {
ctx["Token"] = "<value withheld>"

View File

@ -49,10 +49,15 @@ func EnsureProxyAddon(cfg *kubeadmapi.MasterConfiguration, client clientset.Inte
return fmt.Errorf("error when creating kube-proxy service account: %v", err)
}
// Generate Master Enpoint kubeconfig file
masterEndpoint, err := kubeadmutil.GetMasterEndpoint(cfg)
if err != nil {
return err
}
proxyConfigMapBytes, err := kubeadmutil.ParseTemplate(KubeProxyConfigMap, struct{ MasterEndpoint string }{
// Fetch this value from the kubeconfig file
MasterEndpoint: fmt.Sprintf("https://%s:%d", cfg.API.AdvertiseAddress, cfg.API.BindPort),
})
MasterEndpoint: masterEndpoint})
if err != nil {
return fmt.Errorf("error when parsing kube-proxy configmap template: %v", err)
}

View File

@ -16,6 +16,7 @@ go_library(
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/phases/certs/pkiutil:go_default_library",
"//cmd/kubeadm/app/util:go_default_library",
"//cmd/kubeadm/app/util/kubeconfig:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library",
@ -44,6 +45,7 @@ go_test(
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/phases/certs/pkiutil:go_default_library",
"//cmd/kubeadm/app/util:go_default_library",
"//cmd/kubeadm/test:go_default_library",
"//cmd/kubeadm/test/certs:go_default_library",
"//cmd/kubeadm/test/kubeconfig:go_default_library",

View File

@ -32,6 +32,7 @@ import (
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil"
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig"
)
@ -134,10 +135,15 @@ func getKubeConfigSpecs(cfg *kubeadmapi.MasterConfiguration) (map[string]*kubeCo
return nil, fmt.Errorf("couldn't create a kubeconfig; the CA files couldn't be loaded: %v", err)
}
masterEndpoint, err := kubeadmutil.GetMasterEndpoint(cfg)
if err != nil {
return nil, err
}
var kubeConfigSpec = map[string]*kubeConfigSpec{
kubeadmconstants.AdminKubeConfigFileName: {
CACert: caCert,
APIServer: cfg.GetMasterEndpoint(),
APIServer: masterEndpoint,
ClientName: "kubernetes-admin",
ClientCertAuth: &clientCertAuth{
CAKey: caKey,
@ -146,7 +152,7 @@ func getKubeConfigSpecs(cfg *kubeadmapi.MasterConfiguration) (map[string]*kubeCo
},
kubeadmconstants.KubeletKubeConfigFileName: {
CACert: caCert,
APIServer: cfg.GetMasterEndpoint(),
APIServer: masterEndpoint,
ClientName: fmt.Sprintf("system:node:%s", cfg.NodeName),
ClientCertAuth: &clientCertAuth{
CAKey: caKey,
@ -155,7 +161,7 @@ func getKubeConfigSpecs(cfg *kubeadmapi.MasterConfiguration) (map[string]*kubeCo
},
kubeadmconstants.ControllerManagerKubeConfigFileName: {
CACert: caCert,
APIServer: cfg.GetMasterEndpoint(),
APIServer: masterEndpoint,
ClientName: kubeadmconstants.ControllerManagerUser,
ClientCertAuth: &clientCertAuth{
CAKey: caKey,
@ -163,7 +169,7 @@ func getKubeConfigSpecs(cfg *kubeadmapi.MasterConfiguration) (map[string]*kubeCo
},
kubeadmconstants.SchedulerKubeConfigFileName: {
CACert: caCert,
APIServer: cfg.GetMasterEndpoint(),
APIServer: masterEndpoint,
ClientName: kubeadmconstants.SchedulerUser,
ClientCertAuth: &clientCertAuth{
CAKey: caKey,
@ -266,9 +272,14 @@ func WriteKubeConfigWithClientCert(out io.Writer, cfg *kubeadmapi.MasterConfigur
return fmt.Errorf("couldn't create a kubeconfig; the CA files couldn't be loaded: %v", err)
}
masterEndpoint, err := kubeadmutil.GetMasterEndpoint(cfg)
if err != nil {
return err
}
spec := &kubeConfigSpec{
ClientName: clientName,
APIServer: cfg.GetMasterEndpoint(),
APIServer: masterEndpoint,
CACert: caCert,
ClientCertAuth: &clientCertAuth{
CAKey: caKey,
@ -287,9 +298,14 @@ func WriteKubeConfigWithToken(out io.Writer, cfg *kubeadmapi.MasterConfiguration
return fmt.Errorf("couldn't create a kubeconfig; the CA files couldn't be loaded: %v", err)
}
masterEndpoint, err := kubeadmutil.GetMasterEndpoint(cfg)
if err != nil {
return err
}
spec := &kubeConfigSpec{
ClientName: clientName,
APIServer: cfg.GetMasterEndpoint(),
APIServer: masterEndpoint,
CACert: caCert,
TokenAuth: &tokenAuth{
Token: token,

View File

@ -34,6 +34,7 @@ import (
pkiutil "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil"
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
testutil "k8s.io/kubernetes/cmd/kubeadm/test"
certstestutil "k8s.io/kubernetes/cmd/kubeadm/test/certs"
kubeconfigtestutil "k8s.io/kubernetes/cmd/kubeadm/test/kubeconfig"
@ -117,8 +118,12 @@ func TestGetKubeConfigSpecs(t *testing.T) {
}
// Asserts MasterConfiguration values injected into spec
if spec.APIServer != cfg.GetMasterEndpoint() {
t.Errorf("getKubeConfigSpecs didn't injected cfg.APIServer address into spec for %s", assertion.kubeConfigFile)
masterEndpoint, err := kubeadmutil.GetMasterEndpoint(cfg)
if err != nil {
t.Error(err)
}
if spec.APIServer != masterEndpoint {
t.Errorf("getKubeConfigSpecs didn't injected cfg.APIServer endpoint into spec for %s", assertion.kubeConfigFile)
}
// Asserts CA certs and CA keys loaded into specs

View File

@ -9,11 +9,13 @@ load(
go_library(
name = "go_default_library",
srcs = [
"endpoint.go",
"error.go",
"template.go",
"version.go",
],
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/preflight:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",
],
@ -22,12 +24,16 @@ go_library(
go_test(
name = "go_default_test",
srcs = [
"endpoint_test.go",
"error_test.go",
"template_test.go",
"version_test.go",
],
library = ":go_default_library",
deps = ["//cmd/kubeadm/app/preflight:go_default_library"],
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/preflight:go_default_library",
],
)
filegroup(

View File

@ -0,0 +1,51 @@
/*
Copyright 2017 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 util
import (
"fmt"
"net"
"strconv"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
)
// GetMasterEndpoint returns a properly formatted Master Endpoint
// or passes the error from GetMasterHostPort.
func GetMasterEndpoint(cfg *kubeadmapi.MasterConfiguration) (string, error) {
hostPort, err := GetMasterHostPort(cfg)
if err != nil {
return "", err
}
return fmt.Sprintf("https://%s", hostPort), nil
}
// GetMasterHostPort returns a properly formatted Master IP/port pair or error
// if the IP address can not be parsed or port is outside the valid TCP range.
func GetMasterHostPort(cfg *kubeadmapi.MasterConfiguration) (string, error) {
masterIP := net.ParseIP(cfg.API.AdvertiseAddress)
if masterIP == nil {
return "", fmt.Errorf("error parsing address %s", cfg.API.AdvertiseAddress)
}
if cfg.API.BindPort < 0 || cfg.API.BindPort > 65535 {
return "", fmt.Errorf("api server port must be between 0 and 65535")
}
hostPort := net.JoinHostPort(masterIP.String(), strconv.Itoa(int(cfg.API.BindPort)))
return hostPort, nil
}

View File

@ -0,0 +1,223 @@
/*
Copyright 2017 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 util
import (
"testing"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
)
func TestGetMasterEndpoint(t *testing.T) {
var tests = []struct {
name string
cfg *kubeadmapi.MasterConfiguration
endpoint string
expected bool
}{
{
name: "valid IPv4 endpoint",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.3.4",
BindPort: 1234,
},
},
endpoint: "https://1.2.3.4:1234",
expected: true,
},
{
name: "valid IPv6 endpoint",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "2001:db8::1",
BindPort: 4321,
},
},
endpoint: "https://[2001:db8::1]:4321",
expected: true,
},
{
name: "invalid IPv4 endpoint",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.3.4",
BindPort: 1234,
},
},
endpoint: "https://[1.2.3.4]:1234",
expected: false,
},
{
name: "invalid IPv6 endpoint",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "2001:db8::1",
BindPort: 4321,
},
},
endpoint: "https://2001:db8::1:4321",
expected: false,
},
{
name: "invalid IPv4 AdvertiseAddress",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.34",
BindPort: 1234,
},
},
endpoint: "https://1.2.3.4:1234",
expected: false,
},
{
name: "invalid IPv6 AdvertiseAddress",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "2001::db8::1",
BindPort: 4321,
},
},
endpoint: "https://[2001:db8::1]:4321",
expected: false,
},
}
for _, rt := range tests {
actual, err := GetMasterEndpoint(rt.cfg)
if err != nil && rt.expected {
t.Error(err)
}
if actual != rt.endpoint && rt.expected {
t.Errorf(
"%s test case failed:\n\texpected: %s\n\t actual: %s",
rt.name,
rt.endpoint,
(actual),
)
}
}
}
func TestGetMasterHostPort(t *testing.T) {
var tests = []struct {
name string
cfg *kubeadmapi.MasterConfiguration
hostPort string
expected bool
}{
{
name: "valid IPv4 master host and port",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.3.4",
BindPort: 1234,
},
},
hostPort: "1.2.3.4:1234",
expected: true,
},
{
name: "valid IPv6 master host port",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "2001:db8::1",
BindPort: 4321,
},
},
hostPort: "[2001:db8::1]:4321",
expected: true,
},
{
name: "invalid IPv4 address",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.34",
BindPort: 1234,
},
},
hostPort: "1.2.3.4:1234",
expected: false,
},
{
name: "invalid IPv6 address",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "2001::db8::1",
BindPort: 4321,
},
},
hostPort: "[2001:db8::1]:4321",
expected: false,
},
{
name: "invalid TCP port number",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.3.4",
BindPort: 987654321,
},
},
hostPort: "1.2.3.4:987654321",
expected: false,
},
{
name: "invalid negative TCP port number",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.3.4",
BindPort: -987654321,
},
},
hostPort: "1.2.3.4:-987654321",
expected: false,
},
{
name: "unspecified IPv4 TCP port",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1.2.3.4",
},
},
hostPort: "1.2.3.4:0",
expected: true,
},
{
name: "unspecified IPv6 TCP port",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{
AdvertiseAddress: "1:2:3::4",
},
},
hostPort: "[1:2:3::4]:0",
expected: true,
},
}
for _, rt := range tests {
actual, err := GetMasterHostPort(rt.cfg)
if err != nil && rt.expected {
t.Error(err)
}
if actual != rt.hostPort && rt.expected {
t.Errorf(
"%s test case failed:\n\texpected: %s\n\t actual: %s",
rt.name,
rt.hostPort,
(actual),
)
}
}
}