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 "" }