github.com/GoogleContainerTools/skaffold/v2@v2.13.2/integration/util.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 "bufio" 21 "context" 22 "fmt" 23 "hash/fnv" 24 "io" 25 "os" 26 "os/exec" 27 "strconv" 28 "strings" 29 "testing" 30 "time" 31 32 "github.com/docker/docker/errdefs" 33 appsv1 "k8s.io/api/apps/v1" 34 v1 "k8s.io/api/core/v1" 35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 "k8s.io/apimachinery/pkg/util/wait" 37 "k8s.io/client-go/kubernetes" 38 typedappsv1 "k8s.io/client-go/kubernetes/typed/apps/v1" 39 corev1 "k8s.io/client-go/kubernetes/typed/core/v1" 40 41 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/config" 42 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/docker" 43 kubernetesclient "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/kubernetes/client" 44 kubectx "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/kubernetes/context" 45 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/output/log" 46 k8s "github.com/GoogleContainerTools/skaffold/v2/pkg/webhook/kubernetes" 47 ) 48 49 type TestType int 50 51 const ( 52 CanRunWithoutGcp TestType = iota 53 NeedsGcp 54 ) 55 const numberOfPartition = 4 56 57 func MarkIntegrationTest(t *testing.T, testType TestType) { 58 t.Helper() 59 if testing.Short() { 60 t.Skip("skipping integration test") 61 } 62 63 runOnGCP := os.Getenv("GCP_ONLY") == "true" 64 65 if testType == NeedsGcp && !runOnGCP { 66 t.Skip("skipping GCP integration test") 67 } 68 69 if testType == CanRunWithoutGcp && runOnGCP { 70 t.Skip("skipping non-GCP integration test") 71 } 72 73 if partition() && testType == CanRunWithoutGcp && !matchesPartition(t) { 74 t.Skipf("skipping non-GCP integration test that doesn't match partition %s", getPartition()) 75 } 76 77 if partition() && testType == NeedsGcp && !matchesPartition(t) { 78 t.Skipf("Skipping GCP integration test that doesn't match partition %s", getPartition()) 79 } 80 } 81 82 func partition() bool { 83 return getPartition() != "" 84 } 85 86 func getPartition() string { 87 return os.Getenv("IT_PARTITION") 88 } 89 90 func matchesPartition(t *testing.T) bool { 91 partition := hash(t.Name()) % numberOfPartition 92 t.Logf("Assinged test %s to partition: %d", t.Name(), partition) 93 94 return strconv.FormatUint(partition, 10) == getPartition() 95 } 96 97 func hash(s string) uint64 { 98 h := fnv.New64a() 99 h.Write([]byte(s)) 100 return h.Sum64() 101 } 102 103 func Run(t *testing.T, dir, command string, args ...string) { 104 cmd := exec.Command(command, args...) 105 cmd.Dir = dir 106 if output, err := cmd.Output(); err != nil { 107 t.Fatalf("running command [%s %v]: %s %v", command, args, output, err) 108 } 109 } 110 111 // SetupNamespace creates a Kubernetes namespace to run a test. 112 func SetupNamespace(t *testing.T) (*v1.Namespace, *NSKubernetesClient) { 113 client, err := kubernetesclient.DefaultClient() 114 if err != nil { 115 t.Fatalf("Test setup error: getting Kubernetes client: %s", err) 116 } 117 118 ns, err := client.CoreV1().Namespaces().Create(context.Background(), &v1.Namespace{ 119 ObjectMeta: metav1.ObjectMeta{ 120 GenerateName: "skaffold", 121 }, 122 }, metav1.CreateOptions{}) 123 if err != nil { 124 t.Fatalf("creating namespace: %s", err) 125 } 126 127 ctx := context.Background() 128 log.Entry(ctx).Infoln("Namespace:", ns.Name) 129 130 nsClient := &NSKubernetesClient{ 131 t: t, 132 client: client, 133 ns: ns.Name, 134 } 135 136 t.Cleanup(func() { 137 client.CoreV1().Namespaces().Delete(ctx, ns.Name, metav1.DeleteOptions{}) 138 }) 139 140 return ns, nsClient 141 } 142 143 func DefaultNamespace(t *testing.T) (*v1.Namespace, *NSKubernetesClient) { 144 client, err := kubernetesclient.DefaultClient() 145 if err != nil { 146 t.Fatalf("Test setup error: getting Kubernetes client: %s", err) 147 } 148 ns, err := client.CoreV1().Namespaces().Get(context.Background(), "default", metav1.GetOptions{}) 149 if err != nil { 150 t.Fatalf("getting default namespace: %s", err) 151 } 152 return ns, &NSKubernetesClient{ 153 t: t, 154 client: client, 155 ns: ns.Name, 156 } 157 } 158 159 // NSKubernetesClient wraps a Kubernetes Client for a given namespace. 160 type NSKubernetesClient struct { 161 t *testing.T 162 client kubernetes.Interface 163 ns string 164 } 165 166 func (k *NSKubernetesClient) Pods() corev1.PodInterface { 167 return k.client.CoreV1().Pods(k.ns) 168 } 169 170 func (k *NSKubernetesClient) Secrets() corev1.SecretInterface { 171 return k.client.CoreV1().Secrets(k.ns) 172 } 173 174 func (k *NSKubernetesClient) Services() corev1.ServiceInterface { 175 return k.client.CoreV1().Services(k.ns) 176 } 177 178 func (k *NSKubernetesClient) Deployments() typedappsv1.DeploymentInterface { 179 return k.client.AppsV1().Deployments(k.ns) 180 } 181 182 func (k *NSKubernetesClient) DefaultSecrets() corev1.SecretInterface { 183 return k.client.CoreV1().Secrets("default") 184 } 185 186 func (k *NSKubernetesClient) CreateSecretFrom(ns, name string) { 187 secret, err := k.client.CoreV1().Secrets(ns).Get(context.Background(), name, metav1.GetOptions{}) 188 if err != nil { 189 k.t.Fatalf("failed reading default/e2esecret: %s", err) 190 } 191 192 secret.Namespace = k.ns 193 secret.ResourceVersion = "" 194 if _, err = k.Secrets().Create(context.Background(), secret, metav1.CreateOptions{}); err != nil { 195 k.t.Fatalf("failed creating %s/e2esecret: %s", k.ns, err) 196 } 197 } 198 199 // WaitForPodsReady waits for a list of pods to become ready. 200 func (k *NSKubernetesClient) WaitForPodsReady(podNames ...string) { 201 f := func(pod *v1.Pod) bool { 202 for _, cond := range pod.Status.Conditions { 203 if cond.Type == v1.PodReady && cond.Status == v1.ConditionTrue { 204 return true 205 } 206 } 207 return false 208 } 209 result := k.waitForPods(f, podNames...) 210 log.Entry(context.Background()).Infof("Pods marked as ready: %v", result) 211 } 212 213 // WaitForPodsInPhase waits for a list of pods to reach the given phase. 214 func (k *NSKubernetesClient) WaitForPodsInPhase(expectedPhase v1.PodPhase, podNames ...string) { 215 f := func(pod *v1.Pod) bool { 216 return pod.Status.Phase == expectedPhase 217 } 218 result := k.waitForPods(f, podNames...) 219 log.Entry(context.Background()).Infof("Pods in phase %q: %v", expectedPhase, result) 220 } 221 222 // waitForPods waits for a list of pods to become ready. 223 func (k *NSKubernetesClient) waitForPods(podReady func(*v1.Pod) bool, podNames ...string) (podsReady map[string]bool) { 224 ctx, cancelTimeout := context.WithTimeout(context.Background(), 5*time.Minute) 225 defer cancelTimeout() 226 227 pods := k.Pods() 228 w, err := pods.Watch(ctx, metav1.ListOptions{}) 229 if err != nil { 230 k.t.Fatalf("Unable to watch pods: %v", err) 231 } 232 defer w.Stop() 233 234 waitForAllPods := len(podNames) == 0 235 if waitForAllPods { 236 log.Entry(ctx).Infof("Waiting for all pods in namespace %q to be ready", k.ns) 237 } else { 238 log.Entry(ctx).Infoln("Waiting for pods", podNames, "to be ready") 239 } 240 241 podsReady = map[string]bool{} 242 243 for { 244 waitLoop: 245 select { 246 case <-ctx.Done(): 247 k.printDiskFreeSpace() 248 k.debug("pods") 249 k.logs("pod", podNames) 250 k.t.Fatalf("Timed out waiting for pods %v in namespace %q", podNames, k.ns) 251 252 case event := <-w.ResultChan(): 253 if event.Object == nil { 254 return 255 } 256 pod := event.Object.(*v1.Pod) 257 if pod.Status.Phase == v1.PodFailed { 258 logs, err := pods.GetLogs(pod.Name, &v1.PodLogOptions{}).DoRaw(ctx) 259 if err != nil { 260 k.t.Fatalf("failed to get logs for failed pod %s: %s", pod.Name, err) 261 } 262 k.t.Fatalf("pod %s failed. Logs:\n %s", pod.Name, logs) 263 } 264 265 if _, found := podsReady[pod.Name]; !found && waitForAllPods { 266 podNames = append(podNames, pod.Name) 267 } 268 podsReady[pod.Name] = podReady(pod) 269 270 var waiting []string 271 for _, podName := range podNames { 272 if !podsReady[podName] { 273 waiting = append(waiting, podName) 274 } 275 } 276 if len(waiting) > 0 { 277 log.Entry(ctx).Infof("Still waiting for pods %v", waiting) 278 break waitLoop 279 } else if l := len(w.ResultChan()); l > 0 { 280 // carry on when there are pending messages in case a new pod has been created 281 log.Entry(ctx).Infof("%d pending pod update messages", l) 282 break waitLoop 283 } 284 return 285 } 286 } 287 } 288 289 // GetDeployment gets a deployment by name. 290 func (k *NSKubernetesClient) GetPod(podName string) *v1.Pod { 291 k.t.Helper() 292 k.WaitForPodsReady(podName) 293 294 pod, err := k.Pods().Get(context.Background(), podName, metav1.GetOptions{}) 295 if err != nil { 296 k.t.Fatalf("Could not find pod: %s in namespace %s", podName, k.ns) 297 } 298 return pod 299 } 300 301 // GetDeployment gets a deployment by name. 302 func (k *NSKubernetesClient) GetDeployment(depName string) *appsv1.Deployment { 303 k.t.Helper() 304 k.WaitForDeploymentsToStabilize(depName) 305 306 dep, err := k.Deployments().Get(context.Background(), depName, metav1.GetOptions{}) 307 if err != nil { 308 k.t.Fatalf("Could not find deployment: %s in namespace %s", depName, k.ns) 309 } 310 return dep 311 } 312 313 // WaitForDeploymentsToStabilize waits for a list of deployments to become stable. 314 func (k *NSKubernetesClient) WaitForDeploymentsToStabilize(depNames ...string) { 315 k.t.Helper() 316 k.waitForDeploymentsToStabilizeWithTimeout(2*time.Minute, depNames...) 317 } 318 319 func (k *NSKubernetesClient) waitForDeploymentsToStabilizeWithTimeout(timeout time.Duration, depNames ...string) { 320 k.t.Helper() 321 if len(depNames) == 0 { 322 return 323 } 324 325 ctx, cancelTimeout := context.WithTimeout(context.Background(), timeout) 326 defer cancelTimeout() 327 328 log.Entry(ctx).Infoln("Waiting for deployments", depNames, "to stabilize") 329 330 w, err := k.Deployments().Watch(ctx, metav1.ListOptions{}) 331 if err != nil { 332 k.t.Fatalf("Unable to watch deployments: %v", err) 333 } 334 defer w.Stop() 335 336 deployments := map[string]*appsv1.Deployment{} 337 338 for { 339 waitLoop: 340 select { 341 case <-ctx.Done(): 342 k.printDiskFreeSpace() 343 k.debug("deployments.apps") 344 k.debug("pods") 345 k.logs("deployment.app", depNames) 346 k.t.Fatalf("Timed out waiting for deployments %v to stabilize in namespace %s", depNames, k.ns) 347 348 case event := <-w.ResultChan(): 349 dp, ok := event.Object.(*appsv1.Deployment) 350 if !ok { 351 continue 352 } 353 desiredReplicas := *(dp.Spec.Replicas) 354 log.Entry(ctx).Infof("Deployment %s: Generation %d/%d, Replicas %d/%d, Available %d/%d", 355 dp.Name, 356 dp.Status.ObservedGeneration, dp.Generation, 357 dp.Status.Replicas, desiredReplicas, 358 dp.Status.AvailableReplicas, desiredReplicas) 359 360 deployments[dp.Name] = dp 361 362 for _, depName := range depNames { 363 if d, present := deployments[depName]; !present || !isStable(d) { 364 break waitLoop 365 } 366 } 367 368 log.Entry(ctx).Infoln("Deployments", depNames, "are stable") 369 return 370 } 371 } 372 } 373 374 // debug is used to print all the details about pods or deployments 375 func (k *NSKubernetesClient) debug(entities string) { 376 cmd := exec.Command("kubectl", "-n", k.ns, "get", entities, "-oyaml") 377 log.Entry(context.Background()).Warnln(cmd.Args) 378 out, _ := cmd.CombinedOutput() 379 fmt.Println(string(out)) // Use fmt.Println, not logrus, for prettier output 380 } 381 382 func (k *NSKubernetesClient) printDiskFreeSpace() { 383 cmd := exec.Command("df", "-h") 384 log.Entry(context.Background()).Warnln(cmd.Args) 385 out, _ := cmd.CombinedOutput() 386 fmt.Println(string(out)) 387 } 388 389 // logs is used to print the logs of a resource 390 func (k *NSKubernetesClient) logs(entity string, names []string) { 391 for _, n := range names { 392 cmd := exec.Command("kubectl", "-n", k.ns, "logs", entity+"/"+n) 393 log.Entry(context.Background()).Warnln(cmd.Args) 394 out, _ := cmd.CombinedOutput() 395 fmt.Println(string(out)) // Use fmt.Println, not logrus, for prettier output 396 } 397 } 398 399 // ExternalIP waits for the external IP aof a given service. 400 func (k *NSKubernetesClient) ExternalIP(serviceName string) string { 401 svc, err := k.Services().Get(context.Background(), serviceName, metav1.GetOptions{}) 402 if err != nil { 403 k.t.Fatalf("error getting registry service: %v", err) 404 } 405 406 ip, err := k8s.GetExternalIP(svc) 407 if err != nil { 408 k.t.Fatalf("error getting external ip: %v", err) 409 } 410 411 return ip 412 } 413 414 func isStable(dp *appsv1.Deployment) bool { 415 return dp.Generation <= dp.Status.ObservedGeneration && *(dp.Spec.Replicas) == dp.Status.Replicas && *(dp.Spec.Replicas) == dp.Status.AvailableReplicas 416 } 417 418 func WaitForLogs(t *testing.T, out io.Reader, firstMessage string, moreMessages ...string) { 419 lines := make(chan string) 420 go func() { 421 scanner := bufio.NewScanner(out) 422 for scanner.Scan() { 423 lines <- scanner.Text() 424 } 425 }() 426 427 current := 0 428 message := firstMessage 429 430 timer := time.NewTimer(90 * time.Second) 431 defer timer.Stop() 432 for { 433 select { 434 case <-timer.C: 435 t.Fatal("timeout") 436 case line := <-lines: 437 t.Logf("Expecting %s, received %s \n", message, line) 438 if strings.Contains(line, message) { 439 if current >= len(moreMessages) { 440 return 441 } 442 443 message = moreMessages[current] 444 current++ 445 } 446 } 447 } 448 } 449 450 // SetupDockerClient creates a client against the local docker daemon 451 func SetupDockerClient(t *testing.T) docker.LocalDaemon { 452 kubeConfig, err := kubectx.CurrentConfig() 453 if err != nil { 454 t.Log("unable to get current cluster context: %w", err) 455 t.Logf("test might not be running against the right docker daemon") 456 } 457 kubeContext := kubeConfig.CurrentContext 458 459 client, err := docker.NewAPIClient(context.Background(), fakeDockerConfig{kubeContext: kubeContext}) 460 if err != nil { 461 t.Fail() 462 } 463 return client 464 } 465 466 func waitForContainersRunning(t *testing.T, containerNames ...string) error { 467 t.Helper() 468 469 ctx := context.Background() 470 // Same as waitForPods. 471 timeout := 5 * time.Minute 472 interval := 1 * time.Second 473 client := SetupDockerClient(t) 474 475 return wait.Poll(interval, timeout, func() (bool, error) { 476 containersRunning := 0 477 for _, cn := range containerNames { 478 cInfo, err := client.RawClient().ContainerInspect(ctx, cn) 479 if err != nil && !errdefs.IsNotFound(err) { 480 return false, err 481 } 482 483 if errdefs.IsNotFound(err) { 484 return false, nil 485 } 486 487 if cInfo.State.Running { 488 containersRunning++ 489 } 490 491 if cInfo.State.Dead || cInfo.State.Restarting { 492 return false, fmt.Errorf("container %v is in dead or restarting state", cn) 493 } 494 } 495 496 if containersRunning == len(containerNames) { 497 return true, nil 498 } 499 500 return false, nil 501 }) 502 } 503 504 type fakeDockerConfig struct { 505 kubeContext string 506 } 507 508 func (d fakeDockerConfig) GetKubeContext() string { return d.kubeContext } 509 func (d fakeDockerConfig) MinikubeProfile() string { return "" } 510 func (d fakeDockerConfig) GlobalConfig() string { return "" } 511 func (d fakeDockerConfig) Prune() bool { return false } 512 func (d fakeDockerConfig) ContainerDebugging() bool { return false } 513 func (d fakeDockerConfig) GetInsecureRegistries() map[string]bool { return nil } 514 func (d fakeDockerConfig) Mode() config.RunMode { return "" }