functional_test.go 50.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// +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 (
22
	"bufio"
23 24 25 26 27 28 29 30 31
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"os/exec"
32
	"path"
33
	"path/filepath"
34
	"regexp"
T
tstromberg 已提交
35
	"runtime"
36
	"strings"
37
	"testing"
38 39
	"time"

40 41
	"github.com/google/go-cmp/cmp"

S
Sharif Elgamal 已提交
42
	"k8s.io/minikube/pkg/drivers/kic/oci"
M
Medya Gh 已提交
43
	"k8s.io/minikube/pkg/minikube/config"
44
	"k8s.io/minikube/pkg/minikube/localpath"
45
	"k8s.io/minikube/pkg/minikube/reason"
M
lint  
Medya Gh 已提交
46
	"k8s.io/minikube/pkg/util/retry"
47

48
	"github.com/elazarl/goproxy"
49
	"github.com/hashicorp/go-retryablehttp"
50
	"github.com/otiai10/copy"
51 52 53
	"github.com/phayes/freeport"
	"github.com/pkg/errors"
	"golang.org/x/build/kubernetes/api"
54 55
)

56 57 58
// validateFunc are for subtests that share a single setup
type validateFunc func(context.Context, *testing.T, string)

M
Medya Gh 已提交
59 60 61
// used in validateStartWithProxy and validateSoftStart
var apiPortTest = 8441

62
// TestFunctional are functionality tests which can safely share a profile in parallel
N
Nick Kubala 已提交
63
func TestFunctional(t *testing.T) {
64 65

	profile := UniqueProfileName("functional")
M
Medya Gh 已提交
66
	ctx, cancel := context.WithTimeout(context.Background(), Minutes(40))
67
	defer func() {
68 69 70
		if !*cleanup {
			return
		}
71 72
		p := localSyncTestPath()
		if err := os.Remove(p); err != nil {
73
			t.Logf("unable to remove %q: %v", p, err)
74
		}
75

76
		Cleanup(t, profile, cancel)
77
	}()
78 79 80 81 82 83 84

	// Serial tests
	t.Run("serial", func(t *testing.T) {
		tests := []struct {
			name      string
			validator validateFunc
		}{
85 86
			{"CopySyncFile", setupFileSync},                 // Set file for the file sync test case
			{"StartWithProxy", validateStartWithProxy},      // Set everything else up for success
M
Medya Gh 已提交
87
			{"AuditLog", validateAuditAfterStart},           // check audit feature works
李龙峰 已提交
88
			{"SoftStart", validateSoftStart},                // do a soft start. ensure config didnt change.
89 90 91 92
			{"KubeContext", validateKubeContext},            // Racy: must come immediately after "minikube start"
			{"KubectlGetPods", validateKubectlGetPods},      // Make sure apiserver is up
			{"CacheCmd", validateCacheCmd},                  // Caches images needed for subsequent tests because of proxy
			{"MinikubeKubectlCmd", validateMinikubeKubectl}, // Make sure `minikube kubectl` works
P
Pablo Caderno 已提交
93
			{"MinikubeKubectlCmdDirectly", validateMinikubeKubectlDirectCall},
94
			{"ExtraConfig", validateExtraConfig}, // Ensure extra cmdline config change is saved
95
			{"ComponentHealth", validateComponentHealth},
96 97 98
		}
		for _, tc := range tests {
			tc := tc
99 100 101
			if ctx.Err() == context.DeadlineExceeded {
				t.Fatalf("Unable to run more tests (deadline exceeded)")
			}
102
			t.Run(tc.name, func(t *testing.T) {
M
lint  
Medya Gh 已提交
103
				tc.validator(ctx, t, profile)
104
			})
M
Medya Gh 已提交
105
		}
106
	})
M
Medya Gh 已提交
107

108 109
	defer cleanupUnwantedImages(ctx, t, profile)

110 111 112 113 114 115
	// Parallelized tests
	t.Run("parallel", func(t *testing.T) {
		tests := []struct {
			name      string
			validator validateFunc
		}{
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
			{"ConfigCmd", validateConfigCmd},
			{"DashboardCmd", validateDashboardCmd},
			{"DryRun", validateDryRun},
			{"StatusCmd", validateStatusCmd},
			{"LogsCmd", validateLogsCmd},
			{"MountCmd", validateMountCmd},
			{"ProfileCmd", validateProfileCmd},
			{"ServiceCmd", validateServiceCmd},
			{"AddonsCmd", validateAddonsCmd},
			{"PersistentVolumeClaim", validatePersistentVolumeClaim},
			{"TunnelCmd", validateTunnelCmd},
			{"SSHCmd", validateSSHCmd},
			{"MySQL", validateMySQL},
			{"FileSync", validateFileSync},
			{"CertSync", validateCertSync},
			{"UpdateContextCmd", validateUpdateContextCmd},
			{"DockerEnv", validateDockerEnv},
			{"NodeLabels", validateNodeLabels},
134
			{"LoadImage", validateLoadImage},
135 136 137
		}
		for _, tc := range tests {
			tc := tc
138 139 140 141
			if ctx.Err() == context.DeadlineExceeded {
				t.Fatalf("Unable to run more tests (deadline exceeded)")
			}

142 143 144 145 146
			t.Run(tc.name, func(t *testing.T) {
				MaybeParallel(t)
				tc.validator(ctx, t, profile)
			})
		}
M
Medya Gh 已提交
147
	})
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172

}

func cleanupUnwantedImages(ctx context.Context, t *testing.T, profile string) {
	_, err := exec.LookPath(oci.Docker)
	if err != nil {
		t.Skipf("docker is not installed, cannot delete docker images")
	} else {
		t.Run("delete busybox image", func(t *testing.T) {
			newImage := fmt.Sprintf("busybox:%s", profile)
			rr, err := Run(t, exec.CommandContext(ctx, "docker", "rmi", "-f", newImage))
			if err != nil {
				t.Logf("failed to remove image busybox from docker images. args %q: %v", rr.Command(), err)
			}
		})

		t.Run("delete minikube cached images", func(t *testing.T) {
			img := "minikube-local-cache-test:" + profile
			rr, err := Run(t, exec.CommandContext(ctx, "docker", "rmi", "-f", img))
			if err != nil {
				t.Logf("failed to remove image minikube local cache test images from docker. args %q: %v", rr.Command(), err)
			}
		})
	}

173 174
}

M
Medya Gh 已提交
175 176
// validateNodeLabels checks if minikube cluster is created with correct kubernetes's node label
func validateNodeLabels(ctx context.Context, t *testing.T, profile string) {
177 178
	defer PostMortemLogs(t, profile)

M
Medya Gh 已提交
179
	rr, err := Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "get", "nodes", "--output=go-template", "--template='{{range $k, $v := (index .items 0).metadata.labels}}{{$k}} {{end}}'"))
M
Medya Gh 已提交
180
	if err != nil {
M
Medya Gh 已提交
181
		t.Errorf("failed to 'kubectl get nodes' with args %q: %v", rr.Command(), err)
M
Medya Gh 已提交
182 183 184
	}
	expectedLabels := []string{"minikube.k8s.io/commit", "minikube.k8s.io/version", "minikube.k8s.io/updated_at", "minikube.k8s.io/name"}
	for _, el := range expectedLabels {
M
Medya Gh 已提交
185
		if !strings.Contains(rr.Output(), el) {
M
Medya Gh 已提交
186
			t.Errorf("expected to have label %q in node labels but got : %s", el, rr.Output())
M
Medya Gh 已提交
187 188 189 190
		}
	}
}

191 192
// validateLoadImage makes sure that `minikube load image` works as expected
func validateLoadImage(ctx context.Context, t *testing.T, profile string) {
P
Priya Wadhwa 已提交
193 194 195
	if NoneDriver() {
		t.Skip("load image not available on none driver")
	}
196 197 198
	if GithubActionRunner() && runtime.GOOS == "darwin" {
		t.Skip("skipping on github actions and darwin, as this test requires a running docker daemon")
	}
P
Priya Wadhwa 已提交
199
	defer PostMortemLogs(t, profile)
200 201 202 203
	// pull busybox
	busybox := "busybox:latest"
	rr, err := Run(t, exec.CommandContext(ctx, "docker", "pull", busybox))
	if err != nil {
204
		t.Fatalf("failed to setup test (pull image): %v\n%s", err, rr.Output())
205 206 207 208 209 210
	}

	// tag busybox
	newImage := fmt.Sprintf("busybox:%s", profile)
	rr, err = Run(t, exec.CommandContext(ctx, "docker", "tag", busybox, newImage))
	if err != nil {
211
		t.Fatalf("failed to setup test (tag image) : %v\n%s", err, rr.Output())
212 213 214 215 216 217 218 219 220
	}

	// try to load the new image into minikube
	rr, err = Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "image", "load", newImage))
	if err != nil {
		t.Fatalf("loading image into minikube: %v\n%s", err, rr.Output())
	}

	// make sure the image was correctly loaded
