github.com/kubeshop/testkube@v1.17.23/internal/app/api/v1/server.go (about)

     1  package v1
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"net"
     7  	"os"
     8  	"reflect"
     9  	"strconv"
    10  	"sync"
    11  	"syscall"
    12  	"time"
    13  
    14  	"github.com/pkg/errors"
    15  
    16  	"github.com/kubeshop/testkube/internal/common"
    17  	"github.com/kubeshop/testkube/internal/config"
    18  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    19  	repoConfig "github.com/kubeshop/testkube/pkg/repository/config"
    20  	"github.com/kubeshop/testkube/pkg/tcl/checktcl"
    21  
    22  	"github.com/kubeshop/testkube/pkg/version"
    23  
    24  	"github.com/kubeshop/testkube/pkg/datefilter"
    25  	"github.com/kubeshop/testkube/pkg/repository/result"
    26  	"github.com/kubeshop/testkube/pkg/repository/testresult"
    27  
    28  	"k8s.io/client-go/kubernetes"
    29  
    30  	"github.com/gofiber/fiber/v2"
    31  	"github.com/gofiber/fiber/v2/middleware/cors"
    32  	"github.com/gofiber/fiber/v2/middleware/proxy"
    33  	"github.com/kelseyhightower/envconfig"
    34  
    35  	executorsclientv1 "github.com/kubeshop/testkube-operator/pkg/client/executors/v1"
    36  	templatesclientv1 "github.com/kubeshop/testkube-operator/pkg/client/templates/v1"
    37  	testsclientv3 "github.com/kubeshop/testkube-operator/pkg/client/tests/v3"
    38  	testsourcesclientv1 "github.com/kubeshop/testkube-operator/pkg/client/testsources/v1"
    39  	testsuitesclientv3 "github.com/kubeshop/testkube-operator/pkg/client/testsuites/v3"
    40  	testkubeclientset "github.com/kubeshop/testkube-operator/pkg/clientset/versioned"
    41  	"github.com/kubeshop/testkube/internal/app/api/metrics"
    42  	"github.com/kubeshop/testkube/pkg/event"
    43  	"github.com/kubeshop/testkube/pkg/event/bus"
    44  	"github.com/kubeshop/testkube/pkg/event/kind/cdevent"
    45  	"github.com/kubeshop/testkube/pkg/event/kind/slack"
    46  	"github.com/kubeshop/testkube/pkg/event/kind/webhook"
    47  	ws "github.com/kubeshop/testkube/pkg/event/kind/websocket"
    48  	"github.com/kubeshop/testkube/pkg/executor/client"
    49  	"github.com/kubeshop/testkube/pkg/featureflags"
    50  	logsclient "github.com/kubeshop/testkube/pkg/logs/client"
    51  	"github.com/kubeshop/testkube/pkg/oauth"
    52  	"github.com/kubeshop/testkube/pkg/scheduler"
    53  	"github.com/kubeshop/testkube/pkg/secret"
    54  	"github.com/kubeshop/testkube/pkg/server"
    55  	"github.com/kubeshop/testkube/pkg/storage"
    56  	"github.com/kubeshop/testkube/pkg/telemetry"
    57  	"github.com/kubeshop/testkube/pkg/utils/text"
    58  )
    59  
    60  const (
    61  	HeartbeatInterval    = time.Hour
    62  	DefaultHttpBodyLimit = 1 * 1024 * 1024 * 1024 // 1GB - needed for file uploads
    63  )
    64  
    65  func NewTestkubeAPI(
    66  	namespace string,
    67  	testExecutionResults result.Repository,
    68  	testsuiteExecutionsResults testresult.Repository,
    69  	testsClient *testsclientv3.TestsClient,
    70  	executorsClient *executorsclientv1.ExecutorsClient,
    71  	testsuitesClient *testsuitesclientv3.TestSuitesClient,
    72  	secretClient *secret.Client,
    73  	webhookClient *executorsclientv1.WebhooksClient,
    74  	clientset kubernetes.Interface,
    75  	testkubeClientset testkubeclientset.Interface,
    76  	testsourcesClient *testsourcesclientv1.TestSourcesClient,
    77  	configMap repoConfig.Repository,
    78  	clusterId string,
    79  	eventsEmitter *event.Emitter,
    80  	executor client.Executor,
    81  	containerExecutor client.Executor,
    82  	metrics metrics.Metrics,
    83  	scheduler *scheduler.Scheduler,
    84  	slackLoader *slack.SlackLoader,
    85  	storage storage.Client,
    86  	graphqlPort string,
    87  	artifactsStorage storage.ArtifactsStorage,
    88  	templatesClient *templatesclientv1.TemplatesClient,
    89  	cdeventsTarget string,
    90  	dashboardURI string,
    91  	helmchartVersion string,
    92  	mode string,
    93  	eventsBus bus.Bus,
    94  	enableSecretsEndpoint bool,
    95  	ff featureflags.FeatureFlags,
    96  	logsStream logsclient.Stream,
    97  	logGrpcClient logsclient.StreamGetter,
    98  	subscriptionChecker checktcl.SubscriptionChecker,
    99  	disableSecretCreation bool,
   100  	serviceAccountNames map[string]string,
   101  ) TestkubeAPI {
   102  
   103  	var httpConfig server.Config
   104  	err := envconfig.Process("APISERVER", &httpConfig)
   105  	// Do we want to panic here or just ignore the error
   106  	if err != nil {
   107  		panic(err)
   108  	}
   109  
   110  	httpConfig.ClusterID = clusterId
   111  	httpConfig.Http.BodyLimit = httpConfig.HttpBodyLimit
   112  	if httpConfig.HttpBodyLimit == 0 {
   113  		httpConfig.Http.BodyLimit = DefaultHttpBodyLimit
   114  	}
   115  
   116  	s := TestkubeAPI{
   117  		HTTPServer:            server.NewServer(httpConfig),
   118  		TestExecutionResults:  testsuiteExecutionsResults,
   119  		ExecutionResults:      testExecutionResults,
   120  		TestsClient:           testsClient,
   121  		ExecutorsClient:       executorsClient,
   122  		SecretClient:          secretClient,
   123  		Clientset:             clientset,
   124  		TestsSuitesClient:     testsuitesClient,
   125  		TestKubeClientset:     testkubeClientset,
   126  		Metrics:               metrics,
   127  		Events:                eventsEmitter,
   128  		WebhooksClient:        webhookClient,
   129  		TestSourcesClient:     testsourcesClient,
   130  		Namespace:             namespace,
   131  		ConfigMap:             configMap,
   132  		Executor:              executor,
   133  		ContainerExecutor:     containerExecutor,
   134  		scheduler:             scheduler,
   135  		slackLoader:           slackLoader,
   136  		Storage:               storage,
   137  		graphqlPort:           graphqlPort,
   138  		ArtifactsStorage:      artifactsStorage,
   139  		TemplatesClient:       templatesClient,
   140  		dashboardURI:          dashboardURI,
   141  		helmchartVersion:      helmchartVersion,
   142  		mode:                  mode,
   143  		eventsBus:             eventsBus,
   144  		enableSecretsEndpoint: enableSecretsEndpoint,
   145  		featureFlags:          ff,
   146  		logsStream:            logsStream,
   147  		logGrpcClient:         logGrpcClient,
   148  		SubscriptionChecker:   subscriptionChecker,
   149  		disableSecretCreation: disableSecretCreation,
   150  		LabelSources:          common.Ptr(make([]LabelSource, 0)),
   151  		serviceAccountNames:   serviceAccountNames,
   152  	}
   153  
   154  	// will be reused in websockets handler
   155  	s.WebsocketLoader = ws.NewWebsocketLoader()
   156  
   157  	s.Events.Loader.Register(webhook.NewWebhookLoader(s.Log, webhookClient, templatesClient))
   158  	s.Events.Loader.Register(s.WebsocketLoader)
   159  	s.Events.Loader.Register(s.slackLoader)
   160  
   161  	if cdeventsTarget != "" {
   162  		cdeventLoader, err := cdevent.NewCDEventLoader(cdeventsTarget, clusterId, namespace, dashboardURI, testkube.AllEventTypes)
   163  		if err == nil {
   164  			s.Events.Loader.Register(cdeventLoader)
   165  		} else {
   166  			s.Log.Debug("cdevents init error", "error", err.Error())
   167  		}
   168  	}
   169  
   170  	s.InitEnvs()
   171  	s.InitRoutes()
   172  
   173  	return s
   174  }
   175  
   176  type TestkubeAPI struct {
   177  	server.HTTPServer
   178  	ExecutionResults      result.Repository
   179  	TestExecutionResults  testresult.Repository
   180  	Executor              client.Executor
   181  	ContainerExecutor     client.Executor
   182  	TestsSuitesClient     *testsuitesclientv3.TestSuitesClient
   183  	TestsClient           *testsclientv3.TestsClient
   184  	ExecutorsClient       *executorsclientv1.ExecutorsClient
   185  	SecretClient          *secret.Client
   186  	WebhooksClient        *executorsclientv1.WebhooksClient
   187  	TestKubeClientset     testkubeclientset.Interface
   188  	TestSourcesClient     *testsourcesclientv1.TestSourcesClient
   189  	Metrics               metrics.Metrics
   190  	Storage               storage.Client
   191  	storageParams         storageParams
   192  	Namespace             string
   193  	oauthParams           oauthParams
   194  	WebsocketLoader       *ws.WebsocketLoader
   195  	Events                *event.Emitter
   196  	ConfigMap             repoConfig.Repository
   197  	scheduler             *scheduler.Scheduler
   198  	Clientset             kubernetes.Interface
   199  	slackLoader           *slack.SlackLoader
   200  	graphqlPort           string
   201  	ArtifactsStorage      storage.ArtifactsStorage
   202  	TemplatesClient       *templatesclientv1.TemplatesClient
   203  	dashboardURI          string
   204  	helmchartVersion      string
   205  	mode                  string
   206  	eventsBus             bus.Bus
   207  	enableSecretsEndpoint bool
   208  	featureFlags          featureflags.FeatureFlags
   209  	logsStream            logsclient.Stream
   210  	logGrpcClient         logsclient.StreamGetter
   211  	proContext            *config.ProContext
   212  	SubscriptionChecker   checktcl.SubscriptionChecker
   213  	disableSecretCreation bool
   214  	LabelSources          *[]LabelSource
   215  	serviceAccountNames   map[string]string
   216  }
   217  
   218  type storageParams struct {
   219  	SSL             bool   `envconfig:"STORAGE_SSL" default:"false"`
   220  	SkipVerify      bool   `envconfig:"STORAGE_SKIP_VERIFY" default:"false"`
   221  	CertFile        string `envconfig:"STORAGE_CERT_FILE"`
   222  	KeyFile         string `envconfig:"STORAGE_KEY_FILE"`
   223  	CAFile          string `envconfig:"STORAGE_CA_FILE"`
   224  	Endpoint        string
   225  	AccessKeyId     string
   226  	SecretAccessKey string
   227  	Region          string
   228  	Token           string
   229  	Bucket          string
   230  }
   231  
   232  type oauthParams struct {
   233  	ClientID     string
   234  	ClientSecret string
   235  	Provider     oauth.ProviderType
   236  	Scopes       string
   237  }
   238  
   239  func (s *TestkubeAPI) WithFeatureFlags(ff featureflags.FeatureFlags) *TestkubeAPI {
   240  	s.featureFlags = ff
   241  	return s
   242  }
   243  
   244  type LabelSource interface {
   245  	ListLabels() (map[string][]string, error)
   246  }
   247  
   248  func (s *TestkubeAPI) WithLabelSources(l ...LabelSource) {
   249  	*s.LabelSources = append(*s.LabelSources, l...)
   250  }
   251  
   252  // SendTelemetryStartEvent sends anonymous start event to telemetry trackers
   253  func (s TestkubeAPI) SendTelemetryStartEvent(ctx context.Context, ch chan struct{}) {
   254  	go func() {
   255  		defer func() {
   256  			ch <- struct{}{}
   257  		}()
   258  
   259  		telemetryEnabled, err := s.ConfigMap.GetTelemetryEnabled(ctx)
   260  		if err != nil {
   261  			s.Log.Errorw("error getting config map", "error", err)
   262  		}
   263  
   264  		if !telemetryEnabled {
   265  			return
   266  		}
   267  
   268  		out, err := telemetry.SendServerStartEvent(s.Config.ClusterID, version.Version)
   269  		if err != nil {
   270  			s.Log.Debug("telemetry send error", "error", err.Error())
   271  		} else {
   272  			s.Log.Debugw("sending telemetry server start event", "output", out)
   273  		}
   274  	}()
   275  }
   276  
   277  // InitEnvs initializes api server settings
   278  func (s *TestkubeAPI) InitEnvs() {
   279  	if err := envconfig.Process("STORAGE", &s.storageParams); err != nil {
   280  		s.Log.Infow("Processing STORAGE environment config", err)
   281  	}
   282  
   283  	if err := envconfig.Process("TESTKUBE_OAUTH", &s.oauthParams); err != nil {
   284  		s.Log.Infow("Processing TESTKUBE_OAUTH environment config", err)
   285  	}
   286  }
   287  
   288  func (s *TestkubeAPI) InitRoutes() {
   289  	s.Routes.Static("/api-docs", "./api/v1")
   290  	s.Routes.Use(cors.New())
   291  	s.Routes.Use(s.AuthHandler())
   292  
   293  	s.Routes.Get("/info", s.InfoHandler())
   294  	s.Routes.Get("/routes", s.RoutesHandler())
   295  	s.Routes.Get("/debug", s.DebugHandler())
   296  
   297  	root := s.Routes
   298  
   299  	executors := root.Group("/executors")
   300  
   301  	executors.Post("/", s.CreateExecutorHandler())
   302  	executors.Get("/", s.ListExecutorsHandler())
   303  	executors.Get("/:name", s.GetExecutorHandler())
   304  	executors.Patch("/:name", s.UpdateExecutorHandler())
   305  	executors.Delete("/:name", s.DeleteExecutorHandler())
   306  	executors.Delete("/", s.DeleteExecutorsHandler())
   307  
   308  	executorByTypes := root.Group("/executor-by-types")
   309  	executorByTypes.Get("/", s.GetExecutorByTestTypeHandler())
   310  
   311  	webhooks := root.Group("/webhooks")
   312  
   313  	webhooks.Post("/", s.CreateWebhookHandler())
   314  	webhooks.Patch("/:name", s.UpdateWebhookHandler())
   315  	webhooks.Get("/", s.ListWebhooksHandler())
   316  	webhooks.Get("/:name", s.GetWebhookHandler())
   317  	webhooks.Delete("/:name", s.DeleteWebhookHandler())
   318  	webhooks.Delete("/", s.DeleteWebhooksHandler())
   319  
   320  	executions := root.Group("/executions")
   321  
   322  	executions.Get("/", s.ListExecutionsHandler())
   323  	executions.Post("/", s.ExecuteTestsHandler())
   324  	executions.Get("/:executionID", s.GetExecutionHandler())
   325  	executions.Get("/:executionID/artifacts", s.ListArtifactsHandler())
   326  	executions.Get("/:executionID/logs", s.ExecutionLogsHandler())
   327  	executions.Get("/:executionID/logs/stream", s.ExecutionLogsStreamHandler())
   328  	executions.Get("/:executionID/logs/v2", s.ExecutionLogsHandlerV2())
   329  	executions.Get("/:executionID/logs/stream/v2", s.ExecutionLogsStreamHandlerV2())
   330  	executions.Get("/:executionID/artifacts/:filename", s.GetArtifactHandler())
   331  	executions.Get("/:executionID/artifact-archive", s.GetArtifactArchiveHandler())
   332  
   333  	tests := root.Group("/tests")
   334  
   335  	tests.Get("/", s.ListTestsHandler())
   336  	tests.Post("/", s.CreateTestHandler())
   337  	tests.Patch("/:id", s.UpdateTestHandler())
   338  	tests.Delete("/", s.DeleteTestsHandler())
   339  
   340  	tests.Get("/:id", s.GetTestHandler())
   341  	tests.Delete("/:id", s.DeleteTestHandler())
   342  	tests.Post("/:id/abort", s.AbortTestHandler())
   343  
   344  	tests.Get("/:id/metrics", s.TestMetricsHandler())
   345  
   346  	tests.Post("/:id/executions", s.ExecuteTestsHandler())
   347  
   348  	tests.Get("/:id/executions", s.ListExecutionsHandler())
   349  	tests.Get("/:id/executions/:executionID", s.GetExecutionHandler())
   350  	tests.Patch("/:id/executions/:executionID", s.AbortExecutionHandler())
   351  
   352  	testWithExecutions := s.Routes.Group("/test-with-executions")
   353  	testWithExecutions.Get("/", s.ListTestWithExecutionsHandler())
   354  	testWithExecutions.Get("/:id", s.GetTestWithExecutionHandler())
   355  
   356  	testsuites := root.Group("/test-suites")
   357  
   358  	testsuites.Post("/", s.CreateTestSuiteHandler())
   359  	testsuites.Patch("/:id", s.UpdateTestSuiteHandler())
   360  	testsuites.Get("/", s.ListTestSuitesHandler())
   361  	testsuites.Delete("/", s.DeleteTestSuitesHandler())
   362  	testsuites.Get("/:id", s.GetTestSuiteHandler())
   363  	testsuites.Delete("/:id", s.DeleteTestSuiteHandler())
   364  	testsuites.Post("/:id/abort", s.AbortTestSuiteHandler())
   365  
   366  	testsuites.Post("/:id/executions", s.ExecuteTestSuitesHandler())
   367  	testsuites.Get("/:id/executions", s.ListTestSuiteExecutionsHandler())
   368  	testsuites.Get("/:id/executions/:executionID", s.GetTestSuiteExecutionHandler())
   369  	testsuites.Get("/:id/executions/:executionID/artifacts", s.ListTestSuiteArtifactsHandler())
   370  	testsuites.Patch("/:id/executions/:executionID", s.AbortTestSuiteExecutionHandler())
   371  
   372  	testsuites.Get("/:id/tests", s.ListTestSuiteTestsHandler())
   373  
   374  	testsuites.Get("/:id/metrics", s.TestSuiteMetricsHandler())
   375  
   376  	testSuiteExecutions := root.Group("/test-suite-executions")
   377  	testSuiteExecutions.Get("/", s.ListTestSuiteExecutionsHandler())
   378  	testSuiteExecutions.Post("/", s.ExecuteTestSuitesHandler())
   379  	testSuiteExecutions.Get("/:executionID", s.GetTestSuiteExecutionHandler())
   380  	testSuiteExecutions.Get("/:executionID/artifacts", s.ListTestSuiteArtifactsHandler())
   381  	testSuiteExecutions.Patch("/:executionID", s.AbortTestSuiteExecutionHandler())
   382  
   383  	testSuiteWithExecutions := root.Group("/test-suite-with-executions")
   384  	testSuiteWithExecutions.Get("/", s.ListTestSuiteWithExecutionsHandler())
   385  	testSuiteWithExecutions.Get("/:id", s.GetTestSuiteWithExecutionHandler())
   386  
   387  	testTriggers := root.Group("/triggers")
   388  	testTriggers.Get("/", s.ListTestTriggersHandler())
   389  	testTriggers.Post("/", s.CreateTestTriggerHandler())
   390  	testTriggers.Patch("/", s.BulkUpdateTestTriggersHandler())
   391  	testTriggers.Delete("/", s.DeleteTestTriggersHandler())
   392  	testTriggers.Get("/:id", s.GetTestTriggerHandler())
   393  	testTriggers.Patch("/:id", s.UpdateTestTriggerHandler())
   394  	testTriggers.Delete("/:id", s.DeleteTestTriggerHandler())
   395  
   396  	keymap := root.Group("/keymap")
   397  	keymap.Get("/triggers", s.GetTestTriggerKeyMapHandler())
   398  
   399  	testsources := root.Group("/test-sources")
   400  	testsources.Post("/", s.CreateTestSourceHandler())
   401  	testsources.Get("/", s.ListTestSourcesHandler())
   402  	testsources.Patch("/", s.ProcessTestSourceBatchHandler())
   403  	testsources.Get("/:name", s.GetTestSourceHandler())
   404  	testsources.Patch("/:name", s.UpdateTestSourceHandler())
   405  	testsources.Delete("/:name", s.DeleteTestSourceHandler())
   406  	testsources.Delete("/", s.DeleteTestSourcesHandler())
   407  
   408  	templates := root.Group("/templates")
   409  
   410  	templates.Post("/", s.CreateTemplateHandler())
   411  	templates.Patch("/:name", s.UpdateTemplateHandler())
   412  	templates.Get("/", s.ListTemplatesHandler())
   413  	templates.Get("/:name", s.GetTemplateHandler())
   414  	templates.Delete("/:name", s.DeleteTemplateHandler())
   415  	templates.Delete("/", s.DeleteTemplatesHandler())
   416  
   417  	labels := root.Group("/labels")
   418  	labels.Get("/", s.ListLabelsHandler())
   419  
   420  	slack := root.Group("/slack")
   421  	slack.Get("/", s.OauthHandler())
   422  
   423  	events := root.Group("/events")
   424  	events.Post("/flux", s.FluxEventHandler())
   425  	events.Get("/stream", s.EventsStreamHandler())
   426  
   427  	configs := root.Group("/config")
   428  	configs.Get("/", s.GetConfigsHandler())
   429  	configs.Patch("/", s.UpdateConfigsHandler())
   430  
   431  	debug := root.Group("/debug")
   432  	debug.Get("/listeners", s.GetDebugListenersHandler())
   433  
   434  	files := root.Group("/uploads")
   435  	files.Post("/", s.UploadFiles())
   436  
   437  	if s.enableSecretsEndpoint {
   438  		files := root.Group("/secrets")
   439  		files.Get("/", s.ListSecretsHandler())
   440  	}
   441  
   442  	repositories := root.Group("/repositories")
   443  	repositories.Post("/", s.ValidateRepositoryHandler())
   444  
   445  	// mount dashboard on /ui
   446  	dashboardURI := os.Getenv("TESTKUBE_DASHBOARD_URI")
   447  	if dashboardURI == "" {
   448  		dashboardURI = "http://testkube-dashboard"
   449  	}
   450  	s.Log.Infow("dashboard uri", "uri", dashboardURI)
   451  	s.Mux.All("/", proxy.Forward(dashboardURI))
   452  
   453  	// set up proxy for the internal GraphQL server
   454  	s.Mux.All("/graphql", func(c *fiber.Ctx) error {
   455  		// Connect to server
   456  		serverConn, err := net.Dial("tcp", ":"+s.graphqlPort)
   457  		if err != nil {
   458  			s.Log.Errorw("could not connect to GraphQL server as a proxy", "error", err)
   459  			return err
   460  		}
   461  
   462  		// Resend headers to the server
   463  		_, err = serverConn.Write(c.Request().Header.Header())
   464  		if err != nil {
   465  			serverConn.Close()
   466  			s.Log.Errorw("error while sending headers to GraphQL server", "error", err)
   467  			return err
   468  		}
   469  
   470  		// Resend body to the server
   471  		_, err = serverConn.Write(c.Body())
   472  		if err != nil && err != io.EOF {
   473  			serverConn.Close()
   474  			s.Log.Errorw("error while reading GraphQL client data", "error", err)
   475  			return err
   476  		}
   477  
   478  		// Handle optional WebSocket connection
   479  		c.Context().HijackSetNoResponse(true)
   480  		c.Context().Hijack(func(clientConn net.Conn) {
   481  			// Close the connection afterward
   482  			defer serverConn.Close()
   483  			defer clientConn.Close()
   484  
   485  			// Extract Unix connection
   486  			serverSock, ok := serverConn.(*net.TCPConn)
   487  			if !ok {
   488  				s.Log.Errorw("error while building TCPConn out ouf serverConn", "error", err)
   489  				return
   490  			}
   491  			clientSock, ok := reflect.Indirect(reflect.ValueOf(clientConn)).FieldByName("Conn").Interface().(*net.TCPConn)
   492  			if !ok {
   493  				s.Log.Errorw("error while building TCPConn out of hijacked connection", "error", err)
   494  				return
   495  			}
   496  
   497  			// Duplex communication between client and GraphQL server
   498  			var wg sync.WaitGroup
   499  			wg.Add(2)
   500  			go func() {
   501  				defer wg.Done()
   502  				_, err := io.Copy(clientSock, serverSock)
   503  				if err != nil && err != io.EOF && !errors.Is(err, syscall.ECONNRESET) && !errors.Is(err, syscall.EPIPE) {
   504  					s.Log.Errorw("error while reading GraphQL client data", "error", err)
   505  				}
   506  				serverSock.CloseWrite()
   507  			}()
   508  			go func() {
   509  				defer wg.Done()
   510  				_, err = io.Copy(serverSock, clientSock)
   511  				if err != nil && err != io.EOF {
   512  					s.Log.Errorw("error while reading GraphQL server data", "error", err)
   513  				}
   514  				clientSock.CloseWrite()
   515  			}()
   516  			wg.Wait()
   517  		})
   518  		return nil
   519  	})
   520  }
   521  
   522  func (s TestkubeAPI) StartTelemetryHeartbeats(ctx context.Context, ch chan struct{}) {
   523  	go func() {
   524  		<-ch
   525  
   526  		ticker := time.NewTicker(HeartbeatInterval)
   527  		for {
   528  			telemetryEnabled, err := s.ConfigMap.GetTelemetryEnabled(ctx)
   529  			if err != nil {
   530  				s.Log.Errorw("error getting config map", "error", err)
   531  			}
   532  			if telemetryEnabled {
   533  				l := s.Log.With("measurmentId", telemetry.TestkubeMeasurementID, "secret", text.Obfuscate(telemetry.TestkubeMeasurementSecret))
   534  				host, err := os.Hostname()
   535  				if err != nil {
   536  					l.Debugw("getting hostname error", "hostname", host, "error", err)
   537  				}
   538  				out, err := telemetry.SendHeartbeatEvent(host, version.Version, s.Config.ClusterID)
   539  				if err != nil {
   540  					l.Debugw("sending heartbeat telemetry event error", "error", err)
   541  				} else {
   542  					l.Debugw("sending heartbeat telemetry event", "output", out)
   543  				}
   544  
   545  			}
   546  			<-ticker.C
   547  		}
   548  	}()
   549  }
   550  
   551  // TODO should we use single generic filter for all list based resources ?
   552  // currently filters for e.g. tests are done "by hand"
   553  func getFilterFromRequest(c *fiber.Ctx) result.Filter {
   554  
   555  	filter := result.NewExecutionsFilter()
   556  
   557  	// id for /tests/ID/executions
   558  	testName := c.Params("id", "")
   559  	if testName == "" {
   560  		// query param for /executions?testName
   561  		testName = c.Query("testName", "")
   562  	}
   563  
   564  	if testName != "" {
   565  		filter = filter.WithTestName(testName)
   566  	}
   567  
   568  	textSearch := c.Query("textSearch", "")
   569  	if textSearch != "" {
   570  		filter = filter.WithTextSearch(textSearch)
   571  	}
   572  
   573  	page, err := strconv.Atoi(c.Query("page", ""))
   574  	if err == nil {
   575  		filter = filter.WithPage(page)
   576  	}
   577  
   578  	pageSize, err := strconv.Atoi(c.Query("pageSize", ""))
   579  	if err == nil && pageSize != 0 {
   580  		filter = filter.WithPageSize(pageSize)
   581  	}
   582  
   583  	status := c.Query("status", "")
   584  	if status != "" {
   585  		filter = filter.WithStatus(status)
   586  	}
   587  
   588  	objectType := c.Query("type", "")
   589  	if objectType != "" {
   590  		filter = filter.WithType(objectType)
   591  	}
   592  
   593  	last, err := strconv.Atoi(c.Query("last", "0"))
   594  	if err == nil && last != 0 {
   595  		filter = filter.WithLastNDays(last)
   596  	}
   597  
   598  	dFilter := datefilter.NewDateFilter(c.Query("startDate", ""), c.Query("endDate", ""))
   599  	if dFilter.IsStartValid {
   600  		filter = filter.WithStartDate(dFilter.Start)
   601  	}
   602  
   603  	if dFilter.IsEndValid {
   604  		filter = filter.WithEndDate(dFilter.End)
   605  	}
   606  
   607  	selector := c.Query("selector")
   608  	if selector != "" {
   609  		filter = filter.WithSelector(selector)
   610  	}
   611  
   612  	return filter
   613  }
   614  
   615  // WithProContext sets pro context for the API
   616  func (s *TestkubeAPI) WithProContext(proContext *config.ProContext) *TestkubeAPI {
   617  	s.proContext = proContext
   618  	return s
   619  }
   620  
   621  // WithSubscriptionChecker sets subscription checker for the API
   622  // This is used to check if Pro/Enterprise subscription is valid
   623  func (s *TestkubeAPI) WithSubscriptionChecker(subscriptionChecker checktcl.SubscriptionChecker) *TestkubeAPI {
   624  	s.SubscriptionChecker = subscriptionChecker
   625  	return s
   626  }