github.com/smartcontractkit/chainlink-testing-framework/libs@v0.0.0-20240227141906-ec710b4eb1a3/k8s/client/client.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "os" 8 "regexp" 9 "sort" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/google/uuid" 15 "github.com/rs/zerolog/log" 16 "k8s.io/apimachinery/pkg/runtime/schema" 17 "k8s.io/apimachinery/pkg/runtime/serializer" 18 "k8s.io/apimachinery/pkg/types" 19 "k8s.io/apimachinery/pkg/util/wait" 20 "k8s.io/cli-runtime/pkg/genericclioptions" 21 "k8s.io/client-go/kubernetes/scheme" 22 "k8s.io/client-go/tools/remotecommand" 23 "k8s.io/kubectl/pkg/cmd/cp" 24 25 v1 "k8s.io/api/core/v1" 26 metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/client-go/kubernetes" 28 "k8s.io/client-go/rest" 29 "k8s.io/client-go/tools/clientcmd" 30 cmdutil "k8s.io/kubectl/pkg/cmd/util" 31 ) 32 33 const ( 34 TempDebugManifest = "tmp-manifest-%s.yaml" 35 K8sStatePollInterval = 10 * time.Second 36 JobFinalizedTimeout = 2 * time.Minute 37 AppLabel = "app" 38 ) 39 40 // K8sClient high level k8s client 41 type K8sClient struct { 42 ClientSet *kubernetes.Clientset 43 RESTConfig *rest.Config 44 } 45 46 // GetLocalK8sDeps get local k8s context config 47 func GetLocalK8sDeps() (*kubernetes.Clientset, *rest.Config, error) { 48 loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 49 kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) 50 k8sConfig, err := kubeConfig.ClientConfig() 51 if err != nil { 52 return nil, nil, err 53 } 54 k8sClient, err := kubernetes.NewForConfig(k8sConfig) 55 if err != nil { 56 return nil, nil, err 57 } 58 return k8sClient, k8sConfig, nil 59 } 60 61 // NewK8sClient creates a new k8s client with a REST config 62 func NewK8sClient() (*K8sClient, error) { 63 cs, cfg, err := GetLocalK8sDeps() 64 if err != nil { 65 return nil, err 66 } 67 return &K8sClient{ 68 ClientSet: cs, 69 RESTConfig: cfg, 70 }, nil 71 } 72 73 // ListPods lists pods for a namespace and selector 74 func (m *K8sClient) ListPods(namespace, selector string) (*v1.PodList, error) { 75 pods, err := m.ClientSet.CoreV1().Pods(namespace).List(context.Background(), metaV1.ListOptions{LabelSelector: selector}) 76 sort.Slice(pods.Items, func(i, j int) bool { 77 return pods.Items[i].CreationTimestamp.Before(pods.Items[j].CreationTimestamp.DeepCopy()) 78 }) 79 return pods.DeepCopy(), err 80 } 81 82 // ListPods lists services for a namespace and selector 83 func (m *K8sClient) ListServices(namespace, selector string) (*v1.ServiceList, error) { 84 services, err := m.ClientSet.CoreV1().Services(namespace).List(context.Background(), metaV1.ListOptions{LabelSelector: selector}) 85 return services.DeepCopy(), err 86 } 87 88 // ListNamespaces lists k8s namespaces 89 func (m *K8sClient) ListNamespaces(selector string) (*v1.NamespaceList, error) { 90 return m.ClientSet.CoreV1().Namespaces().List(context.Background(), metaV1.ListOptions{LabelSelector: selector}) 91 } 92 93 // AddLabel adds a new label to a group of pods defined by selector 94 func (m *K8sClient) AddLabel(namespace string, selector string, label string) error { 95 podList, err := m.ListPods(namespace, selector) 96 if err != nil { 97 return err 98 } 99 l := strings.Split(label, "=") 100 if len(l) != 2 { 101 return fmt.Errorf("labels must be in format key=value") 102 } 103 for _, pod := range podList.Items { 104 labelPatch := fmt.Sprintf(`[{"op":"add","path":"/metadata/labels/%s","value":"%s" }]`, l[0], l[1]) 105 _, err := m.ClientSet.CoreV1().Pods(namespace).Patch( 106 context.Background(), 107 pod.GetName(), 108 types.JSONPatchType, 109 []byte(labelPatch), 110 metaV1.PatchOptions{}, 111 ) 112 if err != nil { 113 return fmt.Errorf("failed to update labels %s for pod %s err: %w", labelPatch, pod.Name, err) 114 } 115 } 116 log.Debug().Str("Selector", selector).Str("Label", label).Msg("Updated label") 117 return nil 118 } 119 120 func (m *K8sClient) LabelChaosGroup(namespace string, labelPrefix string, startInstance int, endInstance int, group string) error { 121 for i := startInstance; i <= endInstance; i++ { 122 err := m.AddLabel(namespace, fmt.Sprintf("%s%d", labelPrefix, i), fmt.Sprintf("%s=1", group)) 123 if err != nil { 124 return err 125 } 126 } 127 return nil 128 } 129 130 func (m *K8sClient) LabelChaosGroupByLabels(namespace string, labels map[string]string, group string) error { 131 labelSelector := "" 132 for key, value := range labels { 133 if labelSelector == "" { 134 labelSelector = fmt.Sprintf("%s=%s", key, value) 135 } else { 136 labelSelector = fmt.Sprintf("%s, %s=%s", labelSelector, key, value) 137 } 138 } 139 podList, err := m.ListPods(namespace, labelSelector) 140 if err != nil { 141 return err 142 } 143 for _, pod := range podList.Items { 144 err = m.AddPodLabel(namespace, pod, group, "1") 145 if err != nil { 146 return err 147 } 148 } 149 return nil 150 } 151 152 // AddPodsLabels adds map of labels to all pods in list 153 func (m *K8sClient) AddPodsLabels(namespace string, podList *v1.PodList, labels map[string]string) error { 154 for _, pod := range podList.Items { 155 for k, v := range labels { 156 err := m.AddPodLabel(namespace, pod, k, v) 157 if err != nil { 158 return err 159 } 160 } 161 } 162 return nil 163 } 164 165 // AddPodsAnnotations adds map of annotations to all pods in list 166 func (m *K8sClient) AddPodsAnnotations(namespace string, podList *v1.PodList, annotations map[string]string) error { 167 // when applying annotations the key doesn't like `/` characters here but everywhere else it does 168 // replacing it here with ~1 169 fixedAnnotations := make(map[string]string) 170 for k, v := range annotations { 171 fixedAnnotations[strings.ReplaceAll(k, "/", "~1")] = v 172 } 173 for _, pod := range podList.Items { 174 for k, v := range fixedAnnotations { 175 err := m.AddPodAnnotation(namespace, pod, k, v) 176 if err != nil { 177 return err 178 } 179 } 180 } 181 return nil 182 } 183 184 // UniqueLabels gets all unique application labels 185 func (m *K8sClient) UniqueLabels(namespace string, selector string) ([]string, error) { 186 uniqueLabels := make([]string, 0) 187 isUnique := make(map[string]bool) 188 podList, err := m.ListPods(namespace, selector) 189 if err != nil { 190 return nil, err 191 } 192 for _, p := range podList.Items { 193 appLabel := p.Labels[AppLabel] 194 if _, ok := isUnique[appLabel]; !ok { 195 uniqueLabels = append(uniqueLabels, appLabel) 196 } 197 } 198 log.Info(). 199 Interface("Apps", uniqueLabels). 200 Int("Count", len(uniqueLabels)). 201 Msg("Apps found") 202 return uniqueLabels, nil 203 } 204 205 // AddPodLabel adds a label to a pod 206 func (m *K8sClient) AddPodLabel(namespace string, pod v1.Pod, key, value string) error { 207 labelPatch := fmt.Sprintf(`[{"op":"add","path":"/metadata/labels/%s","value":"%s" }]`, key, value) 208 _, err := m.ClientSet.CoreV1().Pods(namespace).Patch( 209 context.Background(), pod.GetName(), types.JSONPatchType, []byte(labelPatch), metaV1.PatchOptions{}) 210 if err != nil { 211 return err 212 } 213 return nil 214 } 215 216 // AddPodAnnotation adds an annotation to a pod 217 func (m *K8sClient) AddPodAnnotation(namespace string, pod v1.Pod, key, value string) error { 218 labelPatch := fmt.Sprintf(`[{"op":"add","path":"/metadata/annotations/%s","value":"%s" }]`, key, value) 219 _, err := m.ClientSet.CoreV1().Pods(namespace).Patch( 220 context.Background(), pod.GetName(), types.JSONPatchType, []byte(labelPatch), metaV1.PatchOptions{}) 221 if err != nil { 222 return err 223 } 224 return nil 225 } 226 227 // EnumerateInstances enumerate pods with instance label 228 func (m *K8sClient) EnumerateInstances(namespace string, selector string) error { 229 podList, err := m.ListPods(namespace, selector) 230 if err != nil { 231 return err 232 } 233 234 for id, pod := range podList.Items { 235 // skip if already labeled with instance 236 existingLabels := pod.Labels 237 _, exists := existingLabels["instance"] 238 if exists { 239 continue 240 } 241 if err := m.AddPodLabel(namespace, pod, "instance", strconv.Itoa(id)); err != nil { 242 return err 243 } 244 } 245 return nil 246 } 247 248 // waitForPodsExist waits for all the expected number of pods to exist 249 func (m *K8sClient) waitForPodsExist(ns string, expectedPodCount int) error { 250 log.Debug().Int("ExpectedCount", expectedPodCount).Msg("Waiting for pods to exist") 251 var exitErr error 252 timeout := 15 * time.Minute 253 ctx, cancel := context.WithTimeout(context.Background(), timeout) 254 defer cancel() 255 if err := wait.PollUntilContextTimeout(ctx, 2*time.Second, timeout, true, func(ctx context.Context) (bool, error) { 256 // nolint:contextcheck 257 apps, err2 := m.UniqueLabels(ns, AppLabel) 258 if err2 != nil { 259 exitErr = err2 260 return false, nil 261 } 262 if len(apps) >= expectedPodCount { 263 exitErr = nil 264 return true, nil 265 } 266 return false, nil 267 }); err != nil { 268 return err 269 } 270 271 return exitErr 272 } 273 274 // WaitPodsReady waits until all pods are ready 275 func (m *K8sClient) WaitPodsReady(ns string, rcd *ReadyCheckData, expectedPodCount int) error { 276 // Wait for pods to exist 277 err := m.waitForPodsExist(ns, expectedPodCount) 278 if err != nil { 279 return err 280 } 281 282 log.Info().Msg("Waiting for pods to be ready") 283 ticker := time.NewTicker(K8sStatePollInterval) 284 defer ticker.Stop() 285 timeout := time.NewTimer(rcd.Timeout) 286 readyCount := 0 287 defer timeout.Stop() 288 for { 289 select { 290 case <-timeout.C: 291 return fmt.Errorf("waitcontainersready, no pods in '%s' with selector '%s' after timeout '%s'", 292 ns, rcd.ReadinessProbeCheckSelector, rcd.Timeout) 293 case <-ticker.C: 294 podList, err := m.ListPods(ns, rcd.ReadinessProbeCheckSelector) 295 if err != nil { 296 return err 297 } 298 if len(podList.Items) == 0 && expectedPodCount > 0 { 299 log.Debug(). 300 Str("Namespace", ns). 301 Str("Selector", rcd.ReadinessProbeCheckSelector). 302 Msg("No pods found with selector") 303 continue 304 } 305 log.Debug().Interface("Pods", podNames(podList)).Msg("Waiting for pods readiness probes") 306 allReady := true 307 for _, pod := range podList.Items { 308 if pod.Status.Phase == "Succeeded" { 309 log.Debug().Str("Pod", pod.Name).Msg("Pod is in Succeeded state") 310 continue 311 } else if pod.Status.Phase != v1.PodRunning { 312 log.Debug().Str("Pod", pod.Name).Str("Phase", string(pod.Status.Phase)).Msg("Pod is not running") 313 allReady = false 314 break 315 } 316 for _, c := range pod.Status.Conditions { 317 if c.Type == v1.ContainersReady && c.Status != "True" { 318 log.Debug().Str("Text", c.Message).Msg("Pod condition message") 319 allReady = false 320 } 321 } 322 } 323 324 if allReady { 325 readyCount++ 326 // wait for it to be ready 3 times since there is no good way to know if an old pod 327 // was present but not yet decommisiond during a rollout 328 // usually there is just a very small blip that we can run into this and this will 329 // prevent that from happening 330 if readyCount == 3 { 331 return nil 332 } 333 } 334 } 335 } 336 } 337 338 // NamespaceExists check if namespace exists 339 func (m *K8sClient) NamespaceExists(namespace string) bool { 340 if _, err := m.ClientSet.CoreV1().Namespaces().Get(context.Background(), namespace, metaV1.GetOptions{}); err != nil { 341 return false 342 } 343 return true 344 } 345 346 // RemoveNamespace removes namespace 347 func (m *K8sClient) RemoveNamespace(namespace string) error { 348 log.Info().Str("Namespace", namespace).Msg("Removing namespace") 349 return m.ClientSet.CoreV1().Namespaces().Delete(context.Background(), namespace, metaV1.DeleteOptions{}) 350 } 351 352 // RolloutStatefulSets applies "rollout statefulset" to all existing statefulsets in that namespace 353 func (m *K8sClient) RolloutStatefulSets(ctx context.Context, namespace string) error { 354 stsClient := m.ClientSet.AppsV1().StatefulSets(namespace) 355 sts, err := stsClient.List(ctx, metaV1.ListOptions{}) 356 if err != nil { 357 return err 358 } 359 for _, s := range sts.Items { 360 cmd := fmt.Sprintf("kubectl rollout restart statefulset %s --namespace %s", s.Name, namespace) 361 log.Info().Str("Command", cmd).Msg("Applying StatefulSet rollout") 362 if err := ExecCmdWithContext(ctx, cmd); err != nil { 363 return err 364 } 365 } 366 // wait for the statefulsets to be ready in a separate loop otherwise this can take a long time 367 for _, s := range sts.Items { 368 // wait for the rollout to be complete 369 scmd := fmt.Sprintf("kubectl rollout status statefulset %s --namespace %s", s.Name, namespace) 370 log.Info().Str("Command", scmd).Msg("Waiting for StatefulSet rollout to finish") 371 if err := ExecCmdWithContext(ctx, scmd); err != nil { 372 return err 373 } 374 } 375 return nil 376 } 377 378 // RolloutRestartBySelector rollouts and restarts object by selector 379 func (m *K8sClient) RolloutRestartBySelector(ctx context.Context, namespace, resource, selector string) error { 380 cmd := fmt.Sprintf("kubectl --namespace %s rollout restart -l %s %s", namespace, selector, resource) 381 log.Info().Str("Command", cmd).Msg("rollout restart by selector") 382 if err := ExecCmdWithContext(ctx, cmd); err != nil { 383 return err 384 } 385 // wait for the rollout to be complete 386 waitCmd := fmt.Sprintf("kubectl --namespace %s rollout status -l %s %s", namespace, selector, resource) 387 log.Info().Str("Command", waitCmd).Msg("Waiting for StatefulSet rollout to finish") 388 return ExecCmdWithContext(ctx, waitCmd) 389 } 390 391 // ReadyCheckData data to check if selected pods are running and all containers are ready ( readiness check ) are ready 392 type ReadyCheckData struct { 393 ReadinessProbeCheckSelector string 394 Timeout time.Duration 395 } 396 397 // WaitForJob wait for job execution, follow logs and returns an error if job failed 398 func (m *K8sClient) WaitForJob(namespaceName string, jobName string, fundReturnStatus func(string)) error { 399 cmd := fmt.Sprintf("kubectl --namespace %s logs --follow job/%s", namespaceName, jobName) 400 log.Info().Str("Job", jobName).Str("cmd", cmd).Msg("Waiting for job to complete") 401 ctx := context.Background() 402 if err := ExecCmdWithOptions(ctx, cmd, fundReturnStatus); err != nil { 403 return err 404 } 405 var exitErr error 406 ctx, cancel := context.WithTimeout(ctx, JobFinalizedTimeout) 407 defer cancel() 408 if err := wait.PollUntilContextTimeout(ctx, K8sStatePollInterval, JobFinalizedTimeout, true, func(ctx context.Context) (bool, error) { 409 job, err := m.ClientSet.BatchV1().Jobs(namespaceName).Get(ctx, jobName, metaV1.GetOptions{}) 410 if err != nil { 411 exitErr = err 412 } 413 if int(job.Status.Failed) > 0 { 414 exitErr = fmt.Errorf("job failed") 415 return true, nil 416 } 417 if int(job.Status.Succeeded) > 0 { 418 exitErr = nil 419 return true, nil 420 } 421 return false, nil 422 }); err != nil { 423 return err 424 } 425 return exitErr 426 } 427 428 func (m *K8sClient) WaitForDeploymentsAvailable(ctx context.Context, namespace string) error { 429 deployments, err := m.ClientSet.AppsV1().Deployments(namespace).List(ctx, metaV1.ListOptions{}) 430 if err != nil { 431 return err 432 } 433 log.Debug().Int("Number", len(deployments.Items)).Msg("Deployments found") 434 for _, d := range deployments.Items { 435 log.Debug().Str("status", d.Status.String()).Msg("Deployment info") 436 waitCmd := fmt.Sprintf("kubectl rollout status -n %s deployment/%s", namespace, d.Name) 437 log.Debug().Str("cmd", waitCmd).Msg("wait for deployment to be available") 438 if err := ExecCmdWithContext(ctx, waitCmd); err != nil { 439 return err 440 } 441 } 442 return nil 443 } 444 445 // Apply applying a manifest to a currently connected k8s context 446 func (m *K8sClient) Apply(ctx context.Context, manifest, namespace string, waitForDeployment bool) error { 447 manifestFile := fmt.Sprintf(TempDebugManifest, uuid.NewString()) 448 log.Info().Str("File", manifestFile).Msg("Applying manifest") 449 if err := os.WriteFile(manifestFile, []byte(manifest), os.ModePerm); err != nil { 450 return err 451 } 452 cmd := fmt.Sprintf("kubectl apply -f %s", manifestFile) 453 log.Debug().Str("cmd", cmd).Msg("Apply command") 454 if err := ExecCmdWithContext(ctx, cmd); err != nil { 455 return err 456 } 457 if waitForDeployment { 458 return m.WaitForDeploymentsAvailable(ctx, namespace) 459 } 460 return nil 461 } 462 463 // DeleteResource deletes resource 464 func (m *K8sClient) DeleteResource(namespace string, resource string, instance string) error { 465 return ExecCmd(fmt.Sprintf("kubectl delete %s %s --namespace %s", resource, instance, namespace)) 466 } 467 468 // Create creating a manifest to a currently connected k8s context 469 func (m *K8sClient) Create(manifest string) error { 470 manifestFile := fmt.Sprintf(TempDebugManifest, uuid.NewString()) 471 log.Info().Str("File", manifestFile).Msg("Creating manifest") 472 if err := os.WriteFile(manifestFile, []byte(manifest), os.ModePerm); err != nil { 473 return err 474 } 475 cmd := fmt.Sprintf("kubectl create -f %s", manifestFile) 476 return ExecCmd(cmd) 477 } 478 479 // DryRun generates manifest and writes it in a file 480 func (m *K8sClient) DryRun(manifest string) error { 481 manifestFile := fmt.Sprintf(TempDebugManifest, uuid.NewString()) 482 log.Info().Str("File", manifestFile).Msg("Creating manifest") 483 return os.WriteFile(manifestFile, []byte(manifest), os.ModePerm) 484 } 485 486 // CopyToPod copies src to a particular container. Destination should be in the form of a proper K8s destination path 487 // NAMESPACE/POD_NAME:folder/FILE_NAME 488 func (m *K8sClient) CopyToPod(namespace, src, destination, containername string) (*bytes.Buffer, *bytes.Buffer, *bytes.Buffer, error) { 489 m.RESTConfig.APIPath = "/api" 490 m.RESTConfig.GroupVersion = &schema.GroupVersion{Version: "v1"} // this targets the core api groups so the url path will be /api/v1 491 m.RESTConfig.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: scheme.Codecs} 492 ioStreams, in, out, errOut := genericclioptions.NewTestIOStreams() 493 494 copyOptions := cp.NewCopyOptions(ioStreams) 495 configFlags := genericclioptions.NewConfigFlags(false) 496 f := cmdutil.NewFactory(configFlags) 497 cmd := cp.NewCmdCp(f, ioStreams) 498 err := copyOptions.Complete(f, cmd, []string{src, destination}) 499 if err != nil { 500 return nil, nil, nil, err 501 } 502 copyOptions.Clientset = m.ClientSet 503 copyOptions.ClientConfig = m.RESTConfig 504 copyOptions.Container = containername 505 copyOptions.Namespace = namespace 506 507 formatted, err := regexp.MatchString(".*?\\/.*?\\:.*", destination) 508 if err != nil { 509 return nil, nil, nil, fmt.Errorf("could not parse the pod destination: %w", err) 510 } 511 if !formatted { 512 return nil, nil, nil, fmt.Errorf("pod destination string improperly formatted, see reference 'NAMESPACE/POD_NAME:folder/FILE_NAME'") 513 } 514 515 log.Info(). 516 Str("Namespace", namespace). 517 Str("Source", src). 518 Str("Destination", destination). 519 Str("Container", containername). 520 Msg("Uploading file to pod") 521 err = copyOptions.Run() 522 if err != nil { 523 return nil, nil, nil, fmt.Errorf("could not run copy operation: %w", err) 524 } 525 return in, out, errOut, nil 526 } 527 528 // ExecuteInPod is similar to kubectl exec 529 func (m *K8sClient) ExecuteInPod(namespace, podName, containerName string, command []string) ([]byte, []byte, error) { 530 log.Info().Interface("Command", command).Msg("Executing command in pod") 531 req := m.ClientSet.CoreV1().RESTClient().Post(). 532 Resource("pods"). 533 Name(podName). 534 Namespace(namespace). 535 SubResource("exec") 536 req.VersionedParams(&v1.PodExecOptions{ 537 Container: containerName, 538 Command: command, 539 Stdin: false, 540 Stdout: true, 541 Stderr: true, 542 TTY: false, 543 }, scheme.ParameterCodec) 544 545 exec, err := remotecommand.NewSPDYExecutor(m.RESTConfig, "POST", req.URL()) 546 if err != nil { 547 return []byte{}, []byte{}, err 548 } 549 550 var stdout, stderr bytes.Buffer 551 err = exec.Stream(remotecommand.StreamOptions{ 552 Stdin: nil, 553 Stdout: &stdout, 554 Stderr: &stderr, 555 }) 556 return stdout.Bytes(), stderr.Bytes(), err 557 } 558 559 func podNames(podItems *v1.PodList) []string { 560 pn := make([]string, 0) 561 for _, p := range podItems.Items { 562 pn = append(pn, p.Name) 563 } 564 return pn 565 }