From 441a6316a2e8194550d3ca3b401de664363bff5a Mon Sep 17 00:00:00 2001 From: Dawid Rusnak Date: Thu, 27 Jun 2024 12:51:30 +0200 Subject: [PATCH 01/21] fix: 'fetch' operation for parallel steps (#5615) --- cmd/tcl/testworkflow-toolkit/spawn/utils.go | 2 ++ pkg/testworkflows/testworkflowprocessor/utils.go | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cmd/tcl/testworkflow-toolkit/spawn/utils.go b/cmd/tcl/testworkflow-toolkit/spawn/utils.go index df3cf93ed24..75713eb6794 100644 --- a/cmd/tcl/testworkflow-toolkit/spawn/utils.go +++ b/cmd/tcl/testworkflow-toolkit/spawn/utils.go @@ -33,6 +33,7 @@ import ( "github.com/kubeshop/testkube/pkg/api/v1/testkube" "github.com/kubeshop/testkube/pkg/expressions" "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowcontroller" + "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor" "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/constants" ) @@ -173,6 +174,7 @@ func ProcessFetch(transferSrv transfer.Server, fetch []testworkflowsv1.StepParal Env: []corev1.EnvVar{ {Name: "TK_NS", Value: env.Namespace()}, {Name: "TK_REF", Value: env.Ref()}, + testworkflowprocessor.BypassToolkitCheck, }, Args: &result, }, diff --git a/pkg/testworkflows/testworkflowprocessor/utils.go b/pkg/testworkflows/testworkflowprocessor/utils.go index 58e475fb03f..cd0fa239e7a 100644 --- a/pkg/testworkflows/testworkflowprocessor/utils.go +++ b/pkg/testworkflows/testworkflowprocessor/utils.go @@ -10,9 +10,15 @@ import ( "github.com/kubeshop/testkube/internal/common" "github.com/kubeshop/testkube/pkg/expressions" + "github.com/kubeshop/testkube/pkg/rand" "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/constants" ) +var BypassToolkitCheck = corev1.EnvVar{ + Name: "TK_TC_SECURITY", + Value: rand.String(20), +} + func AnnotateControlledBy(obj metav1.Object, rootId, id string) { labels := obj.GetLabels() if labels == nil { @@ -122,8 +128,12 @@ func buildKubernetesContainers(stage Stage, init *initProcess, fsGroup *int64, m init.ResetResults() } + bypass := false refEnvVar := "" for _, e := range cr.Env { + if e.Name == BypassToolkitCheck.Name && e.Value == BypassToolkitCheck.Value { + bypass = true + } if e.Name == "TK_REF" { refEnvVar = e.Value } @@ -135,7 +145,7 @@ func buildKubernetesContainers(stage Stage, init *initProcess, fsGroup *int64, m SetCommand(cr.Command...). SetArgs(cr.Args...). SetWorkingDir(cr.WorkingDir). - SetToolkit(cr.Image == constants.DefaultToolkitImage && c.Ref() == refEnvVar) + SetToolkit(bypass || (cr.Image == constants.DefaultToolkitImage && c.Ref() == refEnvVar)) for _, env := range cr.Env { if strings.Contains(env.Value, "{{") { From 353458fb8694edaaa320532cab464cec2b71557c Mon Sep 17 00:00:00 2001 From: Dejan Zele Pejchev Date: Thu, 27 Jun 2024 21:33:34 +0200 Subject: [PATCH 02/21] whitelist containers for which logs should be extracted (#5612) --- cmd/api-server/main.go | 2 + internal/config/config.go | 62 ++++++++++--------- pkg/executor/client/job.go | 32 +++++++--- pkg/executor/common.go | 21 ++++++- pkg/executor/common_test.go | 42 +++++++++++-- .../containerexecutor/containerexecutor.go | 8 ++- pkg/executor/containerexecutor/logs.go | 10 ++- 7 files changed, 127 insertions(+), 50 deletions(-) diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index d52501c63b7..2309693b589 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -463,6 +463,7 @@ func main() { logsStream, features, cfg.TestkubeDefaultStorageClassName, + cfg.WhitelistedContainers, ) if err != nil { exitOnError("Creating executor client", err) @@ -509,6 +510,7 @@ func main() { logsStream, features, cfg.TestkubeDefaultStorageClassName, + cfg.WhitelistedContainers, ) if err != nil { exitOnError("Creating container executor", err) diff --git a/internal/config/config.go b/internal/config/config.go index b3a10a6397f..b07e70ace13 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,36 +7,38 @@ import ( ) type Config struct { - APIServerPort string `envconfig:"APISERVER_PORT" default:"8088"` - APIServerConfig string `envconfig:"APISERVER_CONFIG" default:""` - APIServerFullname string `envconfig:"APISERVER_FULLNAME" default:"testkube-api-server"` - APIMongoDSN string `envconfig:"API_MONGO_DSN" default:"mongodb://localhost:27017"` - APIMongoAllowTLS bool `envconfig:"API_MONGO_ALLOW_TLS" default:"false"` - APIMongoSSLCert string `envconfig:"API_MONGO_SSL_CERT" default:""` - APIMongoSSLCAFileKey string `envconfig:"API_MONGO_SSL_CA_FILE_KEY" default:"sslCertificateAuthorityFile"` - APIMongoSSLClientFileKey string `envconfig:"API_MONGO_SSL_CLIENT_FILE_KEY" default:"sslClientCertificateKeyFile"` - APIMongoSSLClientFilePass string `envconfig:"API_MONGO_SSL_CLIENT_FILE_PASS_KEY" default:"sslClientCertificateKeyFilePassword"` - APIMongoAllowDiskUse bool `envconfig:"API_MONGO_ALLOW_DISK_USE" default:"false"` - APIMongoDB string `envconfig:"API_MONGO_DB" default:"testkube"` - APIMongoDBType string `envconfig:"API_MONGO_DB_TYPE" default:"mongo"` - SlackToken string `envconfig:"SLACK_TOKEN" default:""` - SlackConfig string `envconfig:"SLACK_CONFIG" default:""` - SlackTemplate string `envconfig:"SLACK_TEMPLATE" default:""` - StorageEndpoint string `envconfig:"STORAGE_ENDPOINT" default:"localhost:9000"` - StorageBucket string `envconfig:"STORAGE_BUCKET" default:"testkube-logs"` - StorageExpiration int `envconfig:"STORAGE_EXPIRATION"` - StorageAccessKeyID string `envconfig:"STORAGE_ACCESSKEYID" default:""` - StorageSecretAccessKey string `envconfig:"STORAGE_SECRETACCESSKEY" default:""` - StorageRegion string `envconfig:"STORAGE_REGION" default:""` - StorageToken string `envconfig:"STORAGE_TOKEN" default:""` - StorageSSL bool `envconfig:"STORAGE_SSL" default:"false"` - StorageSkipVerify bool `envconfig:"STORAGE_SKIP_VERIFY" default:"false"` - StorageCertFile string `envconfig:"STORAGE_CERT_FILE" default:""` - StorageKeyFile string `envconfig:"STORAGE_KEY_FILE" default:""` - StorageCAFile string `envconfig:"STORAGE_CA_FILE" default:""` - ScrapperEnabled bool `envconfig:"SCRAPPERENABLED" default:"false"` - LogsBucket string `envconfig:"LOGS_BUCKET" default:""` - LogsStorage string `envconfig:"LOGS_STORAGE" default:""` + APIServerPort string `envconfig:"APISERVER_PORT" default:"8088"` + APIServerConfig string `envconfig:"APISERVER_CONFIG" default:""` + APIServerFullname string `envconfig:"APISERVER_FULLNAME" default:"testkube-api-server"` + APIMongoDSN string `envconfig:"API_MONGO_DSN" default:"mongodb://localhost:27017"` + APIMongoAllowTLS bool `envconfig:"API_MONGO_ALLOW_TLS" default:"false"` + APIMongoSSLCert string `envconfig:"API_MONGO_SSL_CERT" default:""` + APIMongoSSLCAFileKey string `envconfig:"API_MONGO_SSL_CA_FILE_KEY" default:"sslCertificateAuthorityFile"` + APIMongoSSLClientFileKey string `envconfig:"API_MONGO_SSL_CLIENT_FILE_KEY" default:"sslClientCertificateKeyFile"` + APIMongoSSLClientFilePass string `envconfig:"API_MONGO_SSL_CLIENT_FILE_PASS_KEY" default:"sslClientCertificateKeyFilePassword"` + APIMongoAllowDiskUse bool `envconfig:"API_MONGO_ALLOW_DISK_USE" default:"false"` + APIMongoDB string `envconfig:"API_MONGO_DB" default:"testkube"` + APIMongoDBType string `envconfig:"API_MONGO_DB_TYPE" default:"mongo"` + SlackToken string `envconfig:"SLACK_TOKEN" default:""` + SlackConfig string `envconfig:"SLACK_CONFIG" default:""` + SlackTemplate string `envconfig:"SLACK_TEMPLATE" default:""` + StorageEndpoint string `envconfig:"STORAGE_ENDPOINT" default:"localhost:9000"` + StorageBucket string `envconfig:"STORAGE_BUCKET" default:"testkube-logs"` + StorageExpiration int `envconfig:"STORAGE_EXPIRATION"` + StorageAccessKeyID string `envconfig:"STORAGE_ACCESSKEYID" default:""` + StorageSecretAccessKey string `envconfig:"STORAGE_SECRETACCESSKEY" default:""` + StorageRegion string `envconfig:"STORAGE_REGION" default:""` + StorageToken string `envconfig:"STORAGE_TOKEN" default:""` + StorageSSL bool `envconfig:"STORAGE_SSL" default:"false"` + StorageSkipVerify bool `envconfig:"STORAGE_SKIP_VERIFY" default:"false"` + StorageCertFile string `envconfig:"STORAGE_CERT_FILE" default:""` + StorageKeyFile string `envconfig:"STORAGE_KEY_FILE" default:""` + StorageCAFile string `envconfig:"STORAGE_CA_FILE" default:""` + ScrapperEnabled bool `envconfig:"SCRAPPERENABLED" default:"false"` + LogsBucket string `envconfig:"LOGS_BUCKET" default:""` + LogsStorage string `envconfig:"LOGS_STORAGE" default:""` + // WhitelistedContainers is a list of containers from which logs should be collected. + WhitelistedContainers []string `envconfig:"WHITELISTED_CONTAINERS" default:"init,logs,scraper"` NatsEmbedded bool `envconfig:"NATS_EMBEDDED" default:"false"` NatsEmbeddedStoreDir string `envconfig:"NATS_EMBEDDED_STORE_DIR" default:"/app/nats"` NatsURI string `envconfig:"NATS_URI" default:"nats://localhost:4222"` diff --git a/pkg/executor/client/job.go b/pkg/executor/client/job.go index 2c5ca82a008..f1350658a62 100644 --- a/pkg/executor/client/job.go +++ b/pkg/executor/client/job.go @@ -100,6 +100,7 @@ func NewJobExecutor( logsStream logsclient.Stream, features featureflags.FeatureFlags, defaultStorageClassName string, + whitelistedContainers []string, ) (client *JobExecutor, err error) { if serviceAccountNames == nil { serviceAccountNames = make(map[string]string) @@ -128,6 +129,7 @@ func NewJobExecutor( logsStream: logsStream, features: features, defaultStorageClassName: defaultStorageClassName, + whitelistedContainers: whitelistedContainers, }, nil } @@ -160,6 +162,8 @@ type JobExecutor struct { logsStream logsclient.Stream features featureflags.FeatureFlags defaultStorageClassName string + // whitelistedContainers is a list of containers from which logs are allowed to be streamed. + whitelistedContainers []string } type JobOptions struct { @@ -413,9 +417,9 @@ func (c *JobExecutor) updateResultsFromPod(ctx context.Context, pod corev1.Pod, } l.Debug("poll immediate end") - c.streamLog(ctx, execution.Id, events.NewLog("analyzing test results and artfacts")) + c.streamLog(ctx, execution.Id, events.NewLog("analyzing test results and artifacts")) - logs, err := executor.GetPodLogs(ctx, c.ClientSet, execution.TestNamespace, pod) + logs, err := executor.GetPodLogs(ctx, c.ClientSet, execution.TestNamespace, pod, execution.Id, c.whitelistedContainers) if err != nil { l.Errorw("get pod logs error", "error", err) c.streamLog(ctx, execution.Id, events.NewErrorLog(err)) @@ -667,12 +671,12 @@ func (c *JobExecutor) TailJobLogs(ctx context.Context, id, namespace string, log case corev1.PodRunning: l.Debug("tailing pod logs: immediately") - return c.TailPodLogs(ctx, pod, logs) + return c.TailPodLogs(ctx, id, pod, logs) case corev1.PodFailed: err := errors.Errorf("can't get pod logs, pod failed: %s/%s", pod.Namespace, pod.Name) l.Errorw(err.Error()) - return c.GetLastLogLineError(ctx, pod) + return c.GetLastLogLineError(ctx, pod, id) default: l.Debugw("tailing job logs: waiting for pod to be ready") @@ -682,7 +686,7 @@ func (c *JobExecutor) TailJobLogs(ctx context.Context, id, namespace string, log } l.Debug("tailing pod logs") - return c.TailPodLogs(ctx, pod, logs) + return c.TailPodLogs(ctx, id, pod, logs) } } } @@ -690,7 +694,9 @@ func (c *JobExecutor) TailJobLogs(ctx context.Context, id, namespace string, log return } -func (c *JobExecutor) TailPodLogs(ctx context.Context, pod corev1.Pod, logs chan []byte) (err error) { +// TailPodLogs returns logs from all containers in pod in parallel. +// id parameter corresponds to the test execution id, and test pod containers are prefixed by it. +func (c *JobExecutor) TailPodLogs(ctx context.Context, id string, pod corev1.Pod, logs chan []byte) (err error) { var containers []string for _, container := range pod.Spec.InitContainers { containers = append(containers, container.Name) @@ -706,6 +712,10 @@ func (c *JobExecutor) TailPodLogs(ctx context.Context, pod corev1.Pod, logs chan wg.Add(len(containers)) for _, container := range containers { + if !executor.IsWhitelistedContainer(container, id, c.whitelistedContainers) { + wg.Done() + continue + } go func(container string) { defer wg.Done() @@ -751,15 +761,17 @@ func (c *JobExecutor) TailPodLogs(ctx context.Context, pod corev1.Pod, logs chan } // GetPodLogError returns last line as error -func (c *JobExecutor) GetPodLogError(ctx context.Context, pod corev1.Pod) (logsBytes []byte, err error) { +// id parameter corresponds to the test execution id, and test pod containers are prefixed by it. +func (c *JobExecutor) GetPodLogError(ctx context.Context, pod corev1.Pod, id string) (logsBytes []byte, err error) { // error line should be last one - return executor.GetPodLogs(ctx, c.ClientSet, pod.Namespace, pod, 1) + return executor.GetPodLogs(ctx, c.ClientSet, pod.Namespace, pod, id, c.whitelistedContainers, 1) } // GetLastLogLineError return error if last line is failed -func (c *JobExecutor) GetLastLogLineError(ctx context.Context, pod corev1.Pod) error { +// id parameter corresponds to the test execution id, and test pod containers are prefixed by it. +func (c *JobExecutor) GetLastLogLineError(ctx context.Context, pod corev1.Pod, id string) error { l := c.Log.With("pod", pod.Name, "namespace", pod.Namespace) - errorLog, err := c.GetPodLogError(ctx, pod) + errorLog, err := c.GetPodLogError(ctx, pod, id) if err != nil { l.Errorw("getPodLogs error", "error", err, "pod", pod) return errors.Errorf("getPodLogs error: %v", err) diff --git a/pkg/executor/common.go b/pkg/executor/common.go index 33720f81d90..668e83708bb 100644 --- a/pkg/executor/common.go +++ b/pkg/executor/common.go @@ -372,7 +372,7 @@ func GetJobPods(ctx context.Context, podsClient tcorev1.PodInterface, jobName st } // GetPodLogs returns pod logs bytes -func GetPodLogs(ctx context.Context, c kubernetes.Interface, namespace string, pod corev1.Pod, logLinesCount ...int64) (logs []byte, err error) { +func GetPodLogs(ctx context.Context, c kubernetes.Interface, namespace string, pod corev1.Pod, id string, whitelistedContainers []string, logLinesCount ...int64) (logs []byte, err error) { var count int64 = defaultLogLinesCount if len(logLinesCount) > 0 { count = logLinesCount[0] @@ -388,6 +388,9 @@ func GetPodLogs(ctx context.Context, c kubernetes.Interface, namespace string, p } for _, container := range containers { + if !IsWhitelistedContainer(container, id, whitelistedContainers) { + continue + } containerLogs, err := GetContainerLogs(ctx, c, &pod, container, namespace, &count) if err != nil { if errors.Is(err, ErrPodInitializing) { @@ -618,3 +621,19 @@ func GetPodEventsSummary(ctx context.Context, client kubernetes.Interface, pod * return message, nil } + +func IsWhitelistedContainer(containerName string, id string, whitelistedContainers []string) bool { + if containerName == id { + return true + } + withID := func(name string) string { + return fmt.Sprintf("%s-%s", id, name) + } + for _, whitelistedContainer := range whitelistedContainers { + if containerName == withID(whitelistedContainer) { + return true + } + } + + return false +} diff --git a/pkg/executor/common_test.go b/pkg/executor/common_test.go index cf5eaed0974..67df41a0b5c 100644 --- a/pkg/executor/common_test.go +++ b/pkg/executor/common_test.go @@ -2,6 +2,7 @@ package executor import ( "context" + "fmt" "reflect" "testing" @@ -99,12 +100,12 @@ func TestGetPodLogs(t *testing.T) { Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ { - Name: "init_container", + Name: "1234-init", }, }, Containers: []corev1.Container{ { - Name: "first_container", + Name: "1234", }, { Name: "second_container", @@ -113,13 +114,14 @@ func TestGetPodLogs(t *testing.T) { }, }, }, - wantLogs: []byte("fake logsfake logsfake logs"), + wantLogs: []byte("fake logsfake logs"), wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotLogs, err := GetPodLogs(context.Background(), tt.args.c, tt.args.namespace, tt.args.pod, tt.args.logLinesCount...) + whitelistedContainers := []string{"logs", "init", "scraper"} + gotLogs, err := GetPodLogs(context.Background(), tt.args.c, tt.args.namespace, tt.args.pod, "1234", whitelistedContainers, tt.args.logLinesCount...) if (err != nil) != tt.wantErr { t.Errorf("GetPodLogs() error = %v, wantErr %v", err, tt.wantErr) return @@ -130,3 +132,35 @@ func TestGetPodLogs(t *testing.T) { }) } } + +func TestIsWhitelistedContainer(t *testing.T) { + t.Parallel() + + tests := []struct { + containerName string + id string + expected bool + }{ + {"mycontainer", "mycontainer", true}, + {"mycontainer-init", "mycontainer", true}, + {"mycontainer-scraper", "mycontainer", true}, + {"mycontainer-logs", "mycontainer", true}, + {"anothercontainer", "mycontainer", false}, + {"istio-init", "mycontainer", false}, + {"istio-proxy", "mycontainer", false}, + {"scraper-mycontainer", "mycontainer", false}, + {"logs-mycontainer", "mycontainer", false}, + {"", "mycontainer", false}, + {"mycontainer", "", false}, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("containerName: %s, id: %s", tt.containerName, tt.id), func(t *testing.T) { + whitelisted := []string{"logs", "init", "scraper"} + result := IsWhitelistedContainer(tt.containerName, tt.id, whitelisted) + if result != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} diff --git a/pkg/executor/containerexecutor/containerexecutor.go b/pkg/executor/containerexecutor/containerexecutor.go index 87b169ec12e..0da5513b1e7 100644 --- a/pkg/executor/containerexecutor/containerexecutor.go +++ b/pkg/executor/containerexecutor/containerexecutor.go @@ -84,6 +84,7 @@ func NewContainerExecutor( logsStream logsclient.Stream, features featureflags.FeatureFlags, defaultStorageClassName string, + whitelistedContainers []string, ) (client *ContainerExecutor, err error) { clientSet, err := k8sclient.ConnectToK8s() if err != nil { @@ -119,6 +120,7 @@ func NewContainerExecutor( logsStream: logsStream, features: features, defaultStorageClassName: defaultStorageClassName, + whitelistedContainers: whitelistedContainers, }, nil } @@ -152,6 +154,8 @@ type ContainerExecutor struct { logsStream logsclient.Stream features featureflags.FeatureFlags defaultStorageClassName string + // whitelistedContainers is a list of containers from which logs are allowed to be streamed. + whitelistedContainers []string } type JobOptions struct { @@ -455,7 +459,7 @@ func (c *ContainerExecutor) updateResultsFromPod( execution.ExecutionResult.Error() } - scraperLogs, err = executor.GetPodLogs(ctx, c.clientSet, execution.TestNamespace, *latestScraperPod) + scraperLogs, err = executor.GetPodLogs(ctx, c.clientSet, execution.TestNamespace, *latestScraperPod, execution.Id, c.whitelistedContainers) if err != nil { l.Errorw("get scraper pod logs error", "error", err) } @@ -474,7 +478,7 @@ func (c *ContainerExecutor) updateResultsFromPod( } } - executorLogs, err := executor.GetPodLogs(ctx, c.clientSet, execution.TestNamespace, *latestExecutorPod) + executorLogs, err := executor.GetPodLogs(ctx, c.clientSet, execution.TestNamespace, *latestExecutorPod, execution.Id, c.whitelistedContainers) if err != nil { l.Errorw("get executor pod logs error", "error", err) } diff --git a/pkg/executor/containerexecutor/logs.go b/pkg/executor/containerexecutor/logs.go index 3195ed69e1e..120ba092d47 100644 --- a/pkg/executor/containerexecutor/logs.go +++ b/pkg/executor/containerexecutor/logs.go @@ -33,7 +33,7 @@ func (c *ContainerExecutor) TailJobLogs(ctx context.Context, id, namespace strin case corev1.PodRunning: l.Debug("tailing pod logs: immediately") - return c.TailPodLogs(namespace, pod, logs) + return c.TailPodLogs(id, namespace, pod, logs) case corev1.PodFailed: err := fmt.Errorf("can't get pod logs, pod failed: %s/%s", pod.Namespace, pod.Name) @@ -48,14 +48,14 @@ func (c *ContainerExecutor) TailJobLogs(ctx context.Context, id, namespace strin } l.Debug("tailing pod logs") - return c.TailPodLogs(namespace, pod, logs) + return c.TailPodLogs(id, namespace, pod, logs) } } } return } -func (c *ContainerExecutor) TailPodLogs(namespace string, pod corev1.Pod, logs chan []byte) (err error) { +func (c *ContainerExecutor) TailPodLogs(id, namespace string, pod corev1.Pod, logs chan []byte) (err error) { var containers []string for _, container := range pod.Spec.InitContainers { containers = append(containers, container.Name) @@ -73,6 +73,10 @@ func (c *ContainerExecutor) TailPodLogs(namespace string, pod corev1.Pod, logs c ctx := context.Background() for _, container := range containers { + if !executor.IsWhitelistedContainer(container, id, c.whitelistedContainers) { + wg.Done() + continue + } go func(container string) { defer wg.Done() podLogOptions := corev1.PodLogOptions{ From 3f0f41f0208c0c6c5d02817734b17a71c54b0add Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Jun 2024 09:13:52 +0000 Subject: [PATCH 03/21] build(deps): bump amannn/action-semantic-pull-request Bumps [amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request) from 5.5.2 to 5.5.3. - [Release notes](https://github.com/amannn/action-semantic-pull-request/releases) - [Changelog](https://github.com/amannn/action-semantic-pull-request/blob/main/CHANGELOG.md) - [Commits](https://github.com/amannn/action-semantic-pull-request/compare/v5.5.2...v5.5.3) --- updated-dependencies: - dependency-name: amannn/action-semantic-pull-request dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/lint_pr.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint_pr.yaml b/.github/workflows/lint_pr.yaml index 73a1d1eba36..c5b1a651436 100644 --- a/.github/workflows/lint_pr.yaml +++ b/.github/workflows/lint_pr.yaml @@ -15,6 +15,6 @@ jobs: # Please look up the latest version from # https://github.com/amannn/action-semantic-pull-request/releases - name: Lint PR - uses: amannn/action-semantic-pull-request@v5.5.2 + uses: amannn/action-semantic-pull-request@v5.5.3 env: GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }} \ No newline at end of file From 3ff1c4fa72d237394cd3be13dc400d8d0bdf70e4 Mon Sep 17 00:00:00 2001 From: Jacek Wysocki Date: Fri, 28 Jun 2024 12:32:57 +0200 Subject: [PATCH 04/21] feat: improved debug cmd (#5617) * feat: improved debug cmd * feat: added possibility to move around different debug sets * chore: rename features to show * fix: unused code cleanup * fix: proper error message instead of exit 1 --- cmd/kubectl-testkube/commands/agent/debug.go | 45 +---- cmd/kubectl-testkube/commands/common/flags.go | 28 +++ .../commands/common/helper.go | 110 ++++++++++- cmd/kubectl-testkube/commands/debug.go | 67 +------ cmd/kubectl-testkube/commands/debug/agent.go | 130 +++++++++++++ .../commands/debug/controlplane.go | 175 +++++++++--------- .../commands/debug/features.go | 50 +++++ cmd/kubectl-testkube/commands/debug/info.go | 26 --- cmd/kubectl-testkube/commands/debug/oss.go | 36 ++++ common.UiGetNamespace | 0 pkg/process/exec.go | 24 ++- pkg/ui/textinput.go | 10 +- pkg/ui/ui.go | 4 +- 13 files changed, 481 insertions(+), 224 deletions(-) create mode 100644 cmd/kubectl-testkube/commands/debug/agent.go create mode 100644 cmd/kubectl-testkube/commands/debug/features.go delete mode 100644 cmd/kubectl-testkube/commands/debug/info.go create mode 100644 cmd/kubectl-testkube/commands/debug/oss.go create mode 100644 common.UiGetNamespace diff --git a/cmd/kubectl-testkube/commands/agent/debug.go b/cmd/kubectl-testkube/commands/agent/debug.go index 612342b1c0d..23c15d2efef 100644 --- a/cmd/kubectl-testkube/commands/agent/debug.go +++ b/cmd/kubectl-testkube/commands/agent/debug.go @@ -2,52 +2,13 @@ package agent import ( "github.com/spf13/cobra" - - "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" - "github.com/kubeshop/testkube/cmd/kubectl-testkube/config" - "github.com/kubeshop/testkube/pkg/ui" ) func NewDebugAgentCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "debug", - Short: "Debug Agent info", - Run: func(cmd *cobra.Command, args []string) { - cfg, err := config.Load() - ui.ExitOnError("loading config file", err) - ui.NL() - - if cfg.ContextType != config.ContextTypeCloud { - ui.Errf("Agent debug is only available for cloud context") - ui.NL() - ui.ShellCommand("Please try command below to set your context into Cloud mode", `testkube set context -o -e -k `) - ui.NL() - return - } - - common.UiPrintContext(cfg) - - client, _, err := common.GetClient(cmd) - ui.ExitOnError("getting client", err) - - i, err := client.GetServerInfo() - if err != nil { - ui.Errf("Error %v", err) - ui.NL() - ui.Info("Possible reasons:") - ui.Warn("- Please check if your agent organization and environment are set correctly") - ui.Warn("- Please check if your API token is set correctly") - ui.NL() - } else { - ui.Warn("Agent correctly connected to cloud:\n") - ui.InfoGrid(map[string]string{ - "Agent version ": i.Version, - "Agent namespace": i.Namespace, - }) - } - ui.NL() - }, + Use: "debug", + Short: "Debug Agent info", + Deprecated: "use `testkube debug agent` instead", } return cmd diff --git a/cmd/kubectl-testkube/commands/common/flags.go b/cmd/kubectl-testkube/commands/common/flags.go index 92333bea261..0363c96d009 100644 --- a/cmd/kubectl-testkube/commands/common/flags.go +++ b/cmd/kubectl-testkube/commands/common/flags.go @@ -188,3 +188,31 @@ func ProcessMasterFlags(cmd *cobra.Command, opts *HelmOptions, cfg *config.Data) func IsBothEnabledAndDisabledSet(cmd *cobra.Command) bool { return cmd.Flag("enable-webhooks").Changed && cmd.Flag("disable-webhooks").Changed } + +// CommaList is a custom flag type for features +type CommaList []string + +func (s CommaList) String() string { + return strings.Join(s, ",") +} +func (s *CommaList) Type() string { + return "[]string" +} + +func (s *CommaList) Set(value string) error { + *s = strings.Split(value, ",") + return nil +} + +// Enabled returns true if the feature is enabled, defaults to all +func (s *CommaList) Enabled(value string) bool { + if len(*s) == 0 { + return true + } + for _, f := range *s { + if f == value { + return true + } + } + return false +} diff --git a/cmd/kubectl-testkube/commands/common/helper.go b/cmd/kubectl-testkube/commands/common/helper.go index 1b8ce4ce81a..cdf5da2d3fd 100644 --- a/cmd/kubectl-testkube/commands/common/helper.go +++ b/cmd/kubectl-testkube/commands/common/helper.go @@ -434,7 +434,7 @@ func uiGetToken(tokenChan chan cloudlogin.Tokens) (string, string, error) { return token.IDToken, token.RefreshToken, nil } -func KubectlPrintLogs(namespace string, labels map[string]string) error { +func KubectlLogs(namespace string, labels map[string]string) error { kubectl, err := exec.LookPath("kubectl") if err != nil { return err @@ -491,6 +491,24 @@ func KubectlPrintEvents(namespace string) error { return process.ExecuteAndStreamOutput(kubectl, args...) } +func KubectlDescribePods(namespace string) error { + kubectl, err := exec.LookPath("kubectl") + if err != nil { + return err + } + + args := []string{ + "describe", + "pods", + "-n", namespace, + } + + ui.ShellCommand(kubectl, args...) + ui.NL() + + return process.ExecuteAndStreamOutput(kubectl, args...) +} + func KubectlPrintPods(namespace string) error { kubectl, err := exec.LookPath("kubectl") if err != nil { @@ -510,7 +528,7 @@ func KubectlPrintPods(namespace string) error { return process.ExecuteAndStreamOutput(kubectl, args...) } -func KubectlPrintStorageClass(namespace string) error { +func KubectlGetStorageClass(namespace string) error { kubectl, err := exec.LookPath("kubectl") if err != nil { return err @@ -526,3 +544,91 @@ func KubectlPrintStorageClass(namespace string) error { return process.ExecuteAndStreamOutput(kubectl, args...) } + +func KubectlGetServices(namespace string) error { + kubectl, err := exec.LookPath("kubectl") + if err != nil { + return err + } + + args := []string{ + "get", + "services", + "-n", namespace, + } + + ui.ShellCommand(kubectl, args...) + ui.NL() + + return process.ExecuteAndStreamOutput(kubectl, args...) +} + +func KubectlDescribeServices(namespace string) error { + kubectl, err := exec.LookPath("kubectl") + if err != nil { + return err + } + + args := []string{ + "get", + "services", + "-n", namespace, + "-o", "yaml", + } + + ui.ShellCommand(kubectl, args...) + ui.NL() + + return process.ExecuteAndStreamOutput(kubectl, args...) +} + +func KubectlGetIngresses(namespace string) error { + kubectl, err := exec.LookPath("kubectl") + if err != nil { + return err + } + + args := []string{ + "get", + "ingresses", + "-n", namespace, + } + + ui.ShellCommand(kubectl, args...) + ui.NL() + + return process.ExecuteAndStreamOutput(kubectl, args...) +} + +func KubectlDescribeIngresses(namespace string) error { + kubectl, err := exec.LookPath("kubectl") + if err != nil { + return err + } + + args := []string{ + "get", + "ingresses", + "-n", namespace, + "-o", "yaml", + } + + ui.ShellCommand(kubectl, args...) + ui.NL() + + return process.ExecuteAndStreamOutput(kubectl, args...) +} + +func UiGetNamespace(cmd *cobra.Command, defaultNamespace string) string { + var namespace string + var err error + + if cmd.Flag("namespace").Changed { + namespace, err = cmd.Flags().GetString("namespace") + ui.ExitOnError("getting namespace", err) + } else { + namespace = ui.TextInput("Please provide namespace for Control Plane", defaultNamespace) + } + + return namespace +} diff --git a/cmd/kubectl-testkube/commands/debug.go b/cmd/kubectl-testkube/commands/debug.go index 6e8a362521a..cbc4a4e9bff 100644 --- a/cmd/kubectl-testkube/commands/debug.go +++ b/cmd/kubectl-testkube/commands/debug.go @@ -3,11 +3,7 @@ package commands import ( "github.com/spf13/cobra" - "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" - "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/validator" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/debug" - "github.com/kubeshop/testkube/cmd/kubectl-testkube/config" - "github.com/kubeshop/testkube/pkg/ui" ) // NewDebugCmd creates the 'testkube debug' command @@ -15,66 +11,11 @@ func NewDebugCmd() *cobra.Command { cmd := &cobra.Command{ Use: "debug", Aliases: []string{"dbg", "d"}, - Short: "Print environment information for debugging", - Run: func(cmd *cobra.Command, args []string) { + Short: "Print debugging info", + } - cfg, err := config.Load() - ui.ExitOnError("loading config file", err) - - if cfg.ContextType == config.ContextTypeCloud { - cfg, err := config.Load() - ui.ExitOnError("loading config file", err) - ui.NL() - - if cfg.ContextType != config.ContextTypeCloud { - ui.Errf("Agent debug is only available for cloud context") - ui.NL() - ui.ShellCommand("Please try command below to set your context into Cloud mode", `testkube set context -o -e -k `) - ui.NL() - return - } - - common.UiPrintContext(cfg) - - client, _, err := common.GetClient(cmd) - ui.ExitOnError("getting client", err) - - i, err := client.GetServerInfo() - if err != nil { - ui.Errf("Error %v", err) - ui.NL() - ui.Info("Possible reasons:") - ui.Warn("- Please check if your agent organization and environment are set correctly") - ui.Warn("- Please check if your API token is set correctly") - ui.NL() - } else { - ui.Warn("Agent correctly connected to cloud:\n") - ui.InfoGrid(map[string]string{ - "Agent version ": i.Version, - "Agent namespace": i.Namespace, - }) - } - ui.NL() - } else { - client, _, err := common.GetClient(cmd) - ui.ExitOnError("getting client", err) - - d, err := debug.GetDebugInfo(client) - ui.ExitOnError("get debug info", err) - - debug.PrintDebugInfo(d) - - } - }, - PersistentPreRun: func(cmd *cobra.Command, args []string) { - cfg, err := config.Load() - ui.ExitOnError("loading config", err) - common.UiContextHeader(cmd, cfg) - - validator.PersistentPreRunVersionCheck(cmd, common.Version) - }} - - cmd.AddCommand(debug.NewShowDebugInfoCmd()) + cmd.AddCommand(debug.NewDebugOssCmd()) + cmd.AddCommand(debug.NewDebugAgentCmd()) cmd.AddCommand(debug.NewDebugControlPlaneCmd()) return cmd diff --git a/cmd/kubectl-testkube/commands/debug/agent.go b/cmd/kubectl-testkube/commands/debug/agent.go new file mode 100644 index 00000000000..70195f7b53b --- /dev/null +++ b/cmd/kubectl-testkube/commands/debug/agent.go @@ -0,0 +1,130 @@ +package debug + +import ( + "github.com/spf13/cobra" + + "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" + "github.com/kubeshop/testkube/cmd/kubectl-testkube/config" + "github.com/kubeshop/testkube/pkg/ui" +) + +const ( + defaultAgentNamespace = "testkube" +) + +func NewDebugAgentCmd() *cobra.Command { + var show common.CommaList + + cmd := &cobra.Command{ + Use: "agent", + Aliases: []string{"ag", "a"}, + Short: "Show Agent debug information", + Long: "Get all the necessary information to debug an issue in Testkube Agent you can fiter through comma separated list of items to show with additional flag `--show " + agentFeaturesStr + "`", + Run: RunDebugAgentCmdFunc(&show), + } + + cmd.Flags().VarP(&show, "show", "s", "Comma-separated list of features to show, one of: "+agentFeaturesStr+", defaults to all") + + return cmd +} + +func RunDebugAgentCmdFunc(show *common.CommaList) func(cmd *cobra.Command, args []string) { + return func(cmd *cobra.Command, args []string) { + cfg, err := config.Load() + ui.ExitOnError("loading config file", err) + ui.NL() + + ui.H1("Agent Insights") + + if cfg.ContextType != config.ContextTypeCloud { + ui.Errf("Agent debug is only available for cloud context") + ui.NL() + ui.ShellCommand("Please try command below to set your context into Cloud mode", `testkube set context -o -e -k `) + ui.NL() + return + } + + namespace := common.UiGetNamespace(cmd, defaultAgentNamespace) + + if show.Enabled(showPods) { + ui.H2("Pods") + err = common.KubectlPrintPods(namespace) + ui.WarnOnError("getting Kubernetes pods", err) + + ui.NL(3) + err = common.KubectlDescribePods(namespace) + ui.WarnOnError("describing Kubernetes pods", err) + } + + if show.Enabled(showServices) { + ui.H2("Services") + err = common.KubectlGetServices(namespace) + ui.WarnOnError("describing Kubernetes pods", err) + + ui.NL(3) + err = common.KubectlDescribeServices(namespace) + ui.WarnOnError("describing Kubernetes services", err) + } + + if show.Enabled(showIngresses) { + ui.H2("Ingresses") + err = common.KubectlGetIngresses(namespace) + ui.WarnOnError("describing Kubernetes ingresses", err) + } + + if show.Enabled(showApiLogs) { + ui.H2("Agent API Logs") + err = common.KubectlLogs(namespace, map[string]string{"app.kubernetes.io/name": "api-server"}) + ui.ExitOnError("getting agent logs", err) + ui.NL(2) + } + + if show.Enabled(showNatsLogs) { + ui.H2("NATS logs") + err = common.KubectlLogs(namespace, map[string]string{"app.kubernetes.io/name": "nats"}) + ui.WarnOnError("getting worker service logs", err) + } + + if show.Enabled(showEvents) { + ui.H2("Kubernetes Events") + err = common.KubectlPrintEvents(namespace) + ui.WarnOnError("getting Kubernetes events", err) + } + + client, _, err := common.GetClient(cmd) + ui.ExitOnError("getting client", err) + + if show.Enabled(showRoundtrip) { + ui.H2("Agent connection through Control Plane from CLI") + + i, err := client.GetServerInfo() + if err != nil { + ui.Errf("Error while doing roundtrip to agent: %v", err) + ui.NL() + ui.Info("Possible reasons:") + ui.Warn("- Please check if your agent organization and environment are set correctly") + ui.Warn("- Please check if your API token is set correctly") + ui.NL() + } else { + ui.Warn("Agent correctly connected to cloud:\n") + ui.InfoGrid(map[string]string{ + "Agent version ": i.Version, + "Agent namespace": i.Namespace, + }) + } + } + + if show.Enabled(showCLIToControlPlane) { + ui.H2("Agent connection to Control Plane from CLI") + + debug, err := GetDebugInfo(client) + ui.ExitOnError("connecting to Control Plane", err) + PrintDebugInfo(debug) + ui.NL(2) + + common.UiPrintContext(cfg) + } + + ui.NL() + } +} diff --git a/cmd/kubectl-testkube/commands/debug/controlplane.go b/cmd/kubectl-testkube/commands/debug/controlplane.go index 9efd6dc9bec..959540f056c 100644 --- a/cmd/kubectl-testkube/commands/debug/controlplane.go +++ b/cmd/kubectl-testkube/commands/debug/controlplane.go @@ -1,8 +1,6 @@ package debug import ( - "os" - "github.com/spf13/cobra" "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" @@ -11,100 +9,107 @@ import ( // NewDebugControlPlaneCmd creates a new cobra command to print the debug info to the CLI func NewDebugControlPlaneCmd() *cobra.Command { - var additionalLabels map[string]string - var attachAgentLogs bool + const defaultCPNamespace = "testkube-enterprise" + var features common.CommaList cmd := &cobra.Command{ Use: "controlplane", - Aliases: []string{"ctl", "cp"}, - Short: "Show debug info", - Long: "Get all the necessary information to debug an issue in Testkube Control Plane", + Aliases: []string{"ctl", "cp", "c"}, + Short: "Show Control Plane debug information", + Long: "Get all the necessary information to debug an issue in Testkube Control Plane you can fiter through comma separated list of items to show with additional flag `--show " + controlPlaneFeaturesStr + "`", Run: func(cmd *cobra.Command, args []string) { + namespace := common.UiGetNamespace(cmd, defaultCPNamespace) + + ui.H1("Getting Control Plane insights, namespace: " + namespace) + + if features.Enabled(showPods) { + ui.H2("Pods") + err := common.KubectlPrintPods(namespace) + ui.WarnOnError("getting Kubernetes pods", err) + + ui.NL(3) + err = common.KubectlDescribePods(namespace) + ui.WarnOnError("describing Kubernetes pods", err) + } + + if features.Enabled(showServices) { + ui.H2("Services") + err := common.KubectlGetServices(namespace) + ui.WarnOnError("describing Kubernetes pods", err) + + ui.NL(3) + err = common.KubectlDescribeServices(namespace) + ui.WarnOnError("describing Kubernetes services", err) + } + + if features.Enabled(showIngresses) { + ui.H2("Ingresses") + err := common.KubectlGetIngresses(namespace) + ui.WarnOnError("describing Kubernetes ingresses", err) + + ui.NL(3) + err = common.KubectlDescribeIngresses(namespace) + ui.WarnOnError("describing Kubernetes services", err) + } - spinner := ui.NewSpinner("").WithWriter(os.Stderr) - spinner, err := spinner.Start() - ui.ExitOnError("starting spinner", err) - - namespace, err := cmd.Flags().GetString("namespace") - ui.ExitOnError("getting namespace", err) - - ui.H1("Getting control plane logs") - - spinner.UpdateText("Getting Kubernetes pods") - ui.H2("Kubernetes Pods in namespace:" + namespace) - err = common.KubectlPrintPods(namespace) - ui.WarnOnError("getting Kubernetes pods", err) - - spinner.UpdateText("Kubernetes Storage Classes") - ui.H2("Kubernetes Storage Classes") - err = common.KubectlPrintStorageClass(namespace) - ui.WarnOnError("getting Kubernetes Storage Classes", err) - - spinner.UpdateText("API Server Logs") - ui.H2("API Server Logs") - err = common.KubectlPrintLogs(namespace, map[string]string{"app.kubernetes.io/name": "testkube-cloud-api"}) - ui.WarnOnError("getting api server logs", err) - - spinner.UpdateText("Worker Service Logs") - ui.H2("Worker Service Logs") - err = common.KubectlPrintLogs(namespace, map[string]string{"app.kubernetes.io/name": "testkube-worker-service"}) - ui.WarnOnError("getting worker service logs", err) - - spinner.UpdateText("UI Logs") - ui.H2("UI Logs") - err = common.KubectlPrintLogs(namespace, map[string]string{"app.kubernetes.io/name": "testkube-cloud-ui"}) - ui.WarnOnError("getting UI logs", err) - - spinner.UpdateText("UI Logs") - ui.H2("Dex Logs") - err = common.KubectlPrintLogs(namespace, map[string]string{"app.kubernetes.io/name": "dex"}) - ui.WarnOnError("getting Dex logs", err) - - spinner.UpdateText("UI Logs") - ui.H2("Minio Logs") - err = common.KubectlPrintLogs(namespace, map[string]string{"app.kubernetes.io/name": "minio"}) - ui.WarnOnError("getting MinIO logs", err) - - spinner.UpdateText("MongoDB logs") - ui.H2("MongoDB logs") - err = common.KubectlPrintLogs(namespace, map[string]string{"app.kubernetes.io/name": "mongodb"}) - ui.WarnOnError("getting MongoDB logs", err) - - spinner.UpdateText("NATS Logs") - ui.H2("NATS logs") - err = common.KubectlPrintLogs(namespace, map[string]string{"app.kubernetes.io/name": "nats"}) - ui.WarnOnError("getting worker service logs", err) - - spinner.UpdateText("Kubernetes Events") - ui.H2("Kubernetes Events") - err = common.KubectlPrintEvents(namespace) - ui.WarnOnError("getting Kubernetes events", err) - - if cmd.Flag("attach-agent-log").Value.String() == "true" { - spinner.UpdateText("UI Logs") - ui.H2("Agent Logs") - err = common.KubectlPrintLogs(namespace, map[string]string{"app.kubernetes.io/name": "testkube-agent"}) - ui.ExitOnError("getting agent logs", err) - - spinner.UpdateText("UI Logs") - ui.H1("Agent debug info") - client, _, err := common.GetClient(cmd) - ui.ExitOnError("getting client", err) - - debug, err := GetDebugInfo(client) - ui.ExitOnError("get debug info", err) - - PrintDebugInfo(debug) + if features.Enabled(showStorageClasses) { + ui.H2("Storage Classes") + err := common.KubectlGetStorageClass(namespace) + ui.WarnOnError("getting Kubernetes Storage Classes", err) } - spinner.Success("Testkube logs collected successfully") + if features.Enabled(showEvents) { + ui.H2("Kubernetes Events") + err := common.KubectlPrintEvents(namespace) + ui.WarnOnError("getting Kubernetes events", err) + } + + if features.Enabled(showApiLogs) { + ui.H2("API Server Logs") + err := common.KubectlLogs(namespace, map[string]string{"app.kubernetes.io/name": "testkube-cloud-api"}) + ui.WarnOnError("getting api server logs", err) + } + + if features.Enabled(showWorkerLogs) { + ui.H2("Worker Service Logs") + err := common.KubectlLogs(namespace, map[string]string{"app.kubernetes.io/name": "testkube-worker-service"}) + ui.WarnOnError("getting worker service logs", err) + } + + if features.Enabled(showUiLogs) { + ui.H2("UI Logs") + err := common.KubectlLogs(namespace, map[string]string{"app.kubernetes.io/name": "testkube-cloud-ui"}) + ui.WarnOnError("getting UI logs", err) + } + + if features.Enabled(showDexLogs) { + ui.H2("Dex Logs") + err := common.KubectlLogs(namespace, map[string]string{"app.kubernetes.io/name": "dex"}) + ui.WarnOnError("getting Dex logs", err) + } + + if features.Enabled(showMinioLogs) { + ui.H2("Minio Logs") + err := common.KubectlLogs(namespace, map[string]string{"app.kubernetes.io/name": "minio"}) + ui.WarnOnError("getting MinIO logs", err) + } + + if features.Enabled(showMongoLogs) { + ui.H2("MongoDB logs") + err := common.KubectlLogs(namespace, map[string]string{"app.kubernetes.io/name": "mongodb"}) + ui.WarnOnError("getting MongoDB logs", err) + } + + if features.Enabled(showNatsLogs) { + ui.H2("NATS logs") + err := common.KubectlLogs(namespace, map[string]string{"app.kubernetes.io/name": "nats"}) + ui.WarnOnError("getting worker service logs", err) + } }, } - cmd.Flags().StringToStringVar(&additionalLabels, "labels", map[string]string{}, "Labels to filter logs by") - cmd.Flags().BoolVar(&attachAgentLogs, "attach-agent-log", false, "Attach agent log to the output keep in mind to configure valid agent first in the Testkube CLI") + cmd.Flags().VarP(&features, "show", "s", "Comma-separated list of features to show, one of: "+controlPlaneFeaturesStr+", defaults to all") return cmd - } diff --git a/cmd/kubectl-testkube/commands/debug/features.go b/cmd/kubectl-testkube/commands/debug/features.go new file mode 100644 index 00000000000..cd8996f67cf --- /dev/null +++ b/cmd/kubectl-testkube/commands/debug/features.go @@ -0,0 +1,50 @@ +package debug + +import "strings" + +const ( + showPods = "pods" + showServices = "services" + showIngresses = "ingresses" + showStorageClasses = "storageclasses" + showEvents = "events" + showCLIToControlPlane = "connection" + showRoundtrip = "roundtrip" + + showApiLogs = "api" + showWorkerLogs = "worker" + showUiLogs = "ui" + showDexLogs = "dex" + showNatsLogs = "nats" + showMongoLogs = "mongo" + showMinioLogs = "minio" +) + +var controlPlaneFeatures = []string{ + showPods, + showServices, + showIngresses, + showStorageClasses, + showEvents, + showNatsLogs, + showCLIToControlPlane, + showApiLogs, + showNatsLogs, + showMongoLogs, + showDexLogs, + showUiLogs, + showWorkerLogs, +} + +var agentFeatures = []string{ + showPods, + showServices, + showIngresses, + showEvents, + showNatsLogs, + showCLIToControlPlane, + showRoundtrip, +} + +var agentFeaturesStr = strings.Join(agentFeatures, ",") +var controlPlaneFeaturesStr = strings.Join(controlPlaneFeatures, ",") diff --git a/cmd/kubectl-testkube/commands/debug/info.go b/cmd/kubectl-testkube/commands/debug/info.go deleted file mode 100644 index e6ba33bbe62..00000000000 --- a/cmd/kubectl-testkube/commands/debug/info.go +++ /dev/null @@ -1,26 +0,0 @@ -package debug - -import ( - "github.com/spf13/cobra" - - "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" - "github.com/kubeshop/testkube/pkg/ui" -) - -// NewShowDebugInfoCmd creates a new cobra command to print the debug info to the CLI -func NewShowDebugInfoCmd() *cobra.Command { - return &cobra.Command{ - Use: "info", - Short: "Show debug info", - Long: "Get all the necessary information to debug an issue in Testkube", - Run: func(cmd *cobra.Command, args []string) { - client, _, err := common.GetClient(cmd) - ui.ExitOnError("getting client", err) - - debug, err := GetDebugInfo(client) - ui.ExitOnError("get debug info", err) - - PrintDebugInfo(debug) - }, - } -} diff --git a/cmd/kubectl-testkube/commands/debug/oss.go b/cmd/kubectl-testkube/commands/debug/oss.go new file mode 100644 index 00000000000..e2994bc69f8 --- /dev/null +++ b/cmd/kubectl-testkube/commands/debug/oss.go @@ -0,0 +1,36 @@ +package debug + +import ( + "github.com/spf13/cobra" + + "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" + "github.com/kubeshop/testkube/cmd/kubectl-testkube/config" + "github.com/kubeshop/testkube/pkg/ui" +) + +func NewDebugOssCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "oss", + Aliases: []string{"o"}, + Short: "Show OSS installation debug info", + Run: func(cmd *cobra.Command, args []string) { + cfg, err := config.Load() + ui.ExitOnError("loading config file", err) + + if cfg.ContextType != config.ContextTypeKubeconfig { + ui.Errf("OSS debug is only available for kubeconfig context, use `testkube set context` to set kubeconfig context, or `testkube debug agent|controlplane` to debug other variants of Testkube") + return + } + + client, _, err := common.GetClient(cmd) + ui.ExitOnError("getting client", err) + + d, err := GetDebugInfo(client) + ui.ExitOnError("get debug info", err) + + PrintDebugInfo(d) + }, + } + + return cmd +} diff --git a/common.UiGetNamespace b/common.UiGetNamespace new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/process/exec.go b/pkg/process/exec.go index 3ba974ac1df..8b591629aa5 100644 --- a/pkg/process/exec.go +++ b/pkg/process/exec.go @@ -7,6 +7,8 @@ import ( "io" "os/exec" "strings" + + "github.com/pkg/errors" ) type Options struct { @@ -145,8 +147,15 @@ func ExecuteAndStreamOutput(command string, arguments ...string) error { return err } + stderr, err := cmd.StderrPipe() + if err != nil { + return err + } + err = cmd.Start() if err != nil { + fmt.Printf("ERR: %+v\n", err) + return err } @@ -156,10 +165,23 @@ func ExecuteAndStreamOutput(command string, arguments ...string) error { m := scanner.Text() fmt.Println(m) } + if scanner.Err() != nil { + return scanner.Err() + } + scanner = bufio.NewScanner(stderr) + errorsBuffer := strings.Builder{} + for scanner.Scan() { + errorsBuffer.Write(scanner.Bytes()) + } if scanner.Err() != nil { return scanner.Err() } - return cmd.Wait() + err = cmd.Wait() + if err != nil { + return errors.Wrap(err, errorsBuffer.String()) + } + + return nil } diff --git a/pkg/ui/textinput.go b/pkg/ui/textinput.go index 7293dbc4715..9cc105f425b 100644 --- a/pkg/ui/textinput.go +++ b/pkg/ui/textinput.go @@ -2,10 +2,12 @@ package ui import "github.com/pterm/pterm" -func (ui *UI) TextInput(text string) string { - in, _ := pterm.DefaultInteractiveTextInput. - WithMultiLine(false). - Show(text) +func (ui *UI) TextInput(text string, defaultValue ...string) string { + t := pterm.DefaultInteractiveTextInput.WithMultiLine(false) + if len(defaultValue) > 0 { + t = t.WithDefaultValue(defaultValue[0]) + } + in, _ := t.Show(text) return in } diff --git a/pkg/ui/ui.go b/pkg/ui/ui.go index 6a7d13f5b6c..82563b7c39b 100644 --- a/pkg/ui/ui.go +++ b/pkg/ui/ui.go @@ -101,7 +101,9 @@ func NewArrayTable(a [][]string) ArrayTable { return ui.NewArray func PrintArrayTable(a [][]string) { ui.PrintArrayTable(a) } func Confirm(message string) bool { return ui.Confirm(message) } func Select(title string, options []string) string { return ui.Select(title, options) } -func TextInput(message string) string { return ui.TextInput(message) } +func TextInput(message string, defaultValue ...string) string { + return ui.TextInput(message, defaultValue...) +} func PrintCRD[T interface{}](cr T, kind string, groupVersion schema.GroupVersion) { PrintCRDs([]T{cr}, kind, groupVersion) From 40a5503ae882649ef07b499864f94521ee06c2d5 Mon Sep 17 00:00:00 2001 From: Jacek Wysocki Date: Tue, 2 Jul 2024 11:09:33 +0200 Subject: [PATCH 05/21] fix: (TKC-2164) valid container executor exit code handling (#5614) * fix: valid container exit code handling * fix: handle container executor exit code --- pkg/executor/common.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/executor/common.go b/pkg/executor/common.go index 668e83708bb..df812dc91e9 100644 --- a/pkg/executor/common.go +++ b/pkg/executor/common.go @@ -545,7 +545,7 @@ func GetPodErrorMessage(ctx context.Context, client kubernetes.Interface, pod *c for _, initContainerStatus := range pod.Status.InitContainerStatuses { if initContainerStatus.State.Terminated != nil && - (initContainerStatus.State.Terminated.ExitCode > 1 || initContainerStatus.State.Terminated.ExitCode < -1) && + (initContainerStatus.State.Terminated.ExitCode >= 1 || initContainerStatus.State.Terminated.ExitCode < -1) && (initContainerStatus.State.Terminated.Message != "" || initContainerStatus.State.Terminated.Reason != "") { if message != "" { message += "\n" @@ -560,7 +560,7 @@ func GetPodErrorMessage(ctx context.Context, client kubernetes.Interface, pod *c for _, containerStatus := range pod.Status.ContainerStatuses { if containerStatus.State.Terminated != nil && - (containerStatus.State.Terminated.ExitCode > 1 || containerStatus.State.Terminated.ExitCode < -1) && + (containerStatus.State.Terminated.ExitCode >= 1 || containerStatus.State.Terminated.ExitCode < -1) && (containerStatus.State.Terminated.Message != "" || containerStatus.State.Terminated.Reason != "") { if message != "" { message += "\n" From 5349e35232b4847f064bbf50c7566b2e6acb43d6 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Wed, 26 Jun 2024 21:56:03 +0300 Subject: [PATCH 06/21] feat: check for test workflow execution for execute action Signed-off-by: Vladislav Sukhin --- internal/app/api/metrics/metrics.go | 2 +- .../model_test_workflow_execution_extended.go | 15 +++++++++++++++ .../model_test_workflow_step_extended.go | 19 +++++++++++++++++++ pkg/event/kind/webhook/listener.go | 2 +- 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/internal/app/api/metrics/metrics.go b/internal/app/api/metrics/metrics.go index ef998cbd356..67d91d73515 100644 --- a/internal/app/api/metrics/metrics.go +++ b/internal/app/api/metrics/metrics.go @@ -506,7 +506,7 @@ func (m Metrics) IncTestTriggerEventCount(name, resource, eventType string, caus }).Inc() } -func (m Metrics) InWebhookEventCount(name, eventType, result string) { +func (m Metrics) IncWebhookEventCount(name, eventType, result string) { m.WebhookEventCount.With(map[string]string{ "name": name, "eventType": eventType, diff --git a/pkg/api/v1/testkube/model_test_workflow_execution_extended.go b/pkg/api/v1/testkube/model_test_workflow_execution_extended.go index 5ee141d88c2..a50732cf9bb 100644 --- a/pkg/api/v1/testkube/model_test_workflow_execution_extended.go +++ b/pkg/api/v1/testkube/model_test_workflow_execution_extended.go @@ -45,3 +45,18 @@ func (e *TestWorkflowExecution) GetNamespace(defaultNamespace string) string { } return e.Namespace } + +func (e *TestWorkflowExecution) ContainsExecuteAction() bool { + if e.ResolvedWorkflow == nil || e.ResolvedWorkflow.Spec == nil { + return false + } + + steps := append(e.ResolvedWorkflow.Spec.Setup, append(e.ResolvedWorkflow.Spec.Steps, e.ResolvedWorkflow.Spec.After...)...) + for _, step := range steps { + if step.ContainsExecuteAction() { + return true + } + } + + return false +} diff --git a/pkg/api/v1/testkube/model_test_workflow_step_extended.go b/pkg/api/v1/testkube/model_test_workflow_step_extended.go index daf74da4b29..951e91dc1e6 100644 --- a/pkg/api/v1/testkube/model_test_workflow_step_extended.go +++ b/pkg/api/v1/testkube/model_test_workflow_step_extended.go @@ -27,3 +27,22 @@ func (w *TestWorkflowStep) EscapeDots() *TestWorkflowStep { func (w *TestWorkflowStep) UnscapeDots() *TestWorkflowStep { return w.ConvertDots(utils.UnescapeDots) } + +func (w *TestWorkflowStep) ContainsExecuteAction() bool { + if w.Execute != nil && (len(w.Execute.Tests) != 0 || len(w.Execute.Workflows) != 0) { + return true + } + + steps := append(w.Setup, w.Steps...) + for _, step := range steps { + if step.ContainsExecuteAction() { + return true + } + } + + if w.Parallel.ContainsExecuteAction() { + return true + } + + return false +} diff --git a/pkg/event/kind/webhook/listener.go b/pkg/event/kind/webhook/listener.go index 603d3c73520..66403f267d8 100644 --- a/pkg/event/kind/webhook/listener.go +++ b/pkg/event/kind/webhook/listener.go @@ -127,7 +127,7 @@ func (l *WebhookListener) Notify(event testkube.Event) (result testkube.EventRes res = "error" } - l.metrics.InWebhookEventCount(l.name, eventType, res) + l.metrics.IncWebhookEventCount(l.name, eventType, res) }() switch { From ea3023307a8684ce0e7598464138f8fef3cfb402 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Wed, 26 Jun 2024 22:11:19 +0300 Subject: [PATCH 07/21] fix: add extended model Signed-off-by: Vladislav Sukhin --- ...model_test_workflow_step_parallel_extended.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pkg/api/v1/testkube/model_test_workflow_step_parallel_extended.go diff --git a/pkg/api/v1/testkube/model_test_workflow_step_parallel_extended.go b/pkg/api/v1/testkube/model_test_workflow_step_parallel_extended.go new file mode 100644 index 00000000000..d4f8f785aff --- /dev/null +++ b/pkg/api/v1/testkube/model_test_workflow_step_parallel_extended.go @@ -0,0 +1,16 @@ +package testkube + +func (w *TestWorkflowStepParallel) ContainsExecuteAction() bool { + if w.Execute != nil && (len(w.Execute.Tests) != 0 || len(w.Execute.Workflows) != 0) { + return true + } + + steps := append(w.Setup, append(w.Steps, w.After...)...) + for _, step := range steps { + if step.ContainsExecuteAction() { + return true + } + } + + return false +} From 04002dd419a520c1205c6bb621fb46e4a8b24cba Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Thu, 27 Jun 2024 16:17:16 +0300 Subject: [PATCH 08/21] feat: queued cd event for test workflow Signed-off-by: Vladislav Sukhin --- pkg/mapper/cdevents/mapper.go | 113 ++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/pkg/mapper/cdevents/mapper.go b/pkg/mapper/cdevents/mapper.go index f90d1f286de..c77328e0882 100644 --- a/pkg/mapper/cdevents/mapper.go +++ b/pkg/mapper/cdevents/mapper.go @@ -26,6 +26,19 @@ func MapTestkubeEventToCDEvent(tkEvent testkube.Event, clusterID, defaultNamespa return MapTestkubeEventStartTestSuiteToCDEvent(tkEvent, clusterID, dashboardURI) case *testkube.EventEndTestSuiteAborted, *testkube.EventEndTestSuiteFailed, *testkube.EventEndTestSuiteTimeout, *testkube.EventEndTestSuiteSuccess: return MapTestkubeEventFinishTestSuiteToCDEvent(tkEvent, clusterID, dashboardURI) + case *testkube.EventQueueTestWorkflow: + containsExuctionAction := false + if tkEvent.TestWorkflowExecution != nil { + containsExuctionAction = tkEvent.TestWorkflowExecution.ContainsExecuteAction() + } + + if containsExuctionAction { + return MapTestkubeEventQueuedTestWorkflowTestSuiteToCDEvent(tkEvent, clusterID, defaultNamespace, dashboardURI) + } + + return MapTestkubeEventQueuedTestWorkflowTestToCDEvent(tkEvent, clusterID, defaultNamespace, dashboardURI) + case *testkube.EventStartTestWorkflow: + case *testkube.EventEndTestWorkflowAborted, *testkube.EventEndTestWorkflowFailed, *testkube.EventEndTestWorkflowSuccess: } return nil, fmt.Errorf("not supported event type %s", tkEvent.Type_) @@ -468,3 +481,103 @@ func MapMimeTypeToCDEventOutputType(mimeType string) string { return "other" } + +// MapTestkubeEventQueuedTestWorkflowTestToCDEvent maps OpenAPI spec Queued Test Workflow Test Event to CDEvent CDEventReader +func MapTestkubeEventQueuedTestWorkflowTestToCDEvent(event testkube.Event, clusterID, defaultNamespace, dashboardURI string) (cdevents.CDEventReader, error) { + // Create the base event + ev, err := cdevents.NewTestCaseRunQueuedEvent() + if err != nil { + return nil, err + } + + if event.TestWorkflowExecution != nil { + ev.SetSubjectId(event.TestWorkflowExecution.Id) + } + + ev.SetSubjectSource(clusterID) + ev.SetSource(clusterID) + if event.TestWorkflowExecution != nil { + workflowName := "" + if event.TestWorkflowExecution.Workflow != nil { + workflowName = event.TestWorkflowExecution.Workflow.Name + } + + ev.SetSubjectTestCase(&cdevents.TestCaseRunQueuedSubjectContentTestCase{ + Id: workflowName, + // Type: MapTestkubeTestTypeToCDEventTestCaseType(event.TestExecution.TestType), + Uri: fmt.Sprintf("%s/test-workflows/%s", dashboardURI, workflowName), + }) + + namespace := event.TestWorkflowExecution.Namespace + if namespace == "" { + namespace = defaultNamespace + } + + ev.SetSubjectEnvironment(&cdevents.Reference{ + Id: namespace, + Source: clusterID, + }) + /* + if event.TestWorkflowExecution.RunningContext != nil { + ev.SetSubjectTrigger(&cdevents.TestCaseRunQueuedSubjectContentTrigger{ + Type: MapTestkubeRunningContextTypeToCDEventTiggerType(event.TestWorkflowExecution.RunningContext.Type_), + }) + } + + if event.TestWorkflowExecution.ParentName != "" { + ev.SetSubjectTestSuiteRun(&cdevents.Reference{ + Id: event.TestWorkflowExecution.ParentName, + Source: clusterID, + }) + } + */ + } + + return ev, nil +} + +// MapTestkubeEventQueuedTestWorkflowTestSuiteToCDEvent maps OpenAPI spec Queued Test Workflow Test Suite Event to CDEvent CDEventReader +func MapTestkubeEventQueuedTestWorkflowTestSuiteToCDEvent(event testkube.Event, clusterID, defaultNamespace, dashboardURI string) (cdevents.CDEventReader, error) { + // Create the base event + ev, err := cdevents.NewTestSuiteRunQueuedEvent() + if err != nil { + return nil, err + } + + if event.TestWorkflowExecution != nil { + ev.SetSubjectId(event.TestWorkflowExecution.Id) + } + + ev.SetSubjectSource(clusterID) + ev.SetSource(clusterID) + if event.TestWorkflowExecution != nil { + workflowName := "" + if event.TestWorkflowExecution.Workflow != nil { + workflowName = event.TestWorkflowExecution.Workflow.Name + } + + ev.SetSubjectTestSuite(&cdevents.TestSuiteRunQueuedSubjectContentTestSuite{ + Id: workflowName, + Url: fmt.Sprintf("%s/test-workflows/%s", dashboardURI, workflowName), + }) + + namespace := event.TestWorkflowExecution.Namespace + if namespace == "" { + namespace = defaultNamespace + } + + ev.SetSubjectEnvironment(&cdevents.Reference{ + Id: namespace, + Source: clusterID, + }) + /* + if event.TestWorkflowExecution.RunningContext != nil { + ev.SetSubjectTrigger(&cdevents.TestSuiteRunQueuedSubjectContentTrigger{ + Type: MapTestkubeRunningContextTypeToCDEventTiggerType(event.TestWorkflowExecution.RunningContext.Type_), + }) + } + */ + } + + return ev, nil +} From cc1491808c60e49fa693e5b62f08da06d83bb5ee Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Thu, 27 Jun 2024 19:06:01 +0300 Subject: [PATCH 09/21] feat: start event for test workflow Signed-off-by: Vladislav Sukhin --- pkg/mapper/cdevents/mapper.go | 112 +++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/pkg/mapper/cdevents/mapper.go b/pkg/mapper/cdevents/mapper.go index c77328e0882..9512a7cc3d0 100644 --- a/pkg/mapper/cdevents/mapper.go +++ b/pkg/mapper/cdevents/mapper.go @@ -38,6 +38,16 @@ func MapTestkubeEventToCDEvent(tkEvent testkube.Event, clusterID, defaultNamespa return MapTestkubeEventQueuedTestWorkflowTestToCDEvent(tkEvent, clusterID, defaultNamespace, dashboardURI) case *testkube.EventStartTestWorkflow: + containsExuctionAction := false + if tkEvent.TestWorkflowExecution != nil { + containsExuctionAction = tkEvent.TestWorkflowExecution.ContainsExecuteAction() + } + + if containsExuctionAction { + return MapTestkubeEventStartTestWorkflowTestSuiteToCDEvent(tkEvent, clusterID, defaultNamespace, dashboardURI) + } + + return MapTestkubeEventStartTestWorkflowTestToCDEvent(tkEvent, clusterID, defaultNamespace, dashboardURI) case *testkube.EventEndTestWorkflowAborted, *testkube.EventEndTestWorkflowFailed, *testkube.EventEndTestWorkflowSuccess: } @@ -504,7 +514,7 @@ func MapTestkubeEventQueuedTestWorkflowTestToCDEvent(event testkube.Event, clust ev.SetSubjectTestCase(&cdevents.TestCaseRunQueuedSubjectContentTestCase{ Id: workflowName, - // Type: MapTestkubeTestTypeToCDEventTestCaseType(event.TestExecution.TestType), + // Type: MapTestkubeTestTypeToCDEventTestCaseType(event.TestWorkflowExecution.TestType), Uri: fmt.Sprintf("%s/test-workflows/%s", dashboardURI, workflowName), }) @@ -581,3 +591,103 @@ func MapTestkubeEventQueuedTestWorkflowTestSuiteToCDEvent(event testkube.Event, return ev, nil } + +// MapTestkubeEventStartTestWorkflowTestToCDEvent maps OpenAPI spec Start Test Workflow Test Event to CDEvent CDEventReader +func MapTestkubeEventStartTestWorkflowTestToCDEvent(event testkube.Event, clusterID, defaultNamespace, dashboardURI string) (cdevents.CDEventReader, error) { + // Create the base event + ev, err := cdevents.NewTestCaseRunStartedEvent() + if err != nil { + return nil, err + } + + if event.TestWorkflowExecution != nil { + ev.SetSubjectId(event.TestWorkflowExecution.Id) + } + + ev.SetSubjectSource(clusterID) + ev.SetSource(clusterID) + if event.TestExecution != nil { + workflowName := "" + if event.TestWorkflowExecution.Workflow != nil { + workflowName = event.TestWorkflowExecution.Workflow.Name + } + + ev.SetSubjectTestCase(&cdevents.TestCaseRunStartedSubjectContentTestCase{ + Id: workflowName, + // Type: MapTestkubeTestTypeToCDEventTestCaseType(event.TestWorkflowExecution.TestType), + Uri: fmt.Sprintf("%s/test-workflows/%s", dashboardURI, workflowName), + }) + + namespace := event.TestWorkflowExecution.Namespace + if namespace == "" { + namespace = defaultNamespace + } + + ev.SetSubjectEnvironment(&cdevents.Reference{ + Id: namespace, + Source: clusterID, + }) + /* + if event.TestWorkflowExecution.RunningContext != nil { + ev.SetSubjectTrigger(&cdevents.TestCaseRunStartedSubjectContentTrigger{ + Type: MapTestkubeRunningContextTypeToCDEventTiggerType(event.TestWorkflowExecution.RunningContext.Type_), + }) + } + + if event.TestWorkflowExecution.ParentName != "" { + ev.SetSubjectTestSuiteRun(&cdevents.Reference{ + Id: event.TestWorkflowExecution.ParentName, + Source: clusterID, + }) + } + */ + } + + return ev, nil +} + +// MapTestkubeEventStartTestWorkflowTestSuiteToCDEvent maps OpenAPI spec Start Test Workflow Test Suite Event to CDEvent CDEventReader +func MapTestkubeEventStartTestWorkflowTestSuiteToCDEvent(event testkube.Event, clusterID, defaultNamespace, dashboardURI string) (cdevents.CDEventReader, error) { + // Create the base event + ev, err := cdevents.NewTestSuiteRunStartedEvent() + if err != nil { + return nil, err + } + + if event.TestWorkflowExecution != nil { + ev.SetSubjectId(event.TestWorkflowExecution.Id) + } + + ev.SetSubjectSource(clusterID) + ev.SetSource(clusterID) + if event.TestWorkflowExecution != nil { + workflowName := "" + if event.TestWorkflowExecution.Workflow != nil { + workflowName = event.TestWorkflowExecution.Workflow.Name + } + + ev.SetSubjectTestSuite(&cdevents.TestSuiteRunStartedSubjectContentTestSuite{ + Id: workflowName, + Uri: fmt.Sprintf("%s/test-workflows/%s", dashboardURI, workflowName), + }) + + namespace := event.TestWorkflowExecution.Namespace + if namespace == "" { + namespace = defaultNamespace + } + + ev.SetSubjectEnvironment(&cdevents.Reference{ + Id: namespace, + Source: clusterID, + }) + /* + if event.TestWorkflowExecution.RunningContext != nil { + ev.SetSubjectTrigger(&cdevents.TestSuiteRunStartedSubjectContentTrigger{ + Type: MapTestkubeRunningContextTypeToCDEventTiggerType(event.TestWorkflowExecution.RunningContext.Type_), + }) + } + */ + } + + return ev, nil +} From ccaf620a8990148644f7d92caa5b368d92d2c049 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Thu, 27 Jun 2024 19:51:49 +0300 Subject: [PATCH 10/21] feat: finish cd event for test workflow Signed-off-by: Vladislav Sukhin --- pkg/mapper/cdevents/mapper.go | 153 ++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/pkg/mapper/cdevents/mapper.go b/pkg/mapper/cdevents/mapper.go index 9512a7cc3d0..9dabce04a30 100644 --- a/pkg/mapper/cdevents/mapper.go +++ b/pkg/mapper/cdevents/mapper.go @@ -49,6 +49,16 @@ func MapTestkubeEventToCDEvent(tkEvent testkube.Event, clusterID, defaultNamespa return MapTestkubeEventStartTestWorkflowTestToCDEvent(tkEvent, clusterID, defaultNamespace, dashboardURI) case *testkube.EventEndTestWorkflowAborted, *testkube.EventEndTestWorkflowFailed, *testkube.EventEndTestWorkflowSuccess: + containsExuctionAction := false + if tkEvent.TestWorkflowExecution != nil { + containsExuctionAction = tkEvent.TestWorkflowExecution.ContainsExecuteAction() + } + + if containsExuctionAction { + return MapTestkubeEventFinishTestWorkflowTestSuiteToCDEvent(tkEvent, clusterID, defaultNamespace, dashboardURI) + } + + return MapTestkubeEventFinishTestWorkflowTestToCDEvent(tkEvent, clusterID, defaultNamespace, dashboardURI) } return nil, fmt.Errorf("not supported event type %s", tkEvent.Type_) @@ -691,3 +701,146 @@ func MapTestkubeEventStartTestWorkflowTestSuiteToCDEvent(event testkube.Event, c return ev, nil } + +// MapTestkubeEventFinishTestWorkflowTestToCDEvent maps OpenAPI spec Failed, Aborted, Timeout, Success Test Workflow Test Event to CDEvent CDEventReader +func MapTestkubeEventFinishTestWorkflowTestToCDEvent(event testkube.Event, clusterID, defaultNamespace, dashboardURI string) (cdevents.CDEventReader, error) { + // Create the base event + ev, err := cdevents.NewTestCaseRunFinishedEvent() + if err != nil { + return nil, err + } + + if event.TestExecution != nil { + ev.SetSubjectId(event.TestExecution.Id) + } + + ev.SetSubjectSource(clusterID) + ev.SetSource(clusterID) + if event.TestWorkflowExecution != nil { + workflowName := "" + if event.TestWorkflowExecution.Workflow != nil { + workflowName = event.TestWorkflowExecution.Workflow.Name + } + + ev.SetSubjectTestCase(&cdevents.TestCaseRunFinishedSubjectContentTestCase{ + Id: workflowName, + // Type: MapTestkubeTestTypeToCDEventTestCaseType(event.TestWokflowExecution.TestType), + Uri: fmt.Sprintf("%s/test-workflows/%s", dashboardURI, workflowName), + }) + + namespace := event.TestWorkflowExecution.Namespace + if namespace == "" { + namespace = defaultNamespace + } + + ev.SetSubjectEnvironment(&cdevents.Reference{ + Id: namespace, + Source: clusterID, + }) + + if event.TestWorkflowExecution.Result != nil { + var errs []string + if event.TestWorkflowExecution.Result.Initialization != nil && + event.TestWorkflowExecution.Result.Initialization.ErrorMessage != "" { + errs = append(errs, event.TestWorkflowExecution.Result.Initialization.ErrorMessage) + } + + for _, step := range event.TestWorkflowExecution.Result.Steps { + if step.ErrorMessage != "" { + errs = append(errs, step.ErrorMessage) + } + } + + if event.TestWorkflowExecution.Result.IsAborted() { + ev.SetSubjectOutcome("cancel") + ev.SetSubjectReason(strings.Join(errs, ",")) + } + + if event.TestWorkflowExecution.Result.IsFailed() { + ev.SetSubjectOutcome("fail") + ev.SetSubjectReason(strings.Join(errs, ",")) + } + + if event.TestWorkflowExecution.Result.IsPassed() { + ev.SetSubjectOutcome("pass") + } + } + /* + if event.TestWorkflowExecution.ParentName != "" { + ev.SetSubjectTestSuiteRun(&cdevents.Reference{ + Id: event.TestWorkflowExecution.ParentName, + Source: clusterID, + }) + } + */ + } + + return ev, nil +} + +// MapTestkubeEventFinishTestWorkflowTestSuiteToCDEvent maps OpenAPI spec Failed, Aborted, Timeout, Success Test Workflow Test Event to CDEvent CDEventReader +func MapTestkubeEventFinishTestWorkflowTestSuiteToCDEvent(event testkube.Event, clusterID, defaultNamespace, dashboardURI string) (cdevents.CDEventReader, error) { + // Create the base event + ev, err := cdevents.NewTestSuiteRunFinishedEvent() + if err != nil { + return nil, err + } + + if event.TestWorkflowExecution != nil { + ev.SetSubjectId(event.TestWorkflowExecution.Id) + } + + ev.SetSubjectSource(clusterID) + ev.SetSource(clusterID) + if event.TestWorkflowExecution != nil { + workflowName := "" + if event.TestWorkflowExecution.Workflow != nil { + workflowName = event.TestWorkflowExecution.Workflow.Name + } + + ev.SetSubjectTestSuite(&cdevents.TestSuiteRunFinishedSubjectContentTestSuite{ + Id: workflowName, + Uri: fmt.Sprintf("%s/test-workflows/%s", dashboardURI, workflowName), + }) + + namespace := event.TestWorkflowExecution.Namespace + if namespace == "" { + namespace = defaultNamespace + } + + ev.SetSubjectEnvironment(&cdevents.Reference{ + Id: namespace, + Source: clusterID, + }) + + if event.TestWorkflowExecution.Result != nil { + var errs []string + if event.TestWorkflowExecution.Result.Initialization != nil && + event.TestWorkflowExecution.Result.Initialization.ErrorMessage != "" { + errs = append(errs, event.TestWorkflowExecution.Result.Initialization.ErrorMessage) + } + + for _, step := range event.TestWorkflowExecution.Result.Steps { + if step.ErrorMessage != "" { + errs = append(errs, step.ErrorMessage) + } + } + + if event.TestWorkflowExecution.Result.IsAborted() { + ev.SetSubjectOutcome("cancel") + ev.SetSubjectReason(strings.Join(errs, ",")) + } + + if event.TestWorkflowExecution.Result.IsFailed() { + ev.SetSubjectOutcome("fail") + ev.SetSubjectReason(strings.Join(errs, ",")) + } + + if event.TestWorkflowExecution.Result.IsPassed() { + ev.SetSubjectOutcome("pass") + } + } + } + + return ev, nil +} From 27888b1724371dbdd4e51dcc2b8d67bcbd2e75ae Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Thu, 27 Jun 2024 21:07:45 +0300 Subject: [PATCH 11/21] feat: log cd event for test workflow Signed-off-by: Vladislav Sukhin --- pkg/event/kind/cdevent/listener.go | 15 ++++++++++- pkg/mapper/cdevents/mapper.go | 41 ++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/pkg/event/kind/cdevent/listener.go b/pkg/event/kind/cdevent/listener.go index e4493887024..1f26e5c1733 100644 --- a/pkg/event/kind/cdevent/listener.go +++ b/pkg/event/kind/cdevent/listener.go @@ -79,7 +79,20 @@ func (l *CDEventListener) Notify(event testkube.Event) (result testkube.EventRes if event.Type_ != nil && (*event.Type_ == *testkube.EventEndTestAborted || *event.Type_ == *testkube.EventEndTestFailed || *event.Type_ == *testkube.EventEndTestSuccess || *event.Type_ == *testkube.EventEndTestTimeout) { // Create the output event - ev, err = cde.MapTestkubeLogToCDEvent(event, l.clusterID, l.dashboardURI) + ev, err = cde.MapTestkubeTestLogToCDEvent(event, l.clusterID, l.dashboardURI) + if err != nil { + return testkube.NewFailedEventResult(event.Id, err) + } + + if err := l.sendCDEvent(ev); err != nil { + return testkube.NewFailedEventResult(event.Id, err) + } + } + + if event.Type_ != nil && (*event.Type_ == *testkube.EventEndTestWorkflowAborted || *event.Type_ == *testkube.EventEndTestWorkflowFailed || + *event.Type_ == *testkube.EventEndTestWorkflowSuccess) { + // Create the output event + ev, err = cde.MapTestkubeTestWorkflowLogToCDEvent(event, l.clusterID, l.dashboardURI) if err != nil { return testkube.NewFailedEventResult(event.Id, err) } diff --git a/pkg/mapper/cdevents/mapper.go b/pkg/mapper/cdevents/mapper.go index 9dabce04a30..ce3e2658c04 100644 --- a/pkg/mapper/cdevents/mapper.go +++ b/pkg/mapper/cdevents/mapper.go @@ -243,8 +243,8 @@ func MapTestkubeArtifactToCDEvent(execution *testkube.Execution, clusterID, path return ev, nil } -// MapTestkubeLogToCDEvent maps OpenAPI spec log to CDEvent CDEventReader -func MapTestkubeLogToCDEvent(event testkube.Event, clusterID, dashboardURI string) (cdevents.CDEventReader, error) { +// MapTestkubeTestLogToCDEvent maps OpenAPI spec Test log to CDEvent CDEventReader +func MapTestkubeTestLogToCDEvent(event testkube.Event, clusterID, dashboardURI string) (cdevents.CDEventReader, error) { // Create the base event ev, err := cdevents.NewTestOutputPublishedEvent() if err != nil { @@ -844,3 +844,40 @@ func MapTestkubeEventFinishTestWorkflowTestSuiteToCDEvent(event testkube.Event, return ev, nil } + +// MapTestkubeTestWorkflowLogToCDEvent maps OpenAPI spec Test WWorkflow log to CDEvent CDEventReader +func MapTestkubeTestWorkflowLogToCDEvent(event testkube.Event, clusterID, dashboardURI string) (cdevents.CDEventReader, error) { + // Create the base event + ev, err := cdevents.NewTestOutputPublishedEvent() + if err != nil { + return nil, err + } + + if event.TestWorkflowExecution != nil { + ev.SetSubjectId(event.TestWorkflowExecution.Id + "-log") + } + + ev.SetSubjectSource(clusterID) + ev.SetSource(clusterID) + + if event.TestWorkflowExecution != nil { + ev.SetSubjectTestCaseRun(&cdevents.Reference{ + Id: event.TestWorkflowExecution.Id, + Source: clusterID, + }) + } + + ev.SetSubjectFormat("text/x-uri") + ev.SetSubjectOutputType("log") + if event.TestWorkflowExecution != nil { + workflowName := "" + if event.TestWorkflowExecution.Workflow != nil { + workflowName = event.TestWorkflowExecution.Workflow.Name + } + + ev.SetSubjectUri(fmt.Sprintf("%s/test-workflows/%s/executions/%s/log-output", dashboardURI, + workflowName, event.TestWorkflowExecution.Id)) + } + + return ev, nil +} From c02c3d329cf988f279c839163e487d5a4c522d18 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Fri, 28 Jun 2024 20:44:56 +0300 Subject: [PATCH 12/21] feat: cd event test workflow atifact Signed-off-by: Vladislav Sukhin --- cmd/api-server/main.go | 1 + cmd/testworkflow-toolkit/artifacts/handler.go | 60 ++++++- .../commands/artifacts.go | 13 ++ cmd/testworkflow-toolkit/env/config.go | 2 + .../model_test_workflow_execution_extended.go | 22 +++ .../model_test_workflow_step_extended.go | 21 ++- ...el_test_workflow_step_parallel_extended.go | 15 ++ pkg/mapper/cdevents/mapper.go | 153 +++++++++--------- .../testworkflowexecutor/executor.go | 6 +- .../testworkflowprocessor/container.go | 2 + 10 files changed, 211 insertions(+), 84 deletions(-) diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index 2309693b589..69eb4a57c87 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -573,6 +573,7 @@ func main() { cfg.EnableImageDataPersistentCache, cfg.ImageDataPersistentCacheKey, cfg.TestkubeDashboardURI, + clusterId, ) go testWorkflowExecutor.Recover(context.Background()) diff --git a/cmd/testworkflow-toolkit/artifacts/handler.go b/cmd/testworkflow-toolkit/artifacts/handler.go index 1a635a35c6f..b1e17980539 100644 --- a/cmd/testworkflow-toolkit/artifacts/handler.go +++ b/cmd/testworkflow-toolkit/artifacts/handler.go @@ -1,21 +1,28 @@ package artifacts import ( + "context" "fmt" "io/fs" "path/filepath" "sync/atomic" + cdevents "github.com/cdevents/sdk-go/pkg/api" + cloudevents "github.com/cloudevents/sdk-go/v2" "github.com/dustin/go-humanize" + "github.com/gabriel-vasile/mimetype" + cde "github.com/kubeshop/testkube/pkg/mapper/cdevents" "github.com/kubeshop/testkube/pkg/ui" ) type handler struct { - uploader Uploader - processor Processor - postProcessor PostProcessor - pathPrefix string + uploader Uploader + processor Processor + postProcessor PostProcessor + pathPrefix string + cdeventsClient cloudevents.Client + cdeventsArtifactParameters cde.CDEventsArtifactParameters success atomic.Uint32 errors atomic.Uint32 @@ -42,6 +49,22 @@ func WithPathPrefix(pathPrefix string) HandlerOpts { } } +func WithCDEventsTarget(cdEventsTarget string) HandlerOpts { + return func(h *handler) { + var err error + h.cdeventsClient, err = cloudevents.NewClientHTTP(cloudevents.WithTarget(cdEventsTarget)) + if err != nil { + fmt.Printf(ui.LightYellow("failed to create cloud event client: %s"), err.Error()) + } + } +} + +func WithCDEventsArtifactParameters(cdeventsArtifactParameters cde.CDEventsArtifactParameters) HandlerOpts { + return func(h *handler) { + h.cdeventsArtifactParameters = cdeventsArtifactParameters + } +} + func NewHandler(uploader Uploader, processor Processor, opts ...HandlerOpts) Handler { h := &handler{ uploader: uploader, @@ -82,6 +105,12 @@ func (h *handler) Add(path string, file fs.File, stat fs.FileInfo) (err error) { err = h.processor.Add(h.uploader, uploadPath, file, stat) if err == nil { h.success.Add(1) + if h.cdeventsClient != nil { + err = h.sendCDEvent(path) + if err != nil { + fmt.Printf(ui.LightYellow("failed to send cd event: %s"), err.Error()) + } + } } else { h.errors.Add(1) fmt.Printf(ui.Red("%s: failed: %s"), uploadPath, err.Error()) @@ -128,3 +157,26 @@ func (h *handler) End() (err error) { } return nil } + +func (h *handler) sendCDEvent(path string) error { + mtype, err := mimetype.DetectFile(path) + if err != nil { + return err + } + + ev, err := cde.MapTestkubeTestWorkflowArtifactToCDEvent(h.cdeventsArtifactParameters, path, mtype.String()) + if err != nil { + return err + } + + ce, err := cdevents.AsCloudEvent(ev) + if err != nil { + return err + } + + if result := h.cdeventsClient.Send(context.Background(), *ce); cloudevents.IsUndelivered(result) { + return fmt.Errorf("failed to send, %v", result) + } + + return nil +} diff --git a/cmd/testworkflow-toolkit/commands/artifacts.go b/cmd/testworkflow-toolkit/commands/artifacts.go index ae4bc854259..d047aa01598 100644 --- a/cmd/testworkflow-toolkit/commands/artifacts.go +++ b/cmd/testworkflow-toolkit/commands/artifacts.go @@ -23,6 +23,7 @@ import ( "github.com/kubeshop/testkube/cmd/testworkflow-toolkit/artifacts" "github.com/kubeshop/testkube/cmd/testworkflow-toolkit/env" + "github.com/kubeshop/testkube/pkg/mapper/cdevents" "github.com/kubeshop/testkube/pkg/ui" ) @@ -146,6 +147,18 @@ func NewArtifactsCmd() *cobra.Command { handlerOpts = append(handlerOpts, artifacts.WithPathPrefix(env.Config().Execution.FSPrefix)) } + // Support cd evaents + if env.Config().System.CDEventTarget != "" { + handlerOpts = append(handlerOpts, artifacts.WithCDEventsTarget(env.Config().System.CDEventTarget)) + handlerOpts = append(handlerOpts, artifacts.WithCDEventsArtifactParameters(cdevents.CDEventsArtifactParameters{ + Id: env.Config().Execution.Id, + Name: env.Config().Execution.Name, + WorkflowName: env.Config().Execution.WorkflowName, + ClusterID: env.Config().System.ClusterID, + DashboardURI: env.Config().System.DashboardUrl, + })) + } + handler := artifacts.NewHandler(uploader, processor, handlerOpts...) run(handler, walker, os.DirFS("/")) diff --git a/cmd/testworkflow-toolkit/env/config.go b/cmd/testworkflow-toolkit/env/config.go index ed174880c64..31851130408 100644 --- a/cmd/testworkflow-toolkit/env/config.go +++ b/cmd/testworkflow-toolkit/env/config.go @@ -54,6 +54,8 @@ type envSystemConfig struct { Ip string `envconfig:"TK_IP"` DashboardUrl string `envconfig:"TK_DASH"` ApiUrl string `envconfig:"TK_API"` + ClusterID string `envconfig:"TK_CLU"` + CDEventTarget string `envconfig:"TK_CDE"` } type envImagesConfig struct { diff --git a/pkg/api/v1/testkube/model_test_workflow_execution_extended.go b/pkg/api/v1/testkube/model_test_workflow_execution_extended.go index a50732cf9bb..66a9b90afc7 100644 --- a/pkg/api/v1/testkube/model_test_workflow_execution_extended.go +++ b/pkg/api/v1/testkube/model_test_workflow_execution_extended.go @@ -47,6 +47,10 @@ func (e *TestWorkflowExecution) GetNamespace(defaultNamespace string) string { } func (e *TestWorkflowExecution) ContainsExecuteAction() bool { + if e == nil { + return false + } + if e.ResolvedWorkflow == nil || e.ResolvedWorkflow.Spec == nil { return false } @@ -60,3 +64,21 @@ func (e *TestWorkflowExecution) ContainsExecuteAction() bool { return false } + +func (e *TestWorkflowExecution) GetTemplateRefs() []TestWorkflowTemplateRef { + if e == nil { + return nil + } + + if e.ResolvedWorkflow == nil || e.ResolvedWorkflow.Spec == nil { + return nil + } + + var templateRefs []TestWorkflowTemplateRef + steps := append(e.ResolvedWorkflow.Spec.Setup, append(e.ResolvedWorkflow.Spec.Steps, e.ResolvedWorkflow.Spec.After...)...) + for _, step := range steps { + templateRefs = append(templateRefs, step.GetTemplateRefs()...) + } + + return templateRefs +} diff --git a/pkg/api/v1/testkube/model_test_workflow_step_extended.go b/pkg/api/v1/testkube/model_test_workflow_step_extended.go index 951e91dc1e6..b8ddb9db83e 100644 --- a/pkg/api/v1/testkube/model_test_workflow_step_extended.go +++ b/pkg/api/v1/testkube/model_test_workflow_step_extended.go @@ -40,9 +40,28 @@ func (w *TestWorkflowStep) ContainsExecuteAction() bool { } } - if w.Parallel.ContainsExecuteAction() { + if w.Parallel != nil && w.Parallel.ContainsExecuteAction() { return true } return false } + +func (w *TestWorkflowStep) GetTemplateRefs() []TestWorkflowTemplateRef { + var templateRefs []TestWorkflowTemplateRef + + if w.Template != nil { + templateRefs = append(templateRefs, *w.Template) + } + + steps := append(w.Setup, w.Steps...) + for _, step := range steps { + templateRefs = append(templateRefs, step.GetTemplateRefs()...) + } + + if w.Parallel != nil { + templateRefs = append(templateRefs, w.Parallel.GetTemplateRefs()...) + } + + return templateRefs +} diff --git a/pkg/api/v1/testkube/model_test_workflow_step_parallel_extended.go b/pkg/api/v1/testkube/model_test_workflow_step_parallel_extended.go index d4f8f785aff..7ad342975e1 100644 --- a/pkg/api/v1/testkube/model_test_workflow_step_parallel_extended.go +++ b/pkg/api/v1/testkube/model_test_workflow_step_parallel_extended.go @@ -14,3 +14,18 @@ func (w *TestWorkflowStepParallel) ContainsExecuteAction() bool { return false } + +func (w *TestWorkflowStepParallel) GetTemplateRefs() []TestWorkflowTemplateRef { + var templateRefs []TestWorkflowTemplateRef + + if w.Template != nil { + templateRefs = append(templateRefs, *w.Template) + } + + steps := append(w.Setup, append(w.Steps, w.After...)...) + for _, step := range steps { + templateRefs = append(templateRefs, step.GetTemplateRefs()...) + } + + return templateRefs +} diff --git a/pkg/mapper/cdevents/mapper.go b/pkg/mapper/cdevents/mapper.go index ce3e2658c04..a205f706d59 100644 --- a/pkg/mapper/cdevents/mapper.go +++ b/pkg/mapper/cdevents/mapper.go @@ -27,34 +27,19 @@ func MapTestkubeEventToCDEvent(tkEvent testkube.Event, clusterID, defaultNamespa case *testkube.EventEndTestSuiteAborted, *testkube.EventEndTestSuiteFailed, *testkube.EventEndTestSuiteTimeout, *testkube.EventEndTestSuiteSuccess: return MapTestkubeEventFinishTestSuiteToCDEvent(tkEvent, clusterID, dashboardURI) case *testkube.EventQueueTestWorkflow: - containsExuctionAction := false - if tkEvent.TestWorkflowExecution != nil { - containsExuctionAction = tkEvent.TestWorkflowExecution.ContainsExecuteAction() - } - - if containsExuctionAction { + if tkEvent.TestWorkflowExecution.ContainsExecuteAction() { return MapTestkubeEventQueuedTestWorkflowTestSuiteToCDEvent(tkEvent, clusterID, defaultNamespace, dashboardURI) } return MapTestkubeEventQueuedTestWorkflowTestToCDEvent(tkEvent, clusterID, defaultNamespace, dashboardURI) case *testkube.EventStartTestWorkflow: - containsExuctionAction := false - if tkEvent.TestWorkflowExecution != nil { - containsExuctionAction = tkEvent.TestWorkflowExecution.ContainsExecuteAction() - } - - if containsExuctionAction { + if tkEvent.TestWorkflowExecution.ContainsExecuteAction() { return MapTestkubeEventStartTestWorkflowTestSuiteToCDEvent(tkEvent, clusterID, defaultNamespace, dashboardURI) } return MapTestkubeEventStartTestWorkflowTestToCDEvent(tkEvent, clusterID, defaultNamespace, dashboardURI) case *testkube.EventEndTestWorkflowAborted, *testkube.EventEndTestWorkflowFailed, *testkube.EventEndTestWorkflowSuccess: - containsExuctionAction := false - if tkEvent.TestWorkflowExecution != nil { - containsExuctionAction = tkEvent.TestWorkflowExecution.ContainsExecuteAction() - } - - if containsExuctionAction { + if tkEvent.TestWorkflowExecution.ContainsExecuteAction() { return MapTestkubeEventFinishTestWorkflowTestSuiteToCDEvent(tkEvent, clusterID, defaultNamespace, dashboardURI) } @@ -523,9 +508,9 @@ func MapTestkubeEventQueuedTestWorkflowTestToCDEvent(event testkube.Event, clust } ev.SetSubjectTestCase(&cdevents.TestCaseRunQueuedSubjectContentTestCase{ - Id: workflowName, - // Type: MapTestkubeTestTypeToCDEventTestCaseType(event.TestWorkflowExecution.TestType), - Uri: fmt.Sprintf("%s/test-workflows/%s", dashboardURI, workflowName), + Id: workflowName, + Type: MapTestkubeTestWorkflowTemplateToCDEventTestCaseType(event.TestWorkflowExecution.GetTemplateRefs()), + Uri: fmt.Sprintf("%s/test-workflows/%s", dashboardURI, workflowName), }) namespace := event.TestWorkflowExecution.Namespace @@ -537,20 +522,6 @@ func MapTestkubeEventQueuedTestWorkflowTestToCDEvent(event testkube.Event, clust Id: namespace, Source: clusterID, }) - /* - if event.TestWorkflowExecution.RunningContext != nil { - ev.SetSubjectTrigger(&cdevents.TestCaseRunQueuedSubjectContentTrigger{ - Type: MapTestkubeRunningContextTypeToCDEventTiggerType(event.TestWorkflowExecution.RunningContext.Type_), - }) - } - - if event.TestWorkflowExecution.ParentName != "" { - ev.SetSubjectTestSuiteRun(&cdevents.Reference{ - Id: event.TestWorkflowExecution.ParentName, - Source: clusterID, - }) - } - */ } return ev, nil @@ -590,13 +561,6 @@ func MapTestkubeEventQueuedTestWorkflowTestSuiteToCDEvent(event testkube.Event, Id: namespace, Source: clusterID, }) - /* - if event.TestWorkflowExecution.RunningContext != nil { - ev.SetSubjectTrigger(&cdevents.TestSuiteRunQueuedSubjectContentTrigger{ - Type: MapTestkubeRunningContextTypeToCDEventTiggerType(event.TestWorkflowExecution.RunningContext.Type_), - }) - } - */ } return ev, nil @@ -623,9 +587,9 @@ func MapTestkubeEventStartTestWorkflowTestToCDEvent(event testkube.Event, cluste } ev.SetSubjectTestCase(&cdevents.TestCaseRunStartedSubjectContentTestCase{ - Id: workflowName, - // Type: MapTestkubeTestTypeToCDEventTestCaseType(event.TestWorkflowExecution.TestType), - Uri: fmt.Sprintf("%s/test-workflows/%s", dashboardURI, workflowName), + Id: workflowName, + Type: MapTestkubeTestWorkflowTemplateToCDEventTestCaseType(event.TestWorkflowExecution.GetTemplateRefs()), + Uri: fmt.Sprintf("%s/test-workflows/%s", dashboardURI, workflowName), }) namespace := event.TestWorkflowExecution.Namespace @@ -637,20 +601,6 @@ func MapTestkubeEventStartTestWorkflowTestToCDEvent(event testkube.Event, cluste Id: namespace, Source: clusterID, }) - /* - if event.TestWorkflowExecution.RunningContext != nil { - ev.SetSubjectTrigger(&cdevents.TestCaseRunStartedSubjectContentTrigger{ - Type: MapTestkubeRunningContextTypeToCDEventTiggerType(event.TestWorkflowExecution.RunningContext.Type_), - }) - } - - if event.TestWorkflowExecution.ParentName != "" { - ev.SetSubjectTestSuiteRun(&cdevents.Reference{ - Id: event.TestWorkflowExecution.ParentName, - Source: clusterID, - }) - } - */ } return ev, nil @@ -690,13 +640,6 @@ func MapTestkubeEventStartTestWorkflowTestSuiteToCDEvent(event testkube.Event, c Id: namespace, Source: clusterID, }) - /* - if event.TestWorkflowExecution.RunningContext != nil { - ev.SetSubjectTrigger(&cdevents.TestSuiteRunStartedSubjectContentTrigger{ - Type: MapTestkubeRunningContextTypeToCDEventTiggerType(event.TestWorkflowExecution.RunningContext.Type_), - }) - } - */ } return ev, nil @@ -723,9 +666,9 @@ func MapTestkubeEventFinishTestWorkflowTestToCDEvent(event testkube.Event, clust } ev.SetSubjectTestCase(&cdevents.TestCaseRunFinishedSubjectContentTestCase{ - Id: workflowName, - // Type: MapTestkubeTestTypeToCDEventTestCaseType(event.TestWokflowExecution.TestType), - Uri: fmt.Sprintf("%s/test-workflows/%s", dashboardURI, workflowName), + Id: workflowName, + Type: MapTestkubeTestWorkflowTemplateToCDEventTestCaseType(event.TestWorkflowExecution.GetTemplateRefs()), + Uri: fmt.Sprintf("%s/test-workflows/%s", dashboardURI, workflowName), }) namespace := event.TestWorkflowExecution.Namespace @@ -765,14 +708,6 @@ func MapTestkubeEventFinishTestWorkflowTestToCDEvent(event testkube.Event, clust ev.SetSubjectOutcome("pass") } } - /* - if event.TestWorkflowExecution.ParentName != "" { - ev.SetSubjectTestSuiteRun(&cdevents.Reference{ - Id: event.TestWorkflowExecution.ParentName, - Source: clusterID, - }) - } - */ } return ev, nil @@ -875,9 +810,71 @@ func MapTestkubeTestWorkflowLogToCDEvent(event testkube.Event, clusterID, dashbo workflowName = event.TestWorkflowExecution.Workflow.Name } - ev.SetSubjectUri(fmt.Sprintf("%s/test-workflows/%s/executions/%s/log-output", dashboardURI, + ev.SetSubjectUri(fmt.Sprintf("%s/test-workflows/%s/executions/%s", dashboardURI, workflowName, event.TestWorkflowExecution.Id)) } return ev, nil } + +// MapTestkubeTestWorkflowTemplateToCDEventTestCaseType maps OpenAPI spec Test Workflow Template to CDEvent Test Case Type +func MapTestkubeTestWorkflowTemplateToCDEventTestCaseType(templateRefs []testkube.TestWorkflowTemplateRef) string { + var types = map[string]string{ + "official--artillery": "performance", + "official--cypress": "functional", + "official--gradle": "integration", + "official--jmeter": "performance", + "official--k6": "performance", + "official--maven": "integration", + "official--playwright": "functional", + "official--postman": "functional", + } + + templateNames := make(map[string]struct{}) + for _, templateRef := range templateRefs { + if strings.Contains(templateRef.Name, "official--") { + templateNames[templateRef.Name] = struct{}{} + } + } + + for key, value := range types { + for templateName := range templateNames { + if strings.Contains(templateName, key) { + return value + } + } + } + + return "other" +} + +// CDEventsArtifactParameters contains cd events artifact parameters +type CDEventsArtifactParameters struct { + Id string + Name string + WorkflowName string + ClusterID string + DashboardURI string +} + +// MapTestkubeGTestWorkflowArtifactToCDEvent maps OpenAPI spec Test Artifact to CDEvent CDEventReader +func MapTestkubeTestWorkflowArtifactToCDEvent(parameters CDEventsArtifactParameters, path, format string) (cdevents.CDEventReader, error) { + // Create the base event + ev, err := cdevents.NewTestOutputPublishedEvent() + if err != nil { + return nil, err + } + + ev.SetSubjectId(filepath.Join(parameters.Name, path)) + ev.SetSubjectSource(parameters.ClusterID) + ev.SetSource(parameters.ClusterID) + ev.SetSubjectTestCaseRun(&cdevents.Reference{ + Id: parameters.Id, + Source: parameters.ClusterID, + }) + + ev.SetSubjectFormat(format) + ev.SetSubjectOutputType(MapMimeTypeToCDEventOutputType(format)) + ev.SetSubjectUri(fmt.Sprintf("%s/test-workflows/%s/overview/%s/artifacts", parameters.DashboardURI, parameters.WorkflowName, parameters.Id)) + return ev, nil +} diff --git a/pkg/testworkflows/testworkflowexecutor/executor.go b/pkg/testworkflows/testworkflowexecutor/executor.go index 94c99d2bfec..2dc2c222614 100644 --- a/pkg/testworkflows/testworkflowexecutor/executor.go +++ b/pkg/testworkflows/testworkflowexecutor/executor.go @@ -65,6 +65,7 @@ type executor struct { enableImageDataPersistentCache bool imageDataPersistentCacheKey string dashboardURI string + clusterID string serviceAccountNames map[string]string } @@ -81,7 +82,7 @@ func New(emitter *event.Emitter, metrics v1.Metrics, serviceAccountNames map[string]string, globalTemplateName, namespace, apiUrl, defaultRegistry string, - enableImageDataPersistentCache bool, imageDataPersistentCacheKey, dashboardURI string) TestWorkflowExecutor { + enableImageDataPersistentCache bool, imageDataPersistentCacheKey, dashboardURI, clusterID string) TestWorkflowExecutor { if serviceAccountNames == nil { serviceAccountNames = make(map[string]string) } @@ -106,6 +107,7 @@ func New(emitter *event.Emitter, enableImageDataPersistentCache: enableImageDataPersistentCache, imageDataPersistentCacheKey: imageDataPersistentCacheKey, dashboardURI: dashboardURI, + clusterID: clusterID, } } @@ -425,6 +427,8 @@ func (e *executor) Execute(ctx context.Context, workflow testworkflowsv1.TestWor "api.url": e.apiUrl, "namespace": namespace, "defaultRegistry": e.defaultRegistry, + "clusterId": e.clusterID, + "cdeventsTarget": os.Getenv("CDEVENTS_TARGET"), "images.init": constants.DefaultInitImage, "images.toolkit": constants.DefaultToolkitImage, diff --git a/pkg/testworkflows/testworkflowprocessor/container.go b/pkg/testworkflows/testworkflowprocessor/container.go index 51afcb64548..3b2a01de28b 100644 --- a/pkg/testworkflows/testworkflowprocessor/container.go +++ b/pkg/testworkflows/testworkflowprocessor/container.go @@ -431,6 +431,8 @@ func (c *container) EnableToolkit(ref string) Container { "TK_SA": "{{internal.serviceaccount.default}}", "TK_DASH": "{{internal.dashboard.url}}", "TK_API": "{{internal.api.url}}", + "TK_CLU": "{{internal.clusterId}}", + "TK_CDE": "{{internal.cdeventsTarget}}", "TK_C_URL": "{{internal.cloud.api.url}}", "TK_C_KEY": "{{internal.cloud.api.key}}", "TK_C_TLS_INSECURE": "{{internal.cloud.api.tlsInsecure}}", From 90965ce8834613bd1dcb1970020b5d56ba3197a5 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Mon, 1 Jul 2024 14:53:37 +0300 Subject: [PATCH 13/21] feat: unit tests for cd events mapper test workflows Signed-off-by: Vladislav Sukhin --- pkg/mapper/cdevents/mapper.go | 6 +- pkg/mapper/cdevents/mapper_test.go | 500 ++++++++++++++++++++++++++++- 2 files changed, 502 insertions(+), 4 deletions(-) diff --git a/pkg/mapper/cdevents/mapper.go b/pkg/mapper/cdevents/mapper.go index a205f706d59..27a166ab9bb 100644 --- a/pkg/mapper/cdevents/mapper.go +++ b/pkg/mapper/cdevents/mapper.go @@ -580,7 +580,7 @@ func MapTestkubeEventStartTestWorkflowTestToCDEvent(event testkube.Event, cluste ev.SetSubjectSource(clusterID) ev.SetSource(clusterID) - if event.TestExecution != nil { + if event.TestWorkflowExecution != nil { workflowName := "" if event.TestWorkflowExecution.Workflow != nil { workflowName = event.TestWorkflowExecution.Workflow.Name @@ -653,8 +653,8 @@ func MapTestkubeEventFinishTestWorkflowTestToCDEvent(event testkube.Event, clust return nil, err } - if event.TestExecution != nil { - ev.SetSubjectId(event.TestExecution.Id) + if event.TestWorkflowExecution != nil { + ev.SetSubjectId(event.TestWorkflowExecution.Id) } ev.SetSubjectSource(clusterID) diff --git a/pkg/mapper/cdevents/mapper_test.go b/pkg/mapper/cdevents/mapper_test.go index ad567e3e483..e66146e1ed7 100644 --- a/pkg/mapper/cdevents/mapper_test.go +++ b/pkg/mapper/cdevents/mapper_test.go @@ -94,7 +94,7 @@ func TestMapTestkubeEventQueuedTestToCDEvent(t *testing.T) { } } -func TestMapTestkubeEventStatTestToCDEvent(t *testing.T) { +func TestMapTestkubeEventStartTestToCDEvent(t *testing.T) { t.Parallel() event := testkube.Event{ @@ -488,3 +488,501 @@ func TestMapTestkubeEventFinishTestSuiteToCDEvent(t *testing.T) { t.Errorf("Unexpected reason: %s", reason) } } + +func TestMapTestkubeEventQueuedTestWorkflowTestToCDEvent(t *testing.T) { + t.Parallel() + + event := testkube.Event{ + TestWorkflowExecution: &testkube.TestWorkflowExecution{ + Id: "1", + Name: "test-1", + Namespace: "default", + Workflow: &testkube.TestWorkflow{ + Name: "Test 1", + }, + ResolvedWorkflow: &testkube.TestWorkflow{ + Spec: &testkube.TestWorkflowSpec{ + Steps: []testkube.TestWorkflowStep{ + { + Template: &testkube.TestWorkflowTemplateRef{ + Name: "official--k6--v1", + }, + }, + }, + }, + }, + }, + } + clusterID := "cluster-1" + defaultNamespace := "default" + + ev, err := MapTestkubeEventQueuedTestWorkflowTestToCDEvent(event, clusterID, defaultNamespace, "") + if err != nil { + t.Errorf("Error mapping event: %v", err) + return + } + + subjectID := ev.GetSubjectId() + if subjectID != "1" { + t.Errorf("Unexpected subject ID: %s", subjectID) + } + + subjectSource := ev.GetSubjectSource() + if subjectSource != clusterID { + t.Errorf("Unexpected subject source: %s", subjectSource) + } + + source := ev.GetSource() + if source != clusterID { + t.Errorf("Unexpected source: %s", source) + } + + cde, ok := ev.(*cdevents.TestCaseRunQueuedEvent) + assert.Equal(t, true, ok) + + testID := cde.Subject.Content.TestCase.Id + if testID != "Test 1" { + t.Errorf("Unexpected test case id: %s", testID) + } + + testType := cde.Subject.Content.TestCase.Type + if testType != "performance" { + t.Errorf("Unexpected test case type: %s", testType) + } + + testURI := cde.Subject.Content.TestCase.Uri + if testURI != "/test-workflows/Test 1" { + t.Errorf("Unexpected test case uri: %s", testURI) + } + + envID := cde.Subject.Content.Environment.Id + if envID != defaultNamespace { + t.Errorf("Unexpected environment id: %s", envID) + } + + envSource := cde.Subject.Content.Environment.Source + if envSource != clusterID { + t.Errorf("Unexpected environment source: %s", envSource) + } +} + +func TestMapTestkubeEventQueuedTestWorkflowTestSuiteToCDEvent(t *testing.T) { + t.Parallel() + + event := testkube.Event{ + TestWorkflowExecution: &testkube.TestWorkflowExecution{ + Id: "1", + Name: "suite-1", + Namespace: "default", + Workflow: &testkube.TestWorkflow{ + Name: "Suite 1", + }, + ResolvedWorkflow: &testkube.TestWorkflow{ + Spec: &testkube.TestWorkflowSpec{ + Steps: []testkube.TestWorkflowStep{ + { + Execute: &testkube.TestWorkflowStepExecute{ + Workflows: []testkube.TestWorkflowStepExecuteTestWorkflowRef{ + { + Name: "test-1", + }, + }, + }, + }, + }, + }, + }, + }, + } + clusterID := "cluster-1" + defaultNamespace := "default" + + ev, err := MapTestkubeEventQueuedTestWorkflowTestSuiteToCDEvent(event, clusterID, defaultNamespace, "") + if err != nil { + t.Errorf("Error mapping event: %v", err) + return + } + + subjectID := ev.GetSubjectId() + if subjectID != "1" { + t.Errorf("Unexpected subject ID: %s", subjectID) + } + + subjectSource := ev.GetSubjectSource() + if subjectSource != clusterID { + t.Errorf("Unexpected subject source: %s", subjectSource) + } + + source := ev.GetSource() + if source != clusterID { + t.Errorf("Unexpected source: %s", source) + } + + cde, ok := ev.(*cdevents.TestSuiteRunQueuedEvent) + assert.Equal(t, true, ok) + + suiteID := cde.Subject.Content.TestSuite.Id + if suiteID != "Suite 1" { + t.Errorf("Unexpected test suite id: %s", suiteID) + } + + suiteURI := cde.Subject.Content.TestSuite.Url + if suiteURI != "/test-workflows/Suite 1" { + t.Errorf("Unexpected test case uri: %s", suiteURI) + } + + envID := cde.Subject.Content.Environment.Id + if envID != "default" { + t.Errorf("Unexpected environment id: %s", envID) + } + + envSource := cde.Subject.Content.Environment.Source + if envSource != clusterID { + t.Errorf("Unexpected environment source: %s", envSource) + } +} + +func TestMapTestkubeEventStartTestWorkflowTestToCDEvent(t *testing.T) { + t.Parallel() + + event := testkube.Event{ + TestWorkflowExecution: &testkube.TestWorkflowExecution{ + Id: "1", + Name: "test-1", + Namespace: "default", + Workflow: &testkube.TestWorkflow{ + Name: "Test 1", + }, + ResolvedWorkflow: &testkube.TestWorkflow{ + Spec: &testkube.TestWorkflowSpec{ + Steps: []testkube.TestWorkflowStep{ + { + Template: &testkube.TestWorkflowTemplateRef{ + Name: "official--k6--v1", + }, + }, + }, + }, + }, + }, + } + clusterID := "cluster-1" + defaultNamespace := "default" + + ev, err := MapTestkubeEventStartTestWorkflowTestToCDEvent(event, clusterID, defaultNamespace, "") + if err != nil { + t.Errorf("Error mapping event: %v", err) + return + } + + subjectID := ev.GetSubjectId() + if subjectID != "1" { + t.Errorf("Unexpected subject ID: %s", subjectID) + } + + subjectSource := ev.GetSubjectSource() + if subjectSource != clusterID { + t.Errorf("Unexpected subject source: %s", subjectSource) + } + + source := ev.GetSource() + if source != clusterID { + t.Errorf("Unexpected source: %s", source) + } + + cde, ok := ev.(*cdevents.TestCaseRunStartedEvent) + assert.Equal(t, true, ok) + + testID := cde.Subject.Content.TestCase.Id + if testID != "Test 1" { + t.Errorf("Unexpected test case id: %s", testID) + } + + testType := cde.Subject.Content.TestCase.Type + if testType != "performance" { + t.Errorf("Unexpected test case type: %s", testType) + } + + testURI := cde.Subject.Content.TestCase.Uri + if testURI != "/test-workflows/Test 1" { + t.Errorf("Unexpected test case uri: %s", testURI) + } + + envID := cde.Subject.Content.Environment.Id + if envID != defaultNamespace { + t.Errorf("Unexpected environment id: %s", envID) + } + + envSource := cde.Subject.Content.Environment.Source + if envSource != clusterID { + t.Errorf("Unexpected environment source: %s", envSource) + } + +} + +func TestMapTestkubeEventStartTestWorkflowTestSuiteToCDEvent(t *testing.T) { + t.Parallel() + + event := testkube.Event{ + TestWorkflowExecution: &testkube.TestWorkflowExecution{ + Id: "1", + Name: "suite-1", + Namespace: "default", + Workflow: &testkube.TestWorkflow{ + Name: "Suite 1", + }, + ResolvedWorkflow: &testkube.TestWorkflow{ + Spec: &testkube.TestWorkflowSpec{ + Steps: []testkube.TestWorkflowStep{ + { + Execute: &testkube.TestWorkflowStepExecute{ + Workflows: []testkube.TestWorkflowStepExecuteTestWorkflowRef{ + { + Name: "test-1", + }, + }, + }, + }, + }, + }, + }, + }, + } + clusterID := "cluster-1" + defaultNamespace := "default" + + ev, err := MapTestkubeEventStartTestWorkflowTestSuiteToCDEvent(event, clusterID, defaultNamespace, "") + if err != nil { + t.Errorf("Error mapping event: %v", err) + return + } + + subjectID := ev.GetSubjectId() + if subjectID != "1" { + t.Errorf("Unexpected subject ID: %s", subjectID) + } + + subjectSource := ev.GetSubjectSource() + if subjectSource != clusterID { + t.Errorf("Unexpected subject source: %s", subjectSource) + } + + source := ev.GetSource() + if source != clusterID { + t.Errorf("Unexpected source: %s", source) + } + + cde, ok := ev.(*cdevents.TestSuiteRunStartedEvent) + assert.Equal(t, true, ok) + + suiteID := cde.Subject.Content.TestSuite.Id + if suiteID != "Suite 1" { + t.Errorf("Unexpected test suite id: %s", suiteID) + } + + suiteURI := cde.Subject.Content.TestSuite.Uri + if suiteURI != "/test-workflows/Suite 1" { + t.Errorf("Unexpected test case uri: %s", suiteURI) + } + + envID := cde.Subject.Content.Environment.Id + if envID != "default" { + t.Errorf("Unexpected environment id: %s", envID) + } + + envSource := cde.Subject.Content.Environment.Source + if envSource != clusterID { + t.Errorf("Unexpected environment source: %s", envSource) + } +} + +func TestMapTestkubeEventFinishTestWorkflowTestToCDEvent(t *testing.T) { + t.Parallel() + + status := testkube.FAILED_TestWorkflowStatus + event := testkube.Event{ + TestWorkflowExecution: &testkube.TestWorkflowExecution{ + Id: "1", + Name: "test-1", + Namespace: "default", + Workflow: &testkube.TestWorkflow{ + Name: "Test 1", + }, + ResolvedWorkflow: &testkube.TestWorkflow{ + Spec: &testkube.TestWorkflowSpec{ + Steps: []testkube.TestWorkflowStep{ + { + Template: &testkube.TestWorkflowTemplateRef{ + Name: "official--k6--v1", + }, + }, + }, + }, + }, + Result: &testkube.TestWorkflowResult{ + Status: &status, + Steps: map[string]testkube.TestWorkflowStepResult{ + "first": { + ErrorMessage: "fake", + }, + }, + }, + }, + } + clusterID := "cluster-1" + defaultNamespace := "default" + + ev, err := MapTestkubeEventFinishTestWorkflowTestToCDEvent(event, clusterID, defaultNamespace, "") + if err != nil { + t.Errorf("Error mapping event: %v", err) + return + } + + subjectID := ev.GetSubjectId() + if subjectID != "1" { + t.Errorf("Unexpected subject ID: %s", subjectID) + } + + subjectSource := ev.GetSubjectSource() + if subjectSource != clusterID { + t.Errorf("Unexpected subject source: %s", subjectSource) + } + + source := ev.GetSource() + if source != clusterID { + t.Errorf("Unexpected source: %s", source) + } + + cde, ok := ev.(*cdevents.TestCaseRunFinishedEvent) + assert.Equal(t, true, ok) + + testID := cde.Subject.Content.TestCase.Id + if testID != "Test 1" { + t.Errorf("Unexpected test case id: %s", testID) + } + + testType := cde.Subject.Content.TestCase.Type + if testType != "performance" { + t.Errorf("Unexpected test case type: %s", testType) + } + + testURI := cde.Subject.Content.TestCase.Uri + if testURI != "/test-workflows/Test 1" { + t.Errorf("Unexpected test case uri: %s", testURI) + } + + envID := cde.Subject.Content.Environment.Id + if envID != defaultNamespace { + t.Errorf("Unexpected environment id: %s", envID) + } + + envSource := cde.Subject.Content.Environment.Source + if envSource != clusterID { + t.Errorf("Unexpected environment source: %s", envSource) + } + + outcome := cde.Subject.Content.Outcome + if outcome != "fail" { + t.Errorf("Unexpected outcome: %s", outcome) + } + + reason := cde.Subject.Content.Reason + if reason != "fake" { + t.Errorf("Unexpected reason: %s", reason) + } +} + +func TestMapTestkubeEventFinishTestWorkflowTestSuiteToCDEvent(t *testing.T) { + t.Parallel() + + status := testkube.FAILED_TestWorkflowStatus + event := testkube.Event{ + TestWorkflowExecution: &testkube.TestWorkflowExecution{ + Id: "1", + Name: "suite-1", + Namespace: "default", + Workflow: &testkube.TestWorkflow{ + Name: "Suite 1", + }, + ResolvedWorkflow: &testkube.TestWorkflow{ + Spec: &testkube.TestWorkflowSpec{ + Steps: []testkube.TestWorkflowStep{ + { + Execute: &testkube.TestWorkflowStepExecute{ + Workflows: []testkube.TestWorkflowStepExecuteTestWorkflowRef{ + { + Name: "test-1", + }, + }, + }, + }, + }, + }, + }, + Result: &testkube.TestWorkflowResult{ + Status: &status, + Steps: map[string]testkube.TestWorkflowStepResult{ + "first": { + ErrorMessage: "fake", + }, + }, + }, + }, + } + clusterID := "cluster-1" + defaultNamespace := "default" + + ev, err := MapTestkubeEventFinishTestWorkflowTestSuiteToCDEvent(event, clusterID, defaultNamespace, "") + if err != nil { + t.Errorf("Error mapping event: %v", err) + return + } + + subjectID := ev.GetSubjectId() + if subjectID != "1" { + t.Errorf("Unexpected subject ID: %s", subjectID) + } + + subjectSource := ev.GetSubjectSource() + if subjectSource != clusterID { + t.Errorf("Unexpected subject source: %s", subjectSource) + } + + source := ev.GetSource() + if source != clusterID { + t.Errorf("Unexpected source: %s", source) + } + + cde, ok := ev.(*cdevents.TestSuiteRunFinishedEvent) + assert.Equal(t, true, ok) + + suiteID := cde.Subject.Content.TestSuite.Id + if suiteID != "Suite 1" { + t.Errorf("Unexpected test suite id: %s", suiteID) + } + + suiteURI := cde.Subject.Content.TestSuite.Uri + if suiteURI != "/test-workflows/Suite 1" { + t.Errorf("Unexpected test case uri: %s", suiteURI) + } + + envID := cde.Subject.Content.Environment.Id + if envID != "default" { + t.Errorf("Unexpected environment id: %s", envID) + } + + envSource := cde.Subject.Content.Environment.Source + if envSource != clusterID { + t.Errorf("Unexpected environment source: %s", envSource) + } + + outcome := cde.Subject.Content.Outcome + if outcome != "fail" { + t.Errorf("Unexpected outcome: %s", outcome) + } + + reason := cde.Subject.Content.Reason + if reason != "fake" { + t.Errorf("Unexpected reason: %s", reason) + } +} From be2e7c93b06d3637e6769a1768e30efb640ba4e4 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Mon, 1 Jul 2024 15:04:34 +0300 Subject: [PATCH 14/21] fix: rename field Signed-off-by: Vladislav Sukhin --- cmd/testworkflow-toolkit/commands/artifacts.go | 4 ++-- cmd/testworkflow-toolkit/env/config.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/testworkflow-toolkit/commands/artifacts.go b/cmd/testworkflow-toolkit/commands/artifacts.go index d047aa01598..615cd6daf8f 100644 --- a/cmd/testworkflow-toolkit/commands/artifacts.go +++ b/cmd/testworkflow-toolkit/commands/artifacts.go @@ -148,8 +148,8 @@ func NewArtifactsCmd() *cobra.Command { } // Support cd evaents - if env.Config().System.CDEventTarget != "" { - handlerOpts = append(handlerOpts, artifacts.WithCDEventsTarget(env.Config().System.CDEventTarget)) + if env.Config().System.CDEventsTarget != "" { + handlerOpts = append(handlerOpts, artifacts.WithCDEventsTarget(env.Config().System.CDEventsTarget)) handlerOpts = append(handlerOpts, artifacts.WithCDEventsArtifactParameters(cdevents.CDEventsArtifactParameters{ Id: env.Config().Execution.Id, Name: env.Config().Execution.Name, diff --git a/cmd/testworkflow-toolkit/env/config.go b/cmd/testworkflow-toolkit/env/config.go index 31851130408..c7fd6207610 100644 --- a/cmd/testworkflow-toolkit/env/config.go +++ b/cmd/testworkflow-toolkit/env/config.go @@ -55,7 +55,7 @@ type envSystemConfig struct { DashboardUrl string `envconfig:"TK_DASH"` ApiUrl string `envconfig:"TK_API"` ClusterID string `envconfig:"TK_CLU"` - CDEventTarget string `envconfig:"TK_CDE"` + CDEventsTarget string `envconfig:"TK_CDE"` } type envImagesConfig struct { From c6c4c5cc46d9c769e478528722770dea89f7a085 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Mon, 1 Jul 2024 15:21:27 +0300 Subject: [PATCH 15/21] fix: integration test Signed-off-by: Vladislav Sukhin --- .../commands/artifacts_test.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cmd/testworkflow-toolkit/commands/artifacts_test.go b/cmd/testworkflow-toolkit/commands/artifacts_test.go index 84854f8eff2..83469d16489 100644 --- a/cmd/testworkflow-toolkit/commands/artifacts_test.go +++ b/cmd/testworkflow-toolkit/commands/artifacts_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/kubeshop/testkube/pkg/cloud/data/testworkflow" + "github.com/kubeshop/testkube/pkg/mapper/cdevents" "github.com/kubeshop/testkube/pkg/utils/test" "github.com/golang/mock/gomock" @@ -37,6 +38,10 @@ func TestRun_Integration(t *testing.T) { w.WriteHeader(http.StatusOK) return } + if r.Method == http.MethodPost { + w.WriteHeader(http.StatusOK) + return + } http.NotFound(w, r) }) @@ -66,9 +71,15 @@ func TestRun_Integration(t *testing.T) { return filesystem.NewMockFile(path[1:], b), nil }) postProcessor := artifacts.NewJUnitPostProcessor(mockFs, mockClient, "/", "") - handler := artifacts.NewHandler(uploader, processor, artifacts.WithPostProcessor(postProcessor)) + handler := artifacts.NewHandler(uploader, processor, artifacts.WithPostProcessor(postProcessor), + artifacts.WithCDEventsTarget(server.URL), artifacts.WithCDEventsArtifactParameters(cdevents.CDEventsArtifactParameters{ + Id: "1", + Name: "test-1", + WorkflowName: "test", + ClusterID: "12345", + })) run(handler, walker, testDataFixtures) - assert.Equal(t, 2, httpRequestCount) + assert.Equal(t, 4, httpRequestCount) } From dbe628459cd64628dbf3f6134ff4ac63e81f9418 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 2 Jul 2024 15:42:09 +0300 Subject: [PATCH 16/21] fix: unmarshaling typo Signed-off-by: Vladislav Sukhin --- internal/app/api/v1/testworkflowtemplates.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/api/v1/testworkflowtemplates.go b/internal/app/api/v1/testworkflowtemplates.go index 755faa7cd46..dfbfe12f704 100644 --- a/internal/app/api/v1/testworkflowtemplates.go +++ b/internal/app/api/v1/testworkflowtemplates.go @@ -84,7 +84,7 @@ func (s *TestkubeAPI) CreateTestWorkflowTemplateHandler() fiber.Handler { } } else { var v *testkube.TestWorkflowTemplate - err = c.BodyParser(&obj) + err = c.BodyParser(&v) if err != nil { return s.BadRequest(c, errPrefix, "invalid body", err) } From 5f734c4088b9af65e9f34b696607567e3202114a Mon Sep 17 00:00:00 2001 From: Jacek Wysocki Date: Wed, 3 Jul 2024 16:20:27 +0200 Subject: [PATCH 17/21] fix: error handling when creating TW (#5631) * fix: error handling when creating TW * fix: remove object.name update * fix: move error to debug * fix: handle error on update --- cmd/kubectl-testkube/commands/testworkflows/create.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cmd/kubectl-testkube/commands/testworkflows/create.go b/cmd/kubectl-testkube/commands/testworkflows/create.go index 07424cea09f..47e0db5c8c0 100644 --- a/cmd/kubectl-testkube/commands/testworkflows/create.go +++ b/cmd/kubectl-testkube/commands/testworkflows/create.go @@ -67,7 +67,15 @@ func NewCreateTestWorkflowCmd() *cobra.Command { } } - workflow, _ := client.GetTestWorkflow(obj.Name) + workflow, err := client.GetTestWorkflow(obj.Name) + if err != nil { + if update { + ui.ExitOnError("getting test workflow "+obj.Name+" in namespace "+obj.Namespace, err) + } else { + ui.Debug("getting test workflow "+obj.Name+" in namespace "+obj.Namespace, err.Error()) + } + } + if workflow.Name != "" { if !update { ui.Failf("Test workflow with name '%s' already exists in namespace %s, use --update flag for upsert", obj.Name, namespace) From a1aa8b623cbca2c51c0441848e47aa32552facf2 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 2 Jul 2024 14:15:20 +0300 Subject: [PATCH 18/21] fix: user friendly oss error Signed-off-by: Vladislav Sukhin --- .../commands/testworkflows/run.go | 13 +++++++++ .../constants/constants.go | 27 +++++++++++-------- .../testworkflowprocessor/operations.go | 6 ++--- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/cmd/kubectl-testkube/commands/testworkflows/run.go b/cmd/kubectl-testkube/commands/testworkflows/run.go index f112b9517e5..581f7b05c9b 100644 --- a/cmd/kubectl-testkube/commands/testworkflows/run.go +++ b/cmd/kubectl-testkube/commands/testworkflows/run.go @@ -15,6 +15,7 @@ import ( "github.com/kubeshop/testkube/cmd/testworkflow-init/data" apiclientv1 "github.com/kubeshop/testkube/pkg/api/v1/client" "github.com/kubeshop/testkube/pkg/api/v1/testkube" + "github.com/kubeshop/testkube/pkg/testworkflows/testworkflowprocessor/constants" "github.com/kubeshop/testkube/pkg/ui" ) @@ -69,6 +70,18 @@ func NewRunTestWorkflowCmd() *cobra.Command { Config: config, DisableWebhooks: disableWebhooks, }) + if err != nil { + errs := []error{constants.ErrOpenSourceExecuteOperationIsNotAvailable, + constants.ErrOpenSourceParallelOperationIsNotAvailable, + constants.ErrOpenSourceServicesOperationIsNotAvailable} + for _, e := range errs { + if strings.Contains(err.Error(), e.Error()) { + err = e + break + } + } + } + ui.ExitOnError("execute test workflow "+name+" from namespace "+namespace, err) err = renderer.PrintTestWorkflowExecution(cmd, os.Stdout, execution) ui.ExitOnError("render test workflow execution", err) diff --git a/pkg/testworkflows/testworkflowprocessor/constants/constants.go b/pkg/testworkflows/testworkflowprocessor/constants/constants.go index 39c78de40a0..70e6a3b3da8 100644 --- a/pkg/testworkflows/testworkflowprocessor/constants/constants.go +++ b/pkg/testworkflows/testworkflowprocessor/constants/constants.go @@ -1,6 +1,7 @@ package constants import ( + "errors" "fmt" "os" "path/filepath" @@ -14,15 +15,16 @@ import ( ) const ( - DefaultInternalPath = "/.tktw" - DefaultDataPath = "/data" - DefaultTerminationLogPath = "/dev/termination-log" - DefaultFsGroup = int64(1001) - ResourceIdLabelName = "testworkflowid" - RootResourceIdLabelName = "testworkflowid-root" - GroupIdLabelName = "testworkflowid-group" - SignatureAnnotationName = "testworkflows.testkube.io/signature" - RFC3339Millis = "2006-01-02T15:04:05.000Z07:00" + DefaultInternalPath = "/.tktw" + DefaultDataPath = "/data" + DefaultTerminationLogPath = "/dev/termination-log" + DefaultFsGroup = int64(1001) + ResourceIdLabelName = "testworkflowid" + RootResourceIdLabelName = "testworkflowid-root" + GroupIdLabelName = "testworkflowid-group" + SignatureAnnotationName = "testworkflows.testkube.io/signature" + RFC3339Millis = "2006-01-02T15:04:05.000Z07:00" + OpenSourceOperationErrorMessage = "operation is not available when running the Testkube Agent in the standalone mode" ) var ( @@ -66,8 +68,11 @@ echo -n ',0' > && echo 'Done.' && exit 0 {Name: "CI", Value: "1"}, }, } - DefaultInitImage = getInitImage() - DefaultToolkitImage = getToolkitImage() + DefaultInitImage = getInitImage() + DefaultToolkitImage = getToolkitImage() + ErrOpenSourceExecuteOperationIsNotAvailable = errors.New(`"execute" ` + OpenSourceOperationErrorMessage) + ErrOpenSourceParallelOperationIsNotAvailable = errors.New(`"parallel" ` + OpenSourceOperationErrorMessage) + ErrOpenSourceServicesOperationIsNotAvailable = errors.New(`"services" ` + OpenSourceOperationErrorMessage) ) func stripCommonImagePrefix(image, common string) string { diff --git a/pkg/testworkflows/testworkflowprocessor/operations.go b/pkg/testworkflows/testworkflowprocessor/operations.go index 4fed6e143cc..b25927e3cff 100644 --- a/pkg/testworkflows/testworkflowprocessor/operations.go +++ b/pkg/testworkflows/testworkflowprocessor/operations.go @@ -303,21 +303,21 @@ func ProcessArtifacts(_ InternalProcessor, layer Intermediate, container Contain func StubExecute(_ InternalProcessor, _ Intermediate, _ Container, step testworkflowsv1.Step) (Stage, error) { if step.Execute != nil { - return nil, errors.New(`"execute" operation is not available when running the Testkube Agent in the standalone mode`) + return nil, constants.ErrOpenSourceExecuteOperationIsNotAvailable } return nil, nil } func StubParallel(_ InternalProcessor, _ Intermediate, _ Container, step testworkflowsv1.Step) (Stage, error) { if step.Parallel != nil { - return nil, errors.New(`"parallel" operation is not available when running the Testkube Agent in the standalone mode`) + return nil, constants.ErrOpenSourceParallelOperationIsNotAvailable } return nil, nil } func StubServices(_ InternalProcessor, _ Intermediate, _ Container, step testworkflowsv1.Step) (Stage, error) { if len(step.Services) != 0 { - return nil, errors.New(`"services" are not available when running the Testkube Agent in the standalone mode`) + return nil, constants.ErrOpenSourceServicesOperationIsNotAvailable } return nil, nil } From 8b1286d8fba169a20a6bd76e52a91c6b6d3b04b6 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Tue, 2 Jul 2024 14:19:25 +0300 Subject: [PATCH 19/21] fix: add comment Signed-off-by: Vladislav Sukhin --- cmd/kubectl-testkube/commands/testworkflows/run.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/kubectl-testkube/commands/testworkflows/run.go b/cmd/kubectl-testkube/commands/testworkflows/run.go index 581f7b05c9b..413814f0739 100644 --- a/cmd/kubectl-testkube/commands/testworkflows/run.go +++ b/cmd/kubectl-testkube/commands/testworkflows/run.go @@ -71,6 +71,7 @@ func NewRunTestWorkflowCmd() *cobra.Command { DisableWebhooks: disableWebhooks, }) if err != nil { + // User friendly Open Source operation error errs := []error{constants.ErrOpenSourceExecuteOperationIsNotAvailable, constants.ErrOpenSourceParallelOperationIsNotAvailable, constants.ErrOpenSourceServicesOperationIsNotAvailable} From a6b2cace8e14ff00b2df05157908409d90348e83 Mon Sep 17 00:00:00 2001 From: Vladislav Sukhin Date: Wed, 3 Jul 2024 15:25:44 +0300 Subject: [PATCH 20/21] fix: paarse error message Signed-off-by: Vladislav Sukhin --- .../commands/testworkflows/run.go | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/cmd/kubectl-testkube/commands/testworkflows/run.go b/cmd/kubectl-testkube/commands/testworkflows/run.go index 413814f0739..7a42b731e6e 100644 --- a/cmd/kubectl-testkube/commands/testworkflows/run.go +++ b/cmd/kubectl-testkube/commands/testworkflows/run.go @@ -2,6 +2,7 @@ package testworkflows import ( "bytes" + "errors" "fmt" "os" "strings" @@ -21,6 +22,7 @@ import ( const ( LogTimestampLength = 30 // time.RFC3339Nano without 00:00 timezone + apiErrorMessage = "processing error:" ) var ( @@ -72,13 +74,18 @@ func NewRunTestWorkflowCmd() *cobra.Command { }) if err != nil { // User friendly Open Source operation error - errs := []error{constants.ErrOpenSourceExecuteOperationIsNotAvailable, - constants.ErrOpenSourceParallelOperationIsNotAvailable, - constants.ErrOpenSourceServicesOperationIsNotAvailable} - for _, e := range errs { - if strings.Contains(err.Error(), e.Error()) { - err = e - break + errMessage := err.Error() + if strings.Contains(errMessage, constants.OpenSourceOperationErrorMessage) { + startp := strings.LastIndex(errMessage, apiErrorMessage) + endp := strings.Index(errMessage, constants.OpenSourceOperationErrorMessage) + if startp != -1 && endp != -1 { + startp += len(apiErrorMessage) + operation := "" + if endp > startp { + operation = strings.TrimSpace(errMessage[startp:endp]) + } + + err = errors.New(operation + " " + constants.OpenSourceOperationErrorMessage) } } } From 51a1a58c340ddd5f0107a1c22cd538eb5b97749b Mon Sep 17 00:00:00 2001 From: Emil Davtyan Date: Thu, 4 Jul 2024 09:52:58 +0200 Subject: [PATCH 21/21] Update CODEOWNERS --- .github/CODEOWNERS | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ce3375c92e6..2294b81a09f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,7 +1,16 @@ -/assets/ @olensmar -/docs/ @olensmar @jfermi -README.md @olensmar @jfermi -/img/ @olensmar -/api/ @olensmar +* @kubeshop/testkube-backend -* @exu @nicufk @vsukhin @vLia @povilasv @dejanzele +/assets/ @kubeshop/testkube-docs +/docs/ @kubeshop/testkube-docs +/licenses/ @kubeshop/testkube-docs +LICENSE @kubeshop/testkube-docs +*.md @kubeshop/testkube-docs + +/test/ @kubeshop/testkube-qa + +/.github/ @kubeshop/testkube-devops +/goreleaser_files/ @kubeshop/testkube-devops +.goreleaser.yml @kubeshop/testkube-devops +.goreleaser-dev.yml @kubeshop/testkube-devops +.golangci.yml @kubeshop/testkube-devops +.builds-* @kubeshop/testkube-devops