P
Priya Wadhwa 已提交
221 222
	var cmd *exec.Cmd
	if ContainerRuntime() == "docker" {
223
		cmd = exec.CommandContext(ctx, Target(), "ssh", "-p", profile, "--", "docker", "image", "inspect", newImage)
M
Medya Gh 已提交
224 225 226
	} else if ContainerRuntime() == "containerd" {
		// crictl inspecti busybox:test-example
		cmd = exec.CommandContext(ctx, Target(), "ssh", "-p", profile, "--", "sudo", "crictl", "inspecti", newImage)
P
Priya Wadhwa 已提交
227
	} else {
M
Medya Gh 已提交
228
		// crio adds localhost prefix
229 230
		// crictl inspecti localhost/busybox:test-example
		cmd = exec.CommandContext(ctx, Target(), "ssh", "-p", profile, "--", "sudo", "crictl", "inspecti", "localhost/"+newImage)
P
Priya Wadhwa 已提交
231 232
	}
	rr, err = Run(t, cmd)
233 234 235 236 237 238
	if err != nil {
		t.Fatalf("listing images: %v\n%s", err, rr.Output())
	}
	if !strings.Contains(rr.Output(), newImage) {
		t.Fatalf("expected %s to be loaded into minikube but the image is not there", newImage)
	}
239

240 241
}

242
// check functionality of minikube after evaling docker-env
P
Priya Wadhwa 已提交
243
// TODO: Add validatePodmanEnv for crio runtime: #10231
244
func validateDockerEnv(ctx context.Context, t *testing.T, profile string) {
M
skip  
Medya Gh 已提交
245 246 247 248
	if NoneDriver() {
		t.Skipf("none driver does not support docker-env")
	}

249 250 251
	if cr := ContainerRuntime(); cr != "docker" {
		t.Skipf("only validate docker env with docker container runtime, currently testing %s", cr)
	}
252
	defer PostMortemLogs(t, profile)
253
	mctx, cancel := context.WithTimeout(ctx, Seconds(120))
254
	defer cancel()
M
Medya Gh 已提交
255 256
	var rr *RunResult
	var err error
M
Medya Gh 已提交
257
	if runtime.GOOS == "windows" {
M
Medya Gh 已提交
258 259
		c := exec.CommandContext(mctx, "powershell.exe", "-NoProfile", "-NonInteractive", Target()+" -p "+profile+" docker-env | Invoke-Expression ;"+Target()+" status -p "+profile)
		rr, err = Run(t, c)
M
Medya Gh 已提交
260 261 262 263 264
	} else {
		c := exec.CommandContext(mctx, "/bin/bash", "-c", "eval $("+Target()+" -p "+profile+" docker-env) && "+Target()+" status -p "+profile)
		// we should be able to get minikube status with a bash which evaled docker-env
		rr, err = Run(t, c)
	}
M
Medya Gh 已提交
265 266
	if mctx.Err() == context.DeadlineExceeded {
		t.Errorf("failed to run the command by deadline. exceeded timeout. %s", rr.Command())
M
Medya Gh 已提交
267
	}
268
	if err != nil {
M
Medya Gh 已提交
269
		t.Fatalf("failed to do status after eval-ing docker-env. error: %v", err)
270 271
	}
	if !strings.Contains(rr.Output(), "Running") {
M
Medya Gh 已提交
272
		t.Fatalf("expected status output to include 'Running' after eval docker-env but got: *%s*", rr.Output())
273 274
	}

275
	mctx, cancel = context.WithTimeout(ctx, Seconds(60))
276 277
	defer cancel()
	// do a eval $(minikube -p profile docker-env) and check if we are point to docker inside minikube
M
Medya Gh 已提交
278
	if runtime.GOOS == "windows" { // testing docker-env eval in powershell
M
Medya Gh 已提交
279 280
		c := exec.CommandContext(mctx, "powershell.exe", "-NoProfile", "-NonInteractive", Target(), "-p "+profile+" docker-env | Invoke-Expression ; docker images")
		rr, err = Run(t, c)
M
Medya Gh 已提交
281
	} else {
M
Medya Gh 已提交
282
		c := exec.CommandContext(mctx, "/bin/bash", "-c", "eval $("+Target()+" -p "+profile+" docker-env) && docker images")
M
Medya Gh 已提交
283 284 285
		rr, err = Run(t, c)
	}

M
Medya Gh 已提交
286
	if mctx.Err() == context.DeadlineExceeded {
M
try pwd  
Medya Gh 已提交
287
		t.Errorf("failed to run the command in 30 seconds. exceeded 30s timeout. %s", rr.Command())
M
Medya Gh 已提交
288 289
	}

290
	if err != nil {
M
Medya Gh 已提交
291
		t.Fatalf("failed to run minikube docker-env. args %q : %v ", rr.Command(), err)
292 293 294 295
	}

	expectedImgInside := "gcr.io/k8s-minikube/storage-provisioner"
	if !strings.Contains(rr.Output(), expectedImgInside) {
M
Medya Gh 已提交
296
		t.Fatalf("expected 'docker images' to have %q inside minikube. but the output is: *%s*", expectedImgInside, rr.Output())
297 298 299
	}
}

M
lint  
Medya Gh 已提交
300
func validateStartWithProxy(ctx context.Context, t *testing.T, profile string) {
301 302
	defer PostMortemLogs(t, profile)

303 304
	srv, err := startHTTPProxy(t)
	if err != nil {
305
		t.Fatalf("failed to set up the test proxy: %s", err)
306
	}
307 308

	// Use more memory so that we may reliably fit MySQL and nginx
M
Medya Gh 已提交
309
	// changing api server so later in soft start we verify it didn't change
310
	startArgs := append([]string{"start", "-p", profile, "--memory=4000", fmt.Sprintf("--apiserver-port=%d", apiPortTest), "--wait=all"}, StartArgs()...)
311
	c := exec.CommandContext(ctx, Target(), startArgs...)
312 313 314 315 316 317
	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 {
M
Medya Gh 已提交
318
		t.Errorf("failed minikube start. args %q: %v", rr.Command(), err)
319 320 321 322 323 324 325 326 327 328 329
	}

	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)
	}
330

M
Medya Gh 已提交
331 332 333 334 335 336 337 338 339 340
}

func validateAuditAfterStart(ctx context.Context, t *testing.T, profile string) {
	got, err := auditContains(profile)
	if err != nil {
		t.Fatalf("failed to check audit log: %v", err)
	}
	if !got {
		t.Errorf("audit.json does not contain the profile %q", profile)
	}
341 342
}

M
Medya Gh 已提交
343
// validateSoftStart validates that after minikube already started, a "minikube start" should not change the configs.
M
lint  
Medya Gh 已提交
344
func validateSoftStart(ctx context.Context, t *testing.T, profile string) {
345 346
	defer PostMortemLogs(t, profile)

M
Medya Gh 已提交
347
	start := time.Now()
M
Medya Gh 已提交
348
	// the test before this had been start with --apiserver-port=8441
M
lint  
Medya Gh 已提交
349 350
	beforeCfg, err := config.LoadProfile(profile)
	if err != nil {
351
		t.Fatalf("error reading cluster config before soft start: %v", err)
M
lint  
Medya Gh 已提交
352
	}
M
Medya Gh 已提交
353 354
	if beforeCfg.Config.KubernetesConfig.NodePort != apiPortTest {
		t.Errorf("expected cluster config node port before soft start to be %d but got %d", apiPortTest, beforeCfg.Config.KubernetesConfig.NodePort)
M
Medya Gh 已提交
355 356
	}

M
typo  
Medya Gh 已提交
357
	softStartArgs := []string{"start", "-p", profile, "--alsologtostderr", "-v=8"}
M
Medya Gh 已提交
358
	c := exec.CommandContext(ctx, Target(), softStartArgs...)
M
lint  
Medya Gh 已提交
359
	rr, err := Run(t, c)
M
Medya Gh 已提交
360 361 362
	if err != nil {
		t.Errorf("failed to soft start minikube. args %q: %v", rr.Command(), err)
	}
M
Medya Gh 已提交
363
	t.Logf("soft start took %s for %q cluster.", time.Since(start), profile)
M
Medya Gh 已提交
364

M
lint  
Medya Gh 已提交
365 366 367 368 369
	afterCfg, err := config.LoadProfile(profile)
	if err != nil {
		t.Errorf("error reading cluster config after soft start: %v", err)
	}

M
Medya Gh 已提交
370 371
	if afterCfg.Config.KubernetesConfig.NodePort != apiPortTest {
		t.Errorf("expected node port in the config not change after soft start. exepceted node port to be %d but got %d.", apiPortTest, afterCfg.Config.KubernetesConfig.NodePort)
M
Medya Gh 已提交
372 373 374
	}
}

