Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test runner harness for node e2e tests #17260

Merged
merged 1 commit into from
Nov 23, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions hack/test-go.sh
Expand Up @@ -37,6 +37,7 @@ kube::test::find_dirs() {
-o -path './release/*' \
-o -path './target/*' \
-o -path './test/e2e/*' \
-o -path './test/e2e_node/*' \
-o -path './test/integration/*' \
\) -prune \
\) -name '*_test.go' -print0 | xargs -0n1 dirname | sed 's|^\./||' | sort -u
Expand Down
3 changes: 3 additions & 0 deletions hack/verify-flags/known-flags.txt
Expand Up @@ -13,6 +13,7 @@ allow-privileged
api-burst
api-prefix
api-rate
api-server-host
api-server-port
api-servers
api-token
Expand Down Expand Up @@ -143,6 +144,7 @@ kubelet-certificate-authority
kubelet-client-certificate
kubelet-client-key
kubelet-docker-endpoint
kubelet-host
kubelet-host-network-sources
kubelet-https
kubelet-network-plugin
Expand All @@ -154,6 +156,7 @@ kubelet-sync-frequency
kubelet-timeout
kube-master
kubernetes-service-node-port
k8s-build-output
label-columns
last-release-pr
leave-stdin-open
Expand Down
17 changes: 17 additions & 0 deletions test/e2e_node/doc.go
@@ -0,0 +1,17 @@
/*
Copyright 2015 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.
*/

package e2e_node
45 changes: 45 additions & 0 deletions test/e2e_node/e2e_node_suite_test.go
@@ -0,0 +1,45 @@
/*
Copyright 2015 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.
*/

package e2e_node

import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

"flag"
"testing"
)

var kubeletHost = flag.String("kubelet-host", "localhost", "Host address of the kubelet")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a single flag that is host:port?

var kubeletPort = flag.Int("kubelet-port", 10250, "Kubelet port")

var apiServerHost = flag.String("api-server-host", "localhost", "Host address of the api server")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this flag host:port?

var apiServerPort = flag.Int("api-server-port", 8080, "Api server port")

func TestE2eNode(t *testing.T) {
flag.Parse()
RegisterFailHandler(Fail)
RunSpecs(t, "E2eNode Suite")
}

// Setup the kubelet on the node
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main e2e framework uses ginkgo, which provides in-built setup and teardown semantics. Should we use that instead? We can start with this for now. So a TODO might be ok for this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does use ginkgo, there just isn't much setup teardown. This is the suite generated by ginkgo bootstrap: http://onsi.github.io/ginkgo/#bootstrapping-a-suite.

The reason the node bootstrapping is in the test runner instead of the suite is so that the suite can run against a node that has already been bootstrapped. We could run the runner in another ginkgo suite if you think it would be useful.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged!

var _ = BeforeSuite(func() {
})

// Tear down the kubelet on the node
var _ = AfterSuite(func() {
})
187 changes: 187 additions & 0 deletions test/e2e_node/gcloud/gcloud.go
@@ -0,0 +1,187 @@
/*
Copyright 2015 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.
*/

package gcloud

import (
"errors"
"fmt"
"math/rand"
"os/exec"

"net"
"net/http"
"path/filepath"
"regexp"
"strings"
"time"

"github.com/golang/glog"
)

var freePortRegexp = regexp.MustCompile(".+:([0-9]+)")

type TearDown func()

type GCloudClient interface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a generic interface which can be used even on aws and on-prem in the future?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hadn't thought much about that. I can rename the package if you think that would be better (just "cloud"?). Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about infra?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This package can include all testing infra utilities.

CopyAndWaitTillHealthy(sudo bool, remotePort string, timeout time.Duration, healthUrl string, bin string, args ...string) (*CmdHandle, error)
}

type gCloudClientImpl struct {
host string
zone string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use the default-zone and autodetect that ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. I guess I don't have a default zone setup in my gce config because it always makes me supply one for gcloud compute commands

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have addressed this by making zone optional. If it is not specified, zone should be picked up from the configuration.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged!

}

type RunResult struct {
out []byte
err error
cmd string
}

type CmdHandle struct {
TearDown TearDown
Output chan RunResult
LPort string
}

func NewGCloudClient(host string, zone string) GCloudClient {
return &gCloudClientImpl{host, zone}
}

func (gc *gCloudClientImpl) Command(cmd string, moreargs ...string) ([]byte, error) {
args := append([]string{"compute", "ssh"})
if gc.zone != "" {
args = append(args, "--zone", gc.zone)
}
args = append(args, gc.host, "--", cmd)
args = append(args, moreargs...)
glog.V(2).Infof("Command gcloud %s", strings.Join(args, " "))
return exec.Command("gcloud", args...).CombinedOutput()
}

func (gc *gCloudClientImpl) TunnelCommand(sudo bool, lPort string, rPort string, cmd string, moreargs ...string) ([]byte, error) {
tunnelStr := fmt.Sprintf("-L %s:localhost:%s", lPort, rPort)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a TODO to run tests on the remote host.

args := []string{"compute", "ssh"}
if gc.zone != "" {
args = append(args, "--zone", gc.zone)
}
args = append(args, "--ssh-flag", tunnelStr, gc.host, "--")
if sudo {
args = append(args, "sudo")
}
args = append(args, cmd)
args = append(args, moreargs...)
glog.V(2).Infof("Command gcloud %s", strings.Join(args, " "))
return exec.Command("gcloud", args...).CombinedOutput()
}

