github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/kubetest/kubeadmdind/kubeadm_dind.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package kubeadmdind implements a kubetest deployer based on the scripts
    18  // in the github.com/kubernetes-sigs/kubeadm-dind-cluster repo.
    19  // This deployer can be used to create a multinode, containerized Kubernetes
    20  // cluster that runs inside a Prow DinD container.
    21  package kubeadmdind
    22  
    23  import (
    24  	"bytes"
    25  	"errors"
    26  	"flag"
    27  	"fmt"
    28  	"io/ioutil"
    29  	"log"
    30  	"os"
    31  	"os/exec"
    32  	"path/filepath"
    33  	"strings"
    34  	"time"
    35  
    36  	"k8s.io/test-infra/kubetest/process"
    37  )
    38  
    39  var (
    40  	// Names that are fixed in the Kubeadm DinD scripts
    41  	kubeMasterPrefix = "kube-master"
    42  	kubeNodePrefix   = "kube-node"
    43  
    44  	// Systemd service logs to collect on the host container
    45  	hostServices = []string{
    46  		"docker",
    47  	}
    48  
    49  	// Docker commands to run on the host container and embedded node
    50  	// containers for log dump
    51  	dockerCommands = []struct {
    52  		cmd     string
    53  		logFile string
    54  	}{
    55  		{"docker images", "docker_images.log"},
    56  		{"docker ps -a", "docker_ps.log"},
    57  	}
    58  
    59  	// Systemd service logs to collect on the master and worker embedded
    60  	// node containers for log dump
    61  	systemdServices = []string{
    62  		"kubelet",
    63  		"docker",
    64  	}
    65  	masterKubePods = []string{
    66  		"kube-apiserver",
    67  		"kube-scheduler",
    68  		"kube-controller-manager",
    69  		"kube-proxy",
    70  		"etcd",
    71  	}
    72  	nodeKubePods = []string{
    73  		"kube-proxy",
    74  		"kube-dns",
    75  	}
    76  
    77  	// Where to look for (nested) container log files on the node containers
    78  	nodeLogDir = "/var/log"
    79  
    80  	// Relative path to Kubernetes source tree
    81  	kubeOrg  = "k8s.io"
    82  	kubeRepo = "kubernetes"
    83  
    84  	// Kubeadm-DinD-Cluster (kdc) repo and main script
    85  	kdcOrg    = "github.com/kubernetes-sigs"
    86  	kdcRepo   = "kubeadm-dind-cluster"
    87  	kdcScript = "fixed/dind-cluster-stable.sh"
    88  
    89  	// Number of worker nodes to create for testing
    90  	numWorkerNodes = "2"
    91  
    92  	// Kubeadm-DinD specific flags
    93  	kubeadmDinDIPMode     = flag.String("kubeadm-dind-ip-mode", "ipv4", "(Kubeadm-DinD only) IP Mode. Can be 'ipv4' (default), 'ipv6', or 'dual-stack'.")
    94  	kubeadmDinDK8sTarFile = flag.String("kubeadm-dind-k8s-tar-file", "", "(Kubeadm-DinD only) Location of tar file containing Kubernetes server binaries.")
    95  	k8sExtractSubDir      = "kubernetes/server/bin"
    96  	k8sTestBinSubDir      = "platforms/linux/amd64"
    97  	testBinDir            = "/usr/bin"
    98  	ipv6EnableCmd         = "sysctl -w net.ipv6.conf.all.disable_ipv6=0"
    99  )
   100  
   101  // Deployer is used to implement a kubetest deployer interface
   102  type Deployer struct {
   103  	ipMode     string
   104  	k8sTarFile string
   105  	hostCmder  execCmder
   106  	control    *process.Control
   107  }
   108  
   109  // NewDeployer returns a new Kubeadm-DinD Deployer
   110  func NewDeployer(control *process.Control) (*Deployer, error) {
   111  	d := &Deployer{
   112  		ipMode:     *kubeadmDinDIPMode,
   113  		k8sTarFile: *kubeadmDinDK8sTarFile,
   114  		hostCmder:  new(hostCmder),
   115  		control:    control,
   116  	}
   117  
   118  	switch d.ipMode {
   119  	case "ipv4":
   120  		// Valid value
   121  	case "ipv6", "dual-stack":
   122  		log.Printf("Enabling IPv6")
   123  		if err := d.run(ipv6EnableCmd); err != nil {
   124  			return nil, err
   125  		}
   126  	default:
   127  		return nil, fmt.Errorf("configured --ip-mode=%s is not supported for --deployment=kubeadmdind", d.ipMode)
   128  	}
   129  
   130  	return d, nil
   131  }
   132  
   133  // execCmd executes a command on the host container.
   134  func (d *Deployer) execCmd(cmd string) *exec.Cmd {
   135  	return d.hostCmder.execCmd(cmd)
   136  }
   137  
   138  // run runs a command on the host container, and prints any errors.
   139  func (d *Deployer) run(cmd string) error {
   140  	err := d.control.FinishRunning(d.execCmd(cmd))
   141  	if err != nil {
   142  		fmt.Printf("Error: '%v'", err)
   143  	}
   144  	return err
   145  }
   146  
   147  // getOutput runs a command on the host container, prints any errors,
   148  // and returns command output.
   149  func (d *Deployer) getOutput(cmd string) ([]byte, error) {
   150  	execCmd := d.execCmd(cmd)
   151  	o, err := d.control.Output(execCmd)
   152  	if err != nil {
   153  		log.Printf("Error: '%v'", err)
   154  		return nil, err
   155  	}
   156  	return o, nil
   157  }
   158  
   159  // outputWithStderr runs a command on the host container and returns
   160  // combined stdout and stderr.
   161  func (d *Deployer) outputWithStderr(cmd *exec.Cmd) ([]byte, error) {
   162  	var stdOutErr bytes.Buffer
   163  	cmd.Stdout = &stdOutErr
   164  	cmd.Stderr = &stdOutErr
   165  	err := d.control.FinishRunning(cmd)
   166  	return stdOutErr.Bytes(), err
   167  }
   168  
   169  // Up brings up a multinode, containerized Kubernetes cluster inside a
   170  // Prow DinD container.
   171  func (d *Deployer) Up() error {
   172  
   173  	var binDir string
   174  	if d.k8sTarFile != "" {
   175  		// Extract Kubernetes server binaries
   176  		cmd := fmt.Sprintf("tar -xvf %s", *kubeadmDinDK8sTarFile)
   177  		if err := d.run(cmd); err != nil {
   178  			return err
   179  		}
   180  		// Derive the location of the extracted binaries
   181  		cwd, err := os.Getwd()
   182  		if err != nil {
   183  			return err
   184  		}
   185  		binDir = filepath.Join(cwd, k8sExtractSubDir)
   186  	} else {
   187  		// K-D-C scripts must be run from Kubernetes source tree for
   188  		// building binaries.
   189  		kubeDir, err := findPath(kubeOrg, kubeRepo, "")
   190  		if err == nil {
   191  			err = os.Chdir(kubeDir)
   192  		}
   193  		if err != nil {
   194  			return err
   195  		}
   196  	}
   197  
   198  	d.setEnv(binDir)
   199  
   200  	// Bring up a cluster inside the host Prow container
   201  	script, err := findPath(kdcOrg, kdcRepo, kdcScript)
   202  	if err != nil {
   203  		return err
   204  	}
   205  	return d.run(script + " up")
   206  }
   207  
   208  // setEnv sets environment variables for building and testing
   209  // a cluster.
   210  func (d *Deployer) setEnv(k8sBinDir string) error {
   211  	var doBuild string
   212  	switch {
   213  	case k8sBinDir == "":
   214  		doBuild = "y"
   215  	default:
   216  		doBuild = "n"
   217  	}
   218  
   219  	// Set KUBERNETES_CONFORMANCE_TEST so that the master IP address
   220  	// is derived from kube config rather than through gcloud.
   221  	envMap := map[string]string{
   222  		"NUM_NODES":                   numWorkerNodes,
   223  		"DIND_K8S_BIN_DIR":            k8sBinDir,
   224  		"BUILD_KUBEADM":               doBuild,
   225  		"BUILD_HYPERKUBE":             doBuild,
   226  		"IP_MODE":                     d.ipMode,
   227  		"KUBERNETES_CONFORMANCE_TEST": "y",
   228  		"NAT64_V4_SUBNET_PREFIX":      "172.20",
   229  	}
   230  	for env, val := range envMap {
   231  		if err := os.Setenv(env, val); err != nil {
   232  			return err
   233  		}
   234  	}
   235  	return nil
   236  }
   237  
   238  // IsUp determines if a cluster is up based on whether one or more nodes
   239  // is ready.
   240  func (d *Deployer) IsUp() error {
   241  	n, err := d.clusterSize()
   242  	if err != nil {
   243  		return err
   244  	}
   245  	if n <= 0 {
   246  		return fmt.Errorf("cluster found, but %d nodes reported", n)
   247  	}
   248  	return nil
   249  }
   250  
   251  // DumpClusterLogs copies dumps docker state and service logs for:
   252  // - Host Prow container
   253  // - Kube master node container(s)
   254  // - Kube worker node containers
   255  // to a local artifacts directory.
   256  func (d *Deployer) DumpClusterLogs(localPath, gcsPath string) error {
   257  	// Save logs from the host container
   258  	if err := d.saveHostLogs(localPath); err != nil {
   259  		return err
   260  	}
   261  
   262  	// Save logs from master node container(s)
   263  	if err := d.saveMasterNodeLogs(localPath); err != nil {
   264  		return err
   265  	}
   266  
   267  	// Save logs from worker node containers
   268  	return d.saveWorkerNodeLogs(localPath)
   269  }
   270  
   271  // TestSetup builds end-to-end test and ginkgo binaries.
   272  func (d *Deployer) TestSetup() error {
   273  	if d.k8sTarFile == "" {
   274  		// Build e2e.test and ginkgo binaries
   275  		if err := d.run("make WHAT=test/e2e/e2e.test"); err != nil {
   276  			return err
   277  		}
   278  		return d.run("make WHAT=vendor/github.com/onsi/ginkgo/ginkgo")
   279  	}
   280  	// Copy downloaded e2e.test and ginkgo binaries
   281  	for _, file := range []string{"e2e.test", "ginkgo"} {
   282  		srcPath := filepath.Join(k8sTestBinSubDir, file)
   283  		cmd := fmt.Sprintf("cp %s %s", srcPath, testBinDir)
   284  		if err := d.run(cmd); err != nil {
   285  			return err
   286  		}
   287  	}
   288  	return nil
   289  }
   290  
   291  // Down brings the DinD-based cluster down and cleans up any DinD state
   292  func (d *Deployer) Down() error {
   293  	// Bring the cluster down and clean up kubeadm-dind-cluster state
   294  	script, err := findPath(kdcOrg, kdcRepo, kdcScript)
   295  	if err != nil {
   296  		return err
   297  	}
   298  	clusterDownCommands := []string{
   299  		script + " down",
   300  		script + " clean",
   301  	}
   302  	for _, cmd := range clusterDownCommands {
   303  		if err := d.run(cmd); err != nil {
   304  			return err
   305  		}
   306  	}
   307  	return nil
   308  }
   309  
   310  // GetClusterCreated is not yet implemented.
   311  func (d *Deployer) GetClusterCreated(gcpProject string) (time.Time, error) {
   312  	return time.Time{}, errors.New("not implemented")
   313  }
   314  
   315  func (_ *Deployer) KubectlCommand() (*exec.Cmd, error) { return nil, nil }
   316  
   317  // findPath looks for the existence of a file or directory based on a
   318  // a github organization, github repo, and a relative path.  It looks
   319  // for the file/directory in this order:
   320  //    - $WORKSPACE/<gitOrg>/<gitRepo>/<gitFile>
   321  //    - $GOPATH/src/<gitOrg>/<gitRepo>/<gitFile>
   322  //    - ./<gitRepo>/<gitFile>
   323  //    - ./<gitFile>
   324  //    - ../<gitFile>
   325  // and returns the path for the first match or returns an error.
   326  func findPath(gitOrg, gitRepo, gitFile string) (string, error) {
   327  	workPath := os.Getenv("WORKSPACE")
   328  	if workPath != "" {
   329  		workPath = filepath.Join(workPath, gitOrg, gitRepo, gitFile)
   330  	}
   331  	goPath := os.Getenv("GOPATH")
   332  	if goPath != "" {
   333  		goPath = filepath.Join(goPath, "src", gitOrg, gitRepo, gitFile)
   334  	}
   335  	relPath := filepath.Join(gitRepo, gitFile)
   336  	cwd, err := os.Getwd()
   337  	if err != nil {
   338  		return "", err
   339  	}
   340  	parentDir := filepath.Dir(cwd)
   341  	parentPath := filepath.Join(parentDir, gitFile)
   342  	paths := []string{workPath, goPath, relPath, gitFile, parentPath}
   343  	for _, path := range paths {
   344  		_, err := os.Stat(path)
   345  		if err == nil {
   346  			return path, nil
   347  		}
   348  	}
   349  	err = fmt.Errorf("could not locate %s/%s/%s", gitOrg, gitRepo, gitFile)
   350  	return "", err
   351  }
   352  
   353  // execCmder defines an interface for providing a wrapper for processing
   354  // command line strings before calling os/exec.Command().
   355  // There are two implementations of this interface defined below:
   356  // - hostCmder: For executing commands locally (e.g. in Prow container).
   357  // - nodeCmder: For executing commands on node containers embedded
   358  //              in the Prow container.
   359  type execCmder interface {
   360  	execCmd(cmd string) *exec.Cmd
   361  }
   362  
   363  // hostCmder implements the execCmder interface for processing commands
   364  // locally (e.g. in Prow container).
   365  type hostCmder struct{}
   366  
   367  // execCmd splits a command line string into a command (first word) and
   368  // remaining arguments in variadic form, as required by exec.Command().
   369  func (h *hostCmder) execCmd(cmd string) *exec.Cmd {
   370  	words := strings.Fields(cmd)
   371  	return exec.Command(words[0], words[1:]...)
   372  }
   373  
   374  // nodeCmder implements the nodeExecCmder interface for processing
   375  // commands in an embedded node container.
   376  type nodeCmder struct {
   377  	node string
   378  }
   379  
   380  func newNodeCmder(node string) *nodeCmder {
   381  	cmder := new(nodeCmder)
   382  	cmder.node = node
   383  	return cmder
   384  }
   385  
   386  // execCmd creates an exec.Cmd structure for running a command line on a
   387  // nested node container in the host container. It is equivalent to running
   388  // a command via 'docker exec <node-container-name> <cmd>'.
   389  func (n *nodeCmder) execCmd(cmd string) *exec.Cmd {
   390  	args := strings.Fields(fmt.Sprintf("exec %s %s", n.node, cmd))
   391  	return exec.Command("docker", args...)
   392  }
   393  
   394  // getNode returns the node name for a nodeExecCmder
   395  func (n *nodeCmder) getNode() string {
   396  	return n.node
   397  }
   398  
   399  // execCmdSaveLog executes a command either in the host container or
   400  // in an embedded node container, and writes the combined stdout and
   401  // stderr to a log file in a local artifacts directory. (Stderr is
   402  // required because running 'docker logs ...' on nodes sometimes
   403  // returns results as stderr).
   404  func (d *Deployer) execCmdSaveLog(cmder execCmder, cmd string, logDir string, logFile string) error {
   405  	execCmd := cmder.execCmd(cmd)
   406  	o, err := d.outputWithStderr(execCmd)
   407  	if err != nil {
   408  		log.Printf("%v", err)
   409  		if len(o) > 0 {
   410  			log.Printf("%s", o)
   411  		}
   412  		// Ignore the command error and continue collecting logs
   413  		return nil
   414  	}
   415  	logPath := filepath.Join(logDir, logFile)
   416  	return ioutil.WriteFile(logPath, o, 0644)
   417  }
   418  
   419  // saveDockerState saves docker state for either a host Prow container
   420  // or an embedded node container.
   421  func (d *Deployer) saveDockerState(cmder execCmder, logDir string) error {
   422  	for _, dockerCommand := range dockerCommands {
   423  		if err := d.execCmdSaveLog(cmder, dockerCommand.cmd, logDir, dockerCommand.logFile); err != nil {
   424  			return err
   425  		}
   426  	}
   427  	return nil
   428  }
   429  
   430  // saveServiceLogs saves logs for a list of systemd services on either
   431  // a host Prow container or an embedded node container.
   432  func (d *Deployer) saveServiceLogs(cmder execCmder, services []string, logDir string) error {
   433  	for _, svc := range services {
   434  		cmd := fmt.Sprintf("journalctl -u %s.service", svc)
   435  		logFile := fmt.Sprintf("%s.log", svc)
   436  		if err := d.execCmdSaveLog(cmder, cmd, logDir, logFile); err != nil {
   437  			return err
   438  		}
   439  	}
   440  	return nil
   441  }
   442  
   443  // clusterSize determines the number of nodes in a cluster.
   444  func (d *Deployer) clusterSize() (int, error) {
   445  	o, err := d.getOutput("kubectl get nodes --no-headers")
   446  	if err != nil {
   447  		return -1, fmt.Errorf("kubectl get nodes failed: %s\n%s", err, string(o))
   448  	}
   449  	trimmed := strings.TrimSpace(string(o))
   450  	if trimmed != "" {
   451  		return len(strings.Split(trimmed, "\n")), nil
   452  	}
   453  	return 0, nil
   454  }
   455  
   456  // Create a local log artifacts directory
   457  func (d *Deployer) makeLogDir(logDir string) error {
   458  	cmd := fmt.Sprintf("mkdir -p %s", logDir)
   459  	execCmd := d.execCmd(cmd)
   460  	return d.control.FinishRunning(execCmd)
   461  }
   462  
   463  // saveHostLogs collects service logs and docker state from the host
   464  // container, and saves the logs in a local artifacts directory.
   465  func (d *Deployer) saveHostLogs(artifactsDir string) error {
   466  	log.Printf("Saving logs from host container")
   467  
   468  	// Create directory for the host container artifacts
   469  	logDir := filepath.Join(artifactsDir, "host-container")
   470  	if err := d.run("mkdir -p " + logDir); err != nil {
   471  		return err
   472  	}
   473  
   474  	// Save docker state for the host container
   475  	if err := d.saveDockerState(d.hostCmder, logDir); err != nil {
   476  		return err
   477  	}
   478  
   479  	// Copy service logs from the node container
   480  	return d.saveServiceLogs(d.hostCmder, hostServices, logDir)
   481  }
   482  
   483  // saveMasterNodeLogs collects docker state, service logs, and Kubernetes
   484  // system pod logs from all nested master node containers that are running
   485  // on the host container, and saves the logs in a local artifacts directory.
   486  func (d *Deployer) saveMasterNodeLogs(artifactsDir string) error {
   487  	masters, err := d.detectNodeContainers(kubeMasterPrefix)
   488  	if err != nil {
   489  		return err
   490  	}
   491  	for _, master := range masters {
   492  		if err := d.saveNodeLogs(master, artifactsDir, systemdServices, masterKubePods); err != nil {
   493  			return err
   494  		}
   495  	}
   496  	return nil
   497  }
   498  
   499  // saveWorkerNodeLogs collects docker state, service logs, and Kubernetes
   500  // system pod logs from all nested worker node containers that are running
   501  // on the host container, and saves the logs in a local artifacts directory.
   502  func (d *Deployer) saveWorkerNodeLogs(artifactsDir string) error {
   503  	nodes, err := d.detectNodeContainers(kubeNodePrefix)
   504  	if err != nil {
   505  		return err
   506  	}
   507  	for _, node := range nodes {
   508  		if err := d.saveNodeLogs(node, artifactsDir, systemdServices, nodeKubePods); err != nil {
   509  			return err
   510  		}
   511  	}
   512  	return nil
   513  }
   514  
   515  // detectNodeContainers creates a list of names for either all master or all
   516  // worker node containers. It does this by running 'kubectl get nodes ... '
   517  // and searching for container names that begin with a specified name prefix.
   518  func (d *Deployer) detectNodeContainers(namePrefix string) ([]string, error) {
   519  	log.Printf("Looking for container names beginning with '%s'", namePrefix)
   520  	o, err := d.getOutput("kubectl get nodes --no-headers")
   521  	if err != nil {
   522  		return nil, err
   523  	}
   524  	var nodes []string
   525  	trimmed := strings.TrimSpace(string(o))
   526  	if trimmed != "" {
   527  		lines := strings.Split(trimmed, "\n")
   528  		for _, line := range lines {
   529  			fields := strings.Fields(line)
   530  			name := fields[0]
   531  			if strings.Contains(name, namePrefix) {
   532  				nodes = append(nodes, name)
   533  			}
   534  		}
   535  	}
   536  	return nodes, nil
   537  }
   538  
   539  // detectKubeContainers creates a list of containers (either running or
   540  // exited) on a master or worker node whose names contain any of a list of
   541  // Kubernetes system pod name substrings.
   542  func (d *Deployer) detectKubeContainers(nodeCmder execCmder, node string, kubePods []string) ([]string, error) {
   543  	// Run 'docker ps -a' on the node container
   544  	cmd := fmt.Sprintf("docker ps -a")
   545  	execCmd := nodeCmder.execCmd(cmd)
   546  	o, err := d.control.Output(execCmd)
   547  	if err != nil {
   548  		log.Printf("Error running '%s' on %s: '%v'", cmd, node, err)
   549  		return nil, err
   550  	}
   551  	// Find container names that contain any of a list of pod name substrings
   552  	var containers []string
   553  	if trimmed := strings.TrimSpace(string(o)); trimmed != "" {
   554  		lines := strings.Split(trimmed, "\n")
   555  		for _, line := range lines {
   556  			if fields := strings.Fields(line); len(fields) > 0 {
   557  				name := fields[len(fields)-1]
   558  				if strings.Contains(name, "_POD_") {
   559  					// Ignore infra containers
   560  					continue
   561  				}
   562  				for _, pod := range kubePods {
   563  					if strings.Contains(name, pod) {
   564  						containers = append(containers, name)
   565  						break
   566  					}
   567  				}
   568  			}
   569  		}
   570  	}
   571  	return containers, nil
   572  }
   573  
   574  // saveNodeLogs collects docker state, service logs, and Kubernetes
   575  // system pod logs for a given node container, and saves the logs in a local
   576  // artifacts directory.
   577  func (d *Deployer) saveNodeLogs(node string, artifactsDir string, services []string, kubePods []string) error {
   578  	log.Printf("Saving logs from node container %s", node)
   579  
   580  	// Create directory for node container artifacts
   581  	logDir := filepath.Join(artifactsDir, node)
   582  	if err := d.run("mkdir -p " + logDir); err != nil {
   583  		return err
   584  	}
   585  
   586  	cmder := newNodeCmder(node)
   587  
   588  	// Save docker state for this node
   589  	if err := d.saveDockerState(cmder, logDir); err != nil {
   590  		return err
   591  	}
   592  
   593  	// Copy service logs from the node container
   594  	if err := d.saveServiceLogs(cmder, services, logDir); err != nil {
   595  		return err
   596  	}
   597  
   598  	// Copy log files for kube system pod containers (running or exited)
   599  	// from this node container.
   600  	containers, err := d.detectKubeContainers(cmder, node, kubePods)
   601  	if err != nil {
   602  		return err
   603  	}
   604  	for _, container := range containers {
   605  		cmd := fmt.Sprintf("docker logs %s", container)
   606  		logFile := fmt.Sprintf("%s.log", container)
   607  		if err := d.execCmdSaveLog(cmder, cmd, logDir, logFile); err != nil {
   608  			return err
   609  		}
   610  	}
   611  	return nil
   612  }