375 376
// validateKubeContext asserts that kubectl is properly configured (race-condition prone!)
func validateKubeContext(ctx context.Context, t *testing.T, profile string) {
377 378
	defer PostMortemLogs(t, profile)

379 380
	rr, err := Run(t, exec.CommandContext(ctx, "kubectl", "config", "current-context"))
	if err != nil {
M
Medya Gh 已提交
381
		t.Errorf("failed to get current-context. args %q : %v", rr.Command(), err)
382 383
	}
	if !strings.Contains(rr.Stdout.String(), profile) {
384
		t.Errorf("expected current-context = %q, but got *%q*", profile, rr.Stdout.String())
385 386 387
	}
}

P
Priya Wadhwa 已提交
388 389
// validateKubectlGetPods asserts that `kubectl get pod -A` returns non-zero content
func validateKubectlGetPods(ctx context.Context, t *testing.T, profile string) {
390 391
	defer PostMortemLogs(t, profile)

392
	rr, err := Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "get", "po", "-A"))
P
Priya Wadhwa 已提交
393
	if err != nil {
M
Medya Gh 已提交
394
		t.Errorf("failed to get kubectl pods: args %q : %v", rr.Command(), err)
P
Priya Wadhwa 已提交
395
	}
396
	if rr.Stderr.String() != "" {
397
		t.Errorf("expected stderr to be empty but got *%q*: args %q", rr.Stderr, rr.Command())
398
	}
T
Thomas Stromberg 已提交
399
	if !strings.Contains(rr.Stdout.String(), "kube-system") {
400
		t.Errorf("expected stdout to include *kube-system* but got *%q*. args: %q", rr.Stdout, rr.Command())
P
Priya Wadhwa 已提交
401 402 403
	}
}

404 405
// validateMinikubeKubectl validates that the `minikube kubectl` command returns content
func validateMinikubeKubectl(ctx context.Context, t *testing.T, profile string) {
406 407
	defer PostMortemLogs(t, profile)

408 409
	// Must set the profile so that it knows what version of Kubernetes to use
	kubectlArgs := []string{"-p", profile, "kubectl", "--", "--context", profile, "get", "pods"}
P
Priya Wadhwa 已提交
410
	rr, err := Run(t, exec.CommandContext(ctx, Target(), kubectlArgs...))
411
	if err != nil {
M
Medya Gh 已提交
412
		t.Fatalf("failed to get pods. args %q: %v", rr.Command(), err)
413 414 415
	}
}

P
Pablo Caderno 已提交
416 417 418 419 420 421 422 423 424 425 426 427
// validateMinikubeKubectlDirectCall validates that calling minikube's kubectl
func validateMinikubeKubectlDirectCall(ctx context.Context, t *testing.T, profile string) {
	defer PostMortemLogs(t, profile)
	dir := filepath.Dir(Target())
	dstfn := filepath.Join(dir, "kubectl")
	err := os.Link(Target(), dstfn)

	if err != nil {
		t.Fatal(err)
	}
	defer os.Remove(dstfn) // clean up

428
	kubectlArgs := []string{"--context", profile, "get", "pods"}
P
Pablo Caderno 已提交
429 430
	rr, err := Run(t, exec.CommandContext(ctx, dstfn, kubectlArgs...))
	if err != nil {
431
		t.Fatalf("failed to run kubectl directly. args %q: %v", rr.Command(), err)
P
Pablo Caderno 已提交
432 433 434
	}
}

435 436 437 438 439
func validateExtraConfig(ctx context.Context, t *testing.T, profile string) {
	defer PostMortemLogs(t, profile)

	start := time.Now()
	// The tests before this already created a profile, starting minikube with different --extra-config cmdline option.
440
	startArgs := []string{"start", "-p", profile, "--extra-config=apiserver.enable-admission-plugins=NamespaceAutoProvision", "--wait=all"}
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
	c := exec.CommandContext(ctx, Target(), startArgs...)
	rr, err := Run(t, c)
	if err != nil {
		t.Errorf("failed to restart minikube. args %q: %v", rr.Command(), err)
	}
	t.Logf("restart took %s for %q cluster.", time.Since(start), profile)

	afterCfg, err := config.LoadProfile(profile)
	if err != nil {
		t.Errorf("error reading cluster config after soft start: %v", err)
	}

	expectedExtraOptions := "apiserver.enable-admission-plugins=NamespaceAutoProvision"

	if !strings.Contains(afterCfg.Config.KubernetesConfig.ExtraOptions.String(), expectedExtraOptions) {
		t.Errorf("expected ExtraOptions to contain %s but got %s", expectedExtraOptions, afterCfg.Config.KubernetesConfig.ExtraOptions.String())
	}
}

I
Ilya Zuyev 已提交
460 461 462
// imageID returns a docker image id for image `image` and current architecture
// 'image' is supposed to be one commonly used in minikube integration tests,
// like k8s 'pause'
463 464 465
func imageID(image string) string {
	ids := map[string]map[string]string{
		"pause": {
466 467
			"amd64": "0184c1613d929",
			"arm64": "3d18732f8686c",
468 469 470 471 472 473 474 475 476 477 478 479
		},
	}

	if imgIds, ok := ids[image]; ok {
		if id, ok := imgIds[runtime.GOARCH]; ok {
			return id
		}
		panic(fmt.Sprintf("unexpected architecture for image %q: %v", image, runtime.GOARCH))
	}
	panic("unexpected image name: " + image)
}

480
// validateComponentHealth asserts that all Kubernetes components are healthy
P
Predrag Rogic 已提交
481
// note: it expects all components to be Ready, so it makes sense to run it close after only those tests that include '--wait=all' start flag (ie, with extra wait)
482
func validateComponentHealth(ctx context.Context, t *testing.T, profile string) {
483 484
	defer PostMortemLogs(t, profile)

485 486 487 488 489 490 491 492 493
	// The ComponentStatus API is deprecated in v1.19, so do the next closest thing.
	found := map[string]bool{
		"etcd":                    false,
		"kube-apiserver":          false,
		"kube-controller-manager": false,
		"kube-scheduler":          false,
	}

	rr, err := Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "get", "po", "-l", "tier=control-plane", "-n", "kube-system", "-o=json"))
494
	if err != nil {
M
Medya Gh 已提交
495
		t.Fatalf("failed to get components. args %q: %v", rr.Command(), err)
496
	}
497
	cs := api.PodList{}
498 499
	d := json.NewDecoder(bytes.NewReader(rr.Stdout.Bytes()))
	if err := d.Decode(&cs); err != nil {
M
Medya Gh 已提交
500
		t.Fatalf("failed to decode kubectl json output: args %q : %v", rr.Command(), err)
501 502 503
	}

	for _, i := range cs.Items {
504
		for _, l := range i.Labels {
505
			if _, ok := found[l]; ok { // skip irrelevant (eg, repeating/redundant '"tier": "control-plane"') labels
506
				found[l] = true
507 508
				t.Logf("%s phase: %s", l, i.Status.Phase)
				if i.Status.Phase != api.PodRunning {
509
					t.Errorf("%s is not Running: %+v", l, i.Status)
510 511 512 513 514 515 516 517 518 519 520
					continue
				}
				for _, c := range i.Status.Conditions {
					if c.Type == api.PodReady {
						if c.Status != api.ConditionTrue {
							t.Errorf("%s is not Ready: %+v", l, i.Status)
						} else {
							t.Logf("%s status: %s", l, c.Type)
						}
						break
					}
521
				}
522 523
			}
		}
524 525 526 527 528
	}

	for k, v := range found {
		if !v {
			t.Errorf("expected component %q was not found", k)
529 530 531 532
		}
	}
}

J
Josh Woodcock 已提交
533
func validateStatusCmd(ctx context.Context, t *testing.T, profile string) {
534
	defer PostMortemLogs(t, profile)
535
	rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "status"))
J
Josh Woodcock 已提交
536
	if err != nil {
M
Medya Gh 已提交
537
		t.Errorf("failed to run minikube status. args %q : %v", rr.Command(), err)
J
Josh Woodcock 已提交
538 539 540
	}

	// Custom format
