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 }