github.com/kubeshop/testkube@v1.17.23/pkg/triggers/matcher.go (about)

     1  package triggers
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"regexp"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/pkg/errors"
    13  	"go.uber.org/zap"
    14  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  	"k8s.io/apimachinery/pkg/labels"
    16  
    17  	testtriggersv1 "github.com/kubeshop/testkube-operator/api/testtriggers/v1"
    18  	thttp "github.com/kubeshop/testkube/pkg/http"
    19  )
    20  
    21  const (
    22  	defaultScheme = "http"
    23  	defaultPath   = "/"
    24  )
    25  
    26  var (
    27  	ErrConditionTimeout = errors.New("timed-out waiting for trigger conditions")
    28  	ErrProbeTimeout     = errors.New("timed-out waiting for trigger probes")
    29  )
    30  
    31  func (s *Service) match(ctx context.Context, e *watcherEvent) error {
    32  	for _, status := range s.triggerStatus {
    33  		t := status.testTrigger
    34  		if t.Spec.Resource != testtriggersv1.TestTriggerResource(e.resource) {
    35  			continue
    36  		}
    37  		if !matchEventOrCause(string(t.Spec.Event), e) {
    38  			continue
    39  		}
    40  		if !matchSelector(&t.Spec.ResourceSelector, t.Namespace, e, s.logger) {
    41  			continue
    42  		}
    43  		hasConditions := t.Spec.ConditionSpec != nil && len(t.Spec.ConditionSpec.Conditions) != 0
    44  		if hasConditions && e.conditionsGetter != nil {
    45  			matched, err := s.matchConditions(ctx, e, t, s.logger)
    46  			if err != nil {
    47  				return err
    48  			}
    49  
    50  			if !matched {
    51  				continue
    52  			}
    53  		}
    54  
    55  		hasProbes := t.Spec.ProbeSpec != nil && len(t.Spec.ProbeSpec.Probes) != 0
    56  		if hasProbes {
    57  			matched, err := s.matchProbes(ctx, e, t, s.logger)
    58  			if err != nil {
    59  				return err
    60  			}
    61  
    62  			if !matched {
    63  				continue
    64  			}
    65  		}
    66  
    67  		status := s.getStatusForTrigger(t)
    68  		if t.Spec.ConcurrencyPolicy == testtriggersv1.TestTriggerConcurrencyPolicyForbid {
    69  			if status.hasActiveTests() {
    70  				s.logger.Infof(
    71  					"trigger service: matcher component: skipping trigger execution for trigger %s/%s by event %s on resource %s because it is currently running tests",
    72  					t.Namespace, t.Name, e.eventType, e.resource,
    73  				)
    74  				return nil
    75  			}
    76  		}
    77  
    78  		if t.Spec.ConcurrencyPolicy == testtriggersv1.TestTriggerConcurrencyPolicyReplace {
    79  			if status.hasActiveTests() {
    80  				s.logger.Infof(
    81  					"trigger service: matcher component: aborting trigger execution for trigger %s/%s by event %s on resource %s because it is currently running tests",
    82  					t.Namespace, t.Name, e.eventType, e.resource,
    83  				)
    84  				s.abortExecutions(ctx, t.Name, status)
    85  			}
    86  		}
    87  
    88  		s.logger.Infof("trigger service: matcher component: event %s matches trigger %s/%s for resource %s", e.eventType, t.Namespace, t.Name, e.resource)
    89  		s.logger.Infof("trigger service: matcher component: triggering %s action for %s execution", t.Spec.Action, t.Spec.Execution)
    90  		if err := s.triggerExecutor(ctx, e, t); err != nil {
    91  			return err
    92  		}
    93  	}
    94  	return nil
    95  }
    96  
    97  func matchEventOrCause(targetEvent string, event *watcherEvent) bool {
    98  	if targetEvent == string(event.eventType) {
    99  		return true
   100  	}
   101  	for _, c := range event.causes {
   102  		if targetEvent == string(c) {
   103  			return true
   104  		}
   105  	}
   106  	return false
   107  }
   108  
   109  func matchSelector(selector *testtriggersv1.TestTriggerSelector, namespace string, event *watcherEvent, logger *zap.SugaredLogger) bool {
   110  	if selector.Name != "" {
   111  		isSameName := selector.Name == event.name
   112  		isSameNamespace := selector.Namespace == event.namespace
   113  		isSameTestTriggerNamespace := selector.Namespace == "" && namespace == event.namespace
   114  		return isSameName && (isSameNamespace || isSameTestTriggerNamespace)
   115  	}
   116  	if selector.NameRegex != "" {
   117  		re, err := regexp.Compile(selector.NameRegex)
   118  		if err != nil {
   119  			logger.Errorf("error compiling %v name regex: %v", selector.NameRegex, err)
   120  			return false
   121  		}
   122  
   123  		isSameName := re.MatchString(event.name)
   124  		isSameNamespace := selector.Namespace == event.namespace
   125  		isSameTestTriggerNamespace := selector.Namespace == "" && namespace == event.namespace
   126  		return isSameName && (isSameNamespace || isSameTestTriggerNamespace)
   127  	}
   128  	if selector.LabelSelector != nil && len(event.labels) > 0 {
   129  		k8sSelector, err := v1.LabelSelectorAsSelector(selector.LabelSelector)
   130  		if err != nil {
   131  			logger.Errorf("error creating k8s selector from label selector: %v", err)
   132  			return false
   133  		}
   134  		resourceLabelSet := labels.Set(event.labels)
   135  		_, err = resourceLabelSet.AsValidatedSelector()
   136  		if err != nil {
   137  			logger.Errorf("%s %s/%s labels are invalid: %v", event.resource, event.namespace, event.name, err)
   138  			return false
   139  		}
   140  
   141  		return k8sSelector.Matches(resourceLabelSet)
   142  	}
   143  	return false
   144  }
   145  
   146  func (s *Service) matchConditions(ctx context.Context, e *watcherEvent, t *testtriggersv1.TestTrigger, logger *zap.SugaredLogger) (bool, error) {
   147  	timeout := s.defaultConditionsCheckTimeout
   148  	if t.Spec.ConditionSpec.Timeout > 0 {
   149  		timeout = time.Duration(t.Spec.ConditionSpec.Timeout) * time.Second
   150  	}
   151  	timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
   152  	defer cancel()
   153  
   154  outer:
   155  	for {
   156  		select {
   157  		case <-timeoutCtx.Done():
   158  			logger.Errorf(
   159  				"trigger service: matcher component: error waiting for conditions to match for trigger %s/%s by event %s on resource %s %s/%s"+
   160  					" because context got canceled by timeout or exit signal",
   161  				t.Namespace, t.Name, e.eventType, e.resource, e.namespace, e.name,
   162  			)
   163  			return false, errors.WithStack(ErrConditionTimeout)
   164  		default:
   165  			logger.Debugf(
   166  				"trigger service: matcher component: running conditions check iteration for %s %s/%s",
   167  				e.resource, e.namespace, e.name,
   168  			)
   169  			conditions, err := e.conditionsGetter()
   170  			if err != nil {
   171  				logger.Errorf(
   172  					"trigger service: matcher component: error getting conditions for %s %s/%s because of %v",
   173  					e.resource, e.namespace, e.name, err,
   174  				)
   175  				return false, err
   176  			}
   177  
   178  			conditionMap := make(map[string]testtriggersv1.TestTriggerCondition, len(conditions))
   179  			for _, condition := range conditions {
   180  				conditionMap[condition.Type_] = condition
   181  			}
   182  
   183  			matched := true
   184  			for _, triggerCondition := range t.Spec.ConditionSpec.Conditions {
   185  				resourceCondition, ok := conditionMap[triggerCondition.Type_]
   186  				if !ok || resourceCondition.Status == nil || triggerCondition.Status == nil ||
   187  					*resourceCondition.Status != *triggerCondition.Status ||
   188  					(triggerCondition.Reason != "" && triggerCondition.Reason != resourceCondition.Reason) ||
   189  					(triggerCondition.Ttl != 0 && triggerCondition.Ttl < resourceCondition.Ttl) {
   190  					matched = false
   191  					break
   192  				}
   193  			}
   194  
   195  			if matched {
   196  				break outer
   197  			}
   198  
   199  			delay := s.defaultConditionsCheckBackoff
   200  			if t.Spec.ConditionSpec.Delay > 0 {
   201  				delay = time.Duration(t.Spec.ConditionSpec.Delay) * time.Second
   202  			}
   203  			time.Sleep(delay)
   204  		}
   205  	}
   206  
   207  	return true, nil
   208  }
   209  
   210  func checkProbes(ctx context.Context, httpClient thttp.HttpClient, probes []testtriggersv1.TestTriggerProbe, logger *zap.SugaredLogger) bool {
   211  	var wg sync.WaitGroup
   212  	ch := make(chan bool, len(probes))
   213  	defer close(ch)
   214  
   215  	wg.Add(len(probes))
   216  	for i := range probes {
   217  		go func(probe testtriggersv1.TestTriggerProbe) {
   218  			defer wg.Done()
   219  
   220  			host := probe.Host
   221  			if probe.Port != 0 {
   222  				host = fmt.Sprintf("%s:%d", host, probe.Port)
   223  			}
   224  
   225  			if host == "" {
   226  				ch <- false
   227  				return
   228  			}
   229  
   230  			uri := url.URL{
   231  				Scheme: probe.Scheme,
   232  				Host:   host,
   233  				Path:   probe.Path,
   234  			}
   235  			request, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil)
   236  			if err != nil {
   237  				logger.Debugw("probe request creating error", "error", err)
   238  				ch <- false
   239  				return
   240  			}
   241  
   242  			for key, value := range probe.Headers {
   243  				request.Header.Set(key, value)
   244  			}
   245  
   246  			resp, err := httpClient.Do(request)
   247  			if err != nil {
   248  				logger.Debugw("probe send error", "error", err)
   249  				ch <- false
   250  				return
   251  			}
   252  			defer resp.Body.Close()
   253  
   254  			if resp.StatusCode >= 400 {
   255  				logger.Debugw("probe response with bad status code", "status", resp.StatusCode)
   256  				ch <- false
   257  				return
   258  			}
   259  
   260  			ch <- true
   261  		}(probes[i])
   262  	}
   263  
   264  	wg.Wait()
   265  
   266  	for i := 0; i < len(probes); i++ {
   267  		result := <-ch
   268  		if !result {
   269  			return false
   270  		}
   271  	}
   272  
   273  	return true
   274  }
   275  
   276  func (s *Service) matchProbes(ctx context.Context, e *watcherEvent, t *testtriggersv1.TestTrigger, logger *zap.SugaredLogger) (bool, error) {
   277  	timeout := s.defaultProbesCheckTimeout
   278  	if t.Spec.ProbeSpec.Timeout > 0 {
   279  		timeout = time.Duration(t.Spec.ProbeSpec.Timeout) * time.Second
   280  	}
   281  	timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
   282  	defer cancel()
   283  
   284  	host := ""
   285  	if e.addressGetter != nil {
   286  		var err error
   287  		host, err = e.addressGetter(timeoutCtx, s.defaultProbesCheckBackoff)
   288  		if err != nil {
   289  			logger.Errorf(
   290  				"trigger service: matcher component: error getting addess for %s %s/%s because of %v",
   291  				e.resource, e.namespace, e.name, err,
   292  			)
   293  			return false, err
   294  		}
   295  	}
   296  
   297  	for i := range t.Spec.ProbeSpec.Probes {
   298  		if t.Spec.ProbeSpec.Probes[i].Scheme == "" {
   299  			t.Spec.ProbeSpec.Probes[i].Scheme = defaultScheme
   300  		}
   301  		if t.Spec.ProbeSpec.Probes[i].Host == "" {
   302  			t.Spec.ProbeSpec.Probes[i].Host = host
   303  		}
   304  		if t.Spec.ProbeSpec.Probes[i].Path == "" {
   305  			t.Spec.ProbeSpec.Probes[i].Path = defaultPath
   306  		}
   307  	}
   308  
   309  outer:
   310  	for {
   311  		select {
   312  		case <-timeoutCtx.Done():
   313  			logger.Errorf(
   314  				"trigger service: matcher component: error waiting for probes to match for trigger %s/%s by event %s on resource %s %s/%s"+
   315  					" because context got canceled by timeout or exit signal",
   316  				t.Namespace, t.Name, e.eventType, e.resource, e.namespace, e.name,
   317  			)
   318  			return false, errors.WithStack(ErrProbeTimeout)
   319  		default:
   320  			logger.Debugf(
   321  				"trigger service: matcher component: running probes check iteration for %s %s/%s",
   322  				e.resource, e.namespace, e.name,
   323  			)
   324  
   325  			matched := checkProbes(timeoutCtx, s.httpClient, t.Spec.ProbeSpec.Probes, logger)
   326  			if matched {
   327  				break outer
   328  			}
   329  
   330  			delay := s.defaultProbesCheckBackoff
   331  			if t.Spec.ProbeSpec.Delay > 0 {
   332  				delay = time.Duration(t.Spec.ProbeSpec.Delay) * time.Second
   333  			}
   334  			time.Sleep(delay)
   335  		}
   336  	}
   337  
   338  	return true, nil
   339  }