541
	rr, err = Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "status", "-f", "host:{{.Host}},kublet:{{.Kubelet}},apiserver:{{.APIServer}},kubeconfig:{{.Kubeconfig}}"))
J
Josh Woodcock 已提交
542
	if err != nil {
M
Medya Gh 已提交
543
		t.Errorf("failed to run minikube status with custom format: args %q: %v", rr.Command(), err)
J
Josh Woodcock 已提交
544
	}
545 546
	re := `host:([A-z]+),kublet:([A-z]+),apiserver:([A-z]+),kubeconfig:([A-z]+)`
	match, _ := regexp.MatchString(re, rr.Stdout.String())
J
Josh Woodcock 已提交
547
	if !match {
M
Medya Gh 已提交
548
		t.Errorf("failed to match regex %q for minikube status with custom format. args %q. output: %s", re, rr.Command(), rr.Output())
J
Josh Woodcock 已提交
549 550 551
	}

	// Json output
552
	rr, err = Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "status", "-o", "json"))
J
Josh Woodcock 已提交
553
	if err != nil {
M
Medya Gh 已提交
554
		t.Errorf("failed to run minikube status with json output. args %q : %v", rr.Command(), err)
J
Josh Woodcock 已提交
555 556 557 558
	}
	var jsonObject map[string]interface{}
	err = json.Unmarshal(rr.Stdout.Bytes(), &jsonObject)
	if err != nil {
M
Medya Gh 已提交
559
		t.Errorf("failed to decode json from minikube status. args %q. %v", rr.Command(), err)
J
Josh Woodcock 已提交
560 561
	}
	if _, ok := jsonObject["Host"]; !ok {
M
Medya Gh 已提交
562
		t.Errorf("%q failed: %v. Missing key %s in json object", rr.Command(), err, "Host")
J
Josh Woodcock 已提交
563 564
	}
	if _, ok := jsonObject["Kubelet"]; !ok {
M
Medya Gh 已提交
565
		t.Errorf("%q failed: %v. Missing key %s in json object", rr.Command(), err, "Kubelet")
J
Josh Woodcock 已提交
566 567
	}
	if _, ok := jsonObject["APIServer"]; !ok {
M
Medya Gh 已提交
568
		t.Errorf("%q failed: %v. Missing key %s in json object", rr.Command(), err, "APIServer")
J
Josh Woodcock 已提交
569 570
	}
	if _, ok := jsonObject["Kubeconfig"]; !ok {
M
Medya Gh 已提交
571
		t.Errorf("%q failed: %v. Missing key %s in json object", rr.Command(), err, "Kubeconfig")
J
Josh Woodcock 已提交
572 573 574
	}
}

575 576
// validateDashboardCmd asserts that the dashboard command works
func validateDashboardCmd(ctx context.Context, t *testing.T, profile string) {
577 578
	defer PostMortemLogs(t, profile)

579 580 581
	mctx, cancel := context.WithTimeout(ctx, Seconds(300))
	defer cancel()

582
	args := []string{"dashboard", "--url", "-p", profile, "--alsologtostderr", "-v=1"}
583
	ss, err := Start(t, exec.CommandContext(mctx, Target(), args...))
584
	if err != nil {
585
		t.Errorf("failed to run minikube dashboard. args %q : %v", args, err)
586 587 588 589 590
	}
	defer func() {
		ss.Stop(t)
	}()

591
	s, err := dashboardURL(ss.Stdout)
592
	if err != nil {
T
tstromberg 已提交
593
		if runtime.GOOS == "windows" {
594
			t.Skip(err)
T
tstromberg 已提交
595
		}
596
		t.Fatal(err)
597 598 599 600 601 602 603 604 605
	}

	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 {
606
		t.Fatalf("failed to http get %q: %v\nresponse: %+v", u.String(), err, resp)
607
	}
608

609 610 611
	if resp.StatusCode != http.StatusOK {
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
612
			t.Errorf("failed to read http response body from dashboard %q: %v", u.String(), err)
613 614 615 616
		}
		t.Errorf("%s returned status code %d, expected %d.\nbody:\n%s", u, resp.StatusCode, http.StatusOK, body)
	}
}
M
Medya Gh 已提交
617

618
// dashboardURL gets the dashboard URL from the command stdout.
619
func dashboardURL(b *bufio.Reader) (string, error) {
620
	// match http://127.0.0.1:XXXXX/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/
621
	dashURLRegexp := regexp.MustCompile(`^http:\/\/127\.0\.0\.1:[0-9]{5}\/api\/v1\/namespaces\/kubernetes-dashboard\/services\/http:kubernetes-dashboard:\/proxy\/$`)
622

S
Steven Powell 已提交
623 624 625 626 627
	s := bufio.NewScanner(b)
	for s.Scan() {
		t := s.Text()
		if dashURLRegexp.MatchString(t) {
			return t, nil
628 629
		}
	}
630 631 632
	if err := s.Err(); err != nil {
		return "", fmt.Errorf("failed reading input: %v", err)
	}
633 634 635
	return "", fmt.Errorf("output didn't produce a URL")
}

T
Thomas Stromberg 已提交
636 637
// validateDryRun asserts that the dry-run mode quickly exits with the right code
func validateDryRun(ctx context.Context, t *testing.T, profile string) {
638
	// dry-run mode should always be able to finish quickly (<5s)
M
Medya Gh 已提交
639
	mctx, cancel := context.WithTimeout(ctx, Seconds(5))
T
Thomas Stromberg 已提交
640 641 642
	defer cancel()

	// Too little memory!
643
	startArgs := append([]string{"start", "-p", profile, "--dry-run", "--memory", "250MB", "--alsologtostderr"}, StartArgs()...)
T
Thomas Stromberg 已提交
644 645 646
	c := exec.CommandContext(mctx, Target(), startArgs...)
	rr, err := Run(t, c)

647
	wantCode := reason.ExInsufficientMemory
T
Thomas Stromberg 已提交
648 649 650 651
	if rr.ExitCode != wantCode {
		t.Errorf("dry-run(250MB) exit code = %d, wanted = %d: %v", rr.ExitCode, wantCode, err)
	}

M
Medya Gh 已提交
652
	dctx, cancel := context.WithTimeout(ctx, Seconds(5))
T
Thomas Stromberg 已提交
653
	defer cancel()
654
	startArgs = append([]string{"start", "-p", profile, "--dry-run", "--alsologtostderr", "-v=1"}, StartArgs()...)
T
Thomas Stromberg 已提交
655 656 657 658 659 660 661
	c = exec.CommandContext(dctx, Target(), startArgs...)
	rr, err = Run(t, c)
	if rr.ExitCode != 0 || err != nil {
		t.Errorf("dry-run exit code = %d, wanted = %d: %v", rr.ExitCode, 0, err)
	}
}

M
Medya Gh 已提交
662
// validateCacheCmd tests functionality of cache command (cache add, delete, list)
663
func validateCacheCmd(ctx context.Context, t *testing.T, profile string) {
664 665
	defer PostMortemLogs(t, profile)

666 667 668
	if NoneDriver() {
		t.Skipf("skipping: cache unsupported by none")
	}
669

M
Medya Gh 已提交
670
	t.Run("cache", func(t *testing.T) {
M
Medya Gh 已提交
671
		t.Run("add_remote", func(t *testing.T) {
672
			for _, img := range []string{"k8s.gcr.io/pause:3.1", "k8s.gcr.io/pause:3.3", "k8s.gcr.io/pause:latest"} {
673
				rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "cache", "add", img))
M
Medya Gh 已提交
674
				if err != nil {
M
Medya Gh 已提交
675
					t.Errorf("failed to 'cache add' remote image %q. args %q err %v", img, rr.Command(), err)
M
Medya Gh 已提交
676 677 678
				}
			}
		})
679 680

		t.Run("add_local", func(t *testing.T) {
681 682
			if GithubActionRunner() && runtime.GOOS == "darwin" {
				t.Skipf("skipping this test because Docker can not run in macos on github action free version. https://github.community/t/is-it-possible-to-install-and-configure-docker-on-macos-runner/16981")
S
Sharif Elgamal 已提交
683
			}
684

S
Sharif Elgamal 已提交
685 686 687
			_, err := exec.LookPath(oci.Docker)
			if err != nil {
				t.Skipf("docker is not installed, skipping local image test")
688 689
			}

690 691 692 693 694 695 696 697
			dname, err := ioutil.TempDir("", profile)
			if err != nil {
				t.Fatalf("Cannot create temp dir: %v", err)
			}

			message := []byte("FROM scratch\nADD Dockerfile /x")
			err = ioutil.WriteFile(filepath.Join(dname, "Dockerfile"), message, 0644)
			if err != nil {
M
Medya Gh 已提交
698
				t.Fatalf("unable to write Dockerfile: %v", err)
699 700 701
			}

			img := "minikube-local-cache-test:" + profile
702

T
Thomas Stromberg 已提交
703
			_, err = Run(t, exec.CommandContext(ctx, "docker", "build", "-t", img, dname))
704
			if err != nil {
S
Sharif Elgamal 已提交
705
				t.Skipf("failed to build docker image, skipping local test: %v", err)
706 707
			}

T
Thomas Stromberg 已提交
708
			rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "cache", "add", img))
709
			if err != nil {
M
Medya Gh 已提交
710
				t.Errorf("failed to 'cache add' local image %q. args %q err %v", img, rr.Command(), err)
711 712 713
			}
		})

