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 }