Merge pull request #233 from fabriziopandini/multi-master

support n>1 control plane nodes and the external load balancer
This commit is contained in:
Kubernetes Prow Robot
2019-01-21 12:28:57 -08:00
committed by GitHub
13 changed files with 488 additions and 49 deletions

View File

@@ -2,9 +2,9 @@
kind: Config
apiVersion: kind.sigs.k8s.io/v1alpha2
nodes:
- role: control-plane
replicas: 3
- role: worker
replicas: 2
- role: control-plane
replicas: 3
- role: worker
replicas: 2
- role: external-etcd
- role: external-load-balancer
- role: external-load-balancer

View File

@@ -158,7 +158,7 @@ func (c *Context) Create(cfg *config.Config, retain bool, wait time.Duration) er
// Kubernetes cluster; please note that the list of actions automatically
// adapt to the topology defined in config
// TODO(fabrizio pandini): make the list of executed actions configurable from CLI
err = cc.Exec(nodeList, []string{"config", "init", "join"}, wait)
err = cc.Exec(nodeList, []string{"haproxy", "config", "init", "join"}, wait)
if err != nil {
// In case of errors nodes are deleted (except if retain is explicitly set)
log.Error(err)

View File

@@ -124,6 +124,8 @@ func (cc *Context) ProvisionNodes() (nodeList map[string]*nodes.Node, err error)
var node *nodes.Node
switch configNode.Role {
case config.ExternalLoadBalancerRole:
node, err = nodes.CreateExternalLoadBalancerNode(name, configNode.Image, cc.ClusterLabel())
case config.ControlPlaneRole:
node, err = nodes.CreateControlPlaneNode(name, configNode.Image, cc.ClusterLabel())
case config.WorkerRole:

View File

@@ -76,13 +76,7 @@ func (d *DerivedConfig) Validate() error {
// TODO(fabrizio pandini): this check is temporary / WIP
// kind v1alpha config fully supports multi nodes, but the cluster creation logic implemented in
// pkg/cluster/contex.go does it only partially (yet).
// As soon a external load-balancer and external etcd is implemented in pkg/cluster, this should go away
if d.ExternalLoadBalancer() != nil {
errs = append(errs, fmt.Errorf("multi node support is still a work in progress, currently external load balancer node is not supported"))
}
if d.SecondaryControlPlanes() != nil {
errs = append(errs, fmt.Errorf("multi node support is still a work in progress, currently only single control-plane node are supported"))
}
// As soon as external etcd is implemented in pkg/cluster, this should go away
if d.ExternalEtcd() != nil {
errs = append(errs, fmt.Errorf("multi node support is still a work in progress, currently external etcd node is not supported"))
}

View File

@@ -0,0 +1,133 @@
/*
Copyright 2019 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 create
import (
"fmt"
"io/ioutil"
"os"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/kind/pkg/cluster/internal/haproxy"
"sigs.k8s.io/kind/pkg/cluster/internal/kubeadm"
)
// HAProxyAction implements action for configuring and starting the
// external load balancer in front of the control-plane nodes.
type HAProxyAction struct{}
func init() {
registerAction("haproxy", NewHAProxyAction)
}
// NewHAProxyAction returns a new HAProxyAction
func NewHAProxyAction() Action {
return &HAProxyAction{}
}
// Tasks returns the list of action tasks
func (b *HAProxyAction) Tasks() []Task {
return []Task{
{
Description: "Starting the external load balancer ⛵",
TargetNodes: selectExternalLoadBalancerNode,
Run: runHAProxy,
},
}
}
// runKubeadmJoin executes haproxy
func runHAProxy(ec *execContext, configNode *NodeReplica) error {
// collects info about the existing controlplane nodes
var backendServers = map[string]string{}
for _, n := range ec.ControlPlanes() {
// gets the handle for the control plane node
controlPlaneHandle, ok := ec.NodeFor(n)
if !ok {
return errors.Errorf("unable to get the handle for operating on node: %s", n.Name)
}
// gets the IP of the control plane node
controlPlaneIP, err := controlPlaneHandle.IP()
if err != nil {
return errors.Wrapf(err, "failed to get IP for node %s", n.Name)
}
backendServers[n.Name] = fmt.Sprintf("%s:%d", controlPlaneIP, kubeadm.APIServerPort)
}
// create haproxy config file writing a local temp file on the host machine
haproxyConfig, err := createHAProxyConfig(
&haproxy.ConfigData{
ControlPlanePort: haproxy.ControlPlanePort,
BackendServers: backendServers,
},
)
defer os.Remove(haproxyConfig)
if err != nil {
// TODO(bentheelder): logging here
return errors.Wrap(err, "failed to create kubeadm config")
}
// get the target node for this task (the load balancer node)
node, ok := ec.NodeFor(configNode)
if !ok {
return errors.Errorf("unable to get the handle for operating on node: %s", configNode.Name)
}
// copy the haproxy config file from the host to the node
if err := node.CopyTo(haproxyConfig, "/kind/haproxy.cfg"); err != nil {
// TODO(bentheelder): logging here
return errors.Wrap(err, "failed to copy haproxy config to node")
}
// starts a docker container with HA proxy load balancer
if err := node.Command(
"/bin/sh", "-c",
fmt.Sprintf("docker run -d -v /kind/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro --network host --restart always %s", haproxy.Image),
).Run(); err != nil {
return errors.Wrap(err, "failed to start haproxy")
}
return nil
}
func createHAProxyConfig(data *haproxy.ConfigData) (path string, err error) {
// create haproxy config file
f, err := ioutil.TempFile("", "")
if err != nil {
return "", errors.Wrap(err, "failed to create haproxy config")
}
path = f.Name()
// generate the config contents
config, err := haproxy.Config(data)
if err != nil {
os.Remove(path)
return "", err
}
// write to the file
log.Infof("Using haproxy:\n\n%s\n", config)
_, err = f.WriteString(config)
if err != nil {
os.Remove(path)
return "", err
}
return path, nil
}

View File

@@ -25,6 +25,7 @@ import (
log "github.com/sirupsen/logrus"
"sigs.k8s.io/kind/pkg/cluster/config"
"sigs.k8s.io/kind/pkg/cluster/internal/haproxy"
"sigs.k8s.io/kind/pkg/cluster/internal/kubeadm"
"sigs.k8s.io/kind/pkg/kustomize"
)
@@ -70,27 +71,32 @@ func runKubeadmConfig(ec *execContext, configNode *NodeReplica) error {
return errors.Wrap(err, "failed to get kubernetes version from node: %v")
}
// get the control plane endpoint, in case the cluster has an external load balancer in
// front of the control-plane nodes
controlPlaneEndpoint, err := getControlPlaneEndpoint(ec)
if err != nil {
// TODO(bentheelder): logging here
return err
}
// create kubeadm config file writing a local temp file
kubeadmConfig, err := createKubeadmConfig(
ec.Config,
ec.DerivedConfig,
kubeadm.ConfigData{
ClusterName: ec.Name(),
KubernetesVersion: kubeVersion,
APIBindPort: kubeadm.APIServerPort,
Token: kubeadm.Token,
// TODO(fabriziopandini): when external load-balancer will be
// implemented also controlPlaneAddress should be added
ClusterName: ec.Name(),
KubernetesVersion: kubeVersion,
ControlPlaneEndpoint: controlPlaneEndpoint,
APIBindPort: kubeadm.APIServerPort,
Token: kubeadm.Token,
},
)
defer os.Remove(kubeadmConfig)
if err != nil {
// TODO(bentheelder): logging here
return fmt.Errorf("failed to create kubeadm config: %v", err)
}
// defer deletion of the local temp file
defer os.Remove(kubeadmConfig)
// copy the config to the node
if err := node.CopyTo(kubeadmConfig, "/kind/kubeadm.conf"); err != nil {
// TODO(bentheelder): logging here
@@ -100,6 +106,28 @@ func runKubeadmConfig(ec *execContext, configNode *NodeReplica) error {
return nil
}
// getControlPlaneEndpoint return the control plane endpoint in case the cluster has an external load balancer in
// front of the control-plane nodes, otherwise return an empty string.
func getControlPlaneEndpoint(ec *execContext) (string, error) {
if ec.ExternalLoadBalancer() == nil {
return "", nil
}
// gets the handle for the load balancer node
loadBalancerHandle, ok := ec.NodeFor(ec.ExternalLoadBalancer())
if !ok {
return "", errors.Errorf("unable to get the handle for operating on node: %s", ec.ExternalLoadBalancer().Name)
}
// gets the IP of the load balancer
loadBalancerIP, err := loadBalancerHandle.IP()
if err != nil {
return "", errors.Wrapf(err, "failed to get IP for node: %s", ec.ExternalLoadBalancer().Name)
}
return fmt.Sprintf("%s:%d", loadBalancerIP, haproxy.ControlPlanePort), nil
}
// createKubeadmConfig creates the kubeadm config file for the cluster
// by running data through the template and writing it to a temp file
// the config file path is returned, this file should be removed later

View File

@@ -18,10 +18,13 @@ package create
import (
"fmt"
"os"
"path/filepath"
"github.com/pkg/errors"
"sigs.k8s.io/kind/pkg/cluster/internal/kubeadm"
"sigs.k8s.io/kind/pkg/fs"
)
// kubeadmJoinAction implements action for joining nodes
@@ -40,10 +43,14 @@ func newKubeadmJoinAction() Action {
// Tasks returns the list of action tasks
func (b *kubeadmJoinAction) Tasks() []Task {
return []Task{
// TODO(fabrizio pandini): add Run kubeadm join --experimental-master
// on SecondaryControlPlaneNodes
{
// Run kubeadm join on the WorkeNodes
// Run kubeadm join on the secondary control plane Nodes
Description: "Joining control-plane node to Kubernetes ☸",
TargetNodes: selectSecondaryControlPlaneNodes,
Run: runKubeadmJoinControlPlane,
},
{
// Run kubeadm join on the Worker Nodes
Description: "Joining worker node to Kubernetes ☸",
TargetNodes: selectWorkerNodes,
Run: runKubeadmJoin,
@@ -51,38 +58,115 @@ func (b *kubeadmJoinAction) Tasks() []Task {
}
}
// runKubeadmJoin executes kubadm join
func runKubeadmJoin(ec *execContext, configNode *NodeReplica) error {
// before running join, it should be retrived
// runKubeadmJoinControlPlane executes kubadm join --control-plane command
func runKubeadmJoinControlPlane(ec *execContext, configNode *NodeReplica) error {
// gets the node where
// TODO(fabrizio pandini): when external load-balancer will be
// implemented this should be modified accordingly
controlPlaneHandle, ok := ec.NodeFor(ec.DerivedConfig.BootStrapControlPlane())
if !ok {
return fmt.Errorf("unable to get the handle for operating on node: %s", ec.DerivedConfig.BootStrapControlPlane().Name)
}
// gets the IP of the bootstrap master node
controlPlaneIP, err := controlPlaneHandle.IP()
// get the join addres
joinAddress, err := getJoinAddress(ec)
if err != nil {
return errors.Wrap(err, "failed to get IP for node")
// TODO(bentheelder): logging here
return err
}
// get the target node for this task
// get the target node for this task (the joining node)
node, ok := ec.NodeFor(configNode)
if !ok {
return fmt.Errorf("unable to get the handle for operating on node: %s", configNode.Name)
}
// TODO(fabrizio pandini): might be we want to run pre-kubeadm hooks on workers too
// creates the folder tree for pre-loading necessary cluster certificates
// on the joining node
if err := node.Command("mkdir", "-p", "/etc/kubernetes/pki/etcd").Run(); err != nil {
return errors.Wrap(err, "failed to join node with kubeadm")
}
// run kubeadm
// define the list of necessary cluster certificates
fileNames := []string{
"ca.crt", "ca.key",
"front-proxy-ca.crt", "front-proxy-ca.key",
"sa.pub", "sa.key",
}
if ec.ExternalEtcd() == nil {
fileNames = append(fileNames, "etcd/ca.crt", "etcd/ca.key")
}
// creates a temporary folder on the host that should acts as a transit area
// for moving necessary cluster certificates
tmpDir, err := fs.TempDir("", "")
if err != nil {
return err
}
defer os.RemoveAll(tmpDir)
err = os.MkdirAll(filepath.Join(tmpDir, "/etcd"), os.ModePerm)
if err != nil {
return err
}
// get the handle for the bootstrap control plane node (the source for necessary cluster certificates)
controlPlaneHandle, ok := ec.NodeFor(ec.BootStrapControlPlane())
if !ok {
return errors.Errorf("unable to get the handle for operating on node: %s", ec.BootStrapControlPlane().Name)
}
// copies certificates from the bootstrap control plane node to the joining node
for _, fileName := range fileNames {
// sets the path of the certificate into a node
containerPath := filepath.Join("/etc/kubernetes/pki", fileName)
// set the path of the certificate into the tmp area on the host
tmpPath := filepath.Join(tmpDir, fileName)
// copies from bootstrap control plane node to tmp area
if err := controlPlaneHandle.CopyFrom(containerPath, tmpPath); err != nil {
return errors.Wrapf(err, "failed to copy certificate %s", fileName)
}
// copies from tmp area to joining node
if err := node.CopyTo(tmpPath, containerPath); err != nil {
return errors.Wrapf(err, "failed to copy certificate %s", fileName)
}
}
// run kubeadm join --control-plane
if err := node.Command(
"kubeadm", "join",
// the control plane address uses the docker ip and a well know APIServerPort that
// the join command uses the docker ip and a well know port that
// are accessible only inside the docker network
fmt.Sprintf("%s:%d", controlPlaneIP, kubeadm.APIServerPort),
joinAddress,
// set the node to join as control-plane
"--experimental-control-plane",
// uses a well known token and skips ca certification for automating TLS bootstrap process
"--token", kubeadm.Token,
"--discovery-token-unsafe-skip-ca-verification",
// preflight errors are expected, in particular for swap being enabled
// TODO(bentheelder): limit the set of acceptable errors
"--ignore-preflight-errors=all",
).Run(); err != nil {
return errors.Wrap(err, "failed to join a control plane node with kubeadm")
}
return nil
}
// runKubeadmJoin executes kubadm join command
func runKubeadmJoin(ec *execContext, configNode *NodeReplica) error {
// get the join addres
joinAddress, err := getJoinAddress(ec)
if err != nil {
// TODO(bentheelder): logging here
return err
}
// get the target node for this task (the joining node)
node, ok := ec.NodeFor(configNode)
if !ok {
return fmt.Errorf("unable to get the handle for operating on node: %s", configNode.Name)
}
// run kubeadm join
if err := node.Command(
"kubeadm", "join",
// the join command uses the docker ip and a well know port that
// are accessible only inside the docker network
joinAddress,
// uses a well known token and skipping ca certification for automating TLS bootstrap process
"--token", kubeadm.Token,
"--discovery-token-unsafe-skip-ca-verification",
@@ -93,9 +177,37 @@ func runKubeadmJoin(ec *execContext, configNode *NodeReplica) error {
return errors.Wrap(err, "failed to join node with kubeadm")
}
// TODO(fabrizio pandini): might be we want to run post-kubeadm hooks on workers too
// TODO(fabrizio pandini): might be we want to run post-setup hooks on workers too
return nil
}
// getJoinAddress return the join addres thas is the control plane endpoint in case the cluster has
// an external load balancer in front of the control-plane nodes, otherwise the address of the
// boostrap control plane node.
func getJoinAddress(ec *execContext) (string, error) {
// get the control plane endpoint, in case the cluster has an external load balancer in
// front of the control-plane nodes
controlPlaneEndpoint, err := getControlPlaneEndpoint(ec)
if err != nil {
// TODO(bentheelder): logging here
return "", err
}
// if the control plane endpoint is defined we are using it as a join address
if controlPlaneEndpoint != "" {
return controlPlaneEndpoint, nil
}
// otherwise, gets the BootStrapControlPlane node
controlPlaneHandle, ok := ec.NodeFor(ec.BootStrapControlPlane())
if !ok {
return "", errors.Errorf("unable to get the handle for operating on node: %s", ec.BootStrapControlPlane().Name)
}
// gets the IP of the bootstrap control plane node
controlPlaneIP, err := controlPlaneHandle.IP()
if err != nil {
return "", errors.Wrap(err, "failed to get IP for node")
}
return fmt.Sprintf("%s:%d", controlPlaneIP, kubeadm.APIServerPort), nil
}

View File

@@ -0,0 +1,85 @@
/*
Copyright 2019 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 haproxy
import (
"bytes"
"text/template"
"github.com/pkg/errors"
)
// ConfigData is supplied to the haproxy config template
type ConfigData struct {
ControlPlanePort int
BackendServers map[string]string
}
// DefaultConfigTemplate is the haproxy config template
const DefaultConfigTemplate = `
global
log 127.0.0.1 local2
daemon
defaults
mode http
log global
option httplog
option dontlognull
option http-server-close
option forwardfor except 127.0.0.0/8
option redispatch
retries 3
timeout http-request 10s
timeout queue 1m
timeout connect 10s
timeout client 1m
timeout server 1m
timeout http-keep-alive 10s
timeout check 10s
maxconn 3000
frontend controlPlane
bind *:{{ .ControlPlanePort }}
option tcplog
mode tcp
default_backend kube-apiservers
backend kube-apiservers
mode tcp
balance roundrobin
option ssl-hello-chk
{{range $server, $address := .BackendServers}}
server {{ $server }} {{ $address }} check
{{- end}}
`
// Config returns a kubeadm config generated from config data, in particular
// the kubernetes version
func Config(data *ConfigData) (config string, err error) {
t, err := template.New("haproxy-config").Parse(DefaultConfigTemplate)
if err != nil {
return "", errors.Wrap(err, "failed to parse config template")
}
// execute the template
var buff bytes.Buffer
err = t.Execute(&buff, data)
if err != nil {
return "", errors.Wrap(err, "error executing config template")
}
return buff.String(), nil
}

View File

@@ -0,0 +1,23 @@
/*
Copyright 2019 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 haproxy
// ControlPlanePort defines the port where the control plane is listening on the load balancer node
const ControlPlanePort = 6443
// Image defines the haproxy image:tag
const Image = "haproxy:1.8.14-alpine"

View File

@@ -0,0 +1,18 @@
/*
Copyright 2019 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 haproxy contains haproxy related constants and configuration
package haproxy

View File

@@ -30,7 +30,9 @@ import (
type ConfigData struct {
ClusterName string
KubernetesVersion string
// The API Server port
// The ControlPlaneEndpoint, that is the address of the external loadbalancer, if defined
ControlPlaneEndpoint string
// The Local API Server port
APIBindPort int
// The Token for TLS bootstrap
Token string
@@ -69,6 +71,9 @@ clusterName: "{{.ClusterName}}"
# we use a well know token for TLS bootstrap
bootstrapTokens:
- token: "{{ .Token }}"
{{ if .ControlPlaneEndpoint -}}
controlPlaneEndpoint: {{ .ControlPlaneEndpoint }}
{{- end }}
# we use a well know port for making the API server discoverable inside docker network.
# from the host machine such port will be accessible via a random local port instead.
api:
@@ -93,6 +98,9 @@ apiVersion: kubeadm.k8s.io/v1alpha3
kind: ClusterConfiguration
kubernetesVersion: {{.KubernetesVersion}}
clusterName: "{{.ClusterName}}"
{{ if .ControlPlaneEndpoint -}}
controlPlaneEndpoint: {{ .ControlPlaneEndpoint }}
{{- end }}
# we need nsswitch.conf so we use /etc/hosts
# https://github.com/kubernetes/kubernetes/issues/69195
apiServerExtraVolumes:
@@ -135,6 +143,9 @@ apiVersion: kubeadm.k8s.io/v1beta1
kind: ClusterConfiguration
kubernetesVersion: {{.KubernetesVersion}}
clusterName: "{{.ClusterName}}"
{{ if .ControlPlaneEndpoint -}}
controlPlaneEndpoint: {{ .ControlPlaneEndpoint }}
{{- end }}
# on docker for mac we have to expose the api server via port forward,
# so we need to ensure the cert is valid for localhost so we can talk
# to the cluster after rewriting the kubeconfig to point to localhost

View File

@@ -21,6 +21,7 @@ import (
"net"
"github.com/pkg/errors"
"sigs.k8s.io/kind/pkg/cluster/internal/haproxy"
"sigs.k8s.io/kind/pkg/cluster/internal/kubeadm"
"sigs.k8s.io/kind/pkg/docker"
)
@@ -67,6 +68,30 @@ func CreateControlPlaneNode(name, image, clusterLabel string) (node *Node, err e
return node, nil
}
// CreateExternalLoadBalancerNode creates an external loab balancer node
// and gets ready for exposing the the API server and the load balancer admin console
func CreateExternalLoadBalancerNode(name, image, clusterLabel string) (node *Node, err error) {
// gets a random host port for control-plane load balancer
port, err := getPort()
if err != nil {
return nil, errors.Wrap(err, "failed to get port for control-plane load balancer")
}
node, err = createNode(name, image, clusterLabel,
// publish selected port for the control plane
"--expose", fmt.Sprintf("%d", port),
"-p", fmt.Sprintf("%d:%d", port, haproxy.ControlPlanePort),
)
if err != nil {
return node, err
}
// stores the port mapping into the node internal state
node.ports = map[int]int{haproxy.ControlPlanePort: port}
return node, nil
}
// CreateWorkerNode creates a worker node
func CreateWorkerNode(name, image, clusterLabel string) (node *Node, err error) {
node, err = createNode(name, image, clusterLabel)

View File

@@ -88,6 +88,14 @@ func (n *Node) CopyTo(source, dest string) error {
return docker.CopyTo(source, n.nameOrID, dest)
}
// CopyFrom copies the source file on the node to dest on the host
// TODO(fabrizio pandini): note that this does have limitations around symlinks
// but this should go away when kubeadm automatic copy certs lands,
// otherwise it should be refactored in something more robust in the long term
func (n *Node) CopyFrom(source, dest string) error {
return docker.CopyFrom(n.nameOrID, source, dest)
}
// WaitForDocker waits for Docker to be ready on the node
// it returns true on success, and false on a timeout
func (n *Node) WaitForDocker(until time.Time) bool {