349 lines
11 KiB
Go
349 lines
11 KiB
Go
// +build integration
|
|
|
|
/*
|
|
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.
|
|
*/
|
|
|
|
package integration
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/elazarl/goproxy"
|
|
"github.com/hashicorp/go-retryablehttp"
|
|
"github.com/phayes/freeport"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/build/kubernetes/api"
|
|
"k8s.io/minikube/pkg/util/retry"
|
|
)
|
|
|
|
// validateFunc are for subtests that share a single setup
|
|
type validateFunc func(context.Context, *testing.T, string)
|
|
|
|
// TestFunctional are functionality tests which can safely share a profile in parallel
|
|
func TestFunctional(t *testing.T) {
|
|
|
|
profile := UniqueProfileName("functional")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 40*time.Minute)
|
|
defer CleanupWithLogs(t, profile, cancel)
|
|
|
|
// Serial tests
|
|
t.Run("serial", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
validator validateFunc
|
|
}{
|
|
{"StartWithProxy", validateStartWithProxy}, // Set everything else up for success
|
|
{"KubeContext", validateKubeContext}, // Racy: must come immediately after "minikube start"
|
|
{"CacheCmd", validateCacheCmd}, // Caches images needed for subsequent tests because of proxy
|
|
}
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
tc.validator(ctx, t, profile)
|
|
})
|
|
}
|
|
})
|
|
|
|
// Now that we are out of the woods, lets go.
|
|
MaybeParallel(t)
|
|
|
|
// Parallelized tests
|
|
t.Run("parallel", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
validator validateFunc
|
|
}{
|
|
{"AddonManager", validateAddonManager},
|
|
{"ComponentHealth", validateComponentHealth},
|
|
{"ConfigCmd", validateConfigCmd},
|
|
{"DashboardCmd", validateDashboardCmd},
|
|
{"DNS", validateDNS},
|
|
{"LogsCmd", validateLogsCmd},
|
|
{"MountCmd", validateMountCmd},
|
|
{"ProfileCmd", validateProfileCmd},
|
|
{"ServicesCmd", validateServicesCmd},
|
|
{"PersistentVolumeClaim", validatePersistentVolumeClaim},
|
|
{"TunnelCmd", validateTunnelCmd},
|
|
{"SSHCmd", validateSSHCmd},
|
|
}
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
MaybeParallel(t)
|
|
tc.validator(ctx, t, profile)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func validateStartWithProxy(ctx context.Context, t *testing.T, profile string) {
|
|
srv, err := startHTTPProxy(t)
|
|
if err != nil {
|
|
t.Fatalf("Failed to set up the test proxy: %s", err)
|
|
}
|
|
startArgs := append([]string{"start", "-p", profile, "--wait=false"}, StartArgs()...)
|
|
c := exec.CommandContext(ctx, Target(), startArgs...)
|
|
env := os.Environ()
|
|
env = append(env, fmt.Sprintf("HTTP_PROXY=%s", srv.Addr))
|
|
env = append(env, "NO_PROXY=")
|
|
c.Env = env
|
|
rr, err := Run(t, c)
|
|
if err != nil {
|
|
t.Errorf("%s failed: %v", rr.Args, err)
|
|
}
|
|
|
|
want := "Found network options:"
|
|
if !strings.Contains(rr.Stdout.String(), want) {
|
|
t.Errorf("start stdout=%s, want: *%s*", rr.Stdout.String(), want)
|
|
}
|
|
|
|
want = "You appear to be using a proxy"
|
|
if !strings.Contains(rr.Stderr.String(), want) {
|
|
t.Errorf("start stderr=%s, want: *%s*", rr.Stderr.String(), want)
|
|
}
|
|
}
|
|
|
|
// validateKubeContext asserts that kubectl is properly configured (race-condition prone!)
|
|
func validateKubeContext(ctx context.Context, t *testing.T, profile string) {
|
|
rr, err := Run(t, exec.CommandContext(ctx, "kubectl", "config", "current-context"))
|
|
if err != nil {
|
|
t.Errorf("%s failed: %v", rr.Args, err)
|
|
}
|
|
if !strings.Contains(rr.Stdout.String(), profile) {
|
|
t.Errorf("current-context = %q, want %q", rr.Stdout.String(), profile)
|
|
}
|
|
}
|
|
|
|
// validateAddonManager asserts that the kube-addon-manager pod is deployed properly
|
|
func validateAddonManager(ctx context.Context, t *testing.T, profile string) {
|
|
// If --wait=false, this may take a couple of minutes
|
|
if _, err := PodWait(ctx, t, profile, "kube-system", "component=kube-addon-manager", 3*time.Minute); err != nil {
|
|
t.Errorf("wait: %v", err)
|
|
}
|
|
}
|
|
|
|
// validateComponentHealth asserts that all Kubernetes components are healthy
|
|
func validateComponentHealth(ctx context.Context, t *testing.T, profile string) {
|
|
rr, err := Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "get", "cs", "-o=json"))
|
|
if err != nil {
|
|
t.Fatalf("%s failed: %v", rr.Args, err)
|
|
}
|
|
cs := api.ComponentStatusList{}
|
|
d := json.NewDecoder(bytes.NewReader(rr.Stdout.Bytes()))
|
|
if err := d.Decode(&cs); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
|
|
for _, i := range cs.Items {
|
|
status := api.ConditionFalse
|
|
for _, c := range i.Conditions {
|
|
if c.Type != api.ComponentHealthy {
|
|
continue
|
|
}
|
|
status = c.Status
|
|
}
|
|
if status != api.ConditionTrue {
|
|
t.Errorf("unexpected status: %v - item: %+v", status, i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// validateDashboardCmd asserts that the dashboard command works
|
|
func validateDashboardCmd(ctx context.Context, t *testing.T, profile string) {
|
|
args := []string{"dashboard", "--url", "-p", profile, "--alsologtostderr", "-v=1"}
|
|
ss, err := Start(t, exec.CommandContext(ctx, Target(), args...))
|
|
if err != nil {
|
|
t.Errorf("%s failed: %v", args, err)
|
|
}
|
|
defer func() {
|
|
ss.Stop(t)
|
|
}()
|
|
|
|
start := time.Now()
|
|
s, err := ReadLineWithTimeout(ss.Stdout, 300*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("failed to read url within %s: %v\n", time.Since(start), err)
|
|
}
|
|
|
|
u, err := url.Parse(strings.TrimSpace(s))
|
|
if err != nil {
|
|
t.Fatalf("failed to parse %q: %v", s, err)
|
|
}
|
|
|
|
resp, err := retryablehttp.Get(u.String())
|
|
if err != nil {
|
|
t.Errorf("failed get: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Errorf("Unable to read http response body: %v", err)
|
|
}
|
|
t.Errorf("%s returned status code %d, expected %d.\nbody:\n%s", u, resp.StatusCode, http.StatusOK, body)
|
|
}
|
|
}
|
|
|
|
// validateDNS asserts that all Kubernetes DNS is healthy
|
|
func validateDNS(ctx context.Context, t *testing.T, profile string) {
|
|
rr, err := Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "replace", "--force", "-f", filepath.Join(*testdataDir, "busybox.yaml")))
|
|
if err != nil {
|
|
t.Fatalf("%s failed: %v", rr.Args, err)
|
|
}
|
|
|
|
names, err := PodWait(ctx, t, profile, "default", "integration-test=busybox", 3*time.Minute)
|
|
if err != nil {
|
|
t.Fatalf("wait: %v", err)
|
|
}
|
|
|
|
nslookup := func() error {
|
|
rr, err = Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "exec", names[0], "nslookup", "kubernetes.default"))
|
|
return err
|
|
}
|
|
|
|
// If the coredns process was stable, this retry wouldn't be necessary.
|
|
if err = retry.Expo(nslookup, 1*time.Second, 1*time.Minute); err != nil {
|
|
t.Errorf("nslookup failing: %v", err)
|
|
}
|
|
|
|
want := []byte("10.96.0.1")
|
|
if !bytes.Contains(rr.Stdout.Bytes(), want) {
|
|
t.Errorf("nslookup: got=%q, want=*%q*", rr.Stdout.Bytes(), want)
|
|
}
|
|
}
|
|
|
|
// validateCacheCmd asserts basic "ssh" command functionality
|
|
func validateCacheCmd(ctx context.Context, t *testing.T, profile string) {
|
|
if NoneDriver() {
|
|
t.Skipf("skipping: cache unsupported by none")
|
|
}
|
|
for _, img := range []string{"busybox", "busybox:1.28.4-glibc"} {
|
|
rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "cache", "add", img))
|
|
if err != nil {
|
|
t.Errorf("%s failed: %v", rr.Args, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// validateConfigCmd asserts basic "config" command functionality
|
|
func validateConfigCmd(ctx context.Context, t *testing.T, profile string) {
|
|
tests := []struct {
|
|
args []string
|
|
wantOut string
|
|
wantErr string
|
|
}{
|
|
{[]string{"unset", "cpus"}, "", ""},
|
|
{[]string{"get", "cpus"}, "", "Error: specified key could not be found in config"},
|
|
{[]string{"set", "cpus", "2"}, "! These changes will take effect upon a minikube delete and then a minikube start", ""},
|
|
{[]string{"get", "cpus"}, "2", ""},
|
|
{[]string{"unset", "cpus"}, "", ""},
|
|
{[]string{"get", "cpus"}, "", "Error: specified key could not be found in config"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
args := append([]string{"-p", profile, "config"}, tc.args...)
|
|
rr, err := Run(t, exec.CommandContext(ctx, Target(), args...))
|
|
if err != nil && tc.wantErr == "" {
|
|
t.Errorf("unexpected failure: %s failed: %v", rr.Args, err)
|
|
}
|
|
|
|
got := strings.TrimSpace(rr.Stdout.String())
|
|
if got != tc.wantOut {
|
|
t.Errorf("%s stdout got: %q, want: %q", rr.Command(), got, tc.wantOut)
|
|
}
|
|
got = strings.TrimSpace(rr.Stderr.String())
|
|
if got != tc.wantErr {
|
|
t.Errorf("%s stderr got: %q, want: %q", rr.Command(), got, tc.wantErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// validateLogsCmd asserts basic "logs" command functionality
|
|
func validateLogsCmd(ctx context.Context, t *testing.T, profile string) {
|
|
rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "logs"))
|
|
if err != nil {
|
|
t.Errorf("%s failed: %v", rr.Args, err)
|
|
}
|
|
for _, word := range []string{"Docker", "apiserver", "Linux", "kubelet"} {
|
|
if !strings.Contains(rr.Stdout.String(), word) {
|
|
t.Errorf("minikube logs missing expected word: %q", word)
|
|
}
|
|
}
|
|
}
|
|
|
|
// validateProfileCmd asserts basic "profile" command functionality
|
|
func validateProfileCmd(ctx context.Context, t *testing.T, profile string) {
|
|
rr, err := Run(t, exec.CommandContext(ctx, Target(), "profile", "list"))
|
|
if err != nil {
|
|
t.Errorf("%s failed: %v", rr.Args, err)
|
|
}
|
|
}
|
|
|
|
// validateServiceCmd asserts basic "service" command functionality
|
|
func validateServicesCmd(ctx context.Context, t *testing.T, profile string) {
|
|
rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "service", "list"))
|
|
if err != nil {
|
|
t.Errorf("%s failed: %v", rr.Args, err)
|
|
}
|
|
if !strings.Contains(rr.Stdout.String(), "kubernetes") {
|
|
t.Errorf("service list got %q, wanted *kubernetes*", rr.Stdout.String())
|
|
}
|
|
}
|
|
|
|
// validateSSHCmd asserts basic "ssh" command functionality
|
|
func validateSSHCmd(ctx context.Context, t *testing.T, profile string) {
|
|
if NoneDriver() {
|
|
t.Skipf("skipping: ssh unsupported by none")
|
|
}
|
|
want := "hello\r\n"
|
|
rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "ssh", fmt.Sprintf("echo hello")))
|
|
if err != nil {
|
|
t.Errorf("%s failed: %v", rr.Args, err)
|
|
}
|
|
if rr.Stdout.String() != want {
|
|
t.Errorf("%v = %q, want = %q", rr.Args, rr.Stdout.String(), want)
|
|
}
|
|
}
|
|
|
|
// startHTTPProxy runs a local http proxy and sets the env vars for it.
|
|
func startHTTPProxy(t *testing.T) (*http.Server, error) {
|
|
port, err := freeport.GetFreePort()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "Failed to get an open port")
|
|
}
|
|
|
|
addr := fmt.Sprintf("localhost:%d", port)
|
|
proxy := goproxy.NewProxyHttpServer()
|
|
srv := &http.Server{Addr: addr, Handler: proxy}
|
|
go func(s *http.Server, t *testing.T) {
|
|
if err := s.ListenAndServe(); err != http.ErrServerClosed {
|
|
t.Errorf("Failed to start http server for proxy mock")
|
|
}
|
|
}(srv, t)
|
|
return srv, nil
|
|
}
|