-
Notifications
You must be signed in to change notification settings - Fork 38.6k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
var kubeletPort = flag.Int("kubelet-port", 10250, "Kubelet port") | ||
|
||
var apiServerHost = flag.String("api-server-host", "localhost", "Host address of the api server") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we make this flag |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() { | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use the default-zone and autodetect that ? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not use There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: Once we close the connection, won't the port be freed? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
}) | ||
}) | ||
}) | ||
}) |
There was a problem hiding this comment.
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
?