func (gc *gCloudClientImpl) CopyToHost(from string, to string) ([]byte, error) {
rto := fmt.Sprintf("%s:%s", gc.host, to)
args := []string{"compute", "copy-files"}
if gc.zone != "" {
args = append(args, "--zone", gc.zone)
}
args = append(args, from, rto)
glog.V(2).Infof("Command gcloud %s", strings.Join(args, " "))
return exec.Command("gcloud", args...).CombinedOutput()
}

func (gc *gCloudClientImpl) CopyAndRun(sudo bool, remotePort string, bin string, args ...string) *CmdHandle {
h := &CmdHandle{}
h.Output = make(chan RunResult)

rand.Seed(time.Now().UnixNano())

// Define where we will copy the temp binary
tDir := fmt.Sprintf("/tmp/gcloud-e2e-%d", rand.Int31())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use os.TempDir() with ioutil.TempDir()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The temporary directory must be created on the remote host where the binaries will be copied and the kubelet will be running. afaict the os and ioutil functions create the directory locally.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. And using them would create local directory instances just to get a name to use remote. It then would require cleaning up the local system as well as the remote. Likely more trouble than it's worth. Thanks for the clarification.

_, f := filepath.Split(bin)
cmd := filepath.Join(tDir, f)
h.LPort = getLocalPort()

h.TearDown = func() {
out, err := gc.Command("sudo", "pkill", f)
if err != nil {
h.Output <- RunResult{out, err, fmt.Sprintf("pkill %s", cmd)}
return
}
out, err = gc.Command("rm", "-rf", tDir)
if err != nil {
h.Output <- RunResult{out, err, fmt.Sprintf("rm -rf %s", tDir)}
return
}
}

// Create the tmp directory
out, err := gc.Command("mkdir", "-p", tDir)
if err != nil {
glog.Errorf("mkdir failed %v", err)
h.Output <- RunResult{out, err, fmt.Sprintf("mkdir -p %s", tDir)}
return h
}

// Copy the binary
out, err = gc.CopyToHost(bin, tDir)
if err != nil {
glog.Errorf("copy-files failed %v", err)
h.Output <- RunResult{out, err, fmt.Sprintf("copy-files %s %s", bin, tDir)}
return h
}

// Do the setup
go func() {
// Start the process
out, err = gc.TunnelCommand(sudo, h.LPort, remotePort, cmd, args...)
if err != nil {
glog.Errorf("command failed %v", err)
h.Output <- RunResult{out, err, fmt.Sprintf("%s %s", cmd, strings.Join(args, " "))}
return
}
}()
return h
}

func (gc *gCloudClientImpl) CopyAndWaitTillHealthy(
sudo bool,
remotePort string, timeout time.Duration, healthUrl string, bin string, args ...string) (*CmdHandle, error) {
h := gc.CopyAndRun(sudo, remotePort, bin, args...)
eTime := time.Now().Add(timeout)
done := false
for eTime.After(time.Now()) && !done {
select {
case r := <-h.Output:
glog.V(2).Infof("Error running %s Output:\n%s Error:\n%v", r.cmd, r.out, r.err)
return h, r.err
case <-time.After(2 * time.Second):
resp, err := http.Get(fmt.Sprintf("http://localhost:%s/%s", h.LPort, healthUrl))
if err == nil && resp.StatusCode == http.StatusOK {
done = true
break
}
}
}
if !done {
return h, errors.New(fmt.Sprintf("Timeout waiting for service to be healthy at http://localhost:%s/%s", h.LPort, healthUrl))
}
glog.Info("Healthz Success")
return h, nil
}

// GetLocalPort returns a free local port that can be used for ssh tunneling
func getLocalPort() string {
l, _ := net.Listen("tcp", ":0")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Once we close the connection, won't the port be freed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah there is a race condition here if something else allocates the port before we get to use it in ssh. The goal is to give a free port to "gcloud ssh" to setup a tunnel so that we don't need to worry about firewall rules on the host running the kubelet and so requests come from "localhost". I wasn't able to find a solution for reserving a free port so it can be used by another process so this is the closest thing: Ask the OS to allocate a port for you and then immediately free it and tell the ssh process to use it.

defer l.Close()
return freePortRegexp.FindStringSubmatch(l.Addr().String())[1]
}
53 changes: 53 additions & 0 deletions test/e2e_node/kubelet_test.go
@@ -0,0 +1,53 @@
/*
Copyright 2015 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.
*/

package e2e_node

import (
"fmt"
"io/ioutil"
"net/http"

"github.com/golang/glog"
. "github.com/onsi/ginkgo"
)

var _ = Describe("Kubelet", func() {
BeforeEach(func() {
// Setup the client to talk to the kubelet
})

Describe("checking kubelet status", func() {
Context("when retrieving the node status", func() {
It("should have the container version", func() {

// TODO: This is just a place holder, write a real test here
resp, err := http.Get(fmt.Sprintf("http://%s:%d/api/v2.0/attributes", *kubeletHost, *kubeletPort))
if err != nil {
glog.Errorf("Error: %v", err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
glog.Errorf("Error: %v", err)
return
}
glog.Infof("Resp: %s", body)
})
})
})
})