Files
kind/pkg/cluster/cluster.go

300 lines
9.3 KiB
Go
Raw Normal View History

2018-07-23 10:06:37 -07:00
/*
Copyright 2018 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 cluster
import (
"fmt"
2018-08-27 10:21:43 -07:00
"io/ioutil"
"os"
2018-08-28 11:45:21 -07:00
"path/filepath"
"regexp"
2018-08-23 18:27:52 -07:00
"time"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
2018-08-06 17:05:46 -07:00
"k8s.io/test-infra/kind/pkg/cluster/config"
2018-08-27 10:21:43 -07:00
"k8s.io/test-infra/kind/pkg/cluster/kubeadm"
2018-08-06 17:05:46 -07:00
"k8s.io/test-infra/kind/pkg/exec"
2018-07-23 10:06:37 -07:00
)
2018-08-28 11:45:21 -07:00
// ClusterLabelKey is applied to each "node" docker container for identification
const ClusterLabelKey = "io.k8s.test-infra.kind-cluster"
// Context is used to create / manipulate kubernetes-in-docker clusters
2018-07-23 10:06:37 -07:00
type Context struct {
name string
2018-07-23 10:06:37 -07:00
}
2018-08-28 11:45:21 -07:00
// similar to valid docker container names, but since we will prefix
// and suffix this name, we can relax it a little
// see NewContext() for usage
// https://godoc.org/github.com/docker/docker/daemon/names#pkg-constants
var validNameRE = regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`)
// NewContext returns a new cluster management context
// if name is "" the default ("1") will be used
func NewContext(name string) (ctx *Context, err error) {
if name == "" {
name = "1"
}
// validate the name
if !validNameRE.MatchString(name) {
return nil, fmt.Errorf(
"'%s' is not a valid cluster name, cluster names must match `%s`",
name, validNameRE.String(),
)
2018-07-23 10:06:37 -07:00
}
2018-08-28 11:45:21 -07:00
return &Context{
name: name,
2018-08-28 11:45:21 -07:00
}, nil
}
// ClusterLabel returns the docker object label that will be applied
// to cluster "node" containers
func (c *Context) ClusterLabel() string {
return fmt.Sprintf("%s=%s", ClusterLabelKey, c.name)
}
// Name returns the context's name
func (c *Context) Name() string {
return c.name
2018-08-28 11:45:21 -07:00
}
// ClusterName returns the Kubernetes cluster name based on the context name
// currently this is .Name prefixed with "kind-"
func (c *Context) ClusterName() string {
return fmt.Sprintf("kind-%s", c.name)
2018-08-28 11:45:21 -07:00
}
// KubeConfigPath returns the path to where the Kubeconfig would be placed
// by kind based on the configuration.
func (c *Context) KubeConfigPath() string {
// TODO(bentheelder): Windows?
// configDir matches the standard directory expected by kubectl etc
configDir := filepath.Join(os.Getenv("HOME"), ".kube")
// note that the file name however does not, we do not want to overwite
// the standard config, though in the future we may (?) merge them
fileName := fmt.Sprintf("kind-config-%s", c.name)
2018-08-28 11:45:21 -07:00
return filepath.Join(configDir, fileName)
2018-07-23 10:06:37 -07:00
}
// Create provisions and starts a kubernetes-in-docker cluster
func (c *Context) Create(config *config.Config) error {
2018-08-09 11:26:18 -07:00
// validate config first
2018-08-28 11:45:21 -07:00
if err := config.Validate(); err != nil {
2018-08-09 11:26:18 -07:00
return err
}
2018-08-23 18:27:52 -07:00
2018-08-27 10:21:43 -07:00
// TODO(bentheelder): multiple nodes ...
kubeadmConfig, err := c.provisionControlPlane(
fmt.Sprintf("kind-%s-control-plane", c.name),
2018-08-28 11:45:21 -07:00
config,
2018-08-27 10:21:43 -07:00
)
// clean up the kubeadm config file
// NOTE: in the future we will use this for other nodes first
if kubeadmConfig != "" {
defer os.Remove(kubeadmConfig)
}
if err != nil {
return err
}
2018-08-29 16:01:49 -07:00
log.Infof(
"You can now use the cluster with:\n\nexport KUBECONFIG=\"%s\"\nkubectl cluster-info",
c.KubeConfigPath(),
)
2018-08-27 10:21:43 -07:00
return nil
2018-07-23 10:06:37 -07:00
}
// Delete tears down a kubernetes-in-docker cluster
func (c *Context) Delete() error {
nodes, err := c.ListNodes(true)
if err != nil {
return fmt.Errorf("error listing nodes: %v", err)
}
return c.deleteNodes(nodes...)
}
2018-08-27 10:21:43 -07:00
// provisionControlPlane provisions the control plane node
// and the cluster kubeadm config
func (c *Context) provisionControlPlane(
nodeName string,
config *config.Config,
) (kubeadmConfigPath string, err error) {
2018-07-23 10:06:37 -07:00
// create the "node" container (docker run, but it is paused, see createNode)
node, err := createNode(nodeName, config.Image, c.ClusterLabel())
2018-08-27 10:21:43 -07:00
if err != nil {
return "", err
2018-07-23 10:06:37 -07:00
}
// systemd-in-a-container should have read only /sys
// https://www.freedesktop.org/wiki/Software/systemd/ContainerInterface/
// however, we need other things from `docker run --privileged` ...
// and this flag also happens to make /sys rw, amongst other things
2018-08-27 10:21:43 -07:00
if err := node.Run("mount", "-o", "remount,ro", "/sys"); err != nil {
2018-07-23 10:06:37 -07:00
// TODO(bentheelder): logging here
2018-08-23 18:27:52 -07:00
// TODO(bentheelder): add a flag to retain the broken nodes for debugging
2018-08-27 10:21:43 -07:00
c.deleteNodes(node.nameOrID)
return "", err
2018-07-23 10:06:37 -07:00
}
2018-08-27 10:21:43 -07:00
// signal the node entrypoint to continue booting into systemd
if err := node.SignalStart(); err != nil {
2018-07-23 10:06:37 -07:00
// TODO(bentheelder): logging here
2018-08-27 10:21:43 -07:00
// TODO(bentheelder): add a flag to retain the broken nodes for debugging
c.deleteNodes(node.nameOrID)
return "", err
2018-07-23 10:06:37 -07:00
}
2018-08-23 18:27:52 -07:00
// wait for docker to be ready
2018-08-27 10:21:43 -07:00
if !node.WaitForDocker(time.Now().Add(time.Second * 30)) {
// TODO(bentheelder): logging here
// TODO(bentheelder): add a flag to retain the broken nodes for debugging
c.deleteNodes(node.nameOrID)
return "", fmt.Errorf("timed out waiting for docker to be ready on node")
2018-08-23 18:27:52 -07:00
}
2018-08-27 10:21:43 -07:00
// load the docker image artifacts into the docker daemon
node.LoadImages()
// get installed kubernetes version from the node image
kubeVersion, err := node.KubeVersion()
if err != nil {
// TODO(bentheelder): logging here
// TODO(bentheelder): add a flag to retain the broken nodes for debugging
c.deleteNodes(node.nameOrID)
return "", fmt.Errorf("failed to get kubernetes version from node: %v", err)
}
// create kubeadm config file
2018-08-28 17:21:17 -07:00
kubeadmConfig, err := c.createKubeadmConfig(
config.KubeadmConfigTemplate,
kubeadm.ConfigData{
ClusterName: c.ClusterName(),
KubernetesVersion: kubeVersion,
},
)
2018-08-27 10:21:43 -07:00
// copy the config to the node
if err := node.CopyTo(kubeadmConfig, "/kind/kubeadm.conf"); err != nil {
// TODO(bentheelder): logging here
// TODO(bentheelder): add a flag to retain the broken nodes for debugging
c.deleteNodes(node.nameOrID)
return kubeadmConfig, errors.Wrap(err, "failed to copy kubeadm config to node")
}
// run kubeadm
if err := node.Run(
// init because this is the control plane node
2018-08-23 18:27:52 -07:00
"kubeadm", "init",
2018-08-27 10:21:43 -07:00
// preflight errors are expected, in particular for swap being enabled
// TODO(bentheelder): limit the set of acceptable errors
2018-08-23 18:27:52 -07:00
"--ignore-preflight-errors=all",
2018-08-27 10:21:43 -07:00
// specify our generated config file
"--config=/kind/kubeadm.conf",
); err != nil {
// TODO(bentheelder): logging here
// TODO(bentheelder): add a flag to retain the broken nodes for debugging
c.deleteNodes(node.nameOrID)
return kubeadmConfig, errors.Wrap(err, "failed to init node with kubeadm")
2018-08-23 18:27:52 -07:00
}
2018-08-27 10:21:43 -07:00
// set up the $KUBECONFIG
2018-08-28 11:45:21 -07:00
kubeConfigPath := c.KubeConfigPath()
2018-08-27 10:21:43 -07:00
if err = node.WriteKubeConfig(kubeConfigPath); err != nil {
// TODO(bentheelder): logging here
// TODO(bentheelder): add a flag to retain the broken nodes for debugging
c.deleteNodes(node.nameOrID)
return kubeadmConfig, errors.Wrap(err, "failed to get kubeconfig from node")
}
2018-08-23 18:27:52 -07:00
2018-08-27 10:21:43 -07:00
// TODO(bentheelder): support other overlay networks
if err = node.Run(
"/bin/sh", "-c",
`kubectl apply --kubeconfig=/etc/kubernetes/admin.conf -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"`,
); err != nil {
return kubeadmConfig, errors.Wrap(err, "failed to apply overlay network")
}
2018-07-23 10:06:37 -07:00
2018-08-27 10:21:43 -07:00
// if we are only provisioning one node, remove the master taint
// https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/#master-isolation
2018-08-28 11:45:21 -07:00
if config.NumNodes == 1 {
2018-08-27 10:21:43 -07:00
if err = node.Run(
"kubectl", "--kubeconfig=/etc/kubernetes/admin.conf",
"taint", "nodes", "--all", "node-role.kubernetes.io/master-",
); err != nil {
return kubeadmConfig, errors.Wrap(err, "failed to remove master taint")
2018-08-23 18:27:52 -07:00
}
}
2018-08-27 10:21:43 -07:00
return kubeadmConfig, nil
2018-08-23 18:27:52 -07:00
}
2018-08-27 10:21:43 -07:00
// 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
func (c *Context) createKubeadmConfig(template string, data kubeadm.ConfigData) (path string, err error) {
// create kubeadm config file
f, err := ioutil.TempFile("", "")
if err != nil {
return "", errors.Wrap(err, "failed to create kubeadm config")
}
path = f.Name()
// generate the config contents
config, err := kubeadm.Config(template, data)
if err != nil {
os.Remove(path)
return "", err
}
2018-08-29 16:01:49 -07:00
log.Infof("Using KubeadmConfig:\n\n%s\n", config)
2018-08-27 10:21:43 -07:00
_, err = f.WriteString(config)
if err != nil {
os.Remove(path)
return "", err
}
return path, nil
2018-07-23 10:06:37 -07:00
}
func (c *Context) deleteNodes(names ...string) error {
cmd := exec.Command("docker", "rm")
cmd.Args = append(cmd.Args,
"-f", // force the container to be delete now
)
cmd.Args = append(cmd.Args, names...)
return cmd.Run()
}
// ListNodes returns the list of container IDs for the "nodes" in the cluster
func (c *Context) ListNodes(alsoStopped bool) (containerIDs []string, err error) {
cmd := exec.Command("docker", "ps")
cmd.Args = append(cmd.Args,
// quiet output for parsing
"-q",
// filter for nodes with the cluster label
2018-08-28 11:45:21 -07:00
"--filter", "label="+c.ClusterLabel(),
2018-07-23 10:06:37 -07:00
)
2018-08-27 10:21:43 -07:00
// optionally list nodes that are stopped
2018-07-23 10:06:37 -07:00
if alsoStopped {
cmd.Args = append(cmd.Args, "-a")
}
2018-08-06 17:05:46 -07:00
return cmd.CombinedOutputLines()
2018-07-23 10:06:37 -07:00
}