From c51c606f2236ddee01c3b95aa20300734cef69d2 Mon Sep 17 00:00:00 2001 From: Phillip Wittrock Date: Wed, 24 Feb 2016 13:12:42 -0800 Subject: [PATCH] Node e2e test runner - run against images - support allocating gce instances from images and running tests against them - set --hostname-override to match node name - add jenkins script to source to reproduce jenkins build locally --- hack/jenkins/job-configs/node-e2e.yaml | 35 +-- hack/verify-flags/known-flags.txt | 1 + test/e2e_node/e2e_node_suite_test.go | 4 +- test/e2e_node/e2e_remote.go | 20 +- test/e2e_node/e2e_service.go | 9 +- test/e2e_node/jenkins/e2e-node-jenkins.sh | 38 ++++ test/e2e_node/jenkins/jenkins-ci.properties | 5 + test/e2e_node/jenkins/jenkins-pull.properties | 5 + test/e2e_node/jenkins/template.properties | 10 + test/e2e_node/kubelet_test.go | 2 +- test/e2e_node/runner/run_e2e.go | 204 +++++++++++++++--- 11 files changed, 268 insertions(+), 65 deletions(-) create mode 100755 test/e2e_node/jenkins/e2e-node-jenkins.sh create mode 100644 test/e2e_node/jenkins/jenkins-ci.properties create mode 100644 test/e2e_node/jenkins/jenkins-pull.properties create mode 100644 test/e2e_node/jenkins/template.properties diff --git a/hack/jenkins/job-configs/node-e2e.yaml b/hack/jenkins/job-configs/node-e2e.yaml index 0e9621f396..e1748c36ce 100644 --- a/hack/jenkins/job-configs/node-e2e.yaml +++ b/hack/jenkins/job-configs/node-e2e.yaml @@ -57,7 +57,7 @@ # owner: owner to be notified for job failures. test results are published to owner email # repoName: github repo to checkout e.g. kubernetes/kubernetes or google/cadvisor # gitbasedir: directory under $WORKSPACE/go/src to checkout source repo to - e.g. k8s.io/kubernetes or github.com/google/cadvisor -# shell: shell to execute from workspace +# shell: bash command to execute from gitbasedir. should be a single script such as {gitproject}-jenkins.sh - job-template: name: '{gitproject}-gce-e2e-ci' description: '{gitproject} continuous e2e tests.
Test Owner: {owner}.' @@ -66,7 +66,12 @@ numToKeep: 200 node: node builders: - - shell: '{shell}' + - shell: | + #!/bin/bash + set -e + set -x + cd go/src/{gitbasedir} + {shell} publishers: - claim-build - gcs-uploader @@ -125,11 +130,6 @@ gitbasedir: 'github.com/google/cadvisor' owner: 'vishnuk@google.com' shell: | - #!/bin/bash - set -e - set -x - cd go/src/github.com/google/cadvisor - go get -u github.com/tools/godep ./build/presubmit.sh @@ -145,28 +145,11 @@ repoName: 'kubernetes/heapster' gitbasedir: 'k8s.io/heapster' owner: 'pszczesniak@google.com' - shell: | - #!/bin/bash - set -e - set -x - cd go/src/k8s.io/heapster - - make test-unit test-integration + shell: 'make test-unit test-integration' - 'kubelet': repoName: 'kubernetes/kubernetes' gitbasedir: 'k8s.io/kubernetes' owner: 'pwittroc@google.com' - shell: | - #!/bin/bash - set -e - set -x - cd go/src/k8s.io/kubernetes - - go get -u github.com/tools/godep - go get -u github.com/onsi/ginkgo/ginkgo - go get -u github.com/onsi/gomega - - godep go build test/e2e_node/environment/conformance.go - godep go run test/e2e_node/runner/run_e2e.go --zone us-central1-f --hosts e2e-node-container-vm-v20151215,e2e-node-coreos-beta.c.kubernetes-jenkins.internal,e2e-node-ubuntu-trusty,e2e-node-ubuntu-trusty-docker1-10 --logtostderr -v 2 + shell: 'test/e2e_node/jenkins/e2e-node-jenkins.sh test/e2e_node/jenkins/jenkins-ci.properties' jobs: - '{gitproject}-gce-e2e-ci' diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index e9c37e7dee..570d4e39ca 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -151,6 +151,7 @@ input-dirs insecure-bind-address insecure-port insecure-skip-tls-verify +instance-name-prefix iptables-masquerade-bit iptables-sync-period ir-data-source diff --git a/test/e2e_node/e2e_node_suite_test.go b/test/e2e_node/e2e_node_suite_test.go index 58ceb4398b..69cd8c1c65 100644 --- a/test/e2e_node/e2e_node_suite_test.go +++ b/test/e2e_node/e2e_node_suite_test.go @@ -1,5 +1,5 @@ /* -Copyright 2015 The Kubernetes Authors All rights reserved. +Copyright 2016 The Kubernetes Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ var _ = BeforeSuite(func() { } if *startServices { - e2es = newE2eService() + e2es = newE2eService(*nodeName) if err := e2es.start(); err != nil { Fail(fmt.Sprintf("Unable to start node services.\n%v", err)) } diff --git a/test/e2e_node/e2e_remote.go b/test/e2e_node/e2e_remote.go index 1de8d0600a..e8b65d108c 100644 --- a/test/e2e_node/e2e_remote.go +++ b/test/e2e_node/e2e_remote.go @@ -121,19 +121,19 @@ func CreateTestArchive() string { func RunRemote(archive string, host string) (string, error) { // Create the temp staging directory tmp := fmt.Sprintf("/tmp/gcloud-e2e-%d", rand.Int31()) - _, err := runSshCommand("ssh", host, "--", "mkdir", tmp) + _, err := RunSshCommand("ssh", host, "--", "mkdir", tmp) if err != nil { return "", err } defer func() { - output, err := runSshCommand("ssh", host, "--", "rm", "-rf", tmp) + output, err := RunSshCommand("ssh", host, "--", "rm", "-rf", tmp) if err != nil { glog.Errorf("Failed to cleanup tmp directory %s on host %v. Output:\n%s", tmp, err, output) } }() // Copy the archive to the staging directory - _, err = runSshCommand("scp", archive, fmt.Sprintf("%s:%s/", host, tmp)) + _, err = RunSshCommand("scp", archive, fmt.Sprintf("%s:%s/", host, tmp)) if err != nil { return "", err } @@ -142,18 +142,20 @@ func RunRemote(archive string, host string) (string, error) { cmd := getSshCommand(" ; ", "sudo pkill kubelet", "sudo pkill kube-apiserver", - "sudo pkill etcd") + "sudo pkill etcd", + ) // No need to log an error if pkill fails since pkill will fail if the commands are not running. // If we are unable to stop existing running k8s processes, we should see messages in the kubelet/apiserver/etcd // logs about failing to bind the required ports. - runSshCommand("ssh", host, "--", "sh", "-c", cmd) + RunSshCommand("ssh", host, "--", "sh", "-c", cmd) // Extract the archive and run the tests cmd = getSshCommand(" && ", fmt.Sprintf("cd %s", tmp), fmt.Sprintf("tar -xzvf ./%s", archiveName), - "./e2e_node.test --logtostderr --v 2 --build-services=false --node-name `hostname`") - output, err := runSshCommand("ssh", host, "--", "sh", "-c", cmd) + fmt.Sprintf("./e2e_node.test --logtostderr --v 2 --build-services=false --node-name=%s", host), + ) + output, err := RunSshCommand("ssh", host, "--", "sh", "-c", cmd) if err != nil { return "", err } @@ -167,7 +169,7 @@ func getSshCommand(sep string, args ...string) string { } // runSshCommand executes the ssh or scp command, adding the flag provided --ssh-options -func runSshCommand(cmd string, args ...string) (string, error) { +func RunSshCommand(cmd string, args ...string) (string, error) { if env, found := sshOptionsMap[*sshEnv]; found { args = append(strings.Split(env, " "), args...) } @@ -176,7 +178,7 @@ func runSshCommand(cmd string, args ...string) (string, error) { } output, err := exec.Command(cmd, args...).CombinedOutput() if err != nil { - return fmt.Sprintf("%s", output), fmt.Errorf("command %q %q failed with error: %v and output: %q", cmd, args, err, output) + return fmt.Sprintf("%s", output), fmt.Errorf("Command [%s %s] failed with error: %v and output:\n%s", cmd, strings.Join(args, " "), err, output) } return fmt.Sprintf("%s", output), nil } diff --git a/test/e2e_node/e2e_service.go b/test/e2e_node/e2e_service.go index 56f525fb0d..9dddf14148 100644 --- a/test/e2e_node/e2e_service.go +++ b/test/e2e_node/e2e_service.go @@ -40,10 +40,11 @@ type e2eService struct { apiServerCombinedOut bytes.Buffer kubeletCmd *exec.Cmd kubeletCombinedOut bytes.Buffer + nodeName string } -func newE2eService() *e2eService { - return &e2eService{} +func newE2eService(nodeName string) *e2eService { + return &e2eService{nodeName: nodeName} } func (es *e2eService) start() error { @@ -141,7 +142,9 @@ func (es *e2eService) startKubeletServer() (*exec.Cmd, error) { "--v", "2", "--logtostderr", "--log_dir", "./", "--api-servers", "http://127.0.0.1:8080", "--address", "0.0.0.0", - "--port", "10250"}, + "--port", "10250", + "--hostname-override", es.nodeName, // Required because hostname is inconsistent across hosts + }, }) } diff --git a/test/e2e_node/jenkins/e2e-node-jenkins.sh b/test/e2e_node/jenkins/e2e-node-jenkins.sh new file mode 100755 index 0000000000..d001a558e0 --- /dev/null +++ b/test/e2e_node/jenkins/e2e-node-jenkins.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Copyright 2016 The Kubernetes Authors All rights reserved. +# +# 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. + +# Script executed by jenkins to run node e2e tests against gce +# Usage: test/e2e_node/jenkins/e2e-node-jenkins.sh +# Properties files: +# - test/e2e_node/jenkins/jenkins-ci.properties : for running jenkins ci +# - test/e2e_node/jenkins/jenkins-pull.properties : for running jenkins pull request builder +# - test/e2e_node/jenkins/template.properties : template for creating a properties file to run locally + +set -e +set -x + +: "${1:?Usage test/e2e_node/jenkins/e2e-node-jenkins.sh }" + +. $1 + +if [ "$INSTALL_GODEP" = true ] ; then + go get -u github.com/tools/godep + go get -u github.com/onsi/ginkgo/ginkgo + go get -u github.com/onsi/gomega +fi + +godep go build test/e2e_node/environment/conformance.go +godep go run test/e2e_node/runner/run_e2e.go --logtostderr --v="2" --ssh-env="gce" --zone="$GCE_ZONE" --project="$GCE_PROJECT" --hosts="$GCE_HOSTS" --images="$GCE_IMAGES" diff --git a/test/e2e_node/jenkins/jenkins-ci.properties b/test/e2e_node/jenkins/jenkins-ci.properties new file mode 100644 index 0000000000..bfbf7442e0 --- /dev/null +++ b/test/e2e_node/jenkins/jenkins-ci.properties @@ -0,0 +1,5 @@ +GCE_HOSTS=e2e-node-container-vm-v20151215,e2e-node-coreos-beta,e2e-node-ubuntu-trusty,e2e-node-ubuntu-trusty-docker1-10 +GCE_IMAGES= +GCE_ZONE=us-central1-f +GCE_PROJECT=kubernetes-jenkins +INSTALL_GODEP=true diff --git a/test/e2e_node/jenkins/jenkins-pull.properties b/test/e2e_node/jenkins/jenkins-pull.properties new file mode 100644 index 0000000000..9bdd378d2b --- /dev/null +++ b/test/e2e_node/jenkins/jenkins-pull.properties @@ -0,0 +1,5 @@ +GCE_HOSTS=e2e-node-ubuntu-trusty-docker10 +GCE_IMAGES=e2e-node-ubuntu-trusty-docker10-image +GCE_ZONE=us-central1-f +GCE_PROJECT=kubernetes-jenkins-pull +INSTALL_GODEP=true diff --git a/test/e2e_node/jenkins/template.properties b/test/e2e_node/jenkins/template.properties new file mode 100644 index 0000000000..aacdb9c3ab --- /dev/null +++ b/test/e2e_node/jenkins/template.properties @@ -0,0 +1,10 @@ +# Copy this file to your home directory and modify +# Names of gce hosts to test against (must be resolvable) or empty +GCE_HOSTS= +# Names of gce images to test or empty +GCE_IMAGES= +# Gce zone to use - required when using GCE_IMAGES +GCE_ZONE= +# Gce project to use - required when using GCE_IMAGES +GCE_PROJECT= +INSTALL_GODEP=false diff --git a/test/e2e_node/kubelet_test.go b/test/e2e_node/kubelet_test.go index 69240dad1b..70292efa04 100644 --- a/test/e2e_node/kubelet_test.go +++ b/test/e2e_node/kubelet_test.go @@ -1,5 +1,5 @@ /* -Copyright 2015 The Kubernetes Authors All rights reserved. +Copyright 2016 The Kubernetes Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/test/e2e_node/runner/run_e2e.go b/test/e2e_node/runner/run_e2e.go index e2d5bb8d93..7b84749dde 100644 --- a/test/e2e_node/runner/run_e2e.go +++ b/test/e2e_node/runner/run_e2e.go @@ -1,5 +1,5 @@ /* -Copyright 2015 The Kubernetes Authors All rights reserved. +Copyright 2016 The Kubernetes Authors All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,22 +14,59 @@ See the License for the specific language governing permissions and limitations under the License. */ -// To run the e2e tests against one or more hosts on gce: $ go run run_e2e.go --hosts -// Requires gcloud compute ssh access to the hosts +// To run the e2e tests against one or more hosts on gce: +// $ go run run_e2e.go --logtostderr --v 2 --ssh-env gce --hosts +// To run the e2e tests against one or more images on gce and provision them: +// $ go run run_e2e.go --logtostderr --v 2 --project --zone --ssh-env gce --images package main import ( "flag" "fmt" + "net/http" "os" "strings" + "time" "k8s.io/kubernetes/test/e2e_node" + + "github.com/golang/glog" + "github.com/pborman/uuid" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "google.golang.org/api/compute/v1" ) +var instanceNamePrefix = flag.String("instance-name-prefix", "", "prefix for instance names") +var zone = flag.String("zone", "", "gce zone the hosts live in") +var project = flag.String("project", "", "gce project the hosts live in") +var images = flag.String("images", "", "images to test") var hosts = flag.String("hosts", "", "hosts to test") +var computeService *compute.Service + +type TestResult struct { + output string + err error + host string +} + func main() { + flag.Parse() + + if *hosts == "" && *images == "" { + glog.Fatalf("Must specify one of --images or --hosts flag.") + } + if *images != "" && *zone == "" { + glog.Fatal("Must specify --zone flag") + } + if *images != "" && *project == "" { + glog.Fatal("Must specify --project flag") + } + if *instanceNamePrefix == "" { + *instanceNamePrefix = "tmp-node-e2e-" + uuid.NewUUID().String()[:8] + } + // Setup coloring stat, _ := os.Stdout.Stat() useColor := (stat.Mode() & os.ModeCharDevice) != 0 @@ -40,38 +77,58 @@ func main() { noColour = "\033[0m" } - flag.Parse() - if *hosts == "" { - fmt.Printf("Must specific --hosts flag") - } archive := e2e_node.CreateTestArchive() defer os.Remove(archive) results := make(chan *TestResult) - hs := strings.Split(*hosts, ",") - for _, h := range hs { - fmt.Printf("Starting tests on host %s.", h) - go func(host string) { - output, err := e2e_node.RunRemote(archive, host) - results <- &TestResult{ - output: output, - err: err, - host: host, + running := 0 + if *images != "" { + // Setup the gce client for provisioning instances + // Getting credentials on gce jenkins is flaky, so try a couple times + var err error + for i := 0; i < 10; i++ { + var client *http.Client + client, err = google.DefaultClient(oauth2.NoContext, compute.ComputeScope) + if err != nil { + continue } - }(h) + computeService, err = compute.New(client) + if err != nil { + continue + } + time.Sleep(time.Second * 6) + } + if err != nil { + glog.Fatalf("Unable to create gcloud compute service using defaults. Make sure you are authenticated. %v", err) + } + + for _, image := range strings.Split(*images, ",") { + running++ + fmt.Printf("Initializing e2e tests using image %s.\n", image) + go func(image string) { results <- testImage(image, archive) }(image) + } + } + if *hosts != "" { + for _, host := range strings.Split(*hosts, ",") { + fmt.Printf("Initializing e2e tests using host %s.\n", host) + running++ + go func(host string) { + results <- testHost(host, archive) + }(host) + } } // Wait for all tests to complete and emit the results errCount := 0 - for i := 0; i < len(hs); i++ { + for i := 0; i < running; i++ { tr := <-results host := tr.host fmt.Printf("%s================================================================%s\n", blue, noColour) if tr.err != nil { errCount++ - fmt.Printf("Failure Finished Host %s Test Suite %s %v\n", host, tr.output, tr.err) + fmt.Printf("Failure Finished Host %s Test Suite\n%s\n%v\n", host, tr.output, tr.err) } else { - fmt.Printf("Success Finished Host %s Test Suite %s\n", host, tr.output) + fmt.Printf("Success Finished Host %s Test Suite\n%s\n", host, tr.output) } fmt.Printf("%s================================================================%s\n", blue, noColour) } @@ -83,8 +140,107 @@ func main() { } } -type TestResult struct { - output string - err error - host string +// Run tests in archive against host +func testHost(host, archive string) *TestResult { + output, err := e2e_node.RunRemote(archive, host) + return &TestResult{ + output: output, + err: err, + host: host, + } +} + +// Provision a gce instance using image and run the tests in archive against the instance. +// Delete the instance afterward. +func testImage(image, archive string) *TestResult { + host, err := createInstance(image) + defer deleteInstance(image) + if err != nil { + return &TestResult{ + err: fmt.Errorf("Unable to create gce instance with running docker daemon for image %s. %v", image, err), + } + } + return testHost(host, archive) +} + +// Provision a gce instance using image +func createInstance(image string) (string, error) { + name := imageToInstanceName(image) + i := &compute.Instance{ + Name: name, + MachineType: machineType(), + NetworkInterfaces: []*compute.NetworkInterface{ + { + AccessConfigs: []*compute.AccessConfig{ + { + Type: "ONE_TO_ONE_NAT", + Name: "External NAT", + }, + }}, + }, + Disks: []*compute.AttachedDisk{ + { + AutoDelete: true, + Boot: true, + Type: "PERSISTENT", + InitializeParams: &compute.AttachedDiskInitializeParams{ + SourceImage: sourceImage(image), + }, + }, + }, + } + op, err := computeService.Instances.Insert(*project, *zone, i).Do() + if err != nil { + return "", err + } + if op.Error != nil { + return "", fmt.Errorf("Could not create instance %s: %+v", name, op.Error) + } + + instanceRunning := false + for i := 0; i < 30 && !instanceRunning; i++ { + if i > 0 { + time.Sleep(time.Second * 20) + } + var instance *compute.Instance + instance, err = computeService.Instances.Get(*project, *zone, name).Do() + if err != nil { + continue + } + if strings.ToUpper(instance.Status) != "RUNNING" { + err = fmt.Errorf("Instance %s not in state RUNNING, was %s.", name, instance.Status) + continue + } + var output string + output, err = e2e_node.RunSshCommand("ssh", name, "--", "sudo", "docker", "version") + if err != nil { + err = fmt.Errorf("Instance %s not running docker daemon - Command failed: %s", name, output) + continue + } + if !strings.Contains(output, "Server") { + err = fmt.Errorf("Instance %s not running docker daemon - Server not found: %s", name, output) + continue + } + instanceRunning = true + } + return name, err +} + +func deleteInstance(image string) { + _, err := computeService.Instances.Delete(*project, *zone, imageToInstanceName(image)).Do() + if err != nil { + glog.Infof("Error deleting instance %s", imageToInstanceName(image)) + } +} + +func imageToInstanceName(image string) string { + return *instanceNamePrefix + "-" + image +} + +func sourceImage(image string) string { + return fmt.Sprintf("projects/%s/global/images/%s", *project, image) +} + +func machineType() string { + return fmt.Sprintf("zones/%s/machineTypes/n1-standard-1", *zone) }