github.com/GoogleContainerTools/skaffold/v2@v2.13.2/integration/dev_test.go (about) 1 /* 2 Copyright 2019 The Skaffold Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package integration 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "net/http" 24 "os" 25 "runtime" 26 "strings" 27 "syscall" 28 "testing" 29 "time" 30 31 appsv1 "k8s.io/api/apps/v1" 32 "k8s.io/apimachinery/pkg/util/wait" 33 "k8s.io/client-go/tools/clientcmd" 34 35 "github.com/GoogleContainerTools/skaffold/v2/integration/skaffold" 36 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/config" 37 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/constants" 38 event "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/event/v2" 39 "github.com/GoogleContainerTools/skaffold/v2/proto/v1" 40 V2proto "github.com/GoogleContainerTools/skaffold/v2/proto/v2" 41 "github.com/GoogleContainerTools/skaffold/v2/testutil" 42 ) 43 44 func TestDevNotification(t *testing.T) { 45 tests := []struct { 46 description string 47 trigger string 48 }{ 49 { 50 description: "dev with polling trigger", 51 trigger: "polling", 52 }, 53 { 54 description: "dev with notify trigger", 55 trigger: "notify", 56 }, 57 } 58 for _, test := range tests { 59 t.Run(test.description, func(t *testing.T) { 60 MarkIntegrationTest(t, CanRunWithoutGcp) 61 Run(t, "testdata/dev", "sh", "-c", "echo foo > foo") 62 defer Run(t, "testdata/dev", "rm", "foo") 63 64 // Run skaffold build first to fail quickly on a build failure 65 skaffold.Build().InDir("testdata/dev").RunOrFail(t) 66 67 ns, client := SetupNamespace(t) 68 69 rpcAddr := randomPort() 70 skaffold.Dev("--rpc-port", rpcAddr, "--trigger", test.trigger).InDir("testdata/dev").InNs(ns.Name).RunBackground(t) 71 72 dep := client.GetDeployment(testDev) 73 74 _, entries := v2apiEvents(t, rpcAddr) 75 76 // Wait for the first devloop to register target files to the monitor before running command to change target files 77 failNowIfError(t, waitForV2Event(100*time.Second, entries, func(e *V2proto.Event) bool { 78 taskEvent, ok := e.EventType.(*V2proto.Event_TaskEvent) 79 return ok && taskEvent.TaskEvent.Task == string(constants.DevLoop) && taskEvent.TaskEvent.Status == event.Succeeded 80 })) 81 82 // Make a change to foo so that dev is forced to delete the Deployment and redeploy 83 Run(t, "testdata/dev", "sh", "-c", "echo bar > foo") 84 85 // Make sure the old Deployment and the new Deployment are different 86 err := wait.PollImmediate(time.Millisecond*500, 1*time.Minute, func() (bool, error) { 87 newDep := client.GetDeployment(testDev) 88 t.Logf("old gen: %d, new gen: %d", dep.GetGeneration(), newDep.GetGeneration()) 89 return dep.GetGeneration() != newDep.GetGeneration(), nil 90 }) 91 failNowIfError(t, err) 92 }) 93 } 94 } 95 96 func TestDevGracefulCancel(t *testing.T) { 97 if runtime.GOOS == "windows" { 98 t.Skip("graceful cancel doesn't work on windows") 99 } 100 101 tests := []struct { 102 name string 103 dir string 104 pods []string 105 deployments []string 106 }{ 107 { 108 name: "getting-started", 109 dir: "examples/getting-started", 110 pods: []string{"getting-started"}, 111 }, 112 { 113 name: "multi-config-microservices", 114 dir: "examples/multi-config-microservices", 115 deployments: []string{"leeroy-app", "leeroy-web"}, 116 }, 117 { 118 name: "multiple deployers", 119 dir: "testdata/deploy-multiple", 120 pods: []string{"deploy-kubectl", "deploy-kustomize"}, 121 }, 122 } 123 124 for _, test := range tests { 125 t.Run(test.name, func(t *testing.T) { 126 MarkIntegrationTest(t, CanRunWithoutGcp) 127 128 ns, client := SetupNamespace(t) 129 p, _ := skaffold.Dev("-vtrace").InDir(test.dir).InNs(ns.Name).StartWithProcess(t) 130 client.WaitForPodsReady(test.pods...) 131 client.WaitForDeploymentsToStabilize(test.deployments...) 132 133 defer func() { 134 state, _ := p.Wait() 135 136 // We can't `recover()` from a remotely panicked process, but we can check exit code instead. 137 // Exit code 2 means the process panicked. 138 // https://github.com/golang/go/issues/24284 139 if state.ExitCode() == 2 { 140 t.Fail() 141 } 142 }() 143 144 // once deployments are stable, send a SIGINT and make sure things cleanup correctly 145 p.Signal(syscall.SIGINT) 146 }) 147 } 148 } 149 150 func TestDevCancelWithDockerDeployer(t *testing.T) { 151 if runtime.GOOS == "windows" { 152 t.Skip("graceful cancel doesn't work on windows") 153 } 154 155 tests := []struct { 156 description string 157 dir string 158 containers []string 159 }{ 160 { 161 description: "interrupt dev loop in Docker deployer", 162 dir: "testdata/docker-deploy", 163 containers: []string{"ernie", "bert"}, 164 }, 165 } 166 167 for _, test := range tests { 168 t.Run(test.description, func(t *testing.T) { 169 MarkIntegrationTest(t, CanRunWithoutGcp) 170 p, err := skaffold.Dev().InDir(test.dir).StartWithProcess(t) 171 if err != nil { 172 t.Fatalf("error starting skaffold dev process") 173 } 174 175 if err = waitForContainersRunning(t, test.containers...); err != nil { 176 t.Fatalf("failed waiting for containers: %v", err) 177 } 178 179 p.Signal(syscall.SIGINT) 180 181 state, _ := p.Wait() 182 183 if state.ExitCode() != 0 { 184 t.Fail() 185 } 186 }) 187 } 188 } 189 190 func TestDevAPIBuildTrigger(t *testing.T) { 191 MarkIntegrationTest(t, CanRunWithoutGcp) 192 193 Run(t, "testdata/dev", "sh", "-c", "echo foo > foo") 194 defer Run(t, "testdata/dev", "rm", "foo") 195 196 // Run skaffold build first to fail quickly on a build failure 197 skaffold.Build().InDir("testdata/dev").RunOrFail(t) 198 199 ns, _ := SetupNamespace(t) 200 201 rpcAddr := randomPort() 202 skaffold.Dev("--auto-build=false", "--auto-sync=false", "--auto-deploy=false", "--rpc-port", rpcAddr, "--cache-artifacts=false").InDir("testdata/dev").InNs(ns.Name).RunBackground(t) 203 204 rpcClient, entries := apiEvents(t, rpcAddr) 205 206 // Wait for the first devloop to register target files to the monitor before running command to change target files 207 failNowIfError(t, waitForEvent(90*time.Second, entries, func(e *proto.LogEntry) bool { 208 dle, ok := e.Event.EventType.(*proto.Event_DevLoopEvent) 209 return ok && dle.DevLoopEvent.Status == event.Succeeded 210 })) 211 212 // Make a change to foo 213 Run(t, "testdata/dev", "sh", "-c", "echo bar > foo") 214 215 // Issue a build trigger 216 rpcClient.Execute(context.Background(), &proto.UserIntentRequest{ 217 Intent: &proto.Intent{ 218 Build: true, 219 }, 220 }) 221 222 // Ensure we see a build triggered in the event log 223 err := waitForEvent(2*time.Minute, entries, func(e *proto.LogEntry) bool { 224 return e.GetEvent().GetBuildEvent().GetArtifact() == testDev 225 }) 226 failNowIfError(t, err) 227 } 228 229 func TestDevApiDeployTrigger(t *testing.T) { 230 MarkIntegrationTest(t, CanRunWithoutGcp) 231 232 Run(t, "testdata/dev", "sh", "-c", "echo foo > foo") 233 defer Run(t, "testdata/dev", "rm", "foo") 234 235 // Run skaffold build first to fail quickly on a build failure 236 skaffold.Build().InDir("testdata/dev").RunOrFail(t) 237 238 ns, client := SetupNamespace(t) 239 240 rpcAddr := randomPort() 241 skaffold.Dev("--auto-deploy=false", "--rpc-port", rpcAddr, "--cache-artifacts=false").InDir("testdata/dev").InNs(ns.Name).RunBackground(t) 242 243 rpcClient, entries := apiEvents(t, rpcAddr) 244 dep := client.GetDeployment(testDev) 245 246 // Wait for the first devloop to register target files to the monitor before running command to change target files 247 failNowIfError(t, waitForEvent(90*time.Second, entries, func(e *proto.LogEntry) bool { 248 dle, ok := e.Event.EventType.(*proto.Event_DevLoopEvent) 249 return ok && dle.DevLoopEvent.Status == event.Succeeded 250 })) 251 252 // Make a change to foo 253 Run(t, "testdata/dev", "sh", "-c", "echo bar > foo") 254 255 // Issue a deploy trigger 256 rpcClient.Execute(context.Background(), &proto.UserIntentRequest{ 257 Intent: &proto.Intent{ 258 Deploy: true, 259 }, 260 }) 261 262 verifyDeployment(t, entries, client, dep) 263 } 264 265 func TestDevAPIAutoTriggers(t *testing.T) { 266 MarkIntegrationTest(t, CanRunWithoutGcp) 267 268 Run(t, "testdata/dev", "sh", "-c", "echo foo > foo") 269 defer Run(t, "testdata/dev", "rm", "foo") 270 271 // Run skaffold build first to fail quickly on a build failure 272 skaffold.Build().InDir("testdata/dev").RunOrFail(t) 273 274 ns, client := SetupNamespace(t) 275 276 rpcAddr := randomPort() 277 skaffold.Dev("--auto-build=false", "--auto-sync=false", "--auto-deploy=false", "--rpc-port", rpcAddr, "--cache-artifacts=false").InDir("testdata/dev").InNs(ns.Name).RunBackground(t) 278 279 rpcClient, entries := apiEvents(t, rpcAddr) 280 dep := client.GetDeployment(testDev) 281 282 // Wait for the first devloop to register target files to the monitor before running command to change target files 283 failNowIfError(t, waitForEvent(90*time.Second, entries, func(e *proto.LogEntry) bool { 284 dle, ok := e.Event.EventType.(*proto.Event_DevLoopEvent) 285 return ok && dle.DevLoopEvent.Status == event.Succeeded 286 })) 287 288 // Make a change to foo 289 Run(t, "testdata/dev", "sh", "-c", "echo bar > foo") 290 291 // Enable auto build 292 rpcClient.AutoBuild(context.Background(), &proto.TriggerRequest{ 293 State: &proto.TriggerState{ 294 Val: &proto.TriggerState_Enabled{ 295 Enabled: true, 296 }, 297 }, 298 }) 299 // Ensure we see a build triggered in the event log 300 err := waitForEvent(2*time.Minute, entries, func(e *proto.LogEntry) bool { 301 return e.GetEvent().GetBuildEvent().GetArtifact() == testDev 302 }) 303 failNowIfError(t, err) 304 305 rpcClient.AutoDeploy(context.Background(), &proto.TriggerRequest{ 306 State: &proto.TriggerState{ 307 Val: &proto.TriggerState_Enabled{ 308 Enabled: true, 309 }, 310 }, 311 }) 312 verifyDeployment(t, entries, client, dep) 313 } 314 315 func verifyDeployment(t *testing.T, entries chan *proto.LogEntry, client *NSKubernetesClient, dep *appsv1.Deployment) { 316 // Ensure we see a deploy triggered in the event log 317 err := waitForEvent(2*time.Minute, entries, func(e *proto.LogEntry) bool { 318 return e.GetEvent().GetDeployEvent().GetStatus() == InProgress 319 }) 320 failNowIfError(t, err) 321 322 // Make sure the old Deployment and the new Deployment are different 323 err = wait.Poll(5*time.Second, 3*time.Minute, func() (bool, error) { 324 newDep := client.GetDeployment(testDev) 325 t.Logf("old gen: %d, new gen: %d", dep.GetGeneration(), newDep.GetGeneration()) 326 return dep.GetGeneration() != newDep.GetGeneration(), nil 327 }) 328 failNowIfError(t, err) 329 } 330 331 func TestDevPortForward(t *testing.T) { 332 tests := []struct { 333 name string 334 dir string 335 }{ 336 { 337 name: "microservices", 338 dir: "examples/microservices"}, 339 { 340 name: "multi-config-microservices", 341 dir: "examples/multi-config-microservices"}, 342 } 343 for _, test := range tests { 344 t.Run(test.name, func(t *testing.T) { 345 MarkIntegrationTest(t, CanRunWithoutGcp) 346 // Run skaffold build first to fail quickly on a build failure 347 skaffold.Build().InDir(test.dir).RunOrFail(t) 348 349 ns, _ := SetupNamespace(t) 350 351 rpcAddr := randomPort() 352 skaffold.Dev("--status-check=false", "--port-forward", "--rpc-port", rpcAddr).InDir(test.dir).InNs(ns.Name).RunBackground(t) 353 354 _, entries := apiEvents(t, rpcAddr) 355 356 waitForPortForwardEvent(t, entries, "leeroy-app", "service", ns.Name, "leeroooooy app!!\n") 357 358 original, perms, fErr := replaceInFile("leeroooooy app!!", "test string", fmt.Sprintf("%s/leeroy-app/app.go", test.dir)) 359 failNowIfError(t, fErr) 360 defer func() { 361 if original != nil { 362 os.WriteFile(fmt.Sprintf("%s/leeroy-app/app.go", test.dir), original, perms) 363 } 364 }() 365 366 waitForPortForwardEvent(t, entries, "leeroy-app", "service", ns.Name, "test string\n") 367 }) 368 } 369 } 370 371 func TestDevDeletePreviousBuiltImages(t *testing.T) { 372 tests := []struct { 373 name string 374 dir string 375 }{ 376 { 377 name: "microservices", 378 dir: "examples/microservices"}, 379 } 380 for _, test := range tests { 381 t.Run(test.name, func(t *testing.T) { 382 MarkIntegrationTest(t, CanRunWithoutGcp) 383 // Run skaffold build first to fail quickly on a build failure 384 skaffold.Build().InDir(test.dir).RunOrFail(t) 385 386 ns, k8sClient := SetupNamespace(t) 387 388 rpcAddr := randomPort() 389 skaffold.Dev("--status-check=false", "--port-forward", "--rpc-port", rpcAddr).InDir(test.dir).InNs(ns.Name).RunBackground(t) 390 391 _, entries := apiEvents(t, rpcAddr) 392 393 waitForPortForwardEvent(t, entries, "leeroy-app", "service", ns.Name, "leeroooooy app!!\n") 394 deployment := k8sClient.GetDeployment("leeroy-app") 395 image := deployment.Spec.Template.Spec.Containers[0].Image 396 397 original, perms, fErr := replaceInFile("leeroooooy app!!", "test string", fmt.Sprintf("%s/leeroy-app/app.go", test.dir)) 398 failNowIfError(t, fErr) 399 defer func() { 400 if original != nil { 401 os.WriteFile(fmt.Sprintf("%s/leeroy-app/app.go", test.dir), original, perms) 402 } 403 }() 404 405 waitForPortForwardEvent(t, entries, "leeroy-app", "service", ns.Name, "test string\n") 406 client := SetupDockerClient(t) 407 ctx := context.TODO() 408 wait.Poll(3*time.Second, time.Minute*2, func() (done bool, err error) { 409 return !client.ImageExists(ctx, image), nil 410 }) 411 }) 412 } 413 } 414 415 func TestDevPortForwardDefaultNamespace(t *testing.T) { 416 MarkIntegrationTest(t, CanRunWithoutGcp) 417 418 // Run skaffold build first to fail quickly on a build failure 419 skaffold.Build().InDir("examples/microservices").RunOrFail(t) 420 421 rpcAddr := randomPort() 422 skaffold.Dev("--status-check=false", "--port-forward", "--rpc-port", rpcAddr).InDir("examples/microservices").RunBackground(t) 423 defer skaffold.Delete().InDir("examples/microservices").Run(t) 424 _, entries := apiEvents(t, rpcAddr) 425 426 // No namespace was provided to `skaffold dev`, so we assume "default" 427 waitForPortForwardEvent(t, entries, "leeroy-app", "service", "default", "leeroooooy app!!\n") 428 429 original, perms, fErr := replaceInFile("leeroooooy app!!", "test string", "examples/microservices/leeroy-app/app.go") 430 failNowIfError(t, fErr) 431 defer func() { 432 if original != nil { 433 os.WriteFile("examples/microservices/leeroy-app/app.go", original, perms) 434 } 435 }() 436 437 waitForPortForwardEvent(t, entries, "leeroy-app", "service", "default", "test string\n") 438 } 439 440 func TestDevPortForwardGKELoadBalancer(t *testing.T) { 441 MarkIntegrationTest(t, NeedsGcp) 442 t.Skip("Skipping until resolved") 443 444 // Run skaffold build first to fail quickly on a build failure 445 skaffold.Build().InDir("testdata/gke_loadbalancer").RunOrFail(t) 446 447 ns, _ := SetupNamespace(t) 448 449 rpcAddr := randomPort() 450 env := []string{fmt.Sprintf("TEST_NS=%s", ns.Name)} 451 skaffold.Dev("--port-forward", "--rpc-port", rpcAddr).InDir("testdata/gke_loadbalancer").InNs(ns.Name).WithEnv(env).RunBackground(t) 452 453 _, entries := apiEvents(t, rpcAddr) 454 455 waitForPortForwardEvent(t, entries, "gke-loadbalancer", "service", ns.Name, "hello!!\n") 456 } 457 458 func getLocalPortFromPortForwardEvent(t *testing.T, entries chan *proto.LogEntry, resourceName, resourceType, namespace string) (string, int) { 459 timeout := time.After(2 * time.Minute) 460 for { 461 select { 462 case <-timeout: 463 t.Fatalf("timed out waiting for port forwarding event") 464 case e := <-entries: 465 switch e.Event.GetEventType().(type) { 466 case *proto.Event_PortEvent: 467 t.Logf("port event received: %v", e) 468 if e.Event.GetPortEvent().ResourceName == resourceName && 469 e.Event.GetPortEvent().ResourceType == resourceType && 470 e.Event.GetPortEvent().Namespace == namespace { 471 address := e.Event.GetPortEvent().Address 472 port := e.Event.GetPortEvent().LocalPort 473 t.Logf("Detected %s/%s is forwarded to address %s port %d", resourceType, resourceName, address, port) 474 return address, int(port) 475 } 476 default: 477 t.Logf("event received: %v", e) 478 } 479 } 480 } 481 } 482 483 //nolint:unparam 484 func waitForPortForwardEvent(t *testing.T, entries chan *proto.LogEntry, resourceName, resourceType, namespace, expected string) { 485 address, port := getLocalPortFromPortForwardEvent(t, entries, resourceName, resourceType, namespace) 486 assertResponseFromPort(t, address, port, expected) 487 } 488 489 // assertResponseFromPort waits for two minutes for the expected response at port. 490 func assertResponseFromPort(t *testing.T, address string, port int, expected string) { 491 url := fmt.Sprintf("http://%s:%d", address, port) 492 t.Logf("Waiting on %s to return: %s", url, expected) 493 ctx, cancelTimeout := context.WithTimeout(context.Background(), 5*time.Minute) 494 defer cancelTimeout() 495 496 for { 497 select { 498 case <-ctx.Done(): 499 t.Fatalf("Timed out waiting for response from port %d", port) 500 case <-time.After(1 * time.Second): 501 client := http.Client{Timeout: 1 * time.Second} 502 resp, err := client.Get(url) 503 if err != nil { 504 t.Logf("[retriable error]: %v", err) 505 continue 506 } 507 defer resp.Body.Close() 508 body, err := io.ReadAll(resp.Body) 509 if err != nil { 510 t.Logf("[retriable error] reading response: %v", err) 511 continue 512 } 513 if string(body) == expected { 514 return 515 } 516 t.Logf("[retriable error] didn't get expected response from port. got: %s, expected: %s", string(body), expected) 517 } 518 } 519 } 520 521 func replaceInFile(target, replacement, filepath string) ([]byte, os.FileMode, error) { 522 fInfo, err := os.Stat(filepath) 523 if err != nil { 524 return nil, 0, err 525 } 526 original, err := os.ReadFile(filepath) 527 if err != nil { 528 return nil, 0, err 529 } 530 531 newContents := strings.ReplaceAll(string(original), target, replacement) 532 533 err = os.WriteFile(filepath, []byte(newContents), 0) 534 535 return original, fInfo.Mode(), err 536 } 537 538 func TestDev_WithKubecontextOverride(t *testing.T) { 539 MarkIntegrationTest(t, CanRunWithoutGcp) 540 541 testutil.Run(t, "skaffold run with kubecontext override", func(t *testutil.T) { 542 ns, client := SetupNamespace(t.T) 543 544 modifiedKubeconfig, kubecontext, err := createModifiedKubeconfig(ns.Name) 545 failNowIfError(t, err) 546 547 kubeconfig := t.NewTempDir(). 548 Write("kubeconfig", string(modifiedKubeconfig)). 549 Path("kubeconfig") 550 env := []string{fmt.Sprintf("KUBECONFIG=%s", kubeconfig)} 551 552 // n.b. for the sake of this test the namespace must not be given explicitly 553 skaffold.Run("--kube-context", kubecontext).InDir("examples/getting-started").WithEnv(env).InNs(ns.Name).RunOrFail(t.T) 554 555 client.WaitForPodsReady("getting-started") 556 }) 557 } 558 559 func createModifiedKubeconfig(namespace string) ([]byte, string, error) { 560 // do not use context.CurrentConfig(), because it may have cached a different config 561 kubeConfig, err := clientcmd.NewDefaultClientConfigLoadingRules().Load() 562 if err != nil { 563 return nil, "", err 564 } 565 566 contextName := "modified-context" 567 if config.IsKindCluster(kubeConfig.CurrentContext) { 568 contextName = "kind-" + contextName 569 } 570 if config.IsK3dCluster(kubeConfig.CurrentContext) { 571 contextName = "k3d-" + contextName 572 } 573 574 if kubeConfig.CurrentContext == constants.DefaultMinikubeContext { 575 contextName = constants.DefaultMinikubeContext // skip, since integration test with minikube runs on single cluster 576 } 577 578 activeContext := kubeConfig.Contexts[kubeConfig.CurrentContext] 579 if activeContext == nil { 580 return nil, "", fmt.Errorf("no active kube-context set") 581 } 582 // clear the namespace in the active context 583 activeContext.Namespace = "" 584 585 newContext := activeContext.DeepCopy() 586 newContext.Namespace = namespace 587 kubeConfig.Contexts[contextName] = newContext 588 589 yaml, err := clientcmd.Write(*kubeConfig) 590 return yaml, contextName, err 591 }