github.com/kubeshop/testkube@v1.17.23/pkg/tcl/testworkflowstcl/testworkflowcontroller/utils.go (about) 1 // Copyright 2024 Testkube. 2 // 3 // Licensed as a Testkube Pro file under the Testkube Community 4 // License (the "License"); you may not use this file except in compliance with 5 // the License. You may obtain a copy of the License at 6 // 7 // https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt 8 9 package testworkflowcontroller 10 11 import ( 12 "context" 13 "fmt" 14 "reflect" 15 "regexp" 16 "time" 17 18 batchv1 "k8s.io/api/batch/v1" 19 corev1 "k8s.io/api/core/v1" 20 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 "k8s.io/apimachinery/pkg/watch" 22 "k8s.io/client-go/kubernetes" 23 24 "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor/constants" 25 ) 26 27 const ( 28 KubernetesLogTimeFormat = "2006-01-02T15:04:05.000000000Z" 29 ) 30 31 func IsPodDone(pod *corev1.Pod) bool { 32 return (pod.Status.Phase != corev1.PodPending && pod.Status.Phase != corev1.PodRunning) || pod.ObjectMeta.DeletionTimestamp != nil 33 } 34 35 func IsJobDone(job *batchv1.Job) bool { 36 return (job.Status.Active == 0 && (job.Status.Succeeded > 0 || job.Status.Failed > 0)) || job.ObjectMeta.DeletionTimestamp != nil 37 } 38 39 func WatchJob(ctx context.Context, clientSet kubernetes.Interface, namespace, name string, cacheSize int) Watcher[*batchv1.Job] { 40 w := newWatcher[*batchv1.Job](ctx, cacheSize) 41 42 go func() { 43 defer w.Close() 44 selector := "metadata.name=" + name 45 46 // Get initial pods 47 list, err := clientSet.BatchV1().Jobs(namespace).List(w.ctx, metav1.ListOptions{ 48 FieldSelector: selector, 49 }) 50 51 // Expose the initial value 52 if err != nil { 53 w.SendError(err) 54 return 55 } 56 if len(list.Items) == 1 { 57 job := list.Items[0] 58 w.SendValue(&job) 59 if IsJobDone(&job) { 60 return 61 } 62 } 63 64 // Start watching for changes 65 jobs, err := clientSet.BatchV1().Jobs(namespace).Watch(w.ctx, metav1.ListOptions{ 66 ResourceVersion: list.ResourceVersion, 67 FieldSelector: selector, 68 }) 69 if err != nil { 70 w.SendError(err) 71 return 72 } 73 defer jobs.Stop() 74 for { 75 // Prioritize checking for done 76 select { 77 case <-w.Done(): 78 return 79 default: 80 } 81 // Wait for results 82 select { 83 case <-w.Done(): 84 return 85 case event, ok := <-jobs.ResultChan(): 86 if !ok { 87 return 88 } 89 switch event.Type { 90 case watch.Added, watch.Modified: 91 job := event.Object.(*batchv1.Job) 92 w.SendValue(job) 93 if IsJobDone(job) { 94 return 95 } 96 case watch.Deleted: 97 return 98 } 99 } 100 } 101 }() 102 103 return w 104 } 105 106 func WatchMainPod(ctx context.Context, clientSet kubernetes.Interface, namespace, name string, cacheSize int) Watcher[*corev1.Pod] { 107 return watchPod(ctx, clientSet, namespace, ListOptions{ 108 LabelSelector: constants.ExecutionIdMainPodLabelName + "=" + name, 109 CacheSize: cacheSize, 110 }) 111 } 112 113 func WatchPodByName(ctx context.Context, clientSet kubernetes.Interface, namespace, name string, cacheSize int) Watcher[*corev1.Pod] { 114 return watchPod(ctx, clientSet, namespace, ListOptions{ 115 FieldSelector: "metadata.name=" + name, 116 CacheSize: cacheSize, 117 }) 118 } 119 120 func watchPod(ctx context.Context, clientSet kubernetes.Interface, namespace string, options ListOptions) Watcher[*corev1.Pod] { 121 w := newWatcher[*corev1.Pod](ctx, options.CacheSize) 122 123 go func() { 124 defer w.Close() 125 126 // Get initial pods 127 list, err := clientSet.CoreV1().Pods(namespace).List(w.ctx, metav1.ListOptions{ 128 FieldSelector: options.FieldSelector, 129 LabelSelector: options.LabelSelector, 130 }) 131 132 // Expose the initial value 133 if err != nil { 134 w.SendError(err) 135 return 136 } 137 if len(list.Items) == 1 { 138 pod := list.Items[0] 139 w.SendValue(&pod) 140 if IsPodDone(&pod) { 141 return 142 } 143 } 144 145 // Start watching for changes 146 pods, err := clientSet.CoreV1().Pods(namespace).Watch(w.ctx, metav1.ListOptions{ 147 ResourceVersion: list.ResourceVersion, 148 FieldSelector: options.FieldSelector, 149 LabelSelector: options.LabelSelector, 150 }) 151 if err != nil { 152 w.SendError(err) 153 return 154 } 155 defer pods.Stop() 156 for { 157 // Prioritize checking for done 158 select { 159 case <-w.Done(): 160 return 161 default: 162 } 163 // Wait for results 164 select { 165 case <-w.Done(): 166 return 167 case event, ok := <-pods.ResultChan(): 168 if !ok { 169 return 170 } 171 switch event.Type { 172 case watch.Added, watch.Modified: 173 pod := event.Object.(*corev1.Pod) 174 w.SendValue(pod) 175 if IsPodDone(pod) { 176 return 177 } 178 case watch.Deleted: 179 return 180 } 181 } 182 } 183 }() 184 185 return w 186 } 187 188 type ListOptions struct { 189 FieldSelector string 190 LabelSelector string 191 TypeMeta metav1.TypeMeta 192 CacheSize int 193 } 194 195 func GetEventContainerName(event *corev1.Event) string { 196 regex := regexp.MustCompile(`^spec\.(?:initContainers|containers)\{([^]]+)}`) 197 path := event.InvolvedObject.FieldPath 198 if regex.Match([]byte(path)) { 199 name := regex.ReplaceAllString(event.InvolvedObject.FieldPath, "$1") 200 return name 201 } 202 return "" 203 } 204 205 func WatchContainerEvents(ctx context.Context, podEvents Watcher[*corev1.Event], name string, cacheSize int, includePodWarnings bool) Watcher[*corev1.Event] { 206 w := newWatcher[*corev1.Event](ctx, cacheSize) 207 go func() { 208 stream := podEvents.Stream(ctx) 209 defer stream.Stop() 210 defer w.Close() 211 for { 212 select { 213 case <-w.Done(): 214 return 215 case v, ok := <-stream.Channel(): 216 if ok { 217 if v.Error != nil { 218 w.SendError(v.Error) 219 } else if GetEventContainerName(v.Value) == name { 220 w.SendValue(v.Value) 221 } else if includePodWarnings && v.Value.Type == "Warning" { 222 w.SendValue(v.Value) 223 } 224 } else { 225 return 226 } 227 } 228 } 229 }() 230 return w 231 } 232 233 func WatchContainerStatus(ctx context.Context, pod Watcher[*corev1.Pod], containerName string, cacheSize int) Watcher[corev1.ContainerStatus] { 234 w := newWatcher[corev1.ContainerStatus](ctx, cacheSize) 235 236 go func() { 237 stream := pod.Stream(ctx) 238 defer stream.Stop() 239 defer w.Close() 240 var prev corev1.ContainerStatus 241 for { 242 select { 243 case <-w.Done(): 244 return 245 case p, ok := <-stream.Channel(): 246 if !ok { 247 return 248 } 249 if p.Error != nil { 250 w.SendError(p.Error) 251 continue 252 } 253 if p.Value == nil { 254 continue 255 } 256 for _, s := range append(p.Value.Status.InitContainerStatuses, p.Value.Status.ContainerStatuses...) { 257 if s.Name == containerName { 258 if !reflect.DeepEqual(s, prev) { 259 prev = s 260 w.SendValue(s) 261 } 262 break 263 } 264 } 265 if IsPodDone(p.Value) { 266 return 267 } 268 } 269 } 270 }() 271 272 return w 273 } 274 275 func WatchPodEventsByName(ctx context.Context, clientSet kubernetes.Interface, namespace, name string, cacheSize int) Watcher[*corev1.Event] { 276 return WatchEvents(ctx, clientSet, namespace, ListOptions{ 277 FieldSelector: "involvedObject.name=" + name, 278 TypeMeta: metav1.TypeMeta{Kind: "Pod"}, 279 CacheSize: cacheSize, 280 }) 281 } 282 283 func WatchPodEventsByPodWatcher(ctx context.Context, clientSet kubernetes.Interface, namespace string, pod Watcher[*corev1.Pod], cacheSize int) Watcher[*corev1.Event] { 284 w := newWatcher[*corev1.Event](ctx, cacheSize) 285 286 go func() { 287 defer w.Close() 288 289 v, ok := <-pod.Any(ctx) 290 if v.Error != nil { 291 w.SendError(v.Error) 292 return 293 } 294 if !ok || v.Value == nil { 295 return 296 } 297 _, wch := watchEvents(clientSet, namespace, ListOptions{ 298 FieldSelector: "involvedObject.name=" + v.Value.Name, 299 TypeMeta: metav1.TypeMeta{Kind: "Pod"}, 300 }, w) 301 302 // Wait for all immediate events 303 <-wch 304 305 // Adds missing "Started" events. 306 // It may have duplicated "Started", but better than no events. 307 // @see {@link https://github.com/kubernetes/kubernetes/issues/122904#issuecomment-1944387021} 308 started := map[string]bool{} 309 for p := range pod.Stream(ctx).Channel() { 310 for i, s := range append(p.Value.Status.InitContainerStatuses, p.Value.Status.ContainerStatuses...) { 311 if !started[s.Name] && (s.State.Running != nil || s.State.Terminated != nil) { 312 ts := metav1.Time{Time: time.Now()} 313 if s.State.Running != nil { 314 ts = s.State.Running.StartedAt 315 } else if s.State.Terminated != nil { 316 ts = s.State.Terminated.StartedAt 317 } 318 started[s.Name] = true 319 fieldPath := fmt.Sprintf("spec.containers{%s}", s.Name) 320 if i >= len(p.Value.Status.InitContainerStatuses) { 321 fieldPath = fmt.Sprintf("spec.initContainers{%s}", s.Name) 322 } 323 w.SendValue(&corev1.Event{ 324 ObjectMeta: metav1.ObjectMeta{CreationTimestamp: ts}, 325 FirstTimestamp: ts, 326 Type: "Normal", 327 Reason: "Started", 328 Message: fmt.Sprintf("Started container %s", s.Name), 329 InvolvedObject: corev1.ObjectReference{FieldPath: fieldPath}, 330 }) 331 } 332 } 333 } 334 }() 335 336 return w 337 } 338 339 func WatchJobEvents(ctx context.Context, clientSet kubernetes.Interface, namespace, name string, cacheSize int) Watcher[*corev1.Event] { 340 return WatchEvents(ctx, clientSet, namespace, ListOptions{ 341 FieldSelector: "involvedObject.name=" + name, 342 TypeMeta: metav1.TypeMeta{Kind: "Job"}, 343 CacheSize: cacheSize, 344 }) 345 } 346 347 func WatchJobPreEvents(ctx context.Context, jobEvents Watcher[*corev1.Event], cacheSize int) Watcher[*corev1.Event] { 348 w := newWatcher[*corev1.Event](ctx, cacheSize) 349 go func() { 350 defer w.Close() 351 stream := jobEvents.Stream(ctx) 352 defer stream.Stop() 353 354 for { 355 select { 356 case <-w.Done(): 357 return 358 case v := <-stream.Channel(): 359 if v.Error != nil { 360 w.SendError(v.Error) 361 } else { 362 w.SendValue(v.Value) 363 if v.Value.Reason == "SuccessfulCreate" { 364 return 365 } 366 } 367 } 368 } 369 }() 370 return w 371 } 372 373 func WatchEvents(ctx context.Context, clientSet kubernetes.Interface, namespace string, options ListOptions) Watcher[*corev1.Event] { 374 w, _ := watchEvents(clientSet, namespace, options, newWatcher[*corev1.Event](ctx, options.CacheSize)) 375 return w 376 } 377 378 func watchEvents(clientSet kubernetes.Interface, namespace string, options ListOptions, w *watcher[*corev1.Event]) (Watcher[*corev1.Event], chan struct{}) { 379 initCh := make(chan struct{}) 380 go func() { 381 defer w.Close() 382 383 // Get initial events 384 list, err := clientSet.CoreV1().Events(namespace).List(w.ctx, metav1.ListOptions{ 385 FieldSelector: options.FieldSelector, 386 LabelSelector: options.LabelSelector, 387 TypeMeta: options.TypeMeta, 388 }) 389 390 // Expose the initial value 391 if err != nil { 392 w.SendError(err) 393 close(initCh) 394 return 395 } 396 for _, event := range list.Items { 397 w.SendValue(event.DeepCopy()) 398 } 399 close(initCh) 400 401 // Start watching for changes 402 events, err := clientSet.CoreV1().Events(namespace).Watch(w.ctx, metav1.ListOptions{ 403 ResourceVersion: list.ResourceVersion, 404 FieldSelector: options.FieldSelector, 405 LabelSelector: options.LabelSelector, 406 TypeMeta: options.TypeMeta, 407 }) 408 if err != nil { 409 w.SendError(err) 410 return 411 } 412 defer events.Stop() 413 for { 414 // Prioritize checking for done 415 select { 416 case <-w.Done(): 417 return 418 default: 419 } 420 // Wait for results 421 select { 422 case <-w.Done(): 423 return 424 case event, ok := <-events.ResultChan(): 425 if !ok { 426 return 427 } 428 if event.Object == nil { 429 continue 430 } 431 switch event.Type { 432 case watch.Added, watch.Modified: 433 w.SendValue(event.Object.(*corev1.Event)) 434 } 435 } 436 } 437 }() 438 439 return w, initCh 440 }