diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index b38ebdab7b1..3283f4aaeeb 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -603,7 +603,6 @@ func main() { cfg.GraphqlPort, artifactStorage, templatesClient, - cfg.CDEventsTarget, cfg.TestkubeDashboardURI, cfg.TestkubeHelmchartVersion, mode, @@ -615,7 +614,6 @@ func main() { cfg.DisableSecretCreation, subscriptionChecker, serviceAccountNames, - cfg.EnableK8sEvents, ) if mode == common.ModeAgent { @@ -646,7 +644,7 @@ func main() { eventsEmitter.Loader.Register(agentHandle) } - api.InitEvents() + api.Init(cfg.CDEventsTarget, cfg.EnableK8sEvents) if !cfg.DisableTestTriggers { triggerService := triggers.NewService( sched, @@ -880,6 +878,7 @@ func newProContext(cfg *config.Config, grpcClient cloud.TestKubeCloudAPIClient) OrgID: cfg.TestkubeProOrgID, Migrate: cfg.TestkubeProMigrate, ConnectionTimeout: cfg.TestkubeProConnectionTimeout, + DashboardURI: cfg.TestkubeDashboardURI, } if grpcClient == nil { diff --git a/docs/docs/articles/webhooks.mdx b/docs/docs/articles/webhooks.mdx index c125ab12964..ea0830839e3 100644 --- a/docs/docs/articles/webhooks.mdx +++ b/docs/docs/articles/webhooks.mdx @@ -410,6 +410,17 @@ The full TestSuiteExecution data model can be found [here](https://github.com/ku The full TestWorkflowExecution data model can be found [here](https://github.com/kubeshop/testkube/blob/main/pkg/api/v1/testkube/model_test_workflow_execution.go). +### Additional Top-level Variables: + +- `ExecutionCommand` - The CLI command to access the execution (example: `kubectl testkube get execution 6679893e3b11f4e4900e17a5`). +- `ExecutionURL` - The dashboard URL to look at the execution (example: `https://app.testkube.io/organization/tkcorg_9deb42dda2197657/environment/tkcenv_1145999f3c4d1115/dashboard/tests/git-zap-api-test/executions/6679893e3b11f4e4900e17a5`). +- `ArtifactsCommand` - The CLI command to access the artifacts (example: `kubectl testkube get artifacts 6679893e3b11f4e4900e17a5`). +- `ArtifactsURL` - The dashboard URL to look at the artifacts directly (example: `https://app.testkube.io/organization/tkcorg_9deb42dda2197657/environment/tkcenv_1145999f3c4d1115/dashboard/tests/git-zap-api-test/executions/6679893e3b11f4e4900e17a5/artifacts`). +- `LogsCommand` - The CLI command to access the logs (example: `kubectl testkube get execution 6679893e3b11f4e4900e17a5 --logs-only`). +- `LogsURL` - The dashboard URL to look at the logs (example: `https://app.testkube.io/organization/tkcorg_9deb42dda2197657/environment/tkcenv_1145999f3c4d1115/dashboard/tests/git-zap-api-test/executions/6679893e3b11f4e4900e17a5/log-output`). + +Make sure that the value `testkube-api.dashboardUri` in the helm-charts is set to `app.testkube.io`, or your remote dashboard URL, so that the variables above can be populated with the correct values. + ## Additional Examples ### Microsoft Teams diff --git a/internal/app/api/v1/server.go b/internal/app/api/v1/server.go index 1daba0e6aa1..2b60e1f6045 100644 --- a/internal/app/api/v1/server.go +++ b/internal/app/api/v1/server.go @@ -95,7 +95,6 @@ func NewTestkubeAPI( graphqlPort string, artifactsStorage storage.ArtifactsStorage, templatesClient *templatesclientv1.TemplatesClient, - cdeventsTarget string, dashboardURI string, helmchartVersion string, mode string, @@ -107,7 +106,6 @@ func NewTestkubeAPI( disableSecretCreation bool, subscriptionChecker checktcl.SubscriptionChecker, serviceAccountNames map[string]string, - enableK8sEvents bool, ) TestkubeAPI { var httpConfig server.Config @@ -123,7 +121,7 @@ func NewTestkubeAPI( httpConfig.Http.BodyLimit = DefaultHttpBodyLimit } - s := TestkubeAPI{ + return TestkubeAPI{ HTTPServer: server.NewServer(httpConfig), TestExecutionResults: testSuiteExecutionsResults, ExecutionResults: testExecutionResults, @@ -165,31 +163,6 @@ func NewTestkubeAPI( LabelSources: common.Ptr(make([]LabelSource, 0)), ServiceAccountNames: serviceAccountNames, } - - // will be reused in websockets handler - s.WebsocketLoader = ws.NewWebsocketLoader() - - s.Events.Loader.Register(webhook.NewWebhookLoader(s.Log, webhookClient, templatesClient, testExecutionResults, testSuiteExecutionsResults, testWorkflowResults, metrics)) - s.Events.Loader.Register(s.WebsocketLoader) - s.Events.Loader.Register(s.slackLoader) - - if cdeventsTarget != "" { - cdeventLoader, err := cdevent.NewCDEventLoader(cdeventsTarget, clusterId, namespace, dashboardURI, testkube.AllEventTypes) - if err == nil { - s.Events.Loader.Register(cdeventLoader) - } else { - s.Log.Debug("cdevents init error", "error", err.Error()) - } - } - - if enableK8sEvents { - s.Events.Loader.Register(k8sevent.NewK8sEventLoader(clientset, namespace, testkube.AllEventTypes)) - } - - s.InitEnvs() - s.InitRoutes() - - return s } type TestkubeAPI struct { @@ -298,6 +271,27 @@ func (s TestkubeAPI) SendTelemetryStartEvent(ctx context.Context, ch chan struct }() } +func (s *TestkubeAPI) Init(cdEventsTarget string, enableK8sEvents bool) { + s.InitEventListeners( + s.proContext, + s.WebhooksClient, + s.TemplatesClient, + s.ExecutionResults, + s.TestExecutionResults, + s.TestWorkflowResults, + s.Metrics, + cdEventsTarget, + s.Config.ClusterID, + s.Namespace, + s.dashboardURI, + enableK8sEvents, + s.Clientset, + ) + s.InitEnvs() + s.InitRoutes() + s.InitEvents() +} + // InitEnvs initializes api server settings func (s *TestkubeAPI) InitEnvs() { if err := envconfig.Process("STORAGE", &s.storageParams); err != nil { @@ -590,6 +584,44 @@ func (s *TestkubeAPI) InitRoutes() { }) } +func (s TestkubeAPI) InitEventListeners( + proContext *config.ProContext, + webhookClient *executorsclientv1.WebhooksClient, + templatesClient *templatesclientv1.TemplatesClient, + testExecutionResults result.Repository, + testSuiteExecutionsResults testresult.Repository, + testWorkflowResults testworkflow.Repository, + metrics metrics.Metrics, + cdeventsTarget string, + clusterId string, + namespace string, + dashboardURI string, + enableK8sEvents bool, + clientset kubernetes.Interface, +) { + // will be reused in websockets handler + s.WebsocketLoader = ws.NewWebsocketLoader() + + s.Events.Loader.Register(webhook.NewWebhookLoader( + s.Log, webhookClient, templatesClient, testExecutionResults, testSuiteExecutionsResults, + testWorkflowResults, metrics, s.proContext)) + s.Events.Loader.Register(s.WebsocketLoader) + s.Events.Loader.Register(s.slackLoader) + + if cdeventsTarget != "" { + cdeventLoader, err := cdevent.NewCDEventLoader(cdeventsTarget, clusterId, namespace, dashboardURI, testkube.AllEventTypes) + if err == nil { + s.Events.Loader.Register(cdeventLoader) + } else { + s.Log.Debug("cdevents init error", "error", err.Error()) + } + } + + if enableK8sEvents { + s.Events.Loader.Register(k8sevent.NewK8sEventLoader(clientset, namespace, testkube.AllEventTypes)) + } +} + func (s TestkubeAPI) StartTelemetryHeartbeats(ctx context.Context, ch chan struct{}) { go func() { <-ch diff --git a/internal/config/procontext.go b/internal/config/procontext.go index a62c52340f9..aa3b090be81 100644 --- a/internal/config/procontext.go +++ b/internal/config/procontext.go @@ -12,4 +12,5 @@ type ProContext struct { OrgID string Migrate string ConnectionTimeout int + DashboardURI string } diff --git a/pkg/event/kind/webhook/listener.go b/pkg/event/kind/webhook/listener.go index 9966f72e43d..603d3c73520 100644 --- a/pkg/event/kind/webhook/listener.go +++ b/pkg/event/kind/webhook/listener.go @@ -13,6 +13,7 @@ import ( "go.uber.org/zap" v1 "github.com/kubeshop/testkube/internal/app/api/metrics" + "github.com/kubeshop/testkube/internal/config" "github.com/kubeshop/testkube/pkg/api/v1/testkube" "github.com/kubeshop/testkube/pkg/event/kind/common" thttp "github.com/kubeshop/testkube/pkg/http" @@ -31,7 +32,9 @@ func NewWebhookListener(name, uri, selector string, events []testkube.EventType, onStateChange bool, testExecutionResults result.Repository, testSuiteExecutionResults testresult.Repository, - testWorkflowExecutionResults testworkflow.Repository, metrics v1.Metrics) *WebhookListener { + testWorkflowExecutionResults testworkflow.Repository, + metrics v1.Metrics, + proContext *config.ProContext) *WebhookListener { return &WebhookListener{ name: name, Uri: uri, @@ -48,6 +51,7 @@ func NewWebhookListener(name, uri, selector string, events []testkube.EventType, testSuiteExecutionResults: testSuiteExecutionResults, testWorkflowExecutionResults: testWorkflowExecutionResults, metrics: metrics, + proContext: proContext, } } @@ -67,6 +71,7 @@ type WebhookListener struct { testSuiteExecutionResults testresult.Repository testWorkflowExecutionResults testworkflow.Repository metrics v1.Metrics + proContext *config.ProContext } func (l *WebhookListener) Name() string { @@ -261,7 +266,7 @@ func (l *WebhookListener) processTemplate(field, body string, event testkube.Eve } var buffer bytes.Buffer - if err = tmpl.ExecuteTemplate(&buffer, field, event); err != nil { + if err = tmpl.ExecuteTemplate(&buffer, field, NewTemplateVars(event, l.proContext)); err != nil { log.Errorw(fmt.Sprintf("executing webhook %s error", field), "error", err) return nil, err } diff --git a/pkg/event/kind/webhook/listener_test.go b/pkg/event/kind/webhook/listener_test.go index 36b424370b3..e0ba0fa7d27 100644 --- a/pkg/event/kind/webhook/listener_test.go +++ b/pkg/event/kind/webhook/listener_test.go @@ -34,7 +34,7 @@ func TestWebhookListener_Notify(t *testing.T) { svr := httptest.NewServer(testHandler) defer svr.Close() - l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil, false, false, nil, nil, nil, v1.NewMetrics()) + l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil, false, false, nil, nil, nil, v1.NewMetrics(), nil) // when r := l.Notify(testkube.Event{ @@ -56,7 +56,7 @@ func TestWebhookListener_Notify(t *testing.T) { svr := httptest.NewServer(testHandler) defer svr.Close() - l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil, false, false, nil, nil, nil, v1.NewMetrics()) + l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil, false, false, nil, nil, nil, v1.NewMetrics(), nil) // when r := l.Notify(testkube.Event{ @@ -73,7 +73,7 @@ func TestWebhookListener_Notify(t *testing.T) { t.Parallel() // given - s := NewWebhookListener("l1", "http://baduri.badbadbad", "", testEventTypes, "", "", nil, false, false, nil, nil, nil, v1.NewMetrics()) + s := NewWebhookListener("l1", "http://baduri.badbadbad", "", testEventTypes, "", "", nil, false, false, nil, nil, nil, v1.NewMetrics(), nil) // when r := s.Notify(testkube.Event{ @@ -106,7 +106,7 @@ func TestWebhookListener_Notify(t *testing.T) { svr := httptest.NewServer(testHandler) defer svr.Close() - l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "field", "", nil, false, false, nil, nil, nil, v1.NewMetrics()) + l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "field", "", nil, false, false, nil, nil, nil, v1.NewMetrics(), nil) // when r := l.Notify(testkube.Event{ @@ -133,7 +133,7 @@ func TestWebhookListener_Notify(t *testing.T) { defer svr.Close() l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "{\"id\": \"{{ .Id }}\"}", - map[string]string{"Content-Type": "application/json"}, false, false, nil, nil, nil, v1.NewMetrics()) + map[string]string{"Content-Type": "application/json"}, false, false, nil, nil, nil, v1.NewMetrics(), nil) // when r := l.Notify(testkube.Event{ @@ -150,7 +150,7 @@ func TestWebhookListener_Notify(t *testing.T) { t.Parallel() // given - s := NewWebhookListener("l1", "http://baduri.badbadbad", "", testEventTypes, "", "", nil, true, false, nil, nil, nil, v1.NewMetrics()) + s := NewWebhookListener("l1", "http://baduri.badbadbad", "", testEventTypes, "", "", nil, true, false, nil, nil, nil, v1.NewMetrics(), nil) // when r := s.Notify(testkube.Event{ diff --git a/pkg/event/kind/webhook/loader.go b/pkg/event/kind/webhook/loader.go index 098383d0682..fb78fca9479 100644 --- a/pkg/event/kind/webhook/loader.go +++ b/pkg/event/kind/webhook/loader.go @@ -8,6 +8,7 @@ import ( executorsv1 "github.com/kubeshop/testkube-operator/api/executor/v1" templatesclientv1 "github.com/kubeshop/testkube-operator/pkg/client/templates/v1" v1 "github.com/kubeshop/testkube/internal/app/api/metrics" + "github.com/kubeshop/testkube/internal/config" "github.com/kubeshop/testkube/pkg/api/v1/testkube" "github.com/kubeshop/testkube/pkg/event/kind/common" "github.com/kubeshop/testkube/pkg/mapper/webhooks" @@ -25,7 +26,7 @@ type WebhooksLister interface { func NewWebhookLoader(log *zap.SugaredLogger, webhooksClient WebhooksLister, templatesClient templatesclientv1.Interface, testExecutionResults result.Repository, testSuiteExecutionResults testresult.Repository, testWorkflowExecutionResults testworkflow.Repository, - metrics v1.Metrics, + metrics v1.Metrics, proContext *config.ProContext, ) *WebhooksLoader { return &WebhooksLoader{ log: log, @@ -35,6 +36,7 @@ func NewWebhookLoader(log *zap.SugaredLogger, webhooksClient WebhooksLister, tem testSuiteExecutionResults: testSuiteExecutionResults, testWorkflowExecutionResults: testWorkflowExecutionResults, metrics: metrics, + proContext: proContext, } } @@ -46,6 +48,7 @@ type WebhooksLoader struct { testSuiteExecutionResults testresult.Repository testWorkflowExecutionResults testworkflow.Repository metrics v1.Metrics + proContext *config.ProContext } func (r WebhooksLoader) Kind() string { @@ -83,7 +86,8 @@ func (r WebhooksLoader) Load() (listeners common.Listeners, err error) { name := fmt.Sprintf("%s.%s", webhook.ObjectMeta.Namespace, webhook.ObjectMeta.Name) listeners = append(listeners, NewWebhookListener(name, webhook.Spec.Uri, webhook.Spec.Selector, types, webhook.Spec.PayloadObjectField, payloadTemplate, webhook.Spec.Headers, webhook.Spec.Disabled, - webhook.Spec.OnStateChange, r.testExecutionResults, r.testSuiteExecutionResults, r.testWorkflowExecutionResults, r.metrics)) + webhook.Spec.OnStateChange, r.testExecutionResults, r.testSuiteExecutionResults, r.testWorkflowExecutionResults, + r.metrics, r.proContext)) } return listeners, nil diff --git a/pkg/event/kind/webhook/loader_test.go b/pkg/event/kind/webhook/loader_test.go index 24afc54b029..d4709c899d3 100644 --- a/pkg/event/kind/webhook/loader_test.go +++ b/pkg/event/kind/webhook/loader_test.go @@ -30,7 +30,7 @@ func TestWebhookLoader(t *testing.T) { defer mockCtrl.Finish() mockTemplatesClient := templatesclientv1.NewMockInterface(mockCtrl) - webhooksLoader := NewWebhookLoader(zap.NewNop().Sugar(), &DummyLoader{}, mockTemplatesClient, nil, nil, nil, v1.NewMetrics()) + webhooksLoader := NewWebhookLoader(zap.NewNop().Sugar(), &DummyLoader{}, mockTemplatesClient, nil, nil, nil, v1.NewMetrics(), nil) listeners, err := webhooksLoader.Load() assert.Equal(t, 1, len(listeners)) diff --git a/pkg/event/kind/webhook/templatevars.go b/pkg/event/kind/webhook/templatevars.go new file mode 100644 index 00000000000..391244f62af --- /dev/null +++ b/pkg/event/kind/webhook/templatevars.go @@ -0,0 +1,56 @@ +package webhook + +import ( + "fmt" + + "github.com/kubeshop/testkube/internal/config" + "github.com/kubeshop/testkube/pkg/api/v1/testkube" +) + +type TemplateVars struct { + testkube.Event + ExecutionURL string + ExecutionCommand string + ArtifactURL string + ArtifactCommand string + LogsURL string + LogsCommand string +} + +func NewTemplateVars(event testkube.Event, proContext *config.ProContext) TemplateVars { + vars := TemplateVars{ + Event: event, + } + + switch { + case event.TestExecution != nil: + vars.ExecutionCommand = fmt.Sprintf("kubectl testkube get execution %s", event.TestExecution.Id) + vars.ArtifactCommand = fmt.Sprintf("kubectl testkube get artifacts %s", event.TestExecution.Id) + vars.LogsCommand = fmt.Sprintf("kubectl testkube get execution %s --logs-only", event.TestExecution.Id) + case event.TestSuiteExecution != nil: + vars.ExecutionCommand = fmt.Sprintf("kubectl testkube get testsuiteexecution %s", event.TestSuiteExecution.Id) + case event.TestWorkflowExecution != nil: + vars.ExecutionCommand = fmt.Sprintf("kubectl testkube get testworkflowexecution %s", event.TestWorkflowExecution.Id) + vars.ArtifactCommand = fmt.Sprintf("kubectl testkube get artifacts %s", event.TestWorkflowExecution.Id) + vars.LogsCommand = fmt.Sprintf("kubectl testkube get testworkflowexecution %s", event.TestWorkflowExecution.Id) + } + + if proContext == nil || proContext.DashboardURI == "" || proContext.OrgID == "" || proContext.EnvID == "" { + return vars + } + + switch { + case event.TestExecution != nil: + vars.ExecutionURL = fmt.Sprintf("https://%s/organization/%s/environment/%s/dashboard/tests/%s/executions/%s", proContext.DashboardURI, proContext.OrgID, proContext.EnvID, event.TestExecution.TestName, event.TestExecution.Id) + vars.ArtifactURL = fmt.Sprintf("%s/artifacts", vars.ExecutionURL) + vars.LogsURL = fmt.Sprintf("%s/log-output", vars.ExecutionURL) + case event.TestSuiteExecution != nil: + vars.ExecutionURL = fmt.Sprintf("https://%s/organization/%s/environment/%s/dashboard/test-suites/%s/executions/%s", proContext.DashboardURI, proContext.OrgID, proContext.EnvID, event.TestSuiteExecution.TestSuite.Name, event.TestSuiteExecution.Id) + case event.TestWorkflowExecution != nil: + vars.ExecutionURL = fmt.Sprintf("https://%s/organization/%s/environment/%s/dashboard/test-workflows/%s/executions/%s", proContext.DashboardURI, proContext.OrgID, proContext.EnvID, event.TestWorkflowExecution.Workflow.Name, event.TestWorkflowExecution.Id) + vars.ArtifactURL = fmt.Sprintf("%s/artifacts", vars.ExecutionURL) + vars.LogsURL = fmt.Sprintf("%s/log-output", vars.ExecutionURL) + } + + return vars +}