Commit 8abc54d2 authored by fabriziopandini's avatar fabriziopandini

make API.ControlPlaneEndpoint accept IP

parent afa68cc2
...@@ -133,7 +133,17 @@ type MasterConfiguration struct { ...@@ -133,7 +133,17 @@ type MasterConfiguration struct {
type API struct { type API struct {
// AdvertiseAddress sets the IP address for the API server to advertise. // AdvertiseAddress sets the IP address for the API server to advertise.
AdvertiseAddress string AdvertiseAddress string
// ControlPlaneEndpoint sets the DNS address with optional port for the API server // ControlPlaneEndpoint sets a stable IP address or DNS name for the control plane; it
// can be a valid IP address or a RFC-1123 DNS subdomain, both with optional TCP port.
// In case the ControlPlaneEndpoint is not specified, the AdvertiseAddress + BindPort
// are used; in case the ControlPlaneEndpoint is specified but without a TCP port,
// the BindPort is used.
// Possible usages are:
// e.g. In an cluster with more than one control plane instances, this field should be
// assigned the address of the external load balancer in front of the
// control plane instances.
// e.g. in environments with enforced node recycling, the ControlPlaneEndpoint
// could be used for assigning a stable DNS to the control plane.
ControlPlaneEndpoint string ControlPlaneEndpoint string
// BindPort sets the secure port for the API Server to bind to. // BindPort sets the secure port for the API Server to bind to.
// Defaults to 6443. // Defaults to 6443.
......
...@@ -125,7 +125,17 @@ type MasterConfiguration struct { ...@@ -125,7 +125,17 @@ type MasterConfiguration struct {
type API struct { type API struct {
// AdvertiseAddress sets the IP address for the API server to advertise. // AdvertiseAddress sets the IP address for the API server to advertise.
AdvertiseAddress string `json:"advertiseAddress"` AdvertiseAddress string `json:"advertiseAddress"`
// ControlPlaneEndpoint sets the DNS address for the API server // ControlPlaneEndpoint sets a stable IP address or DNS name for the control plane; it
// can be a valid IP address or a RFC-1123 DNS subdomain, both with optional TCP port.
// In case the ControlPlaneEndpoint is not specified, the AdvertiseAddress + BindPort
// are used; in case the ControlPlaneEndpoint is specified but without a TCP port,
// the BindPort is used.
// Possible usages are:
// e.g. In an cluster with more than one control plane instances, this field should be
// assigned the address of the external load balancer in front of the
// control plane instances.
// e.g. in environments with enforced node recycling, the ControlPlaneEndpoint
// could be used for assigning a stable DNS to the control plane.
ControlPlaneEndpoint string `json:"controlPlaneEndpoint"` ControlPlaneEndpoint string `json:"controlPlaneEndpoint"`
// BindPort sets the secure port for the API Server to bind to. // BindPort sets the secure port for the API Server to bind to.
// Defaults to 6443. // Defaults to 6443.
......
...@@ -275,7 +275,73 @@ func TestValidateAPIEndpoint(t *testing.T) { ...@@ -275,7 +275,73 @@ func TestValidateAPIEndpoint(t *testing.T) {
expected: false, expected: false,
}, },
{ {
name: "Valid IPv4 address and default port", name: "Valid DNS ControlPlaneEndpoint (with port), AdvertiseAddress and default port",
s: &kubeadm.MasterConfiguration{
API: kubeadm.API{
ControlPlaneEndpoint: "cp.k8s.io:8081",
AdvertiseAddress: "4.5.6.7",
BindPort: 6443,
},
},
expected: true,
},
{
name: "Valid IPv4 ControlPlaneEndpoint (with port), AdvertiseAddress and default port",
s: &kubeadm.MasterConfiguration{
API: kubeadm.API{
ControlPlaneEndpoint: "1.2.3.4:8081",
AdvertiseAddress: "4.5.6.7",
BindPort: 6443,
},
},
expected: true,
},
{
name: "Valid IPv6 ControlPlaneEndpoint (with port), ControlPlaneEndpoint and port",
s: &kubeadm.MasterConfiguration{
API: kubeadm.API{
ControlPlaneEndpoint: "[2001:db7::1]:8081",
AdvertiseAddress: "2001:db7::2",
BindPort: 6443,
},
},
expected: true,
},
{
name: "Valid DNS ControlPlaneEndpoint (without port), AdvertiseAddress and default port",
s: &kubeadm.MasterConfiguration{
API: kubeadm.API{
ControlPlaneEndpoint: "cp.k8s.io",
AdvertiseAddress: "4.5.6.7",
BindPort: 6443,
},
},
expected: true,
},
{
name: "Valid IPv4 ControlPlaneEndpoint (without port), AdvertiseAddress and default port",
s: &kubeadm.MasterConfiguration{
API: kubeadm.API{
ControlPlaneEndpoint: "1.2.3.4",
AdvertiseAddress: "4.5.6.7",
BindPort: 6443,
},
},
expected: true,
},
{
name: "Valid IPv6 ControlPlaneEndpoint (without port), ControlPlaneEndpoint and port",
s: &kubeadm.MasterConfiguration{
API: kubeadm.API{
ControlPlaneEndpoint: "2001:db7::1",
AdvertiseAddress: "2001:db7::2",
BindPort: 6443,
},
},
expected: true,
},
{
name: "Valid IPv4 AdvertiseAddress and default port",
s: &kubeadm.MasterConfiguration{ s: &kubeadm.MasterConfiguration{
API: kubeadm.API{ API: kubeadm.API{
AdvertiseAddress: "1.2.3.4", AdvertiseAddress: "1.2.3.4",
...@@ -285,7 +351,7 @@ func TestValidateAPIEndpoint(t *testing.T) { ...@@ -285,7 +351,7 @@ func TestValidateAPIEndpoint(t *testing.T) {
expected: true, expected: true,
}, },
{ {
name: "Valid IPv6 address and port", name: "Valid IPv6 AdvertiseAddress and port",
s: &kubeadm.MasterConfiguration{ s: &kubeadm.MasterConfiguration{
API: kubeadm.API{ API: kubeadm.API{
AdvertiseAddress: "2001:db7::1", AdvertiseAddress: "2001:db7::1",
...@@ -295,7 +361,7 @@ func TestValidateAPIEndpoint(t *testing.T) { ...@@ -295,7 +361,7 @@ func TestValidateAPIEndpoint(t *testing.T) {
expected: true, expected: true,
}, },
{ {
name: "Invalid IPv4 address", name: "Invalid IPv4 AdvertiseAddress",
s: &kubeadm.MasterConfiguration{ s: &kubeadm.MasterConfiguration{
API: kubeadm.API{ API: kubeadm.API{
AdvertiseAddress: "1.2.34", AdvertiseAddress: "1.2.34",
...@@ -305,7 +371,7 @@ func TestValidateAPIEndpoint(t *testing.T) { ...@@ -305,7 +371,7 @@ func TestValidateAPIEndpoint(t *testing.T) {
expected: false, expected: false,
}, },
{ {
name: "Invalid IPv6 address", name: "Invalid IPv6 AdvertiseAddress",
s: &kubeadm.MasterConfiguration{ s: &kubeadm.MasterConfiguration{
API: kubeadm.API{ API: kubeadm.API{
AdvertiseAddress: "2001:db7:1", AdvertiseAddress: "2001:db7:1",
...@@ -314,6 +380,52 @@ func TestValidateAPIEndpoint(t *testing.T) { ...@@ -314,6 +380,52 @@ func TestValidateAPIEndpoint(t *testing.T) {
}, },
expected: false, expected: false,
}, },
{
name: "Invalid BindPort",
s: &kubeadm.MasterConfiguration{
API: kubeadm.API{
AdvertiseAddress: "1.2.3.4",
BindPort: 0,
},
},
expected: false,
},
{
name: "Invalid DNS ControlPlaneEndpoint",
s: &kubeadm.MasterConfiguration{
API: kubeadm.API{
ControlPlaneEndpoint: "bad!!.k8s.io",
},
},
expected: false,
},
{
name: "Invalid ipv4 ControlPlaneEndpoint",
s: &kubeadm.MasterConfiguration{
API: kubeadm.API{
ControlPlaneEndpoint: "1..3.4",
},
},
expected: false,
},
{
name: "Invalid ipv6 ControlPlaneEndpoint",
s: &kubeadm.MasterConfiguration{
API: kubeadm.API{
ControlPlaneEndpoint: "1200::AB00:1234::2552:7777:1313",
},
},
expected: false,
},
{
name: "Invalid ControlPlaneEndpoint port",
s: &kubeadm.MasterConfiguration{
API: kubeadm.API{
ControlPlaneEndpoint: "1.2.3.4:0",
},
},
expected: false,
},
} }
for _, rt := range tests { for _, rt := range tests {
actual := ValidateAPIEndpoint(&rt.s.API, nil) actual := ValidateAPIEndpoint(&rt.s.API, nil)
......
...@@ -29,6 +29,7 @@ import ( ...@@ -29,6 +29,7 @@ import (
certutil "k8s.io/client-go/util/cert" certutil "k8s.io/client-go/util/cert"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
"k8s.io/kubernetes/pkg/registry/core/service/ipallocator" "k8s.io/kubernetes/pkg/registry/core/service/ipallocator"
) )
...@@ -286,9 +287,17 @@ func GetAPIServerAltNames(cfg *kubeadmapi.MasterConfiguration) (*certutil.AltNam ...@@ -286,9 +287,17 @@ func GetAPIServerAltNames(cfg *kubeadmapi.MasterConfiguration) (*certutil.AltNam
}, },
} }
// add api server dns advertise address // add api server controlPlaneEndpoint if present (dns or ip)
if len(cfg.API.ControlPlaneEndpoint) > 0 { if len(cfg.API.ControlPlaneEndpoint) > 0 {
altNames.DNSNames = append(altNames.DNSNames, cfg.API.ControlPlaneEndpoint) if host, _, err := kubeadmutil.ParseHostPort(cfg.API.ControlPlaneEndpoint); err == nil {
if ip := net.ParseIP(host); ip != nil {
altNames.IPs = append(altNames.IPs, ip)
} else {
altNames.DNSNames = append(altNames.DNSNames, host)
}
} else {
return nil, fmt.Errorf("error parsing API api.controlPlaneEndpoint %q: %s", cfg.API.ControlPlaneEndpoint, err)
}
} }
appendSANsToAltNames(altNames, cfg.APIServerCertSANs, kubeadmconstants.APIServerCertName) appendSANsToAltNames(altNames, cfg.APIServerCertSANs, kubeadmconstants.APIServerCertName)
......
...@@ -436,48 +436,69 @@ func TestPathForPublicKey(t *testing.T) { ...@@ -436,48 +436,69 @@ func TestPathForPublicKey(t *testing.T) {
} }
func TestGetAPIServerAltNames(t *testing.T) { func TestGetAPIServerAltNames(t *testing.T) {
hostname := "valid-hostname"
advertiseIP := "1.2.3.4"
controlPlaneEndpoint := "api.k8s.io"
cfg := &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{AdvertiseAddress: advertiseIP, ControlPlaneEndpoint: controlPlaneEndpoint},
Networking: kubeadmapi.Networking{ServiceSubnet: "10.96.0.0/12", DNSDomain: "cluster.local"},
NodeName: hostname,
APIServerCertSANs: []string{"10.1.245.94", "10.1.245.95", "1.2.3.L", "invalid,commas,in,DNS"},
}
altNames, err := GetAPIServerAltNames(cfg) var tests = []struct {
if err != nil { name string
t.Fatalf("failed calling GetAPIServerAltNames: %v", err) cfg *kubeadmapi.MasterConfiguration
expectedDNSNames []string
expectedIPAddresses []string
}{
{
name: "ControlPlaneEndpoint DNS",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{AdvertiseAddress: "1.2.3.4", ControlPlaneEndpoint: "api.k8s.io:6443"},
Networking: kubeadmapi.Networking{ServiceSubnet: "10.96.0.0/12", DNSDomain: "cluster.local"},
NodeName: "valid-hostname",
APIServerCertSANs: []string{"10.1.245.94", "10.1.245.95", "1.2.3.L", "invalid,commas,in,DNS"},
},
expectedDNSNames: []string{"valid-hostname", "kubernetes", "kubernetes.default", "kubernetes.default.svc", "kubernetes.default.svc.cluster.local", "api.k8s.io"},
expectedIPAddresses: []string{"10.96.0.1", "1.2.3.4", "10.1.245.94", "10.1.245.95"},
},
{
name: "ControlPlaneEndpoint IP",
cfg: &kubeadmapi.MasterConfiguration{
API: kubeadmapi.API{AdvertiseAddress: "1.2.3.4", ControlPlaneEndpoint: "4.5.6.7:6443"},
Networking: kubeadmapi.Networking{ServiceSubnet: "10.96.0.0/12", DNSDomain: "cluster.local"},
NodeName: "valid-hostname",
APIServerCertSANs: []string{"10.1.245.94", "10.1.245.95", "1.2.3.L", "invalid,commas,in,DNS"},
},
expectedDNSNames: []string{"valid-hostname", "kubernetes", "kubernetes.default", "kubernetes.default.svc", "kubernetes.default.svc.cluster.local"},
expectedIPAddresses: []string{"10.96.0.1", "1.2.3.4", "10.1.245.94", "10.1.245.95", "4.5.6.7"},
},
} }
expectedDNSNames := []string{hostname, "kubernetes", "kubernetes.default", "kubernetes.default.svc", "kubernetes.default.svc.cluster.local", controlPlaneEndpoint} for _, rt := range tests {
for _, DNSName := range expectedDNSNames { altNames, err := GetAPIServerAltNames(rt.cfg)
found := false if err != nil {
for _, val := range altNames.DNSNames { t.Fatalf("failed calling GetAPIServerAltNames: %s: %v", rt.name, err)
if val == DNSName {
found = true
break
}
} }
if !found { for _, DNSName := range rt.expectedDNSNames {
t.Errorf("altNames does not contain DNSName %s", DNSName) found := false
} for _, val := range altNames.DNSNames {
} if val == DNSName {
found = true
break
}
}
expectedIPAddresses := []string{"10.96.0.1", advertiseIP, "10.1.245.94", "10.1.245.95"} if !found {
for _, IPAddress := range expectedIPAddresses { t.Errorf("%s: altNames does not contain DNSName %s but %v", rt.name, DNSName, altNames.DNSNames)
found := false
for _, val := range altNames.IPs {
if val.Equal(net.ParseIP(IPAddress)) {
found = true
break
} }
} }
if !found { for _, IPAddress := range rt.expectedIPAddresses {
t.Errorf("altNames does not contain IPAddress %s", IPAddress) found := false
for _, val := range altNames.IPs {
if val.Equal(net.ParseIP(IPAddress)) {
found = true
break
}
}
if !found {
t.Errorf("%s: altNames does not contain IPAddress %s but %v", rt.name, IPAddress, altNames.IPs)
}
} }
} }
} }
......
...@@ -72,12 +72,12 @@ func TestGetKubeConfigSpecs(t *testing.T) { ...@@ -72,12 +72,12 @@ func TestGetKubeConfigSpecs(t *testing.T) {
NodeName: "valid-node-name", NodeName: "valid-node-name",
}, },
{ {
API: kubeadmapi.API{ControlPlaneEndpoint: "api.k8s.io", BindPort: 1234}, API: kubeadmapi.API{AdvertiseAddress: "1.2.3.4", ControlPlaneEndpoint: "api.k8s.io", BindPort: 1234},
CertificatesDir: pkidir, CertificatesDir: pkidir,
NodeName: "valid-node-name", NodeName: "valid-node-name",
}, },
{ {
API: kubeadmapi.API{ControlPlaneEndpoint: "api.k8s.io:4321", BindPort: 1234}, API: kubeadmapi.API{AdvertiseAddress: "1.2.3.4", ControlPlaneEndpoint: "api.k8s.io:4321", BindPort: 1234},
CertificatesDir: pkidir, CertificatesDir: pkidir,
NodeName: "valid-node-name", NodeName: "valid-node-name",
}, },
......
...@@ -19,67 +19,101 @@ package util ...@@ -19,67 +19,101 @@ package util
import ( import (
"fmt" "fmt"
"net" "net"
"net/url"
"strconv" "strconv"
"strings"
"k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
) )
// GetMasterEndpoint returns a properly formatted Master Endpoint // GetMasterEndpoint returns a properly formatted endpoint for the control plane built according following rules:
// or passes the error from GetMasterHostPort. // - If the api.ControlPlaneEndpoint is defined, use it.
// - if the api.ControlPlaneEndpoint is defined but without a port number, use the api.ControlPlaneEndpoint + api.BindPort is used.
// - Otherwise, in case the api.ControlPlaneEndpoint is not defined, use the api.AdvertiseAddress + the api.BindPort.
func GetMasterEndpoint(api *kubeadmapi.API) (string, error) { func GetMasterEndpoint(api *kubeadmapi.API) (string, error) {
// parse the bind port
var bindPort = strconv.Itoa(int(api.BindPort))
if _, err := parsePort(bindPort); err != nil {
return "", fmt.Errorf("invalid value %q given for api.bindPort: %s", api.BindPort, err)
}
hostPort, err := GetMasterHostPort(api) // parse the AdvertiseAddress
if err != nil { var ip = net.ParseIP(api.AdvertiseAddress)
return "", err if ip == nil {
return "", fmt.Errorf("invalid value `%s` given for api.advertiseAddress", api.AdvertiseAddress)
} }
return fmt.Sprintf("https://%s", hostPort), nil
}
// GetMasterHostPort returns a properly formatted Master hostname or IP and port pair, or error // set the master url using cfg.API.AdvertiseAddress + the cfg.API.BindPort
// if the hostname or IP address can not be parsed or port is outside the valid TCP range. masterURL := &url.URL{
func GetMasterHostPort(api *kubeadmapi.API) (string, error) { Scheme: "https",
var masterIP string Host: net.JoinHostPort(ip.String(), bindPort),
var portStr string }
// if the controlplane endpoint is defined
if len(api.ControlPlaneEndpoint) > 0 { if len(api.ControlPlaneEndpoint) > 0 {
if strings.Contains(api.ControlPlaneEndpoint, ":") { // parse the controlplane endpoint
var err error var host, port string
masterIP, portStr, err = net.SplitHostPort(api.ControlPlaneEndpoint) var err error
if err != nil { if host, port, err = ParseHostPort(api.ControlPlaneEndpoint); err != nil {
return "", fmt.Errorf("invalid value `%s` given for `ControlPlaneEndpoint`: %s", api.ControlPlaneEndpoint, err) return "", fmt.Errorf("invalid value %q given for api.controlPlaneEndpoint: %s", api.ControlPlaneEndpoint, err)
}
} else {
masterIP = api.ControlPlaneEndpoint
} }
errs := validation.IsDNS1123Subdomain(masterIP)
if len(errs) > 0 { // if a port is provided within the controlPlaneAddress warn the users we are using it, else use the bindport
return "", fmt.Errorf("error parsing `ControlPlaneEndpoint` to valid dns subdomain with errors: %s", errs) if port != "" {
fmt.Println("[endpoint] WARNING: port specified in api.controlPlaneEndpoint overrides api.bindPort in the controlplane address")
} else {
port = bindPort
} }
} else {
ip := net.ParseIP(api.AdvertiseAddress) // overrides the master url using the controlPlaneAddress (and eventually the bindport)
if ip == nil { masterURL = &url.URL{
return "", fmt.Errorf("error parsing address %s", api.AdvertiseAddress) Scheme: "https",
Host: net.JoinHostPort(host, port),
} }
masterIP = ip.String()
} }
var port int32 return masterURL.String(), nil
if len(portStr) > 0 { }
portInt, err := strconv.Atoi(portStr)
if err != nil { // ParseHostPort parses a network address of the form "host:port", "ipv4:port", "[ipv6]:port" into host and port;
return "", fmt.Errorf("error parsing `ControlPlaneEndpoint` port value `%s`: %s", portStr, err.Error()) // ":port" can be eventually omitted.
// If the string is not a valid representation of network address, ParseHostPort returns an error.
func ParseHostPort(hostport string) (string, string, error) {
var host, port string
var err error
// try to split host and port
if host, port, err = net.SplitHostPort(hostport); err != nil {
// if SplitHostPort returns an error, the entire hostport is considered as host
host = hostport
}
// if port is defined, parse and validate it
if port != "" {
if _, err := parsePort(port); err != nil {
return "", "", fmt.Errorf("port must be a valid number between 1 and 65535, inclusive")
} }
port = int32(portInt)
fmt.Println("[endpoint] WARNING: specifying a port for `ControlPlaneEndpoint` overrides `BindPort`")
} else {
port = api.BindPort
} }
if port < 0 || port > 65535 { // if host is a valid IP, returns it
return "", fmt.Errorf("api server port must be between 0 and 65535, %d was given", port) if ip := net.ParseIP(host); ip != nil {
return host, port, nil
}
// if host is a validate RFC-1123 subdomain, returns it
if errs := validation.IsDNS1123Subdomain(host); len(errs) == 0 {
return host, port, nil
}
return "", "", fmt.Errorf("host must be a valid IP address or a valid RFC-1123 DNS subdomain")
}
// ParsePort parses a string representing a TCP port.
// If the string is not a valid representation of a TCP port, ParsePort returns an error.
func parsePort(port string) (int, error) {
if portInt, err := strconv.Atoi(port); err == nil && (1 <= portInt && portInt <= 65535) {
return portInt, nil
} }
hostPort := net.JoinHostPort(masterIP, strconv.Itoa(int(port))) return 0, fmt.Errorf("port must be a valid number between 1 and 65535, inclusive")
return hostPort, nil
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment