knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/test/kube_checks.go (about)

     1  /*
     2  Copyright 2018 The Knative 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  // kube_checks contains functions which poll Kubernetes objects until
    18  // they get into the state desired by the caller or time out.
    19  
    20  package test
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/davecgh/go-spew/spew"
    29  	"github.com/google/go-cmp/cmp"
    30  	appsv1 "k8s.io/api/apps/v1"
    31  	corev1 "k8s.io/api/core/v1"
    32  	apierrs "k8s.io/apimachinery/pkg/api/errors"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/util/wait"
    35  	"k8s.io/client-go/kubernetes"
    36  	k8styped "k8s.io/client-go/kubernetes/typed/core/v1"
    37  
    38  	"knative.dev/pkg/test/logging"
    39  )
    40  
    41  const (
    42  	interval   = 1 * time.Second
    43  	podTimeout = 8 * time.Minute
    44  	logTimeout = 1 * time.Minute
    45  )
    46  
    47  // WaitForDeploymentState polls the status of the Deployment called name
    48  // from client every interval until inState returns `true` indicating it
    49  // is done, returns an error or timeout. desc will be used to name the metric
    50  // that is emitted to track how long it took for name to get into the state checked by inState.
    51  func WaitForDeploymentState(ctx context.Context, client kubernetes.Interface, name string, inState func(d *appsv1.Deployment) (bool, error), desc string, namespace string, timeout time.Duration) error {
    52  	d := client.AppsV1().Deployments(namespace)
    53  	span := logging.GetEmitableSpan(ctx, fmt.Sprintf("WaitForDeploymentState/%s/%s", name, desc))
    54  	defer span.End()
    55  	var lastState *appsv1.Deployment
    56  	waitErr := wait.PollUntilContextTimeout(ctx, interval, timeout, true, func(ctx context.Context) (bool, error) {
    57  		var err error
    58  		lastState, err = d.Get(ctx, name, metav1.GetOptions{})
    59  		if err != nil {
    60  			return true, err
    61  		}
    62  		return inState(lastState)
    63  	})
    64  
    65  	if waitErr != nil {
    66  		return fmt.Errorf("deployment %q is not in desired state, got: %s: %w", name, spew.Sprint(lastState), waitErr)
    67  	}
    68  	return nil
    69  }
    70  
    71  // WaitForPodListState polls the status of the PodList
    72  // from client every interval until inState returns `true` indicating it
    73  // is done, returns an error or timeout. desc will be used to name the metric
    74  // that is emitted to track how long it took to get into the state checked by inState.
    75  func WaitForPodListState(ctx context.Context, client kubernetes.Interface, inState func(p *corev1.PodList) (bool, error), desc string, namespace string) error {
    76  	p := client.CoreV1().Pods(namespace)
    77  	span := logging.GetEmitableSpan(ctx, "WaitForPodListState/"+desc)
    78  	defer span.End()
    79  
    80  	var lastState *corev1.PodList
    81  	waitErr := wait.PollUntilContextTimeout(ctx, interval, podTimeout, true, func(ctx context.Context) (bool, error) {
    82  		var err error
    83  		lastState, err = p.List(ctx, metav1.ListOptions{})
    84  		if err != nil {
    85  			return true, err
    86  		}
    87  		return inState(lastState)
    88  	})
    89  
    90  	if waitErr != nil {
    91  		return fmt.Errorf("pod list is not in desired state, got: %s: %w", spew.Sprint(lastState), waitErr)
    92  	}
    93  	return nil
    94  }
    95  
    96  // WaitForPodState polls the status of the specified Pod
    97  // from client every interval until inState returns `true` indicating it
    98  // is done, returns an error or timeout. desc will be used to name the metric
    99  // that is emitted to track how long it took to get into the state checked by inState.
   100  func WaitForPodState(ctx context.Context, client kubernetes.Interface, inState func(p *corev1.Pod) (bool, error), name string, namespace string) error {
   101  	p := client.CoreV1().Pods(namespace)
   102  	span := logging.GetEmitableSpan(ctx, "WaitForPodState/"+name)
   103  	defer span.End()
   104  
   105  	var lastState *corev1.Pod
   106  	waitErr := wait.PollUntilContextTimeout(ctx, interval, podTimeout, true, func(ctx context.Context) (bool, error) {
   107  		var err error
   108  		lastState, err = p.Get(ctx, name, metav1.GetOptions{})
   109  		if err != nil {
   110  			return false, err
   111  		}
   112  		return inState(lastState)
   113  	})
   114  
   115  	if waitErr != nil {
   116  		return fmt.Errorf("pod %q is not in desired state, got: %s: %w", name, spew.Sprint(lastState), waitErr)
   117  	}
   118  	return nil
   119  }
   120  
   121  // WaitForPodDeleted waits for the given pod to disappear from the given namespace.
   122  func WaitForPodDeleted(ctx context.Context, client kubernetes.Interface, name, namespace string) error {
   123  	if err := WaitForPodState(ctx, client, func(p *corev1.Pod) (bool, error) {
   124  		// Always return false. We're oly interested in the error which indicates pod deletion or timeout.
   125  		return false, nil
   126  	}, name, namespace); err != nil {
   127  		if !apierrs.IsNotFound(err) {
   128  			return err
   129  		}
   130  	}
   131  	return nil
   132  }
   133  
   134  // WaitForServiceEndpoints polls the status of the specified Service
   135  // from client every interval until number of service endpoints = numOfEndpoints
   136  func WaitForServiceEndpoints(ctx context.Context, client kubernetes.Interface, svcName string, svcNamespace string, numOfEndpoints int) error {
   137  	endpointsService := client.CoreV1().Endpoints(svcNamespace)
   138  	span := logging.GetEmitableSpan(ctx, "WaitForServiceHasAtLeastOneEndpoint/"+svcName)
   139  	defer span.End()
   140  
   141  	var endpoints *corev1.Endpoints
   142  	waitErr := wait.PollUntilContextTimeout(ctx, interval, podTimeout, true, func(ctx context.Context) (bool, error) {
   143  		var err error
   144  		endpoints, err = endpointsService.Get(ctx, svcName, metav1.GetOptions{})
   145  		if apierrs.IsNotFound(err) {
   146  			return false, nil
   147  		}
   148  		if err != nil {
   149  			return false, err
   150  		}
   151  
   152  		return countEndpointsNum(endpoints) == numOfEndpoints, nil
   153  	})
   154  	if waitErr != nil {
   155  		return fmt.Errorf("did not reach the desired number of endpoints, got: %d: %w", countEndpointsNum(endpoints), waitErr)
   156  	}
   157  	return nil
   158  }
   159  
   160  func countEndpointsNum(e *corev1.Endpoints) int {
   161  	if e == nil || e.Subsets == nil {
   162  		return 0
   163  	}
   164  	num := 0
   165  	for _, sub := range e.Subsets {
   166  		num += len(sub.Addresses)
   167  	}
   168  	return num
   169  }
   170  
   171  // GetEndpointAddresses returns addresses of endpoints for the given service.
   172  func GetEndpointAddresses(ctx context.Context, client kubernetes.Interface, svcName, svcNamespace string) ([]string, error) {
   173  	endpoints, err := client.CoreV1().Endpoints(svcNamespace).Get(ctx, svcName, metav1.GetOptions{})
   174  	if err != nil || countEndpointsNum(endpoints) == 0 {
   175  		return nil, fmt.Errorf("no endpoints or error: %w", err)
   176  	}
   177  	var hosts []string
   178  	for _, sub := range endpoints.Subsets {
   179  		for _, addr := range sub.Addresses {
   180  			hosts = append(hosts, addr.IP)
   181  		}
   182  	}
   183  	return hosts, nil
   184  }
   185  
   186  // WaitForChangedEndpoints waits until the endpoints for the given service differ from origEndpoints.
   187  func WaitForChangedEndpoints(ctx context.Context, client kubernetes.Interface, svcName, svcNamespace string, origEndpoints []string) error {
   188  	var newEndpoints []string
   189  	waitErr := wait.PollUntilContextTimeout(ctx, 1*time.Second, 2*time.Minute, true, func(ctx context.Context) (bool, error) {
   190  		var err error
   191  		newEndpoints, err = GetEndpointAddresses(ctx, client, svcName, svcNamespace)
   192  		return !cmp.Equal(origEndpoints, newEndpoints), err
   193  	})
   194  	if waitErr != nil {
   195  		return fmt.Errorf("new endpoints are not different from the original ones, got %q: %w", newEndpoints, waitErr)
   196  	}
   197  	return nil
   198  }
   199  
   200  // GetConfigMap gets the configmaps for a given namespace
   201  func GetConfigMap(client kubernetes.Interface, namespace string) k8styped.ConfigMapInterface {
   202  	return client.CoreV1().ConfigMaps(namespace)
   203  }
   204  
   205  // DeploymentScaledToZeroFunc returns a func that evaluates if a deployment has scaled to 0 pods
   206  func DeploymentScaledToZeroFunc() func(d *appsv1.Deployment) (bool, error) {
   207  	return func(d *appsv1.Deployment) (bool, error) {
   208  		return d.Status.ReadyReplicas == 0, nil
   209  	}
   210  }
   211  
   212  // WaitForLogContent waits until logs for given Pod/Container include the given content.
   213  // If the content is not present within timeout it returns error.
   214  func WaitForLogContent(ctx context.Context, client kubernetes.Interface, podName, containerName, namespace, content string) error {
   215  	var logs []byte
   216  	waitErr := wait.PollUntilContextTimeout(ctx, interval, logTimeout, true, func(ctx context.Context) (bool, error) {
   217  		var err error
   218  		logs, err = PodLogs(ctx, client, podName, containerName, namespace)
   219  		if err != nil {
   220  			return true, err
   221  		}
   222  		return strings.Contains(string(logs), content), nil
   223  	})
   224  	if waitErr != nil {
   225  		return fmt.Errorf("logs do not contain the desired content %q, got %q: %w", content, logs, waitErr)
   226  	}
   227  	return nil
   228  }
   229  
   230  // WaitForAllPodsRunning waits for all the pods to be in running state
   231  func WaitForAllPodsRunning(ctx context.Context, client kubernetes.Interface, namespace string) error {
   232  	return WaitForPodListState(ctx, client, podsRunning, "PodsAreRunning", namespace)
   233  }
   234  
   235  // WaitForPodRunning waits for the given pod to be in running state
   236  func WaitForPodRunning(ctx context.Context, client kubernetes.Interface, name string, namespace string) error {
   237  	var p *corev1.Pod
   238  	pods := client.CoreV1().Pods(namespace)
   239  	waitErr := wait.PollUntilContextTimeout(ctx, interval, podTimeout, true, func(ctx context.Context) (bool, error) {
   240  		var err error
   241  		p, err = pods.Get(ctx, name, metav1.GetOptions{})
   242  		if err != nil {
   243  			return true, err
   244  		}
   245  		return podRunning(p), nil
   246  	})
   247  	if waitErr != nil {
   248  		return fmt.Errorf("pod %q did not reach the running state, got %+v: %w", name, p.Status.Phase, waitErr)
   249  	}
   250  	return nil
   251  }
   252  
   253  // podsRunning will check the status conditions of the pod list and return true all pods are Running
   254  func podsRunning(podList *corev1.PodList) (bool, error) {
   255  	// Pods are big, so use indexing, to avoid copying.
   256  	for i := range podList.Items {
   257  		if isRunning := podRunning(&podList.Items[i]); !isRunning {
   258  			return false, nil
   259  		}
   260  	}
   261  	return true, nil
   262  }
   263  
   264  // podRunning will check the status conditions of the pod and return true if it's Running.
   265  func podRunning(pod *corev1.Pod) bool {
   266  	return pod.Status.Phase == corev1.PodRunning || pod.Status.Phase == corev1.PodSucceeded
   267  }
   268  
   269  // WaitForDeploymentScale waits until the given deployment has the expected scale.
   270  func WaitForDeploymentScale(ctx context.Context, client kubernetes.Interface, name, namespace string, scale int) error {
   271  	return WaitForDeploymentState(
   272  		ctx,
   273  		client,
   274  		name,
   275  		func(d *appsv1.Deployment) (bool, error) {
   276  			return d.Status.ReadyReplicas == int32(scale), nil
   277  		},
   278  		"DeploymentIsScaled",
   279  		namespace,
   280  		time.Minute,
   281  	)
   282  }