github.com/kubeshop/testkube@v1.17.23/cmd/api-server/main.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"flag"
     7  	"fmt"
     8  	"net"
     9  	"os"
    10  	"os/signal"
    11  	"path/filepath"
    12  	"strings"
    13  	"syscall"
    14  
    15  	"github.com/nats-io/nats.go"
    16  
    17  	executorsclientv1 "github.com/kubeshop/testkube-operator/pkg/client/executors/v1"
    18  	"github.com/kubeshop/testkube/pkg/imageinspector"
    19  	apitclv1 "github.com/kubeshop/testkube/pkg/tcl/apitcl/v1"
    20  	"github.com/kubeshop/testkube/pkg/tcl/checktcl"
    21  	cloudtestworkflow "github.com/kubeshop/testkube/pkg/tcl/cloudtcl/data/testworkflow"
    22  	"github.com/kubeshop/testkube/pkg/tcl/repositorytcl/testworkflow"
    23  	"github.com/kubeshop/testkube/pkg/tcl/schedulertcl"
    24  
    25  	"go.mongodb.org/mongo-driver/mongo"
    26  	"google.golang.org/grpc"
    27  	"google.golang.org/grpc/credentials"
    28  
    29  	cloudartifacts "github.com/kubeshop/testkube/pkg/cloud/data/artifact"
    30  
    31  	domainstorage "github.com/kubeshop/testkube/pkg/storage"
    32  	"github.com/kubeshop/testkube/pkg/storage/minio"
    33  
    34  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    35  	"github.com/kubeshop/testkube/pkg/event/kind/slack"
    36  
    37  	cloudconfig "github.com/kubeshop/testkube/pkg/cloud/data/config"
    38  
    39  	cloudresult "github.com/kubeshop/testkube/pkg/cloud/data/result"
    40  	cloudtestresult "github.com/kubeshop/testkube/pkg/cloud/data/testresult"
    41  
    42  	"github.com/kubeshop/testkube/internal/common"
    43  	"github.com/kubeshop/testkube/internal/config"
    44  	dbmigrations "github.com/kubeshop/testkube/internal/db-migrations"
    45  	parser "github.com/kubeshop/testkube/internal/template"
    46  	"github.com/kubeshop/testkube/pkg/featureflags"
    47  	"github.com/kubeshop/testkube/pkg/version"
    48  
    49  	"github.com/kubeshop/testkube/pkg/cloud"
    50  	configrepository "github.com/kubeshop/testkube/pkg/repository/config"
    51  	"github.com/kubeshop/testkube/pkg/repository/result"
    52  	"github.com/kubeshop/testkube/pkg/repository/storage"
    53  	"github.com/kubeshop/testkube/pkg/repository/testresult"
    54  
    55  	"golang.org/x/sync/errgroup"
    56  
    57  	"github.com/pkg/errors"
    58  
    59  	"github.com/kubeshop/testkube/internal/app/api/debug"
    60  	"github.com/kubeshop/testkube/internal/app/api/metrics"
    61  	"github.com/kubeshop/testkube/pkg/agent"
    62  	"github.com/kubeshop/testkube/pkg/event"
    63  	"github.com/kubeshop/testkube/pkg/event/bus"
    64  	kubeexecutor "github.com/kubeshop/testkube/pkg/executor"
    65  	"github.com/kubeshop/testkube/pkg/executor/client"
    66  	"github.com/kubeshop/testkube/pkg/executor/containerexecutor"
    67  	logsclient "github.com/kubeshop/testkube/pkg/logs/client"
    68  	"github.com/kubeshop/testkube/pkg/scheduler"
    69  
    70  	testkubeclientset "github.com/kubeshop/testkube-operator/pkg/clientset/versioned"
    71  	"github.com/kubeshop/testkube/pkg/k8sclient"
    72  	"github.com/kubeshop/testkube/pkg/triggers"
    73  
    74  	kubeclient "github.com/kubeshop/testkube-operator/pkg/client"
    75  	scriptsclient "github.com/kubeshop/testkube-operator/pkg/client/scripts/v2"
    76  	templatesclientv1 "github.com/kubeshop/testkube-operator/pkg/client/templates/v1"
    77  	testexecutionsclientv1 "github.com/kubeshop/testkube-operator/pkg/client/testexecutions/v1"
    78  	testsclientv1 "github.com/kubeshop/testkube-operator/pkg/client/tests"
    79  	testsclientv3 "github.com/kubeshop/testkube-operator/pkg/client/tests/v3"
    80  	testsourcesclientv1 "github.com/kubeshop/testkube-operator/pkg/client/testsources/v1"
    81  	testsuiteexecutionsclientv1 "github.com/kubeshop/testkube-operator/pkg/client/testsuiteexecutions/v1"
    82  	testsuitesclientv2 "github.com/kubeshop/testkube-operator/pkg/client/testsuites/v2"
    83  	testsuitesclientv3 "github.com/kubeshop/testkube-operator/pkg/client/testsuites/v3"
    84  	apiv1 "github.com/kubeshop/testkube/internal/app/api/v1"
    85  	"github.com/kubeshop/testkube/internal/migrations"
    86  	"github.com/kubeshop/testkube/pkg/configmap"
    87  	"github.com/kubeshop/testkube/pkg/dbmigrator"
    88  	"github.com/kubeshop/testkube/pkg/log"
    89  	"github.com/kubeshop/testkube/pkg/migrator"
    90  	"github.com/kubeshop/testkube/pkg/reconciler"
    91  	"github.com/kubeshop/testkube/pkg/secret"
    92  	"github.com/kubeshop/testkube/pkg/ui"
    93  )
    94  
    95  var verbose = flag.Bool("v", false, "enable verbosity level")
    96  
    97  func init() {
    98  	flag.Parse()
    99  	ui.Verbose = *verbose
   100  }
   101  
   102  func runMigrations() (err error) {
   103  	results := migrations.Migrator.GetValidMigrations(version.Version, migrator.MigrationTypeServer)
   104  	if len(results) == 0 {
   105  		log.DefaultLogger.Debugw("No migrations available for Testkube", "apiVersion", version.Version)
   106  		return nil
   107  	}
   108  
   109  	var migrationInfo []string
   110  	for _, migration := range results {
   111  		migrationInfo = append(migrationInfo, fmt.Sprintf("%+v - %s", migration.Version(), migration.Info()))
   112  	}
   113  	log.DefaultLogger.Infow("Available migrations for Testkube", "apiVersion", version.Version, "migrations", migrationInfo)
   114  
   115  	return migrations.Migrator.Run(version.Version, migrator.MigrationTypeServer)
   116  }
   117  
   118  func runMongoMigrations(ctx context.Context, db *mongo.Database, migrationsDir string) error {
   119  	migrationsCollectionName := "__migrations"
   120  	activeMigrations, err := dbmigrator.GetDbMigrationsFromFs(dbmigrations.MongoMigrationsFs)
   121  	if err != nil {
   122  		return errors.Wrap(err, "failed to obtain MongoDB migrations from disk")
   123  	}
   124  	dbMigrator := dbmigrator.NewDbMigrator(dbmigrator.NewDatabase(db, migrationsCollectionName), activeMigrations)
   125  	plan, err := dbMigrator.Plan(ctx)
   126  	if err != nil {
   127  		return errors.Wrap(err, "failed to plan MongoDB migrations")
   128  	}
   129  	if plan.Total == 0 {
   130  		log.DefaultLogger.Info("No MongoDB migrations to apply.")
   131  	} else {
   132  		log.DefaultLogger.Info(fmt.Sprintf("Applying MongoDB migrations: %d rollbacks and %d ups.", len(plan.Downs), len(plan.Ups)))
   133  	}
   134  	err = dbMigrator.Apply(ctx)
   135  	return errors.Wrap(err, "failed to apply MongoDB migrations")
   136  }
   137  
   138  func main() {
   139  	cfg, err := config.Get()
   140  	cfg.CleanLegacyVars()
   141  	ui.ExitOnError("error getting application config", err)
   142  
   143  	features, err := featureflags.Get()
   144  	ui.ExitOnError("error getting application feature flags", err)
   145  
   146  	log.DefaultLogger.Infow("Feature flags configured", "ff", features)
   147  
   148  	// Run services within an errgroup to propagate errors between services.
   149  	g, ctx := errgroup.WithContext(context.Background())
   150  
   151  	// Cancel the errgroup context on SIGINT and SIGTERM,
   152  	// which shuts everything down gracefully.
   153  	stopSignal := make(chan os.Signal, 1)
   154  	signal.Notify(stopSignal, syscall.SIGINT, syscall.SIGTERM)
   155  	g.Go(func() error {
   156  		select {
   157  		case <-ctx.Done():
   158  			return nil
   159  		case sig := <-stopSignal:
   160  			go func() {
   161  				<-stopSignal
   162  				os.Exit(137)
   163  			}()
   164  			// Returning an error cancels the errgroup.
   165  			return errors.Errorf("received signal: %v", sig)
   166  		}
   167  	})
   168  
   169  	ln, err := net.Listen("tcp", ":"+cfg.APIServerPort)
   170  	ui.ExitOnError("Checking if port "+cfg.APIServerPort+"is free", err)
   171  	_ = ln.Close()
   172  	log.DefaultLogger.Debugw("TCP Port is available", "port", cfg.APIServerPort)
   173  
   174  	ln, err = net.Listen("tcp", ":"+cfg.GraphqlPort)
   175  	ui.ExitOnError("Checking if port "+cfg.GraphqlPort+"is free", err)
   176  	_ = ln.Close()
   177  	log.DefaultLogger.Debugw("TCP Port is available", "port", cfg.GraphqlPort)
   178  
   179  	kubeClient, err := kubeclient.GetClient()
   180  	ui.ExitOnError("Getting kubernetes client", err)
   181  
   182  	secretClient, err := secret.NewClient(cfg.TestkubeNamespace)
   183  	ui.ExitOnError("Getting secret client", err)
   184  
   185  	configMapClient, err := configmap.NewClient(cfg.TestkubeNamespace)
   186  	ui.ExitOnError("Getting config map client", err)
   187  	// agent
   188  	var grpcClient cloud.TestKubeCloudAPIClient
   189  	var grpcConn *grpc.ClientConn
   190  	mode := common.ModeStandalone
   191  	if cfg.TestkubeProAPIKey != "" {
   192  		mode = common.ModeAgent
   193  	}
   194  	if mode == common.ModeAgent {
   195  		grpcConn, err = agent.NewGRPCConnection(
   196  			ctx,
   197  			cfg.TestkubeProTLSInsecure,
   198  			cfg.TestkubeProSkipVerify,
   199  			cfg.TestkubeProURL,
   200  			cfg.TestkubeProCertFile,
   201  			cfg.TestkubeProKeyFile,
   202  			cfg.TestkubeProCAFile,
   203  			log.DefaultLogger,
   204  		)
   205  		ui.ExitOnError("error creating gRPC connection", err)
   206  		defer grpcConn.Close()
   207  
   208  		grpcClient = cloud.NewTestKubeCloudAPIClient(grpcConn)
   209  	}
   210  
   211  	if cfg.EnableDebugServer {
   212  		debugSrv := debug.NewDebugServer(cfg.DebugListenAddr)
   213  
   214  		g.Go(func() error {
   215  			log.DefaultLogger.Infof("starting debug pprof server")
   216  			return debugSrv.ListenAndServe()
   217  		})
   218  	}
   219  
   220  	// k8s
   221  	scriptsClient := scriptsclient.NewClient(kubeClient, cfg.TestkubeNamespace)
   222  	testsClientV1 := testsclientv1.NewClient(kubeClient, cfg.TestkubeNamespace)
   223  	testsClientV3 := testsclientv3.NewClient(kubeClient, cfg.TestkubeNamespace)
   224  	executorsClient := executorsclientv1.NewClient(kubeClient, cfg.TestkubeNamespace)
   225  	webhooksClient := executorsclientv1.NewWebhooksClient(kubeClient, cfg.TestkubeNamespace)
   226  	testsuitesClientV2 := testsuitesclientv2.NewClient(kubeClient, cfg.TestkubeNamespace)
   227  	testsuitesClientV3 := testsuitesclientv3.NewClient(kubeClient, cfg.TestkubeNamespace)
   228  	testsourcesClient := testsourcesclientv1.NewClient(kubeClient, cfg.TestkubeNamespace)
   229  	testExecutionsClient := testexecutionsclientv1.NewClient(kubeClient, cfg.TestkubeNamespace)
   230  	testsuiteExecutionsClient := testsuiteexecutionsclientv1.NewClient(kubeClient, cfg.TestkubeNamespace)
   231  	templatesClient := templatesclientv1.NewClient(kubeClient, cfg.TestkubeNamespace)
   232  
   233  	clientset, err := k8sclient.ConnectToK8s()
   234  	if err != nil {
   235  		ui.ExitOnError("Creating k8s clientset", err)
   236  	}
   237  
   238  	k8sCfg, err := k8sclient.GetK8sClientConfig()
   239  	if err != nil {
   240  		ui.ExitOnError("Getting k8s client config", err)
   241  	}
   242  	testkubeClientset, err := testkubeclientset.NewForConfig(k8sCfg)
   243  	if err != nil {
   244  		ui.ExitOnError("Creating TestKube Clientset", err)
   245  	}
   246  
   247  	var logGrpcClient logsclient.StreamGetter
   248  	if features.LogsV2 {
   249  		creds, err := newGRPCTransportCredentials(cfg)
   250  		ui.ExitOnError("Getting log server TLS credentials", err)
   251  		logGrpcClient = logsclient.NewGrpcClient(cfg.LogServerGrpcAddress, creds)
   252  	}
   253  
   254  	// DI
   255  	var resultsRepository result.Repository
   256  	var testResultsRepository testresult.Repository
   257  	var testWorkflowResultsRepository testworkflow.Repository
   258  	var testWorkflowOutputRepository testworkflow.OutputRepository
   259  	var configRepository configrepository.Repository
   260  	var triggerLeaseBackend triggers.LeaseBackend
   261  	var artifactStorage domainstorage.ArtifactsStorage
   262  	var storageClient domainstorage.Client
   263  	if mode == common.ModeAgent {
   264  		resultsRepository = cloudresult.NewCloudResultRepository(grpcClient, grpcConn, cfg.TestkubeProAPIKey)
   265  		testResultsRepository = cloudtestresult.NewCloudRepository(grpcClient, grpcConn, cfg.TestkubeProAPIKey)
   266  		configRepository = cloudconfig.NewCloudResultRepository(grpcClient, grpcConn, cfg.TestkubeProAPIKey)
   267  		testWorkflowResultsRepository = cloudtestworkflow.NewCloudRepository(grpcClient, grpcConn, cfg.TestkubeProAPIKey)
   268  		testWorkflowOutputRepository = cloudtestworkflow.NewCloudOutputRepository(grpcClient, grpcConn, cfg.TestkubeProAPIKey)
   269  		triggerLeaseBackend = triggers.NewAcquireAlwaysLeaseBackend()
   270  		artifactStorage = cloudartifacts.NewCloudArtifactsStorage(grpcClient, grpcConn, cfg.TestkubeProAPIKey)
   271  	} else {
   272  		mongoSSLConfig := getMongoSSLConfig(cfg, secretClient)
   273  		db, err := storage.GetMongoDatabase(cfg.APIMongoDSN, cfg.APIMongoDB, cfg.APIMongoDBType, cfg.APIMongoAllowTLS, mongoSSLConfig)
   274  		ui.ExitOnError("Getting mongo database", err)
   275  		isDocDb := cfg.APIMongoDBType == storage.TypeDocDB
   276  		mongoResultsRepository := result.NewMongoRepository(db, cfg.APIMongoAllowDiskUse, isDocDb, result.WithFeatureFlags(features), result.WithLogsClient(logGrpcClient))
   277  		resultsRepository = mongoResultsRepository
   278  		testResultsRepository = testresult.NewMongoRepository(db, cfg.APIMongoAllowDiskUse, isDocDb)
   279  		testWorkflowResultsRepository = testworkflow.NewMongoRepository(db, cfg.APIMongoAllowDiskUse)
   280  		configRepository = configrepository.NewMongoRepository(db)
   281  		triggerLeaseBackend = triggers.NewMongoLeaseBackend(db)
   282  		minioClient := newStorageClient(cfg)
   283  		if err = minioClient.Connect(); err != nil {
   284  			ui.ExitOnError("Connecting to minio", err)
   285  		}
   286  		if expErr := minioClient.SetExpirationPolicy(cfg.StorageExpiration); expErr != nil {
   287  			log.DefaultLogger.Errorw("Error setting expiration policy", "error", expErr)
   288  		}
   289  		storageClient = minioClient
   290  		testWorkflowOutputRepository = testworkflow.NewMinioOutputRepository(storageClient, cfg.LogsBucket)
   291  		artifactStorage = minio.NewMinIOArtifactClient(storageClient)
   292  		// init storage
   293  		isMinioStorage := cfg.LogsStorage == "minio"
   294  		if isMinioStorage {
   295  			bucket := cfg.LogsBucket
   296  			if bucket == "" {
   297  				log.DefaultLogger.Error("LOGS_BUCKET env var is not set")
   298  			} else if ok, err := storageClient.IsConnectionPossible(ctx); ok && (err == nil) {
   299  				log.DefaultLogger.Info("setting minio as logs storage")
   300  				mongoResultsRepository.OutputRepository = result.NewMinioOutputRepository(storageClient, mongoResultsRepository.ResultsColl, bucket)
   301  			} else {
   302  				log.DefaultLogger.Infow("minio is not available, using default logs storage", "error", err)
   303  			}
   304  		}
   305  
   306  		// Run DB migrations
   307  		if !cfg.DisableMongoMigrations {
   308  			err := runMongoMigrations(ctx, db, filepath.Join(cfg.TestkubeConfigDir, "db-migrations"))
   309  			if err != nil {
   310  				log.DefaultLogger.Warnf("failed to apply MongoDB migrations: %v", err)
   311  			}
   312  		}
   313  	}
   314  
   315  	configName := fmt.Sprintf("testkube-api-server-config-%s", cfg.TestkubeNamespace)
   316  	if cfg.APIServerConfig != "" {
   317  		configName = cfg.APIServerConfig
   318  	}
   319  
   320  	configMapConfig, err := configrepository.NewConfigMapConfig(configName, cfg.TestkubeNamespace)
   321  	ui.ExitOnError("Getting config map config", err)
   322  
   323  	// try to load from mongo based config first
   324  	telemetryEnabled, err := configMapConfig.GetTelemetryEnabled(ctx)
   325  	if err != nil {
   326  		// fallback to envs in case of failure (no record yet, or other error)
   327  		telemetryEnabled = cfg.TestkubeAnalyticsEnabled
   328  	}
   329  
   330  	var clusterId string
   331  	cmConfig, err := configMapConfig.Get(ctx)
   332  	if cmConfig.ClusterId != "" {
   333  		clusterId = cmConfig.ClusterId
   334  	}
   335  
   336  	if clusterId == "" {
   337  		cmConfig, err = configRepository.Get(ctx)
   338  		if err != nil {
   339  			log.DefaultLogger.Warnw("error fetching config ConfigMap", "error", err)
   340  		}
   341  		cmConfig.EnableTelemetry = telemetryEnabled
   342  		if cmConfig.ClusterId == "" {
   343  			cmConfig.ClusterId, err = configMapConfig.GetUniqueClusterId(ctx)
   344  			if err != nil {
   345  				log.DefaultLogger.Warnw("error getting unique clusterId", "error", err)
   346  			}
   347  		}
   348  
   349  		clusterId = cmConfig.ClusterId
   350  		_, err = configMapConfig.Upsert(ctx, cmConfig)
   351  		if err != nil {
   352  			log.DefaultLogger.Warn("error upserting config ConfigMap", "error", err)
   353  		}
   354  
   355  	}
   356  
   357  	log.DefaultLogger.Debugw("Getting unique clusterId", "clusterId", clusterId, "error", err)
   358  
   359  	// TODO check if this version exists somewhere in stats (probably could be removed)
   360  	migrations.Migrator.Add(migrations.NewVersion_0_9_2(scriptsClient, testsClientV1, testsClientV3, testsuitesClientV2))
   361  	if err := runMigrations(); err != nil {
   362  		ui.ExitOnError("Running server migrations", err)
   363  	}
   364  
   365  	apiVersion := version.Version
   366  
   367  	envs := make(map[string]string)
   368  	for _, env := range os.Environ() {
   369  		pair := strings.SplitN(env, "=", 2)
   370  		if len(pair) != 2 {
   371  			continue
   372  		}
   373  
   374  		envs[pair[0]] += pair[1]
   375  	}
   376  
   377  	nc, err := newNATSConnection(cfg)
   378  	if err != nil {
   379  		ui.ExitOnError("Creating NATS connection", err)
   380  	}
   381  	eventBus := bus.NewNATSBus(nc)
   382  	eventsEmitter := event.NewEmitter(eventBus, cfg.TestkubeClusterName, envs)
   383  
   384  	var logsStream logsclient.Stream
   385  
   386  	if features.LogsV2 {
   387  		logsStream, err = logsclient.NewNatsLogStream(nc.Conn)
   388  		if err != nil {
   389  			ui.ExitOnError("Creating logs streaming client", err)
   390  		}
   391  	}
   392  
   393  	metrics := metrics.NewMetrics()
   394  
   395  	defaultExecutors, err := parseDefaultExecutors(cfg)
   396  	if err != nil {
   397  		ui.ExitOnError("Parsing default executors", err)
   398  	}
   399  
   400  	images, err := kubeexecutor.SyncDefaultExecutors(executorsClient, cfg.TestkubeNamespace, defaultExecutors, cfg.TestkubeReadonlyExecutors)
   401  	if err != nil {
   402  		ui.ExitOnError("Sync default executors", err)
   403  	}
   404  
   405  	jobTemplates, err := parser.ParseJobTemplates(cfg)
   406  	if err != nil {
   407  		ui.ExitOnError("Creating job templates", err)
   408  	}
   409  
   410  	proContext := config.ProContext{
   411  		APIKey:                           cfg.TestkubeProAPIKey,
   412  		URL:                              cfg.TestkubeProURL,
   413  		TLSInsecure:                      cfg.TestkubeProTLSInsecure,
   414  		WorkerCount:                      cfg.TestkubeProWorkerCount,
   415  		LogStreamWorkerCount:             cfg.TestkubeProLogStreamWorkerCount,
   416  		WorkflowNotificationsWorkerCount: cfg.TestkubeProWorkflowNotificationsWorkerCount,
   417  		SkipVerify:                       cfg.TestkubeProSkipVerify,
   418  		EnvID:                            cfg.TestkubeProEnvID,
   419  		OrgID:                            cfg.TestkubeProOrgID,
   420  		Migrate:                          cfg.TestkubeProMigrate,
   421  		ConnectionTimeout:                cfg.TestkubeProConnectionTimeout,
   422  	}
   423  
   424  	// Check Pro/Enterprise subscription
   425  	var subscriptionChecker checktcl.SubscriptionChecker
   426  	if mode == common.ModeAgent {
   427  		subscriptionChecker, err = checktcl.NewSubscriptionChecker(ctx, proContext, grpcClient, grpcConn)
   428  		ui.ExitOnError("Failed creating subscription checker", err)
   429  	}
   430  
   431  	serviceAccountNames := map[string]string{
   432  		cfg.TestkubeNamespace: cfg.JobServiceAccountName,
   433  	}
   434  
   435  	// Pro edition only (tcl protected code)
   436  	if cfg.TestkubeExecutionNamespaces != "" {
   437  		err = subscriptionChecker.IsActiveOrgPlanEnterpriseForFeature("execution namespace")
   438  		ui.ExitOnError("Subscription checking", err)
   439  
   440  		serviceAccountNames = schedulertcl.GetServiceAccountNamesFromConfig(serviceAccountNames, cfg.TestkubeExecutionNamespaces)
   441  	}
   442  
   443  	executor, err := client.NewJobExecutor(
   444  		resultsRepository,
   445  		images,
   446  		jobTemplates,
   447  		serviceAccountNames,
   448  		metrics,
   449  		eventsEmitter,
   450  		configMapConfig,
   451  		testsClientV3,
   452  		clientset,
   453  		testExecutionsClient,
   454  		templatesClient,
   455  		cfg.TestkubeRegistry,
   456  		cfg.TestkubePodStartTimeout,
   457  		clusterId,
   458  		cfg.TestkubeDashboardURI,
   459  		"http://"+cfg.APIServerFullname+":"+cfg.APIServerPort,
   460  		cfg.NatsURI,
   461  		cfg.Debug,
   462  		logsStream,
   463  		features,
   464  		cfg.TestkubeDefaultStorageClassName,
   465  	)
   466  	if err != nil {
   467  		ui.ExitOnError("Creating executor client", err)
   468  	}
   469  
   470  	containerTemplates, err := parser.ParseContainerTemplates(cfg)
   471  	if err != nil {
   472  		ui.ExitOnError("Creating container job templates", err)
   473  	}
   474  
   475  	inspectorStorages := []imageinspector.Storage{imageinspector.NewMemoryStorage()}
   476  	if cfg.EnableImageDataPersistentCache {
   477  		configmapStorage := imageinspector.NewConfigMapStorage(configMapClient, cfg.ImageDataPersistentCacheKey, true)
   478  		_ = configmapStorage.CopyTo(context.Background(), inspectorStorages[0].(imageinspector.StorageTransfer))
   479  		inspectorStorages = append(inspectorStorages, configmapStorage)
   480  	}
   481  	inspector := imageinspector.NewInspector(
   482  		cfg.TestkubeRegistry,
   483  		imageinspector.NewSkopeoFetcher(),
   484  		imageinspector.NewSecretFetcher(secretClient),
   485  		inspectorStorages...,
   486  	)
   487  
   488  	containerExecutor, err := containerexecutor.NewContainerExecutor(
   489  		resultsRepository,
   490  		images,
   491  		containerTemplates,
   492  		inspector,
   493  		serviceAccountNames,
   494  		metrics,
   495  		eventsEmitter,
   496  		configMapConfig,
   497  		executorsClient,
   498  		testsClientV3,
   499  		testExecutionsClient,
   500  		templatesClient,
   501  		cfg.TestkubeRegistry,
   502  		cfg.TestkubePodStartTimeout,
   503  		clusterId,
   504  		cfg.TestkubeDashboardURI,
   505  		"http://"+cfg.APIServerFullname+":"+cfg.APIServerPort,
   506  		cfg.NatsURI,
   507  		cfg.Debug,
   508  		logsStream,
   509  		features,
   510  		cfg.TestkubeDefaultStorageClassName,
   511  	)
   512  	if err != nil {
   513  		ui.ExitOnError("Creating container executor", err)
   514  	}
   515  
   516  	sched := scheduler.NewScheduler(
   517  		metrics,
   518  		executor,
   519  		containerExecutor,
   520  		resultsRepository,
   521  		testResultsRepository,
   522  		executorsClient,
   523  		testsClientV3,
   524  		testsuitesClientV3,
   525  		testsourcesClient,
   526  		secretClient,
   527  		eventsEmitter,
   528  		log.DefaultLogger,
   529  		configMapConfig,
   530  		configMapClient,
   531  		testsuiteExecutionsClient,
   532  		eventBus,
   533  		cfg.TestkubeDashboardURI,
   534  		features,
   535  		logsStream,
   536  		cfg.TestkubeNamespace,
   537  		cfg.TestkubeProTLSSecret,
   538  		cfg.TestkubeProRunnerCustomCASecret,
   539  	)
   540  	if mode == common.ModeAgent {
   541  		sched.WithSubscriptionChecker(subscriptionChecker)
   542  	}
   543  
   544  	slackLoader, err := newSlackLoader(cfg, envs)
   545  	if err != nil {
   546  		ui.ExitOnError("Creating slack loader", err)
   547  	}
   548  
   549  	api := apiv1.NewTestkubeAPI(
   550  		cfg.TestkubeNamespace,
   551  		resultsRepository,
   552  		testResultsRepository,
   553  		testsClientV3,
   554  		executorsClient,
   555  		testsuitesClientV3,
   556  		secretClient,
   557  		webhooksClient,
   558  		clientset,
   559  		testkubeClientset,
   560  		testsourcesClient,
   561  		configMapConfig,
   562  		clusterId,
   563  		eventsEmitter,
   564  		executor,
   565  		containerExecutor,
   566  		metrics,
   567  		sched,
   568  		slackLoader,
   569  		storageClient,
   570  		cfg.GraphqlPort,
   571  		artifactStorage,
   572  		templatesClient,
   573  		cfg.CDEventsTarget,
   574  		cfg.TestkubeDashboardURI,
   575  		cfg.TestkubeHelmchartVersion,
   576  		mode,
   577  		eventBus,
   578  		cfg.EnableSecretsEndpoint,
   579  		features,
   580  		logsStream,
   581  		logGrpcClient,
   582  		subscriptionChecker,
   583  		cfg.DisableSecretCreation,
   584  		serviceAccountNames,
   585  	)
   586  
   587  	// Apply Pro server enhancements
   588  	apiPro := apitclv1.NewApiTCL(
   589  		api,
   590  		&proContext,
   591  		kubeClient,
   592  		inspector,
   593  		testWorkflowResultsRepository,
   594  		testWorkflowOutputRepository,
   595  		"http://"+cfg.APIServerFullname+":"+cfg.APIServerPort,
   596  		cfg.GlobalWorkflowTemplateName,
   597  		configMapConfig,
   598  	)
   599  	apiPro.AppendRoutes()
   600  
   601  	if mode == common.ModeAgent {
   602  		log.DefaultLogger.Info("starting agent service")
   603  		api.WithProContext(&proContext)
   604  		agentHandle, err := agent.NewAgent(
   605  			log.DefaultLogger,
   606  			api.Mux.Handler(),
   607  			grpcClient,
   608  			api.GetLogsStream,
   609  			apiPro.GetTestWorkflowNotificationsStream,
   610  			clusterId,
   611  			cfg.TestkubeClusterName,
   612  			envs,
   613  			features,
   614  			proContext,
   615  		)
   616  		if err != nil {
   617  			ui.ExitOnError("Starting agent", err)
   618  		}
   619  		g.Go(func() error {
   620  			err = agentHandle.Run(ctx)
   621  			if err != nil {
   622  				ui.ExitOnError("Running agent", err)
   623  			}
   624  			return nil
   625  		})
   626  		eventsEmitter.Loader.Register(agentHandle)
   627  	}
   628  
   629  	api.InitEvents()
   630  	if !cfg.DisableTestTriggers {
   631  		triggerService := triggers.NewService(
   632  			sched,
   633  			clientset,
   634  			testkubeClientset,
   635  			testsuitesClientV3,
   636  			testsClientV3,
   637  			resultsRepository,
   638  			testResultsRepository,
   639  			triggerLeaseBackend,
   640  			log.DefaultLogger,
   641  			configMapConfig,
   642  			executorsClient,
   643  			executor,
   644  			eventBus,
   645  			metrics,
   646  			triggers.WithHostnameIdentifier(),
   647  			triggers.WithTestkubeNamespace(cfg.TestkubeNamespace),
   648  			triggers.WithWatcherNamespaces(cfg.TestkubeWatcherNamespaces),
   649  			triggers.WithDisableSecretCreation(cfg.DisableSecretCreation),
   650  		)
   651  		log.DefaultLogger.Info("starting trigger service")
   652  		triggerService.Run(ctx)
   653  	} else {
   654  		log.DefaultLogger.Info("test triggers are disabled")
   655  	}
   656  
   657  	if !cfg.DisableReconciler {
   658  		reconcilerClient := reconciler.NewClient(clientset,
   659  			resultsRepository,
   660  			testResultsRepository,
   661  			executorsClient,
   662  			log.DefaultLogger)
   663  		g.Go(func() error {
   664  			return reconcilerClient.Run(ctx)
   665  		})
   666  	} else {
   667  		log.DefaultLogger.Info("reconclier is disabled")
   668  	}
   669  
   670  	// telemetry based functions
   671  	telemetryCh := make(chan struct{})
   672  	defer close(telemetryCh)
   673  
   674  	api.SendTelemetryStartEvent(ctx, telemetryCh)
   675  	api.StartTelemetryHeartbeats(ctx, telemetryCh)
   676  
   677  	log.DefaultLogger.Infow(
   678  		"starting Testkube API server",
   679  		"telemetryEnabled", telemetryEnabled,
   680  		"clusterId", clusterId,
   681  		"namespace", cfg.TestkubeNamespace,
   682  		"version", apiVersion,
   683  	)
   684  
   685  	g.Go(func() error {
   686  		return api.Run(ctx)
   687  	})
   688  
   689  	g.Go(func() error {
   690  		return api.RunGraphQLServer(ctx, cfg.GraphqlPort)
   691  	})
   692  
   693  	if err := g.Wait(); err != nil {
   694  		log.DefaultLogger.Fatalf("Testkube is shutting down: %v", err)
   695  	}
   696  }
   697  
   698  func parseDefaultExecutors(cfg *config.Config) (executors []testkube.ExecutorDetails, err error) {
   699  	rawExecutors, err := parser.LoadConfigFromStringOrFile(
   700  		cfg.TestkubeDefaultExecutors,
   701  		cfg.TestkubeConfigDir,
   702  		"executors.json",
   703  		"executors",
   704  	)
   705  	if err != nil {
   706  		return nil, err
   707  	}
   708  
   709  	if err = json.Unmarshal([]byte(rawExecutors), &executors); err != nil {
   710  		return nil, err
   711  	}
   712  
   713  	enabledExecutors, err := parser.LoadConfigFromStringOrFile(
   714  		cfg.TestkubeEnabledExecutors,
   715  		cfg.TestkubeConfigDir,
   716  		"enabledExecutors",
   717  		"enabled executors",
   718  	)
   719  	if err != nil {
   720  		return nil, err
   721  	}
   722  
   723  	specifiedExecutors := make(map[string]struct{})
   724  	if enabledExecutors != "" {
   725  		for _, executor := range strings.Split(enabledExecutors, ",") {
   726  			if strings.TrimSpace(executor) == "" {
   727  				continue
   728  			}
   729  
   730  			specifiedExecutors[strings.TrimSpace(executor)] = struct{}{}
   731  		}
   732  
   733  		for i := len(executors) - 1; i >= 0; i-- {
   734  			if _, ok := specifiedExecutors[executors[i].Name]; !ok {
   735  				executors = append(executors[:i], executors[i+1:]...)
   736  			}
   737  		}
   738  	}
   739  
   740  	return executors, nil
   741  }
   742  
   743  func newNATSConnection(cfg *config.Config) (*nats.EncodedConn, error) {
   744  	return bus.NewNATSEncodedConnection(bus.ConnectionConfig{
   745  		NatsURI:            cfg.NatsURI,
   746  		NatsSecure:         cfg.NatsSecure,
   747  		NatsSkipVerify:     cfg.NatsSkipVerify,
   748  		NatsCertFile:       cfg.NatsCertFile,
   749  		NatsKeyFile:        cfg.NatsKeyFile,
   750  		NatsCAFile:         cfg.NatsCAFile,
   751  		NatsConnectTimeout: cfg.NatsConnectTimeout,
   752  	})
   753  }
   754  
   755  func newStorageClient(cfg *config.Config) *minio.Client {
   756  	opts := minio.GetTLSOptions(cfg.StorageSSL, cfg.StorageSkipVerify, cfg.StorageCertFile, cfg.StorageKeyFile, cfg.StorageCAFile)
   757  	return minio.NewClient(
   758  		cfg.StorageEndpoint,
   759  		cfg.StorageAccessKeyID,
   760  		cfg.StorageSecretAccessKey,
   761  		cfg.StorageRegion,
   762  		cfg.StorageToken,
   763  		cfg.StorageBucket,
   764  		opts...,
   765  	)
   766  }
   767  
   768  func newSlackLoader(cfg *config.Config, envs map[string]string) (*slack.SlackLoader, error) {
   769  	slackTemplate, err := parser.LoadConfigFromStringOrFile(
   770  		cfg.SlackTemplate,
   771  		cfg.TestkubeConfigDir,
   772  		"slack-template.json",
   773  		"slack template",
   774  	)
   775  	if err != nil {
   776  		return nil, err
   777  	}
   778  
   779  	slackConfig, err := parser.LoadConfigFromStringOrFile(cfg.SlackConfig, cfg.TestkubeConfigDir, "slack-config.json", "slack config")
   780  	if err != nil {
   781  		return nil, err
   782  	}
   783  
   784  	return slack.NewSlackLoader(slackTemplate, slackConfig, cfg.TestkubeClusterName, cfg.TestkubeDashboardURI,
   785  		testkube.AllEventTypes, envs), nil
   786  }
   787  
   788  // getMongoSSLConfig builds the necessary SSL connection info from the settings in the environment variables
   789  // and the given secret reference
   790  func getMongoSSLConfig(cfg *config.Config, secretClient *secret.Client) *storage.MongoSSLConfig {
   791  	if cfg.APIMongoSSLCert == "" {
   792  		return nil
   793  	}
   794  
   795  	clientCertPath := "/tmp/mongodb.pem"
   796  	rootCAPath := "/tmp/mongodb-root-ca.pem"
   797  	mongoSSLSecret, err := secretClient.Get(cfg.APIMongoSSLCert)
   798  	ui.ExitOnError(fmt.Sprintf("Could not get secret %s for MongoDB connection", cfg.APIMongoSSLCert), err)
   799  
   800  	var keyFile, caFile, pass string
   801  	var ok bool
   802  	if keyFile, ok = mongoSSLSecret[cfg.APIMongoSSLClientFileKey]; !ok {
   803  		ui.Warn("Could not find sslClientCertificateKeyFile with key %s in secret %s", cfg.APIMongoSSLClientFileKey, cfg.APIMongoSSLCert)
   804  	}
   805  	if caFile, ok = mongoSSLSecret[cfg.APIMongoSSLCAFileKey]; !ok {
   806  		ui.Warn("Could not find sslCertificateAuthorityFile with key %s in secret %s", cfg.APIMongoSSLCAFileKey, cfg.APIMongoSSLCert)
   807  	}
   808  	if pass, ok = mongoSSLSecret[cfg.APIMongoSSLClientFilePass]; !ok {
   809  		ui.Warn("Could not find sslClientCertificateKeyFilePassword with key %s in secret %s", cfg.APIMongoSSLClientFilePass, cfg.APIMongoSSLCert)
   810  	}
   811  
   812  	err = os.WriteFile(clientCertPath, []byte(keyFile), 0644)
   813  	ui.ExitOnError(fmt.Sprintf("Could not place mongodb certificate key file: %s", err))
   814  
   815  	err = os.WriteFile(rootCAPath, []byte(caFile), 0644)
   816  	ui.ExitOnError(fmt.Sprintf("Could not place mongodb ssl ca file: %s", err))
   817  
   818  	return &storage.MongoSSLConfig{
   819  		SSLClientCertificateKeyFile:         clientCertPath,
   820  		SSLClientCertificateKeyFilePassword: pass,
   821  		SSLCertificateAuthoritiyFile:        rootCAPath,
   822  	}
   823  }
   824  
   825  func newGRPCTransportCredentials(cfg *config.Config) (credentials.TransportCredentials, error) {
   826  	return logsclient.GetGrpcTransportCredentials(logsclient.GrpcConnectionConfig{
   827  		Secure:     cfg.LogServerSecure,
   828  		SkipVerify: cfg.LogServerSkipVerify,
   829  		CertFile:   cfg.LogServerCertFile,
   830  		KeyFile:    cfg.LogServerKeyFile,
   831  		CAFile:     cfg.LogServerCAFile,
   832  	})
   833  }