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

     1  package triggers
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  	"time"
     9  
    10  	"go.uber.org/zap"
    11  	"k8s.io/client-go/kubernetes"
    12  
    13  	testsv3 "github.com/kubeshop/testkube-operator/api/tests/v3"
    14  	testsuitev3 "github.com/kubeshop/testkube-operator/api/testsuite/v3"
    15  	testtriggersv1 "github.com/kubeshop/testkube-operator/api/testtriggers/v1"
    16  	executorsclientv1 "github.com/kubeshop/testkube-operator/pkg/client/executors/v1"
    17  	testsclientv3 "github.com/kubeshop/testkube-operator/pkg/client/tests/v3"
    18  	testsuitesclientv3 "github.com/kubeshop/testkube-operator/pkg/client/testsuites/v3"
    19  	testkubeclientsetv1 "github.com/kubeshop/testkube-operator/pkg/clientset/versioned"
    20  	"github.com/kubeshop/testkube/internal/app/api/metrics"
    21  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    22  	"github.com/kubeshop/testkube/pkg/event/bus"
    23  	"github.com/kubeshop/testkube/pkg/executor/client"
    24  	"github.com/kubeshop/testkube/pkg/http"
    25  	"github.com/kubeshop/testkube/pkg/repository/config"
    26  	"github.com/kubeshop/testkube/pkg/repository/result"
    27  	"github.com/kubeshop/testkube/pkg/repository/testresult"
    28  	"github.com/kubeshop/testkube/pkg/scheduler"
    29  	"github.com/kubeshop/testkube/pkg/telemetry"
    30  	"github.com/kubeshop/testkube/pkg/utils"
    31  	"github.com/kubeshop/testkube/pkg/version"
    32  )
    33  
    34  const (
    35  	defaultScraperInterval        = 5 * time.Second
    36  	defaultLeaseCheckInterval     = 5 * time.Second
    37  	defaultMaxLeaseDuration       = 1 * time.Minute
    38  	defaultConditionsCheckBackoff = 1 * time.Second
    39  	defaultConditionsCheckTimeout = 60 * time.Second
    40  	defaultProbesCheckBackoff     = 1 * time.Second
    41  	defaultProbesCheckTimeout     = 60 * time.Second
    42  	defaultClusterID              = "testkube-api"
    43  	defaultIdentifierFormat       = "testkube-api-%s"
    44  )
    45  
    46  type Service struct {
    47  	informers                     *k8sInformers
    48  	leaseBackend                  LeaseBackend
    49  	identifier                    string
    50  	clusterID                     string
    51  	triggerExecutor               ExecutorF
    52  	scraperInterval               time.Duration
    53  	leaseCheckInterval            time.Duration
    54  	maxLeaseDuration              time.Duration
    55  	defaultConditionsCheckTimeout time.Duration
    56  	defaultConditionsCheckBackoff time.Duration
    57  	defaultProbesCheckTimeout     time.Duration
    58  	defaultProbesCheckBackoff     time.Duration
    59  	watchFromDate                 time.Time
    60  	triggerStatus                 map[statusKey]*triggerStatus
    61  	scheduler                     *scheduler.Scheduler
    62  	clientset                     kubernetes.Interface
    63  	testKubeClientset             testkubeclientsetv1.Interface
    64  	testSuitesClient              testsuitesclientv3.Interface
    65  	testsClient                   testsclientv3.Interface
    66  	resultRepository              result.Repository
    67  	testResultRepository          testresult.Repository
    68  	logger                        *zap.SugaredLogger
    69  	configMap                     config.Repository
    70  	executorsClient               executorsclientv1.Interface
    71  	httpClient                    http.HttpClient
    72  	testExecutor                  client.Executor
    73  	eventsBus                     bus.Bus
    74  	metrics                       metrics.Metrics
    75  	testkubeNamespace             string
    76  	watcherNamespaces             []string
    77  	disableSecretCreation         bool
    78  }
    79  
    80  type Option func(*Service)
    81  
    82  func NewService(
    83  	scheduler *scheduler.Scheduler,
    84  	clientset kubernetes.Interface,
    85  	testKubeClientset testkubeclientsetv1.Interface,
    86  	testSuitesClient testsuitesclientv3.Interface,
    87  	testsClient testsclientv3.Interface,
    88  	resultRepository result.Repository,
    89  	testResultRepository testresult.Repository,
    90  	leaseBackend LeaseBackend,
    91  	logger *zap.SugaredLogger,
    92  	configMap config.Repository,
    93  	executorsClient executorsclientv1.Interface,
    94  	testExecutor client.Executor,
    95  	eventsBus bus.Bus,
    96  	metrics metrics.Metrics,
    97  	opts ...Option,
    98  ) *Service {
    99  	identifier := fmt.Sprintf(defaultIdentifierFormat, utils.RandAlphanum(10))
   100  	s := &Service{
   101  		identifier:                    identifier,
   102  		clusterID:                     defaultClusterID,
   103  		scraperInterval:               defaultScraperInterval,
   104  		leaseCheckInterval:            defaultLeaseCheckInterval,
   105  		maxLeaseDuration:              defaultMaxLeaseDuration,
   106  		defaultConditionsCheckTimeout: defaultConditionsCheckTimeout,
   107  		defaultConditionsCheckBackoff: defaultConditionsCheckBackoff,
   108  		defaultProbesCheckTimeout:     defaultProbesCheckTimeout,
   109  		defaultProbesCheckBackoff:     defaultProbesCheckBackoff,
   110  		scheduler:                     scheduler,
   111  		clientset:                     clientset,
   112  		testKubeClientset:             testKubeClientset,
   113  		testSuitesClient:              testSuitesClient,
   114  		testsClient:                   testsClient,
   115  		resultRepository:              resultRepository,
   116  		testResultRepository:          testResultRepository,
   117  		leaseBackend:                  leaseBackend,
   118  		logger:                        logger,
   119  		configMap:                     configMap,
   120  		executorsClient:               executorsClient,
   121  		testExecutor:                  testExecutor,
   122  		eventsBus:                     eventsBus,
   123  		metrics:                       metrics,
   124  		httpClient:                    http.NewClient(),
   125  		watchFromDate:                 time.Now(),
   126  		triggerStatus:                 make(map[statusKey]*triggerStatus),
   127  	}
   128  	if s.triggerExecutor == nil {
   129  		s.triggerExecutor = s.execute
   130  	}
   131  
   132  	for _, opt := range opts {
   133  		opt(s)
   134  	}
   135  
   136  	s.informers = newK8sInformers(clientset, testKubeClientset, s.testkubeNamespace, s.watcherNamespaces)
   137  
   138  	return s
   139  }
   140  
   141  func WithIdentifier(id string) Option {
   142  	return func(s *Service) {
   143  		s.identifier = id
   144  	}
   145  }
   146  
   147  func WithHostnameIdentifier() Option {
   148  	return func(s *Service) {
   149  		identifier, err := os.Hostname()
   150  		if err == nil {
   151  			s.identifier = identifier
   152  		}
   153  	}
   154  }
   155  
   156  func WithClusterID(id string) Option {
   157  	return func(s *Service) {
   158  		s.clusterID = id
   159  	}
   160  }
   161  
   162  func WithWatchFromDate(from time.Time) Option {
   163  	return func(s *Service) {
   164  		s.watchFromDate = from
   165  	}
   166  }
   167  
   168  func WithLeaseCheckerInterval(interval time.Duration) Option {
   169  	return func(s *Service) {
   170  		s.leaseCheckInterval = interval
   171  	}
   172  }
   173  
   174  func WithScraperInterval(interval time.Duration) Option {
   175  	return func(s *Service) {
   176  		s.scraperInterval = interval
   177  	}
   178  }
   179  
   180  func WithExecutor(triggerExecutor ExecutorF) Option {
   181  	return func(s *Service) {
   182  		s.triggerExecutor = triggerExecutor
   183  	}
   184  }
   185  
   186  func WithTestkubeNamespace(namespace string) Option {
   187  	return func(s *Service) {
   188  		s.testkubeNamespace = namespace
   189  	}
   190  }
   191  
   192  func WithWatcherNamespaces(namespaces string) Option {
   193  	return func(s *Service) {
   194  		for _, namespace := range strings.Split(namespaces, ",") {
   195  			value := strings.TrimSpace(namespace)
   196  			if value != "" {
   197  				s.watcherNamespaces = append(s.watcherNamespaces, value)
   198  			}
   199  		}
   200  	}
   201  }
   202  
   203  func WithDisableSecretCreation(disableSecretCreation bool) Option {
   204  	return func(s *Service) {
   205  		s.disableSecretCreation = disableSecretCreation
   206  	}
   207  }
   208  
   209  func (s *Service) Run(ctx context.Context) {
   210  	leaseChan := make(chan bool)
   211  
   212  	go s.runLeaseChecker(ctx, leaseChan)
   213  
   214  	go s.runWatcher(ctx, leaseChan)
   215  
   216  	go s.runExecutionScraper(ctx)
   217  }
   218  
   219  func (s *Service) addTrigger(t *testtriggersv1.TestTrigger) {
   220  	key := newStatusKey(t.Namespace, t.Name)
   221  	s.triggerStatus[key] = newTriggerStatus(t)
   222  }
   223  
   224  func (s *Service) updateTrigger(target *testtriggersv1.TestTrigger) {
   225  	key := newStatusKey(target.Namespace, target.Name)
   226  	if s.triggerStatus[key] != nil {
   227  		s.triggerStatus[key].testTrigger = target
   228  	} else {
   229  		s.triggerStatus[key] = newTriggerStatus(target)
   230  	}
   231  }
   232  
   233  func (s *Service) removeTrigger(target *testtriggersv1.TestTrigger) {
   234  	key := newStatusKey(target.Namespace, target.Name)
   235  	delete(s.triggerStatus, key)
   236  }
   237  
   238  func (s *Service) addTest(test *testsv3.Test) {
   239  	ctx := context.Background()
   240  	telemetryEnabled, err := s.configMap.GetTelemetryEnabled(ctx)
   241  	if err != nil {
   242  		s.logger.Debugw("getting telemetry enabled error", "error", err)
   243  	}
   244  
   245  	if !telemetryEnabled {
   246  		return
   247  	}
   248  
   249  	clusterID, err := s.configMap.GetUniqueClusterId(ctx)
   250  	if err != nil {
   251  		s.logger.Debugw("getting cluster id error", "error", err)
   252  	}
   253  
   254  	host, err := os.Hostname()
   255  	if err != nil {
   256  		s.logger.Debugw("getting hostname error", "hostname", host, "error", err)
   257  	}
   258  
   259  	var dataSource string
   260  	if test.Spec.Content != nil {
   261  		dataSource = string(test.Spec.Content.Type_)
   262  	}
   263  
   264  	out, err := telemetry.SendCreateEvent("testkube_api_create_test", telemetry.CreateParams{
   265  		AppVersion: version.Version,
   266  		DataSource: dataSource,
   267  		Host:       host,
   268  		ClusterID:  clusterID,
   269  		TestType:   test.Spec.Type_,
   270  		TestSource: test.Spec.Source,
   271  	})
   272  	if err != nil {
   273  		s.logger.Debugw("sending create test telemetry event error", "error", err)
   274  	} else {
   275  		s.logger.Debugw("sending create test telemetry event", "output", out)
   276  	}
   277  
   278  	if test.Labels == nil {
   279  		test.Labels = make(map[string]string)
   280  	}
   281  
   282  	test.Labels[testkube.TestLabelTestType] = utils.SanitizeName(test.Spec.Type_)
   283  	executorCR, err := s.executorsClient.GetByType(test.Spec.Type_)
   284  	if err == nil {
   285  		test.Labels[testkube.TestLabelExecutor] = executorCR.Name
   286  	} else {
   287  		s.logger.Debugw("can't get executor spec", "error", err)
   288  	}
   289  
   290  	if _, err = s.testsClient.Update(test, s.disableSecretCreation); err != nil {
   291  		s.logger.Debugw("can't update test spec", "error", err)
   292  	}
   293  }
   294  
   295  func (s *Service) updateTest(test *testsv3.Test) {
   296  	changed := false
   297  	if test.Labels == nil {
   298  		test.Labels = make(map[string]string)
   299  	}
   300  
   301  	testType := utils.SanitizeName(test.Spec.Type_)
   302  	if test.Labels[testkube.TestLabelTestType] != testType {
   303  		test.Labels[testkube.TestLabelTestType] = testType
   304  		changed = true
   305  	}
   306  
   307  	executorCR, err := s.executorsClient.GetByType(test.Spec.Type_)
   308  	if err == nil {
   309  		if test.Labels[testkube.TestLabelExecutor] != executorCR.Name {
   310  			test.Labels[testkube.TestLabelExecutor] = executorCR.Name
   311  			changed = true
   312  		}
   313  	} else {
   314  		s.logger.Debugw("can't get executor spec", "error", err)
   315  	}
   316  
   317  	if changed {
   318  		if _, err = s.testsClient.Update(test, s.disableSecretCreation); err != nil {
   319  			s.logger.Debugw("can't update test spec", "error", err)
   320  		}
   321  	}
   322  }
   323  
   324  func (s *Service) addTestSuite(testSuite *testsuitev3.TestSuite) {
   325  	ctx := context.Background()
   326  	telemetryEnabled, err := s.configMap.GetTelemetryEnabled(ctx)
   327  	if err != nil {
   328  		s.logger.Debugw("getting telemetry enabled error", "error", err)
   329  	}
   330  
   331  	if !telemetryEnabled {
   332  		return
   333  	}
   334  
   335  	clusterID, err := s.configMap.GetUniqueClusterId(ctx)
   336  	if err != nil {
   337  		s.logger.Debugw("getting cluster id error", "error", err)
   338  	}
   339  
   340  	host, err := os.Hostname()
   341  	if err != nil {
   342  		s.logger.Debugw("getting hostname error", "hostname", host, "error", err)
   343  	}
   344  
   345  	out, err := telemetry.SendCreateEvent("testkube_api_create_test_suite", telemetry.CreateParams{
   346  		AppVersion:     version.Version,
   347  		Host:           host,
   348  		ClusterID:      clusterID,
   349  		TestSuiteSteps: int32(len(testSuite.Spec.Before) + len(testSuite.Spec.Steps) + len(testSuite.Spec.After)),
   350  	})
   351  	if err != nil {
   352  		s.logger.Debugw("sending create test suite telemetry event error", "error", err)
   353  	} else {
   354  		s.logger.Debugw("sending create test suite telemetry event", "output", out)
   355  	}
   356  }