M
Medya Gh 已提交
714 715
		t.Run("delete_k8s.gcr.io/pause:3.3", func(t *testing.T) {
			rr, err := Run(t, exec.CommandContext(ctx, Target(), "cache", "delete", "k8s.gcr.io/pause:3.3"))
M
Medya Gh 已提交
716
			if err != nil {
M
Medya Gh 已提交
717
				t.Errorf("failed to delete image k8s.gcr.io/pause:3.3 from cache. args %q: %v", rr.Command(), err)
M
Medya Gh 已提交
718 719
			}
		})
M
Medya Gh 已提交
720

M
Medya Gh 已提交
721 722 723
		t.Run("list", func(t *testing.T) {
			rr, err := Run(t, exec.CommandContext(ctx, Target(), "cache", "list"))
			if err != nil {
M
Medya Gh 已提交
724
				t.Errorf("failed to do cache list. args %q: %v", rr.Command(), err)
M
Medya Gh 已提交
725 726
			}
			if !strings.Contains(rr.Output(), "k8s.gcr.io/pause") {
M
Medya Gh 已提交
727
				t.Errorf("expected 'cache list' output to include 'k8s.gcr.io/pause' but got: ***%s***", rr.Output())
M
Medya Gh 已提交
728
			}
M
Medya Gh 已提交
729 730
			if strings.Contains(rr.Output(), "k8s.gcr.io/pause:3.3") {
				t.Errorf("expected 'cache list' output not to include k8s.gcr.io/pause:3.3 but got: ***%s***", rr.Output())
M
Medya Gh 已提交
731 732
			}
		})
M
Medya Gh 已提交
733

M
Medya Ghazizadeh 已提交
734
		t.Run("verify_cache_inside_node", func(t *testing.T) {
M
Medya Gh 已提交
735
			rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "ssh", "sudo", "crictl", "images"))
M
Medya Gh 已提交
736
			if err != nil {
M
Medya Gh 已提交
737
				t.Errorf("failed to get images by %q ssh %v", rr.Command(), err)
M
Medya Gh 已提交
738
			}
739
			pauseID := imageID("pause")
I
Ilya Zuyev 已提交
740
			if !strings.Contains(rr.Output(), pauseID) {
741
				t.Errorf("expected sha for pause:3.3 %q to be in the output but got *%s*", pauseID, rr.Output())
M
Medya Gh 已提交
742 743
			}
		})
M
Medya Gh 已提交
744

M
Medya Ghazizadeh 已提交
745
		t.Run("cache_reload", func(t *testing.T) { // deleting image inside minikube node manually and expecting reload to bring it back
746
			img := "k8s.gcr.io/pause:latest"
M
Medya Gh 已提交
747
			// deleting image inside minikube node manually
748 749 750 751 752 753 754 755 756 757

			var binary string
			switch ContainerRuntime() {
			case "docker":
				binary = "docker"
			case "containerd", "crio":
				binary = "crictl"
			}

			rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "ssh", "sudo", binary, "rmi", img))
M
Medya Gh 已提交
758

M
Medya Gh 已提交
759
			if err != nil {
M
spell  
Medya Gh 已提交
760
				t.Errorf("failed to manually delete image %q : %v", rr.Command(), err)
M
Medya Gh 已提交
761 762 763 764
			}
			// make sure the image is deleted.
			rr, err = Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "ssh", "sudo", "crictl", "inspecti", img))
			if err == nil {
M
Medya Gh 已提交
765
				t.Errorf("expected an error  but got no error. image should not exist. ! cmd: %q", rr.Command())
M
Medya Gh 已提交
766
			}
M
Medya Gh 已提交
767
			// minikube cache reload.
M
Medya Gh 已提交
768 769
			rr, err = Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "cache", "reload"))
			if err != nil {
770
				t.Errorf("expected %q to run successfully but got error: %v", rr.Command(), err)
M
Medya Gh 已提交
771
			}
M
Medya Gh 已提交
772
			// make sure 'cache reload' brought back the manually deleted image.
M
Medya Gh 已提交
773 774
			rr, err = Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "ssh", "sudo", "crictl", "inspecti", img))
			if err != nil {
775
				t.Errorf("expected %q to run successfully but got error: %v", rr.Command(), err)
M
Medya Gh 已提交
776 777 778
			}
		})

M
Medya Gh 已提交
779 780
		// delete will clean up the cached images since they are global and all other tests will load it for no reason
		t.Run("delete", func(t *testing.T) {
781
			for _, img := range []string{"k8s.gcr.io/pause:3.1", "k8s.gcr.io/pause:latest"} {
M
Medya Gh 已提交
782 783 784 785 786 787
				rr, err := Run(t, exec.CommandContext(ctx, Target(), "cache", "delete", img))
				if err != nil {
					t.Errorf("failed to delete %s from cache. args %q: %v", img, rr.Command(), err)
				}
			}
		})
M
Medya Gh 已提交
788
	})
789 790 791 792 793 794 795 796 797 798 799
}

// 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"},
T
Thomas Stromberg 已提交
800
		{[]string{"set", "cpus", "2"}, "", "! These changes will take effect upon a minikube delete and then a minikube start"},
801 802 803 804 805 806 807 808 809
		{[]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 == "" {
M
Medya Gh 已提交
810
			t.Errorf("failed to config minikube. args %q : %v", rr.Command(), err)
811 812 813 814
		}

		got := strings.TrimSpace(rr.Stdout.String())
		if got != tc.wantOut {
815
			t.Errorf("expected config output for %q to be -%q- but got *%q*", rr.Command(), tc.wantOut, got)
816 817 818
		}
		got = strings.TrimSpace(rr.Stderr.String())
		if got != tc.wantErr {
819
			t.Errorf("expected config error for %q to be -%q- but got *%q*", rr.Command(), tc.wantErr, got)
820 821 822 823 824 825 826 827
		}
	}
}

// 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 {
M
Medya Gh 已提交
828
		t.Errorf("%s failed: %v", rr.Command(), err)
829
	}
830
	expectedWords := []string{"apiserver", "Linux", "kubelet", "Audit", "Last Start"}
831 832 833 834 835 836 837 838 839 840
	switch ContainerRuntime() {
	case "docker":
		expectedWords = append(expectedWords, "Docker")
	case "containerd":
		expectedWords = append(expectedWords, "containerd")
	case "crio":
		expectedWords = append(expectedWords, "crio")
	}

	for _, word := range expectedWords {
841
		if !strings.Contains(rr.Stdout.String(), word) {
842
			t.Errorf("expected minikube logs to include word: -%q- but got \n***%s***\n", word, rr.Output())
843 844 845 846
		}
	}
}

