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 }