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 }