J
Josh Woodcock 已提交
847
// validateProfileCmd asserts "profile" command functionality
848
func validateProfileCmd(ctx context.Context, t *testing.T, profile string) {
M
Medya Gh 已提交
849 850 851 852 853
	t.Run("profile_not_create", func(t *testing.T) {
		// Profile command should not create a nonexistent profile
		nonexistentProfile := "lis"
		rr, err := Run(t, exec.CommandContext(ctx, Target(), "profile", nonexistentProfile))
		if err != nil {
M
Medya Gh 已提交
854
			t.Errorf("%s failed: %v", rr.Command(), err)
M
Medya Gh 已提交
855 856 857
		}
		rr, err = Run(t, exec.CommandContext(ctx, Target(), "profile", "list", "--output", "json"))
		if err != nil {
M
Medya Gh 已提交
858
			t.Errorf("%s failed: %v", rr.Command(), err)
M
Medya Gh 已提交
859 860 861 862
		}
		var profileJSON map[string][]map[string]interface{}
		err = json.Unmarshal(rr.Stdout.Bytes(), &profileJSON)
		if err != nil {
M
Medya Gh 已提交
863
			t.Errorf("%s failed: %v", rr.Command(), err)
M
Medya Gh 已提交
864 865 866 867 868 869 870
		}
		for profileK := range profileJSON {
			for _, p := range profileJSON[profileK] {
				var name = p["Name"]
				if name == nonexistentProfile {
					t.Errorf("minikube profile %s should not exist", nonexistentProfile)
				}
871
			}
872
		}
M
Medya Gh 已提交
873
	})
874

M
Medya Gh 已提交
875
	t.Run("profile_list", func(t *testing.T) {
876 877 878 879 880 881 882 883 884 885 886 887
		// helper function to run command then, return target profile line from table output.
		extractrofileListFunc := func(rr *RunResult) string {
			listLines := strings.Split(strings.TrimSpace(rr.Stdout.String()), "\n")
			for i := 3; i < (len(listLines) - 1); i++ {
				profileLine := listLines[i]
				if strings.Contains(profileLine, profile) {
					return profileLine
				}
			}
			return ""
		}

M
Medya Gh 已提交
888
		// List profiles
889
		start := time.Now()
M
lint  
Medya Gh 已提交
890
		rr, err := Run(t, exec.CommandContext(ctx, Target(), "profile", "list"))
891
		elapsed := time.Since(start)
M
Medya Gh 已提交
892
		if err != nil {
M
Medya Gh 已提交
893
			t.Errorf("failed to list profiles: args %q : %v", rr.Command(), err)
M
Medya Gh 已提交
894
		}
895
		t.Logf("Took %q to run %q", elapsed, rr.Command())
J
Josh Woodcock 已提交
896

897 898
		profileLine := extractrofileListFunc(rr)
		if profileLine == "" {
M
Medya Gh 已提交
899
			t.Errorf("expected 'profile list' output to include %q but got *%q*. args: %q", profile, rr.Stdout.String(), rr.Command())
J
Josh Woodcock 已提交
900
		}
901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918

		// List profiles with light option.
		start = time.Now()
		lrr, err := Run(t, exec.CommandContext(ctx, Target(), "profile", "list", "-l"))
		lightElapsed := time.Since(start)
		if err != nil {
			t.Errorf("failed to list profiles: args %q : %v", lrr.Command(), err)
		}
		t.Logf("Took %q to run %q", lightElapsed, lrr.Command())

		profileLine = extractrofileListFunc(lrr)
		if profileLine == "" || !strings.Contains(profileLine, "Skipped") {
			t.Errorf("expected 'profile list' output to include %q with 'Skipped' status but got *%q*. args: %q", profile, rr.Stdout.String(), rr.Command())
		}

		if lightElapsed > 3*time.Second {
			t.Errorf("expected running time of '%q' is less than 3 seconds. Took %q ", lrr.Command(), lightElapsed)
		}
M
Medya Gh 已提交
919 920 921
	})

	t.Run("profile_json_output", func(t *testing.T) {
922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941
		// helper function to run command then, return target profile object from json output.
		extractProfileObjFunc := func(rr *RunResult) *config.Profile {
			var jsonObject map[string][]config.Profile
			err := json.Unmarshal(rr.Stdout.Bytes(), &jsonObject)
			if err != nil {
				t.Errorf("failed to decode json from profile list: args %q: %v", rr.Command(), err)
				return nil
			}

			for _, profileObject := range jsonObject["valid"] {
				if profileObject.Name == profile {
					return &profileObject
				}
			}
			return nil
		}

		start := time.Now()
		rr, err := Run(t, exec.CommandContext(ctx, Target(), "profile", "list", "-o", "json"))
		elapsed := time.Since(start)
M
Medya Gh 已提交
942
		if err != nil {
M
Medya Gh 已提交
943
			t.Errorf("failed to list profiles with json format. args %q: %v", rr.Command(), err)
J
Josh Woodcock 已提交
944
		}
945 946 947 948 949 950 951 952 953 954
		t.Logf("Took %q to run %q", elapsed, rr.Command())

		pr := extractProfileObjFunc(rr)
		if pr == nil {
			t.Errorf("expected the json of 'profile list' to include %q but got *%q*. args: %q", profile, rr.Stdout.String(), rr.Command())
		}

		start = time.Now()
		lrr, err := Run(t, exec.CommandContext(ctx, Target(), "profile", "list", "-o", "json", "--light"))
		lightElapsed := time.Since(start)
M
Medya Gh 已提交
955
		if err != nil {
956
			t.Errorf("failed to list profiles with json format. args %q: %v", lrr.Command(), err)
M
Medya Gh 已提交
957
		}
958 959 960 961 962
		t.Logf("Took %q to run %q", lightElapsed, lrr.Command())

		pr = extractProfileObjFunc(lrr)
		if pr == nil || pr.Status != "Skipped" {
			t.Errorf("expected the json of 'profile list' to include 'Skipped' status for %q but got *%q*. args: %q", profile, lrr.Stdout.String(), lrr.Command())
M
Medya Gh 已提交
963 964
		}

965 966 967
		if lightElapsed > 3*time.Second {
			t.Errorf("expected running time of '%q' is less than 3 seconds. Took %q ", lrr.Command(), lightElapsed)
		}
M
Medya Gh 已提交
968
	})
969 970 971
}

// validateServiceCmd asserts basic "service" command functionality
972
func validateServiceCmd(ctx context.Context, t *testing.T, profile string) {
973 974
	defer PostMortemLogs(t, profile)

975 976 977
	defer func() {
		if t.Failed() {
			t.Logf("service test failed - dumping debug information")
978 979 980
			t.Logf("-----------------------service failure post-mortem--------------------------------")
			ctx, cancel := context.WithTimeout(context.Background(), Minutes(2))
			defer cancel()
981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000
			rr, err := Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "describe", "po", "hello-node"))
			if err != nil {
				t.Logf("%q failed: %v", rr.Command(), err)
			}
			t.Logf("hello-node pod describe:\n%s", rr.Stdout)

			rr, err = Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "logs", "-l", "app=hello-node"))
			if err != nil {
				t.Logf("%q failed: %v", rr.Command(), err)
			}
			t.Logf("hello-node logs:\n%s", rr.Stdout)

			rr, err = Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "describe", "svc", "hello-node"))
			if err != nil {
				t.Logf("%q failed: %v", rr.Command(), err)
			}
			t.Logf("hello-node svc describe:\n%s", rr.Stdout)
		}
	}()

1001 1002 1003
	var rr *RunResult
	var err error
	// k8s.gcr.io/echoserver is not multi-arch
I
Ilya Zuyev 已提交
1004
	if arm64Platform() {
1005
		rr, err = Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "create", "deployment", "hello-node", "--image=k8s.gcr.io/echoserver-arm:1.8"))
I
Ilya Zuyev 已提交
1006
	} else {
1007
		rr, err = Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "create", "deployment", "hello-node", "--image=k8s.gcr.io/echoserver:1.8"))
I
Ilya Zuyev 已提交
1008 1009
	}

1010
	if err != nil {
1011
		t.Fatalf("failed to create hello-node deployment with this command %q: %v.", rr.Command(), err)
1012
	}
1013
	rr, err = Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "expose", "deployment", "hello-node", "--type=NodePort", "--port=8080"))
1014
	if err != nil {
1015
		t.Fatalf("failed to expose hello-node deployment: %q : %v", rr.Command(), err)
1016 1017
	}

1018
	if _, err := PodWait(ctx, t, profile, "default", "app=hello-node", Minutes(10)); err != nil {
1019
		t.Fatalf("failed waiting for hello-node pod: %v", err)
1020 1021
	}

1022
	rr, err = Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "service", "list"))
1023
	if err != nil {
M
Medya Gh 已提交
1024
		t.Errorf("failed to do service list. args %q : %v", rr.Command(), err)
1025
	}
1026
	if !strings.Contains(rr.Stdout.String(), "hello-node") {
1027
		t.Errorf("expected 'service list' to contain *hello-node* but got -%q-", rr.Stdout.String())
1028 1029
	}

1030 1031 1032 1033
	if NeedsPortForward() {
		t.Skipf("test is broken for port-forwarded drivers: https://github.com/kubernetes/minikube/issues/7383")
	}

T
Thomas Stromberg 已提交
1034
	// Test --https --url mode
1035
	rr, err = Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "service", "--namespace=default", "--https", "--url", "hello-node"))
