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  }