1036
	if err != nil {
M
Medya Gh 已提交
1037
		t.Fatalf("failed to get service url. args %q : %v", rr.Command(), err)
1038 1039
	}
	if rr.Stderr.String() != "" {
M
Medya Gh 已提交
1040
		t.Errorf("expected stderr to be empty but got *%q* . args %q", rr.Stderr, rr.Command())
1041
	}
1042

T
Thomas Stromberg 已提交
1043
	endpoint := strings.TrimSpace(rr.Stdout.String())
1044
	t.Logf("found endpoint: %s", endpoint)
T
Thomas Stromberg 已提交
1045

1046 1047
	u, err := url.Parse(endpoint)
	if err != nil {
1048
		t.Fatalf("failed to parse service url endpoint %q: %v", endpoint, err)
1049 1050
	}
	if u.Scheme != "https" {
1051
		t.Errorf("expected scheme for %s to be 'https' but got %q", endpoint, u.Scheme)
1052 1053 1054
	}

	// Test --format=IP
1055
	rr, err = Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "service", "hello-node", "--url", "--format={{.IP}}"))
1056
	if err != nil {
M
Medya Gh 已提交
1057
		t.Errorf("failed to get service url with custom format. args %q: %v", rr.Command(), err)
1058
	}
1059
	if strings.TrimSpace(rr.Stdout.String()) != u.Hostname() {
M
Medya Gh 已提交
1060
		t.Errorf("expected 'service --format={{.IP}}' output to be -%q- but got *%q* . args %q.", u.Hostname(), rr.Stdout.String(), rr.Command())
1061 1062
	}

1063
	// Test a regular URL
1064
	rr, err = Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "service", "hello-node", "--url"))
1065
	if err != nil {
M
Medya Gh 已提交
1066
		t.Errorf("failed to get service url. args: %q: %v", rr.Command(), err)
1067
	}
1068 1069

	endpoint = strings.TrimSpace(rr.Stdout.String())
1070 1071
	t.Logf("found endpoint for hello-node: %s", endpoint)

1072 1073 1074 1075
	u, err = url.Parse(endpoint)
	if err != nil {
		t.Fatalf("failed to parse %q: %v", endpoint, err)
	}
1076

1077
	if u.Scheme != "http" {
M
lint  
Medya Gh 已提交
1078
		t.Fatalf("expected scheme to be -%q- got scheme: *%q*", "http", u.Scheme)
1079 1080
	}

1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102
	t.Logf("Attempting to fetch %s ...", endpoint)

	fetch := func() error {
		resp, err := http.Get(endpoint)
		if err != nil {
			t.Logf("error fetching %s: %v", endpoint, err)
			return err
		}

		defer resp.Body.Close()

		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			t.Logf("error reading body from %s: %v", endpoint, err)
			return err
		}
		if resp.StatusCode != http.StatusOK {
			t.Logf("%s: unexpected status code %d - body:\n%s", endpoint, resp.StatusCode, body)
		} else {
			t.Logf("%s: success! body:\n%s", endpoint, body)
		}
		return nil
1103
	}
1104 1105 1106

	if err = retry.Expo(fetch, 1*time.Second, Seconds(30)); err != nil {
		t.Errorf("failed to fetch %s: %v", endpoint, err)
1107 1108 1109
	}
}

1110 1111
// validateAddonsCmd asserts basic "addon" command functionality
func validateAddonsCmd(ctx context.Context, t *testing.T, profile string) {
1112 1113
	defer PostMortemLogs(t, profile)

M
Medya Gh 已提交
1114
	// Table output
1115 1116
	rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "addons", "list"))
	if err != nil {
M
Medya Gh 已提交
1117
		t.Errorf("failed to do addon list: args %q : %v", rr.Command(), err)
1118
	}
M
Medya Gh 已提交
1119 1120
	for _, a := range []string{"dashboard", "ingress", "ingress-dns"} {
		if !strings.Contains(rr.Output(), a) {
M
Medya Gh 已提交
1121
			t.Errorf("expected 'addon list' output to include -%q- but got *%s*", a, rr.Output())
1122 1123
		}
	}
J
Josh Woodcock 已提交
1124 1125 1126 1127

	// Json output
	rr, err = Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "addons", "list", "-o", "json"))
	if err != nil {
M
Medya Gh 已提交
1128
		t.Errorf("failed to do addon list with json output. args %q: %v", rr.Command(), err)
J
Josh Woodcock 已提交
1129 1130 1131 1132
	}
	var jsonObject map[string]interface{}
	err = json.Unmarshal(rr.Stdout.Bytes(), &jsonObject)
	if err != nil {
1133
		t.Errorf("failed to decode addon list json output : %v", err)
J
Josh Woodcock 已提交
1134
	}
1135 1136
}

1137 1138
// validateSSHCmd asserts basic "ssh" command functionality
func validateSSHCmd(ctx context.Context, t *testing.T, profile string) {
1139
	defer PostMortemLogs(t, profile)
1140 1141 1142
	if NoneDriver() {
		t.Skipf("skipping: ssh unsupported by none")
	}
M
Medya Gh 已提交
1143 1144
	mctx, cancel := context.WithTimeout(ctx, Minutes(1))
	defer cancel()
M
Medya Gh 已提交
1145

S
Sharif Elgamal 已提交
1146
	want := "hello"
M
Medya Gh 已提交
1147

M
Medya Gh 已提交
1148 1149
	rr, err := Run(t, exec.CommandContext(mctx, Target(), "-p", profile, "ssh", "echo hello"))
	if mctx.Err() == context.DeadlineExceeded {
M
try pwd  
Medya Gh 已提交
1150 1151 1152 1153 1154
		t.Errorf("failed to run command by deadline. exceeded timeout : %s", rr.Command())
	}
	if err != nil {
		t.Errorf("failed to run an ssh command. args %q : %v", rr.Command(), err)
	}
1155
	// trailing whitespace differs between native and external SSH clients, so let's trim it and call it a day
S
Sharif Elgamal 已提交
1156
	if strings.TrimSpace(rr.Stdout.String()) != want {
M
try pwd  
Medya Gh 已提交
1157 1158 1159
		t.Errorf("expected minikube ssh command output to be -%q- but got *%q*. args %q", want, rr.Stdout.String(), rr.Command())
	}

M
Medya Gh 已提交
1160 1161
	// testing hostname as well because testing something like "minikube ssh echo" could be confusing
	// because it  is not clear if echo was run inside minikube on the powershell
M
Medya Gh 已提交
1162
	// so better to test something inside minikube, that is meaningful per profile
M
Medya Gh 已提交
1163
	// in this case /etc/hostname is same as the profile name
1164
	want = profile
M
Medya Gh 已提交
1165
	rr, err = Run(t, exec.CommandContext(mctx, Target(), "-p", profile, "ssh", "cat /etc/hostname"))
M
Medya Gh 已提交
1166
	if mctx.Err() == context.DeadlineExceeded {
M
try pwd  
Medya Gh 已提交
1167
		t.Errorf("failed to run command by deadline. exceeded timeout : %s", rr.Command())
M
Medya Gh 已提交
1168 1169
	}

1170
	if err != nil {
M
Medya Gh 已提交
1171
		t.Errorf("failed to run an ssh command. args %q : %v", rr.Command(), err)
1172
	}
S
Sharif Elgamal 已提交
1173
	// trailing whitespace differs between native and external SSH clients, so let's trim it and call it a day
S
Sharif Elgamal 已提交
1174
	if strings.TrimSpace(rr.Stdout.String()) != want {
M
Medya Gh 已提交
1175
		t.Errorf("expected minikube ssh command output to be -%q- but got *%q*. args %q", want, rr.Stdout.String(), rr.Command())
1176 1177 1178
	}
}

1179 1180
// validateMySQL validates a minimalist MySQL deployment
func validateMySQL(ctx context.Context, t *testing.T, profile string) {
I
Ilya Zuyev 已提交
1181
	if arm64Platform() {
1182
		t.Skip("arm64 is not supported by mysql. Skip the test. See https://github.com/kubernetes/minikube/issues/10144")
I
Ilya Zuyev 已提交
1183 1184
	}

1185 1186
	defer PostMortemLogs(t, profile)

1187 1188
	rr, err := Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "replace", "--force", "-f", filepath.Join(*testdataDir, "mysql.yaml")))
	if err != nil {
M
Medya Gh 已提交
1189
		t.Fatalf("failed to kubectl replace mysql: args %q failed: %v", rr.Command(), err)
1190 1191
	}

1192
	names, err := PodWait(ctx, t, profile, "default", "app=mysql", Minutes(10))
1193
	if err != nil {
1194
		t.Fatalf("failed waiting for mysql pod: %v", err)
1195 1196
	}

1197 1198 1199 1200
	// Retry, as mysqld first comes up without users configured. Scan for names in case of a reschedule.
	mysql := func() error {
		rr, err = Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "exec", names[0], "--", "mysql", "-ppassword", "-e", "show databases;"))
		return err
1201
	}
M
Medya Gh 已提交
1202
	if err = retry.Expo(mysql, 1*time.Second, Minutes(5)); err != nil {
1203
		t.Errorf("failed to exec 'mysql -ppassword -e show databases;': %v", err)
1204 1205 1206
	}
}

1207 1208 1209 1210 1211 1212 1213 1214 1215 1216
// vmSyncTestPath is where the test file will be synced into the VM
func vmSyncTestPath() string {
	return fmt.Sprintf("/etc/test/nested/copy/%d/hosts", os.Getpid())
}

// localSyncTestPath is where the test file will be synced into the VM
func localSyncTestPath() string {
	return filepath.Join(localpath.MiniPath(), "/files", vmSyncTestPath())
}

1217 1218 1219 1220 1221 1222 1223 1224 1225 1226
// testCert is name of the test certificate installed
func testCert() string {
	return fmt.Sprintf("%d.pem", os.Getpid())
}

// localTestCertPath is where the test file will be synced into the VM
func localTestCertPath() string {
	return filepath.Join(localpath.MiniPath(), "/certs", testCert())
}

1227 1228 1229 1230 1231
// localEmptyCertPath is where the test file will be synced into the VM
func localEmptyCertPath() string {
	return filepath.Join(localpath.MiniPath(), "/certs", fmt.Sprintf("%d_empty.pem", os.Getpid()))
}

1232 1233
// Copy extra file into minikube home folder for file sync test
func setupFileSync(ctx context.Context, t *testing.T, profile string) {
1234 1235
	p := localSyncTestPath()
	t.Logf("local sync path: %s", p)
1236 1237
	syncFile := filepath.Join(*testdataDir, "sync.test")
	err := copy.Copy(syncFile, p)
1238
	if err != nil {
1239
		t.Fatalf("failed to copy testdata/sync.test: %v", err)
1240
	}
1241

1242
	testPem := filepath.Join(*testdataDir, "minikube_test.pem")
1243

1244 1245 1246
	// Write to a temp file for an atomic write
	tmpPem := localTestCertPath() + ".pem"
	if err := copy.Copy(testPem, tmpPem); err != nil {
1247 1248 1249
		t.Fatalf("failed to copy %s: %v", testPem, err)
	}

1250 1251 1252 1253
	if err := os.Rename(tmpPem, localTestCertPath()); err != nil {
		t.Fatalf("failed to rename %s: %v", tmpPem, err)
	}

1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270
	want, err := os.Stat(testPem)
	if err != nil {
		t.Fatalf("stat failed: %v", err)
	}

	got, err := os.Stat(localTestCertPath())
	if err != nil {
		t.Fatalf("stat failed: %v", err)
	}

	if want.Size() != got.Size() {
		t.Errorf("%s size=%d, want %d", localTestCertPath(), got.Size(), want.Size())
	}

	// Create an empty file just to mess with people
	if _, err := os.Create(localEmptyCertPath()); err != nil {
		t.Fatalf("create failed: %v", err)
1271
	}
1272 1273 1274 1275
}

// validateFileSync to check existence of the test file
func validateFileSync(ctx context.Context, t *testing.T, profile string) {
1276 1277
	defer PostMortemLogs(t, profile)

1278 1279 1280
	if NoneDriver() {
		t.Skipf("skipping: ssh unsupported by none")
	}
1281 1282 1283

	vp := vmSyncTestPath()
	t.Logf("Checking for existence of %s within VM", vp)
1284
	rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "ssh", fmt.Sprintf("sudo cat %s", vp)))
1285
	if err != nil {
M
Medya Gh 已提交
1286
		t.Errorf("%s failed: %v", rr.Command(), err)
1287
	}
1288 1289
	got := rr.Stdout.String()
	t.Logf("file sync test content: %s", got)
1290

1291 1292
	syncFile := filepath.Join(*testdataDir, "sync.test")
	expected, err := ioutil.ReadFile(syncFile)
1293
	if err != nil {
1294
		t.Errorf("failed to read test file 'testdata/sync.test' : %v", err)
1295 1296
	}

1297
	if diff := cmp.Diff(string(expected), got); diff != "" {
1298 1299 1300 1301
		t.Errorf("/etc/sync.test content mismatch (-want +got):\n%s", diff)
	}
}

1302 1303
// validateCertSync to check existence of the test certificate
func validateCertSync(ctx context.Context, t *testing.T, profile string) {
1304 1305
	defer PostMortemLogs(t, profile)

1306 1307 1308 1309
	if NoneDriver() {
		t.Skipf("skipping: ssh unsupported by none")
	}

1310 1311
	testPem := filepath.Join(*testdataDir, "minikube_test.pem")
	want, err := ioutil.ReadFile(testPem)
1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324
	if err != nil {
		t.Errorf("test file not found: %v", err)
	}

	// Check both the installed & reference certs (they should be symlinked)
	paths := []string{
		path.Join("/etc/ssl/certs", testCert()),
		path.Join("/usr/share/ca-certificates", testCert()),
		// hashed path generated by: 'openssl x509 -hash -noout -in testCert()'
		"/etc/ssl/certs/51391683.0",
	}
	for _, vp := range paths {
		t.Logf("Checking for existence of %s within VM", vp)
1325
		rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "ssh", fmt.Sprintf("sudo cat %s", vp)))
1326
		if err != nil {
M
Medya Gh 已提交
1327
			t.Errorf("failed to check existence of %q inside minikube. args %q: %v", vp, rr.Command(), err)
1328 1329 1330 1331 1332
		}

		// Strip carriage returned by ssh
		got := strings.Replace(rr.Stdout.String(), "\r", "", -1)
		if diff := cmp.Diff(string(want), got); diff != "" {
1333
			t.Errorf("failed verify pem file. minikube_test.pem -> %s mismatch (-want +got):\n%s", vp, diff)
1334 1335 1336 1337
		}
	}
}

1338 1339
// validateUpdateContextCmd asserts basic "update-context" command functionality
func validateUpdateContextCmd(ctx context.Context, t *testing.T, profile string) {
1340 1341
	defer PostMortemLogs(t, profile)

K
Kazuki Suda 已提交
1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388
	tests := []struct {
		name       string
		kubeconfig []byte
		want       []byte
	}{
		{
			name:       "no changes",
			kubeconfig: nil,
			want:       []byte("No changes"),
		},
		{
			name: "no minikube cluster",
			kubeconfig: []byte(`
apiVersion: v1
clusters:
- cluster:
    certificate-authority: /home/la-croix/apiserver.crt
    server: 192.168.1.1:8080
  name: la-croix
contexts:
- context:
    cluster: la-croix
    user: la-croix
  name: la-croix
current-context: la-croix
kind: Config
preferences: {}
users:
- name: la-croix
  user:
    client-certificate: /home/la-croix/apiserver.crt
    client-key: /home/la-croix/apiserver.key
`),
			want: []byte("context has been updated"),
		},
		{
			name: "no clusters",
			kubeconfig: []byte(`
apiVersion: v1
clusters:
contexts:
kind: Config
preferences: {}
users:
`),
			want: []byte("context has been updated"),
		},
1389 1390
	}

K
Kazuki Suda 已提交
1391 1392
	for _, tc := range tests {
		tc := tc
1393 1394 1395 1396 1397

		if ctx.Err() == context.DeadlineExceeded {
			t.Fatalf("Unable to run more tests (deadline exceeded)")
		}

K
Kazuki Suda 已提交
1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			c := exec.CommandContext(ctx, Target(), "-p", profile, "update-context", "--alsologtostderr", "-v=2")
			if tc.kubeconfig != nil {
				tf, err := ioutil.TempFile("", "kubeconfig")
				if err != nil {
					t.Fatal(err)
				}

				if err := ioutil.WriteFile(tf.Name(), tc.kubeconfig, 0644); err != nil {
					t.Fatal(err)
				}

				t.Cleanup(func() {
					os.Remove(tf.Name())
				})

				c.Env = append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", tf.Name()))
			}

			rr, err := Run(t, c)
			if err != nil {
				t.Errorf("failed to run minikube update-context: args %q: %v", rr.Command(), err)
			}

			if !bytes.Contains(rr.Stdout.Bytes(), tc.want) {
				t.Errorf("update-context: got=%q, want=*%q*", rr.Stdout.Bytes(), tc.want)
			}
		})
1427 1428 1429
	}
}

1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445
// 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
1446
}