diff --git a/.builds-linux.goreleaser.yml b/.builds-linux.goreleaser.yml
index 512281a9b20..f733a63b880 100644
--- a/.builds-linux.goreleaser.yml
+++ b/.builds-linux.goreleaser.yml
@@ -70,16 +70,6 @@ dockers:
- "--cache-from={{ .Env.DOCKER_BUILDX_CACHE_FROM }}"
- "--build-arg=ALPINE_IMAGE={{ .Env.ALPINE_IMAGE }}"
-docker_manifests:
- - name_template: kubeshop/testkube-cli:{{ .Env.DOCKER_IMAGE_TAG }}
- image_templates:
- - kubeshop/testkube-cli:{{ .Env.DOCKER_IMAGE_TAG }}-amd64
- - kubeshop/testkube-cli:{{ .Env.DOCKER_IMAGE_TAG }}-arm64v8
- - name_template: kubeshop/testkube-cli:latest
- image_templates:
- - kubeshop/testkube-cli:{{ .Env.DOCKER_IMAGE_TAG }}-amd64
- - kubeshop/testkube-cli:{{ .Env.DOCKER_IMAGE_TAG }}-arm64v8
-
docker_signs:
- cmd: cosign
artifacts: all
diff --git a/.github/workflows/docker-build-api-executors-tag.yaml b/.github/workflows/docker-build-api-executors-tag.yaml
index 07bc3616a21..68740f23969 100644
--- a/.github/workflows/docker-build-api-executors-tag.yaml
+++ b/.github/workflows/docker-build-api-executors-tag.yaml
@@ -78,6 +78,116 @@ jobs:
DOCKER_BUILDX_CACHE_TO: "type=gha,mode=max"
ALPINE_IMAGE: ${{ env.ALPINE_IMAGE }}
+ testworkflow-init:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - uses: sigstore/cosign-installer@v3.0.5
+ - uses: anchore/sbom-action/download-syft@v0.14.2
+
+ - name: Set up Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@v1
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v1
+
+ - name: Set-up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ cache: false
+
+ - name: Go Cache
+ uses: actions/cache@v2
+ with:
+ path: |
+ ~/go/pkg/mod
+ ~/.cache/go-build
+ key: testkube-tw-init-go-${{ hashFiles('**/go.sum') }}
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Release
+ uses: goreleaser/goreleaser-action@v4
+ with:
+ distribution: goreleaser
+ version: latest
+ args: release -f goreleaser_files/.goreleaser-docker-build-testworkflow-init.yml
+ env:
+ GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
+ ANALYTICS_TRACKING_ID: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_ID}}
+ ANALYTICS_API_KEY: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_SECRET}}
+ SLACK_BOT_CLIENT_ID: ${{secrets.TESTKUBE_SLACK_BOT_CLIENT_ID}}
+ SLACK_BOT_CLIENT_SECRET: ${{secrets.TESTKUBE_SLACK_BOT_CLIENT_SECRET}}
+ SEGMENTIO_KEY: ${{secrets.TESTKUBE_API_SEGMENTIO_KEY}}
+ CLOUD_SEGMENTIO_KEY: ${{secrets.TESTKUBE_API_CLOUD_SEGMENTIO_KEY}}
+ DOCKER_BUILDX_BUILDER: "${{ steps.buildx.outputs.name }}"
+ DOCKER_BUILDX_CACHE_FROM: "type=gha"
+ DOCKER_BUILDX_CACHE_TO: "type=gha,mode=max"
+ ALPINE_IMAGE: ${{ env.ALPINE_IMAGE }}
+
+ testworkflow-toolkit:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - uses: sigstore/cosign-installer@v3.0.5
+ - uses: anchore/sbom-action/download-syft@v0.14.2
+
+ - name: Set up Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@v1
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v1
+
+ - name: Set-up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ cache: false
+
+ - name: Go Cache
+ uses: actions/cache@v2
+ with:
+ path: |
+ ~/go/pkg/mod
+ ~/.cache/go-build
+ key: testkube-tw-toolkit-go-${{ hashFiles('**/go.sum') }}
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Release
+ uses: goreleaser/goreleaser-action@v4
+ with:
+ distribution: goreleaser
+ version: latest
+ args: release -f goreleaser_files/.goreleaser-docker-build-testworkflow-toolkit.yml
+ env:
+ GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
+ ANALYTICS_TRACKING_ID: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_ID}}
+ ANALYTICS_API_KEY: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_SECRET}}
+ SLACK_BOT_CLIENT_ID: ${{secrets.TESTKUBE_SLACK_BOT_CLIENT_ID}}
+ SLACK_BOT_CLIENT_SECRET: ${{secrets.TESTKUBE_SLACK_BOT_CLIENT_SECRET}}
+ SEGMENTIO_KEY: ${{secrets.TESTKUBE_API_SEGMENTIO_KEY}}
+ CLOUD_SEGMENTIO_KEY: ${{secrets.TESTKUBE_API_CLOUD_SEGMENTIO_KEY}}
+ DOCKER_BUILDX_BUILDER: "${{ steps.buildx.outputs.name }}"
+ DOCKER_BUILDX_CACHE_FROM: "type=gha"
+ DOCKER_BUILDX_CACHE_TO: "type=gha,mode=max"
+ ALPINE_IMAGE: ${{ env.ALPINE_IMAGE }}
+
single_executor:
strategy:
matrix:
diff --git a/.github/workflows/docker-build-develop.yaml b/.github/workflows/docker-build-develop.yaml
index 99db6d7fb9c..8e667c8968e 100644
--- a/.github/workflows/docker-build-develop.yaml
+++ b/.github/workflows/docker-build-develop.yaml
@@ -67,6 +67,120 @@ jobs:
run: |
docker push kubeshop/testkube-api-server:${{ steps.commit.outputs.short }}
+ testworkflow-init:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@v1
+
+ - name: Set-up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ cache: false
+
+ - name: Go Cache
+ uses: actions/cache@v2
+ with:
+ path: |
+ ~/go/pkg/mod
+ ~/.cache/go-build
+ key: testkube-tw-init-go-${{ hashFiles('**/go.sum') }}
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - id: commit
+ uses: prompt/actions-commit-hash@v3
+
+ - name: Release
+ uses: goreleaser/goreleaser-action@v4
+ with:
+ distribution: goreleaser
+ version: latest
+ args: release -f goreleaser_files/.goreleaser-docker-build-testworkflow-init.yml --snapshot
+ env:
+ GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
+ ANALYTICS_TRACKING_ID: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_ID}}
+ ANALYTICS_API_KEY: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_SECRET}}
+ SLACK_BOT_CLIENT_ID: ${{secrets.TESTKUBE_SLACK_BOT_CLIENT_ID}}
+ SLACK_BOT_CLIENT_SECRET: ${{secrets.TESTKUBE_SLACK_BOT_CLIENT_SECRET}}
+ SEGMENTIO_KEY: ${{secrets.TESTKUBE_API_SEGMENTIO_KEY}}
+ CLOUD_SEGMENTIO_KEY: ${{secrets.TESTKUBE_API_CLOUD_SEGMENTIO_KEY}}
+ DOCKER_BUILDX_BUILDER: "${{ steps.buildx.outputs.name }}"
+ DOCKER_BUILDX_CACHE_FROM: "type=gha"
+ DOCKER_BUILDX_CACHE_TO: "type=gha,mode=max"
+ ALPINE_IMAGE: ${{ env.ALPINE_IMAGE }}
+ IMAGE_TAG_SHA: true
+
+ - name: Push Docker images
+ run: |
+ docker push kubeshop/testkube-tw-init:${{ steps.commit.outputs.short }}
+
+ testworkflow-toolkit:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@v1
+
+ - name: Set-up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ cache: false
+
+ - name: Go Cache
+ uses: actions/cache@v2
+ with:
+ path: |
+ ~/go/pkg/mod
+ ~/.cache/go-build
+ key: testkube-tw-toolkit-go-${{ hashFiles('**/go.sum') }}
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - id: commit
+ uses: prompt/actions-commit-hash@v3
+
+ - name: Release
+ uses: goreleaser/goreleaser-action@v4
+ with:
+ distribution: goreleaser
+ version: latest
+ args: release -f goreleaser_files/.goreleaser-docker-build-testworkflow-toolkit.yml --snapshot
+ env:
+ GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
+ ANALYTICS_TRACKING_ID: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_ID}}
+ ANALYTICS_API_KEY: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_SECRET}}
+ SLACK_BOT_CLIENT_ID: ${{secrets.TESTKUBE_SLACK_BOT_CLIENT_ID}}
+ SLACK_BOT_CLIENT_SECRET: ${{secrets.TESTKUBE_SLACK_BOT_CLIENT_SECRET}}
+ SEGMENTIO_KEY: ${{secrets.TESTKUBE_API_SEGMENTIO_KEY}}
+ CLOUD_SEGMENTIO_KEY: ${{secrets.TESTKUBE_API_CLOUD_SEGMENTIO_KEY}}
+ DOCKER_BUILDX_BUILDER: "${{ steps.buildx.outputs.name }}"
+ DOCKER_BUILDX_CACHE_FROM: "type=gha"
+ DOCKER_BUILDX_CACHE_TO: "type=gha,mode=max"
+ ALPINE_IMAGE: ${{ env.ALPINE_IMAGE }}
+ IMAGE_TAG_SHA: true
+
+ - name: Push Docker images
+ run: |
+ docker push kubeshop/testkube-tw-toolkit:${{ steps.commit.outputs.short }}
+
single_executor:
strategy:
matrix:
diff --git a/.github/workflows/docker-build-release.yaml b/.github/workflows/docker-build-release.yaml
index efbd76e5ad1..b5a9b6bf2d1 100644
--- a/.github/workflows/docker-build-release.yaml
+++ b/.github/workflows/docker-build-release.yaml
@@ -68,6 +68,120 @@ jobs:
run: |
docker push kubeshop/testkube-api-server:${{ steps.commit.outputs.short }}
+ testworkflow-init:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@v1
+
+ - name: Set-up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ cache: false
+
+ - name: Go Cache
+ uses: actions/cache@v2
+ with:
+ path: |
+ ~/go/pkg/mod
+ ~/.cache/go-build
+ key: testkube-tw-init-go-${{ hashFiles('**/go.sum') }}
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - id: commit
+ uses: prompt/actions-commit-hash@v3
+
+ - name: Release
+ uses: goreleaser/goreleaser-action@v4
+ with:
+ distribution: goreleaser
+ version: latest
+ args: release -f goreleaser_files/.goreleaser-docker-build-testworkflow-init.yml --snapshot
+ env:
+ GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
+ ANALYTICS_TRACKING_ID: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_ID}}
+ ANALYTICS_API_KEY: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_SECRET}}
+ SLACK_BOT_CLIENT_ID: ${{secrets.TESTKUBE_SLACK_BOT_CLIENT_ID}}
+ SLACK_BOT_CLIENT_SECRET: ${{secrets.TESTKUBE_SLACK_BOT_CLIENT_SECRET}}
+ SEGMENTIO_KEY: ${{secrets.TESTKUBE_API_SEGMENTIO_KEY}}
+ CLOUD_SEGMENTIO_KEY: ${{secrets.TESTKUBE_API_CLOUD_SEGMENTIO_KEY}}
+ DOCKER_BUILDX_BUILDER: "${{ steps.buildx.outputs.name }}"
+ DOCKER_BUILDX_CACHE_FROM: "type=gha"
+ DOCKER_BUILDX_CACHE_TO: "type=gha,mode=max"
+ ALPINE_IMAGE: ${{ env.ALPINE_IMAGE }}
+ IMAGE_TAG_SHA: true
+
+ - name: Push Docker images
+ run: |
+ docker push kubeshop/testkube-tw-init:${{ steps.commit.outputs.short }}
+
+ testworkflow-toolkit:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@v1
+
+ - name: Set-up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ cache: false
+
+ - name: Go Cache
+ uses: actions/cache@v2
+ with:
+ path: |
+ ~/go/pkg/mod
+ ~/.cache/go-build
+ key: testkube-tw-toolkit-go-${{ hashFiles('**/go.sum') }}
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - id: commit
+ uses: prompt/actions-commit-hash@v3
+
+ - name: Release
+ uses: goreleaser/goreleaser-action@v4
+ with:
+ distribution: goreleaser
+ version: latest
+ args: release -f goreleaser_files/.goreleaser-docker-build-testworkflow-toolkit.yml --snapshot
+ env:
+ GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
+ ANALYTICS_TRACKING_ID: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_ID}}
+ ANALYTICS_API_KEY: ${{secrets.TESTKUBE_API_GA_MEASUREMENT_SECRET}}
+ SLACK_BOT_CLIENT_ID: ${{secrets.TESTKUBE_SLACK_BOT_CLIENT_ID}}
+ SLACK_BOT_CLIENT_SECRET: ${{secrets.TESTKUBE_SLACK_BOT_CLIENT_SECRET}}
+ SEGMENTIO_KEY: ${{secrets.TESTKUBE_API_SEGMENTIO_KEY}}
+ CLOUD_SEGMENTIO_KEY: ${{secrets.TESTKUBE_API_CLOUD_SEGMENTIO_KEY}}
+ DOCKER_BUILDX_BUILDER: "${{ steps.buildx.outputs.name }}"
+ DOCKER_BUILDX_CACHE_FROM: "type=gha"
+ DOCKER_BUILDX_CACHE_TO: "type=gha,mode=max"
+ ALPINE_IMAGE: ${{ env.ALPINE_IMAGE }}
+ IMAGE_TAG_SHA: true
+
+ - name: Push Docker images
+ run: |
+ docker push kubeshop/testkube-tw-toolkit:${{ steps.commit.outputs.short }}
+
single_executor:
strategy:
matrix:
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 1737d7b9deb..3ebd9165abb 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -47,7 +47,7 @@ jobs:
${{ runner.os }}-go-
- name: Lint using golangci-lint
- uses: golangci/golangci-lint-action@v3
+ uses: golangci/golangci-lint-action@v4
with:
version: latest
args: --timeout=5m
diff --git a/.github/workflows/release-dev-log-server.yaml b/.github/workflows/release-dev-log-server.yaml
index 8a7d3d2f794..8abeab84322 100644
--- a/.github/workflows/release-dev-log-server.yaml
+++ b/.github/workflows/release-dev-log-server.yaml
@@ -2,8 +2,8 @@ name: Release logs server dev
on:
push:
- tags:
- - "v[0-9]+.[0-9]+.[0-9]+-*"
+ branches:
+ - develop
permissions:
id-token: write
@@ -62,7 +62,6 @@ jobs:
args: release -f ./goreleaser_files/.goreleaser-docker-build-logs-server.yml --snapshot
env:
GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
- # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
DOCKER_BUILDX_BUILDER: "${{ steps.buildx.outputs.name }}"
DOCKER_BUILDX_CACHE_FROM: "type=gha"
@@ -75,6 +74,9 @@ jobs:
docker push kubeshop/testkube-logs-server:${{ steps.github_sha.outputs.sha_short }}-arm64v8
docker push kubeshop/testkube-logs-server:${{ steps.github_sha.outputs.sha_short }}-amd64
+ docker manifest create kubeshop/testkube-logs-server:${{ steps.github_sha.outputs.sha_short }} --amend kubeshop/testkube-logs-server:${{ steps.github_sha.outputs.sha_short }}-arm64v8 --amend kubeshop/testkube-logs-server:${{ steps.github_sha.outputs.sha_short }}-amd64
+ docker manifest push -p kubeshop/testkube-logs-server:${{ steps.github_sha.outputs.sha_short }}
+
- name: Push README to Dockerhub
uses: christian-korneck/update-container-description-action@v1
env:
diff --git a/.github/workflows/release-dev-log-sidecar.yaml b/.github/workflows/release-dev-log-sidecar.yaml
index 9b0c186dd2e..489dfc8c24e 100644
--- a/.github/workflows/release-dev-log-sidecar.yaml
+++ b/.github/workflows/release-dev-log-sidecar.yaml
@@ -2,8 +2,8 @@ name: Release logs sidecar dev
on:
push:
- tags:
- - "v[0-9]+.[0-9]+.[0-9]+-*"
+ branches:
+ - develop
permissions:
id-token: write
@@ -62,7 +62,6 @@ jobs:
args: release -f ./goreleaser_files/.goreleaser-docker-build-logs-sidecar.yml --snapshot
env:
GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
- # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
DOCKER_BUILDX_BUILDER: "${{ steps.buildx.outputs.name }}"
DOCKER_BUILDX_CACHE_FROM: "type=gha"
@@ -75,6 +74,9 @@ jobs:
docker push kubeshop/testkube-logs-sidecar:${{ steps.github_sha.outputs.sha_short }}-arm64v8
docker push kubeshop/testkube-logs-sidecar:${{ steps.github_sha.outputs.sha_short }}-amd64
+ docker manifest create kubeshop/testkube-logs-sidecar:${{ steps.github_sha.outputs.sha_short }} --amend kubeshop/testkube-logs-sidecar:${{ steps.github_sha.outputs.sha_short }}-arm64v8 --amend kubeshop/testkube-logs-sidecar:${{ steps.github_sha.outputs.sha_short }}-amd64
+ docker manifest push -p kubeshop/testkube-logs-sidecar:${{ steps.github_sha.outputs.sha_short }}
+
- name: Push README to Dockerhub
uses: christian-korneck/update-container-description-action@v1
env:
diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml
index 656fec54f57..c2311de10d3 100644
--- a/.github/workflows/release-dev.yaml
+++ b/.github/workflows/release-dev.yaml
@@ -62,9 +62,11 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- - name: Get github sha
- id: github_sha
- run: echo "::set-output name=sha_short::${GITHUB_SHA::7}"
+ - name: Get tag
+ id: tag
+ uses: dawidd6/action-get-tag@v1
+ with:
+ strip_v: true
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v4
@@ -82,13 +84,20 @@ jobs:
DOCKER_BUILDX_CACHE_FROM: "type=gha"
DOCKER_BUILDX_CACHE_TO: "type=gha,mode=max"
ALPINE_IMAGE: ${{ env.ALPINE_IMAGE }}
- DOCKER_IMAGE_TAG: ${{ steps.github_sha.outputs.sha_short }}
+ DOCKER_IMAGE_TAG: ${{steps.tag.outputs.tag}}
- name: Push Docker images
if: matrix.name == 'linux'
run: |
- docker push kubeshop/testkube-cli:${{ steps.github_sha.outputs.sha_short }}-arm64v8
- docker push kubeshop/testkube-cli:${{ steps.github_sha.outputs.sha_short }}-amd64
+ docker push kubeshop/testkube-cli:${{steps.tag.outputs.tag}}-arm64v8
+ docker push kubeshop/testkube-cli:${{steps.tag.outputs.tag}}-amd64
+
+ # adding the docker manifest for the latest image tag
+ docker manifest create kubeshop/testkube-cli:latest --amend kubeshop/testkube-cli:${{steps.tag.outputs.tag}}-amd64 --amend kubeshop/testkube-cli:${{steps.tag.outputs.tag}}-arm64v8
+ docker manifest push -p kubeshop/testkube-cli:latest
+
+ docker manifest create kubeshop/testkube-cli:${{steps.tag.outputs.tag}} --amend kubeshop/testkube-cli:${{steps.tag.outputs.tag}}-amd64 --amend kubeshop/testkube-cli:${{steps.tag.outputs.tag}}-arm64v8
+ docker manifest push -p kubeshop/testkube-cli:${{steps.tag.outputs.tag}}
- name: Push README to Dockerhub
if: matrix.name == 'linux'
diff --git a/.github/workflows/release-log-server.yaml b/.github/workflows/release-log-server.yaml
index 4688b1e589d..53ad4ff6801 100644
--- a/.github/workflows/release-log-server.yaml
+++ b/.github/workflows/release-log-server.yaml
@@ -29,6 +29,9 @@ jobs:
id: buildx
uses: docker/setup-buildx-action@v1
+ - uses: sigstore/cosign-installer@v3.0.5
+ - uses: anchore/sbom-action/download-syft@v0.14.2
+
- name: Set up Go
uses: actions/setup-go@v2
with:
@@ -59,28 +62,14 @@ jobs:
with:
distribution: goreleaser-pro
version: latest
- args: release -f ./goreleaser_files/.goreleaser-docker-build-logs-server.yml --skip-publish
+ args: release -f ./goreleaser_files/.goreleaser-docker-build-logs-server.yml
env:
GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
- # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
DOCKER_BUILDX_BUILDER: "${{ steps.buildx.outputs.name }}"
DOCKER_BUILDX_CACHE_FROM: "type=gha"
DOCKER_BUILDX_CACHE_TO: "type=gha,mode=max"
ALPINE_IMAGE: ${{ env.ALPINE_IMAGE }}
- DOCKER_IMAGE_TAG: ${{ steps.github_sha.outputs.sha_short }}
-
- - name: Push Docker images
- run: |
- docker push kubeshop/testkube-logs-server:${{ steps.github_sha.outputs.sha_short }}-arm64v8
- docker push kubeshop/testkube-logs-server:${{ steps.github_sha.outputs.sha_short }}-amd64
- # adding the docker manifest for the latest image tag
- docker manifest create kubeshop/testkube-logs-server:latest \
- kubeshop/testkube-logs-server:${{ steps.github_sha.outputs.sha_short }}-amd64 \
- kubeshop/testkube-logs-server:${{ steps.github_sha.outputs.sha_short }}-arm64v8
- docker manifest annotate kubeshop/testkube-logs-server:latest kubeshop/testkube-logs-server:${{ steps.github_sha.outputs.sha_short }}-amd64 --arch amd64
- docker manifest annotate kubeshop/testkube-logs-server:latest kubeshop/testkube-logs-server:${{ steps.github_sha.outputs.sha_short }}-arm64v8 --arch arm64 --variant v8
- docker manifest push kubeshop/testkube-logs-server:latest
- name: Push README to Dockerhub
uses: christian-korneck/update-container-description-action@v1
diff --git a/.github/workflows/release-log-sidecar.yaml b/.github/workflows/release-log-sidecar.yaml
index c2f36907536..bc35a3a288c 100644
--- a/.github/workflows/release-log-sidecar.yaml
+++ b/.github/workflows/release-log-sidecar.yaml
@@ -29,6 +29,9 @@ jobs:
id: buildx
uses: docker/setup-buildx-action@v1
+ - uses: sigstore/cosign-installer@v3.0.5
+ - uses: anchore/sbom-action/download-syft@v0.14.2
+
- name: Set up Go
uses: actions/setup-go@v2
with:
@@ -59,28 +62,14 @@ jobs:
with:
distribution: goreleaser-pro
version: latest
- args: release -f ./goreleaser_files/.goreleaser-docker-build-logs-sidecar.yml --skip-publish
+ args: release -f ./goreleaser_files/.goreleaser-docker-build-logs-sidecar.yml
env:
GITHUB_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
- # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
DOCKER_BUILDX_BUILDER: "${{ steps.buildx.outputs.name }}"
DOCKER_BUILDX_CACHE_FROM: "type=gha"
DOCKER_BUILDX_CACHE_TO: "type=gha,mode=max"
ALPINE_IMAGE: ${{ env.ALPINE_IMAGE }}
- DOCKER_IMAGE_TAG: ${{ steps.github_sha.outputs.sha_short }}
-
- - name: Push Docker images
- run: |
- docker push kubeshop/testkube-logs-sidecar:${{ steps.github_sha.outputs.sha_short }}-arm64v8
- docker push kubeshop/testkube-logs-sidecar:${{ steps.github_sha.outputs.sha_short }}-amd64
- # adding the docker manifest for the latest image tag
- docker manifest create kubeshop/testkube-logs-sidecar:latest \
- kubeshop/testkube-logs-sidecar:${{ steps.github_sha.outputs.sha_short }}-amd64 \
- kubeshop/testkube-logs-sidecar:${{ steps.github_sha.outputs.sha_short }}-arm64v8
- docker manifest annotate kubeshop/testkube-logs-sidecar:latest kubeshop/testkube-logs-sidecar:${{ steps.github_sha.outputs.sha_short }}-amd64 --arch amd64
- docker manifest annotate kubeshop/testkube-logs-sidecar:latest kubeshop/testkube-logs-sidecar:${{ steps.github_sha.outputs.sha_short }}-arm64v8 --arch arm64 --variant v8
- docker manifest push kubeshop/testkube-logs-sidecar:latest
- name: Push README to Dockerhub
uses: christian-korneck/update-container-description-action@v1
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index f0e0cd5d7d5..43e4cb75938 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -63,12 +63,14 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- - name: Get github sha
- id: github_sha
- run: echo "::set-output name=sha_short::${GITHUB_SHA::7}"
+ - name: Get tag
+ id: tag
+ uses: dawidd6/action-get-tag@v1
+ with:
+ strip_v: true
- name: Run GoReleaser
- uses: goreleaser/goreleaser-action@v2
+ uses: goreleaser/goreleaser-action@v4
with:
distribution: goreleaser-pro
version: latest
@@ -83,20 +85,20 @@ jobs:
DOCKER_BUILDX_CACHE_FROM: "type=gha"
DOCKER_BUILDX_CACHE_TO: "type=gha,mode=max"
ALPINE_IMAGE: ${{ env.ALPINE_IMAGE }}
- DOCKER_IMAGE_TAG: ${{ steps.github_sha.outputs.sha_short }}
+ DOCKER_IMAGE_TAG: ${{steps.tag.outputs.tag}}
- name: Push Docker images
if: matrix.name == 'linux'
run: |
- docker push kubeshop/testkube-cli:${{ steps.github_sha.outputs.sha_short }}-arm64v8
- docker push kubeshop/testkube-cli:${{ steps.github_sha.outputs.sha_short }}-amd64
+ docker push kubeshop/testkube-cli:${{steps.tag.outputs.tag}}-arm64v8
+ docker push kubeshop/testkube-cli:${{steps.tag.outputs.tag}}-amd64
+
# adding the docker manifest for the latest image tag
- docker manifest create kubeshop/testkube-cli:latest \
- kubeshop/testkube-cli:${{ steps.github_sha.outputs.sha_short }}-amd64 \
- kubeshop/testkube-cli:${{ steps.github_sha.outputs.sha_short }}-arm64v8
- docker manifest annotate kubeshop/testkube-cli:latest kubeshop/testkube-cli:${{ steps.github_sha.outputs.sha_short }}-amd64 --arch amd64
- docker manifest annotate kubeshop/testkube-cli:latest kubeshop/testkube-cli:${{ steps.github_sha.outputs.sha_short }}-arm64v8 --arch arm64 --variant v8
- docker manifest push kubeshop/testkube-cli:latest
+ docker manifest create kubeshop/testkube-cli:latest --amend kubeshop/testkube-cli:${{steps.tag.outputs.tag}}-amd64 --amend kubeshop/testkube-cli:${{steps.tag.outputs.tag}}-arm64v8
+ docker manifest push -p kubeshop/testkube-cli:latest
+
+ docker manifest create kubeshop/testkube-cli:${{steps.tag.outputs.tag}} --amend kubeshop/testkube-cli:${{steps.tag.outputs.tag}}-amd64 --amend kubeshop/testkube-cli:${{steps.tag.outputs.tag}}-arm64v8
+ docker manifest push -p kubeshop/testkube-cli:${{steps.tag.outputs.tag}}
- name: Upload Artifacts
uses: actions/upload-artifact@master
diff --git a/DESIGN.md b/DESIGN.md
index 8bc838e663c..cc5e6a5a1ad 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -19,24 +19,24 @@ Testkube consists of 3 different parts.
## 🚢 How to contribute design
-1. Check out open [issues](https://github.com/kubeshop/testkube/issues) here on GitHub (we tend to label them with `🚨 needs-ux`)
+1. Check out open [issues](https://github.com/kubeshop/testkube/issues) here on GitHub (we tend to label them with `🚨 needs-ux`).
2. Feel free to open an issue on your own if you find something you would like to contribute to the project and use the `idea 💡` label for it.
-3. Clone the public Figma files or create new ones and share them publicly
-4. Add your contributions to an issue and we promise we will review your contribution carefully and foster discussions around it
+3. Clone the public Figma files or create new ones and share them publicly.
+4. Add your contributions to an issue and we promise we will review your contribution carefully and foster discussions around it.
**We encourage you to:**
-- Get in touch with the team by starting a discussion on [GitHub](https://github.com/kubeshop/testkube/issues) or on our [Discord Server](https://discord.gg/hfq44wtR6Q).
+- Get in touch with the team by starting a discussion on [GitHub](https://github.com/kubeshop/testkube/issues) or on our [Slack Channel](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email).
- Check out our [Contributor Guide](https://github.com/kubeshop/testkube/blob/main/CONTRIBUTING.md) and
- [Code of Conduct](https://github.com/kubeshop/testkube/blob/main/CODE_OF_CONDUCT.md)
+ [Code of Conduct](https://github.com/kubeshop/testkube/blob/main/CODE_OF_CONDUCT.md).
-## 🎭 Target audience
+## 🎭 Target Audience
Since we are creating a product for Testers and Developers our target audience is pretty straight forward. Sometimes wo do also like to include DevOps people into our considerations.
-## 💅 Design relevant materials
+## 💅 Design Relevant Materials
-We currently aim to to build a more comprehensive Design System which will also include some guidance on Component usage, Wording, and patterns.
+We currently aim to to build a more comprehensive Design System which will also include some guidance on Component usage, Wording, and Patterns.
For now – here is a list of design relevant information and materials:
@@ -56,7 +56,6 @@ https://www.figma.com/file/59vZTaJ6O2wTk0Qyqh2IJJ/Testkube-CLI?t=CBjcXzIKoEcG2AG
## 🎓 License
-All design work is licensed under the
-[MIT](https://mit-license.org/)
+All design work is licensed under [MIT](https://mit-license.org/).
[(Back to top)](#-table-of-contents)
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 8761fc91e87..d37673fb9db 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,8 @@
-MIT License
-
-Copyright (c) 2022 Kubeshop
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+Source code in this repository is variously licensed under the Testkube
+Community License (TCL) and the MIT license.
+
+Source code in a given file is licensed under the applicable license
+for that source code. Source code is licensed under the MIT license
+unless otherwise indicated in the header referenced at the beginning
+of the file or specified by a LICENSE file in the same containing
+folder as the file.
diff --git a/Makefile b/Makefile
index 7d43529ebbe..492edf02f94 100644
--- a/Makefile
+++ b/Makefile
@@ -39,6 +39,10 @@ refresh-config:
wget "https://raw.githubusercontent.com/kubeshop/helm-charts/develop/charts/testkube-api/slack-config.json" -O config/slack-config.json &
wget "https://raw.githubusercontent.com/kubeshop/helm-charts/develop/charts/testkube-api/slack-template.json" -O config/slack-template.json
+
+generate-protobuf: use-env-file
+ protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pkg/logs/pb/logs.proto
+
just-run-api: use-env-file
TESTKUBE_DASHBOARD_URI=$(DASHBOARD_URI) APISERVER_CONFIG=testkube-api-server-config-testkube TESTKUBE_ANALYTICS_ENABLED=$(TESTKUBE_ANALYTICS_ENABLED) TESTKUBE_NAMESPACE=$(NAMESPACE) SCRAPPERENABLED=true STORAGE_SSL=true DEBUG=$(DEBUG) APISERVER_PORT=8088 go run -ldflags='$(LD_FLAGS)' cmd/api-server/main.go
@@ -113,6 +117,7 @@ openapi-generate-model-testkube:
rm -rf tmp
find ./pkg/api/v1/testkube -type f -exec sed -i '' -e "s/package swagger/package testkube/g" {} \;
find ./pkg/api/v1/testkube -type f -exec sed -i '' -e "s/\*map\[string\]/map[string]/g" {} \;
+ find ./pkg/api/v1/testkube -name "*.go" -type f -exec sed -i '' -e "s/ map\[string\]Object / map\[string\]interface\{\} /g" {} \; # support map with empty additional properties
find ./pkg/api/v1/testkube -name "*update*.go" -type f -exec sed -i '' -e "s/ map/ \*map/g" {} \;
find ./pkg/api/v1/testkube -name "*update*.go" -type f -exec sed -i '' -e "s/ string/ \*string/g" {} \;
find ./pkg/api/v1/testkube -name "*update*.go" -type f -exec sed -i '' -e "s/ \[\]/ \*\[\]/g" {} \;
diff --git a/README.md b/README.md
index 5c83dc888cc..ade5b2914df 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
Website |
Documentation |
Twitter |
- Discord |
+ Slack |
Blog
@@ -84,7 +84,6 @@ Main Testkube components are:
- [Ginkgo](https://docs.testkube.io/test-types/executor-ginkgo/) - Runs tests written in Go using Ginkgo ([@jdborneman-terminus](https://github.com/jdborneman-terminus))
- [Executor Template](https://github.com/kubeshop/testkube-executor-template) - for creating your own executors
- Results DB - for centralized test results aggregation and analysis
-- [Testkube Dashboard](https://github.com/kubeshop/testkube-dashboard) - standalone web application for viewing real-time Testkube test results
## Getting Started
@@ -112,4 +111,4 @@ Go to [contribution document](CONTRIBUTING.md) to read more how can you help us
# Feedback
Whether it helps you or not - we'd LOVE to hear from you. Please let us know what you think and of course, how we can make it better.
-Please join our growing community on [Discord](https://discord.com/invite/6zupCZFQbe)
+Please join our growing community on [Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)
diff --git a/api/v1/testkube.yaml b/api/v1/testkube.yaml
index ba7b5601660..37fc6c51ebc 100644
--- a/api/v1/testkube.yaml
+++ b/api/v1/testkube.yaml
@@ -1132,7 +1132,7 @@ paths:
- $ref: "#/components/parameters/Namespace"
- $ref: "#/components/parameters/Selector"
- $ref: "#/components/parameters/ExecutionSelector"
- - $ref: "#/components/parameters/ConcurrencyLevel"
+ - $ref: "#/components/parameters/ConcurrencyLevel"
tags:
- api
- tests
@@ -1337,6 +1337,35 @@ paths:
items:
$ref: "#/components/schemas/Problem"
+ /executions/{id}/logs/v2:
+ get:
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ tags:
+ - logs
+ - executions
+ - api
+ summary: "Get execution's logs by ID version 2"
+ description: "Returns logs of the given executionID version 2"
+ operationId: getExecutionLogsV2
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/LogV2"
+ 500:
+ description: "problem with getting execution's logs version 2"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+
/executions/{id}/artifacts/{filename}:
get:
parameters:
@@ -2230,6 +2259,51 @@ paths:
items:
$ref: "#/components/schemas/Problem"
+ /executor-by-types:
+ get:
+ parameters:
+ - $ref: "#/components/parameters/TestType"
+ tags:
+ - api
+ - executor
+ summary: "Get executor details by type"
+ description: "Returns executors data with executions passed to executor"
+ operationId: getExecutorByType
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ExecutorDetails"
+ text/yaml:
+ schema:
+ type: string
+ 400:
+ description: "problem with input for CRD generation"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: "problem with communicating with kubernetes cluster"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 500:
+ description: "problem with getting executor data"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+
/labels:
get:
tags:
@@ -2318,7 +2392,7 @@ paths:
$ref: "#/components/schemas/WebhookCreateRequest"
text/yaml:
schema:
- type: string
+ type: string
responses:
200:
description: "successful operation"
@@ -2468,7 +2542,7 @@ paths:
$ref: "#/components/schemas/WebhookUpdateRequest"
text/yaml:
schema:
- type: string
+ type: string
responses:
200:
description: "successful operation"
@@ -2555,7 +2629,7 @@ paths:
$ref: "#/components/schemas/TemplateCreateRequest"
text/yaml:
schema:
- type: string
+ type: string
responses:
200:
description: "successful operation"
@@ -2705,7 +2779,7 @@ paths:
$ref: "#/components/schemas/TemplateUpdateRequest"
text/yaml:
schema:
- type: string
+ type: string
responses:
200:
description: "successful operation"
@@ -3215,572 +3289,2771 @@ paths:
items:
$ref: "#/components/schemas/Problem"
-components:
- schemas:
- ExecutionsMetrics:
- type: object
- properties:
- passFailRatio:
- type: number
- description: Percentage pass to fail ratio
- example: 50
- executionDurationP50:
- type: string
- description: 50th percentile of all durations
- example: "7m2.71s"
- executionDurationP50ms:
- type: integer
- description: 50th percentile of all durations in milliseconds
- example: 422
- executionDurationP90:
- type: string
- description: 90th percentile of all durations
- example: "7m2.71s"
- executionDurationP90ms:
- type: integer
- description: 90th percentile of all durations in milliseconds
- example: 422
- executionDurationP95:
- type: string
- description: 95th percentile of all durations
- example: "7m2.71s"
- executionDurationP95ms:
- type: integer
- description: 95th percentile of all durations in milliseconds
- example: 422
- executionDurationP99:
- type: string
- description: 99th percentile of all durations
- example: "7m2.71s"
- executionDurationP99ms:
- type: integer
- description: 99th percentile of all durations in milliseconds
- example: 422
- totalExecutions:
- type: integer
- description: total executions number
- example: 2
- failedExecutions:
- type: integer
- description: failed executions number
- example: 1
- executions:
- type: array
- description: List of test/testsuite executions
- items:
- $ref: "#/components/schemas/ExecutionsMetricsExecutions"
-
- ExecutionsMetricsExecutions:
- type: object
- properties:
- executionId:
- type: string
- duration:
- type: string
- durationMs:
- type: integer
- status:
- type: string
- name:
- type: string
- startTime:
- type: string
- format: date-time
-
- Variables:
- type: object
- description: "execution variables passed to executor converted to vars for usage in tests"
- additionalProperties:
- $ref: "#/components/schemas/Variable"
- example:
- var1:
- name: "var1"
- type: "basic"
- value: "value1"
- secret1:
- name: "secret1"
- type: "secret"
- value: "secretvalue1"
-
- Variable:
- type: object
- properties:
- name:
- type: string
- value:
- type: string
- type:
- $ref: "#/components/schemas/VariableType"
- secretRef:
- $ref: "#/components/schemas/SecretRef"
- configMapRef:
- $ref: "#/components/schemas/ConfigMapRef"
-
- VariableType:
- type: string
- enum:
- - basic
- - secret
-
- ObjectRef:
- required:
- - name
- type: object
- properties:
- namespace:
- type: string
- description: object kubernetes namespace
- example: "testkube"
- name:
- type: string
- description: object name
- example: "name"
-
- SecretRef:
- required:
- - name
- - key
- type: object
- description: Testkube internal reference for secret storage in Kubernetes secrets
- properties:
- namespace:
- type: string
- description: object kubernetes namespace
- name:
- type: string
- description: object name
- key:
- type: string
- description: object key
-
- ConfigMapRef:
- required:
- - name
- - key
- type: object
- description: Testkube internal reference for data in Kubernetes config maps
- properties:
- namespace:
- type: string
- description: object kubernetes namespace
- name:
- type: string
- description: object name
- key:
- type: string
- description: object key
-
- TestSuite:
- type: object
- required:
- - name
- - status
- properties:
- name:
- type: string
- example: "test-suite1"
- namespace:
- type: string
- example: "testkube"
- description:
- type: string
- example: "collection of tests"
- before:
- type: array
- items:
- $ref: "#/components/schemas/TestSuiteBatchStep"
- description: Run these batch steps before whole suite
- example:
- - stopOnFailure: true
- execute:
- - test: "example-test"
- steps:
- type: array
- items:
- $ref: "#/components/schemas/TestSuiteBatchStep"
- description: Batch steps to run
- example:
- - stopOnFailure: true
- execute:
+ /test-workflows:
+ get:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/Selector"
+ summary: List test workflows
+ description: List test workflows from the kubernetes cluster
+ operationId: listTestWorkflows
+ responses:
+ 200:
+ description: successful list operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflow"
+ text/yaml:
+ schema:
+ type: string
+ 400:
+ description: "problem with selector parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ delete:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/Selector"
+ summary: Delete test workflows
+ description: Delete test workflows from the kubernetes cluster
+ operationId: deleteTestWorkflows
+ responses:
+ 204:
+ description: no content
+ 400:
+ description: "problem with selector parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ post:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ summary: Create test workflow
+ description: Create test workflow in the kubernetes cluster
+ operationId: createTestWorkflow
+ requestBody:
+ description: test workflow body
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TestWorkflow"
+ text/yaml:
+ schema:
+ type: string
+ responses:
+ 200:
+ description: successful creation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflow"
+ text/yaml:
+ schema:
+ type: string
+ 400:
+ description: "problem with body parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ /test-workflow-with-executions:
+ get:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/Selector"
+ summary: List test workflows with latest execution
+ description: List test workflows from the kubernetes cluster with latest execution
+ operationId: listTestWorkflowWithExecutions
+ responses:
+ 200:
+ description: successful list operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflowWithExecutionSummary"
+ text/yaml:
+ schema:
+ type: string
+ 400:
+ description: "problem with selector parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ /test-workflow-with-executions/{id}:
+ get:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ summary: Get test workflow details with latest execution
+ description: Get test workflow details from the kubernetes cluster with latest execution
+ operationId: getTestWorkflowWithExecution
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TestWorkflowWithExecution"
+ text/yaml:
+ schema:
+ type: string
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 404:
+ description: "the resource has not been found"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ /test-workflows/{id}/executions:
+ get:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ summary: List test workflow executions
+ description: List test workflow executions
+ operationId: listTestWorkflowExecutionsByTestWorkflow
+ responses:
+ 200:
+ description: successful list operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflowExecutionsResult"
+ text/yaml:
+ schema:
+ type: string
+ 400:
+ description: "problem with selector parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ post:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ summary: Execute test workflow
+ description: Execute test workflow in the kubernetes cluster
+ operationId: executeTestWorkflow
+ requestBody:
+ description: test workflow execution request
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TestWorkflowExecutionRequest"
+ responses:
+ 200:
+ description: successful execution
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflowExecution"
+ 400:
+ description: "problem with body parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ /test-workflows/{id}/metrics:
+ get:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ summary: Get test workflow metrics
+ description: Get metrics of test workflow executions
+ operationId: getTestWorkflowMetrics
+ responses:
+ 200:
+ description: successful list operation
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ExecutionsMetrics"
+ text/yaml:
+ schema:
+ type: string
+ 400:
+ description: "problem with selector parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ /test-workflows/{id}/executions/{executionID}:
+ get:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ - $ref: "#/components/parameters/executionID"
+ summary: Get test workflow execution
+ description: Get test workflow execution details
+ operationId: getTestWorkflowExecutionByTestWorkflow
+ responses:
+ 200:
+ description: successful list operation
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TestWorkflowExecution"
+ text/yaml:
+ schema:
+ type: string
+ 400:
+ description: "problem with selector parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ /test-workflows/{id}/executions/{executionID}/abort:
+ post:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ - $ref: "#/components/parameters/executionID"
+ summary: Abort test workflow execution
+ description: Abort test workflow execution
+ operationId: abortTestWorkflowExecutionByTestWorkflow
+ responses:
+ 204:
+ description: "no content"
+ 400:
+ description: "problem with selector parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ /test-workflow-executions:
+ get:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ summary: List test workflow executions
+ description: List test workflow executions
+ operationId: listTestWorkflowExecutions
+ responses:
+ 200:
+ description: successful list operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflowExecutionsResult"
+ text/yaml:
+ schema:
+ type: string
+ 400:
+ description: "problem with selector parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ /test-workflow-executions/{executionID}:
+ get:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/executionID"
+ summary: Get test workflow execution
+ description: Get test workflow execution details
+ operationId: getTestWorkflowExecution
+ responses:
+ 200:
+ description: successful list operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflowExecution"
+ text/yaml:
+ schema:
+ type: string
+ 400:
+ description: "problem with selector parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+
+ /test-workflow-executions/{executionID}/artifacts:
+ get:
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ tags:
+ - test-workflows
+ - artifacts
+ - executions
+ - api
+ - pro
+ summary: "Get test workflow execution's artifacts by ID"
+ description: "Returns artifacts of the given executionID"
+ operationId: getTestWorkflowExecutionArtifacts
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Artifact"
+ 404:
+ description: "execution not found"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 500:
+ description: "problem with getting execution's artifacts from storage"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+
+ /test-workflow-executions/{executionID}/artifacts/{filename}:
+ get:
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ - $ref: "#/components/parameters/Filename"
+ tags:
+ - test-workflows
+ - artifacts
+ - executions
+ - api
+ - pro
+ summary: "Download test workflow artifact"
+ description: "Download the artifact file from the given execution"
+ operationId: downloadTestWorkflowArtifact
+ responses:
+ 200:
+ description: "successful operation"
+ content:
+ application/octet-stream:
+ schema:
+ type: string
+ format: binary
+ 404:
+ description: "execution not found"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 500:
+ description: "problem with getting artifacts from storage"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+
+ /test-workflow-executions/{executionID}/artifact-archive:
+ get:
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ - $ref: "#/components/parameters/Mask"
+ tags:
+ - test-workflows
+ - artifacts
+ - executions
+ - api
+ - pro
+ summary: "Download test workflow artifact archive"
+ description: "Download the artifact archive from the given execution"
+ operationId: downloadTestWorkflowArtifactArchive
+ responses:
+ 200:
+ description: "successful operation"
+ content:
+ application/octet-stream:
+ schema:
+ type: string
+ format: binary
+ 404:
+ description: "execution not found"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 500:
+ description: "problem with getting artifact archive from storage"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+
+ /test-workflow-executions/{executionID}/abort:
+ post:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/executionID"
+ summary: Abort test workflow execution
+ description: Abort test workflow execution
+ operationId: abortTestWorkflowExecution
+ responses:
+ 204:
+ description: "no content"
+ 400:
+ description: "problem with selector parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ /test-workflows/{id}/abort:
+ post:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ summary: Abort all test workflow executions
+ description: Abort all test workflow executions
+ operationId: abortAllTestWorkflowExecutions
+ responses:
+ 204:
+ description: "no content"
+ 400:
+ description: "problem with selector parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ /preview-test-workflow:
+ post:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/InlineTemplates"
+ summary: Preview test workflow
+ description: Preview test workflow after including templates inside
+ operationId: previewTestWorkflow
+ requestBody:
+ description: test workflow body
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TestWorkflow"
+ text/yaml:
+ schema:
+ type: string
+ responses:
+ 200:
+ description: resolved test workflow
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflow"
+ text/yaml:
+ schema:
+ type: string
+ 400:
+ description: "problem with body parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ /test-workflows/{id}:
+ get:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ summary: Get test workflow details
+ description: Get test workflow details from the kubernetes cluster
+ operationId: getTestWorkflow
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TestWorkflow"
+ text/yaml:
+ schema:
+ type: string
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 404:
+ description: "the resource has not been found"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ put:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ summary: Update test workflow details
+ description: Update test workflow details in the kubernetes cluster
+ operationId: updateTestWorkflow
+ requestBody:
+ description: test workflow body
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TestWorkflow"
+ text/yaml:
+ schema:
+ type: string
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TestWorkflow"
+ text/yaml:
+ schema:
+ type: string
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 404:
+ description: "the resource has not been found"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ delete:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ - $ref: "#/components/parameters/SkipDeleteExecutions"
+ summary: Delete test workflow
+ description: Delete test workflow from the kubernetes cluster
+ operationId: deleteTestWorkflow
+ responses:
+ 204:
+ description: no content
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 404:
+ description: "the resource has not been found"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+
+ /test-workflow-templates:
+ get:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/Selector"
+ summary: List test workflow templates
+ description: List test workflow templates from the kubernetes cluster
+ operationId: listTestWorkflowTemplates
+ responses:
+ 200:
+ description: successful list operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflowTemplate"
+ text/yaml:
+ schema:
+ type: string
+ 400:
+ description: "problem with selector parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ delete:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/Selector"
+ summary: Delete test workflow templates
+ description: Delete test workflow templates from the kubernetes cluster
+ operationId: deleteTestWorkflowTemplates
+ responses:
+ 204:
+ description: no content
+ 400:
+ description: "problem with selector parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ post:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ summary: Create test workflow template
+ description: Create test workflow template in the kubernetes cluster
+ operationId: createTestWorkflowTemplate
+ requestBody:
+ description: test workflow template body
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TestWorkflowTemplate"
+ text/yaml:
+ schema:
+ type: string
+ responses:
+ 200:
+ description: successful creation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflowTemplate"
+ text/yaml:
+ schema:
+ type: string
+ 400:
+ description: "problem with body parsing - probably some bad input occurs"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ /test-workflow-templates/{id}:
+ get:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ summary: Get test workflow template details
+ description: Get test workflow template details from the kubernetes cluster
+ operationId: getTestWorkflowTemplate
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TestWorkflowTemplate"
+ text/yaml:
+ schema:
+ type: string
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 404:
+ description: "the resource has not been found"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ put:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ summary: Update test workflow template details
+ description: Update test workflow template details in the kubernetes cluster
+ operationId: updateTestWorkflowTemplate
+ requestBody:
+ description: test workflow template body
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TestWorkflowTemplate"
+ text/yaml:
+ schema:
+ type: string
+ responses:
+ 200:
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TestWorkflowTemplate"
+ text/yaml:
+ schema:
+ type: string
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 404:
+ description: "the resource has not been found"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ delete:
+ tags:
+ - test-workflows
+ - api
+ - pro
+ parameters:
+ - $ref: "#/components/parameters/ID"
+ summary: Delete test workflow template
+ description: Delete test workflow template from the kubernetes cluster
+ operationId: deleteTestWorkflowTemplate
+ responses:
+ 204:
+ description: no content
+ 402:
+ description: "missing Pro subscription for a commercial feature"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 404:
+ description: "the resource has not been found"
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+ 502:
+ description: problem communicating with kubernetes cluster
+ content:
+ application/problem+json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Problem"
+
+components:
+ schemas:
+ ExecutionsMetrics:
+ type: object
+ properties:
+ passFailRatio:
+ type: number
+ description: Percentage pass to fail ratio
+ example: 50
+ executionDurationP50:
+ type: string
+ description: 50th percentile of all durations
+ example: "7m2.71s"
+ executionDurationP50ms:
+ type: integer
+ description: 50th percentile of all durations in milliseconds
+ example: 422
+ executionDurationP90:
+ type: string
+ description: 90th percentile of all durations
+ example: "7m2.71s"
+ executionDurationP90ms:
+ type: integer
+ description: 90th percentile of all durations in milliseconds
+ example: 422
+ executionDurationP95:
+ type: string
+ description: 95th percentile of all durations
+ example: "7m2.71s"
+ executionDurationP95ms:
+ type: integer
+ description: 95th percentile of all durations in milliseconds
+ example: 422
+ executionDurationP99:
+ type: string
+ description: 99th percentile of all durations
+ example: "7m2.71s"
+ executionDurationP99ms:
+ type: integer
+ description: 99th percentile of all durations in milliseconds
+ example: 422
+ totalExecutions:
+ type: integer
+ description: total executions number
+ example: 2
+ failedExecutions:
+ type: integer
+ description: failed executions number
+ example: 1
+ executions:
+ type: array
+ description: List of test/testsuite executions
+ items:
+ $ref: "#/components/schemas/ExecutionsMetricsExecutions"
+
+ ExecutionsMetricsExecutions:
+ type: object
+ properties:
+ executionId:
+ type: string
+ duration:
+ type: string
+ durationMs:
+ type: integer
+ status:
+ type: string
+ name:
+ type: string
+ startTime:
+ type: string
+ format: date-time
+
+ Variables:
+ type: object
+ description: "execution variables passed to executor converted to vars for usage in tests"
+ additionalProperties:
+ $ref: "#/components/schemas/Variable"
+ example:
+ var1:
+ name: "var1"
+ type: "basic"
+ value: "value1"
+ secret1:
+ name: "secret1"
+ type: "secret"
+ value: "secretvalue1"
+
+ Variable:
+ type: object
+ properties:
+ name:
+ type: string
+ value:
+ type: string
+ type:
+ $ref: "#/components/schemas/VariableType"
+ secretRef:
+ $ref: "#/components/schemas/SecretRef"
+ configMapRef:
+ $ref: "#/components/schemas/ConfigMapRef"
+
+ VariableType:
+ type: string
+ enum:
+ - basic
+ - secret
+
+ ObjectRef:
+ required:
+ - name
+ type: object
+ properties:
+ namespace:
+ type: string
+ description: object kubernetes namespace
+ example: "testkube"
+ name:
+ type: string
+ description: object name
+ example: "name"
+
+ SecretRef:
+ required:
+ - name
+ - key
+ type: object
+ description: Testkube internal reference for secret storage in Kubernetes secrets
+ properties:
+ namespace:
+ type: string
+ description: object kubernetes namespace
+ name:
+ type: string
+ description: object name
+ key:
+ type: string
+ description: object key
+
+ ConfigMapRef:
+ required:
+ - name
+ - key
+ type: object
+ description: Testkube internal reference for data in Kubernetes config maps
+ properties:
+ namespace:
+ type: string
+ description: object kubernetes namespace
+ name:
+ type: string
+ description: object name
+ key:
+ type: string
+ description: object key
+
+ TestSuite:
+ type: object
+ required:
+ - name
+ - status
+ properties:
+ name:
+ type: string
+ example: "test-suite1"
+ namespace:
+ type: string
+ example: "testkube"
+ description:
+ type: string
+ example: "collection of tests"
+ before:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestSuiteBatchStep"
+ description: Run these batch steps before whole suite
+ example:
+ - stopOnFailure: true
+ execute:
+ - test: "example-test"
+ steps:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestSuiteBatchStep"
+ description: Batch steps to run
+ example:
+ - stopOnFailure: true
+ execute:
+ - test: "example-test"
+ after:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestSuiteBatchStep"
+ description: Run these batch steps after whole suite
+ example:
+ - stopOnFailure: true
+ execute:
- test: "example-test"
+ labels:
+ type: object
+ description: "test suite labels"
+ additionalProperties:
+ type: string
+ example:
+ env: "prod"
+ app: "backend"
+ schedule:
+ type: string
+ description: schedule to run test suite
+ example: "* * * * *"
+ repeats:
+ type: integer
+ default: 1
+ example: 1
+ created:
+ type: string
+ format: date-time
+ executionRequest:
+ $ref: "#/components/schemas/TestSuiteExecutionRequest"
+ status:
+ $ref: "#/components/schemas/TestSuiteStatus"
+ readOnly:
+ type: boolean
+ description: if test suite is offline and cannot be executed
+
+ TestSuiteV2:
+ type: object
+ required:
+ - name
+ - status
+ properties:
+ name:
+ type: string
+ example: "test-suite1"
+ namespace:
+ type: string
+ example: "testkube"
+ description:
+ type: string
+ example: "collection of tests"
+ before:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestSuiteStepV2"
+ description: Run this step before whole suite
+ example:
+ - stopTestOnFailure: true
+ execute:
+ namespace: "testkube"
+ name: "example-test"
+ steps:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestSuiteStepV2"
+ description: Steps to run
+ example:
+ - stopTestOnFailure: true
+ execute:
+ namespace: "testkube"
+ name: "example-test"
after:
type: array
items:
- $ref: "#/components/schemas/TestSuiteBatchStep"
- description: Run these batch steps after whole suite
- example:
- - stopOnFailure: true
- execute:
- - test: "example-test"
+ $ref: "#/components/schemas/TestSuiteStepV2"
+ description: Run this step after whole suite
+ example:
+ - stopTestOnFailure: true
+ execute:
+ namespace: "testkube"
+ name: "example-test"
+ labels:
+ type: object
+ description: "test suite labels"
+ additionalProperties:
+ type: string
+ example:
+ env: "prod"
+ app: "backend"
+ schedule:
+ type: string
+ description: schedule to run test suite
+ example: "* * * * *"
+ repeats:
+ type: integer
+ default: 1
+ example: 1
+ created:
+ type: string
+ format: date-time
+ executionRequest:
+ $ref: "#/components/schemas/TestSuiteExecutionRequest"
+ status:
+ $ref: "#/components/schemas/TestSuiteStatus"
+
+ TestSuiteStepType:
+ type: string
+ enum:
+ - executeTest
+ - delay
+
+ TestSuiteBatchStep:
+ description: set of steps run in parallel
+ type: object
+ required:
+ - stopOnFailure
+ properties:
+ stopOnFailure:
+ type: boolean
+ default: true
+ downloadArtifacts:
+ $ref: "#/components/schemas/DownloadArtifactOptions"
+ execute:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestSuiteStep"
+
+ DownloadArtifactOptions:
+ description: options to download artifacts from previous steps
+ type: object
+ properties:
+ allPreviousSteps:
+ type: boolean
+ default: false
+ previousStepNumbers:
+ type: array
+ description: previous step numbers starting from 1
+ items:
+ type: integer
+ previousTestNames:
+ type: array
+ description: previous test names
+ items:
+ type: string
+
+ TestSuiteStep:
+ type: object
+ properties:
+ test:
+ type: string
+ description: object name
+ example: "name"
+ delay:
+ type: string
+ format: duration
+ example: 1s
+ description: delay duration in time units
+ executionRequest:
+ $ref: "#/components/schemas/TestSuiteStepExecutionRequest"
+ description: test suite step execution request parameters
+
+ TestSuiteStepV2:
+ type: object
+ required:
+ - name
+ - type
+ - stopTestOnFailure
+ properties:
+ stopTestOnFailure:
+ type: boolean
+ default: true
+ execute:
+ $ref: "#/components/schemas/TestSuiteStepExecuteTestV2"
+ delay:
+ $ref: "#/components/schemas/TestSuiteStepDelayV2"
+
+ TestSuiteStepExecuteTestV2:
+ allOf:
+ - $ref: "#/components/schemas/ObjectRef"
+
+ TestSuiteStepDelayV2:
+ type: object
+ required:
+ - duration
+ properties:
+ duration:
+ type: integer
+ default: 0
+ description: delay duration in milliseconds
+
+ TestSuiteExecution:
+ type: object
+ description: Test suite executions data
+ required:
+ - id
+ - name
+ - testSuite
+ properties:
+ id:
+ type: string
+ description: execution id
+ format: bson objectId
+ example: "62f395e004109209b50edfc1"
+ name:
+ type: string
+ description: "execution name"
+ example: "test-suite1.needlessly-sweet-imp"
+ testSuite:
+ $ref: "#/components/schemas/ObjectRef"
+ description: object name and namespace
+ status:
+ $ref: "#/components/schemas/TestSuiteExecutionStatus"
+ envs:
+ deprecated: true
+ type: object
+ description: "Environment variables passed to executor. Deprecated: use Basic Variables instead"
+ additionalProperties:
+ type: string
+ example:
+ record: "true"
+ prefix: "some-"
+ variables:
+ $ref: "#/components/schemas/Variables"
+ secretUUID:
+ type: string
+ description: secret uuid
+ readOnly: true
+ example: "7934600f-b367-48dd-b981-4353304362fb"
+ startTime:
+ type: string
+ description: "test start time"
+ format: date-time
+ endTime:
+ type: string
+ description: "test end time"
+ format: date-time
+ duration:
+ type: string
+ description: "test duration"
+ example: "2m"
+ durationMs:
+ type: integer
+ description: "test duration in ms"
+ example: 6000
+ stepResults:
+ type: array
+ description: "steps execution results"
+ items:
+ $ref: "#/components/schemas/TestSuiteStepExecutionResultV2"
+ description: test execution results
+ executeStepResults:
+ type: array
+ description: "batch steps execution results"
+ items:
+ $ref: "#/components/schemas/TestSuiteBatchStepExecutionResult"
+ description: test execution results
+ labels:
+ type: object
+ description: "test suite labels"
+ additionalProperties:
+ type: string
+ example:
+ env: "prod"
+ app: "backend"
+ runningContext:
+ $ref: "#/components/schemas/RunningContext"
+ description: running context for the test suite execution
+ testSuiteExecutionName:
+ type: string
+ description: test suite execution name started the test suite execution
+
+ TestSuiteExecutionCR:
+ type: object
+ required:
+ - testSuite
+ properties:
+ testSuite:
+ $ref: "#/components/schemas/ObjectRef"
+ description: test suite name and namespace
+ executionRequest:
+ $ref: "#/components/schemas/TestSuiteExecutionRequest"
+ description: test suite execution request parameters
+ status:
+ $ref: "#/components/schemas/TestSuiteExecutionStatusCR"
+ description: test suite execution status
+
+ TestSuiteExecutionStatusCR:
+ type: object
+ description: test suite execution status
+ properties:
+ latestExecution:
+ $ref: "#/components/schemas/TestSuiteExecution"
+ generation:
+ type: integer
+ format: int64
+ description: test suite execution generation
+
+ TestSuiteExecutionStatus:
+ type: string
+ enum:
+ - queued
+ - running
+ - passed
+ - failed
+ - aborting
+ - aborted
+ - timeout
+
+ TestSuiteStepExecutionResult:
+ description: execution result returned from executor
+ type: object
+ properties:
+ step:
+ $ref: "#/components/schemas/TestSuiteStep"
+ test:
+ $ref: "#/components/schemas/ObjectRef"
+ description: object name and namespace
+ execution:
+ $ref: "#/components/schemas/Execution"
+ description: "test step execution, NOTE: the execution output will be empty, retrieve it directly form the test execution"
+
+ TestSuiteStepExecutionResultV2:
+ description: execution result returned from executor
+ type: object
+ properties:
+ step:
+ $ref: "#/components/schemas/TestSuiteStepV2"
+ test:
+ $ref: "#/components/schemas/ObjectRef"
+ description: object name and namespace
+ execution:
+ $ref: "#/components/schemas/Execution"
+ description: test step execution
+
+ TestSuiteBatchStepExecutionResult:
+ description: execution result returned from executor
+ type: object
+ properties:
+ step:
+ $ref: "#/components/schemas/TestSuiteBatchStep"
+ execute:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestSuiteStepExecutionResult"
+ startTime:
+ type: string
+ description: "step start time"
+ format: date-time
+ endTime:
+ type: string
+ description: "step end time"
+ format: date-time
+ duration:
+ type: string
+ description: "step duration"
+ example: "2m"
+
+ TestSuiteExecutionsResult:
+ description: the result for a page of executions
+ type: object
+ required:
+ - totals
+ - results
+ properties:
+ totals:
+ $ref: "#/components/schemas/ExecutionsTotals"
+ filtered:
+ $ref: "#/components/schemas/ExecutionsTotals"
+ results:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestSuiteExecutionSummary"
+
+ TestSuiteExecutionSummary:
+ description: "Test execution summary"
+ type: object
+ required:
+ - id
+ - name
+ - testSuiteName
+ - status
+ properties:
+ id:
+ type: string
+ description: execution id
+ format: bson objectId
+ example: "62f395e004109209b50edfc1"
+ name:
+ type: string
+ description: execution name
+ example: "test-suite1.needlessly-sweet-imp"
+ testSuiteName:
+ type: string
+ description: name of the test suite
+ example: "test-suite1"
+ status:
+ $ref: "#/components/schemas/TestSuiteExecutionStatus"
+ startTime:
+ type: string
+ description: "test suite execution start time"
+ format: date-time
+ endTime:
+ type: string
+ description: "test suite execution end time"
+ format: date-time
+ duration:
+ type: string
+ description: "test suite execution duration"
+ example: "00:00:09"
+ durationMs:
+ type: integer
+ description: "test suite execution duration in ms"
+ example: 9009
+ execution:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestSuiteBatchStepExecutionSummary"
+ labels:
+ type: object
+ description: "test suite and execution labels"
+ additionalProperties:
+ type: string
+ example:
+ env: "prod"
+ app: "backend"
+
+ TestSuiteStepExecutionSummary:
+ description: "Test suite execution summary"
+ type: object
+ required:
+ - id
+ - name
+ - status
+ properties:
+ id:
+ type: string
+ example: "62f395e004109209b50edfc4"
+ name:
+ type: string
+ description: execution name
+ example: "run:testkube/test1"
+ testName:
+ type: string
+ description: test name
+ example: "test1"
+ status:
+ $ref: "#/components/schemas/ExecutionStatus"
+ type:
+ $ref: "#/components/schemas/TestSuiteStepType"
+
+ TestSuiteBatchStepExecutionSummary:
+ description: "Test suite batch execution summary"
+ type: object
+ properties:
+ execute:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestSuiteStepExecutionSummary"
+
+ TestSuiteStatus:
+ type: object
+ description: test suite status
+ properties:
+ latestExecution:
+ $ref: "#/components/schemas/TestSuiteExecutionCore"
+
+ TestSuiteExecutionCore:
+ type: object
+ description: test suite execution core
+ properties:
+ id:
+ type: string
+ description: execution id
+ format: bson objectId
+ example: "62f395e004109209b50edfc4"
+ startTime:
+ type: string
+ description: "test suite execution start time"
+ format: date-time
+ endTime:
+ type: string
+ description: "test suite execution end time"
+ format: date-time
+ status:
+ $ref: "#/components/schemas/TestSuiteExecutionStatus"
+
+ Test:
+ type: object
+ properties:
+ name:
+ type: string
+ description: test name
+ example: "test1"
+ namespace:
+ type: string
+ description: test namespace
+ example: "testkube"
+ description:
+ type: string
+ description: test description
+ example: "this test is used for that purpose"
+ type:
+ type: string
+ description: test type
+ example: "postman/collection"
+ content:
+ $ref: "#/components/schemas/TestContent"
+ description: test content
+ source:
+ type: string
+ description: reference to test source resource
+ example: "my-private-repository-test"
+ created:
+ type: string
+ format: date-time
+ example: "2022-07-30T06:54:15Z"
+ labels:
+ type: object
+ description: "test labels"
+ additionalProperties:
+ type: string
+ example:
+ env: "prod"
+ app: "backend"
+ schedule:
+ type: string
+ description: schedule to run test
+ example: "* * * * *"
+ readOnly:
+ type: boolean
+ description: if test is offline and cannot be executed
+
+ uploads:
+ type: array
+ items:
+ type: string
+ description: list of file paths that will be needed from uploads
+ example:
+ - settings/config.txt
+ executionRequest:
+ $ref: "#/components/schemas/ExecutionRequest"
+ status:
+ $ref: "#/components/schemas/TestStatus"
+
+ TestExecutionCR:
+ type: object
+ required:
+ - test
+ properties:
+ test:
+ $ref: "#/components/schemas/ObjectRef"
+ description: test name and namespace
+ executionRequest:
+ $ref: "#/components/schemas/ExecutionRequest"
+ description: test execution request parameters
+ status:
+ $ref: "#/components/schemas/TestExecutionStatusCR"
+ description: test execution status
+
+ TestExecutionStatusCR:
+ type: object
+ description: test execution status
+ properties:
+ latestExecution:
+ $ref: "#/components/schemas/Execution"
+ generation:
+ type: integer
+ format: int64
+ description: test execution generation
+
+ TestContent:
+ type: object
+ properties:
+ type:
+ type: string
+ description: |
+ type of sources a runner can get data from.
+ string: String content (e.g. Postman JSON file).
+ file-uri: content stored on the webserver.
+ git-file: the file stored in the Git repo in the given repository.path field (Deprecated: use git instead).
+ git-dir: the entire git repo or git subdirectory depending on the repository.path field (Testkube does a shadow clone and sparse checkout to limit IOs in the case of monorepos). (Deprecated: use git instead).
+ git: automatically provisions either a file, directory or whole git repository depending on the repository.path field.
+
+ enum:
+ - string
+ - file-uri
+ # Deprecated: use git instead
+ - git-file
+ # Deprecated: use git instead
+ - git-dir
+ - git
+ repository:
+ $ref: "#/components/schemas/Repository"
+ data:
+ type: string
+ description: test content data as string
+ uri:
+ type: string
+ description: test content
+ example: "https://github.com/kubeshop/testkube"
+
+ TestContentRequest:
+ description: test content request body
+ type: object
+ properties:
+ repository:
+ $ref: "#/components/schemas/RepositoryParameters"
+
+ TestContentUpdate:
+ description: test content update body
+ type: object
+ nullable: true
+ allOf:
+ - $ref: "#/components/schemas/TestContent"
+
+ TestContentUpdateRequest:
+ description: test content update request body
+ type: object
+ nullable: true
+ allOf:
+ - $ref: "#/components/schemas/TestContentRequest"
+
+ TestSource:
+ description: Test source resource for shared test content
+ type: object
+ allOf:
+ - $ref: "#/components/schemas/TestContent"
+ properties:
+ name:
+ type: string
+ description: test source name
+ example: "testsource1"
+ namespace:
+ type: string
+ description: test source namespace
+ example: "testkube"
+ labels:
+ type: object
+ description: "test source labels"
+ additionalProperties:
+ type: string
+
+ TestSourceUpdate:
+ description: Test source resource update for shared test content
+ type: object
+ nullable: true
+ allOf:
+ - $ref: "#/components/schemas/TestContent"
+ properties:
+ name:
+ type: string
+ description: test source name
+ example: "testsource1"
+ namespace:
+ type: string
+ description: test source namespace
+ example: "testkube"
+ labels:
+ type: object
+ description: "test source labels"
+ additionalProperties:
+ type: string
+
+ TestSourceUpsertRequest:
+ description: test source create request body
+ type: object
+ allOf:
+ - $ref: "#/components/schemas/TestContent"
+ properties:
+ name:
+ type: string
+ description: test source name
+ example: "testsource1"
+ namespace:
+ type: string
+ description: test source namespace
+ example: "testkube"
+ labels:
+ type: object
+ description: "test source labels"
+ additionalProperties:
+ type: string
+
+ TestSourceUpdateRequest:
+ description: test source update request body
+ type: object
+ nullable: true
+ allOf:
+ - $ref: "#/components/schemas/TestContent"
+ properties:
+ name:
+ type: string
+ description: test source name
+ example: "testsource1"
+ namespace:
+ type: string
+ description: test source namespace
+ example: "testkube"
+ labels:
+ type: object
+ description: "test source labels"
+ additionalProperties:
+ type: string
+
+ TestStatus:
+ type: object
+ description: test status
+ properties:
+ latestExecution:
+ $ref: "#/components/schemas/ExecutionCore"
+
+ ExecutionCore:
+ type: object
+ description: test execution core
+ properties:
+ id:
+ type: string
+ description: execution id
+ format: bson objectId
+ example: "62f395e004109209b50edfc4"
+ number:
+ type: integer
+ description: "execution number"
+ example: 1
+ startTime:
+ type: string
+ description: "test start time"
+ format: date-time
+ endTime:
+ type: string
+ description: "test end time"
+ format: date-time
+ status:
+ $ref: "#/components/schemas/ExecutionStatus"
+
+ Execution:
+ type: object
+ description: test execution
+ properties:
+ id:
+ type: string
+ description: execution id
+ format: bson objectId
+ example: "62f395e004109209b50edfc4"
+ testName:
+ type: string
+ description: unique test name (CRD Test name)
+ example: "example-test"
+ testSuiteName:
+ type: string
+ description: unique test suite name (CRD Test suite name), if it's run as a part of test suite
+ example: "test-suite1"
+ testNamespace:
+ type: string
+ description: test namespace
+ example: "testkube"
+ testType:
+ type: string
+ description: test type e.g. postman/collection
+ example: "postman/collection"
+ name:
+ type: string
+ description: "execution name"
+ example: "test-suite1-example-test-1"
+ number:
+ type: integer
+ description: "execution number"
+ example: 1
+ envs:
+ deprecated: true
+ type: object
+ description: "Environment variables passed to executor. Deprecated: use Basic Variables instead"
+ additionalProperties:
+ type: string
+ example:
+ record: "true"
+ prefix: "some-"
+ command:
+ type: array
+ description: "executor image command"
+ example: ["curl"]
+ items:
+ type: string
+ args:
+ type: array
+ description: "additional arguments/flags passed to executor binary"
+ example: ["--concurrency", "2", "--remote", "--some", "blabla"]
+ items:
+ type: string
+ args_mode:
+ type: string
+ description: usage mode for arguments
+ enum:
+ - append
+ - override
+ - replace
+ variables:
+ $ref: "#/components/schemas/Variables"
+ isVariablesFileUploaded:
+ type: boolean
+ description: in case the variables file is too big, it will be uploaded to storage
+ example: false
+ variablesFile:
+ type: string
+ description: variables file content - need to be in format for particular executor (e.g. postman envs file)
+ testSecretUUID:
+ type: string
+ description: test secret uuid
+ readOnly: true
+ example: "7934600f-b367-48dd-b981-4353304362fb"
+ testSuiteSecretUUID:
+ type: string
+ description: test suite secret uuid, if it's run as a part of test suite
+ readOnly: true
+ example: "7934600f-b367-48dd-b981-4353304362fb"
+ content:
+ $ref: "#/components/schemas/TestContent"
+ startTime:
+ type: string
+ description: "test start time"
+ format: date-time
+ endTime:
+ type: string
+ description: "test end time"
+ format: date-time
+ duration:
+ type: string
+ description: "test duration"
+ example: "88s"
+ durationMs:
+ type: integer
+ description: "test duration in milliseconds"
+ example: 10000
+ executionResult:
+ $ref: "#/components/schemas/ExecutionResult"
+ description: result get from executor
labels:
type: object
- description: "test suite labels"
+ description: "test and execution labels"
additionalProperties:
type: string
example:
env: "prod"
app: "backend"
- schedule:
+ uploads:
+ type: array
+ items:
+ type: string
+ description: list of file paths that need to be copied into the test from uploads
+ example:
+ - settings/config.txt
+ bucketName:
type: string
- description: schedule to run test suite
- example: "* * * * *"
- repeats:
+ description: minio bucket name to get uploads from
+ example: execution-c01d7cf6-ec3f-47f0-9556-a5d6e9009a43
+ artifactRequest:
+ $ref: "#/components/schemas/ArtifactRequest"
+ description: configuration parameters for storing test artifacts
+ preRunScript:
+ type: string
+ description: script to run before test execution
+ example: "echo -n '$SECRET_ENV' > ./secret_file"
+ postRunScript:
+ type: string
+ description: script to run after test execution
+ example: "sleep 30"
+ executePostRunScriptBeforeScraping:
+ type: boolean
+ description: execute post run script before scraping (prebuilt executor only)
+ sourceScripts:
+ type: boolean
+ description: run scripts using source command (container executor only)
+ runningContext:
+ $ref: "#/components/schemas/RunningContext"
+ description: running context for the test execution
+ containerShell:
+ type: string
+ description: shell used in container executor
+ example: "/bin/sh"
+ testExecutionName:
+ type: string
+ description: test execution name started the test execution
+ downloadArtifactExecutionIDs:
+ type: array
+ description: "execution ids for artifacts to download"
+ items:
+ type: string
+ downloadArtifactTestNames:
+ type: array
+ description: "test names for artifacts to download from latest executions"
+ items:
+ type: string
+ slavePodRequest:
+ $ref: "#/components/schemas/PodRequest"
+ description: configuration parameters for executed slave pods
+ executionNamespace:
+ type: string
+ description: namespace for test execution (Pro edition only)
+
+ Artifact:
+ type: object
+ description: API server artifact
+ properties:
+ name:
+ type: string
+ description: artifact file path
+ size:
type: integer
- default: 1
- example: 1
- created:
+ description: file size in bytes
+ executionName:
type: string
- format: date-time
- executionRequest:
- $ref: "#/components/schemas/TestSuiteExecutionRequest"
+ description: execution name that produced the artifact
+ example: "test-1"
status:
- $ref: "#/components/schemas/TestSuiteStatus"
- readOnly:
- type: boolean
- description: if test suite is offline and cannot be executed
+ type: string
+ enum:
+ - ready
+ - processing
+ - failed
- TestSuiteV2:
+ ExecutionsResult:
+ description: the result for a page of executions
+ type: object
+ required:
+ - totals
+ - results
+ properties:
+ totals:
+ $ref: "#/components/schemas/ExecutionsTotals"
+ filtered:
+ $ref: "#/components/schemas/ExecutionsTotals"
+ results:
+ type: array
+ items:
+ $ref: "#/components/schemas/ExecutionSummary"
+
+ ExecutionSummary:
+ description: "Execution summary"
type: object
required:
+ - id
- name
+ - testName
+ - testType
- status
properties:
+ id:
+ type: string
+ description: execution id
+ format: bson objectId
+ example: "62f395e004109209b50edfc4"
name:
type: string
- example: "test-suite1"
- namespace:
+ description: execution name
+ example: "test-suite1-test1"
+ number:
+ type: integer
+ description: execution number
+ example: 1
+ testName:
+ type: string
+ description: name of the test
+ example: "test1"
+ testNamespace:
type: string
+ description: name of the test
example: "testkube"
- description:
+ testType:
type: string
- example: "collection of tests"
- before:
- type: array
- items:
- $ref: "#/components/schemas/TestSuiteStepV2"
- description: Run this step before whole suite
- example:
- - stopTestOnFailure: true
- execute:
- namespace: "testkube"
- name: "example-test"
- steps:
- type: array
- items:
- $ref: "#/components/schemas/TestSuiteStepV2"
- description: Steps to run
- example:
- - stopTestOnFailure: true
- execute:
- namespace: "testkube"
- name: "example-test"
- after:
- type: array
- items:
- $ref: "#/components/schemas/TestSuiteStepV2"
- description: Run this step after whole suite
- example:
- - stopTestOnFailure: true
- execute:
- namespace: "testkube"
- name: "example-test"
+ description: the type of test for this execution
+ example: "postman/collection"
+ status:
+ $ref: "#/components/schemas/ExecutionStatus"
+ startTime:
+ type: string
+ description: "test execution start time"
+ format: date-time
+ endTime:
+ type: string
+ description: "test execution end time"
+ format: date-time
+ duration:
+ type: string
+ description: calculated test duration
+ example: "00:00:13"
+ durationMs:
+ type: integer
+ description: calculated test duration in ms
+ example: 10000
labels:
type: object
- description: "test suite labels"
+ description: "test and execution labels"
additionalProperties:
type: string
example:
env: "prod"
app: "backend"
- schedule:
- type: string
- description: schedule to run test suite
- example: "* * * * *"
- repeats:
- type: integer
- default: 1
- example: 1
- created:
- type: string
- format: date-time
- executionRequest:
- $ref: "#/components/schemas/TestSuiteExecutionRequest"
- status:
- $ref: "#/components/schemas/TestSuiteStatus"
+ runningContext:
+ $ref: "#/components/schemas/RunningContext"
+ description: running context for the test execution
- TestSuiteStepType:
+ ExecutionStatus:
type: string
enum:
- - executeTest
- - delay
+ - queued
+ - running
+ - passed
+ - failed
+ - aborted
+ - timeout
+ - skipped
- TestSuiteBatchStep:
- description: set of steps run in parallel
+ ExecutionResult:
+ description: execution result returned from executor
type: object
required:
- - stopOnFailure
+ - status
properties:
- stopOnFailure:
- type: boolean
- default: true
- downloadArtifacts:
- $ref: "#/components/schemas/DownloadArtifactOptions"
- execute:
+ status:
+ $ref: "#/components/schemas/ExecutionStatus"
+ output:
+ type: string
+ description: "RAW Test execution output, depends of reporter used in particular tool"
+ outputType:
+ type: string
+ description: "output type depends of reporter used in particular tool"
+ enum:
+ - text/plain
+ - application/junit+xml
+ - application/json
+ errorMessage:
+ type: string
+ description: "error message when status is error, separate to output as output can be partial in case of error"
+ steps:
type: array
items:
- $ref: "#/components/schemas/TestSuiteStep"
+ $ref: "#/components/schemas/ExecutionStepResult"
+ description: execution steps (for collection of requests)
+ reports:
+ type: object
+ properties:
+ junit:
+ type: string
- DownloadArtifactOptions:
- description: options to download artifacts from previous steps
+ ExecutionStepResult:
+ description: execution result data
type: object
+ required:
+ - name
+ - status
properties:
- allPreviousSteps:
+ name:
+ type: string
+ description: step name
+ example: "step1"
+ duration:
+ type: string
+ format: duration
+ example: "10m0s"
+ status:
+ type: string
+ description: execution step status
+ enum: [passed, failed]
+ assertionResults:
+ type: array
+ items:
+ $ref: "#/components/schemas/AssertionResult"
+
+ AssertionResult:
+ description: execution result data
+ type: object
+ properties:
+ name:
+ type: string
+ example: "assertion1"
+ status:
+ type: string
+ enum: [passed, failed]
+ errorMessage:
+ type: string
+ nullable: true
+
+ ExecutionsTotals:
+ type: object
+ description: various execution counters
+ required:
+ - results
+ - passed
+ - failed
+ - queued
+ - running
+ properties:
+ results:
+ type: integer
+ description: the total number of executions available
+ passed:
+ type: integer
+ description: the total number of passed executions available
+ failed:
+ type: integer
+ description: the total number of failed executions available
+ queued:
+ type: integer
+ description: the total number of queued executions available
+ running:
+ type: integer
+ description: the total number of running executions available
+
+ ServerInfo:
+ type: object
+ description: Server information with build version, build commit etc.
+ required:
+ - version
+ properties:
+ version:
+ type: string
+ description: build version
+ example: "v1.4.4"
+ commit:
+ type: string
+ description: build commit
+ example: "aaff223ae68aab1af56e8ed8c84c2b80ed63d9b8"
+ namespace:
+ type: string
+ description: server installaton namespace
+ example: "my-testkube"
+ context:
+ type: string
+ description: currently configured testkube API context
+ example: "cloud|oss"
+ orgId:
+ type: string
+ description: cloud organization id
+ example: "tkcorg_xxxx"
+ envId:
+ type: string
+ description: cloud env id
+ example: "tkcenv_xxxx"
+ helmchartVersion:
+ type: string
+ description: helm chart version
+ example: "1.4.14"
+ dashboardUri:
+ type: string
+ description: dashboard uri
+ example: "http://localhost:8080"
+ disableSecretCreation:
type: boolean
- default: false
- previousStepNumbers:
- type: array
- description: previous step numbers starting from 1
- items:
- type: integer
- previousTestNames:
- type: array
- description: previous test names
- items:
- type: string
+ description: disable secret creation for tests and test sources
+ features:
+ $ref: "#/components/schemas/Features"
- TestSuiteStep:
+ Repository:
+ description: repository representation for tests in git repositories
type: object
+ required:
+ - type
+ - uri
properties:
- test:
+ type:
type: string
- description: object name
- example: "name"
- delay:
+ enum:
+ - git
+ description: VCS repository type
+ uri:
type: string
- format: duration
- example: 1s
- description: delay duration in time units
+ description: uri of content file or git directory
+ example: "https://github.com/kubeshop/testkube"
+ branch:
+ type: string
+ description: branch/tag name for checkout
+ example: "main"
+ commit:
+ type: string
+ description: commit id (sha) for checkout
+ example: "b928cbb7186944ab9275937ec1ac3d3738ca2e1d"
+ path:
+ type: string
+ description: if needed we can checkout particular path (dir or file) in case of BIG/mono repositories
+ example: "test/perf"
+ username:
+ type: string
+ description: git auth username for private repositories
+ token:
+ type: string
+ description: git auth token for private repositories
+ usernameSecret:
+ $ref: "#/components/schemas/SecretRef"
+ tokenSecret:
+ $ref: "#/components/schemas/SecretRef"
+ certificateSecret:
+ type: string
+ description: secret with certificate for private repositories. Should contain one key ending with .crt such as "mycorp.crt", whose value is the certificate file content, suitable for git config http.sslCAInfo
+ workingDir:
+ type: string
+ description: if provided we checkout the whole repository and run test from this directory
+ example: "/"
+ authType:
+ type: string
+ enum:
+ - basic
+ - header
+ description: auth type for git requests
- TestSuiteStepV2:
+ RepositoryParameters:
+ description: repository parameters for tests in git repositories
type: object
- required:
- - name
- - type
- - stopTestOnFailure
properties:
- stopTestOnFailure:
- type: boolean
- default: true
- execute:
- $ref: "#/components/schemas/TestSuiteStepExecuteTestV2"
- delay:
- $ref: "#/components/schemas/TestSuiteStepDelayV2"
+ branch:
+ type: string
+ description: branch/tag name for checkout
+ example: "main"
+ commit:
+ type: string
+ description: commit id (sha) for checkout
+ example: "b928cbb7186944ab9275937ec1ac3d3738ca2e1d"
+ path:
+ type: string
+ description: if needed we can checkout particular path (dir or file) in case of BIG/mono repositories
+ example: "test/perf"
+ workingDir:
+ type: string
+ description: if provided we checkout the whole repository and run test from this directory
+ example: "/"
- TestSuiteStepExecuteTestV2:
+ RepositoryUpdate:
+ description: repository update body
+ type: object
+ nullable: true
allOf:
- - $ref: "#/components/schemas/ObjectRef"
+ - $ref: "#/components/schemas/Repository"
- TestSuiteStepDelayV2:
+ RepositoryUpdateParameters:
+ description: repository update parameters for tests in git repositories
type: object
- required:
- - duration
- properties:
- duration:
- type: integer
- default: 0
- description: delay duration in milliseconds
+ nullable: true
+ allOf:
+ - $ref: "#/components/schemas/RepositoryParameters"
- TestSuiteExecution:
+ ArtifactRequest:
+ description: artifact request body with test artifacts
type: object
- description: Test suite executions data
- required:
- - id
- - name
- - testSuite
properties:
- id:
- type: string
- description: execution id
- format: bson objectId
- example: "62f395e004109209b50edfc1"
- name:
- type: string
- description: "execution name"
- example: "test-suite1.needlessly-sweet-imp"
- testSuite:
- $ref: "#/components/schemas/ObjectRef"
- description: object name and namespace
- status:
- $ref: "#/components/schemas/TestSuiteExecutionStatus"
- envs:
- deprecated: true
- type: object
- description: "Environment variables passed to executor. Deprecated: use Basic Variables instead"
- additionalProperties:
- type: string
- example:
- record: "true"
- prefix: "some-"
- variables:
- $ref: "#/components/schemas/Variables"
- secretUUID:
- type: string
- description: secret uuid
- readOnly: true
- example: "7934600f-b367-48dd-b981-4353304362fb"
- startTime:
- type: string
- description: "test start time"
- format: date-time
- endTime:
+ storageClassName:
type: string
- description: "test end time"
- format: date-time
- duration:
+ description: artifact storage class name for container executor
+ example: artifact-volume-local
+ volumeMountPath:
type: string
- description: "test duration"
- example: "2m"
- durationMs:
- type: integer
- description: "test duration in ms"
- example: 6000
- stepResults:
+ description: artifact volume mount path for container executor
+ dirs:
type: array
- description: "steps execution results"
items:
- $ref: "#/components/schemas/TestSuiteStepExecutionResultV2"
- description: test execution results
- executeStepResults:
+ type: string
+ description: artifact directories for scraping
+ masks:
type: array
- description: "batch steps execution results"
items:
- $ref: "#/components/schemas/TestSuiteBatchStepExecutionResult"
- description: test execution results
- labels:
- type: object
- description: "test suite labels"
- additionalProperties:
type: string
- example:
- env: "prod"
- app: "backend"
- runningContext:
- $ref: "#/components/schemas/RunningContext"
- description: running context for the test suite execution
- testSuiteExecutionName:
+ description: regexp to filter scraped artifacts, single or comma separated
+ storageBucket:
type: string
- description: test suite execution name started the test suite execution
+ description: artifact bucket storage
+ example: test1-artifacts
+ omitFolderPerExecution:
+ type: boolean
+ description: don't use a separate folder for execution artifacts
+ sharedBetweenPods:
+ type: boolean
+ description: whether to share volume between pods
- TestSuiteExecutionCR:
+ ArtifactUpdateRequest:
+ description: artifact request update body
type: object
- required:
- - testSuite
- properties:
- testSuite:
- $ref: "#/components/schemas/ObjectRef"
- description: test suite name and namespace
- executionRequest:
- $ref: "#/components/schemas/TestSuiteExecutionRequest"
- description: test suite execution request parameters
- status:
- $ref: "#/components/schemas/TestSuiteExecutionStatusCR"
- description: test suite execution status
+ nullable: true
+ allOf:
+ - $ref: "#/components/schemas/ArtifactRequest"
- TestSuiteExecutionStatusCR:
+ PodRequest:
+ description: pod request body
type: object
- description: test suite execution status
properties:
- latestExecution:
- $ref: "#/components/schemas/TestSuiteExecution"
- generation:
- type: integer
- format: int64
- description: test suite execution generation
-
- TestSuiteExecutionStatus:
- type: string
- enum:
- - queued
- - running
- - passed
- - failed
- - aborting
- - aborted
- - timeout
+ resources:
+ $ref: "#/components/schemas/PodResourcesRequest"
+ description: pod resources request parameters
+ podTemplate:
+ type: string
+ description: pod template extensions
+ podTemplateReference:
+ type: string
+ description: name of the template resource
- TestSuiteStepExecutionResult:
- description: execution result returned from executor
+ PodUpdateRequest:
+ description: pod request update body
type: object
- properties:
- step:
- $ref: "#/components/schemas/TestSuiteStep"
- test:
- $ref: "#/components/schemas/ObjectRef"
- description: object name and namespace
- execution:
- $ref: "#/components/schemas/Execution"
- description: "test step execution, NOTE: the execution output will be empty, retrieve it directly form the test execution"
+ nullable: true
+ allOf:
+ - $ref: "#/components/schemas/PodRequest"
- TestSuiteStepExecutionResultV2:
- description: execution result returned from executor
+ PodResourcesRequest:
+ description: pod resources request specification
type: object
properties:
- step:
- $ref: "#/components/schemas/TestSuiteStepV2"
- test:
- $ref: "#/components/schemas/ObjectRef"
- description: object name and namespace
- execution:
- $ref: "#/components/schemas/Execution"
- description: test step execution
+ requests:
+ $ref: "#/components/schemas/ResourceRequest"
+ description: pod resources requests
+ limits:
+ $ref: "#/components/schemas/ResourceRequest"
+ description: pod resources limits
- TestSuiteBatchStepExecutionResult:
- description: execution result returned from executor
+ PodResourcesUpdateRequest:
+ description: pod resources update request specification
+ type: object
+ nullable: true
+ allOf:
+ - $ref: "#/components/schemas/PodResourcesRequest"
+
+ ResourceRequest:
+ description: resource request specification
type: object
properties:
- step:
- $ref: "#/components/schemas/TestSuiteBatchStep"
- execute:
- type: array
- items:
- $ref: "#/components/schemas/TestSuiteStepExecutionResult"
- startTime:
- type: string
- description: "step start time"
- format: date-time
- endTime:
+ cpu:
type: string
- description: "step end time"
- format: date-time
- duration:
+ description: requested cpu units
+ example: 250m
+ memory:
type: string
- description: "step duration"
- example: "2m"
+ description: requested memory units
+ example: 64Mi
- TestSuiteExecutionsResult:
- description: the result for a page of executions
+ ResourceUpdateRequest:
+ description: resource update request specification
type: object
- required:
- - totals
- - results
- properties:
- totals:
- $ref: "#/components/schemas/ExecutionsTotals"
- filtered:
- $ref: "#/components/schemas/ExecutionsTotals"
- results:
- type: array
- items:
- $ref: "#/components/schemas/TestSuiteExecutionSummary"
+ nullable: true
+ allOf:
+ - $ref: "#/components/schemas/ResourceRequest"
- TestSuiteExecutionSummary:
- description: "Test execution summary"
+ ExecutionRequest:
+ description: test execution request body
type: object
- required:
- - id
- - name
- - testSuiteName
- - status
properties:
id:
type: string
@@ -3789,2131 +6062,2773 @@ components:
example: "62f395e004109209b50edfc1"
name:
type: string
- description: execution name
- example: "test-suite1.needlessly-sweet-imp"
+ description: test execution custom name
+ example: "testing with 1000 users"
testSuiteName:
type: string
- description: name of the test suite
+ description: unique test suite name (CRD Test suite name), if it's run as a part of test suite
example: "test-suite1"
- status:
- $ref: "#/components/schemas/TestSuiteExecutionStatus"
- startTime:
+ number:
+ type: integer
+ description: test execution number
+ executionLabels:
+ type: object
+ description: "test execution labels"
+ additionalProperties:
+ type: string
+ example:
+ users: "3"
+ prefix: "some-"
+ namespace:
type: string
- description: "test suite execution start time"
- format: date-time
- endTime:
+ description: test kubernetes namespace ("testkube" when not set)
+ example: testkube
+ isVariablesFileUploaded:
+ type: boolean
+ description: in case the variables file is too big, it will be uploaded
+ example: false
+ variablesFile:
type: string
- description: "test suite execution end time"
- format: date-time
- duration:
+ description: variables file content - need to be in format for particular executor (e.g. postman envs file)
+ variables:
+ $ref: "#/components/schemas/Variables"
+ testSecretUUID:
type: string
- description: "test suite execution duration"
- example: "00:00:09"
- durationMs:
- type: integer
- description: "test suite execution duration in ms"
- example: 9009
- execution:
+ description: test secret uuid
+ readOnly: true
+ example: "7934600f-b367-48dd-b981-4353304362fb"
+ testSuiteSecretUUID:
+ type: string
+ description: test suite secret uuid, if it's run as a part of test suite
+ readOnly: true
+ example: "7934600f-b367-48dd-b981-4353304362fb"
+ command:
type: array
+ description: "executor image command"
items:
- $ref: "#/components/schemas/TestSuiteBatchStepExecutionSummary"
- labels:
+ type: string
+ example:
+ - "curl"
+ args:
+ type: array
+ description: "additional executor binary arguments"
+ items:
+ type: string
+ example:
+ - "--repeats"
+ - "5"
+ - "--insecure"
+ args_mode:
+ type: string
+ description: usage mode for arguments
+ enum:
+ - append
+ - override
+ - replace
+ image:
+ type: string
+ description: container image, executor will run inside this image
+ example: kubeshop/testkube-executor-custom:1.10.11-dev-0a9c91
+ imagePullSecrets:
+ type: array
+ description: "container image pull secrets"
+ items:
+ $ref: "#/components/schemas/LocalObjectReference"
+ envs:
+ deprecated: true
type: object
- description: "test suite and execution labels"
+ description: "Environment variables passed to executor. Deprecated: use Basic Variables instead"
additionalProperties:
type: string
example:
- env: "prod"
- app: "backend"
-
- TestSuiteStepExecutionSummary:
- description: "Test suite execution summary"
- type: object
- required:
- - id
- - name
- - status
- properties:
- id:
+ record: "true"
+ prefix: "some-"
+ secretEnvs:
+ deprecated: true
+ type: object
+ description: "Execution variables passed to executor from secrets. Deprecated: use Secret Variables instead"
+ additionalProperties:
+ type: string
+ example:
+ secret_key_name1: "secret-name"
+ secret_Key_name2: "secret-name"
+ sync:
+ type: boolean
+ description: whether to start execution sync or async
+ httpProxy:
type: string
- example: "62f395e004109209b50edfc4"
- name:
+ description: http proxy for executor containers
+ example: user:pass@my.proxy.server:8080
+ httpsProxy:
type: string
- description: execution name
- example: "run:testkube/test1"
- testName:
+ description: https proxy for executor containers
+ example: user:pass@my.proxy.server:8081
+ negativeTest:
+ type: boolean
+ description: whether to run test as negative test
+ example: false
+ isNegativeTestChangedOnRun:
+ type: boolean
+ description: whether negativeTest was changed by user
+ example: false
+ activeDeadlineSeconds:
+ type: integer
+ format: int64
+ description: duration in seconds the test may be active, until its stopped
+ example: 1
+ uploads:
+ type: array
+ items:
+ type: string
+ description: list of file paths that need to be copied into the test from uploads
+ example:
+ - settings/config.txt
+ bucketName:
+ type: string
+ description: minio bucket name to get uploads from
+ example: execution-c01d7cf6-ec3f-47f0-9556-a5d6e9009a43
+ artifactRequest:
+ $ref: "#/components/schemas/ArtifactRequest"
+ description: configuration parameters for storing test artifacts
+ jobTemplate:
+ type: string
+ description: job template extensions
+ jobTemplateReference:
+ type: string
+ description: name of the template resource
+ cronJobTemplate:
type: string
- description: test name
- example: "test1"
- status:
- $ref: "#/components/schemas/ExecutionStatus"
- type:
- $ref: "#/components/schemas/TestSuiteStepType"
-
- TestSuiteBatchStepExecutionSummary:
- description: "Test suite batch execution summary"
- type: object
- properties:
- execute:
- type: array
- items:
- $ref: "#/components/schemas/TestSuiteStepExecutionSummary"
-
- TestSuiteStatus:
- type: object
- description: test suite status
- properties:
- latestExecution:
- $ref: "#/components/schemas/TestSuiteExecutionCore"
-
- TestSuiteExecutionCore:
- type: object
- description: test suite execution core
- properties:
- id:
+ description: cron job template extensions
+ cronJobTemplateReference:
type: string
- description: execution id
- format: bson objectId
- example: "62f395e004109209b50edfc4"
- startTime:
+ description: name of the template resource
+ contentRequest:
+ $ref: "#/components/schemas/TestContentRequest"
+ description: adjusting parameters for test content
+ preRunScript:
type: string
- description: "test suite execution start time"
- format: date-time
- endTime:
+ description: script to run before test execution
+ example: "echo -n '$SECRET_ENV' > ./secret_file"
+ postRunScript:
type: string
- description: "test suite execution end time"
- format: date-time
- status:
- $ref: "#/components/schemas/TestSuiteExecutionStatus"
-
- Test:
- type: object
- properties:
- name:
+ description: script to run after test execution
+ example: "sleep 30"
+ executePostRunScriptBeforeScraping:
+ type: boolean
+ description: execute post run script before scraping (prebuilt executor only)
+ sourceScripts:
+ type: boolean
+ description: run scripts using source command (container executor only)
+ scraperTemplate:
type: string
- description: test name
- example: "test1"
- namespace:
+ description: scraper template extensions
+ scraperTemplateReference:
type: string
- description: test namespace
- example: "testkube"
- description:
+ description: name of the template resource
+ pvcTemplate:
type: string
- description: test description
- example: "this test is used for that purpose"
- type:
+ description: pvc template extensions
+ pvcTemplateReference:
type: string
- description: test type
- example: "postman/collection"
- content:
- $ref: "#/components/schemas/TestContent"
- description: test content
- source:
+ description: name of the template resource
+ envConfigMaps:
+ type: array
+ description: "config map references"
+ items:
+ $ref: "#/components/schemas/EnvReference"
+ envSecrets:
+ type: array
+ description: "secret references"
+ items:
+ $ref: "#/components/schemas/EnvReference"
+ runningContext:
+ $ref: "#/components/schemas/RunningContext"
+ description: running context for the test execution
+ testExecutionName:
type: string
- description: reference to test source resource
- example: "my-private-repository-test"
- created:
+ description: test execution name started the test execution
+ downloadArtifactExecutionIDs:
+ type: array
+ description: "execution ids for artifacts to download"
+ items:
+ type: string
+ downloadArtifactTestNames:
+ type: array
+ description: "test names for artifacts to download from latest executions"
+ items:
+ type: string
+ slavePodRequest:
+ $ref: "#/components/schemas/PodRequest"
+ description: configuration parameters for executed slave pods
+ executionNamespace:
type: string
- format: date-time
- example: "2022-07-30T06:54:15Z"
- labels:
+ description: namespace for test execution (Pro edition only)
+
+ TestSuiteStepExecutionRequest:
+ description: test step execution request body
+ type: object
+ readOnly: true
+ properties:
+ executionLabels:
type: object
- description: "test labels"
+ description: "test execution labels"
additionalProperties:
type: string
example:
- env: "prod"
- app: "backend"
- schedule:
- type: string
- description: schedule to run test
- example: "* * * * *"
- readOnly:
- type: boolean
- description: if test is offline and cannot be executed
-
- uploads:
+ users: "3"
+ prefix: "some-"
+ variables:
+ $ref: "#/components/schemas/Variables"
+ command:
type: array
+ description: "executor image command"
items:
type: string
- description: list of file paths that will be needed from uploads
example:
- - settings/config.txt
- executionRequest:
- $ref: "#/components/schemas/ExecutionRequest"
- status:
- $ref: "#/components/schemas/TestStatus"
-
- TestExecutionCR:
- type: object
- required:
- - test
- properties:
- test:
- $ref: "#/components/schemas/ObjectRef"
- description: test name and namespace
- executionRequest:
- $ref: "#/components/schemas/ExecutionRequest"
- description: test execution request parameters
- status:
- $ref: "#/components/schemas/TestExecutionStatusCR"
- description: test execution status
-
- TestExecutionStatusCR:
- type: object
- description: test execution status
- properties:
- latestExecution:
- $ref: "#/components/schemas/Execution"
- generation:
- type: integer
- format: int64
- description: test execution generation
-
- TestContent:
- type: object
- properties:
- type:
+ - "curl"
+ args:
+ type: array
+ description: "additional executor binary arguments"
+ items:
+ type: string
+ example:
+ - "--repeats"
+ - "5"
+ - "--insecure"
+ args_mode:
type: string
- description: |
- type of sources a runner can get data from.
- string: String content (e.g. Postman JSON file).
- file-uri: content stored on the webserver.
- git-file: the file stored in the Git repo in the given repository.path field (Deprecated: use git instead).
- git-dir: the entire git repo or git subdirectory depending on the repository.path field (Testkube does a shadow clone and sparse checkout to limit IOs in the case of monorepos). (Deprecated: use git instead).
- git: automatically provisions either a file, directory or whole git repository depending on the repository.path field.
-
+ description: usage mode for arguments
enum:
- - string
- - file-uri
- # Deprecated: use git instead
- - git-file
- # Deprecated: use git instead
- - git-dir
- - git
- repository:
- $ref: "#/components/schemas/Repository"
- data:
+ - append
+ - override
+ - replace
+ sync:
+ type: boolean
+ description: whether to start execution sync or async
+ httpProxy:
type: string
- description: test content data as string
- uri:
+ description: http proxy for executor containers
+ example: user:pass@my.proxy.server:8080
+ httpsProxy:
type: string
- description: test content
- example: "https://github.com/kubeshop/testkube"
-
- TestContentRequest:
- description: test content request body
- type: object
- properties:
- repository:
- $ref: "#/components/schemas/RepositoryParameters"
-
- TestContentUpdate:
- description: test content update body
- type: object
- nullable: true
- allOf:
- - $ref: "#/components/schemas/TestContent"
+ description: https proxy for executor containers
+ example: user:pass@my.proxy.server:8081
+ negativeTest:
+ type: boolean
+ description: whether to run test as negative test
+ example: false
+ jobTemplate:
+ type: string
+ description: job template extensions
+ jobTemplateReference:
+ type: string
+ description: name of the template resource
+ cronJobTemplate:
+ type: string
+ description: cron job template extensions
+ cronJobTemplateReference:
+ type: string
+ description: name of the template resource
+ scraperTemplate:
+ type: string
+ description: scraper template extensions
+ scraperTemplateReference:
+ type: string
+ description: name of the template resource
+ pvcTemplate:
+ type: string
+ description: pvc template extensions
+ pvcTemplateReference:
+ type: string
+ description: name of the template resource
+ runningContext:
+ $ref: "#/components/schemas/RunningContext"
+ description: running context for the test execution
- TestContentUpdateRequest:
- description: test content update request body
+ ExecutionUpdateRequest:
+ description: test execution request update body
type: object
nullable: true
allOf:
- - $ref: "#/components/schemas/TestContentRequest"
+ - $ref: "#/components/schemas/ExecutionRequest"
- TestSource:
- description: Test source resource for shared test content
+ TestSuiteExecutionRequest:
+ description: test suite execution request body
type: object
- allOf:
- - $ref: "#/components/schemas/TestContent"
properties:
name:
type: string
- description: test source name
- example: "testsource1"
+ description: test execution custom name
+ example: "testing with 1000 users"
+ number:
+ type: integer
+ description: test suite execution number
+ example: 1
namespace:
type: string
- description: test source namespace
- example: "testkube"
+ description: test kubernetes namespace ("testkube" when not set)
+ example: testkube
+ variables:
+ $ref: "#/components/schemas/Variables"
+ secretUUID:
+ type: string
+ description: secret uuid
+ readOnly: true
+ example: "7934600f-b367-48dd-b981-4353304362fb"
labels:
type: object
- description: "test source labels"
+ description: "test suite labels"
+ additionalProperties:
+ type: string
+ example:
+ users: "3"
+ prefix: "some-"
+ executionLabels:
+ type: object
+ description: "execution labels"
additionalProperties:
type: string
+ example:
+ users: "3"
+ prefix: "some-"
+ sync:
+ type: boolean
+ description: whether to start execution sync or async
+ httpProxy:
+ type: string
+ description: http proxy for executor containers
+ example: user:pass@my.proxy.server:8080
+ httpsProxy:
+ type: string
+ description: https proxy for executor containers
+ example: user:pass@my.proxy.server:8081
+ timeout:
+ type: integer
+ format: int32
+ description: duration in seconds the test suite may be active, until its stopped
+ example: 1
+ contentRequest:
+ $ref: "#/components/schemas/TestContentRequest"
+ description: adjusting parameters for test content
+ runningContext:
+ $ref: "#/components/schemas/RunningContext"
+ description: running context for the test suite execution
+ jobTemplate:
+ type: string
+ description: job template extensions
+ jobTemplateReference:
+ type: string
+ description: name of the template resource
+ cronJobTemplate:
+ type: string
+ description: cron job template extensions
+ cronJobTemplateReference:
+ type: string
+ description: name of the template resource
+ scraperTemplate:
+ type: string
+ description: scraper template extensions
+ scraperTemplateReference:
+ type: string
+ description: name of the template resource
+ pvcTemplate:
+ type: string
+ description: pvc template extensions
+ pvcTemplateReference:
+ type: string
+ description: name of the template resource
+ concurrencyLevel:
+ type: integer
+ format: int32
+ description: number of tests run in parallel
+ example: 10
+ testSuiteExecutionName:
+ type: string
+ description: test suite execution name started the test suite execution
- TestSourceUpdate:
- description: Test source resource update for shared test content
+ TestSuiteExecutionUpdateRequest:
+ description: test suite execution update request body
type: object
nullable: true
allOf:
- - $ref: "#/components/schemas/TestContent"
- properties:
- name:
- type: string
- description: test source name
- example: "testsource1"
- namespace:
- type: string
- description: test source namespace
- example: "testkube"
- labels:
- type: object
- description: "test source labels"
- additionalProperties:
- type: string
+ - $ref: "#/components/schemas/TestSuiteExecutionRequest"
- TestSourceUpsertRequest:
- description: test source create request body
+ TestUpsertRequest:
+ description: test create request body
type: object
allOf:
- - $ref: "#/components/schemas/TestContent"
- properties:
- name:
- type: string
- description: test source name
- example: "testsource1"
- namespace:
- type: string
- description: test source namespace
- example: "testkube"
- labels:
- type: object
- description: "test source labels"
- additionalProperties:
- type: string
+ - $ref: "#/components/schemas/Test"
- TestSourceUpdateRequest:
- description: test source update request body
+ TestUpdateRequest:
+ description: test update request body
type: object
nullable: true
allOf:
- - $ref: "#/components/schemas/TestContent"
- properties:
- name:
- type: string
- description: test source name
- example: "testsource1"
- namespace:
- type: string
- description: test source namespace
- example: "testkube"
- labels:
- type: object
- description: "test source labels"
- additionalProperties:
- type: string
+ - $ref: "#/components/schemas/Test"
- TestStatus:
+ TestSuiteUpsertRequest:
+ description: test suite create request body
type: object
- description: test status
- properties:
- latestExecution:
- $ref: "#/components/schemas/ExecutionCore"
+ required:
+ - name
+ - namespace
+ allOf:
+ - $ref: "#/components/schemas/TestSuite"
+ - $ref: "#/components/schemas/ObjectRef"
- ExecutionCore:
+ TestSuiteUpsertRequestV2:
+ description: test suite create request body
type: object
- description: test execution core
- properties:
- id:
- type: string
- description: execution id
- format: bson objectId
- example: "62f395e004109209b50edfc4"
- number:
- type: integer
- description: "execution number"
- example: 1
- startTime:
- type: string
- description: "test start time"
- format: date-time
- endTime:
- type: string
- description: "test end time"
- format: date-time
- status:
- $ref: "#/components/schemas/ExecutionStatus"
+ required:
+ - name
+ - namespace
+ allOf:
+ - $ref: "#/components/schemas/TestSuiteV2"
+ - $ref: "#/components/schemas/ObjectRef"
+
+ TestSuiteUpdateRequest:
+ description: test suite update body
+ type: object
+ nullable: true
+ allOf:
+ - $ref: "#/components/schemas/TestSuite"
+ - $ref: "#/components/schemas/ObjectRef"
+
+ TestSuiteUpdateRequestV2:
+ description: test suite update body
+ type: object
+ nullable: true
+ allOf:
+ - $ref: "#/components/schemas/TestSuiteV2"
+ - $ref: "#/components/schemas/ObjectRef"
+
+ TestTriggerUpsertRequest:
+ description: test trigger create or update request body
+ type: object
+ required:
+ - resource
+ - resourceSelector
+ - event
+ - action
+ - execution
+ - testSelector
+ allOf:
+ - $ref: "#/components/schemas/TestTrigger"
+ - $ref: "#/components/schemas/ObjectRef"
+
+ ExecutorUpsertRequest:
+ description: executor create request body
+ type: object
+ required:
+ - name
+ - namespace
+ - types
+ allOf:
+ - $ref: "#/components/schemas/Executor"
+ - $ref: "#/components/schemas/ObjectRef"
+
+ ExecutorUpdateRequest:
+ description: executor update request body
+ type: object
+ nullable: true
+ allOf:
+ - $ref: "#/components/schemas/Executor"
+ - $ref: "#/components/schemas/ObjectRef"
- Execution:
+ WebhookCreateRequest:
+ description: webhook create request body
+ type: object
+ allOf:
+ - $ref: "#/components/schemas/Webhook"
+
+ WebhookUpdateRequest:
+ description: webhook update request body
+ type: object
+ nullable: true
+ allOf:
+ - $ref: "#/components/schemas/Webhook"
+
+ # Copied from CRD spec
+ # https://github.com/kubeshop/testkube-operator/blob/main/config/crd/bases/executor.testkube.io_executors.yaml
+ # TODO we need to sync those in some nice way
+ Executor:
+ description: CRD based executor data
type: object
- description: test execution
properties:
- id:
- type: string
- description: execution id
- format: bson objectId
- example: "62f395e004109209b50edfc4"
- testName:
- type: string
- description: unique test name (CRD Test name)
- example: "example-test"
- testSuiteName:
- type: string
- description: unique test suite name (CRD Test suite name), if it's run as a part of test suite
- example: "test-suite1"
- testNamespace:
- type: string
- description: test namespace
- example: "testkube"
- testType:
+ executorType:
+ description:
+ ExecutorType one of "rest" for rest openapi based executors
+ or "job" which will be default runners for testkube soon
type: string
- description: test type e.g. postman/collection
- example: "postman/collection"
- name:
+ image:
+ description: Image for kube-job
type: string
- description: "execution name"
- example: "test-suite1-example-test-1"
- number:
- type: integer
- description: "execution number"
- example: 1
- envs:
- deprecated: true
- type: object
- description: "Environment variables passed to executor. Deprecated: use Basic Variables instead"
- additionalProperties:
- type: string
- example:
- record: "true"
- prefix: "some-"
- command:
+ slaves:
+ $ref: "#/components/schemas/SlavesMeta"
+ imagePullSecrets:
type: array
- description: "executor image command"
- example: ["curl"]
+ description: "container image pull secrets"
items:
- type: string
- args:
+ $ref: "#/components/schemas/LocalObjectReference"
+ command:
type: array
- description: "additional arguments/flags passed to executor binary"
- example: ["--concurrency", "2", "--remote", "--some", "blabla"]
+ description: "executor image command"
items:
type: string
- args_mode:
- type: string
- description: usage mode for arguments
- enum:
- - append
- - override
- variables:
- $ref: "#/components/schemas/Variables"
- isVariablesFileUploaded:
- type: boolean
- description: in case the variables file is too big, it will be uploaded to storage
- example: false
- variablesFile:
- type: string
- description: variables file content - need to be in format for particular executor (e.g. postman envs file)
- testSecretUUID:
- type: string
- description: test secret uuid
- readOnly: true
- example: "7934600f-b367-48dd-b981-4353304362fb"
- testSuiteSecretUUID:
- type: string
- description: test suite secret uuid, if it's run as a part of test suite
- readOnly: true
- example: "7934600f-b367-48dd-b981-4353304362fb"
- content:
- $ref: "#/components/schemas/TestContent"
- startTime:
- type: string
- description: "test start time"
- format: date-time
- endTime:
- type: string
- description: "test end time"
- format: date-time
- duration:
- type: string
- description: "test duration"
- example: "88s"
- durationMs:
- type: integer
- description: "test duration in milliseconds"
- example: 10000
- executionResult:
- $ref: "#/components/schemas/ExecutionResult"
- description: result get from executor
- labels:
- type: object
- description: "test and execution labels"
- additionalProperties:
- type: string
example:
- env: "prod"
- app: "backend"
- uploads:
+ - "curl"
+ args:
type: array
+ description: "additional executor binary argument"
items:
type: string
- description: list of file paths that need to be copied into the test from uploads
example:
- - settings/config.txt
- bucketName:
- type: string
- description: minio bucket name to get uploads from
- example: execution-c01d7cf6-ec3f-47f0-9556-a5d6e9009a43
- artifactRequest:
- $ref: "#/components/schemas/ArtifactRequest"
- description: configuration parameters for storing test artifacts
- preRunScript:
- type: string
- description: script to run before test execution
- example: "echo -n '$SECRET_ENV' > ./secret_file"
- postRunScript:
- type: string
- description: script to run after test execution
- example: "sleep 30"
- executePostRunScriptBeforeScraping:
- type: boolean
- description: execute post run script before scraping (prebuilt executor only)
- runningContext:
- $ref: "#/components/schemas/RunningContext"
- description: running context for the test execution
- containerShell:
- type: string
- description: shell used in container executor
- example: "/bin/sh"
- testExecutionName:
- type: string
- description: test execution name started the test execution
- downloadArtifactExecutionIDs:
- type: array
- description: "execution ids for artifacts to download"
+ - "--repeats"
+ - "5"
+ - "--insecure"
+ types:
+ description: Types defines what types can be handled by executor e.g.
+ "postman/collection", ":curl/command" etc
items:
type: string
- downloadArtifactTestNames:
type: array
- description: "test names for artifacts to download from latest executions"
+ uri:
+ description: URI for rest based executors
+ type: string
+ contentTypes:
+ description: list of handled content types
items:
type: string
- slavePodRequest:
- $ref: "#/components/schemas/PodRequest"
- description: configuration parameters for executed slave pods
-
- Artifact:
- type: object
- description: API server artifact
- properties:
- name:
- type: string
- description: artifact file path
- size:
- type: integer
- description: file size in bytes
- executionName:
- type: string
- description: execution name that produced the artifact
- example: "test-1"
-
- ExecutionsResult:
- description: the result for a page of executions
- type: object
- required:
- - totals
- - results
- properties:
- totals:
- $ref: "#/components/schemas/ExecutionsTotals"
- filtered:
- $ref: "#/components/schemas/ExecutionsTotals"
- results:
type: array
- items:
- $ref: "#/components/schemas/ExecutionSummary"
-
- ExecutionSummary:
- description: "Execution summary"
- type: object
- required:
- - id
- - name
- - testName
- - testType
- - status
- properties:
- id:
- type: string
- description: execution id
- format: bson objectId
- example: "62f395e004109209b50edfc4"
- name:
- type: string
- description: execution name
- example: "test-suite1-test1"
- number:
- type: integer
- description: execution number
- example: 1
- testName:
- type: string
- description: name of the test
- example: "test1"
- testNamespace:
- type: string
- description: name of the test
- example: "testkube"
- testType:
- type: string
- description: the type of test for this execution
- example: "postman/collection"
- status:
- $ref: "#/components/schemas/ExecutionStatus"
- startTime:
- type: string
- description: "test execution start time"
- format: date-time
- endTime:
+ jobTemplate:
+ description: Job template to launch executor
type: string
- description: "test execution end time"
- format: date-time
- duration:
+ jobTemplateReference:
type: string
- description: calculated test duration
- example: "00:00:13"
- durationMs:
- type: integer
- description: calculated test duration in ms
- example: 10000
+ description: name of the template resource
labels:
type: object
- description: "test and execution labels"
+ description: "executor labels"
additionalProperties:
type: string
example:
env: "prod"
app: "backend"
- runningContext:
- $ref: "#/components/schemas/RunningContext"
- description: running context for the test execution
-
- ExecutionStatus:
- type: string
- enum:
- - queued
- - running
- - passed
- - failed
- - aborted
- - timeout
- - skipped
-
- ExecutionResult:
- description: execution result returned from executor
- type: object
- required:
- - status
- properties:
- status:
- $ref: "#/components/schemas/ExecutionStatus"
- output:
- type: string
- description: "RAW Test execution output, depends of reporter used in particular tool"
- outputType:
- type: string
- description: "output type depends of reporter used in particular tool"
- enum:
- - text/plain
- - application/junit+xml
- - application/json
- errorMessage:
- type: string
- description: "error message when status is error, separate to output as output can be partial in case of error"
- steps:
+ features:
+ description: Available executor features
type: array
items:
- $ref: "#/components/schemas/ExecutionStepResult"
- description: execution steps (for collection of requests)
- reports:
- type: object
- properties:
- junit:
- type: string
+ type: string
+ enum:
+ - artifacts
+ - junit-report
+ meta:
+ $ref: "#/components/schemas/ExecutorMeta"
+ useDataDirAsWorkingDir:
+ type: boolean
+ description: use data dir as working dir for executor
- ExecutionStepResult:
- description: execution result data
+ ExecutorDetails:
+ description: Executor details with Executor data and additional information like list of executions
type: object
- required:
- - name
- - status
properties:
name:
+ description: Executor name
type: string
- description: step name
- example: "step1"
- duration:
- type: string
- format: duration
- example: "10m0s"
- status:
- type: string
- description: execution step status
- enum: [passed, failed]
- assertionResults:
- type: array
- items:
- $ref: "#/components/schemas/AssertionResult"
+ executor:
+ $ref: "#/components/schemas/Executor"
+ executions:
+ $ref: "#/components/schemas/ExecutionsResult"
- AssertionResult:
- description: execution result data
+ ExecutorOutput:
+ description: CRD based executor data
type: object
+ required:
+ - type
properties:
- name:
+ type:
type: string
- example: "assertion1"
- status:
+ description: One of possible output types
+ enum:
+ - error
+ - log
+ - event
+ - result
+ content:
type: string
- enum: [passed, failed]
- errorMessage:
+ description: Message/event data passed from executor (like log lines etc)
+ result:
+ $ref: "#/components/schemas/ExecutionResult"
+ description: Execution result when job is finished
+ time:
type: string
- nullable: true
-
- ExecutionsTotals:
- type: object
- description: various execution counters
- required:
- - results
- - passed
- - failed
- - queued
- - running
- properties:
- results:
- type: integer
- description: the total number of executions available
- passed:
- type: integer
- description: the total number of passed executions available
- failed:
- type: integer
- description: the total number of failed executions available
- queued:
- type: integer
- description: the total number of queued executions available
- running:
- type: integer
- description: the total number of running executions available
+ format: date-time
+ description: Timestamp of log
+ example: "2018-03-20T09:12:28Z"
- ServerInfo:
+ LogV2:
+ description: Log format version 2
type: object
- description: Server information with build version, build commit etc.
required:
- - version
+ - logVersion
+ - source
properties:
- version:
- type: string
- description: build version
- example: "v1.4.4"
- commit:
- type: string
- description: build commit
- example: "aaff223ae68aab1af56e8ed8c84c2b80ed63d9b8"
- namespace:
- type: string
- description: server installaton namespace
- example: "my-testkube"
- context:
+ time:
type: string
- description: currently configured testkube API context
- example: "cloud|oss"
- orgId:
+ format: date-time
+ description: Timestamp of log
+ example: "2018-03-20T09:12:28Z"
+ content:
type: string
- description: cloud organization id
- example: "tkcorg_xxxx"
- envId:
+ description: Message/event data passed from executor (like log lines etc)
+ type:
type: string
- description: cloud env id
- example: "tkcenv_xxxx"
- helmchartVersion:
+ description: One of possible log types
+ source:
type: string
- description: helm chart version
- example: "1.4.14"
- dashboardUri:
+ description: One of possible log sources
+ enum:
+ - job-pod
+ - test-scheduler
+ - container-executor
+ - job-executor
+ error:
+ type: boolean
+ description: indicates a log error
+ version:
type: string
- description: dashboard uri
- example: "http://localhost:8080"
-
- Repository:
- description: repository representation for tests in git repositories
+ description: One of possible log versions
+ enum:
+ - v1
+ - v2
+ metadata:
+ type: object
+ description: additional log details
+ additionalProperties:
+ type: string
+ example:
+ argsl: "passed command arguments"
+ v1:
+ $ref: "#/components/schemas/LogV1"
+ description: Old output - for backwards compatibility - will be removed for non-structured logs
+
+ LogV1:
+ description: Log format version 1
type: object
required:
- type
- - uri
properties:
- type:
- type: string
- enum:
- - git
- description: VCS repository type
- uri:
- type: string
- description: uri of content file or git directory
- example: "https://github.com/kubeshop/testkube"
- branch:
- type: string
- description: branch/tag name for checkout
- example: "main"
- commit:
- type: string
- description: commit id (sha) for checkout
- example: "b928cbb7186944ab9275937ec1ac3d3738ca2e1d"
- path:
- type: string
- description: if needed we can checkout particular path (dir or file) in case of BIG/mono repositories
- example: "test/perf"
- username:
- type: string
- description: git auth username for private repositories
- token:
- type: string
- description: git auth token for private repositories
- usernameSecret:
- $ref: "#/components/schemas/SecretRef"
- tokenSecret:
- $ref: "#/components/schemas/SecretRef"
- certificateSecret:
- type: string
- description: secret with certificate for private repositories. Should contain one key ending with .crt such as "mycorp.crt", whose value is the certificate file content, suitable for git config http.sslCAInfo
- workingDir:
- type: string
- description: if provided we checkout the whole repository and run test from this directory
- example: "/"
- authType:
- type: string
- enum:
- - basic
- - header
- description: auth type for git requests
+ result:
+ $ref: "#/components/schemas/ExecutionResult"
+ description: output for previous log format
- RepositoryParameters:
- description: repository parameters for tests in git repositories
+ ExecutorMeta:
+ description: Executor meta data
type: object
properties:
- branch:
- type: string
- description: branch/tag name for checkout
- example: "main"
- commit:
- type: string
- description: commit id (sha) for checkout
- example: "b928cbb7186944ab9275937ec1ac3d3738ca2e1d"
- path:
+ iconURI:
+ description: URI for executor icon
type: string
- description: if needed we can checkout particular path (dir or file) in case of BIG/mono repositories
- example: "test/perf"
- workingDir:
+ example: /assets/k6.jpg
+ docsURI:
+ description: URI for executor docs
type: string
- description: if provided we checkout the whole repository and run test from this directory
- example: "/"
+ example: https://docs.testkube.io/test-types/executor-k6
+ tooltips:
+ type: object
+ description: executor tooltips
+ additionalProperties:
+ type: string
+ example:
+ general: "please provide k6 test script for execution"
- RepositoryUpdate:
- description: repository update body
+ ExecutorMetaUpdate:
+ description: Executor meta update data
type: object
nullable: true
allOf:
- - $ref: "#/components/schemas/Repository"
+ - $ref: "#/components/schemas/ExecutorMeta"
- RepositoryUpdateParameters:
- description: repository update parameters for tests in git repositories
+ SlavesMeta:
+ description: Slave data for executing tests in distributed environment
type: object
- nullable: true
- allOf:
- - $ref: "#/components/schemas/RepositoryParameters"
+ properties:
+ image:
+ description: slave image
+ type: string
+ example: kubeshop/ex-slaves-image:latest
+ required:
+ - image
- ArtifactRequest:
- description: artifact request body with test artifacts
+ RunningContext:
+ description: running context for test or test suite execution
type: object
+ required:
+ - type
properties:
- storageClassName:
+ type:
type: string
- description: artifact storage class name for container executor
- example: artifact-volume-local
- volumeMountPath:
+ description: One of possible context types
+ enum:
+ - userCLI
+ - userUI
+ - testsuite
+ - testtrigger
+ - scheduler
+ context:
type: string
- description: artifact volume mount path for container executor
- dirs:
+ description: Context value depending from its type
+
+ Webhook:
+ description: CRD based webhook data
+ type: object
+ required:
+ - uri
+ - events
+ properties:
+ name:
+ type: string
+ example: "webhook1"
+ namespace:
+ type: string
+ example: "testkube"
+ uri:
+ type: string
+ example: "https://hooks.app.com/services/1"
+ events:
type: array
items:
+ $ref: "#/components/schemas/EventType"
+ selector:
+ type: string
+ description: Labels to filter for tests and test suites
+ payloadObjectField:
+ type: string
+ description: will load the generated payload for notification inside the object
+ payloadTemplate:
+ type: string
+ description: golang based template for notification payload
+ payloadTemplateReference:
+ type: string
+ description: name of the template resource
+ headers:
+ type: object
+ description: "webhook headers (golang template supported)"
+ additionalProperties:
type: string
- description: artifact directories for scraping
- masks:
- type: array
- items:
+ example:
+ Content-Type: "application/xml"
+ labels:
+ type: object
+ description: "webhook labels"
+ additionalProperties:
type: string
- description: regexp to filter scraped artifacts, single or comma separated
- storageBucket:
- type: string
- description: artifact bucket storage
- example: test1-artifacts
- omitFolderPerExecution:
- type: boolean
- description: don't use a separate folder for execution artifacts
- sharedBetweenPods:
- type: boolean
- description: whether to share volume between pods
-
- ArtifactUpdateRequest:
- description: artifact request update body
- type: object
- nullable: true
- allOf:
- - $ref: "#/components/schemas/ArtifactRequest"
+ example:
+ env: "prod"
+ app: "backend"
- PodRequest:
- description: pod request body
+ Event:
+ description: Event data
type: object
+ required:
+ - type
+ - id
+ - resourceId
+ - resource
properties:
- resources:
- $ref: "#/components/schemas/PodResourcesRequest"
- description: pod resources request parameters
- podTemplate:
+ id:
type: string
- description: pod template extensions
- podTemplateReference:
+ description: UUID of event
+ streamTopic:
+ type: string
+ description: stream topic
+ resource:
+ $ref: "#/components/schemas/EventResource"
+ resourceId:
+ type: string
+ description: ID of resource
+ type:
+ $ref: "#/components/schemas/EventType"
+ testExecution:
+ $ref: "#/components/schemas/Execution"
+ testSuiteExecution:
+ $ref: "#/components/schemas/TestSuiteExecution"
+ testWorkflowExecution:
+ $ref: "#/components/schemas/TestWorkflowExecution"
+ clusterName:
type: string
- description: name of the template resource
+ description: cluster name of event
+ envs:
+ type: object
+ description: "environment variables"
+ additionalProperties:
+ type: string
+ example:
+ WEBHOOK_PARAMETER: "any value"
- PodUpdateRequest:
- description: pod request update body
+ EventResource:
+ type: string
+ enum:
+ - test
+ - testsuite
+ - executor
+ - trigger
+ - webhook
+ - testexecution
+ - testsuiteexecution
+ - testsource
+ - testworkflow
+ - testworkflowexecution
+
+ EventType:
+ type: string
+ enum:
+ - start-test
+ - end-test-success
+ - end-test-failed
+ - end-test-aborted
+ - end-test-timeout
+ - start-testsuite
+ - end-testsuite-success
+ - end-testsuite-failed
+ - end-testsuite-aborted
+ - end-testsuite-timeout
+ - queue-testworkflow
+ - start-testworkflow
+ - end-testworkflow-success
+ - end-testworkflow-failed
+ - end-testworkflow-aborted
+ - created
+ - updated
+ - deleted
+
+ EventResult:
+ description: Listener result after sending particular event
type: object
- nullable: true
- allOf:
- - $ref: "#/components/schemas/PodRequest"
+ required:
+ - type
+ properties:
+ id:
+ type: string
+ description: UUID of event
+ error:
+ type: string
+ description: error message if any
+ result:
+ type: string
+ format: error
+ description: result of event
- PodResourcesRequest:
- description: pod resources request specification
+ TestWithExecution:
+ description: Test with latest Execution result
type: object
+ required:
+ - test
properties:
- requests:
- $ref: "#/components/schemas/ResourceRequest"
- description: pod resources requests
- limits:
- $ref: "#/components/schemas/ResourceRequest"
- description: pod resources limits
+ test:
+ $ref: "#/components/schemas/Test"
+ latestExecution:
+ $ref: "#/components/schemas/Execution"
- PodResourcesUpdateRequest:
- description: pod resources update request specification
+ TestWithExecutionSummary:
+ description: Test with latest Execution result summary
type: object
- nullable: true
- allOf:
- - $ref: "#/components/schemas/PodResourcesRequest"
+ required:
+ - test
+ properties:
+ test:
+ $ref: "#/components/schemas/Test"
+ latestExecution:
+ $ref: "#/components/schemas/ExecutionSummary"
- ResourceRequest:
- description: resource request specification
+ TestSuiteWithExecution:
+ description: Test suite with latest execution result
type: object
+ required:
+ - testSuite
properties:
- cpu:
- type: string
- description: requested cpu units
- example: 250m
- memory:
- type: string
- description: requested memory units
- example: 64Mi
+ testSuite:
+ $ref: "#/components/schemas/TestSuite"
+ latestExecution:
+ $ref: "#/components/schemas/TestSuiteExecution"
- ResourceUpdateRequest:
- description: resource update request specification
+ TestSuiteWithExecutionSummary:
+ description: Test suite with latest execution result
type: object
- nullable: true
- allOf:
- - $ref: "#/components/schemas/ResourceRequest"
+ required:
+ - testSuite
+ properties:
+ testSuite:
+ $ref: "#/components/schemas/TestSuite"
+ latestExecution:
+ $ref: "#/components/schemas/TestSuiteExecutionSummary"
- ExecutionRequest:
- description: test execution request body
+ Config:
+ description: Testkube API config data structure
type: object
+ required:
+ - id
+ - clusterId
+ - enableTelemetry
properties:
id:
type: string
- description: execution id
- format: bson objectId
- example: "62f395e004109209b50edfc1"
- name:
- type: string
- description: test execution custom name
- example: "testing with 1000 users"
- testSuiteName:
- type: string
- description: unique test suite name (CRD Test suite name), if it's run as a part of test suite
- example: "test-suite1"
- number:
- type: integer
- description: test execution number
- executionLabels:
- type: object
- description: "test execution labels"
- additionalProperties:
- type: string
- example:
- users: "3"
- prefix: "some-"
- namespace:
+ clusterId:
type: string
- description: test kubernetes namespace ("testkube" when not set)
- example: testkube
- isVariablesFileUploaded:
+ enableTelemetry:
type: boolean
- description: in case the variables file is too big, it will be uploaded
- example: false
- variablesFile:
+
+ DebugInfo:
+ description: Testkube debug info
+ type: object
+ properties:
+ clientVersion:
type: string
- description: variables file content - need to be in format for particular executor (e.g. postman envs file)
- variables:
- $ref: "#/components/schemas/Variables"
- testSecretUUID:
+ example: "1.4.9"
+ serverVersion:
type: string
- description: test secret uuid
- readOnly: true
- example: "7934600f-b367-48dd-b981-4353304362fb"
- testSuiteSecretUUID:
+ example: "v1.4.9"
+ clusterVersion:
type: string
- description: test suite secret uuid, if it's run as a part of test suite
- readOnly: true
- example: "7934600f-b367-48dd-b981-4353304362fb"
- command:
- type: array
- description: "executor image command"
- items:
- type: string
- example:
- - "curl"
- args:
+ example: "v1.23.4"
+ apiLogs:
type: array
- description: "additional executor binary arguments"
items:
type: string
- example:
- - "--repeats"
- - "5"
- - "--insecure"
- args_mode:
- type: string
- description: usage mode for arguments
- enum:
- - append
- - override
- image:
- type: string
- description: container image, executor will run inside this image
- example: kubeshop/testkube-executor-custom:1.10.11-dev-0a9c91
- imagePullSecrets:
+ example: ["logline1", "logline2", "logline3"]
+ operatorLogs:
type: array
- description: "container image pull secrets"
items:
- $ref: "#/components/schemas/LocalObjectReference"
- envs:
- deprecated: true
- type: object
- description: "Environment variables passed to executor. Deprecated: use Basic Variables instead"
- additionalProperties:
type: string
- example:
- record: "true"
- prefix: "some-"
- secretEnvs:
- deprecated: true
+ example: ["logline1", "logline2", "logline3"]
+ executionLogs:
type: object
- description: "Execution variables passed to executor from secrets. Deprecated: use Secret Variables instead"
additionalProperties:
- type: string
- example:
- secret_key_name1: "secret-name"
- secret_Key_name2: "secret-name"
- sync:
+ type: array
+ items:
+ type: string
+ example: ["logline1", "logline2", "logline3"]
+
+ Features:
+ type: object
+ required:
+ - logsV2
+ properties:
+ logsV2:
type: boolean
- description: whether to start execution sync or async
- httpProxy:
+ description: Log processing version 2
+
+ TestTrigger:
+ type: object
+ required:
+ - resource
+ - resourceSelector
+ - event
+ - action
+ - execution
+ - testSelector
+ properties:
+ name:
type: string
- description: http proxy for executor containers
- example: user:pass@my.proxy.server:8080
- httpsProxy:
+ description: test trigger name
+ example: "test1"
+ namespace:
type: string
- description: https proxy for executor containers
- example: user:pass@my.proxy.server:8081
- negativeTest:
- type: boolean
- description: whether to run test as negative test
- example: false
- isNegativeTestChangedOnRun:
- type: boolean
- description: whether negativeTest was changed by user
- example: false
- activeDeadlineSeconds:
- type: integer
- format: int64
- description: duration in seconds the test may be active, until its stopped
- example: 1
- uploads:
- type: array
- items:
+ description: test trigger namespace
+ example: "testkube"
+ labels:
+ type: object
+ description: "test trigger labels"
+ additionalProperties:
type: string
- description: list of file paths that need to be copied into the test from uploads
example:
- - settings/config.txt
- bucketName:
- type: string
- description: minio bucket name to get uploads from
- example: execution-c01d7cf6-ec3f-47f0-9556-a5d6e9009a43
- artifactRequest:
- $ref: "#/components/schemas/ArtifactRequest"
- description: configuration parameters for storing test artifacts
- jobTemplate:
- type: string
- description: job template extensions
- jobTemplateReference:
- type: string
- description: name of the template resource
- cronJobTemplate:
- type: string
- description: cron job template extensions
- cronJobTemplateReference:
- type: string
- description: name of the template resource
- contentRequest:
- $ref: "#/components/schemas/TestContentRequest"
- description: adjusting parameters for test content
- preRunScript:
- type: string
- description: script to run before test execution
- example: "echo -n '$SECRET_ENV' > ./secret_file"
- postRunScript:
- type: string
- description: script to run after test execution
- example: "sleep 30"
- executePostRunScriptBeforeScraping:
- type: boolean
- description: execute post run script before scraping (prebuilt executor only)
- scraperTemplate:
- type: string
- description: scraper template extensions
- scraperTemplateReference:
- type: string
- description: name of the template resource
- pvcTemplate:
- type: string
- description: pvc template extensions
- pvcTemplateReference:
+ env: "prod"
+ app: "backend"
+ resource:
+ $ref: "#/components/schemas/TestTriggerResources"
+ resourceSelector:
+ $ref: "#/components/schemas/TestTriggerSelector"
+ event:
type: string
- description: name of the template resource
- envConfigMaps:
- type: array
- description: "config map references"
- items:
- $ref: "#/components/schemas/EnvReference"
- envSecrets:
- type: array
- description: "secret references"
- items:
- $ref: "#/components/schemas/EnvReference"
- runningContext:
- $ref: "#/components/schemas/RunningContext"
- description: running context for the test execution
- testExecutionName:
+ description: listen for event for selected resource
+ example: modified
+ conditionSpec:
+ $ref: "#/components/schemas/TestTriggerConditionSpec"
+ probeSpec:
+ $ref: "#/components/schemas/TestTriggerProbeSpec"
+ action:
+ $ref: "#/components/schemas/TestTriggerActions"
+ execution:
+ $ref: "#/components/schemas/TestTriggerExecutions"
+ testSelector:
+ $ref: "#/components/schemas/TestTriggerSelector"
+ concurrencyPolicy:
+ $ref: "#/components/schemas/TestTriggerConcurrencyPolicies"
+
+ LocalObjectReference:
+ description: Reference to Kubernetes object
+ type: object
+ properties:
+ name:
type: string
- description: test execution name started the test execution
- downloadArtifactExecutionIDs:
- type: array
- description: "execution ids for artifacts to download"
- items:
- type: string
- downloadArtifactTestNames:
- type: array
- description: "test names for artifacts to download from latest executions"
- items:
- type: string
- slavePodRequest:
- $ref: "#/components/schemas/PodRequest"
- description: configuration parameters for executed slave pods
- ExecutionUpdateRequest:
- description: test execution request update body
+ EnvReference:
+ description: Reference to env resource
type: object
- nullable: true
- allOf:
- - $ref: "#/components/schemas/ExecutionRequest"
+ required:
+ - reference
+ properties:
+ reference:
+ $ref: "#/components/schemas/LocalObjectReference"
+ mount:
+ type: boolean
+ description: whether we shoud mount resource
+ example: /etc/data
+ mountPath:
+ type: string
+ description: where we shoud mount resource
+ mapToVariables:
+ type: boolean
+ description: whether we shoud map to variables from resource
+ default: false
- TestSuiteExecutionRequest:
- description: test suite execution request body
+ TestTriggerSelector:
type: object
properties:
name:
type: string
- description: test execution custom name
- example: "testing with 1000 users"
- number:
- type: integer
- description: test suite execution number
- example: 1
+ description: kubernetes resource name selector
+ example: nginx
+ nameRegex:
+ type: string
+ description: kubernetes resource name regex
+ example: nginx.*
namespace:
type: string
- description: test kubernetes namespace ("testkube" when not set)
+ description: resource namespace
example: testkube
- variables:
- $ref: "#/components/schemas/Variables"
- secretUUID:
- type: string
- description: secret uuid
- readOnly: true
- example: "7934600f-b367-48dd-b981-4353304362fb"
- labels:
- type: object
- description: "test suite labels"
- additionalProperties:
- type: string
- example:
- users: "3"
- prefix: "some-"
- executionLabels:
- type: object
- description: "execution labels"
- additionalProperties:
- type: string
- example:
- users: "3"
- prefix: "some-"
- sync:
- type: boolean
- description: whether to start execution sync or async
- httpProxy:
- type: string
- description: http proxy for executor containers
- example: user:pass@my.proxy.server:8080
- httpsProxy:
- type: string
- description: https proxy for executor containers
- example: user:pass@my.proxy.server:8081
+ labelSelector:
+ $ref: "https://raw.githubusercontent.com/garethr/kubernetes-json-schema/master/v1.7.8/_definitions.json#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector"
+ description: label selector for Kubernetes resources
+
+ TestTriggerResources:
+ description: supported kubernetes resources for test triggers
+ type: string
+ enum:
+ - pod
+ - deployment
+ - statefulset
+ - daemonset
+ - service
+ - ingress
+ - event
+ - configmap
+
+ TestTriggerExecutions:
+ description: supported test resources for test triggers
+ type: string
+ enum:
+ - test
+ - testsuite
+
+ TestTriggerActions:
+ description: supported actions for test triggers
+ type: string
+ enum:
+ - run
+
+ TestTriggerConditionSpec:
+ type: object
+ properties:
+ conditions:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestTriggerCondition"
+ description: list of test trigger conditions
timeout:
type: integer
format: int32
- description: duration in seconds the test suite may be active, until its stopped
+ description: duration in seconds the test trigger waits for conditions, until its stopped
example: 1
- contentRequest:
- $ref: "#/components/schemas/TestContentRequest"
- description: adjusting parameters for test content
- runningContext:
- $ref: "#/components/schemas/RunningContext"
- description: running context for the test suite execution
- jobTemplate:
- type: string
- description: job template extensions
- jobTemplateReference:
- type: string
- description: name of the template resource
- cronJobTemplate:
- type: string
- description: cron job template extensions
- cronJobTemplateReference:
- type: string
- description: name of the template resource
- scraperTemplate:
- type: string
- description: scraper template extensions
- scraperTemplateReference:
- type: string
- description: name of the template resource
- pvcTemplate:
+ delay:
+ type: integer
+ format: int32
+ description: duration in seconds the test trigger waits between condition checks
+ example: 1
+
+ TestTriggerCondition:
+ description: supported condition for test triggers
+ type: object
+ required:
+ - status
+ - type
+ properties:
+ status:
+ $ref: "#/components/schemas/TestTriggerConditionStatuses"
+ type:
type: string
- description: pvc template extensions
- pvcTemplateReference:
+ description: test trigger condition
+ example: Progressing
+ reason:
type: string
- description: name of the template resource
- concurrencyLevel:
+ description: test trigger condition reason
+ example: NewReplicaSetAvailable
+ ttl:
type: integer
format: int32
- description: number of tests run in parallel
- example: 10
- testSuiteExecutionName:
- type: string
- description: test suite execution name started the test suite execution
+ description: duration in seconds in the past from current time when the condition is still valid
+ example: 1
- TestSuiteExecutionUpdateRequest:
- description: test suite execution update request body
- type: object
- nullable: true
- allOf:
- - $ref: "#/components/schemas/TestSuiteExecutionRequest"
+ TestTriggerConditionStatuses:
+ description: supported kubernetes condition statuses for test triggers
+ type: string
+ enum:
+ - "True"
+ - "False"
+ - "Unknown"
- TestUpsertRequest:
- description: test create request body
+ TestTriggerProbeSpec:
type: object
- allOf:
- - $ref: "#/components/schemas/Test"
+ properties:
+ probes:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestTriggerProbe"
+ description: list of test trigger probes
+ timeout:
+ type: integer
+ format: int32
+ description: duration in seconds the test trigger waits for probes, until its stopped
+ example: 1
+ delay:
+ type: integer
+ format: int32
+ description: duration in seconds the test trigger waits between probes
+ example: 1
- TestUpdateRequest:
- description: test update request body
+ TestTriggerProbe:
+ description: supported probe for test triggers
type: object
- nullable: true
- allOf:
- - $ref: "#/components/schemas/Test"
+ properties:
+ scheme:
+ type: string
+ description: test trigger condition probe scheme to connect to host, default is http
+ example: http
+ host:
+ type: string
+ description: test trigger condition probe host, default is pod ip or service name
+ example: testkube-api-server
+ path:
+ type: string
+ description: test trigger condition probe path to check, default is /
+ example: /
+ port:
+ type: integer
+ format: int32
+ description: test trigger condition probe port to connect
+ example: 80
+ headers:
+ type: object
+ description: test trigger condition probe headers to submit
+ additionalProperties:
+ type: string
+ example:
+ Content-Type: "application/xml"
+
+ TestTriggerConcurrencyPolicies:
+ description: supported concurrency policies for test triggers
+ type: string
+ enum:
+ - allow
+ - forbid
+ - replace
- TestSuiteUpsertRequest:
- description: test suite create request body
+ TestTriggerKeyMap:
type: object
required:
- - name
- - namespace
- allOf:
- - $ref: "#/components/schemas/TestSuite"
- - $ref: "#/components/schemas/ObjectRef"
+ - resources
+ - actions
+ - executions
+ - events
+ - concurrencyPolicies
+ properties:
+ resources:
+ type: array
+ items:
+ type: string
+ description: list of supported values for resources
+ example:
+ [
+ "pod",
+ "deployment",
+ "statefulset",
+ "daemonset",
+ "service",
+ "ingress",
+ "event",
+ "configmap",
+ ]
+ actions:
+ type: array
+ items:
+ type: string
+ description: list of supported values for actions
+ example: ["run"]
+ executions:
+ type: array
+ items:
+ type: string
+ description: list of supported values for executions
+ example: ["test", "testsuite"]
+ events:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ type: string
+ description: mapping between resources and supported events
+ example:
+ {
+ "pod": ["created", "modified", "deleted"],
+ "deployment": ["created", "modified", "deleted"],
+ }
+ conditions:
+ type: array
+ items:
+ type: string
+ description: list of supported values for conditions
+ example: ["Available", "Progressing"]
+ concurrencyPolicies:
+ type: array
+ items:
+ type: string
+ description: list of supported values for concurrency policies
+ example: ["allow", "forbid", "replace"]
- TestSuiteUpsertRequestV2:
- description: test suite create request body
+ TestSourceBatchRequest:
+ description: Test source batch request
type: object
required:
- - name
- - namespace
- allOf:
- - $ref: "#/components/schemas/TestSuiteV2"
- - $ref: "#/components/schemas/ObjectRef"
-
- TestSuiteUpdateRequest:
- description: test suite update body
- type: object
- nullable: true
- allOf:
- - $ref: "#/components/schemas/TestSuite"
- - $ref: "#/components/schemas/ObjectRef"
-
- TestSuiteUpdateRequestV2:
- description: test suite update body
- type: object
- nullable: true
- allOf:
- - $ref: "#/components/schemas/TestSuiteV2"
- - $ref: "#/components/schemas/ObjectRef"
+ - batch
+ properties:
+ batch:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestSourceUpsertRequest"
- TestTriggerUpsertRequest:
- description: test trigger create or update request body
+ TestSourceBatchResult:
+ description: Test source batch result
type: object
- required:
- - resource
- - resourceSelector
- - event
- - action
- - execution
- - testSelector
- allOf:
- - $ref: "#/components/schemas/TestTrigger"
- - $ref: "#/components/schemas/ObjectRef"
+ properties:
+ created:
+ type: array
+ items:
+ type: string
+ description: created test sources
+ example: ["name1", "name2", "name3"]
+ updated:
+ type: array
+ items:
+ type: string
+ description: updated test sources
+ example: ["name4", "name5", "name6"]
+ deleted:
+ type: array
+ items:
+ type: string
+ description: deleted test sources
+ example: ["name7", "name8", "name9"]
- ExecutorUpsertRequest:
- description: executor create request body
+ Template:
+ description: Golang based template
type: object
required:
- name
- - namespace
- - types
- allOf:
- - $ref: "#/components/schemas/Executor"
- - $ref: "#/components/schemas/ObjectRef"
+ - type
+ - body
+ properties:
+ name:
+ type: string
+ description: template name for reference
+ example: "webhook-template"
+ namespace:
+ type: string
+ description: template namespace
+ example: "testkube"
+ type:
+ $ref: "#/components/schemas/TemplateType"
+ body:
+ type: string
+ description: template body to use
+ example: '{"id": "{{ .Id }}"}'
+ labels:
+ type: object
+ description: "template labels"
+ additionalProperties:
+ type: string
+ example:
+ env: "prod"
+ app: "backend"
- ExecutorUpdateRequest:
- description: executor update request body
- type: object
- nullable: true
- allOf:
- - $ref: "#/components/schemas/Executor"
- - $ref: "#/components/schemas/ObjectRef"
+ TemplateType:
+ description: template type by purpose
+ type: string
+ enum:
+ - job
+ - container
+ - cronjob
+ - scraper
+ - pvc
+ - webhook
+ - pod
- WebhookCreateRequest:
- description: webhook create request body
+ TemplateCreateRequest:
+ description: template create request body
type: object
allOf:
- - $ref: "#/components/schemas/Webhook"
+ - $ref: "#/components/schemas/Template"
- WebhookUpdateRequest:
- description: webhook update request body
+ TemplateUpdateRequest:
+ description: template update request body
type: object
nullable: true
allOf:
- - $ref: "#/components/schemas/Webhook"
+ - $ref: "#/components/schemas/Template"
- # Copied from CRD spec
- # https://github.com/kubeshop/testkube-operator/blob/main/config/crd/bases/executor.testkube.io_executors.yaml
- # TODO we need to sync those in some nice way
- Executor:
- description: CRD based executor data
+ Secret:
+ description: Secret with keys
type: object
+ required:
+ - name
properties:
- executorType:
- description:
- ExecutorType one of "rest" for rest openapi based executors
- or "job" which will be default runners for testkube soon
- type: string
- image:
- description: Image for kube-job
+ name:
type: string
- slaves:
- $ref: "#/components/schemas/SlavesMeta"
- imagePullSecrets:
- type: array
- description: "container image pull secrets"
- items:
- $ref: "#/components/schemas/LocalObjectReference"
- command:
- type: array
- description: "executor image command"
- items:
- type: string
- example:
- - "curl"
- args:
- type: array
- description: "additional executor binary argument"
- items:
- type: string
- example:
- - "--repeats"
- - "5"
- - "--insecure"
- types:
- description: Types defines what types can be handled by executor e.g.
- "postman/collection", ":curl/command" etc
- items:
- type: string
+ description: secret name
+ example: "git-secret"
+ keys:
type: array
- uri:
- description: URI for rest based executors
- type: string
- contentTypes:
- description: list of handled content types
+ description: secret keys
items:
type: string
- type: array
- jobTemplate:
- description: Job template to launch executor
+ example: ["key1", "key2", "key3"]
+
+ TestWorkflow:
+ type: object
+ properties:
+ name:
type: string
- jobTemplateReference:
+ description: kubernetes resource name
+ namespace:
type: string
- description: name of the template resource
+ description: kubernetes namespace
+ description:
+ type: string
+ description: human-readable description
labels:
type: object
- description: "executor labels"
+ description: "test workflow labels"
additionalProperties:
type: string
example:
env: "prod"
app: "backend"
- features:
- description: Available executor features
- type: array
- items:
+ annotations:
+ type: object
+ description: "test workflow annotations"
+ additionalProperties:
type: string
- enum:
- - artifacts
- - junit-report
- meta:
- $ref: "#/components/schemas/ExecutorMeta"
- useDataDirAsWorkingDir:
- type: boolean
- description: use data dir as working dir for executor
+ created:
+ type: string
+ format: date-time
+ example: "2022-07-30T06:54:15Z"
+ spec:
+ $ref: "#/components/schemas/TestWorkflowSpec"
- ExecutorDetails:
- description: Executor details with Executor data and additional information like list of executions
+ TestWorkflowExecutionRequest:
type: object
properties:
name:
- description: Executor name
type: string
- executor:
- $ref: "#/components/schemas/Executor"
- executions:
- $ref: "#/components/schemas/ExecutionsResult"
+ description: custom execution name
+ config:
+ $ref: "#/components/schemas/TestWorkflowConfigValue"
- ExecutorOutput:
- description: CRD based executor data
+ TestWorkflowWithExecution:
type: object
- required:
- - type
properties:
- type:
- type: string
- description: One of possible output types
- enum:
- - error
- - log
- - event
- - result
- content:
- type: string
- description: Message/event data passed from executor (like log lines etc)
- result:
- $ref: "#/components/schemas/ExecutionResult"
- description: Execution result when job is finished
- time:
- type: string
- format: date-time
- description: Timestamp of log
- example: "2018-03-20T09:12:28Z"
+ workflow:
+ $ref: "#/components/schemas/TestWorkflow"
+ latestExecution:
+ $ref: "#/components/schemas/TestWorkflowExecution"
- ExecutorMeta:
- description: Executor meta data
+ TestWorkflowWithExecutionSummary:
type: object
properties:
- iconURI:
- description: URI for executor icon
- type: string
- example: /assets/k6.jpg
- docsURI:
- description: URI for executor docs
- type: string
- example: https://docs.testkube.io/test-types/executor-k6
- tooltips:
- type: object
- description: executor tooltips
- additionalProperties:
- type: string
- example:
- general: "please provide k6 test script for execution"
-
- ExecutorMetaUpdate:
- description: Executor meta update data
- type: object
- nullable: true
- allOf:
- - $ref: "#/components/schemas/ExecutorMeta"
+ workflow:
+ $ref: "#/components/schemas/TestWorkflow"
+ latestExecution:
+ $ref: "#/components/schemas/TestWorkflowExecutionSummary"
- SlavesMeta:
- description: Slave data for executing tests in distributed environment
+ TestWorkflowExecutionsResult:
type: object
properties:
- image:
- description: slave image
- type: string
- example: kubeshop/ex-slaves-image:latest
+ totals:
+ $ref: "#/components/schemas/ExecutionsTotals"
+ filtered:
+ $ref: "#/components/schemas/ExecutionsTotals"
+ results:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflowExecutionSummary"
required:
- - image
+ - totals
+ - filtered
+ - results
- RunningContext:
- description: running context for test or test suite execution
+ TestWorkflowExecution:
type: object
- required:
- - type
properties:
- type:
- type: string
- description: One of possible context types
- enum:
- - userCLI
- - userUI
- - testsuite
- - testtrigger
- - scheduler
- context:
+ id:
type: string
- description: Context value depending from its type
-
- Webhook:
- description: CRD based webhook data
- type: object
- required:
- - uri
- - events
- properties:
+ description: unique execution identifier
+ format: bson objectId
+ example: "62f395e004109209b50edfc1"
name:
type: string
- example: "webhook1"
- namespace:
+ description: execution name
+ example: "some-workflow-name-1"
+ number:
+ type: integer
+ description: sequence number for the execution
+ scheduledAt:
type: string
- example: "testkube"
- uri:
+ format: date-time
+ description: when the execution has been scheduled to run
+ statusAt:
type: string
- example: "https://hooks.app.com/services/1"
- events:
+ format: date-time
+ description: when the execution result's status has changed last time (queued, passed, failed)
+ signature:
type: array
+ description: structured tree of steps
items:
- $ref: "#/components/schemas/EventType"
- selector:
+ $ref: "#/components/schemas/TestWorkflowSignature"
+ result:
+ $ref: "#/components/schemas/TestWorkflowResult"
+ output:
+ type: array
+ description: additional information from the steps, like referenced executed tests or artifacts
+ items:
+ $ref: "#/components/schemas/TestWorkflowOutput"
+ workflow:
+ $ref: "#/components/schemas/TestWorkflow"
+ resolvedWorkflow:
+ $ref: "#/components/schemas/TestWorkflow"
+ required:
+ - id
+ - name
+ - workflow
+
+ TestWorkflowExecutionSummary:
+ type: object
+ properties:
+ id:
type: string
- description: Labels to filter for tests and test suites
- payloadObjectField:
+ description: unique execution identifier
+ format: bson objectId
+ example: "62f395e004109209b50edfc1"
+ name:
type: string
- description: will load the generated payload for notification inside the object
- payloadTemplate:
+ description: execution name
+ example: "some-workflow-name-1"
+ number:
+ type: integer
+ description: sequence number for the execution
+ scheduledAt:
type: string
- description: golang based template for notification payload
- payloadTemplateReference:
+ format: date-time
+ description: when the execution has been scheduled to run
+ statusAt:
type: string
- description: name of the template resource
- headers:
- type: object
- description: "webhook headers (golang template supported)"
- additionalProperties:
- type: string
- example:
- Content-Type: "application/xml"
- labels:
- type: object
- description: "webhook labels"
- additionalProperties:
- type: string
- example:
- env: "prod"
- app: "backend"
-
- Event:
- description: Event data
- type: object
+ format: date-time
+ description: when the execution result's status has changed last time (queued, passed, failed)
+ result:
+ $ref: "#/components/schemas/TestWorkflowResultSummary"
+ workflow:
+ $ref: "#/components/schemas/TestWorkflowSummary"
required:
- - type
- id
- - resourceId
- - resource
+ - name
+ - workflow
+
+ TestWorkflowSummary:
+ type: object
properties:
- id:
- type: string
- description: UUID of event
- resource:
- $ref: "#/components/schemas/EventResource"
- resourceId:
+ name:
type: string
- description: ID of resource
- type:
- $ref: "#/components/schemas/EventType"
- testExecution:
- $ref: "#/components/schemas/Execution"
- testSuiteExecution:
- $ref: "#/components/schemas/TestSuiteExecution"
- clusterName:
+ namespace:
type: string
- description: cluster name of event
- envs:
+ labels:
+ type: object
+ additionalProperties:
+ type: string
+ annotations:
type: object
- description: "environment variables"
additionalProperties:
type: string
- example:
- WEBHOOK_PARAMETER: "any value"
-
- EventResource:
- type: string
- enum:
- - test
- - testsuite
- - executor
- - trigger
- - webhook
- - testexecution
- - testsuiteexecution
- - testsource
-
- EventType:
- type: string
- enum:
- - start-test
- - end-test-success
- - end-test-failed
- - end-test-aborted
- - end-test-timeout
- - start-testsuite
- - end-testsuite-success
- - end-testsuite-failed
- - end-testsuite-aborted
- - end-testsuite-timeout
- - created
- - updated
- - deleted
- EventResult:
- description: Listener result after sending particular event
+ TestWorkflowResultSummary:
type: object
- required:
- - type
properties:
- id:
+ status:
+ $ref: "#/components/schemas/TestWorkflowStatus"
+ predictedStatus:
+ $ref: "#/components/schemas/TestWorkflowStatus"
+ queuedAt:
type: string
- description: UUID of event
- error:
+ format: date-time
+ description: when the pod was created
+ startedAt:
type: string
- description: error message if any
- result:
+ format: date-time
+ description: when the pod has been successfully assigned
+ finishedAt:
type: string
- format: error
- description: result of event
-
- TestWithExecution:
- description: Test with latest Execution result
- type: object
+ format: date-time
+ description: when the pod has been completed
+ duration:
+ type: string
+ description: Go-formatted (human-readable) duration
+ durationMs:
+ type: integer
+ description: Duration in milliseconds
required:
- - test
- properties:
- test:
- $ref: "#/components/schemas/Test"
- latestExecution:
- $ref: "#/components/schemas/Execution"
+ - status
+ - predictedStatus
- TestWithExecutionSummary:
- description: Test with latest Execution result summary
+ TestWorkflowExecutionNotification:
type: object
- required:
- - test
properties:
- test:
- $ref: "#/components/schemas/Test"
- latestExecution:
- $ref: "#/components/schemas/ExecutionSummary"
+ ts:
+ type: string
+ format: date-time
+ description: timestamp for the notification if available
+ result:
+ $ref: "#/components/schemas/TestWorkflowResult"
+ ref:
+ type: string
+ description: step reference, if related to some specific step
+ log:
+ type: string
+ description: log content, if it's just a log. note, that it includes 30 chars timestamp + space
+ output:
+ $ref: "#/components/schemas/TestWorkflowOutput"
- TestSuiteWithExecution:
- description: Test suite with latest execution result
+ TestWorkflowOutput:
type: object
- required:
- - testSuite
properties:
- testSuite:
- $ref: "#/components/schemas/TestSuite"
- latestExecution:
- $ref: "#/components/schemas/TestSuiteExecution"
+ ref:
+ type: string
+ description: step reference
+ name:
+ type: string
+ description: output kind name
+ value:
+ type: object
+ additionalProperties: {}
+ description: value returned
- TestSuiteWithExecutionSummary:
- description: Test suite with latest execution result
+ TestWorkflowResult:
type: object
- required:
- - testSuite
properties:
- testSuite:
- $ref: "#/components/schemas/TestSuite"
- latestExecution:
- $ref: "#/components/schemas/TestSuiteExecutionSummary"
+ status:
+ $ref: "#/components/schemas/TestWorkflowStatus"
+ predictedStatus:
+ $ref: "#/components/schemas/TestWorkflowStatus"
+ queuedAt:
+ type: string
+ format: date-time
+ description: when the pod was created
+ startedAt:
+ type: string
+ format: date-time
+ description: when the pod has been successfully assigned
+ finishedAt:
+ type: string
+ format: date-time
+ description: when the pod has been completed
+ duration:
+ type: string
+ description: Go-formatted (human-readable) duration
+ durationMs:
+ type: integer
+ description: Duration in milliseconds
+ initialization:
+ $ref: "#/components/schemas/TestWorkflowStepResult"
+ steps:
+ type: object
+ additionalProperties:
+ $ref: "#/components/schemas/TestWorkflowStepResult"
+ required:
+ - status
+ - predictedStatus
- Config:
- description: Testkube API config data structure
+ TestWorkflowStepResult:
type: object
- required:
- - id
- - clusterId
- - enableTelemetry
properties:
- id:
+ errorMessage:
type: string
- clusterId:
+ status:
+ $ref: "#/components/schemas/TestWorkflowStepStatus"
+ exitCode:
+ type: number
+ queuedAt:
type: string
- enableTelemetry:
- type: boolean
+ format: date-time
+ description: when the container was created
+ startedAt:
+ type: string
+ format: date-time
+ description: when the container was started
+ finishedAt:
+ type: string
+ format: date-time
+ description: when the container was finished
- DebugInfo:
- description: Testkube debug info
+ TestWorkflowSignature:
type: object
properties:
- clientVersion:
+ ref:
type: string
- example: "1.4.9"
- serverVersion:
+ description: step reference
+ name:
type: string
- example: "v1.4.9"
- clusterVersion:
+ description: step name
+ category:
type: string
- example: "v1.23.4"
- apiLogs:
- type: array
- items:
- type: string
- example: ["logline1", "logline2", "logline3"]
- operatorLogs:
+ description: step category, that may be used as name fallback
+ optional:
+ type: boolean
+ description: is the step/group meant to be optional
+ negative:
+ type: boolean
+ description: is the step/group meant to be negative
+ children:
type: array
items:
- type: string
- example: ["logline1", "logline2", "logline3"]
- executionLogs:
- type: object
- additionalProperties:
- type: array
- items:
- type: string
- example: ["logline1", "logline2", "logline3"]
+ $ref: "#/components/schemas/TestWorkflowSignature"
- TestTrigger:
+ TestWorkflowStatus:
+ type: string
+ enum:
+ - queued
+ - running
+ - passed
+ - failed
+ - aborted
+
+ TestWorkflowStepStatus:
+ type: string
+ enum:
+ - queued
+ - running
+ - passed
+ - failed
+ - timeout
+ - skipped
+ - aborted
+
+ TestWorkflowTemplate:
type: object
- required:
- - resource
- - resourceSelector
- - event
- - action
- - execution
- - testSelector
properties:
name:
type: string
- description: test trigger name
- example: "test1"
+ description: kubernetes resource name
namespace:
type: string
- description: test trigger namespace
- example: "testkube"
+ description: kubernetes namespace
+ description:
+ type: string
+ description: human-readable description
labels:
type: object
- description: "test trigger labels"
+ description: "test workflow labels"
additionalProperties:
type: string
example:
env: "prod"
app: "backend"
- resource:
- $ref: "#/components/schemas/TestTriggerResources"
- resourceSelector:
- $ref: "#/components/schemas/TestTriggerSelector"
- event:
+ annotations:
+ type: object
+ description: "test workflow annotations"
+ additionalProperties:
+ type: string
+ created:
type: string
- description: listen for event for selected resource
- example: modified
- conditionSpec:
- $ref: "#/components/schemas/TestTriggerConditionSpec"
- probeSpec:
- $ref: "#/components/schemas/TestTriggerProbeSpec"
- action:
- $ref: "#/components/schemas/TestTriggerActions"
- execution:
- $ref: "#/components/schemas/TestTriggerExecutions"
- testSelector:
- $ref: "#/components/schemas/TestTriggerSelector"
- concurrencyPolicy:
- $ref: "#/components/schemas/TestTriggerConcurrencyPolicies"
+ format: date-time
+ example: "2022-07-30T06:54:15Z"
+ spec:
+ $ref: "#/components/schemas/TestWorkflowTemplateSpec"
- LocalObjectReference:
- description: Reference to Kubernetes object
+ TestWorkflowSpec:
type: object
properties:
- name:
- type: string
+ use:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflowTemplateRef"
+ config:
+ $ref: "#/components/schemas/TestWorkflowConfigSchema"
+ content:
+ $ref: "#/components/schemas/TestWorkflowContent"
+ container:
+ $ref: "#/components/schemas/TestWorkflowContainerConfig"
+ job:
+ $ref: "#/components/schemas/TestWorkflowJobConfig"
+ pod:
+ $ref: "#/components/schemas/TestWorkflowPodConfig"
+ setup:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflowStep"
+ steps:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflowStep"
+ after:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflowStep"
- EnvReference:
- description: Reference to env resource
+ TestWorkflowTemplateSpec:
type: object
- required:
- - reference
properties:
- reference:
- $ref: "#/components/schemas/LocalObjectReference"
- mount:
- type: boolean
- description: whether we shoud mount resource
- example: /etc/data
- mountPath:
- type: string
- description: where we shoud mount resource
- mapToVariables:
- type: boolean
- description: whether we shoud map to variables from resource
- default: false
+ config:
+ $ref: "#/components/schemas/TestWorkflowConfigSchema"
+ content:
+ $ref: "#/components/schemas/TestWorkflowContent"
+ container:
+ $ref: "#/components/schemas/TestWorkflowContainerConfig"
+ job:
+ $ref: "#/components/schemas/TestWorkflowJobConfig"
+ pod:
+ $ref: "#/components/schemas/TestWorkflowPodConfig"
+ setup:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflowIndependentStep"
+ steps:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflowIndependentStep"
+ after:
+ type: array
+ items:
+ $ref: "#/components/schemas/TestWorkflowIndependentStep"
- TestTriggerSelector:
+ TestWorkflowIndependentStep:
type: object
properties:
name:
type: string
- description: kubernetes resource name selector
- example: nginx
- nameRegex:
+ description: readable name for the step
+ condition:
type: string
- description: kubernetes resource name regex
- example: nginx.*
- namespace:
+ description: expression to declare under which conditions the step should be run; defaults to "passed", except artifacts where it defaults to "always"
+ negative:
+ type: boolean
+ description: is the step expected to fail
+ optional:
+ type: boolean
+ description: is the step optional, so the failure won't affect the TestWorkflow result
+ retry:
+ $ref: "#/components/schemas/TestWorkflowRetryPolicy"
+ timeout:
type: string
- description: resource namespace
- example: testkube
- labelSelector:
- $ref: "https://raw.githubusercontent.com/garethr/kubernetes-json-schema/master/v1.7.8/_definitions.json#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector"
- description: label selector for Kubernetes resources
-
- TestTriggerResources:
- description: supported kubernetes resources for test triggers
- type: string
- enum:
- - pod
- - deployment
- - statefulset
- - daemonset
- - service
- - ingress
- - event
- - configmap
-
- TestTriggerExecutions:
- description: supported test resources for test triggers
- type: string
- enum:
- - test
- - testsuite
-
- TestTriggerActions:
- description: supported actions for test triggers
- type: string
- enum:
- - run
+ pattern: "^((0|[1-9][0-9]*)h)?((0|[1-9][0-9]*)m)?((0|[1-9][0-9]*)s)?((0|[1-9][0-9]*)ms)?$"
+ description: maximum time this step may take
+ delay:
+ type: string
+ pattern: "^((0|[1-9][0-9]*)h)?((0|[1-9][0-9]*)m)?((0|[1-9][0-9]*)s)?((0|[1-9][0-9]*)ms)?$"
+ description: delay before the step
+ content:
+ $ref: "#/components/schemas/TestWorkflowContent"
+ shell:
+ type: string
+ description: script to run in a default shell for the container
+ run:
+ $ref: "#/components/schemas/TestWorkflowContainerConfig"
+ workingDir:
+ $ref: "#/components/schemas/BoxedString"
+ container:
+ $ref: "#/components/schemas/TestWorkflowContainerConfig"
+ execute:
+ $ref: "#/components/schemas/TestWorkflowStepExecute"
+ artifacts:
+ $ref: "#/components/schemas/TestWorkflowStepArtifacts"
+ setup:
+ type: array
+ description: nested setup steps to run
+ items:
+ $ref: "#/components/schemas/TestWorkflowIndependentStep"
+ steps:
+ type: array
+ description: nested steps to run
+ items:
+ $ref: "#/components/schemas/TestWorkflowIndependentStep"
- TestTriggerConditionSpec:
+ TestWorkflowStep:
type: object
properties:
- conditions:
+ name:
+ type: string
+ description: readable name for the step
+ condition:
+ type: string
+ description: expression to declare under which conditions the step should be run; defaults to "passed", except artifacts where it defaults to "always"
+ negative:
+ type: boolean
+ description: is the step expected to fail
+ optional:
+ type: boolean
+ description: is the step optional, so the failure won't affect the TestWorkflow result
+ use:
type: array
+ description: list of TestWorkflowTemplates to use
items:
- $ref: "#/components/schemas/TestTriggerCondition"
- description: list of test trigger conditions
+ $ref: "#/components/schemas/TestWorkflowTemplateRef"
+ template:
+ $ref: "#/components/schemas/TestWorkflowTemplateRef"
+ retry:
+ $ref: "#/components/schemas/TestWorkflowRetryPolicy"
timeout:
- type: integer
- format: int32
- description: duration in seconds the test trigger waits for conditions, until its stopped
- example: 1
+ type: string
+ pattern: "^((0|[1-9][0-9]*)h)?((0|[1-9][0-9]*)m)?((0|[1-9][0-9]*)s)?((0|[1-9][0-9]*)ms)?$"
+ description: maximum time this step may take
delay:
+ type: string
+ pattern: "^((0|[1-9][0-9]*)h)?((0|[1-9][0-9]*)m)?((0|[1-9][0-9]*)s)?((0|[1-9][0-9]*)ms)?$"
+ description: delay before the step
+ content:
+ $ref: "#/components/schemas/TestWorkflowContent"
+ shell:
+ type: string
+ description: script to run in a default shell for the container
+ run:
+ $ref: "#/components/schemas/TestWorkflowContainerConfig"
+ workingDir:
+ $ref: "#/components/schemas/BoxedString"
+ container:
+ $ref: "#/components/schemas/TestWorkflowContainerConfig"
+ execute:
+ $ref: "#/components/schemas/TestWorkflowStepExecute"
+ artifacts:
+ $ref: "#/components/schemas/TestWorkflowStepArtifacts"
+ setup:
+ type: array
+ description: nested setup steps to run
+ items:
+ $ref: "#/components/schemas/TestWorkflowStep"
+ steps:
+ type: array
+ description: nested steps to run
+ items:
+ $ref: "#/components/schemas/TestWorkflowStep"
+
+ TestWorkflowStepExecute:
+ type: object
+ properties:
+ parallelism:
type: integer
- format: int32
- description: duration in seconds the test trigger waits between condition checks
- example: 1
+ description: how many resources could be scheduled in parallel
+ async:
+ type: boolean
+ description: only schedule the resources, don't watch for the results (unless it is needed for parallelism)
+ tests:
+ type: array
+ description: tests to schedule
+ items:
+ $ref: "#/components/schemas/TestWorkflowStepExecuteTestRef"
+ workflows:
+ type: array
+ description: workflows to schedule
+ items:
+ $ref: "#/components/schemas/TestWorkflowRef"
- TestTriggerCondition:
- description: supported condition for test triggers
+ TestWorkflowStepExecuteTestRef:
type: object
- required:
- - status
- - type
properties:
- status:
- $ref: "#/components/schemas/TestTriggerConditionStatuses"
- type:
+ name:
type: string
- description: test trigger condition
- example: Progressing
- reason:
+ description: test name to schedule
+
+ TestWorkflowStepArtifacts:
+ type: object
+ properties:
+ workingDir:
+ $ref: "#/components/schemas/BoxedString"
+ compress:
+ $ref: "#/components/schemas/TestWorkflowStepArtifactsCompression"
+ paths:
+ type: array
+ description: file paths to fetch from the container
+ items:
+ type: string
+ minItems: 1
+ required:
+ - paths
+
+ TestWorkflowStepArtifactsCompression:
+ type: object
+ properties:
+ name:
type: string
- description: test trigger condition reason
- example: NewReplicaSetAvailable
- ttl:
- type: integer
- format: int32
- description: duration in seconds in the past from current time when the condition is still valid
- example: 1
+ description: artifact name
- TestTriggerConditionStatuses:
- description: supported kubernetes condition statuses for test triggers
- type: string
- enum:
- - "True"
- - "False"
- - "Unknown"
+ TestWorkflowRetryPolicy:
+ type: object
+ properties:
+ count:
+ type: integer
+ minimum: 1
+ description: how many times at most it should retry
+ until:
+ type: string
+ description: until when it should retry (defaults to "passed")
+ required:
+ - count
- TestTriggerProbeSpec:
+ TestWorkflowContent:
type: object
properties:
- probes:
+ git:
+ $ref: "#/components/schemas/TestWorkflowContentGit"
+ files:
type: array
items:
- $ref: "#/components/schemas/TestTriggerProbe"
- description: list of test trigger probes
- timeout:
- type: integer
- format: int32
- description: duration in seconds the test trigger waits for probes, until its stopped
- example: 1
- delay:
- type: integer
- format: int32
- description: duration in seconds the test trigger waits between probes
- example: 1
+ $ref: "#/components/schemas/TestWorkflowContentFile"
- TestTriggerProbe:
- description: supported probe for test triggers
+ TestWorkflowContentGit:
type: object
properties:
- scheme:
+ uri:
type: string
- description: test trigger condition probe scheme to connect to host, default is http
- example: http
- host:
+ description: uri for the Git repository
+ revision:
type: string
- description: test trigger condition probe host, default is pod ip or service name
- example: testkube-api-server
+ description: branch, commit or a tag name to fetch
+ username:
+ type: string
+ description: plain text username to fetch with
+ usernameFrom:
+ $ref: "#/components/schemas/EnvVarSource"
+ token:
+ type: string
+ description: plain text token to fetch with
+ tokenFrom:
+ $ref: "#/components/schemas/EnvVarSource"
+ authType:
+ $ref: "#/components/schemas/ContentGitAuthType"
+ mountPath:
+ type: string
+ description: where to mount the fetched repository contents (defaults to "repo" directory in the data volume)
+ paths:
+ type: array
+ description: paths to fetch for the sparse checkout
+ items:
+ type: string
+
+ TestWorkflowContentFile:
+ type: object
+ properties:
path:
type: string
- description: test trigger condition probe path to check, default is /
- example: /
- port:
- type: integer
- format: int32
- description: test trigger condition probe port to connect
- example: 80
- headers:
+ description: path where the file should be accessible at
+ minLength: 1
+ content:
+ type: string
+ description: plain-text content to put inside
+ contentFrom:
+ $ref: "#/components/schemas/EnvVarSource"
+ mode:
+ $ref: "#/components/schemas/BoxedInteger"
+ required:
+ - path
+
+ TestWorkflowRef:
+ type: object
+ properties:
+ name:
+ type: string
+ description: TestWorkflow name to include
+ config:
+ $ref: "#/components/schemas/TestWorkflowConfigValue"
+ required:
+ - name
+ - path
+
+ TestWorkflowTemplateRef:
+ type: object
+ properties:
+ name:
+ type: string
+ description: TestWorkflowTemplate name to include
+ config:
+ $ref: "#/components/schemas/TestWorkflowConfigValue"
+ required:
+ - name
+
+ TestWorkflowJobConfig:
+ type: object
+ properties:
+ labels:
type: object
- description: test trigger condition probe headers to submit
+ description: labels to attach to the job
+ additionalProperties:
+ type: string
+ annotations:
+ type: object
+ description: annotations to attach to the job
additionalProperties:
type: string
- example:
- Content-Type: "application/xml"
-
- TestTriggerConcurrencyPolicies:
- description: supported concurrency policies for test triggers
- type: string
- enum:
- - allow
- - forbid
- - replace
- TestTriggerKeyMap:
+ TestWorkflowPodConfig:
type: object
- required:
- - resources
- - actions
- - executions
- - events
- - concurrencyPolicies
properties:
- resources:
+ labels:
+ type: object
+ description: labels to attach to the pod
+ additionalProperties:
+ type: string
+ annotations:
+ type: object
+ description: annotations to attach to the pod
+ additionalProperties:
+ type: string
+ imagePullSecrets:
type: array
+ description: secret references for pulling images
items:
+ $ref: "#/components/schemas/LocalObjectReference"
+ serviceAccountName:
+ type: string
+ description: default service account name for the containers
+ nodeSelector:
+ type: object
+ description: label selector for node that the pod should land on
+ additionalProperties:
type: string
- description: list of supported values for resources
- example:
- [
- "pod",
- "deployment",
- "statefulset",
- "daemonset",
- "service",
- "ingress",
- "event",
- "configmap",
- ]
- actions:
+ volumes:
type: array
+ description: volumes to append to the pod
items:
- type: string
- description: list of supported values for actions
- example: ["run"]
- executions:
+ $ref: "#/components/schemas/Volume"
+
+ TestWorkflowContainerConfig:
+ type: object
+ properties:
+ workingDir:
+ $ref: "#/components/schemas/BoxedString"
+ image:
+ type: string
+ description: image to be used for the container
+ imagePullPolicy:
+ $ref: "#/components/schemas/ImagePullPolicy"
+ env:
type: array
+ description: environment variables to append to the container
items:
- type: string
- description: list of supported values for executions
- example: ["test", "testsuite"]
- events:
- type: object
- additionalProperties:
- type: array
- items:
- type: string
- description: mapping between resources and supported events
- example:
- {
- "pod": ["created", "modified", "deleted"],
- "deployment": ["created", "modified", "deleted"],
- }
- conditions:
+ $ref: "#/components/schemas/EnvVar"
+ envFrom:
+ type: array
+ description: external environment variables to append to the container
+ items:
+ $ref: "#/components/schemas/EnvFromSource"
+ command:
+ $ref: "#/components/schemas/BoxedStringList"
+ args:
+ $ref: "#/components/schemas/BoxedStringList"
+ resources:
+ $ref: "#/components/schemas/TestWorkflowResources"
+ securityContext:
+ $ref: "#/components/schemas/SecurityContext"
+ volumeMounts:
+ type: array
+ description: volumes to mount to the container
+ items:
+ $ref: "#/components/schemas/VolumeMount"
+
+ TestWorkflowConfigValue:
+ type: object
+ description: configuration values to pass to the template
+ additionalProperties:
+ type: string
+
+ TestWorkflowConfigSchema:
+ type: object
+ description: configuration definition
+ additionalProperties:
+ $ref: "#/components/schemas/TestWorkflowParameterSchema"
+
+ TestWorkflowResources:
+ type: object
+ properties:
+ limits:
+ $ref: "#/components/schemas/TestWorkflowResourcesList"
+ requests:
+ $ref: "#/components/schemas/TestWorkflowResourcesList"
+
+ TestWorkflowResourcesList:
+ type: object
+ properties:
+ cpu:
+ type: string
+ description: number of CPUs
+ pattern: "^[0-9]+m?$"
+ memory:
+ type: string
+ description: size of RAM memory
+ pattern: "^[0-9]+[GMK]i$"
+ storage:
+ type: string
+ description: storage size
+ pattern: "^[0-9]+[GMK]i$"
+ ephemeral-storage:
+ type: string
+ description: ephemeral storage size
+ pattern: "^[0-9]+[GMK]i$"
+
+ TestWorkflowParameterSchema:
+ type: object
+ properties:
+ description:
+ type: string
+ description: human-readable description for the property
+ type:
+ $ref: "#/components/schemas/TestWorkflowParameterType"
+ enum:
type: array
+ description: list of acceptable values
items:
type: string
- description: list of supported values for conditions
- example: ["Available", "Progressing"]
- concurrencyPolicies:
+ example:
+ type: string
+ description: example value for the parameter
+ default:
+ $ref: "#/components/schemas/BoxedString"
+ format:
+ type: string
+ description: "predefined format for the string"
+ pattern:
+ type: string
+ description: "regular expression to match"
+ minLength:
+ $ref: "#/components/schemas/BoxedInteger"
+ maxLength:
+ $ref: "#/components/schemas/BoxedInteger"
+ minimum:
+ $ref: "#/components/schemas/BoxedInteger"
+ maximum:
+ $ref: "#/components/schemas/BoxedInteger"
+ exclusiveMinimum:
+ $ref: "#/components/schemas/BoxedInteger"
+ exclusiveMaximum:
+ $ref: "#/components/schemas/BoxedInteger"
+ multipleOf:
+ $ref: "#/components/schemas/BoxedInteger"
+ required:
+ - type
+
+ TestWorkflowParameterType:
+ type: string
+ description: type of the config parameter
+ enum:
+ - string
+ - integer
+ - number
+ - boolean
+
+ ContentGitAuthType:
+ type: string
+ description: auth type for git requests
+ enum:
+ - basic
+ - header
+
+ BoxedStringList:
+ type: object
+ properties:
+ value:
type: array
items:
type: string
- description: list of supported values for concurrency policies
- example: ["allow", "forbid", "replace"]
+ required:
+ - value
- TestSourceBatchRequest:
- description: Test source batch request
+ BoxedString:
type: object
+ properties:
+ value:
+ type: string
required:
- - batch
+ - value
+
+ BoxedInteger:
+ type: object
properties:
- batch:
- type: array
- items:
- $ref: "#/components/schemas/TestSourceUpsertRequest"
+ value:
+ type: integer
+ required:
+ - value
- TestSourceBatchResult:
- description: Test source batch result
+ BoxedBoolean:
type: object
properties:
- created:
- type: array
- items:
- type: string
- description: created test sources
- example: ["name1", "name2", "name3"]
- updated:
- type: array
- items:
- type: string
- description: updated test sources
- example: ["name4", "name5", "name6"]
- deleted:
- type: array
- items:
- type: string
- description: deleted test sources
- example: ["name7", "name8", "name9"]
+ value:
+ type: boolean
+ required:
+ - value
- Template:
- description: Golang based template
+ ImagePullPolicy:
+ type: string
+ enum:
+ - Always
+ - Never
+ - IfNotPresent
+
+ EnvVar:
+ type: object
+ properties:
+ name:
+ type: string
+ value:
+ type: string
+ valueFrom:
+ $ref: "#/components/schemas/EnvVarSource"
+
+ ConfigMapEnvSource:
type: object
+ properties:
+ name:
+ type: string
+ optional:
+ type: boolean
+ default: false
required:
- name
- - type
- - body
+
+ SecretEnvSource:
+ type: object
properties:
name:
type: string
- description: template name for reference
- example: "webhook-template"
- namespace:
+ optional:
+ type: boolean
+ default: false
+ required:
+ - name
+
+ EnvFromSource:
+ type: object
+ properties:
+ prefix:
type: string
- description: template namespace
- example: "testkube"
- type:
- $ref: "#/components/schemas/TemplateType"
- body:
+ configMapRef:
+ $ref: "#/components/schemas/ConfigMapEnvSource"
+ secretRef:
+ $ref: "#/components/schemas/SecretEnvSource"
+
+ SecurityContext:
+ type: object
+ properties:
+ privileged:
+ $ref: "#/components/schemas/BoxedBoolean"
+ runAsUser:
+ $ref: "#/components/schemas/BoxedInteger"
+ runAsGroup:
+ $ref: "#/components/schemas/BoxedInteger"
+ runAsNonRoot:
+ $ref: "#/components/schemas/BoxedBoolean"
+ readOnlyRootFilesystem:
+ $ref: "#/components/schemas/BoxedBoolean"
+ allowPrivilegeEscalation:
+ $ref: "#/components/schemas/BoxedBoolean"
+
+ VolumeMount:
+ description: VolumeMount describes a mounting of a Volume
+ within a container.
+ properties:
+ mountPath:
+ description: Path within the container at which the
+ volume should be mounted. Must not contain ':'.
type: string
- description: template body to use
- example: "{\"id\": \"{{ .Id }}\"}"
- labels:
- type: object
- description: "template labels"
- additionalProperties:
- type: string
- example:
- env: "prod"
- app: "backend"
+ mountPropagation:
+ $ref: "#/components/schemas/BoxedString"
+ name:
+ description: This must match the Name of a Volume.
+ type: string
+ readOnly:
+ description: Mounted read-only if true, read-write
+ otherwise (false or unspecified). Defaults to false.
+ type: boolean
+ subPath:
+ description: Path within the volume from which the
+ container's volume should be mounted. Defaults to
+ "" (volume's root).
+ type: string
+ subPathExpr:
+ description: Expanded path within the volume from
+ which the container's volume should be mounted.
+ Behaves similarly to SubPath but environment variable
+ references $(VAR_NAME) are expanded using the container's
+ environment. Defaults to "" (volume's root). SubPathExpr
+ and SubPath are mutually exclusive.
+ type: string
+ required:
+ - mountPath
+ - name
+ type: object
- TemplateType:
- description: template type by purpose
- type: string
- enum:
- - job
- - container
- - cronjob
- - scraper
- - pvc
- - webhook
- - pod
+ HostPathVolumeSource:
+ description: 'hostPath represents a pre-existing file or
+ directory on the host machine that is directly exposed
+ to the container. This is generally used for system agents
+ or other privileged things that are allowed to see the
+ host machine. Most containers will NOT need this. More
+ info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath
+ --- TODO(jonesdl) We need to restrict who can use host
+ directory mounts and who can/can not mount host directories
+ as read/write.'
+ properties:
+ path:
+ description: 'path of the directory on the host. If
+ the path is a symlink, it will follow the link to
+ the real path. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath'
+ type: string
+ type:
+ $ref: "#/components/schemas/BoxedString"
+ required:
+ - path
+ type: object
- TemplateCreateRequest:
- description: template create request body
+ EmptyDirVolumeSource:
+ description: 'emptyDir represents a temporary directory
+ that shares a pod''s lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir'
+ properties:
+ medium:
+ description: 'medium represents what type of storage
+ medium should back this directory. The default is
+ "" which means to use the node''s default medium.
+ Must be an empty string (default) or Memory. More
+ info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir'
+ type: string
+ sizeLimit:
+ $ref: "#/components/schemas/BoxedString"
type: object
- allOf:
- - $ref: "#/components/schemas/Template"
- TemplateUpdateRequest:
- description: template update request body
+ GCEPersistentDiskVolumeSource:
+ description: 'gcePersistentDisk represents a GCE Disk resource
+ that is attached to a kubelet''s host machine and then
+ exposed to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk'
+ properties:
+ fsType:
+ description: 'fsType is filesystem type of the volume
+ that you want to mount. Tip: Ensure that the filesystem
+ type is supported by the host operating system. Examples:
+ "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4"
+ if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk
+ TODO: how do we prevent errors in the filesystem from
+ compromising the machine'
+ type: string
+ partition:
+ description: 'partition is the partition in the volume
+ that you want to mount. If omitted, the default is
+ to mount by volume name. Examples: For volume /dev/sda1,
+ you specify the partition as "1". Similarly, the volume
+ partition for /dev/sda is "0" (or you can leave the
+ property empty). More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk'
+ format: int32
+ type: integer
+ pdName:
+ description: 'pdName is unique name of the PD resource
+ in GCE. Used to identify the disk in GCE. More info:
+ https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk'
+ type: string
+ readOnly:
+ description: 'readOnly here will force the ReadOnly
+ setting in VolumeMounts. Defaults to false. More info:
+ https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk'
+ type: boolean
+ required:
+ - pdName
type: object
- nullable: true
- allOf:
- - $ref: "#/components/schemas/Template"
- Secret:
- description: Secret with keys
+ AWSElasticBlockStoreVolumeSource:
+ description: 'awsElasticBlockStore represents an AWS Disk
+ resource that is attached to a kubelet''s host machine
+ and then exposed to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore'
+ properties:
+ fsType:
+ description: 'fsType is the filesystem type of the volume
+ that you want to mount. Tip: Ensure that the filesystem
+ type is supported by the host operating system. Examples:
+ "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4"
+ if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore
+ TODO: how do we prevent errors in the filesystem from
+ compromising the machine'
+ type: string
+ partition:
+ description: 'partition is the partition in the volume
+ that you want to mount. If omitted, the default is
+ to mount by volume name. Examples: For volume /dev/sda1,
+ you specify the partition as "1". Similarly, the volume
+ partition for /dev/sda is "0" (or you can leave the
+ property empty).'
+ format: int32
+ type: integer
+ readOnly:
+ description: 'readOnly value true will force the readOnly
+ setting in VolumeMounts. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore'
+ type: boolean
+ volumeID:
+ description: 'volumeID is unique ID of the persistent
+ disk resource in AWS (Amazon EBS volume). More info:
+ https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore'
+ type: string
+ required:
+ - volumeID
+ type: object
+
+ SecretVolumeSource:
+ description: 'secret represents a secret that should populate
+ this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret'
+ properties:
+ defaultMode:
+ $ref: "#/components/schemas/BoxedInteger"
+ items:
+ description: items If unspecified, each key-value pair
+ in the Data field of the referenced Secret will be
+ projected into the volume as a file whose name is
+ the key and content is the value. If specified, the
+ listed keys will be projected into the specified paths,
+ and unlisted keys will not be present. If a key is
+ specified which is not present in the Secret, the
+ volume setup will error unless it is marked optional.
+ Paths must be relative and may not contain the '..'
+ path or start with '..'.
+ items:
+ description: Maps a string key to a path within a
+ volume.
+ properties:
+ key:
+ description: key is the key to project.
+ type: string
+ mode:
+ $ref: "#/components/schemas/BoxedInteger"
+ path:
+ description: path is the relative path of the
+ file to map the key to. May not be an absolute
+ path. May not contain the path element '..'.
+ May not start with the string '..'.
+ type: string
+ required:
+ - key
+ - path
+ type: object
+ type: array
+ optional:
+ description: optional field specify whether the Secret
+ or its keys must be defined
+ type: boolean
+ secretName:
+ description: 'secretName is the name of the secret in
+ the pod''s namespace to use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret'
+ type: string
type: object
+
+ NFSVolumeSource:
+ description: 'nfs represents an NFS mount on the host that
+ shares a pod''s lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs'
+ properties:
+ path:
+ description: 'path that is exported by the NFS server.
+ More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs'
+ type: string
+ readOnly:
+ description: 'readOnly here will force the NFS export
+ to be mounted with read-only permissions. Defaults
+ to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs'
+ type: boolean
+ server:
+ description: 'server is the hostname or IP address of
+ the NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs'
+ type: string
required:
- - name
+ - path
+ - server
+ type: object
+
+ PersistentVolumeClaimVolumeSource:
+ description: 'persistentVolumeClaimVolumeSource represents
+ a reference to a PersistentVolumeClaim in the same namespace.
+ More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims'
properties:
- name:
+ claimName:
+ description: 'claimName is the name of a PersistentVolumeClaim
+ in the same namespace as the pod using this volume.
+ More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims'
type: string
- description: secret name
- example: "git-secret"
- keys:
- type: array
- description: secret keys
+ readOnly:
+ description: readOnly Will force the ReadOnly setting
+ in VolumeMounts. Default false.
+ type: boolean
+ required:
+ - claimName
+ type: object
+
+ CephFSVolumeSource:
+ description: cephFS represents a Ceph FS mount on the host
+ that shares a pod's lifetime
+ properties:
+ monitors:
+ description: 'monitors is Required: Monitors is a collection
+ of Ceph monitors More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it'
items:
type: string
- example: ["key1", "key2", "key3"]
+ type: array
+ path:
+ description: 'path is Optional: Used as the mounted
+ root, rather than the full Ceph tree, default is /'
+ type: string
+ readOnly:
+ description: 'readOnly is Optional: Defaults to false
+ (read/write). ReadOnly here will force the ReadOnly
+ setting in VolumeMounts. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it'
+ type: boolean
+ secretFile:
+ description: 'secretFile is Optional: SecretFile is
+ the path to key ring for User, default is /etc/ceph/user.secret
+ More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it'
+ type: string
+ secretRef:
+ $ref: "#/components/schemas/LocalObjectReference"
+ user:
+ description: 'user is optional: User is the rados user
+ name, default is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it'
+ type: string
+ required:
+ - monitors
+ type: object
+
+ AzureFileVolumeSource:
+ description: azureFile represents an Azure File Service
+ mount on the host and bind mount to the pod.
+ properties:
+ readOnly:
+ description: readOnly defaults to false (read/write).
+ ReadOnly here will force the ReadOnly setting in VolumeMounts.
+ type: boolean
+ secretName:
+ description: secretName is the name of secret that
+ contains Azure Storage Account Name and Key
+ type: string
+ shareName:
+ description: shareName is the azure share Name
+ type: string
+ required:
+ - secretName
+ - shareName
+ type: object
+
+ ConfigMapVolumeSource:
+ description: configMap represents a configMap that should
+ populate this volume
+ properties:
+ defaultMode:
+ $ref: "#/components/schemas/BoxedInteger"
+ items:
+ description: items if unspecified, each key-value pair
+ in the Data field of the referenced ConfigMap will
+ be projected into the volume as a file whose name
+ is the key and content is the value. If specified,
+ the listed keys will be projected into the specified
+ paths, and unlisted keys will not be present. If a
+ key is specified which is not present in the ConfigMap,
+ the volume setup will error unless it is marked optional.
+ Paths must be relative and may not contain the '..'
+ path or start with '..'.
+ items:
+ description: Maps a string key to a path within a
+ volume.
+ properties:
+ key:
+ description: key is the key to project.
+ type: string
+ mode:
+ $ref: "#/components/schemas/BoxedInteger"
+ path:
+ description: path is the relative path of the
+ file to map the key to. May not be an absolute
+ path. May not contain the path element '..'.
+ May not start with the string '..'.
+ type: string
+ required:
+ - key
+ - path
+ type: object
+ type: array
+ name:
+ description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Add other useful fields. apiVersion, kind, uid?'
+ type: string
+ optional:
+ description: optional specify whether the ConfigMap
+ or its keys must be defined
+ type: boolean
+ type: object
+
+ AzureDiskVolumeSource:
+ description: azureDisk represents an Azure Data Disk mount
+ on the host and bind mount to the pod.
+ properties:
+ cachingMode:
+ $ref: "#/components/schemas/BoxedString"
+ diskName:
+ description: diskName is the Name of the data disk in
+ the blob storage
+ type: string
+ diskURI:
+ description: diskURI is the URI of data disk in the
+ blob storage
+ type: string
+ fsType:
+ $ref: "#/components/schemas/BoxedString"
+ kind:
+ $ref: "#/components/schemas/BoxedString"
+ readOnly:
+ description: readOnly Defaults to false (read/write).
+ ReadOnly here will force the ReadOnly setting in VolumeMounts.
+ type: boolean
+ required:
+ - diskName
+ - diskURI
+ type: object
+
+ Volume:
+ type: object
+ description: Volume represents a named volume in a pod that
+ may be accessed by any container in the pod.
+ properties:
+ name:
+ type: string
+ hostPath:
+ $ref: "#/components/schemas/HostPathVolumeSource"
+ emptyDir:
+ $ref: "#/components/schemas/EmptyDirVolumeSource"
+ gcePersistentDisk:
+ $ref: "#/components/schemas/GCEPersistentDiskVolumeSource"
+ awsElasticBlockStore:
+ $ref: "#/components/schemas/AWSElasticBlockStoreVolumeSource"
+ secret:
+ $ref: "#/components/schemas/SecretVolumeSource"
+ nfs:
+ $ref: "#/components/schemas/NFSVolumeSource"
+ persistentVolumeClaim:
+ $ref: "#/components/schemas/PersistentVolumeClaimVolumeSource"
+ cephfs:
+ $ref: "#/components/schemas/CephFSVolumeSource"
+ azureFile:
+ $ref: "#/components/schemas/AzureFileVolumeSource"
+ azureDisk:
+ $ref: "#/components/schemas/AzureDiskVolumeSource"
+ configMap:
+ $ref: "#/components/schemas/ConfigMapVolumeSource"
+ required:
+ - name
+
+ VolumeSource:
+ type: object
+
+ EnvVarSource:
+ type: object
+ description: EnvVarSource represents a source for the value
+ of an EnvVar.
+ properties:
+ configMapKeyRef:
+ type: object
+ required:
+ - key
+ description: Selects a key of a ConfigMap.
+ properties:
+ key:
+ description: The key to select.
+ type: string
+ name:
+ description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Add other useful fields. apiVersion, kind,
+ uid?'
+ type: string
+ optional:
+ description: Specify whether the ConfigMap or its
+ key must be defined
+ type: boolean
+ fieldRef:
+ type: object
+ required:
+ - fieldPath
+ description: 'Selects a field of the pod: supports metadata.name,
+ metadata.namespace, `metadata.labels['''']`,
+ `metadata.annotations['''']`, spec.nodeName,
+ spec.serviceAccountName, status.hostIP, status.podIP,
+ status.podIPs.'
+ properties:
+ apiVersion:
+ description: Version of the schema the FieldPath
+ is written in terms of, defaults to "v1".
+ type: string
+ fieldPath:
+ description: Path of the field to select in the
+ specified API version.
+ type: string
+ resourceFieldRef:
+ type: object
+ required:
+ - resource
+ description: 'Selects a resource of the container: only
+ resources limits and requests (limits.cpu, limits.memory,
+ limits.ephemeral-storage, requests.cpu, requests.memory
+ and requests.ephemeral-storage) are currently supported.'
+ properties:
+ containerName:
+ description: 'Container name: required for volumes,
+ optional for env vars'
+ type: string
+ divisor:
+ type: string
+ pattern: "^[0-9]+(m|[GMK]i)$"
+ resource:
+ description: 'Required: resource to select'
+ type: string
+ secretKeyRef:
+ type: object
+ required:
+ - key
+ description: Selects a key of a secret in the pod's
+ namespace
+ properties:
+ key:
+ description: The key of the secret to select from. Must
+ be a valid secret key.
+ type: string
+ name:
+ description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Add other useful fields. apiVersion, kind,
+ uid?'
+ type: string
+ optional:
+ description: Specify whether the Secret or its key
+ must be defined
+ type: boolean
#
# Errors
@@ -6085,7 +9000,7 @@ components:
name: testSuiteExecutionName
schema:
type: string
- description: test suite execution name stated the test suite execution
+ description: test suite execution name stated the test suite execution
Namespace:
in: query
name: namespace
@@ -6114,6 +9029,21 @@ components:
default: false
description: dont delete executions
required: false
+ TestType:
+ in: query
+ name: testType
+ schema:
+ type: string
+ required: true
+ description: test type of the executor
+ InlineTemplates:
+ in: query
+ name: inline
+ schema:
+ type: boolean
+ default: false
+ description: should inline templates in the resolved workflow
+ required: false
requestBodies:
UploadsBody:
description: "Upload files request body data"
@@ -6133,4 +9063,4 @@ components:
- execution
filePath:
type: string
- example: folder/file.txt
\ No newline at end of file
+ example: folder/file.txt
diff --git a/build/sidecar/Dockerfile b/build/sidecar/Dockerfile
index 8642df1803b..a2618e91bb7 100644
--- a/build/sidecar/Dockerfile
+++ b/build/sidecar/Dockerfile
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
ARG ALPINE_IMAGE
FROM ${ALPINE_IMAGE}
-RUN apk --no-cache add ca-certificates libssl1.1 git skopeo
+RUN apk --no-cache add ca-certificates libssl1.1
WORKDIR /root/
COPY testkube-logs-sidecar /bin/app
USER 1001
diff --git a/build/testworkflow-init/Dockerfile b/build/testworkflow-init/Dockerfile
new file mode 100644
index 00000000000..40f6a3192d7
--- /dev/null
+++ b/build/testworkflow-init/Dockerfile
@@ -0,0 +1,6 @@
+# syntax=docker/dockerfile:1
+ARG ALPINE_IMAGE
+FROM ${ALPINE_IMAGE}
+COPY testworkflow-init /init
+USER 1001
+ENTRYPOINT ["/init"]
diff --git a/build/testworkflow-toolkit/Dockerfile b/build/testworkflow-toolkit/Dockerfile
new file mode 100644
index 00000000000..6666347bd28
--- /dev/null
+++ b/build/testworkflow-toolkit/Dockerfile
@@ -0,0 +1,7 @@
+# syntax=docker/dockerfile:1
+ARG ALPINE_IMAGE
+FROM ${ALPINE_IMAGE}
+RUN apk --no-cache add ca-certificates libssl3 git
+COPY testworkflow-toolkit /toolkit
+USER 1001
+ENTRYPOINT ["/toolkit"]
diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go
index f25259182b8..10728c3dd15 100644
--- a/cmd/api-server/main.go
+++ b/cmd/api-server/main.go
@@ -2,7 +2,6 @@ package main
import (
"context"
- "crypto/tls"
"encoding/json"
"flag"
"fmt"
@@ -16,9 +15,16 @@ import (
"github.com/nats-io/nats.go"
executorsclientv1 "github.com/kubeshop/testkube-operator/pkg/client/executors/v1"
+ "github.com/kubeshop/testkube/pkg/imageinspector"
+ apitclv1 "github.com/kubeshop/testkube/pkg/tcl/apitcl/v1"
+ "github.com/kubeshop/testkube/pkg/tcl/checktcl"
+ cloudtestworkflow "github.com/kubeshop/testkube/pkg/tcl/cloudtcl/data/testworkflow"
+ "github.com/kubeshop/testkube/pkg/tcl/repositorytcl/testworkflow"
+ "github.com/kubeshop/testkube/pkg/tcl/schedulertcl"
"go.mongodb.org/mongo-driver/mongo"
"google.golang.org/grpc"
+ "google.golang.org/grpc/credentials"
cloudartifacts "github.com/kubeshop/testkube/pkg/cloud/data/artifact"
@@ -36,8 +42,8 @@ import (
"github.com/kubeshop/testkube/internal/common"
"github.com/kubeshop/testkube/internal/config"
dbmigrations "github.com/kubeshop/testkube/internal/db-migrations"
- "github.com/kubeshop/testkube/internal/featureflags"
parser "github.com/kubeshop/testkube/internal/template"
+ "github.com/kubeshop/testkube/pkg/featureflags"
"github.com/kubeshop/testkube/pkg/version"
"github.com/kubeshop/testkube/pkg/cloud"
@@ -58,6 +64,7 @@ import (
kubeexecutor "github.com/kubeshop/testkube/pkg/executor"
"github.com/kubeshop/testkube/pkg/executor/client"
"github.com/kubeshop/testkube/pkg/executor/containerexecutor"
+ logsclient "github.com/kubeshop/testkube/pkg/logs/client"
"github.com/kubeshop/testkube/pkg/scheduler"
testkubeclientset "github.com/kubeshop/testkube-operator/pkg/clientset/versioned"
@@ -133,10 +140,10 @@ func main() {
cfg.CleanLegacyVars()
ui.ExitOnError("error getting application config", err)
- ff, err := featureflags.Get()
+ features, err := featureflags.Get()
ui.ExitOnError("error getting application feature flags", err)
- log.DefaultLogger.Infow("Feature flags configured", "ff", ff)
+ log.DefaultLogger.Infow("Feature flags configured", "ff", features)
// Run services within an errgroup to propagate errors between services.
g, ctx := errgroup.WithContext(context.Background())
@@ -150,6 +157,10 @@ func main() {
case <-ctx.Done():
return nil
case sig := <-stopSignal:
+ go func() {
+ <-stopSignal
+ os.Exit(137)
+ }()
// Returning an error cancels the errgroup.
return errors.Errorf("received signal: %v", sig)
}
@@ -181,7 +192,16 @@ func main() {
mode = common.ModeAgent
}
if mode == common.ModeAgent {
- grpcConn, err = agent.NewGRPCConnection(ctx, cfg.TestkubeProTLSInsecure, cfg.TestkubeProSkipVerify, cfg.TestkubeProURL, log.DefaultLogger)
+ grpcConn, err = agent.NewGRPCConnection(
+ ctx,
+ cfg.TestkubeProTLSInsecure,
+ cfg.TestkubeProSkipVerify,
+ cfg.TestkubeProURL,
+ cfg.TestkubeProCertFile,
+ cfg.TestkubeProKeyFile,
+ cfg.TestkubeProCAFile,
+ log.DefaultLogger,
+ )
ui.ExitOnError("error creating gRPC connection", err)
defer grpcConn.Close()
@@ -224,9 +244,18 @@ func main() {
ui.ExitOnError("Creating TestKube Clientset", err)
}
+ var logGrpcClient logsclient.StreamGetter
+ if features.LogsV2 {
+ creds, err := newGRPCTransportCredentials(cfg)
+ ui.ExitOnError("Getting log server TLS credentials", err)
+ logGrpcClient = logsclient.NewGrpcClient(cfg.LogServerGrpcAddress, creds)
+ }
+
// DI
var resultsRepository result.Repository
var testResultsRepository testresult.Repository
+ var testWorkflowResultsRepository testworkflow.Repository
+ var testWorkflowOutputRepository testworkflow.OutputRepository
var configRepository configrepository.Repository
var triggerLeaseBackend triggers.LeaseBackend
var artifactStorage domainstorage.ArtifactsStorage
@@ -235,6 +264,8 @@ func main() {
resultsRepository = cloudresult.NewCloudResultRepository(grpcClient, grpcConn, cfg.TestkubeProAPIKey)
testResultsRepository = cloudtestresult.NewCloudRepository(grpcClient, grpcConn, cfg.TestkubeProAPIKey)
configRepository = cloudconfig.NewCloudResultRepository(grpcClient, grpcConn, cfg.TestkubeProAPIKey)
+ testWorkflowResultsRepository = cloudtestworkflow.NewCloudRepository(grpcClient, grpcConn, cfg.TestkubeProAPIKey)
+ testWorkflowOutputRepository = cloudtestworkflow.NewCloudOutputRepository(grpcClient, grpcConn, cfg.TestkubeProAPIKey)
triggerLeaseBackend = triggers.NewAcquireAlwaysLeaseBackend()
artifactStorage = cloudartifacts.NewCloudArtifactsStorage(grpcClient, grpcConn, cfg.TestkubeProAPIKey)
} else {
@@ -242,9 +273,10 @@ func main() {
db, err := storage.GetMongoDatabase(cfg.APIMongoDSN, cfg.APIMongoDB, cfg.APIMongoDBType, cfg.APIMongoAllowTLS, mongoSSLConfig)
ui.ExitOnError("Getting mongo database", err)
isDocDb := cfg.APIMongoDBType == storage.TypeDocDB
- mongoResultsRepository := result.NewMongoRepository(db, cfg.APIMongoAllowDiskUse, isDocDb)
+ mongoResultsRepository := result.NewMongoRepository(db, cfg.APIMongoAllowDiskUse, isDocDb, result.WithFeatureFlags(features), result.WithLogsClient(logGrpcClient))
resultsRepository = mongoResultsRepository
testResultsRepository = testresult.NewMongoRepository(db, cfg.APIMongoAllowDiskUse, isDocDb)
+ testWorkflowResultsRepository = testworkflow.NewMongoRepository(db, cfg.APIMongoAllowDiskUse)
configRepository = configrepository.NewMongoRepository(db)
triggerLeaseBackend = triggers.NewMongoLeaseBackend(db)
minioClient := newStorageClient(cfg)
@@ -255,6 +287,7 @@ func main() {
log.DefaultLogger.Errorw("Error setting expiration policy", "error", expErr)
}
storageClient = minioClient
+ testWorkflowOutputRepository = testworkflow.NewMinioOutputRepository(storageClient, cfg.LogsBucket)
artifactStorage = minio.NewMinIOArtifactClient(storageClient)
// init storage
isMinioStorage := cfg.LogsStorage == "minio"
@@ -348,6 +381,15 @@ func main() {
eventBus := bus.NewNATSBus(nc)
eventsEmitter := event.NewEmitter(eventBus, cfg.TestkubeClusterName, envs)
+ var logsStream logsclient.Stream
+
+ if features.LogsV2 {
+ logsStream, err = logsclient.NewNatsLogStream(nc.Conn)
+ if err != nil {
+ ui.ExitOnError("Creating logs streaming client", err)
+ }
+ }
+
metrics := metrics.NewMetrics()
defaultExecutors, err := parseDefaultExecutors(cfg)
@@ -365,12 +407,45 @@ func main() {
ui.ExitOnError("Creating job templates", err)
}
+ proContext := config.ProContext{
+ APIKey: cfg.TestkubeProAPIKey,
+ URL: cfg.TestkubeProURL,
+ LogsPath: cfg.TestkubeProLogsPath,
+ TLSInsecure: cfg.TestkubeProTLSInsecure,
+ WorkerCount: cfg.TestkubeProWorkerCount,
+ LogStreamWorkerCount: cfg.TestkubeProLogStreamWorkerCount,
+ WorkflowNotificationsWorkerCount: cfg.TestkubeProWorkflowNotificationsWorkerCount,
+ SkipVerify: cfg.TestkubeProSkipVerify,
+ EnvID: cfg.TestkubeProEnvID,
+ OrgID: cfg.TestkubeProOrgID,
+ Migrate: cfg.TestkubeProMigrate,
+ ConnectionTimeout: cfg.TestkubeProConnectionTimeout,
+ }
+
+ // Check Pro/Enterprise subscription
+ var subscriptionChecker checktcl.SubscriptionChecker
+ if mode == common.ModeAgent {
+ subscriptionChecker, err = checktcl.NewSubscriptionChecker(ctx, proContext, grpcClient, grpcConn)
+ ui.ExitOnError("Failed creating subscription checker", err)
+ }
+
+ serviceAccountNames := map[string]string{
+ cfg.TestkubeNamespace: cfg.JobServiceAccountName,
+ }
+
+ // Pro edition only (tcl protected code)
+ if cfg.TestkubeExecutionNamespaces != "" {
+ err = subscriptionChecker.IsActiveOrgPlanEnterpriseForFeature("execution namespace")
+ ui.ExitOnError("Subscription checking", err)
+
+ serviceAccountNames = schedulertcl.GetServiceAccountNamesFromConfig(serviceAccountNames, cfg.TestkubeExecutionNamespaces)
+ }
+
executor, err := client.NewJobExecutor(
resultsRepository,
- cfg.TestkubeNamespace,
images,
jobTemplates,
- cfg.JobServiceAccountName,
+ serviceAccountNames,
metrics,
eventsEmitter,
configMapConfig,
@@ -385,6 +460,8 @@ func main() {
"http://"+cfg.APIServerFullname+":"+cfg.APIServerPort,
cfg.NatsURI,
cfg.Debug,
+ logsStream,
+ features,
)
if err != nil {
ui.ExitOnError("Creating executor client", err)
@@ -395,12 +472,25 @@ func main() {
ui.ExitOnError("Creating container job templates", err)
}
+ inspectorStorages := []imageinspector.Storage{imageinspector.NewMemoryStorage()}
+ if cfg.EnableImageDataPersistentCache {
+ configmapStorage := imageinspector.NewConfigMapStorage(configMapClient, cfg.ImageDataPersistentCacheKey, true)
+ _ = configmapStorage.CopyTo(context.Background(), inspectorStorages[0].(imageinspector.StorageTransfer))
+ inspectorStorages = append(inspectorStorages, configmapStorage)
+ }
+ inspector := imageinspector.NewInspector(
+ cfg.TestkubeRegistry,
+ imageinspector.NewSkopeoFetcher(),
+ imageinspector.NewSecretFetcher(secretClient),
+ inspectorStorages...,
+ )
+
containerExecutor, err := containerexecutor.NewContainerExecutor(
resultsRepository,
- cfg.TestkubeNamespace,
images,
containerTemplates,
- cfg.JobServiceAccountName,
+ inspector,
+ serviceAccountNames,
metrics,
eventsEmitter,
configMapConfig,
@@ -415,6 +505,8 @@ func main() {
"http://"+cfg.APIServerFullname+":"+cfg.APIServerPort,
cfg.NatsURI,
cfg.Debug,
+ logsStream,
+ features,
)
if err != nil {
ui.ExitOnError("Creating container executor", err)
@@ -438,7 +530,10 @@ func main() {
testsuiteExecutionsClient,
eventBus,
cfg.TestkubeDashboardURI,
- ff,
+ features,
+ logsStream,
+ cfg.TestkubeNamespace,
+ cfg.TestkubeProTLSSecret,
)
slackLoader, err := newSlackLoader(cfg, envs)
@@ -476,23 +571,39 @@ func main() {
mode,
eventBus,
cfg.EnableSecretsEndpoint,
- ff,
+ features,
+ logsStream,
+ logGrpcClient,
+ cfg.DisableSecretCreation,
+ subscriptionChecker,
)
+ // Apply Pro server enhancements
+ apiPro := apitclv1.NewApiTCL(
+ api,
+ &proContext,
+ kubeClient,
+ inspector,
+ testWorkflowResultsRepository,
+ testWorkflowOutputRepository,
+ "http://"+cfg.APIServerFullname+":"+cfg.APIServerPort,
+ )
+ apiPro.AppendRoutes()
+
if mode == common.ModeAgent {
log.DefaultLogger.Info("starting agent service")
-
+ api.WithProContext(&proContext)
agentHandle, err := agent.NewAgent(
log.DefaultLogger,
api.Mux.Handler(),
- cfg.TestkubeProAPIKey,
grpcClient,
- cfg.TestkubeProWorkerCount,
- cfg.TestkubeProLogStreamWorkerCount,
api.GetLogsStream,
+ apiPro.GetTestWorkflowNotificationsStream,
clusterId,
cfg.TestkubeClusterName,
envs,
+ features,
+ proContext,
)
if err != nil {
ui.ExitOnError("Starting agent", err)
@@ -508,7 +619,6 @@ func main() {
}
api.InitEvents()
-
if !cfg.DisableTestTriggers {
triggerService := triggers.NewService(
sched,
@@ -540,8 +650,7 @@ func main() {
resultsRepository,
testResultsRepository,
executorsClient,
- log.DefaultLogger,
- cfg.TestkubeNamespace)
+ log.DefaultLogger)
g.Go(func() error {
return reconcilerClient.Run(ctx)
})
@@ -623,22 +732,15 @@ func parseDefaultExecutors(cfg *config.Config) (executors []testkube.ExecutorDet
}
func newNATSConnection(cfg *config.Config) (*nats.EncodedConn, error) {
- var opts []nats.Option
- if cfg.NatsSecure {
- if cfg.NatsSkipVerify {
- opts = append(opts, nats.Secure(&tls.Config{InsecureSkipVerify: true}))
- } else {
- opts = append(opts, nats.ClientCert(cfg.NatsCertFile, cfg.NatsKeyFile))
- if cfg.NatsCAFile != "" {
- opts = append(opts, nats.RootCAs(cfg.NatsCAFile))
- }
- }
- }
- nc, err := bus.NewNATSEncoddedConnection(cfg.NatsURI, opts...)
- if err != nil {
- log.DefaultLogger.Errorw("error creating NATS connection", "error", err)
- }
- return nc, nil
+ return bus.NewNATSEncodedConnection(bus.ConnectionConfig{
+ NatsURI: cfg.NatsURI,
+ NatsSecure: cfg.NatsSecure,
+ NatsSkipVerify: cfg.NatsSkipVerify,
+ NatsCertFile: cfg.NatsCertFile,
+ NatsKeyFile: cfg.NatsKeyFile,
+ NatsCAFile: cfg.NatsCAFile,
+ NatsConnectTimeout: cfg.NatsConnectTimeout,
+ })
}
func newStorageClient(cfg *config.Config) *minio.Client {
@@ -710,3 +812,13 @@ func getMongoSSLConfig(cfg *config.Config, secretClient *secret.Client) *storage
SSLCertificateAuthoritiyFile: rootCAPath,
}
}
+
+func newGRPCTransportCredentials(cfg *config.Config) (credentials.TransportCredentials, error) {
+ return logsclient.GetGrpcTransportCredentials(logsclient.GrpcConnectionConfig{
+ Secure: cfg.LogServerSecure,
+ SkipVerify: cfg.LogServerSkipVerify,
+ CertFile: cfg.LogServerCertFile,
+ KeyFile: cfg.LogServerKeyFile,
+ CAFile: cfg.LogServerCAFile,
+ })
+}
diff --git a/cmd/kubectl-testkube/commands/abort.go b/cmd/kubectl-testkube/commands/abort.go
index 57156e36ddb..13d6ba59752 100644
--- a/cmd/kubectl-testkube/commands/abort.go
+++ b/cmd/kubectl-testkube/commands/abort.go
@@ -7,6 +7,7 @@ import (
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/validator"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/tests"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsuites"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testworkflows"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/config"
"github.com/kubeshop/testkube/pkg/ui"
)
@@ -32,6 +33,8 @@ func NewAbortCmd() *cobra.Command {
cmd.AddCommand(tests.NewAbortExecutionsCmd())
cmd.AddCommand(testsuites.NewAbortTestSuiteExecutionCmd())
cmd.AddCommand(testsuites.NewAbortTestSuiteExecutionsCmd())
+ cmd.AddCommand(testworkflows.NewAbortTestWorkflowExecutionCmd())
+ cmd.AddCommand(testworkflows.NewAbortTestWorkflowExecutionsCmd())
return cmd
}
diff --git a/cmd/kubectl-testkube/commands/artifacts/artifacts.go b/cmd/kubectl-testkube/commands/artifacts/artifacts.go
index 83478802691..e6deec5ebfc 100644
--- a/cmd/kubectl-testkube/commands/artifacts/artifacts.go
+++ b/cmd/kubectl-testkube/commands/artifacts/artifacts.go
@@ -3,6 +3,7 @@ package artifacts
import (
"os"
+ "github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
@@ -19,7 +20,7 @@ func NewListArtifactsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "artifact ",
Aliases: []string{"artifacts"},
- Short: "List artifacts of the given test or test suite execution name",
+ Short: "List artifacts of the given test, test suite or test workflow execution name",
Args: validator.ExecutionName,
Run: func(cmd *cobra.Command, args []string) {
executionID = args[0]
@@ -31,16 +32,29 @@ func NewListArtifactsCmd() *cobra.Command {
var artifacts testkube.Artifacts
var errArtifacts error
if err == nil && execution.Id != "" {
- artifacts, errArtifacts = client.GetExecutionArtifacts(executionID)
- ui.ExitOnError("getting test artifacts ", errArtifacts)
- } else {
- _, err := client.GetTestSuiteExecution(executionID)
- ui.ExitOnError("no test or test suite execution was found with the following id", err)
- artifacts, errArtifacts = client.GetTestSuiteExecutionArtifacts(executionID)
- ui.ExitOnError("getting test suite artifacts ", errArtifacts)
+ artifacts, errArtifacts = client.GetExecutionArtifacts(execution.Id)
+ ui.ExitOnError("getting test artifacts", errArtifacts)
+ ui.Table(artifacts, os.Stdout)
+ return
}
-
- ui.Table(artifacts, os.Stdout)
+ tsExecution, err := client.GetTestSuiteExecution(executionID)
+ if err == nil && tsExecution.Id != "" {
+ artifacts, errArtifacts = client.GetTestSuiteExecutionArtifacts(tsExecution.Id)
+ ui.ExitOnError("getting test suite artifacts", errArtifacts)
+ ui.Table(artifacts, os.Stdout)
+ return
+ }
+ twExecution, err := client.GetTestWorkflowExecution(executionID)
+ if err == nil && twExecution.Id != "" {
+ artifacts, errArtifacts = client.GetTestWorkflowExecutionArtifacts(twExecution.Id)
+ ui.ExitOnError("getting test workflow artifacts", errArtifacts)
+ ui.Table(artifacts, os.Stdout)
+ return
+ }
+ if err == nil {
+ err = errors.New("no test, test suite or test workflow execution was found with the following id")
+ }
+ ui.Fail(err)
},
}
diff --git a/cmd/kubectl-testkube/commands/cloud.go b/cmd/kubectl-testkube/commands/cloud.go
index e9902c9aa2d..3565134aac0 100644
--- a/cmd/kubectl-testkube/commands/cloud.go
+++ b/cmd/kubectl-testkube/commands/cloud.go
@@ -3,7 +3,7 @@ package commands
import (
"github.com/spf13/cobra"
- "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/cloud"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/pro"
"github.com/kubeshop/testkube/pkg/ui"
)
@@ -15,15 +15,15 @@ func NewCloudCmd() *cobra.Command {
Hidden: true,
Short: "[Deprecated] Testkube Cloud commands",
Aliases: []string{"cl"},
- Run: func(cmd *cobra.Command, args []string) {
- ui.Warn("You are using a deprecated command, please switch to `testkube pro`.")
+ PersistentPreRun: func(cmd *cobra.Command, args []string) {
+ ui.Errf("You are using a deprecated command, please switch to `testkube pro` prefix.\n\n")
},
}
- cmd.AddCommand(cloud.NewConnectCmd())
- cmd.AddCommand(cloud.NewDisconnectCmd())
- cmd.AddCommand(cloud.NewInitCmd())
- cmd.AddCommand(cloud.NewLoginCmd())
+ cmd.AddCommand(pro.NewConnectCmd())
+ cmd.AddCommand(pro.NewDisconnectCmd())
+ cmd.AddCommand(pro.NewInitCmd())
+ cmd.AddCommand(pro.NewLoginCmd())
return cmd
}
diff --git a/cmd/kubectl-testkube/commands/cloud/connect.go b/cmd/kubectl-testkube/commands/cloud/connect.go
deleted file mode 100644
index 1ffb4b58d99..00000000000
--- a/cmd/kubectl-testkube/commands/cloud/connect.go
+++ /dev/null
@@ -1,215 +0,0 @@
-package cloud
-
-import (
- "fmt"
- "os"
- "strings"
-
- "github.com/pterm/pterm"
- "github.com/spf13/cobra"
-
- "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
- "github.com/kubeshop/testkube/cmd/kubectl-testkube/config"
- cloudclient "github.com/kubeshop/testkube/pkg/cloud/client"
- "github.com/kubeshop/testkube/pkg/ui"
-)
-
-const (
- listenAddr = "127.0.0.1:8090"
- docsUrl = "https://docs.testkube.io/testkube-pro/intro"
- tokenQueryParam = "token"
-)
-
-func NewConnectCmd() *cobra.Command {
- var opts = common.HelmOptions{}
-
- cmd := &cobra.Command{
- Use: "connect",
- Deprecated: "Use `testkube pro connect` instead",
- Hidden: true,
- Aliases: []string{"c"},
- Short: "[Deprecated] Testkube Cloud connect ",
- Run: func(cmd *cobra.Command, args []string) {
- ui.Warn("You are using a deprecated command, please switch to `testkube pro connect`.")
-
- os.Exit(1)
-
- client, _, err := common.GetClient(cmd)
- ui.ExitOnError("getting client", err)
-
- info, err := client.GetServerInfo()
- firstInstall := err != nil && strings.Contains(err.Error(), "not found")
- if err != nil && !firstInstall {
- ui.Failf("Can't get testkube cluster information: %s", err.Error())
- }
-
- var apiContext string
- if actx, ok := contextDescription[info.Context]; ok {
- apiContext = actx
- }
-
- ui.H1("Connect your cloud environment:")
- ui.Paragraph("You can learn more about connecting your Testkube instance to the Cloud here:\n" + docsUrl)
- ui.H2("You can safely switch between connecting Cloud and disconnecting without losing your data.")
-
- cfg, err := config.Load()
- ui.ExitOnError("loading config", err)
-
- common.ProcessMasterFlags(cmd, &opts, &cfg)
-
- var clusterContext string
- if cfg.ContextType == config.ContextTypeKubeconfig {
- clusterContext, err = common.GetCurrentKubernetesContext()
- ui.ExitOnError("getting current kubernetes context", err)
- }
-
- // TODO: implement context info
- ui.H1("Current status of your Testkube instance")
- ui.Properties([][]string{
- {"Context", apiContext},
- {"Kubectl context", clusterContext},
- {"Namespace", cfg.Namespace},
- })
-
- newStatus := [][]string{
- {"Testkube mode"},
- {"Context", contextDescription["cloud"]},
- {"Kubectl context", clusterContext},
- {"Namespace", cfg.Namespace},
- {ui.Separator, ""},
- }
-
- var (
- token string
- refreshToken string
- )
- // if no agent is passed create new environment and get its token
- if opts.Master.AgentToken == "" && opts.Master.OrgId == "" && opts.Master.EnvId == "" {
- token, refreshToken, err = common.LoginUser(opts.Master.URIs.Auth)
- ui.ExitOnError("login", err)
-
- orgId, orgName, err := common.UiGetOrganizationId(opts.Master.URIs.Api, token)
- ui.ExitOnError("getting organization", err)
-
- envName, err := uiGetEnvName()
- ui.ExitOnError("getting environment name", err)
-
- envClient := cloudclient.NewEnvironmentsClient(opts.Master.URIs.Api, token, orgId)
- env, err := envClient.Create(cloudclient.Environment{Name: envName, Owner: orgId})
- ui.ExitOnError("creating environment", err)
-
- opts.Master.OrgId = orgId
- opts.Master.EnvId = env.Id
- opts.Master.AgentToken = env.AgentToken
-
- newStatus = append(
- newStatus,
- [][]string{
- {"Testkube will be connected to cloud org/env"},
- {"Organization Id", opts.Master.OrgId},
- {"Organization name", orgName},
- {"Environment Id", opts.Master.EnvId},
- {"Environment name", env.Name},
- {ui.Separator, ""},
- }...)
- }
-
- // validate if user created env - or was passed from flags
- if opts.Master.EnvId == "" {
- ui.Failf("You need pass valid environment id to connect to cloud")
- }
- if opts.Master.OrgId == "" {
- ui.Failf("You need pass valid organization id to connect to cloud")
- }
-
- // update summary
- newStatus = append(newStatus, []string{"Testkube support services not needed anymore"})
- newStatus = append(newStatus, []string{"MinIO ", "Stopped and scaled down, (not deleted)"})
- newStatus = append(newStatus, []string{"MongoDB ", "Stopped and scaled down, (not deleted)"})
- newStatus = append(newStatus, []string{"Dashboard", "Stopped and scaled down, (not deleted)"})
-
- ui.NL(2)
-
- ui.H1("Summary of your setup after connecting to Testkube Cloud")
- ui.Properties(newStatus)
-
- ui.NL()
- ui.Warn("Remember: All your historical data and artifacts will be safe in case you want to rollback. OSS and cloud executions will be separated.")
- ui.NL()
-
- if ok := ui.Confirm("Proceed with connecting Testkube Cloud?"); !ok {
- return
- }
-
- spinner := ui.NewSpinner("Connecting Testkube Cloud")
- err = common.HelmUpgradeOrInstallTestkubeCloud(opts, cfg, true)
- ui.ExitOnError("Installing Testkube Cloud", err)
- spinner.Success()
-
- ui.NL()
-
- // let's scale down deployment of mongo
- if opts.MongoReplicas == 0 {
- spinner = ui.NewSpinner("Scaling down MongoDB")
- common.KubectlScaleDeployment(opts.Namespace, "testkube-mongodb", opts.MongoReplicas)
- spinner.Success()
- }
- if opts.MinioReplicas == 0 {
- spinner = ui.NewSpinner("Scaling down MinIO")
- common.KubectlScaleDeployment(opts.Namespace, "testkube-minio-testkube", opts.MinioReplicas)
- spinner.Success()
- }
- if opts.DashboardReplicas == 0 {
- spinner = ui.NewSpinner("Scaling down Dashbaord")
- common.KubectlScaleDeployment(opts.Namespace, "testkube-dashboard", opts.DashboardReplicas)
- spinner.Success()
- }
-
- ui.H2("Testkube Cloud is connected to your Testkube instance, saving local configuration")
-
- ui.H2("Saving testkube cli cloud context")
- if token == "" && !common.IsUserLoggedIn(cfg, opts) {
- token, refreshToken, err = common.LoginUser(opts.Master.URIs.Auth)
- ui.ExitOnError("user login", err)
- }
- err = common.PopulateLoginDataToContext(opts.Master.OrgId, opts.Master.EnvId, token, refreshToken, opts, cfg)
-
- ui.ExitOnError("Setting cloud environment context", err)
-
- ui.NL(2)
-
- ui.ShellCommand("In case you want to roll back you can simply run the following command in your CLI:", "testkube cloud disconnect")
-
- ui.Success("You can now login to Testkube Cloud and validate your connection:")
- ui.NL()
- ui.Link(opts.Master.URIs.Ui + "/organization/" + opts.Master.OrgId + "/environment/" + opts.Master.EnvId + "/dashboard/tests")
-
- ui.NL(2)
- },
- }
-
- common.PopulateHelmFlags(cmd, &opts)
- common.PopulateMasterFlags(cmd, &opts)
-
- cmd.Flags().IntVar(&opts.MinioReplicas, "minio-replicas", 0, "MinIO replicas")
- cmd.Flags().IntVar(&opts.MongoReplicas, "mongo-replicas", 0, "MongoDB replicas")
- cmd.Flags().IntVar(&opts.DashboardReplicas, "dashboard-replicas", 0, "Dashboard replicas")
- return cmd
-}
-
-var contextDescription = map[string]string{
- "": "Unknown context, try updating your testkube cluster installation",
- "oss": "Open Source Testkube",
- "cloud": "Testkube in Cloud mode",
-}
-
-func uiGetEnvName() (string, error) {
- for i := 0; i < 3; i++ {
- if envName := ui.TextInput("Tell us the name of your environment"); envName != "" {
- return envName, nil
- }
- pterm.Error.Println("Environment name cannot be empty")
- }
-
- return "", fmt.Errorf("environment name cannot be empty")
-}
diff --git a/cmd/kubectl-testkube/commands/cloud/disconnect.go b/cmd/kubectl-testkube/commands/cloud/disconnect.go
deleted file mode 100644
index ad5de8a30db..00000000000
--- a/cmd/kubectl-testkube/commands/cloud/disconnect.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package cloud
-
-import (
- "strings"
-
- "github.com/pterm/pterm"
-
- "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 NewDisconnectCmd() *cobra.Command {
-
- var opts common.HelmOptions
-
- cmd := &cobra.Command{
- Use: "disconnect",
- Deprecated: "Use `testkube pro disconnect` instead",
- Hidden: true,
- Aliases: []string{"d"},
- Short: "[Deprecated] Switch back to Testkube OSS mode, based on active .kube/config file",
- Run: func(cmd *cobra.Command, args []string) {
- ui.Warn("You are using a deprecated command, please switch to `testkube pro disconnect`.")
-
- ui.H1("Disconnecting your cloud environment:")
- ui.Paragraph("Rolling back to your clusters testkube OSS installation")
- ui.Paragraph("If you need more details click into following link: " + docsUrl)
- ui.H2("You can safely switch between connecting Cloud and disconnecting without losing your data.")
-
- cfg, err := config.Load()
- if err != nil {
- pterm.Error.Printfln("Failed to load config file: %s", err.Error())
- return
- }
-
- client, _, err := common.GetClient(cmd)
- ui.ExitOnError("getting client", err)
-
- info, err := client.GetServerInfo()
- firstInstall := err != nil && strings.Contains(err.Error(), "not found")
- if err != nil && !firstInstall {
- ui.Failf("Can't get testkube cluster information: %s", err.Error())
- }
- var apiContext string
- if actx, ok := contextDescription[info.Context]; ok {
- apiContext = actx
- }
- var clusterContext string
- if cfg.ContextType == config.ContextTypeKubeconfig {
- clusterContext, err = common.GetCurrentKubernetesContext()
- if err != nil {
- pterm.Error.Printfln("Failed to get current kubernetes context: %s", err.Error())
- return
- }
- }
-
- // TODO: implement context info
- ui.H1("Current status of your Testkube instance")
-
- summary := [][]string{
- {"Testkube mode"},
- {"Context", apiContext},
- {"Kubectl context", clusterContext},
- {"Namespace", cfg.Namespace},
- {ui.Separator, ""},
-
- {"Testkube is connected to cloud organizations environment"},
- {"Organization Id", info.OrgId},
- {"Environment Id", info.EnvId},
- }
-
- ui.Properties(summary)
-
- if ok := ui.Confirm("Shall we disconnect your cloud environment now?"); !ok {
- return
- }
-
- ui.NL(2)
-
- spinner := ui.NewSpinner("Disonnecting from Testkube Cloud")
-
- err = common.HelmUpgradeOrInstalTestkube(opts)
- ui.ExitOnError("Installing Testkube Cloud", err)
- spinner.Success()
-
- // let's scale down deployment of mongo
- if opts.MongoReplicas > 0 {
- spinner = ui.NewSpinner("Scaling up MongoDB")
- common.KubectlScaleDeployment(opts.Namespace, "testkube-mongodb", opts.MongoReplicas)
- spinner.Success()
- }
- if opts.MinioReplicas > 0 {
- spinner = ui.NewSpinner("Scaling up MinIO")
- common.KubectlScaleDeployment(opts.Namespace, "testkube-minio-testkube", opts.MinioReplicas)
- spinner.Success()
- }
- if opts.DashboardReplicas > 0 {
- spinner = ui.NewSpinner("Scaling up Dashbaord")
- common.KubectlScaleDeployment(opts.Namespace, "testkube-dashboard", opts.DashboardReplicas)
- spinner.Success()
- }
-
- ui.NL()
- ui.Success("Disconnect finished successfully")
- ui.NL()
- ui.ShellCommand("You can now open your local Dashboard and validate the successfull disconnect", "testkube dashboard")
- },
- }
-
- // populate options
- common.PopulateHelmFlags(cmd, &opts)
- cmd.Flags().IntVar(&opts.MinioReplicas, "minio-replicas", 1, "MinIO replicas")
- cmd.Flags().IntVar(&opts.MongoReplicas, "mongo-replicas", 1, "MongoDB replicas")
- cmd.Flags().IntVar(&opts.DashboardReplicas, "dashboard-replicas", 1, "Dashboard replicas")
-
- return cmd
-}
diff --git a/cmd/kubectl-testkube/commands/cloud/init.go b/cmd/kubectl-testkube/commands/cloud/init.go
deleted file mode 100644
index c42b46d3e83..00000000000
--- a/cmd/kubectl-testkube/commands/cloud/init.go
+++ /dev/null
@@ -1,111 +0,0 @@
-package cloud
-
-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/telemetry"
- "github.com/kubeshop/testkube/pkg/ui"
-)
-
-func NewInitCmd() *cobra.Command {
- options := common.HelmOptions{
- NoMinio: true,
- NoMongo: true,
- NoDashboard: true,
- }
-
- cmd := &cobra.Command{
- Use: "init",
- Deprecated: "Use `testkube pro init` instead",
- Hidden: true,
- Short: "[Deprecated] Install Testkube Cloud Agent and connect to Testkube Cloud environment",
- Aliases: []string{"install"},
- Run: func(cmd *cobra.Command, args []string) {
- ui.Warn("You are using a deprecated command, please switch to `testkube pro init`.")
-
- ui.Info("WELCOME TO")
- ui.Logo()
-
- cfg, err := config.Load()
- ui.ExitOnError("loading config file", err)
- ui.NL()
-
- common.ProcessMasterFlags(cmd, &options, &cfg)
-
- sendAttemptTelemetry(cmd, cfg)
-
- // create new cloud uris
- if !options.NoConfirm {
- ui.Warn("This will install Testkube to the latest version. This may take a few minutes.")
- ui.Warn("Please be sure you're on valid kubectl context before continuing!")
- ui.NL()
-
- currentContext, err := common.GetCurrentKubernetesContext()
- sendErrTelemetry(cmd, cfg, "k8s_context")
- ui.ExitOnError("getting current context", err)
- ui.Alert("Current kubectl context:", currentContext)
- ui.NL()
-
- ok := ui.Confirm("Do you want to continue?")
- if !ok {
- ui.Errf("Testkube installation cancelled")
- sendErrTelemetry(cmd, cfg, "user_cancel")
- return
- }
- }
-
- spinner := ui.NewSpinner("Installing Testkube")
- err = common.HelmUpgradeOrInstallTestkubeCloud(options, cfg, false)
- sendErrTelemetry(cmd, cfg, "helm_install")
- ui.ExitOnError("Installing Testkube", err)
- spinner.Success()
-
- ui.NL()
-
- ui.H2("Saving testkube cli cloud context")
- var token, refreshToken string
- if !common.IsUserLoggedIn(cfg, options) {
- token, refreshToken, err = common.LoginUser(options.Master.URIs.Auth)
- sendErrTelemetry(cmd, cfg, "login")
- ui.ExitOnError("user login", err)
- }
- err = common.PopulateLoginDataToContext(options.Master.OrgId, options.Master.EnvId, token, refreshToken, options, cfg)
- sendErrTelemetry(cmd, cfg, "setting_context")
- ui.ExitOnError("Setting cloud environment context", err)
-
- ui.Info(" Happy Testing! 🚀")
- ui.NL()
- },
- }
-
- common.PopulateHelmFlags(cmd, &options)
- common.PopulateMasterFlags(cmd, &options)
-
- cmd.Flags().BoolVar(&options.MultiNamespace, "multi-namespace", false, "multi namespace mode")
- cmd.Flags().BoolVar(&options.NoOperator, "no-operator", false, "should operator be installed (for more instances in multi namespace mode it should be set to true)")
- return cmd
-}
-
-func sendErrTelemetry(cmd *cobra.Command, clientCfg config.Data, errType string) {
- if clientCfg.TelemetryEnabled {
- ui.Debug("collecting anonymous telemetry data, you can disable it by calling `kubectl testkube disable telemetry`")
- out, err := telemetry.SendCmdErrorEvent(cmd, common.Version, errType)
- if ui.Verbose && err != nil {
- ui.Err(err)
- }
- ui.Debug("telemetry send event response", out)
- }
-}
-
-func sendAttemptTelemetry(cmd *cobra.Command, clientCfg config.Data) {
- if clientCfg.TelemetryEnabled {
- ui.Debug("collecting anonymous telemetry data, you can disable it by calling `kubectl testkube disable telemetry`")
- out, err := telemetry.SendCmdAttemptEvent(cmd, common.Version)
- if ui.Verbose && err != nil {
- ui.Err(err)
- }
- ui.Debug("telemetry send event response", out)
- }
-}
diff --git a/cmd/kubectl-testkube/commands/cloud/login.go b/cmd/kubectl-testkube/commands/cloud/login.go
deleted file mode 100644
index 44970cd7c20..00000000000
--- a/cmd/kubectl-testkube/commands/cloud/login.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package cloud
-
-import (
- "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
- "github.com/kubeshop/testkube/cmd/kubectl-testkube/config"
- "github.com/kubeshop/testkube/pkg/ui"
-
- "github.com/spf13/cobra"
-)
-
-func NewLoginCmd() *cobra.Command {
- var opts common.HelmOptions
-
- cmd := &cobra.Command{
- Use: "login",
- Deprecated: "Use `testkube pro login` instead",
- Hidden: true,
- Aliases: []string{"l"},
- Short: "[Deprecated] Login to Testkube Pro",
- Run: func(cmd *cobra.Command, args []string) {
- token, refreshToken, err := common.LoginUser(opts.Master.URIs.Auth)
- ui.ExitOnError("getting token", err)
-
- orgID := opts.Master.OrgId
- envID := opts.Master.EnvId
-
- if orgID == "" {
- orgID, _, err = common.UiGetOrganizationId(opts.Master.URIs.Api, token)
- ui.ExitOnError("getting organization", err)
- }
- if envID == "" {
- envID, _, err = common.UiGetEnvironmentID(opts.Master.URIs.Api, token, orgID)
- ui.ExitOnError("getting environment", err)
- }
- cfg, err := config.Load()
- ui.ExitOnError("loading config file", err)
-
- common.ProcessMasterFlags(cmd, &opts, &cfg)
-
- err = common.PopulateLoginDataToContext(orgID, envID, token, refreshToken, opts, cfg)
- ui.ExitOnError("saving config file", err)
-
- ui.Success("Your config was updated with new values")
- ui.NL()
- common.UiPrintContext(cfg)
- },
- }
-
- common.PopulateMasterFlags(cmd, &opts)
-
- return cmd
-}
diff --git a/cmd/kubectl-testkube/commands/common/client.go b/cmd/kubectl-testkube/commands/common/client.go
index 709841d9396..a7cdf7d1b3a 100644
--- a/cmd/kubectl-testkube/commands/common/client.go
+++ b/cmd/kubectl-testkube/commands/common/client.go
@@ -80,10 +80,9 @@ func GetClient(cmd *cobra.Command) (client.Client, string, error) {
token := cfg.CloudContext.ApiKey
- if cfg.CloudContext.ApiKey != "" && cfg.CloudContext.RefreshToken != "" {
+ if cfg.CloudContext.ApiKey != "" && cfg.CloudContext.RefreshToken != "" && cfg.OAuth2Data.Enabled {
var refreshToken string
authURI := fmt.Sprintf("%s/idp", cfg.CloudContext.ApiUri)
-
token, refreshToken, err = cloudlogin.CheckAndRefreshToken(context.Background(), authURI, cfg.CloudContext.ApiKey, cfg.CloudContext.RefreshToken)
if err != nil {
// Error: failed refreshing, go thru login flow
diff --git a/cmd/kubectl-testkube/commands/common/flags.go b/cmd/kubectl-testkube/commands/common/flags.go
index 0d710ce2e79..eecf5a73766 100644
--- a/cmd/kubectl-testkube/commands/common/flags.go
+++ b/cmd/kubectl-testkube/commands/common/flags.go
@@ -11,24 +11,35 @@ import (
)
func CreateVariables(cmd *cobra.Command, ignoreSecretVariable bool) (vars map[string]testkube.Variable, err error) {
- basicParams, err := cmd.Flags().GetStringToString("variable")
+ basicParams, err := cmd.Flags().GetStringArray("variable")
if err != nil {
return vars, err
}
vars = map[string]testkube.Variable{}
- for k, v := range basicParams {
- vars[k] = testkube.NewBasicVariable(k, v)
+ for _, v := range basicParams {
+ values := strings.SplitN(v, "=", 2)
+ if len(values) != 2 {
+ return vars, errors.New("wrong number of variable params")
+ }
+
+ vars[values[0]] = testkube.NewBasicVariable(values[0], values[1])
}
if !ignoreSecretVariable {
- secretParams, err := cmd.Flags().GetStringToString("secret-variable")
+ secretParams, err := cmd.Flags().GetStringArray("secret-variable")
if err != nil {
return vars, err
}
- for k, v := range secretParams {
- vars[k] = testkube.NewSecretVariable(k, v)
+
+ for _, v := range secretParams {
+ values := strings.SplitN(v, "=", 2)
+ if len(values) != 2 {
+ return vars, errors.New("wrong number of secret variable params")
+ }
+
+ vars[values[0]] = testkube.NewSecretVariable(values[0], values[1])
}
}
diff --git a/cmd/kubectl-testkube/commands/common/helper.go b/cmd/kubectl-testkube/commands/common/helper.go
index f3540d3b4a6..04f5178f8cd 100644
--- a/cmd/kubectl-testkube/commands/common/helper.go
+++ b/cmd/kubectl-testkube/commands/common/helper.go
@@ -21,9 +21,9 @@ import (
)
type HelmOptions struct {
- Name, Namespace, Chart, Values string
- NoDashboard, NoMinio, NoMongo, NoConfirm bool
- MinioReplicas, MongoReplicas, DashboardReplicas int
+ Name, Namespace, Chart, Values string
+ NoMinio, NoMongo, NoConfirm bool
+ MinioReplicas, MongoReplicas int
Master config.Master
// For debug
@@ -101,13 +101,11 @@ func HelmUpgradeOrInstallTestkubeCloud(options HelmOptions, cfg config.Data, isM
args = append(args, "--set", fmt.Sprintf("testkube-api.multinamespace.enabled=%t", options.MultiNamespace))
args = append(args, "--set", fmt.Sprintf("testkube-operator.enabled=%t", !options.NoOperator))
- args = append(args, "--set", fmt.Sprintf("testkube-dashboard.enabled=%t", !options.NoDashboard))
args = append(args, "--set", fmt.Sprintf("mongodb.enabled=%t", !options.NoMongo))
args = append(args, "--set", fmt.Sprintf("testkube-api.minio.enabled=%t", !options.NoMinio))
args = append(args, "--set", fmt.Sprintf("testkube-api.minio.replicas=%d", options.MinioReplicas))
args = append(args, "--set", fmt.Sprintf("mongodb.replicas=%d", options.MongoReplicas))
- args = append(args, "--set", fmt.Sprintf("testkube-dashboard.replicas=%d", options.DashboardReplicas))
args = append(args, options.Name, options.Chart)
@@ -147,7 +145,6 @@ func HelmUpgradeOrInstalTestkube(options HelmOptions) error {
args = []string{"upgrade", "--install", "--create-namespace", "--namespace", options.Namespace}
args = append(args, "--set", fmt.Sprintf("testkube-api.multinamespace.enabled=%t", options.MultiNamespace))
args = append(args, "--set", fmt.Sprintf("testkube-operator.enabled=%t", !options.NoOperator))
- args = append(args, "--set", fmt.Sprintf("testkube-dashboard.enabled=%t", !options.NoDashboard))
args = append(args, "--set", fmt.Sprintf("mongodb.enabled=%t", !options.NoMongo))
args = append(args, "--set", fmt.Sprintf("testkube-api.minio.enabled=%t", !options.NoMinio))
if options.NoMinio {
@@ -178,7 +175,6 @@ func PopulateHelmFlags(cmd *cobra.Command, options *HelmOptions) {
cmd.Flags().StringVar(&options.Values, "values", "", "path to Helm values file")
cmd.Flags().BoolVar(&options.NoMinio, "no-minio", false, "don't install MinIO")
- cmd.Flags().BoolVar(&options.NoDashboard, "no-dashboard", false, "don't install dashboard")
cmd.Flags().BoolVar(&options.NoMongo, "no-mongo", false, "don't install MongoDB")
cmd.Flags().BoolVar(&options.NoConfirm, "no-confirm", false, "don't ask for confirmation - unatended installation mode")
cmd.Flags().BoolVar(&options.DryRun, "dry-run", false, "dry run mode - only print commands that would be executed")
diff --git a/cmd/kubectl-testkube/commands/common/render/common.go b/cmd/kubectl-testkube/commands/common/render/common.go
index af161a62b92..1494891342a 100644
--- a/cmd/kubectl-testkube/commands/common/render/common.go
+++ b/cmd/kubectl-testkube/commands/common/render/common.go
@@ -65,13 +65,16 @@ func RenderPrettyList(obj ui.TableData, w io.Writer) error {
return nil
}
-func RenderExecutionResult(client client.Client, execution *testkube.Execution, logsOnly bool) error {
+func RenderExecutionResult(client client.Client, execution *testkube.Execution, logsOnly bool, showLogs bool) error {
result := execution.ExecutionResult
if result == nil {
ui.Errf("got execution without `Result`")
return nil
}
+ info, err := client.GetServerInfo()
+ ui.ExitOnError("getting server info", err)
+
ui.NL()
switch true {
case result.IsQueued():
@@ -81,14 +84,13 @@ func RenderExecutionResult(client client.Client, execution *testkube.Execution,
ui.Warn("Test execution started")
case result.IsPassed():
- ui.Info(result.Output)
+ if showLogs {
+ PrintLogs(client, info, *execution)
+ }
+
if !logsOnly {
duration := execution.EndTime.Sub(execution.StartTime)
ui.Success("Test execution completed with success in " + duration.String())
-
- info, err := client.GetServerInfo()
- ui.ExitOnError("getting server info", err)
-
PrintExecutionURIs(execution, info.DashboardUri)
}
@@ -105,14 +107,12 @@ func RenderExecutionResult(client client.Client, execution *testkube.Execution,
ui.UseStderr()
ui.Warn("Test execution failed:\n")
ui.Errf(result.ErrorMessage)
-
- info, err := client.GetServerInfo()
- ui.ExitOnError("getting server info", err)
-
PrintExecutionURIs(execution, info.DashboardUri)
}
- ui.Info(result.Output)
+ if showLogs {
+ PrintLogs(client, info, *execution)
+ }
return errors.New(result.ErrorMessage)
default:
@@ -124,13 +124,43 @@ func RenderExecutionResult(client client.Client, execution *testkube.Execution,
ui.Errf(result.ErrorMessage)
}
- ui.Info(result.Output)
+ if showLogs {
+ PrintLogs(client, info, *execution)
+ }
return errors.New(result.ErrorMessage)
}
return nil
}
+func PrintLogs(client client.Client, info testkube.ServerInfo, execution testkube.Execution) {
+ if !info.Features.LogsV2 {
+ // fallback to default logs
+ ui.Info(execution.ExecutionResult.Output)
+ return
+ }
+
+ logsCh, err := client.LogsV2(execution.Id)
+ ui.ExitOnError("getting logs", err)
+
+ ui.H1("Logs:")
+ lastSource := ""
+ for log := range logsCh {
+
+ if log.Source != lastSource {
+ ui.H2("source: " + log.Source)
+ ui.NL()
+ lastSource = log.Source
+ }
+
+ if ui.Verbose {
+ ui.Print(log.Time.Format("2006-01-02 15:04:05") + " " + log.Content)
+ } else {
+ ui.Print(log.Content)
+ }
+ }
+}
+
func PrintExecutionURIs(execution *testkube.Execution, dashboardURI string) {
ui.NL()
ui.Link("Test URI:", fmt.Sprintf("%s/tests/%s", dashboardURI, execution.TestName))
diff --git a/cmd/kubectl-testkube/commands/common/render/list.go b/cmd/kubectl-testkube/commands/common/render/list.go
index 5eef452673f..6ec89864f51 100644
--- a/cmd/kubectl-testkube/commands/common/render/list.go
+++ b/cmd/kubectl-testkube/commands/common/render/list.go
@@ -3,6 +3,7 @@ package render
import (
"fmt"
"io"
+ "reflect"
"github.com/spf13/cobra"
@@ -25,10 +26,14 @@ func List(cmd *cobra.Command, obj interface{}, w io.Writer) error {
return RenderJSON(obj, w)
case OutputGoTemplate:
tpl := cmd.Flag("go-template").Value.String()
- list, ok := obj.([]interface{})
- if !ok {
+ value := reflect.ValueOf(obj)
+ if value.Kind() != reflect.Slice {
return fmt.Errorf("can't render, need list type but got: %+v", obj)
}
+ list := make([]interface{}, value.Len())
+ for i := 0; i < value.Len(); i++ {
+ list[i] = value.Index(i).Interface()
+ }
return RenderGoTemplateList(list, w, tpl)
default:
return RenderYaml(obj, w)
diff --git a/cmd/kubectl-testkube/commands/common/render/obj.go b/cmd/kubectl-testkube/commands/common/render/obj.go
index f1a23fc7d0f..110ad14e826 100644
--- a/cmd/kubectl-testkube/commands/common/render/obj.go
+++ b/cmd/kubectl-testkube/commands/common/render/obj.go
@@ -1,7 +1,6 @@
package render
import (
- "fmt"
"io"
"github.com/spf13/cobra"
@@ -11,7 +10,11 @@ import (
)
func Obj(cmd *cobra.Command, obj interface{}, w io.Writer, renderer ...CliObjRenderer) error {
- outputType := OutputType(cmd.Flag("output").Value.String())
+ outputFlag := cmd.Flag("output")
+ outputType := OutputPretty
+ if outputFlag != nil {
+ outputType = OutputType(outputFlag.Value.String())
+ }
switch outputType {
case OutputPretty:
@@ -30,12 +33,7 @@ func Obj(cmd *cobra.Command, obj interface{}, w io.Writer, renderer ...CliObjRen
return RenderJSON(obj, w)
case OutputGoTemplate:
tpl := cmd.Flag("go-template").Value.String()
- // need to make type assetion to list first
- list, ok := obj.([]interface{})
- if !ok {
- return fmt.Errorf("can't render, need list type but got: %+v", obj)
- }
- return RenderGoTemplateList(list, w, tpl)
+ return RenderGoTemplate(obj, w, tpl)
default:
return RenderYaml(obj, w)
}
diff --git a/cmd/kubectl-testkube/commands/common/repository.go b/cmd/kubectl-testkube/commands/common/repository.go
index f714537d987..80f8bf1897e 100644
--- a/cmd/kubectl-testkube/commands/common/repository.go
+++ b/cmd/kubectl-testkube/commands/common/repository.go
@@ -2,15 +2,21 @@ package common
import (
"fmt"
+ "strconv"
"github.com/spf13/cobra"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/ui"
)
-func hasGitParamsInCmd(cmd *cobra.Command) bool {
- var fields = []string{"git-uri", "git-branch", "git-commit", "git-path", "git-username", "git-token",
+func hasGitParamsInCmd(cmd *cobra.Command, crdOnly bool) bool {
+ var fields = []string{"git-uri", "git-branch", "git-commit", "git-path",
"git-username-secret", "git-token-secret", "git-working-dir", "git-certificate-secret", "git-auth-type"}
+ if !crdOnly {
+ fields = append(fields, "git-username", "git-token")
+ }
+
for _, field := range fields {
if cmd.Flag(field).Changed {
return true
@@ -22,12 +28,30 @@ func hasGitParamsInCmd(cmd *cobra.Command) bool {
// NewRepositoryFromFlags creates repository from command flags
func NewRepositoryFromFlags(cmd *cobra.Command) (repository *testkube.Repository, err error) {
+ crdOnly, err := strconv.ParseBool(cmd.Flag("crd-only").Value.String())
+ if err != nil {
+ return nil, err
+ }
+
gitUri := cmd.Flag("git-uri").Value.String()
gitBranch := cmd.Flag("git-branch").Value.String()
gitCommit := cmd.Flag("git-commit").Value.String()
gitPath := cmd.Flag("git-path").Value.String()
- gitUsername := cmd.Flag("git-username").Value.String()
- gitToken := cmd.Flag("git-token").Value.String()
+
+ var gitUsername, gitToken string
+ if !crdOnly {
+ client, _, err := GetClient(cmd)
+ ui.ExitOnError("getting client", err)
+
+ info, err := client.GetServerInfo()
+ ui.ExitOnError("getting server info", err)
+
+ if !info.DisableSecretCreation {
+ gitUsername = cmd.Flag("git-username").Value.String()
+ gitToken = cmd.Flag("git-token").Value.String()
+ }
+ }
+
gitUsernameSecret, err := cmd.Flags().GetStringToString("git-username-secret")
if err != nil {
return nil, err
@@ -42,7 +66,7 @@ func NewRepositoryFromFlags(cmd *cobra.Command) (repository *testkube.Repository
gitCertificateSecret := cmd.Flag("git-certificate-secret").Value.String()
gitAuthType := cmd.Flag("git-auth-type").Value.String()
- hasGitParams := hasGitParamsInCmd(cmd)
+ hasGitParams := hasGitParamsInCmd(cmd, crdOnly)
if !hasGitParams {
return nil, nil
}
@@ -101,14 +125,6 @@ func NewRepositoryUpdateFromFlags(cmd *cobra.Command) (repository *testkube.Repo
"git-path",
&repository.Path,
},
- {
- "git-username",
- &repository.Username,
- },
- {
- "git-token",
- &repository.Token,
- },
{
"git-working-dir",
&repository.WorkingDir,
@@ -123,6 +139,27 @@ func NewRepositoryUpdateFromFlags(cmd *cobra.Command) (repository *testkube.Repo
},
}
+ client, _, err := GetClient(cmd)
+ ui.ExitOnError("getting client", err)
+
+ info, err := client.GetServerInfo()
+ ui.ExitOnError("getting server info", err)
+
+ if !info.DisableSecretCreation {
+ fields = append(fields, []struct {
+ name string
+ destination **string
+ }{
+ {
+ "git-username",
+ &repository.Username,
+ },
+ {
+ "git-token",
+ &repository.Token,
+ }}...)
+ }
+
var nonEmpty bool
for _, field := range fields {
if cmd.Flag(field.name).Changed {
@@ -174,11 +211,29 @@ func NewRepositoryUpdateFromFlags(cmd *cobra.Command) (repository *testkube.Repo
// ValidateUpsertOptions validates upsert options
func ValidateUpsertOptions(cmd *cobra.Command, sourceName string) error {
+ crdOnly, err := strconv.ParseBool(cmd.Flag("crd-only").Value.String())
+ if err != nil {
+ return err
+ }
+
gitUri := cmd.Flag("git-uri").Value.String()
gitBranch := cmd.Flag("git-branch").Value.String()
gitCommit := cmd.Flag("git-commit").Value.String()
- gitUsername := cmd.Flag("git-username").Value.String()
- gitToken := cmd.Flag("git-token").Value.String()
+
+ var gitUsername, gitToken string
+ if !crdOnly {
+ client, _, err := GetClient(cmd)
+ ui.ExitOnError("getting client", err)
+
+ info, err := client.GetServerInfo()
+ ui.ExitOnError("getting server info", err)
+
+ if !info.DisableSecretCreation {
+ gitUsername = cmd.Flag("git-username").Value.String()
+ gitToken = cmd.Flag("git-token").Value.String()
+ }
+ }
+
gitUsernameSecret, err := cmd.Flags().GetStringToString("git-username-secret")
if err != nil {
return err
@@ -193,7 +248,7 @@ func ValidateUpsertOptions(cmd *cobra.Command, sourceName string) error {
file := cmd.Flag("file").Value.String()
uri := cmd.Flag("uri").Value.String()
- hasGitParams := hasGitParamsInCmd(cmd)
+ hasGitParams := hasGitParamsInCmd(cmd, crdOnly)
if hasGitParams && uri != "" {
return fmt.Errorf("found git params and `--uri` flag, please use `--git-uri` for git based repo or `--uri` without git based params")
}
diff --git a/cmd/kubectl-testkube/commands/common/validator/cloudcontext.go b/cmd/kubectl-testkube/commands/common/validator/cloudcontext.go
index 558cba14230..e4c85cd3d0f 100644
--- a/cmd/kubectl-testkube/commands/common/validator/cloudcontext.go
+++ b/cmd/kubectl-testkube/commands/common/validator/cloudcontext.go
@@ -12,11 +12,11 @@ func ValidateCloudContext(cfg config.Data) error {
}
if cfg.CloudContext.ApiUri == "" {
- return errors.New("please provide Testkube Cloud URI")
+ return errors.New("please provide Testkube Pro URI")
}
if cfg.CloudContext.ApiKey == "" {
- return errors.New("please provide Testkube Cloud API token")
+ return errors.New("please provide Testkube Pro API token")
}
if cfg.CloudContext.EnvironmentId == "" {
diff --git a/cmd/kubectl-testkube/commands/config.go b/cmd/kubectl-testkube/commands/config.go
index e6457ed6d54..699c5d32807 100644
--- a/cmd/kubectl-testkube/commands/config.go
+++ b/cmd/kubectl-testkube/commands/config.go
@@ -32,8 +32,6 @@ func NewConfigCmd() *cobra.Command {
cmd.AddCommand(oauth.NewConfigureOAuthCmd())
cmd.AddCommand(commands.NewConfigureAPIServerNameCmd())
cmd.AddCommand(commands.NewConfigureAPIServerPortCmd())
- cmd.AddCommand(commands.NewConfigureDashboardNameCmd())
- cmd.AddCommand(commands.NewConfigureDashboardPortCmd())
return cmd
}
diff --git a/cmd/kubectl-testkube/commands/config/dashboard_name.go b/cmd/kubectl-testkube/commands/config/dashboard_name.go
deleted file mode 100644
index 372cae2dff4..00000000000
--- a/cmd/kubectl-testkube/commands/config/dashboard_name.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package config
-
-import (
- "fmt"
-
- "github.com/spf13/cobra"
-
- "github.com/kubeshop/testkube/cmd/kubectl-testkube/config"
- "github.com/kubeshop/testkube/pkg/ui"
-)
-
-// NewConfigureDashboardNameCmd is dashboard name config command
-func NewConfigureDashboardNameCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "dashboard-name ",
- Short: "Set dashboard name for testkube client",
- Args: func(cmd *cobra.Command, args []string) error {
- if len(args) < 1 {
- return fmt.Errorf("please pass valid dashboard name value")
- }
-
- return nil
- },
- Run: func(cmd *cobra.Command, args []string) {
- cfg, err := config.Load()
- ui.ExitOnError("loading config file", err)
-
- cfg.DashboardName = args[0]
- err = config.Save(cfg)
- ui.ExitOnError("saving config file", err)
- ui.Success("New dashboard name set to", cfg.DashboardName)
- },
- }
-
- return cmd
-}
diff --git a/cmd/kubectl-testkube/commands/config/dashboard_port.go b/cmd/kubectl-testkube/commands/config/dashboard_port.go
deleted file mode 100644
index 90d4231e32e..00000000000
--- a/cmd/kubectl-testkube/commands/config/dashboard_port.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package config
-
-import (
- "fmt"
- "strconv"
-
- "github.com/spf13/cobra"
-
- "github.com/kubeshop/testkube/cmd/kubectl-testkube/config"
- "github.com/kubeshop/testkube/pkg/ui"
-)
-
-// NewConfigureDashboardPortCmd is dashboard port config command
-func NewConfigureDashboardPortCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "dashboard-port ",
- Short: "Set dashboard port for testkube client",
- Args: func(cmd *cobra.Command, args []string) error {
- if len(args) < 1 {
- return fmt.Errorf("please pass valid dashboard port value")
- }
-
- if _, err := strconv.Atoi(args[0]); err != nil {
- return fmt.Errorf("please pass integer dashboard port value: %w", err)
- }
-
- return nil
- },
- Run: func(cmd *cobra.Command, args []string) {
- cfg, err := config.Load()
- ui.ExitOnError("loading config file", err)
-
- port, err := strconv.Atoi(args[0])
- ui.ExitOnError("converting port value", err)
-
- cfg.DashboardPort = port
- err = config.Save(cfg)
- ui.ExitOnError("saving config file", err)
- ui.Success("New dashboard port set to", strconv.Itoa(cfg.DashboardPort))
- },
- }
-
- return cmd
-}
diff --git a/cmd/kubectl-testkube/commands/create.go b/cmd/kubectl-testkube/commands/create.go
index 46de9b05cc9..c428b011f0c 100644
--- a/cmd/kubectl-testkube/commands/create.go
+++ b/cmd/kubectl-testkube/commands/create.go
@@ -10,6 +10,8 @@ import (
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/tests"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsources"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsuites"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testworkflows"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testworkflowtemplates"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/webhooks"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/config"
"github.com/kubeshop/testkube/pkg/ui"
@@ -43,6 +45,8 @@ func NewCreateCmd() *cobra.Command {
cmd.AddCommand(executors.NewCreateExecutorCmd())
cmd.AddCommand(testsources.NewCreateTestSourceCmd())
cmd.AddCommand(templates.NewCreateTemplateCmd())
+ cmd.AddCommand(testworkflows.NewCreateTestWorkflowCmd())
+ cmd.AddCommand(testworkflowtemplates.NewCreateTestWorkflowTemplateCmd())
cmd.PersistentFlags().BoolVar(&crdOnly, "crd-only", false, "generate only crd")
diff --git a/cmd/kubectl-testkube/commands/dashboard.go b/cmd/kubectl-testkube/commands/dashboard.go
index 98adfe68bfd..93c8378bf14 100644
--- a/cmd/kubectl-testkube/commands/dashboard.go
+++ b/cmd/kubectl-testkube/commands/dashboard.go
@@ -1,70 +1,34 @@
package commands
import (
- "errors"
"fmt"
- "net"
- "os"
- "os/exec"
- "os/signal"
- "strconv"
- "time"
"github.com/skratchdot/open-golang/open"
"github.com/spf13/cobra"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/config"
- "github.com/kubeshop/testkube/pkg/http"
- "github.com/kubeshop/testkube/pkg/process"
"github.com/kubeshop/testkube/pkg/ui"
)
-const maxPortNumber = 65535
-
// NewDashboardCmd is a method to create new dashboard command
func NewDashboardCmd() *cobra.Command {
- var (
- useGlobalDashboard bool
- )
-
cmd := &cobra.Command{
Use: "dashboard",
Aliases: []string{"d", "open-dashboard"},
- Short: "Open testkube dashboard",
- Long: `Open testkube dashboard`,
+ Short: "Open Testkube Pro/Enterprise dashboard",
+ Long: `Open Testkube Pro/Enterprise dashboard`,
Run: func(cmd *cobra.Command, args []string) {
cfg, err := config.Load()
ui.ExitOnError("loading config file", err)
- if cfg.APIServerName == "" {
- cfg.APIServerName = config.APIServerName
- }
-
- if cfg.APIServerPort == 0 {
- cfg.APIServerPort = config.APIServerPort
- }
-
- if cfg.DashboardName == "" {
- cfg.DashboardName = config.DashboardName
- }
-
- if cfg.DashboardPort == 0 {
- cfg.DashboardPort = config.DashboardPort
- }
-
- namespace := cmd.Flag("namespace").Value.String()
-
- if cfg.ContextType == config.ContextTypeCloud {
- openCloudDashboard(cfg)
-
+ if cfg.ContextType != config.ContextTypeCloud {
+ ui.Warn("As of 1.17 the dashboard is no longer included with Testkube OSS - please refer to https://bit.ly/tk-dashboard for more info")
} else {
- openLocalDashboard(cmd, cfg, useGlobalDashboard, namespace)
+ openCloudDashboard(cfg)
}
-
},
}
- cmd.Flags().BoolVar(&useGlobalDashboard, "use-global-dashboard", false, "use global dashboard for viewing testkube results")
return cmd
}
@@ -74,125 +38,3 @@ func openCloudDashboard(cfg config.Data) {
err := open.Run(uri)
ui.PrintOnError("openning dashboard", err)
}
-
-func openLocalDashboard(cmd *cobra.Command, cfg config.Data, useGlobalDashboard bool, namespace string) {
-
- dashboardLocalPort, err := getDashboardLocalPort(cfg.APIServerPort)
- ui.PrintOnError("checking dashboard port", err)
-
- uri := fmt.Sprintf("http://localhost:%d", dashboardLocalPort)
- if useGlobalDashboard {
- uri = DashboardURI
- }
-
- apiURI := fmt.Sprintf("localhost:%d/%s", cfg.APIServerPort, ApiVersion)
- dashboardAddress := fmt.Sprintf("%s/apiEndpoint?apiEndpoint=%s", uri, apiURI)
- apiAddress := fmt.Sprintf("http://%s", apiURI)
-
- var commandsToKill []*exec.Cmd
-
- // kill background port-forwarded processes
- defer func() {
- for _, command := range commandsToKill {
- if command != nil {
- err := command.Process.Kill()
- ui.PrintOnError("killing command: "+command.String(), err)
- }
- }
- }()
-
- // if not global dasboard - we'll try to port-forward current cluster API
- if !useGlobalDashboard {
- command, err := asyncPortForward(namespace, cfg.DashboardName, dashboardLocalPort, cfg.DashboardPort)
- ui.PrintOnError("port forwarding dashboard endpoint", err)
- commandsToKill = append(commandsToKill, command)
- }
-
- command, err := asyncPortForward(namespace, cfg.APIServerName, cfg.APIServerPort, cfg.APIServerPort)
- ui.PrintOnError("port forwarding api endpoint", err)
- commandsToKill = append(commandsToKill, command)
-
- // check for api and dasboard to be ready
- ready, err := readinessCheck(cmd, apiAddress, dashboardAddress)
- ui.PrintOnError("checking readiness of services", err)
- ui.Debug("Endpoints readiness", fmt.Sprintf("%v", ready))
-
- // open browser
- err = open.Run(dashboardAddress)
- ui.PrintOnError("openning dashboard", err)
-
- // wait for Ctrl/Cmd + c signal to clear everything
- c := make(chan os.Signal, 1)
- signal.Notify(c, os.Interrupt)
-
- ui.NL()
- ui.Success("The dashboard is accessible here:", dashboardAddress)
- ui.Success("The API is accessible here:", apiAddress+"/info")
- ui.Success("Port forwarding is started for the test results endpoint, hit Ctrl+c (or Cmd+c) to stop")
-
- s := <-c
- fmt.Println("Got signal:", s)
-}
-
-func readinessCheck(cmd *cobra.Command, apiURI, dashboardURI string) (bool, error) {
- const readinessCheckTimeout = 30 * time.Second
- insecure, err := strconv.ParseBool(cmd.Flag("insecure").Value.String())
- if err != nil {
- return false, fmt.Errorf("parsing flag value %w", err)
- }
-
- client := http.NewClient(insecure)
-
- ticker := time.NewTicker(readinessCheckTimeout)
- defer ticker.Stop()
-
- for {
- select {
- case <-ticker.C:
- return false, fmt.Errorf("timed-out waiting for dashboard and api")
- default:
- apiResp, err := client.Get(apiURI + "/info")
- if err != nil {
- continue
- }
- dashboardResp, err := client.Get(dashboardURI)
- if err != nil {
- continue
- }
-
- if apiResp.StatusCode < 400 && dashboardResp.StatusCode < 400 {
- return true, nil
- }
- }
- }
-}
-
-func asyncPortForward(namespace, deploymentName string, localPort, clusterPort int) (command *exec.Cmd, err error) {
- fullDeploymentName := fmt.Sprintf("deployment/%s", deploymentName)
- ports := fmt.Sprintf("%d:%d", localPort, clusterPort)
- return process.ExecuteAsync("kubectl", "port-forward", "--namespace", namespace, fullDeploymentName, ports)
-}
-
-func localPortCheck(port int) error {
- ln, err := net.Listen("tcp", ":"+fmt.Sprint(port))
- if err != nil {
- return err
- }
-
- ln.Close()
- return nil
-}
-
-func getDashboardLocalPort(apiServerPort int) (int, error) {
- for port := DashboardLocalPort; port <= maxPortNumber; port++ {
- if port == apiServerPort {
- continue
- }
-
- if localPortCheck(port) == nil {
- return port, nil
- }
- }
-
- return 0, errors.New("no available local port")
-}
diff --git a/cmd/kubectl-testkube/commands/delete.go b/cmd/kubectl-testkube/commands/delete.go
index 8f8cfa87727..e3ae7f95db7 100644
--- a/cmd/kubectl-testkube/commands/delete.go
+++ b/cmd/kubectl-testkube/commands/delete.go
@@ -10,6 +10,8 @@ import (
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/tests"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsources"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsuites"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testworkflows"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testworkflowtemplates"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/webhooks"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/config"
"github.com/kubeshop/testkube/pkg/ui"
@@ -42,6 +44,8 @@ func NewDeleteCmd() *cobra.Command {
cmd.AddCommand(executors.NewDeleteExecutorCmd())
cmd.AddCommand(testsources.NewDeleteTestSourceCmd())
cmd.AddCommand(templates.NewDeleteTemplateCmd())
+ cmd.AddCommand(testworkflows.NewDeleteTestWorkflowCmd())
+ cmd.AddCommand(testworkflowtemplates.NewDeleteTestWorkflowTemplateCmd())
return cmd
}
diff --git a/cmd/kubectl-testkube/commands/download.go b/cmd/kubectl-testkube/commands/download.go
index b6f72524cb7..90dd47e57b0 100644
--- a/cmd/kubectl-testkube/commands/download.go
+++ b/cmd/kubectl-testkube/commands/download.go
@@ -90,10 +90,22 @@ func NewDownloadSingleArtifactsCmd() *cobra.Command {
client, _, err := common.GetClient(cmd)
ui.ExitOnError("getting client", err)
- f, err := client.DownloadFile(executionID, filename, destination)
- ui.ExitOnError("downloading file"+filename, err)
-
- ui.Info(fmt.Sprintf("File %s downloaded.\n", f))
+ execution, err := client.GetExecution(executionID)
+ if err == nil && execution.Id != "" {
+ f, err := client.DownloadFile(executionID, filename, destination)
+ ui.ExitOnError("downloading file "+filename, err)
+ ui.Info(fmt.Sprintf("File %s downloaded.\n", f))
+ return
+ }
+ twExecution, err := client.GetTestWorkflowExecution(executionID)
+ if err == nil && twExecution.Id != "" {
+ f, err := client.DownloadTestWorkflowArtifact(executionID, filename, destination)
+ ui.ExitOnError("downloading file "+filename, err)
+ ui.Info(fmt.Sprintf("File %s downloaded.\n", f))
+ return
+ }
+
+ ui.ExitOnError("retrieving execution", err)
},
}
@@ -119,7 +131,16 @@ func NewDownloadAllArtifactsCmd() *cobra.Command {
client, _, err := common.GetClient(cmd)
ui.ExitOnError("getting client", err)
- tests.DownloadArtifacts(executionID, downloadDir, format, masks, client)
+ execution, err := client.GetExecution(executionID)
+ if err == nil && execution.Id != "" {
+ tests.DownloadTestArtifacts(executionID, downloadDir, format, masks, client)
+ return
+ }
+ twExecution, err := client.GetTestWorkflowExecution(executionID)
+ if err == nil && twExecution.Id != "" {
+ tests.DownloadTestWorkflowArtifacts(executionID, downloadDir, format, masks, client)
+ return
+ }
},
}
diff --git a/cmd/kubectl-testkube/commands/get.go b/cmd/kubectl-testkube/commands/get.go
index b0355e79623..3e07c70e49a 100644
--- a/cmd/kubectl-testkube/commands/get.go
+++ b/cmd/kubectl-testkube/commands/get.go
@@ -12,6 +12,8 @@ import (
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/tests"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsources"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsuites"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testworkflows"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testworkflowtemplates"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/webhooks"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/config"
"github.com/kubeshop/testkube/pkg/ui"
@@ -48,6 +50,9 @@ func NewGetCmd() *cobra.Command {
cmd.AddCommand(testsources.NewGetTestSourceCmd())
cmd.AddCommand(context.NewGetContextCmd())
cmd.AddCommand(templates.NewGetTemplateCmd())
+ cmd.AddCommand(testworkflows.NewGetTestWorkflowsCmd())
+ cmd.AddCommand(testworkflows.NewGetTestWorkflowExecutionsCmd())
+ cmd.AddCommand(testworkflowtemplates.NewGetTestWorkflowTemplatesCmd())
cmd.PersistentFlags().StringP("output", "o", "pretty", "output type can be one of json|yaml|pretty|go-template")
cmd.PersistentFlags().StringP("go-template", "", "{{.}}", "go template to render")
diff --git a/cmd/kubectl-testkube/commands/pro/connect.go b/cmd/kubectl-testkube/commands/pro/connect.go
index dab60d7ff13..fb37a5d5eaf 100644
--- a/cmd/kubectl-testkube/commands/pro/connect.go
+++ b/cmd/kubectl-testkube/commands/pro/connect.go
@@ -153,11 +153,6 @@ func NewConnectCmd() *cobra.Command {
common.KubectlScaleDeployment(opts.Namespace, "testkube-minio-testkube", opts.MinioReplicas)
spinner.Success()
}
- if opts.DashboardReplicas == 0 {
- spinner = ui.NewSpinner("Scaling down Dashbaord")
- common.KubectlScaleDeployment(opts.Namespace, "testkube-dashboard", opts.DashboardReplicas)
- spinner.Success()
- }
ui.H2("Testkube Pro is connected to your Testkube instance, saving local configuration")
@@ -187,7 +182,6 @@ func NewConnectCmd() *cobra.Command {
cmd.Flags().IntVar(&opts.MinioReplicas, "minio-replicas", 0, "MinIO replicas")
cmd.Flags().IntVar(&opts.MongoReplicas, "mongo-replicas", 0, "MongoDB replicas")
- cmd.Flags().IntVar(&opts.DashboardReplicas, "dashboard-replicas", 0, "Dashboard replicas")
return cmd
}
diff --git a/cmd/kubectl-testkube/commands/pro/disconnect.go b/cmd/kubectl-testkube/commands/pro/disconnect.go
index 8d0408af14e..07510b21990 100644
--- a/cmd/kubectl-testkube/commands/pro/disconnect.go
+++ b/cmd/kubectl-testkube/commands/pro/disconnect.go
@@ -1,6 +1,7 @@
package pro
import (
+ "fmt"
"strings"
"github.com/pterm/pterm"
@@ -23,7 +24,7 @@ func NewDisconnectCmd() *cobra.Command {
Run: func(cmd *cobra.Command, args []string) {
ui.H1("Disconnecting your Pro environment:")
- ui.Paragraph("Rolling back to your clusters testkube OSS installation")
+ ui.Paragraph("Rolling back to your clusters Testkube OSS installation")
ui.Paragraph("If you need more details click into following link: " + docsUrl)
ui.H2("You can safely switch between connecting Pro and disconnecting without losing your data.")
@@ -39,7 +40,7 @@ func NewDisconnectCmd() *cobra.Command {
info, err := client.GetServerInfo()
firstInstall := err != nil && strings.Contains(err.Error(), "not found")
if err != nil && !firstInstall {
- ui.Failf("Can't get testkube cluster information: %s", err.Error())
+ ui.Failf("Can't get Testkube cluster information: %s", err.Error())
}
var apiContext string
if actx, ok := contextDescription[info.Context]; ok {
@@ -49,7 +50,7 @@ func NewDisconnectCmd() *cobra.Command {
if cfg.ContextType == config.ContextTypeKubeconfig {
clusterContext, err = common.GetCurrentKubernetesContext()
if err != nil {
- pterm.Error.Printfln("Failed to get current kubernetes context: %s", err.Error())
+ pterm.Error.Printfln("Failed to get current Kubernetes context: %s", err.Error())
return
}
}
@@ -65,8 +66,8 @@ func NewDisconnectCmd() *cobra.Command {
{ui.Separator, ""},
{"Testkube is connected to Pro organizations environment"},
- {"Organization Id", info.OrgId},
- {"Environment Id", info.EnvId},
+ {"Organization Id", cfg.CloudContext.OrganizationId},
+ {"Environment Id", cfg.CloudContext.EnvironmentId},
}
ui.Properties(summary)
@@ -77,7 +78,7 @@ func NewDisconnectCmd() *cobra.Command {
ui.NL(2)
- spinner := ui.NewSpinner("Disonnecting from Testkube Pro")
+ spinner := ui.NewSpinner("Disconnecting from Testkube Pro")
err = common.HelmUpgradeOrInstalTestkube(opts)
ui.ExitOnError("Installing Testkube Pro", err)
@@ -94,16 +95,20 @@ func NewDisconnectCmd() *cobra.Command {
common.KubectlScaleDeployment(opts.Namespace, "testkube-minio-testkube", opts.MinioReplicas)
spinner.Success()
}
- if opts.DashboardReplicas > 0 {
- spinner = ui.NewSpinner("Scaling up Dashbaord")
- common.KubectlScaleDeployment(opts.Namespace, "testkube-dashboard", opts.DashboardReplicas)
+
+ spinner = ui.NewSpinner("Resetting Testkube config.json")
+ cfg.ContextType = config.ContextTypeKubeconfig
+ cfg.CloudContext = config.CloudContext{}
+ if err = config.Save(cfg); err != nil {
+ spinner.Fail(fmt.Sprintf("Error updating local Testkube config file: %s", err))
+ ui.Warn("Please manually remove the fields contextType and cloudContext from your config file.")
+ } else {
spinner.Success()
}
ui.NL()
ui.Success("Disconnect finished successfully")
ui.NL()
- ui.ShellCommand("You can now open your local Dashboard and validate the successfull disconnect", "testkube dashboard")
},
}
@@ -111,6 +116,5 @@ func NewDisconnectCmd() *cobra.Command {
common.PopulateHelmFlags(cmd, &opts)
cmd.Flags().IntVar(&opts.MinioReplicas, "minio-replicas", 1, "MinIO replicas")
cmd.Flags().IntVar(&opts.MongoReplicas, "mongo-replicas", 1, "MongoDB replicas")
- cmd.Flags().IntVar(&opts.DashboardReplicas, "dashboard-replicas", 1, "Dashboard replicas")
return cmd
}
diff --git a/cmd/kubectl-testkube/commands/pro/init.go b/cmd/kubectl-testkube/commands/pro/init.go
index b886cd2bdd6..7328b6b4468 100644
--- a/cmd/kubectl-testkube/commands/pro/init.go
+++ b/cmd/kubectl-testkube/commands/pro/init.go
@@ -1,6 +1,8 @@
package pro
import (
+ "fmt"
+
"github.com/spf13/cobra"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
@@ -11,9 +13,8 @@ import (
func NewInitCmd() *cobra.Command {
options := common.HelmOptions{
- NoMinio: true,
- NoMongo: true,
- NoDashboard: true,
+ NoMinio: true,
+ NoMongo: true,
}
cmd := &cobra.Command{
@@ -38,23 +39,28 @@ func NewInitCmd() *cobra.Command {
ui.NL()
currentContext, err := common.GetCurrentKubernetesContext()
- sendErrTelemetry(cmd, cfg, "k8s_context")
- ui.ExitOnError("getting current context", err)
+
+ if err != nil {
+ sendErrTelemetry(cmd, cfg, "k8s_context", err)
+ ui.ExitOnError("getting current context", err)
+ }
ui.Alert("Current kubectl context:", currentContext)
ui.NL()
ok := ui.Confirm("Do you want to continue?")
if !ok {
ui.Errf("Testkube installation cancelled")
- sendErrTelemetry(cmd, cfg, "user_cancel")
+ sendErrTelemetry(cmd, cfg, "user_cancel", err)
return
}
}
spinner := ui.NewSpinner("Installing Testkube")
err = common.HelmUpgradeOrInstallTestkubeCloud(options, cfg, false)
- sendErrTelemetry(cmd, cfg, "helm_install")
- ui.ExitOnError("Installing Testkube", err)
+ if err != nil {
+ sendErrTelemetry(cmd, cfg, "helm_install", err)
+ ui.ExitOnError("Installing Testkube", err)
+ }
spinner.Success()
ui.NL()
@@ -63,13 +69,14 @@ func NewInitCmd() *cobra.Command {
var token, refreshToken string
if !common.IsUserLoggedIn(cfg, options) {
token, refreshToken, err = common.LoginUser(options.Master.URIs.Auth)
- sendErrTelemetry(cmd, cfg, "login")
+ sendErrTelemetry(cmd, cfg, "login", err)
ui.ExitOnError("user login", err)
}
err = common.PopulateLoginDataToContext(options.Master.OrgId, options.Master.EnvId, token, refreshToken, options, cfg)
- sendErrTelemetry(cmd, cfg, "setting_context")
- ui.ExitOnError("Setting Pro environment context", err)
-
+ if err != nil {
+ sendErrTelemetry(cmd, cfg, "setting_context", err)
+ ui.ExitOnError("Setting Pro environment context", err)
+ }
ui.Info(" Happy Testing! 🚀")
ui.NL()
},
@@ -84,13 +91,16 @@ func NewInitCmd() *cobra.Command {
return cmd
}
-func sendErrTelemetry(cmd *cobra.Command, clientCfg config.Data, errType string) {
+func sendErrTelemetry(cmd *cobra.Command, clientCfg config.Data, errType string, errorLogs error) {
+ var errorStackTrace string
+ errorStackTrace = fmt.Sprintf("%+v", errorLogs)
if clientCfg.TelemetryEnabled {
ui.Debug("collecting anonymous telemetry data, you can disable it by calling `kubectl testkube disable telemetry`")
- out, err := telemetry.SendCmdErrorEvent(cmd, common.Version, errType)
+ out, err := telemetry.SendCmdErrorEvent(cmd, common.Version, errType, errorStackTrace)
if ui.Verbose && err != nil {
ui.Err(err)
}
+
ui.Debug("telemetry send event response", out)
}
}
diff --git a/cmd/kubectl-testkube/commands/root.go b/cmd/kubectl-testkube/commands/root.go
index 6dcf94c328a..5c5468d0055 100644
--- a/cmd/kubectl-testkube/commands/root.go
+++ b/cmd/kubectl-testkube/commands/root.go
@@ -180,6 +180,10 @@ func Execute() {
case <-ctx.Done():
return nil
case sig := <-stopSignal:
+ go func() {
+ <-stopSignal
+ os.Exit(137)
+ }()
return errors.Errorf("received signal: %v", sig)
}
})
diff --git a/cmd/kubectl-testkube/commands/run.go b/cmd/kubectl-testkube/commands/run.go
index 5321340b922..ac921fc6055 100644
--- a/cmd/kubectl-testkube/commands/run.go
+++ b/cmd/kubectl-testkube/commands/run.go
@@ -7,6 +7,7 @@ import (
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/validator"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/tests"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsuites"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testworkflows"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/config"
"github.com/kubeshop/testkube/pkg/ui"
)
@@ -15,7 +16,7 @@ func NewRunCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "run ",
Aliases: []string{"r", "start"},
- Short: "Runs tests or test suites",
+ Short: "Runs tests, test suites or test workflows",
Annotations: map[string]string{cmdGroupAnnotation: cmdGroupCommands},
Run: func(cmd *cobra.Command, args []string) {
err := cmd.Help()
@@ -31,6 +32,7 @@ func NewRunCmd() *cobra.Command {
cmd.AddCommand(tests.NewRunTestCmd())
cmd.AddCommand(testsuites.NewRunTestSuiteCmd())
+ cmd.AddCommand(testworkflows.NewRunTestWorkflowCmd())
return cmd
}
diff --git a/cmd/kubectl-testkube/commands/tests/common.go b/cmd/kubectl-testkube/commands/tests/common.go
index adf08934b62..2d07f7b7a05 100644
--- a/cmd/kubectl-testkube/commands/tests/common.go
+++ b/cmd/kubectl-testkube/commands/tests/common.go
@@ -52,11 +52,40 @@ func printExecutionDetails(execution testkube.Execution) {
ui.NL()
}
-func DownloadArtifacts(id, dir, format string, masks []string, client apiclientv1.Client) {
+func DownloadTestArtifacts(id, dir, format string, masks []string, client apiclientv1.Client) {
artifacts, err := client.GetExecutionArtifacts(id)
- ui.ExitOnError("getting artifacts ", err)
+ ui.ExitOnError("getting artifacts", err)
- err = os.MkdirAll(dir, os.ModePerm)
+ downloadFile := func(artifact testkube.Artifact, dir string) (string, error) {
+ return client.DownloadFile(id, artifact.Name, dir)
+ }
+ downloadArchive := func(dir string, masks []string) (string, error) {
+ return client.DownloadArchive(id, dir, masks)
+ }
+ downloadArtifacts(dir, format, masks, artifacts, downloadFile, downloadArchive)
+}
+
+func DownloadTestWorkflowArtifacts(id, dir, format string, masks []string, client apiclientv1.Client) {
+ artifacts, err := client.GetTestWorkflowExecutionArtifacts(id)
+ ui.ExitOnError("getting artifacts", err)
+
+ downloadFile := func(artifact testkube.Artifact, dir string) (string, error) {
+ return client.DownloadTestWorkflowArtifact(id, artifact.Name, dir)
+ }
+ downloadArchive := func(dir string, masks []string) (string, error) {
+ return client.DownloadTestWorkflowArtifactArchive(id, dir, masks)
+ }
+ downloadArtifacts(dir, format, masks, artifacts, downloadFile, downloadArchive)
+}
+
+func downloadArtifacts(
+ dir, format string,
+ masks []string,
+ artifacts testkube.Artifacts,
+ downloadFile func(artifact testkube.Artifact, dir string) (string, error),
+ downloadArchive func(dir string, masks []string) (string, error),
+) {
+ err := os.MkdirAll(dir, os.ModePerm)
ui.ExitOnError("creating dir "+dir, err)
if len(artifacts) > 0 {
@@ -91,7 +120,7 @@ func DownloadArtifacts(id, dir, format string, masks []string, client apiclientv
continue
}
- f, err := client.DownloadFile(id, artifact.Name, dir)
+ f, err := downloadFile(artifact, dir)
ui.ExitOnError("downloading file: "+f, err)
ui.Warn(" - downloading file ", f)
}
@@ -106,7 +135,7 @@ func DownloadArtifacts(id, dir, format string, masks []string, client apiclientv
defer close(ch)
go func() {
- f, err := client.DownloadArchive(id, dir, masks)
+ f, err := downloadArchive(dir, masks)
ui.ExitOnError("downloading archive: "+f, err)
ch <- f
@@ -177,6 +206,45 @@ func watchLogs(id string, silentMode bool, client apiclientv1.Client) error {
return result
}
+func watchLogsV2(id string, silentMode bool, client apiclientv1.Client) error {
+ ui.Info("Getting logs from test job", id)
+
+ logs, err := client.LogsV2(id)
+ ui.ExitOnError("getting logs from executor", err)
+
+ var result error
+ for l := range logs {
+ if l.Error_ {
+ ui.UseStderr()
+ ui.Errf(l.Content)
+ result = errors.New(l.Content)
+ continue
+ }
+
+ if !silentMode {
+ ui.LogLine(l.Content)
+ }
+ }
+
+ ui.NL()
+
+ // TODO Websocket research + plug into Event bus (EventEmitter)
+ // watch for success | error status - in case of connection error on logs watch need fix in 0.8
+ for range time.Tick(time.Second) {
+ execution, err := client.GetExecution(id)
+ ui.ExitOnError("get test execution details", err)
+
+ fmt.Print(".")
+
+ if execution.ExecutionResult.IsCompleted() {
+ ui.Info("Execution completed")
+ return result
+ }
+ }
+
+ return result
+}
+
func newContentFromFlags(cmd *cobra.Command) (content *testkube.TestContent, err error) {
testContentType := cmd.Flag("test-content-type").Value.String()
uri := cmd.Flag("uri").Value.String()
@@ -451,10 +519,15 @@ func newExecutionRequestFromFlags(cmd *cobra.Command) (request *testkube.Executi
cronJobTemplateReference := cmd.Flag("cronjob-template-reference").Value.String()
scraperTemplateReference := cmd.Flag("scraper-template-reference").Value.String()
pvcTemplateReference := cmd.Flag("pvc-template-reference").Value.String()
+ executionNamespace := cmd.Flag("execution-namespace").Value.String()
executePostRunScriptBeforeScraping, err := cmd.Flags().GetBool("execute-postrun-script-before-scraping")
if err != nil {
return nil, err
}
+ sourceScripts, err := cmd.Flags().GetBool("source-scripts")
+ if err != nil {
+ return nil, err
+ }
request = &testkube.ExecutionRequest{
Name: executionName,
@@ -475,6 +548,8 @@ func newExecutionRequestFromFlags(cmd *cobra.Command) (request *testkube.Executi
PvcTemplateReference: pvcTemplateReference,
NegativeTest: negativeTest,
ExecutePostRunScriptBeforeScraping: executePostRunScriptBeforeScraping,
+ SourceScripts: sourceScripts,
+ ExecutionNamespace: executionNamespace,
}
var fields = []struct {
@@ -870,6 +945,10 @@ func newExecutionUpdateRequestFromFlags(cmd *cobra.Command) (request *testkube.E
"pvc-template-reference",
&request.PvcTemplateReference,
},
+ {
+ "execution-namespace",
+ &request.ExecutionNamespace,
+ },
}
var nonEmpty bool
@@ -1066,6 +1145,15 @@ func newExecutionUpdateRequestFromFlags(cmd *cobra.Command) (request *testkube.E
nonEmpty = true
}
+ if cmd.Flag("source-scripts").Changed {
+ sourceScripts, err := cmd.Flags().GetBool("source-scripts")
+ if err != nil {
+ return nil, err
+ }
+ request.SourceScripts = &sourceScripts
+ nonEmpty = true
+ }
+
artifactRequest, err := newArtifactUpdateRequestFromFlags(cmd)
if err != nil {
return nil, err
diff --git a/cmd/kubectl-testkube/commands/tests/create.go b/cmd/kubectl-testkube/commands/tests/create.go
index 4a4ca6e0d95..73cf8b77fa0 100644
--- a/cmd/kubectl-testkube/commands/tests/create.go
+++ b/cmd/kubectl-testkube/commands/tests/create.go
@@ -20,8 +20,8 @@ import (
type CreateCommonFlags struct {
ExecutorType string
Labels map[string]string
- Variables map[string]string
- SecretVariables map[string]string
+ Variables []string
+ SecretVariables []string
Schedule string
ExecutorArgs []string
ArgsMode string
@@ -47,6 +47,7 @@ type CreateCommonFlags struct {
PreRunScript string
PostRunScript string
ExecutePostRunScriptBeforeScraping bool
+ SourceScripts bool
ScraperTemplate string
ScraperTemplateReference string
PvcTemplate string
@@ -67,6 +68,7 @@ type CreateCommonFlags struct {
SlavePodLimitsMemory string
SlavePodTemplate string
SlavePodTemplateReference string
+ ExecutionNamespace string
}
// NewCreateTestsCmd is a command tp create new Test Custom Resource
@@ -228,12 +230,12 @@ func AddCreateFlags(cmd *cobra.Command, flags *CreateCommonFlags) {
cmd.Flags().StringVarP(&flags.ExecutorType, "type", "t", "", "test type")
cmd.Flags().StringToStringVarP(&flags.Labels, "label", "l", nil, "label key value pair: --label key1=value1")
- cmd.Flags().StringToStringVarP(&flags.Variables, "variable", "v", nil, "variable key value pair: --variable key1=value1")
- cmd.Flags().StringToStringVarP(&flags.SecretVariables, "secret-variable", "s", nil, "secret variable key value pair: --secret-variable key1=value1")
+ cmd.Flags().StringArrayVarP(&flags.Variables, "variable", "v", nil, "variable key value pair: --variable key1=value1")
+ cmd.Flags().StringArrayVarP(&flags.SecretVariables, "secret-variable", "s", nil, "secret variable key value pair: --secret-variable key1=value1")
cmd.Flags().StringVarP(&flags.Schedule, "schedule", "", "", "test schedule in a cron job form: * * * * *")
cmd.Flags().StringArrayVar(&flags.Command, "command", []string{}, "command passed to image in executor")
cmd.Flags().StringArrayVarP(&flags.ExecutorArgs, "executor-args", "", []string{}, "executor binary additional arguments")
- cmd.Flags().StringVarP(&flags.ArgsMode, "args-mode", "", "append", "usage mode for arguments. one of append|override")
+ cmd.Flags().StringVarP(&flags.ArgsMode, "args-mode", "", "append", "usage mode for arguments. one of append|override|replace")
cmd.Flags().StringVarP(&flags.ExecutionName, "execution-name", "", "", "execution name, if empty will be autogenerated")
cmd.Flags().StringVarP(&flags.VariablesFile, "variables-file", "", "", "variables file path, e.g. postman env file - will be passed to executor if supported")
cmd.Flags().StringToStringVarP(&flags.Envs, "env", "", map[string]string{}, "envs in a form of name1=val1 passed to executor")
@@ -256,6 +258,7 @@ func AddCreateFlags(cmd *cobra.Command, flags *CreateCommonFlags) {
cmd.Flags().StringVarP(&flags.PreRunScript, "prerun-script", "", "", "path to script to be run before test execution")
cmd.Flags().StringVarP(&flags.PostRunScript, "postrun-script", "", "", "path to script to be run after test execution")
cmd.Flags().BoolVarP(&flags.ExecutePostRunScriptBeforeScraping, "execute-postrun-script-before-scraping", "", false, "whether to execute postrun scipt before scraping or not (prebuilt executor only)")
+ cmd.Flags().BoolVarP(&flags.SourceScripts, "source-scripts", "", false, "run scripts using source command (container executor only)")
cmd.Flags().StringVar(&flags.ScraperTemplate, "scraper-template", "", "scraper template file path for extensions to scraper template")
cmd.Flags().StringVar(&flags.ScraperTemplateReference, "scraper-template-reference", "", "reference to scraper template to use for the test")
cmd.Flags().StringVar(&flags.PvcTemplate, "pvc-template", "", "pvc template file path for extensions to pvc template")
@@ -276,6 +279,7 @@ func AddCreateFlags(cmd *cobra.Command, flags *CreateCommonFlags) {
cmd.Flags().StringVar(&flags.SlavePodLimitsMemory, "slave-pod-limits-memory", "", "slave pod resource limits memory")
cmd.Flags().StringVar(&flags.SlavePodTemplate, "slave-pod-template", "", "slave pod template file path for extensions to slave pod template")
cmd.Flags().StringVar(&flags.SlavePodTemplateReference, "slave-pod-template-reference", "", "reference to slave pod template to use for the test")
+ cmd.Flags().StringVar(&flags.ExecutionNamespace, "execution-namespace", "", "namespace for test execution (Pro edition only)")
}
func validateExecutorTypeAndContent(executorType, contentType string, executors testkube.ExecutorsDetails) error {
diff --git a/cmd/kubectl-testkube/commands/tests/executions.go b/cmd/kubectl-testkube/commands/tests/executions.go
index b5577e3623a..1c5f39556de 100644
--- a/cmd/kubectl-testkube/commands/tests/executions.go
+++ b/cmd/kubectl-testkube/commands/tests/executions.go
@@ -35,7 +35,7 @@ func NewGetExecutionCmd() *cobra.Command {
ui.ExitOnError("getting test execution: "+executionID, err)
if logsOnly {
- if err = render.RenderExecutionResult(client, &execution, logsOnly); err != nil {
+ if err = render.RenderExecutionResult(client, &execution, logsOnly, true); err != nil {
os.Exit(1)
}
} else {
diff --git a/cmd/kubectl-testkube/commands/tests/renderer/execution_obj.go b/cmd/kubectl-testkube/commands/tests/renderer/execution_obj.go
index 50f32e29b3b..b4a6c895a1e 100644
--- a/cmd/kubectl-testkube/commands/tests/renderer/execution_obj.go
+++ b/cmd/kubectl-testkube/commands/tests/renderer/execution_obj.go
@@ -22,6 +22,7 @@ func ExecutionRenderer(client client.Client, ui *ui.UI, obj interface{}) error {
ui.Warn("Number: ", fmt.Sprintf("%d", execution.Number))
}
ui.Warn("Test name: ", execution.TestName)
+ ui.Warn("Test namespace: ", execution.TestNamespace)
ui.Warn("Type: ", execution.TestType)
if execution.ExecutionResult != nil && execution.ExecutionResult.Status != nil {
ui.Warn("Status: ", string(*execution.ExecutionResult.Status))
@@ -59,7 +60,7 @@ func ExecutionRenderer(client client.Client, ui *ui.UI, obj interface{}) error {
ui.Warn(" Auth type: ", execution.Content.Repository.AuthType)
}
- if err := render.RenderExecutionResult(client, &execution, false); err != nil {
+ if err := render.RenderExecutionResult(client, &execution, false, true); err != nil {
return err
}
diff --git a/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go b/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go
index 2b8b49264d7..1ff8d838176 100644
--- a/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go
+++ b/cmd/kubectl-testkube/commands/tests/renderer/test_obj.go
@@ -159,7 +159,14 @@ func TestRenderer(client client.Client, ui *ui.UI, obj interface{}) error {
ui.Warn(" Post run script: ", "\n", test.ExecutionRequest.PostRunScript)
}
- ui.Warn(" Execute postrun script before scraping: ", fmt.Sprint(test.ExecutionRequest.ExecutePostRunScriptBeforeScraping))
+ if test.ExecutionRequest.ExecutePostRunScriptBeforeScraping {
+ ui.Warn(" Execute postrun script before scraping: ", fmt.Sprint(test.ExecutionRequest.ExecutePostRunScriptBeforeScraping))
+ }
+
+ if test.ExecutionRequest.SourceScripts {
+ ui.Warn(" Source scripts: ", fmt.Sprint(test.ExecutionRequest.SourceScripts))
+ }
+
if test.ExecutionRequest.ScraperTemplate != "" {
ui.Warn(" Scraper template: ", "\n", test.ExecutionRequest.ScraperTemplate)
}
@@ -176,6 +183,10 @@ func TestRenderer(client client.Client, ui *ui.UI, obj interface{}) error {
ui.Warn(" PVC template reference: ", test.ExecutionRequest.PvcTemplateReference)
}
+ if test.ExecutionRequest.ExecutionNamespace != "" {
+ ui.Warn(" Execution namespace: ", test.ExecutionRequest.ExecutionNamespace)
+ }
+
if test.ExecutionRequest.SlavePodRequest != nil {
ui.Warn(" Slave pod request: ")
if test.ExecutionRequest.SlavePodRequest.Resources != nil {
diff --git a/cmd/kubectl-testkube/commands/tests/run.go b/cmd/kubectl-testkube/commands/tests/run.go
index 360966a1acd..1c759457ed1 100644
--- a/cmd/kubectl-testkube/commands/tests/run.go
+++ b/cmd/kubectl-testkube/commands/tests/run.go
@@ -25,8 +25,8 @@ func NewRunTestCmd() *cobra.Command {
iterations int
watchEnabled bool
binaryArgs []string
- variables map[string]string
- secretVariables map[string]string
+ variables []string
+ secretVariables []string
variablesFile string
downloadArtifactsEnabled bool
downloadDir string
@@ -51,6 +51,7 @@ func NewRunTestCmd() *cobra.Command {
preRunScript string
postRunScript string
executePostRunScriptBeforeScraping bool
+ sourceScripts bool
scraperTemplate string
scraperTemplateReference string
pvcTemplate string
@@ -76,6 +77,7 @@ func NewRunTestCmd() *cobra.Command {
slavePodLimitsMemory string
slavePodTemplate string
slavePodTemplateReference string
+ executionNamespace string
)
cmd := &cobra.Command{
@@ -123,6 +125,8 @@ func NewRunTestCmd() *cobra.Command {
Context: runningContext,
},
ExecutePostRunScriptBeforeScraping: executePostRunScriptBeforeScraping,
+ SourceScripts: sourceScripts,
+ ExecutionNamespace: executionNamespace,
}
var fields = []struct {
@@ -302,8 +306,17 @@ func NewRunTestCmd() *cobra.Command {
if execution.Id != "" {
if watchEnabled && len(args) > 0 {
- if err = watchLogs(execution.Id, silentMode, client); err != nil {
- execErrors = append(execErrors, err)
+ info, err := client.GetServerInfo()
+ ui.ExitOnError("getting server info", err)
+
+ if info.Features != nil && info.Features.LogsV2 {
+ if err = watchLogsV2(execution.Id, silentMode, client); err != nil {
+ execErrors = append(execErrors, err)
+ }
+ } else {
+ if err = watchLogs(execution.Id, silentMode, client); err != nil {
+ execErrors = append(execErrors, err)
+ }
}
}
@@ -311,14 +324,14 @@ func NewRunTestCmd() *cobra.Command {
ui.ExitOnError("getting recent execution data id:"+execution.Id, err)
}
- if err = render.RenderExecutionResult(client, &execution, false); err != nil {
+ if err = render.RenderExecutionResult(client, &execution, false, !watchEnabled); err != nil {
execErrors = append(execErrors, err)
}
if execution.Id != "" {
if watchEnabled && len(args) > 0 {
if downloadArtifactsEnabled && (execution.IsPassed() || execution.IsFailed()) {
- DownloadArtifacts(execution.Id, downloadDir, format, masks, client)
+ DownloadTestArtifacts(execution.Id, downloadDir, format, masks, client)
}
}
@@ -335,11 +348,11 @@ func NewRunTestCmd() *cobra.Command {
cmd.Flags().StringVarP(&name, "name", "n", "", "execution name, if empty will be autogenerated")
cmd.Flags().StringVarP(&image, "image", "", "", "override executor container image")
cmd.Flags().StringVarP(&variablesFile, "variables-file", "", "", "variables file path, e.g. postman env file - will be passed to executor if supported")
- cmd.Flags().StringToStringVarP(&variables, "variable", "v", map[string]string{}, "execution variable passed to executor")
- cmd.Flags().StringToStringVarP(&secretVariables, "secret-variable", "s", map[string]string{}, "execution secret variable passed to executor")
+ cmd.Flags().StringArrayVarP(&variables, "variable", "v", []string{}, "execution variable passed to executor")
+ cmd.Flags().StringArrayVarP(&secretVariables, "secret-variable", "s", []string{}, "execution secret variable passed to executor")
cmd.Flags().StringArrayVar(&command, "command", []string{}, "command passed to image in executor")
cmd.Flags().StringArrayVarP(&binaryArgs, "args", "", []string{}, "executor binary additional arguments")
- cmd.Flags().StringVarP(&argsMode, "args-mode", "", "append", "usage mode for argumnets. one of append|override")
+ cmd.Flags().StringVarP(&argsMode, "args-mode", "", "append", "usage mode for argumnets. one of append|override|replace")
cmd.Flags().BoolVarP(&watchEnabled, "watch", "f", false, "watch for changes after start")
cmd.Flags().StringVar(&downloadDir, "download-dir", "artifacts", "download dir")
cmd.Flags().BoolVarP(&downloadArtifactsEnabled, "download-artifacts", "d", false, "download artifacts automatically")
@@ -366,6 +379,7 @@ func NewRunTestCmd() *cobra.Command {
cmd.Flags().StringVarP(&preRunScript, "prerun-script", "", "", "path to script to be run before test execution")
cmd.Flags().StringVarP(&postRunScript, "postrun-script", "", "", "path to script to be run after test execution")
cmd.Flags().BoolVarP(&executePostRunScriptBeforeScraping, "execute-postrun-script-before-scraping", "", false, "whether to execute postrun scipt before scraping or not (prebuilt executor only)")
+ cmd.Flags().BoolVarP(&sourceScripts, "source-scripts", "", false, "run scripts using source command (container executor only)")
cmd.Flags().StringVar(&scraperTemplate, "scraper-template", "", "scraper template file path for extensions to scraper template")
cmd.Flags().StringVar(&scraperTemplateReference, "scraper-template-reference", "", "reference to scraper template to use for the test")
cmd.Flags().StringVar(&pvcTemplate, "pvc-template", "", "pvc template file path for extensions to pvc template")
@@ -391,6 +405,7 @@ func NewRunTestCmd() *cobra.Command {
cmd.Flags().StringVar(&slavePodLimitsMemory, "slave-pod-limits-memory", "", "slave pod resource limits memory")
cmd.Flags().StringVar(&slavePodTemplate, "slave-pod-template", "", "slave pod template file path for extensions to slave pod template")
cmd.Flags().StringVar(&slavePodTemplateReference, "slave-pod-template-reference", "", "reference to slave pod template to use for the test")
+ cmd.Flags().StringVar(&executionNamespace, "execution-namespace", "", "namespace for test execution (Pro edition only)")
return cmd
}
diff --git a/cmd/kubectl-testkube/commands/tests/watch.go b/cmd/kubectl-testkube/commands/tests/watch.go
index 6a749dc6914..ae82c96dce9 100644
--- a/cmd/kubectl-testkube/commands/tests/watch.go
+++ b/cmd/kubectl-testkube/commands/tests/watch.go
@@ -28,7 +28,15 @@ func NewWatchExecutionCmd() *cobra.Command {
if execution.ExecutionResult.IsCompleted() {
ui.Completed("execution is already finished")
} else {
- err = watchLogs(execution.Id, false, client)
+ info, err := client.GetServerInfo()
+ ui.ExitOnError("getting server info", err)
+
+ if info.Features.LogsV2 {
+ err = watchLogsV2(execution.Id, false, client)
+ } else {
+ err = watchLogs(execution.Id, false, client)
+ }
+
ui.NL()
uiShellGetExecution(execution.Id)
if err != nil {
diff --git a/cmd/kubectl-testkube/commands/testsuites/common.go b/cmd/kubectl-testkube/commands/testsuites/common.go
index ba43ac3460d..b9c5ee63ebc 100644
--- a/cmd/kubectl-testkube/commands/testsuites/common.go
+++ b/cmd/kubectl-testkube/commands/testsuites/common.go
@@ -411,7 +411,7 @@ func DownloadArtifacts(id, dir, format string, masks []string, client apiclientv
for _, step := range execution.Execute {
if step.Execution != nil && step.Step != nil && step.Step.Test != "" {
if step.Execution.IsPassed() || step.Execution.IsFailed() {
- tests.DownloadArtifacts(step.Execution.Id, filepath.Join(dir, step.Execution.TestName+"-"+step.Execution.Id), format, masks, client)
+ tests.DownloadTestArtifacts(step.Execution.Id, filepath.Join(dir, step.Execution.TestName+"-"+step.Execution.Id), format, masks, client)
}
}
}
diff --git a/cmd/kubectl-testkube/commands/testsuites/create.go b/cmd/kubectl-testkube/commands/testsuites/create.go
index 1ab55360574..08dc6745bc7 100644
--- a/cmd/kubectl-testkube/commands/testsuites/create.go
+++ b/cmd/kubectl-testkube/commands/testsuites/create.go
@@ -21,8 +21,8 @@ func NewCreateTestSuitesCmd() *cobra.Command {
name string
file string
labels map[string]string
- variables map[string]string
- secretVariables map[string]string
+ variables []string
+ secretVariables []string
schedule string
executionName string
httpProxy, httpsProxy string
@@ -104,8 +104,8 @@ func NewCreateTestSuitesCmd() *cobra.Command {
cmd.Flags().StringVarP(&file, "file", "f", "", "JSON test suite file - will be read from stdin if not specified, look at testkube.TestUpsertRequest")
cmd.Flags().StringVar(&name, "name", "", "Set/Override test suite name")
cmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "label key value pair: --label key1=value1")
- cmd.Flags().StringToStringVarP(&variables, "variable", "v", nil, "param key value pair: --variable key1=value1")
- cmd.Flags().StringToStringVarP(&secretVariables, "secret-variable", "s", nil, "secret variable key value pair: --secret-variable key1=value1")
+ cmd.Flags().StringArrayVarP(&variables, "variable", "v", nil, "param key value pair: --variable key1=value1")
+ cmd.Flags().StringArrayVarP(&secretVariables, "secret-variable", "s", nil, "secret variable key value pair: --secret-variable key1=value1")
cmd.Flags().StringVarP(&schedule, "schedule", "", "", "test suite schedule in a cron job form: * * * * *")
cmd.Flags().StringVarP(&executionName, "execution-name", "", "", "execution name, if empty will be autogenerated")
cmd.Flags().StringVar(&httpProxy, "http-proxy", "", "http proxy for executor containers")
diff --git a/cmd/kubectl-testkube/commands/testsuites/run.go b/cmd/kubectl-testkube/commands/testsuites/run.go
index ef85cb3d0b7..e0e435bdb24 100644
--- a/cmd/kubectl-testkube/commands/testsuites/run.go
+++ b/cmd/kubectl-testkube/commands/testsuites/run.go
@@ -24,8 +24,8 @@ func NewRunTestSuiteCmd() *cobra.Command {
var (
name string
watchEnabled bool
- variables map[string]string
- secretVariables map[string]string
+ variables []string
+ secretVariables []string
executionLabels map[string]string
selectors []string
concurrencyLevel int
@@ -190,8 +190,8 @@ func NewRunTestSuiteCmd() *cobra.Command {
}
cmd.Flags().StringVarP(&name, "name", "n", "", "execution name, if empty will be autogenerated")
- cmd.Flags().StringToStringVarP(&variables, "variable", "v", map[string]string{}, "execution variables passed to executor")
- cmd.Flags().StringToStringVarP(&secretVariables, "secret-variable", "s", map[string]string{}, "execution variables passed to executor")
+ cmd.Flags().StringArrayVarP(&variables, "variable", "v", []string{}, "execution variables passed to executor")
+ cmd.Flags().StringArrayVarP(&secretVariables, "secret-variable", "s", []string{}, "execution variables passed to executor")
cmd.Flags().BoolVarP(&watchEnabled, "watch", "f", false, "watch for changes after start")
cmd.Flags().StringSliceVarP(&selectors, "label", "l", nil, "label key value pair: --label key1=value1")
cmd.Flags().IntVar(&concurrencyLevel, "concurrency", 10, "concurrency level for multiple test suite execution")
diff --git a/cmd/kubectl-testkube/commands/testsuites/update.go b/cmd/kubectl-testkube/commands/testsuites/update.go
index abd0772f831..238c391d815 100644
--- a/cmd/kubectl-testkube/commands/testsuites/update.go
+++ b/cmd/kubectl-testkube/commands/testsuites/update.go
@@ -15,8 +15,8 @@ func UpdateTestSuitesCmd() *cobra.Command {
labels map[string]string
schedule string
executionName string
- variables map[string]string
- secretVariables map[string]string
+ variables []string
+ secretVariables []string
httpProxy, httpsProxy string
secretVariableReferences map[string]string
timeout int32
@@ -64,8 +64,8 @@ func UpdateTestSuitesCmd() *cobra.Command {
cmd.Flags().StringVarP(&file, "file", "f", "", "JSON test file - will be read from stdin if not specified, look at testkube.TestUpsertRequest")
cmd.Flags().StringVar(&name, "name", "", "Set/Override test suite name")
cmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "label key value pair: --label key1=value1")
- cmd.Flags().StringToStringVarP(&variables, "variable", "v", nil, "param key value pair: --variable key1=value1")
- cmd.Flags().StringToStringVarP(&secretVariables, "secret-variable", "s", nil, "secret variable key value pair: --secret-variable key1=value1")
+ cmd.Flags().StringArrayVarP(&variables, "variable", "v", nil, "param key value pair: --variable key1=value1")
+ cmd.Flags().StringArrayVarP(&secretVariables, "secret-variable", "s", nil, "secret variable key value pair: --secret-variable key1=value1")
cmd.Flags().StringVarP(&schedule, "schedule", "", "", "test suite schedule in a cron job form: * * * * *")
cmd.Flags().StringVarP(&executionName, "execution-name", "", "", "execution name, if empty will be autogenerated")
cmd.Flags().StringVar(&httpProxy, "http-proxy", "", "http proxy for executor containers")
diff --git a/cmd/kubectl-testkube/commands/testworkflows/abort.go b/cmd/kubectl-testkube/commands/testworkflows/abort.go
new file mode 100644
index 00000000000..b1842d053b0
--- /dev/null
+++ b/cmd/kubectl-testkube/commands/testworkflows/abort.go
@@ -0,0 +1,56 @@
+package testworkflows
+
+import (
+ "fmt"
+
+ "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/pkg/ui"
+)
+
+func NewAbortTestWorkflowExecutionCmd() *cobra.Command {
+ return &cobra.Command{
+ Use: "testworkflowexecution ",
+ Aliases: []string{"twe", "testworkflows-execution", "testworkflow-execution"},
+ Short: "Abort test workflow execution",
+ Args: validator.ExecutionName,
+
+ Run: func(cmd *cobra.Command, args []string) {
+ executionID := args[0]
+
+ client, _, err := common.GetClient(cmd)
+ ui.ExitOnError("getting client", err)
+
+ execution, err := client.GetTestWorkflowExecution(executionID)
+ ui.ExitOnError("get execution failed", err)
+
+ err = client.AbortTestWorkflowExecution(execution.Workflow.Name, execution.Id)
+ ui.ExitOnError(fmt.Sprintf("aborting testworkflow execution %s", executionID), err)
+
+ ui.SuccessAndExit("Succesfully aborted test workflow execution", executionID)
+ },
+ }
+}
+
+func NewAbortTestWorkflowExecutionsCmd() *cobra.Command {
+ return &cobra.Command{
+ Use: "testworkflowexecutions ",
+ Aliases: []string{"twes", "testworkflows-executions", "testworkflow-executions"},
+ Short: "Abort all test workflow executions",
+ Args: cobra.ExactArgs(1),
+
+ Run: func(cmd *cobra.Command, args []string) {
+ testWorkflowName := args[0]
+
+ client, _, err := common.GetClient(cmd)
+ ui.ExitOnError("getting client", err)
+
+ err = client.AbortTestWorkflowExecutions(testWorkflowName)
+ ui.ExitOnError(fmt.Sprintf("aborting test workflow executions for test workflow %s", testWorkflowName), err)
+
+ ui.SuccessAndExit("Successfully aborted all test workflow executions", testWorkflowName)
+ },
+ }
+}
diff --git a/cmd/kubectl-testkube/commands/testworkflows/create.go b/cmd/kubectl-testkube/commands/testworkflows/create.go
new file mode 100644
index 00000000000..5615d42a291
--- /dev/null
+++ b/cmd/kubectl-testkube/commands/testworkflows/create.go
@@ -0,0 +1,85 @@
+package testworkflows
+
+import (
+ "io"
+ "os"
+
+ "github.com/spf13/cobra"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
+ common2 "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/tcl/mapperstcl/testworkflows"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func NewCreateTestWorkflowCmd() *cobra.Command {
+ var (
+ name string
+ filePath string
+ update bool
+ )
+
+ cmd := &cobra.Command{
+ Use: "testworkflow",
+ Aliases: []string{"testworkflows", "tw"},
+ Args: cobra.MaximumNArgs(0),
+ Short: "Create test workflow",
+
+ Run: func(cmd *cobra.Command, _ []string) {
+ namespace := cmd.Flag("namespace").Value.String()
+
+ var input io.Reader
+ if filePath == "" {
+ fi, err := os.Stdin.Stat()
+ ui.ExitOnError("reading stdin", err)
+ if fi.Mode()&os.ModeDevice != 0 {
+ ui.Failf("you need to pass stdin or --file argument with file path")
+ }
+ input = cmd.InOrStdin()
+ } else {
+ file, err := os.Open(filePath)
+ ui.ExitOnError("reading "+filePath+" file", err)
+ input = file
+ }
+
+ bytes, err := io.ReadAll(input)
+ ui.ExitOnError("reading input", err)
+
+ obj := new(testworkflowsv1.TestWorkflow)
+ err = common2.DeserializeCRD(obj, bytes)
+ ui.ExitOnError("deserializing input", err)
+ if obj.Kind != "" && obj.Kind != "TestWorkflow" {
+ ui.Failf("Only TestWorkflow objects are accepted. Received: %s", obj.Kind)
+ }
+ common2.AppendTypeMeta("TestWorkflow", testworkflowsv1.GroupVersion, obj)
+ obj.Namespace = namespace
+ if name != "" {
+ obj.Name = name
+ }
+
+ client, _, err := common.GetClient(cmd)
+ ui.ExitOnError("getting client", err)
+
+ workflow, _ := client.GetTestWorkflow(obj.Name)
+ 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)
+ }
+ _, err = client.UpdateTestWorkflow(testworkflows.MapTestWorkflowKubeToAPI(*obj))
+ ui.ExitOnError("updating test workflow "+obj.Name+" in namespace "+obj.Namespace, err)
+ ui.Success("Test workflow updated", namespace, "/", obj.Name)
+ } else {
+ _, err = client.CreateTestWorkflow(testworkflows.MapTestWorkflowKubeToAPI(*obj))
+ ui.ExitOnError("creating test workflow "+obj.Name+" in namespace "+obj.Namespace, err)
+ ui.Success("Test workflow created", namespace, "/", obj.Name)
+ }
+ },
+ }
+
+ cmd.Flags().StringVar(&name, "name", "", "test workflow name")
+ cmd.Flags().BoolVar(&update, "update", false, "update, if test workflow already exists")
+ cmd.Flags().StringVarP(&filePath, "file", "f", "", "file path to get the test workflow specification")
+
+ return cmd
+}
diff --git a/cmd/kubectl-testkube/commands/testworkflows/delete.go b/cmd/kubectl-testkube/commands/testworkflows/delete.go
new file mode 100644
index 00000000000..c601d94cc64
--- /dev/null
+++ b/cmd/kubectl-testkube/commands/testworkflows/delete.go
@@ -0,0 +1,54 @@
+package testworkflows
+
+import (
+ "strings"
+
+ "github.com/spf13/cobra"
+
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func NewDeleteTestWorkflowCmd() *cobra.Command {
+ var deleteAll bool
+ var selectors []string
+
+ cmd := &cobra.Command{
+ Use: "testworkflow [name]",
+ Aliases: []string{"testworkflows", "tw"},
+ Args: cobra.MaximumNArgs(1),
+ Short: "Delete test workflows",
+
+ Run: func(cmd *cobra.Command, args []string) {
+ namespace := cmd.Flag("namespace").Value.String()
+ client, _, err := common.GetClient(cmd)
+ ui.ExitOnError("getting client", err)
+
+ if len(args) == 0 {
+ if len(selectors) > 0 {
+ selector := strings.Join(selectors, ",")
+ err = client.DeleteTestWorkflows(selector)
+ ui.ExitOnError("deleting test workflows by labels: "+selector, err)
+ ui.SuccessAndExit("Successfully deleted test workflows by labels", selector)
+ } else if deleteAll {
+ err = client.DeleteTestWorkflows("")
+ ui.ExitOnError("delete all test workflows from namespace "+namespace, err)
+ ui.SuccessAndExit("Successfully deleted all test workflows in namespace", namespace)
+ } else {
+ ui.Failf("Pass test workflow name, --all flag to delete all or labels to delete by labels")
+ }
+ return
+ }
+
+ name := args[0]
+ err = client.DeleteTestWorkflow(name)
+ ui.ExitOnError("delete test workflow "+name+" from namespace "+namespace, err)
+ ui.SuccessAndExit("Successfully deleted test workflow", name)
+ },
+ }
+
+ cmd.Flags().BoolVar(&deleteAll, "all", false, "Delete all test workflows")
+ cmd.Flags().StringSliceVarP(&selectors, "label", "l", nil, "label key value pair: --label key1=value1")
+
+ return cmd
+}
diff --git a/cmd/kubectl-testkube/commands/testworkflows/executions.go b/cmd/kubectl-testkube/commands/testworkflows/executions.go
new file mode 100644
index 00000000000..4a6874c8c5a
--- /dev/null
+++ b/cmd/kubectl-testkube/commands/testworkflows/executions.go
@@ -0,0 +1,58 @@
+package testworkflows
+
+import (
+ "os"
+ "strings"
+
+ "github.com/spf13/cobra"
+
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/render"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testworkflows/renderer"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func NewGetTestWorkflowExecutionsCmd() *cobra.Command {
+ var (
+ limit int
+ selectors []string
+ testWorkflowName string
+ )
+
+ cmd := &cobra.Command{
+ Use: "testworkflowexecution [executionID]",
+ Aliases: []string{"testworkflowexecutions", "twe", "tw-execution", "twexecution"},
+ Args: cobra.MaximumNArgs(1),
+ Short: "Gets TestWorkflow execution details",
+ Long: `Gets TestWorkflow execution details by ID, or list if id is not passed`,
+
+ Run: func(cmd *cobra.Command, args []string) {
+ client, _, err := common.GetClient(cmd)
+ ui.ExitOnError("getting client", err)
+
+ if len(args) == 0 {
+ client, _, err := common.GetClient(cmd)
+ ui.ExitOnError("getting client", err)
+
+ executions, err := client.ListTestWorkflowExecutions(testWorkflowName, limit, strings.Join(selectors, ","))
+ ui.ExitOnError("getting test workflow executions list", err)
+ err = render.List(cmd, testkube.TestWorkflowExecutionSummaries(executions.Results), os.Stdout)
+ ui.ExitOnError("rendering list", err)
+ return
+ }
+
+ executionID := args[0]
+ execution, err := client.GetTestWorkflowExecution(executionID)
+ ui.ExitOnError("getting recent test workflow execution data id:"+execution.Id, err)
+ err = render.Obj(cmd, execution, os.Stdout, renderer.TestWorkflowExecutionRenderer)
+ ui.ExitOnError("rendering obj", err)
+ },
+ }
+
+ cmd.Flags().StringVarP(&testWorkflowName, "testworkflow", "w", "", "test workflow name")
+ cmd.Flags().IntVar(&limit, "limit", 1000, "max number of records to return")
+ cmd.Flags().StringSliceVarP(&selectors, "label", "l", nil, "label key value pair: --label key1=value1")
+
+ return cmd
+}
diff --git a/cmd/kubectl-testkube/commands/testworkflows/get.go b/cmd/kubectl-testkube/commands/testworkflows/get.go
new file mode 100644
index 00000000000..6f833eed05d
--- /dev/null
+++ b/cmd/kubectl-testkube/commands/testworkflows/get.go
@@ -0,0 +1,74 @@
+package testworkflows
+
+import (
+ "os"
+ "strings"
+
+ "github.com/spf13/cobra"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/render"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testworkflows/renderer"
+ common2 "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/tcl/mapperstcl/testworkflows"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func NewGetTestWorkflowsCmd() *cobra.Command {
+ var (
+ selectors []string
+ crdOnly bool
+ )
+
+ cmd := &cobra.Command{
+ Use: "testworkflow [name]",
+ Aliases: []string{"testworkflows", "tw"},
+ Args: cobra.MaximumNArgs(1),
+ Short: "Get all available test workflows",
+ Long: `Getting all available test workflows from given namespace - if no namespace given "testkube" namespace is used`,
+
+ Run: func(cmd *cobra.Command, args []string) {
+ namespace := cmd.Flag("namespace").Value.String()
+ client, _, err := common.GetClient(cmd)
+ ui.ExitOnError("getting client", err)
+
+ if len(args) == 0 {
+ workflows, err := client.ListTestWorkflowWithExecutions(strings.Join(selectors, ","))
+ ui.ExitOnError("getting all test workflows in namespace "+namespace, err)
+
+ if crdOnly {
+ ui.PrintCRDs(common2.MapSlice(workflows, func(t testkube.TestWorkflowWithExecution) testworkflowsv1.TestWorkflow {
+ return *testworkflows.MapAPIToKube(t.Workflow)
+ }), "TestWorkflow", testworkflowsv1.GroupVersion)
+ } else {
+ err = render.List(cmd, workflows, os.Stdout)
+ ui.PrintOnError("Rendering list", err)
+ }
+ return
+ }
+
+ name := args[0]
+ workflow, err := client.GetTestWorkflowWithExecution(name)
+ ui.ExitOnError("getting test workflow in namespace "+namespace, err)
+
+ if crdOnly {
+ ui.PrintCRD(testworkflows.MapTestWorkflowAPIToKube(*workflow.Workflow), "TestWorkflow", testworkflowsv1.GroupVersion)
+ } else {
+ err = render.Obj(cmd, *workflow.Workflow, os.Stdout, renderer.TestWorkflowRenderer)
+ ui.ExitOnError("rendering obj", err)
+
+ if workflow.LatestExecution != nil {
+ ui.NL()
+ err = render.Obj(cmd, *workflow.LatestExecution, os.Stdout, renderer.TestWorkflowExecutionRenderer)
+ ui.ExitOnError("rendering obj", err)
+ }
+ }
+ },
+ }
+ cmd.Flags().StringSliceVarP(&selectors, "label", "l", nil, "label key value pair: --label key1=value1")
+ cmd.Flags().BoolVar(&crdOnly, "crd-only", false, "show only test workflow crd")
+
+ return cmd
+}
diff --git a/cmd/kubectl-testkube/commands/testworkflows/renderer/testworkflow_obj.go b/cmd/kubectl-testkube/commands/testworkflows/renderer/testworkflow_obj.go
new file mode 100644
index 00000000000..75ad485db3e
--- /dev/null
+++ b/cmd/kubectl-testkube/commands/testworkflows/renderer/testworkflow_obj.go
@@ -0,0 +1,32 @@
+package renderer
+
+import (
+ "fmt"
+
+ "github.com/kubeshop/testkube/pkg/api/v1/client"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func TestWorkflowRenderer(client client.Client, ui *ui.UI, obj interface{}) error {
+ workflow, ok := obj.(testkube.TestWorkflow)
+ if !ok {
+ return fmt.Errorf("can't use '%T' as testkube.TestWorkflow in RenderObj for test workflow", obj)
+ }
+
+ ui.Info("Test Workflow:")
+ ui.Warn("Name: ", workflow.Name)
+ ui.Warn("Namespace:", workflow.Namespace)
+ ui.Warn("Created: ", workflow.Created.String())
+ if workflow.Description != "" {
+ ui.NL()
+ ui.Warn("Description: ", workflow.Description)
+ }
+ if len(workflow.Labels) > 0 {
+ ui.NL()
+ ui.Warn("Labels: ", testkube.MapToString(workflow.Labels))
+ }
+
+ return nil
+
+}
diff --git a/cmd/kubectl-testkube/commands/testworkflows/renderer/testworkflowexecution_obj.go b/cmd/kubectl-testkube/commands/testworkflows/renderer/testworkflowexecution_obj.go
new file mode 100644
index 00000000000..ecca4033f75
--- /dev/null
+++ b/cmd/kubectl-testkube/commands/testworkflows/renderer/testworkflowexecution_obj.go
@@ -0,0 +1,50 @@
+package renderer
+
+import (
+ "fmt"
+
+ "github.com/pkg/errors"
+
+ "github.com/kubeshop/testkube/pkg/api/v1/client"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func TestWorkflowExecutionRenderer(client client.Client, ui *ui.UI, obj interface{}) error {
+ execution, ok := obj.(testkube.TestWorkflowExecution)
+ if !ok {
+ return fmt.Errorf("can't use '%T' as testkube.TestWorkflowExecution in RenderObj for test workflow execution", obj)
+ }
+
+ ui.Info("Test Workflow Execution:")
+ ui.Warn("Name: ", execution.Workflow.Name)
+ if execution.Id != "" {
+ ui.Warn("Execution ID: ", execution.Id)
+ ui.Warn("Execution name: ", execution.Name)
+ if execution.Number != 0 {
+ ui.Warn("Execution number: ", fmt.Sprintf("%d", execution.Number))
+ }
+ ui.Warn("Requested at: ", execution.ScheduledAt.String())
+ if execution.Result != nil && execution.Result.Status != nil {
+ ui.Warn("Status: ", string(*execution.Result.Status))
+ if !execution.Result.QueuedAt.IsZero() {
+ ui.Warn("Queued at: ", execution.Result.QueuedAt.String())
+ }
+ if !execution.Result.StartedAt.IsZero() {
+ ui.Warn("Started at: ", execution.Result.StartedAt.String())
+ }
+ if !execution.Result.FinishedAt.IsZero() {
+ ui.Warn("Finished at: ", execution.Result.FinishedAt.String())
+ ui.Warn("Duration: ", execution.Result.FinishedAt.Sub(execution.Result.QueuedAt).String())
+ }
+ }
+ }
+
+ if execution.Result != nil && execution.Result.Initialization != nil && execution.Result.Initialization.ErrorMessage != "" {
+ ui.NL()
+ ui.Err(errors.New(execution.Result.Initialization.ErrorMessage))
+ }
+
+ return nil
+
+}
diff --git a/cmd/kubectl-testkube/commands/testworkflows/run.go b/cmd/kubectl-testkube/commands/testworkflows/run.go
new file mode 100644
index 00000000000..f8e81b6c3e8
--- /dev/null
+++ b/cmd/kubectl-testkube/commands/testworkflows/run.go
@@ -0,0 +1,213 @@
+package testworkflows
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/spf13/cobra"
+
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/render"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testworkflows/renderer"
+ apiclientv1 "github.com/kubeshop/testkube/pkg/api/v1/client"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+const (
+ LogTimestampLength = 30 // time.RFC3339Nano without 00:00 timezone
+)
+
+func NewRunTestWorkflowCmd() *cobra.Command {
+ var (
+ executionName string
+ config map[string]string
+ watchEnabled bool
+ )
+
+ cmd := &cobra.Command{
+ Use: "testworkflow [name]",
+ Aliases: []string{"testworkflows", "tw"},
+ Args: cobra.ExactArgs(1),
+ Short: "Starts test workflow execution",
+
+ Run: func(cmd *cobra.Command, args []string) {
+ namespace := cmd.Flag("namespace").Value.String()
+ client, _, err := common.GetClient(cmd)
+ ui.ExitOnError("getting client", err)
+
+ name := args[0]
+ execution, err := client.ExecuteTestWorkflow(name, testkube.TestWorkflowExecutionRequest{
+ Name: executionName,
+ Config: config,
+ })
+ ui.ExitOnError("execute test workflow "+name+" from namespace "+namespace, err)
+ err = render.Obj(cmd, execution, os.Stdout, renderer.TestWorkflowExecutionRenderer)
+ ui.ExitOnError("render test workflow execution", err)
+
+ ui.NL()
+ var exitCode = 0
+ if watchEnabled {
+ exitCode = uiWatch(execution, client)
+ ui.NL()
+ } else {
+ uiShellWatchExecution(execution.Id)
+ }
+
+ uiShellGetExecution(execution.Id)
+ os.Exit(exitCode)
+ },
+ }
+
+ cmd.Flags().StringVarP(&executionName, "name", "n", "", "execution name, if empty will be autogenerated")
+ cmd.Flags().StringToStringVarP(&config, "config", "", map[string]string{}, "configuration variables in a form of name1=val1 passed to executor")
+ cmd.Flags().BoolVarP(&watchEnabled, "watch", "f", false, "watch for changes after start")
+
+ return cmd
+}
+
+func uiWatch(execution testkube.TestWorkflowExecution, client apiclientv1.Client) int {
+ result, err := watchTestWorkflowLogs(execution.Id, execution.Signature, client)
+ ui.ExitOnError("reading test workflow execution logs", err)
+
+ // Apply the result in the execution
+ execution.Result = result
+ if result.IsFinished() {
+ execution.StatusAt = result.FinishedAt
+ }
+
+ // Display message depending on the result
+ switch {
+ case result.Initialization.ErrorMessage != "":
+ ui.Warn("test workflow execution failed:\n")
+ ui.Errf(result.Initialization.ErrorMessage)
+ return 1
+ case result.IsFailed():
+ ui.Warn("test workflow execution failed")
+ return 1
+ case result.IsAborted():
+ ui.Warn("test workflow execution aborted")
+ return 1
+ case result.IsPassed():
+ ui.Success("test workflow execution completed with success in " + result.FinishedAt.Sub(result.QueuedAt).String())
+ }
+ return 0
+}
+
+func uiShellGetExecution(id string) {
+ ui.ShellCommand(
+ "Use following command to get test workflow execution details",
+ "kubectl testkube get twe "+id,
+ )
+}
+
+func uiShellWatchExecution(id string) {
+ ui.ShellCommand(
+ "Watch test workflow execution until complete",
+ "kubectl testkube watch twe "+id,
+ )
+}
+
+func flattenSignatures(sig []testkube.TestWorkflowSignature) []testkube.TestWorkflowSignature {
+ res := make([]testkube.TestWorkflowSignature, 0)
+ for _, s := range sig {
+ if len(s.Children) == 0 {
+ res = append(res, s)
+ } else {
+ res = append(res, flattenSignatures(s.Children)...)
+ }
+ }
+ return res
+}
+
+func printResultDifference(res1 *testkube.TestWorkflowResult, res2 *testkube.TestWorkflowResult, steps []testkube.TestWorkflowSignature) bool {
+ if res1 == nil || res2 == nil {
+ return false
+ }
+ changed := false
+ for i, s := range steps {
+ r1 := res1.Steps[s.Ref]
+ r2 := res2.Steps[s.Ref]
+ r1Status := testkube.QUEUED_TestWorkflowStepStatus
+ r2Status := testkube.QUEUED_TestWorkflowStepStatus
+ if r1.Status != nil {
+ r1Status = *r1.Status
+ }
+ if r2.Status != nil {
+ r2Status = *r2.Status
+ }
+ if r1Status == r2Status {
+ continue
+ }
+ name := s.Category
+ if s.Name != "" {
+ name = s.Name
+ }
+ took := r2.FinishedAt.Sub(r2.QueuedAt).Round(time.Millisecond)
+ changed = true
+
+ switch r2Status {
+ case testkube.RUNNING_TestWorkflowStepStatus:
+ fmt.Print(ui.LightCyan(fmt.Sprintf("\n• (%d/%d) %s\n", i+1, len(steps), name)))
+ case testkube.SKIPPED_TestWorkflowStepStatus:
+ fmt.Print(ui.LightGray("• skipped\n"))
+ case testkube.PASSED_TestWorkflowStepStatus:
+ fmt.Print(ui.Green(fmt.Sprintf("\n• passed in %s\n", took)))
+ case testkube.ABORTED_TestWorkflowStepStatus:
+ fmt.Print(ui.Red("\n• aborted\n"))
+ default:
+ if s.Optional {
+ fmt.Print(ui.Yellow(fmt.Sprintf("\n• %s in %s (ignored)\n", string(r2Status), took)))
+ } else {
+ fmt.Print(ui.Red(fmt.Sprintf("\n• %s in %s\n", string(r2Status), took)))
+ }
+ }
+ }
+
+ return changed
+}
+
+func watchTestWorkflowLogs(id string, signature []testkube.TestWorkflowSignature, client apiclientv1.Client) (*testkube.TestWorkflowResult, error) {
+ ui.Info("Getting logs from test workflow job", id)
+
+ notifications, err := client.GetTestWorkflowExecutionNotifications(id)
+ ui.ExitOnError("getting logs from executor", err)
+
+ steps := flattenSignatures(signature)
+
+ var result *testkube.TestWorkflowResult
+ var isLineBeginning = true
+ for l := range notifications {
+ if l.Output != nil {
+ continue
+ }
+ if l.Result != nil {
+ isLineBeginning = printResultDifference(result, l.Result, steps)
+ result = l.Result
+ continue
+ }
+
+ // Strip timestamp + space for all new lines in the log
+ for len(l.Log) > 0 {
+ if isLineBeginning {
+ l.Log = l.Log[LogTimestampLength+1:]
+ isLineBeginning = false
+ }
+ newLineIndex := strings.Index(l.Log, "\n")
+ if newLineIndex == -1 {
+ fmt.Print(l.Log)
+ break
+ } else {
+ fmt.Print(l.Log[0 : newLineIndex+1])
+ l.Log = l.Log[newLineIndex+1:]
+ isLineBeginning = true
+ }
+ }
+ }
+
+ ui.NL()
+
+ return result, err
+}
diff --git a/cmd/kubectl-testkube/commands/testworkflows/watch.go b/cmd/kubectl-testkube/commands/testworkflows/watch.go
new file mode 100644
index 00000000000..f4946dabe53
--- /dev/null
+++ b/cmd/kubectl-testkube/commands/testworkflows/watch.go
@@ -0,0 +1,43 @@
+package testworkflows
+
+import (
+ "os"
+
+ "github.com/spf13/cobra"
+
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/render"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/validator"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testworkflows/renderer"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func NewWatchTestWorkflowExecutionCmd() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "testworkflowexecution ",
+ Aliases: []string{"testworkflowexecutions", "twe", "tw"},
+ Args: validator.ExecutionName,
+ Short: "Watch output from test workflow execution",
+ Long: `Gets test workflow execution details, until it's in success/error state, blocks until gets complete state`,
+
+ Run: func(cmd *cobra.Command, args []string) {
+ client, _, err := common.GetClient(cmd)
+ ui.ExitOnError("getting client", err)
+
+ executionID := args[0]
+ execution, err := client.GetTestWorkflowExecution(executionID)
+ ui.ExitOnError("get execution failed", err)
+ err = render.Obj(cmd, execution, os.Stdout, renderer.TestWorkflowExecutionRenderer)
+ ui.ExitOnError("render test workflow execution", err)
+
+ ui.NL()
+ exitCode := uiWatch(execution, client)
+ ui.NL()
+
+ uiShellGetExecution(execution.Id)
+ os.Exit(exitCode)
+ },
+ }
+
+ return cmd
+}
diff --git a/cmd/kubectl-testkube/commands/testworkflowtemplates/create.go b/cmd/kubectl-testkube/commands/testworkflowtemplates/create.go
new file mode 100644
index 00000000000..f0e01812f39
--- /dev/null
+++ b/cmd/kubectl-testkube/commands/testworkflowtemplates/create.go
@@ -0,0 +1,85 @@
+package testworkflowtemplates
+
+import (
+ "io"
+ "os"
+
+ "github.com/spf13/cobra"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
+ common2 "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/tcl/mapperstcl/testworkflows"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func NewCreateTestWorkflowTemplateCmd() *cobra.Command {
+ var (
+ name string
+ filePath string
+ update bool
+ )
+
+ cmd := &cobra.Command{
+ Use: "testworkflowtemplate",
+ Aliases: []string{"testworkflowtemplates", "twt"},
+ Args: cobra.MaximumNArgs(0),
+ Short: "Create test workflow template",
+
+ Run: func(cmd *cobra.Command, _ []string) {
+ namespace := cmd.Flag("namespace").Value.String()
+
+ var input io.Reader
+ if filePath == "" {
+ fi, err := os.Stdin.Stat()
+ ui.ExitOnError("reading stdin", err)
+ if fi.Mode()&os.ModeDevice != 0 {
+ ui.Failf("you need to pass stdin or --file argument with file path")
+ }
+ input = cmd.InOrStdin()
+ } else {
+ file, err := os.Open(filePath)
+ ui.ExitOnError("reading "+filePath+" file", err)
+ input = file
+ }
+
+ bytes, err := io.ReadAll(input)
+ ui.ExitOnError("reading input", err)
+
+ obj := new(testworkflowsv1.TestWorkflowTemplate)
+ err = common2.DeserializeCRD(obj, bytes)
+ ui.ExitOnError("deserializing input", err)
+ if obj.Kind != "" && obj.Kind != "TestWorkflowTemplate" {
+ ui.Failf("Only TestWorkflowTemplate objects are accepted. Received: %s", obj.Kind)
+ }
+ common2.AppendTypeMeta("TestWorkflowTemplate", testworkflowsv1.GroupVersion, obj)
+ obj.Namespace = namespace
+ if name != "" {
+ obj.Name = name
+ }
+
+ client, _, err := common.GetClient(cmd)
+ ui.ExitOnError("getting client", err)
+
+ workflow, _ := client.GetTestWorkflowTemplate(obj.Name)
+ if workflow.Name != "" {
+ if !update {
+ ui.Failf("Test workflow template with name '%s' already exists in namespace %s, use --update flag for upsert", obj.Name, namespace)
+ }
+ _, err = client.UpdateTestWorkflowTemplate(testworkflows.MapTestWorkflowTemplateKubeToAPI(*obj))
+ ui.ExitOnError("updating test workflow template "+obj.Name+" in namespace "+obj.Namespace, err)
+ ui.Success("Test workflow template updated", namespace, "/", obj.Name)
+ } else {
+ _, err = client.CreateTestWorkflowTemplate(testworkflows.MapTestWorkflowTemplateKubeToAPI(*obj))
+ ui.ExitOnError("creating test workflow "+obj.Name+" in namespace "+obj.Namespace, err)
+ ui.Success("Test workflow template created", namespace, "/", obj.Name)
+ }
+ },
+ }
+
+ cmd.Flags().StringVar(&name, "name", "", "test workflow template name")
+ cmd.Flags().BoolVar(&update, "update", false, "update, if test workflow template already exists")
+ cmd.Flags().StringVarP(&filePath, "file", "f", "", "file path to get the test workflow template specification")
+
+ return cmd
+}
diff --git a/cmd/kubectl-testkube/commands/testworkflowtemplates/delete.go b/cmd/kubectl-testkube/commands/testworkflowtemplates/delete.go
new file mode 100644
index 00000000000..5cb1b1ab8cc
--- /dev/null
+++ b/cmd/kubectl-testkube/commands/testworkflowtemplates/delete.go
@@ -0,0 +1,54 @@
+package testworkflowtemplates
+
+import (
+ "strings"
+
+ "github.com/spf13/cobra"
+
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func NewDeleteTestWorkflowTemplateCmd() *cobra.Command {
+ var deleteAll bool
+ var selectors []string
+
+ cmd := &cobra.Command{
+ Use: "testworkflowtemplate [name]",
+ Aliases: []string{"testworkflowtemplates", "twt"},
+ Args: cobra.MaximumNArgs(1),
+ Short: "Delete test workflow templates",
+
+ Run: func(cmd *cobra.Command, args []string) {
+ namespace := cmd.Flag("namespace").Value.String()
+ client, _, err := common.GetClient(cmd)
+ ui.ExitOnError("getting client", err)
+
+ if len(args) == 0 {
+ if len(selectors) > 0 {
+ selector := strings.Join(selectors, ",")
+ err = client.DeleteTestWorkflowTemplates(selector)
+ ui.ExitOnError("deleting test workflow templates by labels: "+selector, err)
+ ui.SuccessAndExit("Successfully deleted test workflow templates by labels", selector)
+ } else if deleteAll {
+ err = client.DeleteTestWorkflowTemplates("")
+ ui.ExitOnError("delete all test workflow templates from namespace "+namespace, err)
+ ui.SuccessAndExit("Successfully deleted all test workflow templates in namespace", namespace)
+ } else {
+ ui.Failf("Pass test workflow template name, --all flag to delete all or labels to delete by labels")
+ }
+ return
+ }
+
+ name := args[0]
+ err = client.DeleteTestWorkflowTemplate(name)
+ ui.ExitOnError("delete test workflow template "+name+" from namespace "+namespace, err)
+ ui.SuccessAndExit("Successfully deleted test workflow template", name)
+ },
+ }
+
+ cmd.Flags().BoolVar(&deleteAll, "all", false, "Delete all test workflow templates")
+ cmd.Flags().StringSliceVarP(&selectors, "label", "l", nil, "label key value pair: --label key1=value1")
+
+ return cmd
+}
diff --git a/cmd/kubectl-testkube/commands/testworkflowtemplates/get.go b/cmd/kubectl-testkube/commands/testworkflowtemplates/get.go
new file mode 100644
index 00000000000..ad1dc996cfe
--- /dev/null
+++ b/cmd/kubectl-testkube/commands/testworkflowtemplates/get.go
@@ -0,0 +1,64 @@
+package testworkflowtemplates
+
+import (
+ "os"
+ "strings"
+
+ "github.com/spf13/cobra"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/render"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testworkflowtemplates/renderer"
+ "github.com/kubeshop/testkube/pkg/tcl/mapperstcl/testworkflows"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func NewGetTestWorkflowTemplatesCmd() *cobra.Command {
+ var (
+ selectors []string
+ crdOnly bool
+ )
+
+ cmd := &cobra.Command{
+ Use: "testworkflowtemplate [name]",
+ Aliases: []string{"testworkflowtemplates", "twt"},
+ Args: cobra.MaximumNArgs(1),
+ Short: "Get all available test workflow templates",
+ Long: `Getting all available test workflow templates from given namespace - if no namespace given "testkube" namespace is used`,
+
+ Run: func(cmd *cobra.Command, args []string) {
+ namespace := cmd.Flag("namespace").Value.String()
+ client, _, err := common.GetClient(cmd)
+ ui.ExitOnError("getting client", err)
+
+ if len(args) == 0 {
+ templates, err := client.ListTestWorkflowTemplates(strings.Join(selectors, ","))
+ ui.ExitOnError("getting all test workflow templates in namespace "+namespace, err)
+
+ if crdOnly {
+ ui.PrintCRDs(testworkflows.MapTemplateListAPIToKube(templates).Items, "TestWorkflowTemplate", testworkflowsv1.GroupVersion)
+ } else {
+ err = render.List(cmd, templates, os.Stdout)
+ ui.PrintOnError("Rendering list", err)
+ }
+ return
+ }
+
+ name := args[0]
+ template, err := client.GetTestWorkflowTemplate(name)
+ ui.ExitOnError("getting test workflow in namespace "+namespace, err)
+
+ if crdOnly {
+ ui.PrintCRD(testworkflows.MapTestWorkflowTemplateAPIToKube(template), "TestWorkflowTemplate", testworkflowsv1.GroupVersion)
+ } else {
+ err = render.Obj(cmd, template, os.Stdout, renderer.TestWorkflowTemplateRenderer)
+ ui.ExitOnError("rendering obj", err)
+ }
+ },
+ }
+ cmd.Flags().StringSliceVarP(&selectors, "label", "l", nil, "label key value pair: --label key1=value1")
+ cmd.Flags().BoolVar(&crdOnly, "crd-only", false, "show only test workflow template crd")
+
+ return cmd
+}
diff --git a/cmd/kubectl-testkube/commands/testworkflowtemplates/renderer/testworkflow_obj.go b/cmd/kubectl-testkube/commands/testworkflowtemplates/renderer/testworkflow_obj.go
new file mode 100644
index 00000000000..8bd70e4ca99
--- /dev/null
+++ b/cmd/kubectl-testkube/commands/testworkflowtemplates/renderer/testworkflow_obj.go
@@ -0,0 +1,32 @@
+package renderer
+
+import (
+ "fmt"
+
+ "github.com/kubeshop/testkube/pkg/api/v1/client"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func TestWorkflowTemplateRenderer(client client.Client, ui *ui.UI, obj interface{}) error {
+ template, ok := obj.(testkube.TestWorkflowTemplate)
+ if !ok {
+ return fmt.Errorf("can't use '%T' as testkube.TestWorkflowTemplate in RenderObj for test workflow template", obj)
+ }
+
+ ui.Info("Test Workflow Template:")
+ ui.Warn("Name: ", template.Name)
+ ui.Warn("Namespace:", template.Namespace)
+ ui.Warn("Created: ", template.Created.String())
+ if template.Description != "" {
+ ui.NL()
+ ui.Warn("Description: ", template.Description)
+ }
+ if len(template.Labels) > 0 {
+ ui.NL()
+ ui.Warn("Labels: ", testkube.MapToString(template.Labels))
+ }
+
+ return nil
+
+}
diff --git a/cmd/kubectl-testkube/commands/watch.go b/cmd/kubectl-testkube/commands/watch.go
index db02ff293db..a2cd1ce08a2 100644
--- a/cmd/kubectl-testkube/commands/watch.go
+++ b/cmd/kubectl-testkube/commands/watch.go
@@ -7,6 +7,7 @@ import (
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/validator"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/tests"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testsuites"
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testworkflows"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/config"
"github.com/kubeshop/testkube/pkg/ui"
)
@@ -31,6 +32,7 @@ func NewWatchCmd() *cobra.Command {
cmd.AddCommand(tests.NewWatchExecutionCmd())
cmd.AddCommand(testsuites.NewWatchTestSuiteExecutionCmd())
+ cmd.AddCommand(testworkflows.NewWatchTestWorkflowExecutionCmd())
return cmd
}
diff --git a/cmd/logs/main.go b/cmd/logs/main.go
index 3f23ba3a2e4..3bca5bdb049 100644
--- a/cmd/logs/main.go
+++ b/cmd/logs/main.go
@@ -3,23 +3,44 @@ package main
import (
"context"
"errors"
+
"os"
"os/signal"
"syscall"
+ "github.com/nats-io/nats.go/jetstream"
+ "github.com/oklog/run"
"go.uber.org/zap"
+ "google.golang.org/grpc/credentials"
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/agent"
"github.com/kubeshop/testkube/pkg/event/bus"
"github.com/kubeshop/testkube/pkg/log"
"github.com/kubeshop/testkube/pkg/logs"
"github.com/kubeshop/testkube/pkg/logs/adapter"
+ "github.com/kubeshop/testkube/pkg/logs/client"
"github.com/kubeshop/testkube/pkg/logs/config"
+ "github.com/kubeshop/testkube/pkg/logs/pb"
+ "github.com/kubeshop/testkube/pkg/logs/repository"
"github.com/kubeshop/testkube/pkg/logs/state"
-
- "github.com/nats-io/nats.go/jetstream"
- "github.com/oklog/run"
+ "github.com/kubeshop/testkube/pkg/storage/minio"
+ "github.com/kubeshop/testkube/pkg/ui"
)
+func newStorageClient(cfg *config.Config) *minio.Client {
+ opts := minio.GetTLSOptions(cfg.StorageSSL, cfg.StorageSkipVerify, cfg.StorageCertFile, cfg.StorageKeyFile, cfg.StorageCAFile)
+ return minio.NewClient(
+ cfg.StorageEndpoint,
+ cfg.StorageAccessKeyID,
+ cfg.StorageSecretAccessKey,
+ cfg.StorageRegion,
+ cfg.StorageToken,
+ cfg.StorageBucket,
+ opts...,
+ )
+}
+
func main() {
var g run.Group
@@ -29,24 +50,99 @@ func main() {
cfg := Must(config.Get())
+ mode := common.ModeStandalone
+ if cfg.TestkubeProAPIKey != "" {
+ mode = common.ModeAgent
+ }
+
// Event bus
- nc := Must(bus.NewNATSConnection(cfg.NatsURI))
+ nc := Must(bus.NewNATSConnection(bus.ConnectionConfig{
+ NatsURI: cfg.NatsURI,
+ NatsSecure: cfg.NatsSecure,
+ NatsSkipVerify: cfg.NatsSkipVerify,
+ NatsCertFile: cfg.NatsCertFile,
+ NatsKeyFile: cfg.NatsKeyFile,
+ NatsCAFile: cfg.NatsCAFile,
+ NatsConnectTimeout: cfg.NatsConnectTimeout,
+ }))
defer func() {
log.Infof("closing nats connection")
nc.Close()
}()
js := Must(jetstream.New(nc))
+ logStream := Must(client.NewNatsLogStream(nc))
+
+ minioClient := newStorageClient(cfg)
+ if err := minioClient.Connect(); err != nil {
+ log.Fatalw("error connecting to minio", "error", err)
+ }
+
+ if err := minioClient.SetExpirationPolicy(cfg.StorageExpiration); err != nil {
+ log.Warnw("error setting expiration policy", "error", err)
+ }
kv := Must(js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: cfg.KVBucketName}))
state := state.NewState(kv)
- svc := logs.NewLogsService(nc, js, state).
+ svc := logs.NewLogsService(nc, js, state, logStream).
WithHttpAddress(cfg.HttpAddress).
- WithGrpcAddress(cfg.GrpcAddress)
+ WithGrpcAddress(cfg.GrpcAddress).
+ WithLogsRepositoryFactory(repository.NewJsMinioFactory(minioClient, cfg.StorageBucket, logStream)).
+ WithMessageTracing(cfg.TraceMessages)
- // TODO - add adapters here
- svc.AddAdapter(adapter.NewDummyAdapter())
+ // quite noisy in logs - will echo all messages incoming from logs
+ if cfg.AttachDebugAdapter {
+ svc.AddAdapter(adapter.NewDebugAdapter())
+ }
+
+ creds, err := newGRPCTransportCredentials(cfg)
+ if err != nil {
+ log.Fatalw("error getting tls credentials", "error", err)
+ }
+
+ log.Infow("starting logs service", "mode", mode)
+
+ // add given log adapter depends from mode
+ switch mode {
+
+ case common.ModeAgent:
+ grpcConn, err := agent.NewGRPCConnection(
+ ctx,
+ cfg.TestkubeProTLSInsecure,
+ cfg.TestkubeProSkipVerify,
+ cfg.TestkubeProURL+cfg.TestkubeProLogsPath,
+ cfg.TestkubeProCertFile,
+ cfg.TestkubeProKeyFile,
+ cfg.TestkubeProCAFile,
+ log,
+ )
+ ui.ExitOnError("error creating gRPC connection for logs service", err)
+ defer grpcConn.Close()
+ grpcClient := pb.NewCloudLogsServiceClient(grpcConn)
+ cloudAdapter := adapter.NewCloudAdapter(grpcClient, cfg.TestkubeProAPIKey)
+ log.Infow("cloud adapter created", "endpoint", cfg.TestkubeProURL)
+ svc.AddAdapter(cloudAdapter)
+
+ case common.ModeStandalone:
+ minioAdapter, err := adapter.NewMinioAdapter(cfg.StorageEndpoint,
+ cfg.StorageAccessKeyID,
+ cfg.StorageSecretAccessKey,
+ cfg.StorageRegion,
+ cfg.StorageToken,
+ cfg.StorageBucket,
+ cfg.StorageSSL,
+ cfg.StorageSkipVerify,
+ cfg.StorageCertFile,
+ cfg.StorageKeyFile,
+ cfg.StorageCAFile)
+
+ if err != nil {
+ log.Errorw("error creating minio adapter", "error", err)
+ }
+ log.Infow("minio adapter created", "bucket", cfg.StorageBucket, "endpoint", cfg.StorageEndpoint)
+ svc.AddAdapter(minioAdapter)
+ }
g.Add(func() error {
err := interrupt(log, ctx)
@@ -67,7 +163,7 @@ func main() {
})
g.Add(func() error {
- return svc.RunGRPCServer(ctx)
+ return svc.RunGRPCServer(ctx, creds)
}, func(error) {
cancel()
})
@@ -104,3 +200,13 @@ func Must[T any](val T, err error) T {
}
return val
}
+
+func newGRPCTransportCredentials(cfg *config.Config) (credentials.TransportCredentials, error) {
+ return logs.GetGrpcTransportCredentials(logs.GrpcConnectionConfig{
+ Secure: cfg.GrpcSecure,
+ ClientAuth: cfg.GrpcClientAuth,
+ CertFile: cfg.GrpcCertFile,
+ KeyFile: cfg.GrpcKeyFile,
+ ClientCAFile: cfg.GrpcClientCAFile,
+ })
+}
diff --git a/cmd/sidecar/main.go b/cmd/sidecar/main.go
index d2a54bb19e7..6f069b8c20b 100644
--- a/cmd/sidecar/main.go
+++ b/cmd/sidecar/main.go
@@ -23,7 +23,15 @@ func main() {
cfg := Must(config.Get())
// Event bus
- nc := Must(bus.NewNATSConnection(cfg.NatsURI))
+ nc := Must(bus.NewNATSConnection(bus.ConnectionConfig{
+ NatsURI: cfg.NatsURI,
+ NatsSecure: cfg.NatsSecure,
+ NatsSkipVerify: cfg.NatsSkipVerify,
+ NatsCertFile: cfg.NatsCertFile,
+ NatsKeyFile: cfg.NatsKeyFile,
+ NatsCAFile: cfg.NatsCAFile,
+ NatsConnectTimeout: cfg.NatsConnectTimeout,
+ }))
defer func() {
log.Infof("closing nats connection")
nc.Close()
@@ -39,7 +47,7 @@ func main() {
podsClient := clientset.CoreV1().Pods(cfg.Namespace)
- logsStream, err := client.NewNatsLogStream(nc, cfg.ExecutionId)
+ logsStream, err := client.NewNatsLogStream(nc)
if err != nil {
ui.ExitOnError("error creating logs stream", err)
return
diff --git a/cmd/tcl/README.md b/cmd/tcl/README.md
new file mode 100644
index 00000000000..25ca004f001
--- /dev/null
+++ b/cmd/tcl/README.md
@@ -0,0 +1,7 @@
+# Testkube - TCL Package
+
+This folder contains special code with the Testkube Community license.
+
+## License
+
+The code in this folder is licensed under the Testkube Community License. Please see the [LICENSE](../../licenses/TCL.txt) file for more information.
diff --git a/cmd/tcl/testworkflow-init/constants/commands.go b/cmd/tcl/testworkflow-init/constants/commands.go
new file mode 100644
index 00000000000..a11d172a7f9
--- /dev/null
+++ b/cmd/tcl/testworkflow-init/constants/commands.go
@@ -0,0 +1,20 @@
+package constants
+
+const (
+ ArgSeparator = "--"
+ ArgInit = "-i"
+ ArgInitLong = "--init"
+ ArgCondition = "-c"
+ ArgConditionLong = "--cond"
+ ArgResult = "-r"
+ ArgResultLong = "--result"
+ ArgTimeout = "-t"
+ ArgTimeoutLong = "--timeout"
+ ArgComputeEnv = "-e"
+ ArgComputeEnvLong = "--env"
+ ArgNegative = "-n"
+ ArgNegativeLong = "--negative"
+ ArgDebug = "--debug"
+ ArgRetryUntil = "--retryUntil" // TODO: Replace when multi-level retry will be there
+ ArgRetryCount = "--retryCount" // TODO: Replace when multi-level retry will be there
+)
diff --git a/cmd/tcl/testworkflow-init/data/config.go b/cmd/tcl/testworkflow-init/data/config.go
new file mode 100644
index 00000000000..bbcf47feb1d
--- /dev/null
+++ b/cmd/tcl/testworkflow-init/data/config.go
@@ -0,0 +1,33 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package data
+
+import (
+ "os"
+)
+
+type config struct {
+ Negative bool
+ Debug bool
+ RetryCount int
+ RetryUntil string
+
+ Resulting []Rule
+}
+
+var Config = &config{
+ Debug: os.Getenv("DEBUG") == "1",
+}
+
+func LoadConfig(config map[string]string) {
+ Config.Debug = getBool(config, "debug", Config.Debug)
+ Config.RetryCount = getInt(config, "retryCount", 0)
+ Config.RetryUntil = getStr(config, "retryUntil", "self.passed")
+ Config.Negative = getBool(config, "negative", false)
+}
diff --git a/cmd/tcl/testworkflow-init/data/emit.go b/cmd/tcl/testworkflow-init/data/emit.go
new file mode 100644
index 00000000000..83fc8e69416
--- /dev/null
+++ b/cmd/tcl/testworkflow-init/data/emit.go
@@ -0,0 +1,84 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package data
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+)
+
+const (
+ InstructionPrefix = "\u0001\u0005"
+ HintPrefix = "\u0006"
+ InstructionSeparator = "\u0003"
+ InstructionValueSeparator = "\u0004"
+)
+
+func SprintOutput(ref string, name string, value interface{}) string {
+ j, err := json.Marshal(value)
+ if err != nil {
+ panic(fmt.Sprintf("error while marshalling reference: %v", err))
+ }
+ var sb strings.Builder
+ sb.WriteString("\n")
+ sb.WriteString(InstructionPrefix)
+ sb.WriteString(ref)
+ sb.WriteString(InstructionSeparator)
+ sb.WriteString(name)
+ sb.WriteString(InstructionValueSeparator)
+ sb.Write(j)
+ sb.WriteString(InstructionSeparator)
+ sb.WriteString("\n")
+ return sb.String()
+}
+
+func SprintHint(ref string, name string) string {
+ var sb strings.Builder
+ sb.WriteString("\n")
+ sb.WriteString(InstructionPrefix)
+ sb.WriteString(HintPrefix)
+ sb.WriteString(ref)
+ sb.WriteString(InstructionSeparator)
+ sb.WriteString(name)
+ sb.WriteString(InstructionSeparator)
+ sb.WriteString("\n")
+ return sb.String()
+}
+
+func SprintHintDetails(ref string, name string, value interface{}) string {
+ j, err := json.Marshal(value)
+ if err != nil {
+ panic(fmt.Sprintf("error while marshalling reference: %v", err))
+ }
+ var sb strings.Builder
+ sb.WriteString("\n")
+ sb.WriteString(InstructionPrefix)
+ sb.WriteString(HintPrefix)
+ sb.WriteString(ref)
+ sb.WriteString(InstructionSeparator)
+ sb.WriteString(name)
+ sb.WriteString(InstructionValueSeparator)
+ sb.Write(j)
+ sb.WriteString(InstructionSeparator)
+ sb.WriteString("\n")
+ return sb.String()
+}
+
+func PrintOutput(ref string, name string, value interface{}) {
+ fmt.Print(SprintOutput(ref, name, value))
+}
+
+func PrintHint(ref string, name string) {
+ fmt.Print(SprintHint(ref, name))
+}
+
+func PrintHintDetails(ref string, name string, value interface{}) {
+ fmt.Print(SprintHintDetails(ref, name, value))
+}
diff --git a/cmd/tcl/testworkflow-init/data/expressions.go b/cmd/tcl/testworkflow-init/data/expressions.go
new file mode 100644
index 00000000000..5febf92e390
--- /dev/null
+++ b/cmd/tcl/testworkflow-init/data/expressions.go
@@ -0,0 +1,130 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package data
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/pkg/errors"
+
+ "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+)
+
+var aliases = map[string]string{
+ "always": `true`,
+ "never": `false`,
+
+ "error": `failed`,
+ "success": `passed`,
+
+ "self.error": `self.failed`,
+ "self.success": `self.passed`,
+
+ "passed": `!status`,
+ "failed": `bool(status) && status != "skipped"`,
+
+ "self.passed": `!self.status`,
+ "self.failed": `bool(self.status) && self.status != "skipped"`,
+}
+
+var LocalMachine = expressionstcl.NewMachine().
+ Register("status", expressionstcl.MustCompile("self.status"))
+
+var RefMachine = expressionstcl.NewMachine().
+ RegisterAccessor(func(name string) (interface{}, bool) {
+ if name == "_ref" {
+ return Step.Ref, true
+ }
+ return nil, false
+ })
+
+var AliasMachine = expressionstcl.NewMachine().
+ RegisterAccessorExt(func(name string) (interface{}, bool, error) {
+ alias, ok := aliases[name]
+ if !ok {
+ return nil, false, nil
+ }
+ expr, err := expressionstcl.Compile(alias)
+ if err != nil {
+ return expr, false, err
+ }
+ expr, err = expr.Resolve(RefMachine)
+ return expr, true, err
+ })
+
+var StateMachine = expressionstcl.NewMachine().
+ RegisterAccessor(func(name string) (interface{}, bool) {
+ if name == "status" {
+ return State.GetStatus(), true
+ } else if name == "self.status" {
+ return State.GetSelfStatus(), true
+ }
+ return nil, false
+ }).
+ RegisterAccessorExt(func(name string) (interface{}, bool, error) {
+ if strings.HasPrefix(name, "output.") {
+ return State.GetOutput(name[7:])
+ }
+ return nil, false, nil
+ })
+
+var EnvMachine = expressionstcl.NewMachine().
+ RegisterAccessor(func(name string) (interface{}, bool) {
+ if strings.HasPrefix(name, "env.") {
+ return os.Getenv(name[4:]), true
+ }
+ return nil, false
+ })
+
+var RefSuccessMachine = expressionstcl.NewMachine().
+ RegisterAccessor(func(ref string) (interface{}, bool) {
+ s := State.GetStep(ref)
+ return s.Status == StepStatusPassed || s.Status == StepStatusSkipped, s.HasStatus
+ })
+
+var RefStatusMachine = expressionstcl.NewMachine().
+ RegisterAccessor(func(ref string) (interface{}, bool) {
+ return string(State.GetStep(ref).Status), true
+ })
+
+var FileMachine = expressionstcl.NewMachine().
+ RegisterFunction("file", func(values ...expressionstcl.StaticValue) (interface{}, bool, error) {
+ if len(values) != 1 {
+ return nil, true, errors.New("file() function takes a single argument")
+ }
+ if !values[0].IsString() {
+ return nil, true, fmt.Errorf("file() function expects a string argument, provided: %v", values[0].String())
+ }
+ filePath, _ := values[0].StringValue()
+ file, err := os.ReadFile(filePath)
+ if err != nil {
+ return nil, true, fmt.Errorf("reading file(%s): %s", filePath, err.Error())
+ }
+ return string(file), true, nil
+ })
+
+func Template(tpl string, m ...expressionstcl.Machine) (string, error) {
+ m = append(m, AliasMachine, EnvMachine, StateMachine, FileMachine)
+ return expressionstcl.EvalTemplate(tpl, m...)
+}
+
+func Expression(expr string, m ...expressionstcl.Machine) (expressionstcl.StaticValue, error) {
+ m = append(m, AliasMachine, EnvMachine, StateMachine, FileMachine)
+ return expressionstcl.EvalExpression(expr, m...)
+}
+
+func RefSuccessExpression(expr string) (expressionstcl.StaticValue, error) {
+ return expressionstcl.EvalExpression(expr, RefSuccessMachine)
+}
+
+func RefStatusExpression(expr string) (expressionstcl.StaticValue, error) {
+ return expressionstcl.EvalExpression(expr, RefStatusMachine)
+}
diff --git a/cmd/tcl/testworkflow-init/data/state.go b/cmd/tcl/testworkflow-init/data/state.go
new file mode 100644
index 00000000000..352053eca7c
--- /dev/null
+++ b/cmd/tcl/testworkflow-init/data/state.go
@@ -0,0 +1,170 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package data
+
+import (
+ "bytes"
+ "encoding/gob"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+)
+
+const (
+ defaultInternalPath = "/.tktw"
+ defaultTerminationLogPath = "/dev/termination-log"
+)
+
+type state struct {
+ Status TestWorkflowStatus `json:"status"`
+ Steps map[string]*StepInfo `json:"steps"`
+ Output map[string]string `json:"output"`
+}
+
+var State = &state{
+ Steps: map[string]*StepInfo{},
+ Output: map[string]string{},
+}
+
+func (s *state) GetStep(ref string) *StepInfo {
+ _, ok := State.Steps[ref]
+ if !ok {
+ State.Steps[ref] = &StepInfo{Ref: ref}
+ }
+ return State.Steps[ref]
+}
+
+func (s *state) GetOutput(name string) (expressionstcl.Expression, bool, error) {
+ v, ok := s.Output[name]
+ if !ok {
+ return expressionstcl.None, false, nil
+ }
+ expr, err := expressionstcl.Compile(v)
+ return expr, true, err
+}
+
+func (s *state) GetSelfStatus() string {
+ if Step.Executed {
+ return string(Step.Status)
+ }
+ v := s.GetStep(Step.Ref)
+ if v.Status != StepStatusPassed {
+ return string(v.Status)
+ }
+ return string(Step.Status)
+}
+
+func (s *state) GetStatus() string {
+ if Step.Executed {
+ return string(Step.Status)
+ }
+ if Step.InitStatus == "" {
+ return string(s.Status)
+ }
+ v, err := RefStatusExpression(Step.InitStatus)
+ if err != nil {
+ return string(s.Status)
+ }
+ str, _ := v.Static().StringValue()
+ if str == "" {
+ return string(s.Status)
+ }
+ return str
+}
+
+func readState(filePath string) {
+ b, err := os.ReadFile(filePath)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ panic(err)
+ }
+ return
+ }
+ if len(b) == 0 {
+ return
+ }
+ err = gob.NewDecoder(bytes.NewBuffer(b)).Decode(&State)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func persistState(filePath string) {
+ b := bytes.Buffer{}
+ err := gob.NewEncoder(&b).Encode(State)
+ if err != nil {
+ panic(err)
+ }
+
+ err = os.WriteFile(filePath, b.Bytes(), 0777)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func recomputeStatuses() {
+ // Read current status
+ status := StepStatus(State.GetSelfStatus())
+
+ // Update own status
+ State.GetStep(Step.Ref).SetStatus(status)
+
+ // Update expected failure statuses
+ Iterate(Config.Resulting, func(r Rule) bool {
+ v, err := RefSuccessExpression(r.Expr)
+ if err != nil {
+ return false
+ }
+ vv, _ := v.Static().BoolValue()
+ if !vv {
+ for _, ref := range r.Refs {
+ if ref == "" {
+ State.Status = TestWorkflowStatusFailed
+ } else {
+ State.GetStep(ref).SetStatus(StepStatusFailed)
+ }
+ }
+ }
+ return true
+ })
+}
+
+func persistStatus(filePath string) {
+ // Persist container termination result
+ res := fmt.Sprintf(`%s,%d`, State.GetStep(Step.Ref).Status, Step.ExitCode)
+ err := os.WriteFile(filePath, []byte(res), 0755)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func LoadState() {
+ readState(filepath.Join(defaultInternalPath, "state"))
+}
+
+func Finish() {
+ // Persist step information and shared data
+ recomputeStatuses()
+ persistStatus(defaultTerminationLogPath)
+ persistState(filepath.Join(defaultInternalPath, "state"))
+
+ // Kill the sub-process
+ if Step.Cmd != nil && Step.Cmd.Process != nil {
+ _ = Step.Cmd.Process.Kill()
+ }
+
+ // Emit end hint to allow exporting the timestamp
+ PrintHint(Step.Ref, "end")
+
+ // The init process needs to finish with zero exit code,
+ // to continue with the next container.
+ os.Exit(0)
+}
diff --git a/cmd/tcl/testworkflow-init/data/step.go b/cmd/tcl/testworkflow-init/data/step.go
new file mode 100644
index 00000000000..531d51b63f6
--- /dev/null
+++ b/cmd/tcl/testworkflow-init/data/step.go
@@ -0,0 +1,22 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package data
+
+import "os/exec"
+
+type step struct {
+ Ref string
+ Cmd *exec.Cmd
+ Status StepStatus
+ ExitCode uint8
+ Executed bool
+ InitStatus string
+}
+
+var Step = &step{}
diff --git a/cmd/tcl/testworkflow-init/data/types.go b/cmd/tcl/testworkflow-init/data/types.go
new file mode 100644
index 00000000000..39449f8732e
--- /dev/null
+++ b/cmd/tcl/testworkflow-init/data/types.go
@@ -0,0 +1,105 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package data
+
+import (
+ "strings"
+ "time"
+)
+
+type TestWorkflowStatus string
+
+const (
+ TestWorkflowStatusPassed TestWorkflowStatus = ""
+ TestWorkflowStatusFailed TestWorkflowStatus = "failed"
+ TestWorkflowStatusAborted TestWorkflowStatus = "aborted"
+)
+
+type StepStatus string
+
+const (
+ StepStatusPassed StepStatus = ""
+ StepStatusTimeout StepStatus = "timeout"
+ StepStatusFailed StepStatus = "failed"
+ StepStatusAborted StepStatus = "aborted"
+ StepStatusSkipped StepStatus = "skipped"
+)
+
+type Rule struct {
+ Expr string
+ Refs []string
+}
+
+type Timeout struct {
+ Ref string
+ Duration string
+}
+
+type StepInfo struct {
+ Ref string `json:"ref"`
+ Status StepStatus `json:"status"`
+ HasStatus bool `json:"hasStatus"`
+ StartTime time.Time `json:"startTime"`
+ TimeoutAt time.Time `json:"timeoutAt"`
+ Iteration uint64 `json:"iteration"`
+}
+
+func (s *StepInfo) Start(t time.Time) {
+ if s.StartTime.IsZero() {
+ s.StartTime = t
+ s.Iteration = 1
+ PrintHint(s.Ref, "start")
+ }
+}
+
+func (s *StepInfo) Next() {
+ if s.StartTime.IsZero() {
+ s.Start(time.Now())
+ } else {
+ s.Iteration++
+ PrintHintDetails(s.Ref, "iteration", s.Iteration)
+ }
+}
+
+func (s *StepInfo) Skip(t time.Time) {
+ if s.Status != StepStatusSkipped {
+ s.StartTime = t
+ s.Iteration = 0
+ s.SetStatus(StepStatusSkipped)
+ }
+}
+
+func (s *StepInfo) SetTimeoutDuration(t time.Time, duration string) error {
+ if !s.TimeoutAt.IsZero() {
+ return nil
+ }
+ s.Start(t)
+ v, err := Template(duration)
+ if err != nil {
+ return err
+ }
+ d, err := time.ParseDuration(strings.ReplaceAll(v, " ", ""))
+ if err != nil {
+ return err
+ }
+ s.TimeoutAt = s.StartTime.Add(d)
+ return nil
+}
+
+func (s *StepInfo) SetStatus(status StepStatus) {
+ if !s.HasStatus || s.Status == StepStatusPassed {
+ s.Status = status
+ s.HasStatus = true
+ if status == StepStatusPassed {
+ PrintHintDetails(s.Ref, "status", "passed")
+ } else {
+ PrintHintDetails(s.Ref, "status", status)
+ }
+ }
+}
diff --git a/cmd/tcl/testworkflow-init/data/utils.go b/cmd/tcl/testworkflow-init/data/utils.go
new file mode 100644
index 00000000000..c0c5f7fb541
--- /dev/null
+++ b/cmd/tcl/testworkflow-init/data/utils.go
@@ -0,0 +1,61 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package data
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+)
+
+func getStr(config map[string]string, key string, defaultValue string) string {
+ val, ok := config[key]
+ if !ok {
+ return defaultValue
+ }
+ return val
+}
+
+func getInt(config map[string]string, key string, defaultValue int) int {
+ str := getStr(config, key, "")
+ if str == "" {
+ return defaultValue
+ }
+ val, err := strconv.Atoi(str)
+ if err != nil {
+ fmt.Printf("invalid '%s' provided: '%s': %v\n", key, str, err)
+ os.Exit(155)
+ }
+ return val
+}
+
+func getBool(config map[string]string, key string, defaultValue bool) bool {
+ str := getStr(config, key, "")
+ if str == "" {
+ return defaultValue
+ }
+ return strings.ToLower(str) == "true" || str == "1"
+}
+
+// Iterate over all items, all the time, until no more is done
+func Iterate[T any](v []T, fn func(T) bool) {
+ result := v
+ for {
+ l := len(result)
+ for i := 0; i < len(result); i++ {
+ if fn(result[i]) {
+ result = append(result[0:i], result[i+1:]...)
+ }
+ }
+ if len(result) == l {
+ return
+ }
+ }
+}
diff --git a/cmd/tcl/testworkflow-init/main.go b/cmd/tcl/testworkflow-init/main.go
new file mode 100644
index 00000000000..d387ed452db
--- /dev/null
+++ b/cmd/tcl/testworkflow-init/main.go
@@ -0,0 +1,215 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "os/signal"
+ "slices"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/kballard/go-shellquote"
+
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-init/constants"
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-init/data"
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-init/output"
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-init/run"
+)
+
+func main() {
+ if len(os.Args) < 2 {
+ output.Failf(output.CodeInputError, "missing step reference")
+ }
+ data.Step.Ref = os.Args[1]
+
+ now := time.Now()
+
+ // Load shared state
+ data.LoadState()
+
+ // Initialize space for parsing args
+ config := map[string]string{}
+ computed := []string(nil)
+ conditions := []data.Rule(nil)
+ resulting := []data.Rule(nil)
+ timeouts := []data.Timeout(nil)
+ args := []string(nil)
+
+ // Read arguments into the base data
+ for i := 2; i < len(os.Args); i += 2 {
+ if i+1 == len(os.Args) {
+ break
+ }
+ switch os.Args[i] {
+ case constants.ArgSeparator:
+ args = os.Args[i+1:]
+ i = len(os.Args)
+ case constants.ArgInit, constants.ArgInitLong:
+ data.Step.InitStatus = os.Args[i+1]
+ case constants.ArgCondition, constants.ArgConditionLong:
+ v := strings.SplitN(os.Args[i+1], "=", 2)
+ refs := strings.Split(v[0], ",")
+ if len(v) == 2 {
+ conditions = append(conditions, data.Rule{Expr: v[1], Refs: refs})
+ } else {
+ conditions = append(conditions, data.Rule{Expr: "true", Refs: refs})
+ }
+ case constants.ArgResult, constants.ArgResultLong:
+ v := strings.SplitN(os.Args[i+1], "=", 2)
+ refs := strings.Split(v[0], ",")
+ if len(v) == 2 {
+ resulting = append(resulting, data.Rule{Expr: v[1], Refs: refs})
+ } else {
+ resulting = append(resulting, data.Rule{Expr: "true", Refs: refs})
+ }
+ case constants.ArgTimeout, constants.ArgTimeoutLong:
+ v := strings.SplitN(os.Args[i+1], "=", 2)
+ if len(v) == 2 {
+ timeouts = append(timeouts, data.Timeout{Ref: v[0], Duration: v[1]})
+ } else {
+ timeouts = append(timeouts, data.Timeout{Ref: v[0], Duration: ""})
+ }
+ case constants.ArgComputeEnv, constants.ArgComputeEnvLong:
+ computed = append(computed, strings.Split(os.Args[i+1], ",")...)
+ case constants.ArgNegative, constants.ArgNegativeLong:
+ config["negative"] = os.Args[i+1]
+ case constants.ArgRetryCount:
+ config["retryCount"] = os.Args[i+1]
+ case constants.ArgRetryUntil:
+ config["retryUntil"] = os.Args[i+1]
+ case constants.ArgDebug:
+ config["debug"] = os.Args[i+1]
+ default:
+ output.Failf(output.CodeInputError, "unknown parameter: %s", os.Args[i])
+ }
+ }
+
+ // Compute environment variables
+ for _, name := range computed {
+ initial := os.Getenv(name)
+ value, err := data.Template(initial)
+ if err != nil {
+ output.Failf(output.CodeInputError, `resolving "%s" environment variable: %s: %s`, name, initial, err.Error())
+ }
+ _ = os.Setenv(name, value)
+ }
+
+ // Compute conditional steps - ignore errors initially, as the may be dependent on themselves
+ data.Iterate(conditions, func(c data.Rule) bool {
+ expr, err := data.Expression(c.Expr)
+ if err != nil {
+ return false
+ }
+ v, _ := expr.BoolValue()
+ if !v {
+ for _, r := range c.Refs {
+ data.State.GetStep(r).Skip(now)
+ }
+ }
+ return true
+ })
+
+ // Fail invalid conditional steps
+ for _, c := range conditions {
+ _, err := data.Expression(c.Expr)
+ if err != nil {
+ output.Failf(output.CodeInputError, "broken condition for refs: %s: %s: %s", strings.Join(c.Refs, ", "), c.Expr, err.Error())
+ }
+ }
+
+ // Start all acknowledged steps
+ for _, f := range resulting {
+ for _, r := range f.Refs {
+ if r != "" {
+ data.State.GetStep(r).Start(now)
+ }
+ }
+ }
+ for _, t := range timeouts {
+ if t.Ref != "" {
+ data.State.GetStep(t.Ref).Start(now)
+ }
+ }
+ data.State.GetStep(data.Step.Ref).Start(now)
+
+ // Register timeouts
+ for _, t := range timeouts {
+ err := data.State.GetStep(t.Ref).SetTimeoutDuration(now, t.Duration)
+ if err != nil {
+ output.Failf(output.CodeInputError, "broken timeout for ref: %s: %s: %s", t.Ref, t.Duration, err.Error())
+ }
+ }
+
+ // Save the resulting conditions
+ data.Config.Resulting = resulting
+
+ // Don't call further if the step is already skipped
+ if data.State.GetStep(data.Step.Ref).Status == data.StepStatusSkipped {
+ if data.Config.Debug {
+ fmt.Printf("Skipped.\n")
+ }
+ data.Finish()
+ }
+
+ // Load the rest of the configuration
+ for k, v := range config {
+ value, err := data.Template(v)
+ if err != nil {
+ output.Failf(output.CodeInputError, `resolving "%s" param: %s: %s`, k, v, err.Error())
+ }
+ data.LoadConfig(map[string]string{k: value})
+ }
+
+ // Compute templates in the cmd/args
+ original := slices.Clone(args)
+ var err error
+ for i := range args {
+ args[i], err = data.Template(args[i])
+ if err != nil {
+ output.Failf(output.CodeInputError, `resolving command: %s: %s`, shellquote.Join(original...), err.Error())
+ }
+ }
+
+ // Fail when there is nothing to run
+ if len(args) == 0 {
+ output.Failf(output.CodeNoCommand, "missing command to run")
+ }
+
+ // Handle aborting
+ stopSignal := make(chan os.Signal, 1)
+ signal.Notify(stopSignal, syscall.SIGINT, syscall.SIGTERM)
+ go func() {
+ <-stopSignal
+ fmt.Println("The task was aborted.")
+ data.Step.Status = data.StepStatusAborted
+ data.Step.ExitCode = output.CodeAborted
+ data.Finish()
+ }()
+
+ // Handle timeouts
+ for _, t := range timeouts {
+ go func(ref string) {
+ time.Sleep(data.State.GetStep(ref).TimeoutAt.Sub(time.Now()))
+ fmt.Printf("Timed out.\n")
+ data.State.GetStep(ref).SetStatus(data.StepStatusTimeout)
+ data.Step.Status = data.StepStatusTimeout
+ data.Step.ExitCode = output.CodeTimeout
+ data.Finish()
+ }(t.Ref)
+ }
+
+ // Start the task
+ data.Step.Executed = true
+ run.Run(args[0], args[1:])
+
+ os.Exit(0)
+}
diff --git a/cmd/tcl/testworkflow-init/output/constants.go b/cmd/tcl/testworkflow-init/output/constants.go
new file mode 100644
index 00000000000..ef95dbdc271
--- /dev/null
+++ b/cmd/tcl/testworkflow-init/output/constants.go
@@ -0,0 +1,16 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package output
+
+const (
+ CodeTimeout uint8 = 124
+ CodeAborted uint8 = 137
+ CodeInputError uint8 = 155
+ CodeNoCommand uint8 = 189
+)
diff --git a/cmd/tcl/testworkflow-init/output/output.go b/cmd/tcl/testworkflow-init/output/output.go
new file mode 100644
index 00000000000..8c2e911e331
--- /dev/null
+++ b/cmd/tcl/testworkflow-init/output/output.go
@@ -0,0 +1,29 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package output
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-init/data"
+)
+
+func Failf(exitCode uint8, message string, args ...interface{}) {
+ // Print message
+ fmt.Printf(message+"\n", args...)
+
+ // Kill the sub-process
+ if data.Step.Cmd != nil && data.Step.Cmd.Process != nil {
+ _ = data.Step.Cmd.Process.Kill()
+ }
+
+ // Exit
+ os.Exit(int(exitCode))
+}
diff --git a/cmd/tcl/testworkflow-init/run/run.go b/cmd/tcl/testworkflow-init/run/run.go
new file mode 100644
index 00000000000..9a743b6e051
--- /dev/null
+++ b/cmd/tcl/testworkflow-init/run/run.go
@@ -0,0 +1,89 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package run
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-init/data"
+)
+
+func getProcessStatus(err error) (bool, uint8) {
+ if err == nil {
+ return true, 0
+ }
+ if e, ok := err.(*exec.ExitError); ok {
+ if e.ProcessState != nil {
+ return false, uint8(e.ProcessState.ExitCode())
+ }
+ return false, 1
+ }
+ fmt.Println(err.Error())
+ return false, 1
+}
+
+// TODO: Obfuscate Stdout/Stderr streams
+func createCommand(cmd string, args ...string) (c *exec.Cmd) {
+ c = exec.Command(cmd, args...)
+ c.Stdout = os.Stdout
+ c.Stderr = os.Stderr
+ c.Stdin = os.Stdin
+ return
+}
+
+func execute(cmd string, args ...string) {
+ data.Step.Cmd = createCommand(cmd, args...)
+ success, exitCode := getProcessStatus(data.Step.Cmd.Run())
+ data.Step.ExitCode = exitCode
+
+ actualSuccess := success
+ if data.Config.Negative {
+ actualSuccess = !success
+ }
+
+ if actualSuccess {
+ data.Step.Status = data.StepStatusPassed
+ } else {
+ data.Step.Status = data.StepStatusFailed
+ }
+
+ if data.Config.Negative {
+ fmt.Printf("Expected to fail: finished with exit code %d.\n", exitCode)
+ } else if data.Config.Debug {
+ fmt.Printf("Exit code: %d.\n", exitCode)
+ }
+}
+
+func Run(cmd string, args []string) {
+ // Instantiate the command and run
+ execute(cmd, args...)
+
+ // Retry if it's expected
+ // TODO: Support nested retries
+ step := data.State.GetStep(data.Step.Ref)
+ for step.Iteration <= uint64(data.Config.RetryCount) {
+ expr, err := data.Expression(data.Config.RetryUntil, data.LocalMachine)
+ if err != nil {
+ fmt.Printf("Failed to execute retry condition: %s: %s\n", data.Config.RetryUntil, err.Error())
+ data.Finish()
+ }
+ v, _ := expr.BoolValue()
+ if v {
+ break
+ }
+ step.Next()
+ fmt.Printf("\nExit code: %d • Retrying: attempt #%d (of %d):\n", data.Step.ExitCode, step.Iteration-1, data.Config.RetryCount)
+ execute(cmd, args...)
+ }
+
+ // Finish
+ data.Finish()
+}
diff --git a/cmd/tcl/testworkflow-toolkit/artifacts/cloud_uploader.go b/cmd/tcl/testworkflow-toolkit/artifacts/cloud_uploader.go
new file mode 100644
index 00000000000..657f3689d4d
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/artifacts/cloud_uploader.go
@@ -0,0 +1,173 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package artifacts
+
+import (
+ "bytes"
+ "context"
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/pkg/errors"
+
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-toolkit/env"
+ "github.com/kubeshop/testkube/pkg/cloud/data/artifact"
+ cloudexecutor "github.com/kubeshop/testkube/pkg/cloud/data/executor"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+type CloudUploaderRequestEnhancer = func(req *http.Request, path string, size int64)
+
+func NewCloudUploader(opts ...CloudUploaderOpt) Uploader {
+ uploader := &cloudUploader{
+ parallelism: 1,
+ reqEnhancers: make([]CloudUploaderRequestEnhancer, 0),
+ }
+ for _, opt := range opts {
+ opt(uploader)
+ }
+ return uploader
+}
+
+type cloudUploader struct {
+ client cloudexecutor.Executor
+ wg sync.WaitGroup
+ sema chan struct{}
+ parallelism int
+ error atomic.Bool
+ reqEnhancers []CloudUploaderRequestEnhancer
+}
+
+func (d *cloudUploader) Start() (err error) {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ d.client = env.Cloud(ctx)
+ d.sema = make(chan struct{}, d.parallelism)
+ return err
+}
+
+func (d *cloudUploader) getSignedURL(name, contentType string) (string, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ response, err := d.client.Execute(ctx, artifact.CmdScraperPutObjectSignedURL, &artifact.PutObjectSignedURLRequest{
+ Object: name,
+ ExecutionID: env.ExecutionId(),
+ TestWorkflowName: env.WorkflowName(),
+ ContentType: contentType,
+ })
+ if err != nil {
+ return "", err
+ }
+ var commandResponse artifact.PutObjectSignedURLResponse
+ if err := json.Unmarshal(response, &commandResponse); err != nil {
+ return "", err
+ }
+ return commandResponse.URL, nil
+}
+
+func (d *cloudUploader) getContentType(path string, size int64) string {
+ req, err := http.NewRequestWithContext(context.Background(), http.MethodPut, "/", &bytes.Buffer{})
+ if err != nil {
+ return ""
+ }
+ for _, r := range d.reqEnhancers {
+ r(req, path, size)
+ }
+ contentType := req.Header.Get("Content-Type")
+ if contentType == "" {
+ return "application/octet-stream"
+ }
+ return contentType
+}
+
+func (d *cloudUploader) putObject(url string, path string, file io.Reader, size int64) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
+ defer cancel()
+ req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, file)
+ if err != nil {
+ return err
+ }
+ for _, r := range d.reqEnhancers {
+ r(req, path, size)
+ }
+ req.ContentLength = size
+ if req.Header.Get("Content-Type") == "" {
+ req.Header.Set("Content-Type", "application/octet-stream")
+ }
+ tr := http.DefaultTransport.(*http.Transport).Clone()
+ tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+ client := &http.Client{Transport: tr}
+ res, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ if res.StatusCode != http.StatusOK {
+ b, _ := io.ReadAll(res.Body)
+ return errors.Errorf("failed saving file: status code: %d / message: %s", res.StatusCode, string(b))
+ }
+ return nil
+}
+
+func (d *cloudUploader) upload(path string, file io.Reader, size int64) {
+ url, err := d.getSignedURL(path, d.getContentType(path, size))
+ if err != nil {
+ d.error.Store(true)
+ ui.Errf("%s: failed: get signed URL: %s", path, err.Error())
+ return
+ }
+ err = d.putObject(url, path, file, size)
+ if err != nil {
+ d.error.Store(true)
+ ui.Errf("%s: failed: store file: %s", path, err.Error())
+ return
+ }
+}
+
+func (d *cloudUploader) Add(path string, file io.ReadCloser, size int64) error {
+ d.wg.Add(1)
+ d.sema <- struct{}{}
+ go func() {
+ d.upload(path, file, size)
+ _ = file.Close()
+ d.wg.Done()
+ <-d.sema
+ }()
+ return nil
+}
+
+func (d *cloudUploader) End() error {
+ d.wg.Wait()
+ if d.error.Load() {
+ return fmt.Errorf("upload failed")
+ }
+ return nil
+}
+
+type CloudUploaderOpt = func(uploader *cloudUploader)
+
+func WithParallelismCloud(parallelism int) CloudUploaderOpt {
+ return func(uploader *cloudUploader) {
+ if parallelism < 1 {
+ parallelism = 1
+ }
+ uploader.parallelism = parallelism
+ }
+}
+
+func WithRequestEnhancerCloud(enhancer CloudUploaderRequestEnhancer) CloudUploaderOpt {
+ return func(uploader *cloudUploader) {
+ uploader.reqEnhancers = append(uploader.reqEnhancers, enhancer)
+ }
+}
diff --git a/cmd/tcl/testworkflow-toolkit/artifacts/direct_processor.go b/cmd/tcl/testworkflow-toolkit/artifacts/direct_processor.go
new file mode 100644
index 00000000000..f9d76a309bc
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/artifacts/direct_processor.go
@@ -0,0 +1,32 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package artifacts
+
+import (
+ "io/fs"
+)
+
+func NewDirectProcessor() Processor {
+ return &directProcessor{}
+}
+
+type directProcessor struct {
+}
+
+func (d *directProcessor) Start() error {
+ return nil
+}
+
+func (d *directProcessor) Add(uploader Uploader, path string, file fs.File, stat fs.FileInfo) error {
+ return uploader.Add(path, file, stat.Size())
+}
+
+func (d *directProcessor) End() error {
+ return nil
+}
diff --git a/cmd/tcl/testworkflow-toolkit/artifacts/direct_uploader.go b/cmd/tcl/testworkflow-toolkit/artifacts/direct_uploader.go
new file mode 100644
index 00000000000..2dc5cdc01df
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/artifacts/direct_uploader.go
@@ -0,0 +1,110 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package artifacts
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "sync"
+ "sync/atomic"
+
+ minio2 "github.com/minio/minio-go/v7"
+
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-toolkit/env"
+ "github.com/kubeshop/testkube/pkg/storage/minio"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+type PutObjectOptionsEnhancer = func(options *minio2.PutObjectOptions, path string, size int64)
+
+func NewDirectUploader(opts ...DirectUploaderOpt) Uploader {
+ uploader := &directUploader{
+ parallelism: 1,
+ options: make([]PutObjectOptionsEnhancer, 0),
+ }
+ for _, opt := range opts {
+ opt(uploader)
+ }
+ return uploader
+}
+
+type directUploader struct {
+ client *minio.Client
+ wg sync.WaitGroup
+ sema chan struct{}
+ parallelism int
+ error atomic.Bool
+ options []PutObjectOptionsEnhancer
+}
+
+func (d *directUploader) Start() (err error) {
+ d.client, err = env.ObjectStorageClient()
+ d.sema = make(chan struct{}, d.parallelism)
+ return err
+}
+
+func (d *directUploader) buildOptions(path string, size int64) (options minio2.PutObjectOptions) {
+ for _, enhance := range d.options {
+ enhance(&options, path, size)
+ }
+ if options.ContentType == "" {
+ options.ContentType = "application/octet-stream"
+ }
+ return options
+}
+
+func (d *directUploader) upload(path string, file io.ReadCloser, size int64) {
+ ns := env.ExecutionId()
+ opts := d.buildOptions(path, size)
+ err := d.client.SaveFileDirect(context.Background(), ns, path, file, size, opts)
+
+ if err != nil {
+ d.error.Store(true)
+ ui.Errf("%s: failed: %s", path, err.Error())
+ return
+ }
+}
+
+func (d *directUploader) Add(path string, file io.ReadCloser, size int64) error {
+ d.wg.Add(1)
+ d.sema <- struct{}{}
+ go func() {
+ d.upload(path, file, size)
+ _ = file.Close()
+ d.wg.Done()
+ <-d.sema
+ }()
+ return nil
+}
+
+func (d *directUploader) End() error {
+ d.wg.Wait()
+ if d.error.Load() {
+ return fmt.Errorf("upload failed")
+ }
+ return nil
+}
+
+type DirectUploaderOpt = func(uploader *directUploader)
+
+func WithParallelism(parallelism int) DirectUploaderOpt {
+ return func(uploader *directUploader) {
+ if parallelism < 1 {
+ parallelism = 1
+ }
+ uploader.parallelism = parallelism
+ }
+}
+
+func WithMinioOptionsEnhancer(fn PutObjectOptionsEnhancer) DirectUploaderOpt {
+ return func(uploader *directUploader) {
+ uploader.options = append(uploader.options, fn)
+ }
+}
diff --git a/cmd/tcl/testworkflow-toolkit/artifacts/handler.go b/cmd/tcl/testworkflow-toolkit/artifacts/handler.go
new file mode 100644
index 00000000000..a1708ef5be9
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/artifacts/handler.go
@@ -0,0 +1,92 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package artifacts
+
+import (
+ "fmt"
+ "io/fs"
+ "sync/atomic"
+
+ "github.com/dustin/go-humanize"
+
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+type handler struct {
+ uploader Uploader
+ processor Processor
+
+ success atomic.Uint32
+ errors atomic.Uint32
+ totalSize atomic.Uint64
+}
+
+type Handler interface {
+ Start() error
+ Add(path string, file fs.File, stat fs.FileInfo) error
+ End() error
+}
+
+func NewHandler(uploader Uploader, processor Processor) Handler {
+ return &handler{
+ uploader: uploader,
+ processor: processor,
+ }
+}
+
+func (h *handler) Start() (err error) {
+ err = h.processor.Start()
+ if err != nil {
+ return err
+ }
+ return h.uploader.Start()
+}
+
+func (h *handler) Add(path string, file fs.File, stat fs.FileInfo) (err error) {
+ size := uint64(stat.Size())
+ h.totalSize.Add(size)
+
+ fmt.Printf(ui.LightGray("%s (%s)\n"), path, humanize.Bytes(uint64(stat.Size())))
+
+ err = h.processor.Add(h.uploader, path, file, stat)
+ if err == nil {
+ h.success.Add(1)
+ } else {
+ h.errors.Add(1)
+ fmt.Printf(ui.Red("%s: failed: %s"), path, err.Error())
+ }
+ return err
+}
+
+func (h *handler) End() (err error) {
+ fmt.Printf("\n")
+
+ err = h.processor.End()
+ if err != nil {
+ go h.uploader.End()
+ return err
+ }
+ err = h.uploader.End()
+ if err != nil {
+ return err
+ }
+
+ errs := h.errors.Load()
+ success := h.success.Load()
+ totalSize := h.totalSize.Load()
+ if errs == 0 && success == 0 {
+ fmt.Printf("No artifacts found.\n")
+ } else {
+ fmt.Printf("Found and uploaded %s files (%s).\n", ui.LightCyan(success), ui.LightCyan(humanize.Bytes(totalSize)))
+ }
+ if errs > 0 {
+ return fmt.Errorf(" %d problems while uploading files", errs)
+ }
+ return nil
+}
diff --git a/cmd/tcl/testworkflow-toolkit/artifacts/mimetype.go b/cmd/tcl/testworkflow-toolkit/artifacts/mimetype.go
new file mode 100644
index 00000000000..15db78e55fd
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/artifacts/mimetype.go
@@ -0,0 +1,29 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package artifacts
+
+import (
+ "path/filepath"
+
+ "github.com/h2non/filetype"
+)
+
+func DetectMimetype(filePath string) string {
+ ext := filepath.Ext(filePath)
+
+ // Remove the dot from the file extension
+ if len(ext) > 0 && ext[0] == '.' {
+ ext = ext[1:]
+ }
+ t := filetype.GetType(ext)
+ if t == filetype.Unknown {
+ return ""
+ }
+ return t.MIME.Value
+}
diff --git a/cmd/tcl/testworkflow-toolkit/artifacts/processor.go b/cmd/tcl/testworkflow-toolkit/artifacts/processor.go
new file mode 100644
index 00000000000..f4a284b7194
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/artifacts/processor.go
@@ -0,0 +1,17 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package artifacts
+
+import "io/fs"
+
+type Processor interface {
+ Start() error
+ Add(uploader Uploader, path string, file fs.File, stat fs.FileInfo) error
+ End() error
+}
diff --git a/cmd/tcl/testworkflow-toolkit/artifacts/tar_processor.go b/cmd/tcl/testworkflow-toolkit/artifacts/tar_processor.go
new file mode 100644
index 00000000000..00c83d79e5f
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/artifacts/tar_processor.go
@@ -0,0 +1,78 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package artifacts
+
+import (
+ "fmt"
+ "io/fs"
+ "sync"
+
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func NewTarProcessor(name string) Processor {
+ return &tarProcessor{
+ name: name,
+ }
+}
+
+type tarProcessor struct {
+ name string
+ mu *sync.Mutex
+ errCh chan error
+ ts *tarStream
+}
+
+func (d *tarProcessor) Start() (err error) {
+ d.errCh = make(chan error)
+ d.mu = &sync.Mutex{}
+
+ return err
+}
+
+func (d *tarProcessor) init(uploader Uploader) {
+ if d.ts != nil {
+ return
+ }
+ d.ts = NewTarStream()
+
+ // Start uploading the file
+ go func() {
+ err := uploader.Add(d.name, d.ts, -1)
+ if err != nil {
+ _ = d.ts.Close()
+ }
+ d.errCh <- err
+ }()
+}
+
+func (d *tarProcessor) upload(path string, file fs.File, stat fs.FileInfo) error {
+ defer file.Close()
+ return d.ts.Add(path, file, stat)
+}
+
+func (d *tarProcessor) Add(uploader Uploader, path string, file fs.File, stat fs.FileInfo) error {
+ d.mu.Lock()
+ d.init(uploader)
+ defer d.mu.Unlock()
+ return d.upload(path, file, stat)
+}
+
+func (d *tarProcessor) End() (err error) {
+ if d.ts != nil {
+ <-d.ts.Done()
+ }
+ err = d.ts.Close()
+ if err != nil {
+ return fmt.Errorf("problem closing writer: %w", err)
+ }
+
+ fmt.Printf("Archived everything in %s archive.\n", ui.LightCyan(d.name))
+ return <-d.errCh
+}
diff --git a/cmd/tcl/testworkflow-toolkit/artifacts/tar_stream.go b/cmd/tcl/testworkflow-toolkit/artifacts/tar_stream.go
new file mode 100644
index 00000000000..b7c228d3b22
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/artifacts/tar_stream.go
@@ -0,0 +1,93 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package artifacts
+
+import (
+ "archive/tar"
+ "compress/gzip"
+ "fmt"
+ "io"
+ "io/fs"
+ "sync"
+)
+
+type tarStream struct {
+ reader io.ReadCloser
+ writer io.WriteCloser
+ gzip io.WriteCloser
+ tar *tar.Writer
+ mu *sync.Mutex
+ wg sync.WaitGroup
+}
+
+func NewTarStream() *tarStream {
+ reader, writer := io.Pipe()
+ gzip := gzip.NewWriter(writer)
+ tar := tar.NewWriter(gzip)
+ return &tarStream{
+ reader: reader,
+ writer: writer,
+ gzip: gzip,
+ tar: tar,
+ mu: &sync.Mutex{},
+ }
+}
+
+func (t *tarStream) Add(path string, file fs.File, stat fs.FileInfo) error {
+ t.wg.Add(1)
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ defer t.wg.Done()
+
+ // Write file header
+ name := stat.Name()
+ header, err := tar.FileInfoHeader(stat, name)
+ if err != nil {
+ return err
+ }
+ header.Name = path
+ err = t.tar.WriteHeader(header)
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(t.tar, file)
+ return err
+}
+
+func (t *tarStream) Read(p []byte) (n int, err error) {
+ return t.reader.Read(p)
+}
+
+func (t *tarStream) Done() chan struct{} {
+ ch := make(chan struct{})
+ go func() {
+ t.wg.Wait()
+ close(ch)
+ }()
+ return ch
+}
+
+func (t *tarStream) Close() (err error) {
+ err = t.tar.Close()
+ if err != nil {
+ _ = t.gzip.Close()
+ _ = t.writer.Close()
+ return fmt.Errorf("closing tar: tar: %v", err)
+ }
+ err = t.gzip.Close()
+ if err != nil {
+ _ = t.writer.Close()
+ return fmt.Errorf("closing tar: gzip: %v", err)
+ }
+ err = t.writer.Close()
+ if err != nil {
+ return fmt.Errorf("closing tar: pipe: %v", err)
+ }
+ return nil
+}
diff --git a/cmd/tcl/testworkflow-toolkit/artifacts/tarcached_processor.go b/cmd/tcl/testworkflow-toolkit/artifacts/tarcached_processor.go
new file mode 100644
index 00000000000..cc93e981230
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/artifacts/tarcached_processor.go
@@ -0,0 +1,111 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package artifacts
+
+import (
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "sync"
+
+ "github.com/dustin/go-humanize"
+
+ "github.com/kubeshop/testkube/pkg/tmp"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func NewTarCachedProcessor(name string, cachePath string) Processor {
+ if cachePath == "" {
+ cachePath = tmp.Name()
+ }
+ return &tarCachedProcessor{
+ name: name,
+ cachePath: cachePath,
+ }
+}
+
+type tarCachedProcessor struct {
+ uploader Uploader
+ name string
+ cachePath string
+ mu *sync.Mutex
+ errCh chan error
+ file *os.File
+ ts *tarStream
+}
+
+func (d *tarCachedProcessor) Start() (err error) {
+ d.errCh = make(chan error)
+ d.mu = &sync.Mutex{}
+ d.file, err = os.Create(d.cachePath)
+
+ return err
+}
+
+func (d *tarCachedProcessor) init(uploader Uploader) {
+ if d.ts != nil {
+ return
+ }
+ d.ts = NewTarStream()
+ d.uploader = uploader
+ go func() {
+ _, err := io.Copy(d.file, d.ts)
+ d.errCh <- err
+ }()
+}
+
+func (d *tarCachedProcessor) clean() {
+ _ = os.Remove(d.cachePath)
+}
+
+func (d *tarCachedProcessor) upload(path string, file fs.File, stat fs.FileInfo) error {
+ defer file.Close()
+ return d.ts.Add(path, file, stat)
+}
+
+func (d *tarCachedProcessor) Add(uploader Uploader, path string, file fs.File, stat fs.FileInfo) error {
+ d.mu.Lock()
+ d.init(uploader)
+ defer d.mu.Unlock()
+ return d.upload(path, file, stat)
+}
+
+func (d *tarCachedProcessor) End() (err error) {
+ defer d.clean()
+
+ if d.ts != nil {
+ <-d.ts.Done()
+ }
+ err = d.ts.Close()
+ if err != nil {
+ return fmt.Errorf("problem closing writer: %w", err)
+ }
+ err = <-d.errCh
+ if err != nil {
+ return fmt.Errorf("problem writing to disk cache: %w", err)
+ }
+
+ if d.uploader == nil {
+ return nil
+ }
+
+ file, err := os.Open(d.cachePath)
+ if err != nil {
+ return fmt.Errorf("problem reading disk cache: %w", err)
+ }
+
+ stat, err := file.Stat()
+ if err != nil {
+ return fmt.Errorf("problem reading disk cache: stat: %w", err)
+ }
+
+ fmt.Printf("Archived everything in %s archive (%s).\n", ui.LightCyan(d.name), ui.LightCyan(humanize.Bytes(uint64(stat.Size()))))
+ return d.uploader.Add(d.name, file, stat.Size())
+}
diff --git a/cmd/tcl/testworkflow-toolkit/artifacts/uploader.go b/cmd/tcl/testworkflow-toolkit/artifacts/uploader.go
new file mode 100644
index 00000000000..d164ed89509
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/artifacts/uploader.go
@@ -0,0 +1,19 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package artifacts
+
+import (
+ "io"
+)
+
+type Uploader interface {
+ Start() error
+ Add(path string, file io.ReadCloser, size int64) error
+ End() error
+}
diff --git a/cmd/tcl/testworkflow-toolkit/artifacts/walker.go b/cmd/tcl/testworkflow-toolkit/artifacts/walker.go
new file mode 100644
index 00000000000..8310b085cf8
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/artifacts/walker.go
@@ -0,0 +1,221 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package artifacts
+
+import (
+ "fmt"
+ "io/fs"
+ "path/filepath"
+ "strings"
+
+ "github.com/bmatcuk/doublestar/v4"
+)
+
+func mapSlice[T any, U any](s []T, fn func(T) U) []U {
+ result := make([]U, len(s))
+ for i := range s {
+ result[i] = fn(s[i])
+ }
+ return result
+}
+
+func deduplicateRoots(paths []string) []string {
+ result := make([]string, 0)
+loop:
+ for _, path := range paths {
+ for _, path2 := range paths {
+ if strings.HasPrefix(path, path2+"/") {
+ continue loop
+ }
+ }
+ result = append(result, path)
+ }
+ return result
+}
+
+func findSearchRoot(pattern string) string {
+ path, _ := doublestar.SplitPattern(pattern + "/")
+ return strings.TrimRight(path, "/")
+}
+
+// TODO: Support wildcards better:
+// - /**/*.json is a part of /data
+// - /data/s*me/*a*/abc.json is a part of /data/some/path/
+func isPatternIn(pattern string, dirs []string) bool {
+ return isPathIn(findSearchRoot(pattern), dirs)
+}
+
+func isPathIn(path string, dirs []string) bool {
+ for _, dir := range dirs {
+ path = strings.TrimRight(path, "/")
+ if dir == path || strings.HasPrefix(path, dir+"/") {
+ return true
+ }
+ }
+ return false
+}
+
+func sanitizePath(path string) (string, error) {
+ path, err := filepath.Abs(path)
+ path = strings.TrimRight(filepath.ToSlash(path), "/")
+ if path == "" {
+ path = "/"
+ }
+ return path, err
+}
+
+func sanitizePaths(input []string) ([]string, error) {
+ paths := make([]string, len(input))
+ for i := range input {
+ var err error
+ paths[i], err = sanitizePath(input[i])
+ if err != nil {
+ return nil, fmt.Errorf("error while resolving path: %s: %w", input[i], err)
+ }
+ }
+ return paths, nil
+}
+
+func filterPatterns(patterns, dirs []string) []string {
+ result := make([]string, 0)
+ for _, p := range patterns {
+ if isPatternIn(p, dirs) {
+ result = append(result, p)
+ }
+ }
+ return result
+}
+
+func detectCommonPath(path1, path2 string) string {
+ if path1 == path2 {
+ return path1
+ }
+ common := 0
+ parts1 := strings.Split(path1, "/")
+ parts2 := strings.Split(path2, "/")
+ for i := 0; i < len(parts1) && i < len(parts2); i++ {
+ if parts1[i] != parts2[i] {
+ break
+ }
+ common++
+ }
+ if common == 1 && parts1[0] == "" {
+ return "/"
+ }
+ return strings.Join(parts1[0:common], "/")
+}
+
+func detectRoot(potential string, paths []string) string {
+ potential = strings.TrimRight(potential, "/")
+ if potential == "" {
+ potential = "/"
+ }
+ for _, path := range paths {
+ potential = detectCommonPath(potential, path)
+ }
+ return potential
+}
+
+func CreateWalker(patterns, roots []string, root string) (Walker, error) {
+ var err error
+
+ // Build absolute paths
+ if patterns, err = sanitizePaths(patterns); err != nil {
+ return nil, err
+ }
+ if roots, err = sanitizePaths(roots); err != nil {
+ return nil, err
+ }
+ if root, err = sanitizePath(root); err != nil {
+ return nil, err
+ }
+ // Include only if it is matching some mounted volumes
+ patterns = filterPatterns(patterns, roots)
+ // Detect top-level paths for searching
+ searchPaths := deduplicateRoots(mapSlice(patterns, findSearchRoot))
+ // Detect root path for the bucket
+ root = detectRoot(root, searchPaths)
+
+ return &walker{
+ root: root,
+ searchPaths: searchPaths,
+ patterns: patterns,
+ }, nil
+}
+
+type walker struct {
+ root string
+ searchPaths []string
+ patterns []string // TODO: Optimize to check only patterns matching specific searchPaths
+}
+
+type WalkerFn = func(path string, file fs.File, err error) error
+
+type Walker interface {
+ Root() string
+ SearchPaths() []string
+ Patterns() []string
+ Walk(fsys fs.FS, walker WalkerFn) error
+}
+
+func (w *walker) Root() string {
+ return w.root
+}
+
+func (w *walker) SearchPaths() []string {
+ return w.searchPaths
+}
+
+func (w *walker) Patterns() []string {
+ return w.patterns
+}
+
+func (w *walker) matches(filePath string) bool {
+ for _, p := range w.patterns {
+ v, _ := doublestar.PathMatch(p, filePath)
+ if v {
+ return true
+ }
+ }
+ return false
+}
+
+func (w *walker) walk(fsys fs.FS, path string, walker WalkerFn) error {
+ sanitizedPath := strings.TrimLeft(path, "/")
+ if sanitizedPath == "" {
+ sanitizedPath = "."
+ }
+
+ return fs.WalkDir(fsys, sanitizedPath, func(filePath string, d fs.DirEntry, err error) error {
+ resolvedPath := "/" + filepath.ToSlash(filePath)
+ if !w.matches(resolvedPath) {
+ return nil
+ }
+ if err != nil {
+ fmt.Printf("Warning: '%s' ignored from scraping: %v\n", resolvedPath, err)
+ return nil
+ }
+ if d.IsDir() {
+ return nil
+ }
+
+ file, err := fsys.Open(filePath)
+ return walker(strings.TrimLeft(resolvedPath[len(w.root):], "/"), file, err)
+ })
+}
+
+func (w *walker) Walk(fsys fs.FS, walker WalkerFn) (err error) {
+ for _, s := range w.searchPaths {
+ err = w.walk(fsys, s, walker)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/cmd/tcl/testworkflow-toolkit/commands/artifacts.go b/cmd/tcl/testworkflow-toolkit/commands/artifacts.go
new file mode 100644
index 00000000000..3dafc314129
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/commands/artifacts.go
@@ -0,0 +1,177 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package commands
+
+import (
+ "fmt"
+ "io/fs"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/minio/minio-go/v7"
+ "github.com/spf13/cobra"
+
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-toolkit/artifacts"
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-toolkit/env"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+var directAddGzipEncoding = artifacts.WithMinioOptionsEnhancer(func(options *minio.PutObjectOptions, path string, size int64) {
+ options.ContentType = "application/gzip"
+ options.ContentEncoding = "gzip"
+})
+
+var directDisableMultipart = artifacts.WithMinioOptionsEnhancer(func(options *minio.PutObjectOptions, path string, size int64) {
+ options.DisableMultipart = true
+})
+
+var directDetectMimetype = artifacts.WithMinioOptionsEnhancer(func(options *minio.PutObjectOptions, path string, size int64) {
+ if options.ContentType == "" {
+ options.ContentType = artifacts.DetectMimetype(path)
+ }
+})
+
+var directUnpack = artifacts.WithMinioOptionsEnhancer(func(options *minio.PutObjectOptions, path string, size int64) {
+ options.UserMetadata = map[string]string{
+ "X-Amz-Meta-Snowball-Auto-Extract": "true",
+ "X-Amz-Meta-Minio-Snowball-Prefix": env.WorkflowName() + "/" + env.ExecutionId(),
+ }
+})
+
+var cloudAddGzipEncoding = artifacts.WithRequestEnhancerCloud(func(req *http.Request, path string, size int64) {
+ req.Header.Set("Content-Type", "application/gzip")
+ req.Header.Set("Content-Encoding", "gzip")
+})
+
+var cloudUnpack = artifacts.WithRequestEnhancerCloud(func(req *http.Request, path string, size int64) {
+ req.Header.Set("X-Amz-Meta-Snowball-Auto-Extract", "true")
+})
+
+var cloudDetectMimetype = artifacts.WithRequestEnhancerCloud(func(req *http.Request, path string, size int64) {
+ if req.Header.Get("Content-Type") == "" {
+ contentType := artifacts.DetectMimetype(path)
+ if contentType != "" {
+ req.Header.Set("Content-Type", contentType)
+ }
+ if contentType == "application/gzip" && req.Header.Get("Content-Encoding") == "" {
+ req.Header.Set("Content-Encoding", "gzip")
+ }
+ }
+})
+
+func NewArtifactsCmd() *cobra.Command {
+ var (
+ mounts []string
+ id string
+ compress string
+ compressCachePath string
+ unpack bool
+ )
+
+ cmd := &cobra.Command{
+ Use: "artifacts ",
+ Short: "Save workflow artifacts",
+ Args: cobra.MinimumNArgs(1),
+
+ Run: func(cmd *cobra.Command, paths []string) {
+ root, _ := os.Getwd()
+ walker, err := artifacts.CreateWalker(paths, mounts, root)
+ ui.ExitOnError("building a walker", err)
+
+ if len(walker.Patterns()) == 0 || len(walker.SearchPaths()) == 0 {
+ ui.Failf("error: did not found any valid path pattern in the mounted directories")
+ }
+
+ fmt.Printf("Root: %s\nPatterns:\n", ui.LightCyan(walker.Root()))
+ for _, p := range walker.Patterns() {
+ fmt.Printf("- %s\n", ui.LightMagenta(p))
+ }
+ fmt.Printf("\n")
+
+ // Configure uploader
+ var processor artifacts.Processor
+ var uploader artifacts.Uploader
+
+ // Sanitize archive name
+ compress = strings.Trim(filepath.ToSlash(filepath.Clean(compress)), "/.")
+ if compress != "" {
+ compressLower := strings.ToLower(compress)
+ if strings.HasSuffix(compressLower, ".tar") {
+ compress += ".gz"
+ } else if !strings.HasSuffix(compressLower, ".tgz") && !strings.HasSuffix(compressLower, ".tar.gz") {
+ compress += ".tar.gz"
+ }
+ }
+
+ // Archive
+ if env.CloudEnabled() {
+ if compress != "" {
+ processor = artifacts.NewTarCachedProcessor(compress, compressCachePath)
+ opts := []artifacts.CloudUploaderOpt{cloudAddGzipEncoding}
+ if unpack {
+ opts = append(opts, cloudUnpack)
+ }
+ uploader = artifacts.NewCloudUploader(opts...)
+ } else {
+ processor = artifacts.NewDirectProcessor()
+ uploader = artifacts.NewCloudUploader(artifacts.WithParallelismCloud(30), cloudDetectMimetype)
+ }
+ } else if compress != "" && unpack {
+ processor = artifacts.NewTarCachedProcessor(compress, compressCachePath)
+ uploader = artifacts.NewDirectUploader(directAddGzipEncoding, directDisableMultipart, directUnpack)
+ } else if compress != "" && compressCachePath != "" {
+ processor = artifacts.NewTarCachedProcessor(compress, compressCachePath)
+ uploader = artifacts.NewDirectUploader(directAddGzipEncoding, directDisableMultipart)
+ } else if compress != "" {
+ processor = artifacts.NewTarProcessor(compress)
+ uploader = artifacts.NewDirectUploader(directAddGzipEncoding)
+ } else {
+ processor = artifacts.NewDirectProcessor()
+ uploader = artifacts.NewDirectUploader(artifacts.WithParallelism(30), directDetectMimetype)
+ }
+
+ handler := artifacts.NewHandler(uploader, processor)
+
+ err = handler.Start()
+ ui.ExitOnError("initializing uploader", err)
+
+ started := time.Now()
+ err = walker.Walk(os.DirFS("/"), func(path string, file fs.File, err error) error {
+ if err != nil {
+ fmt.Printf("Warning: '%s' has been ignored, as there was a problem reading it: %s\n", path, err.Error())
+ return nil
+ }
+
+ stat, err := file.Stat()
+ if err != nil {
+ fmt.Printf("Warning: '%s' has been ignored, as there was a problem reading it: %s\n", path, err.Error())
+ return nil
+ }
+ return handler.Add(path, file, stat)
+ })
+ ui.ExitOnError("reading the file system", err)
+ err = handler.End()
+
+ // TODO: Emit information about artifacts
+ ui.ExitOnError("finishing upload", err)
+ fmt.Printf("Took %s.\n", time.Now().Sub(started).Truncate(time.Millisecond))
+ },
+ }
+
+ cmd.Flags().StringSliceVarP(&mounts, "mount", "m", nil, "mounted volumes for limiting paths")
+ cmd.Flags().StringVar(&id, "id", "", "execution ID")
+ cmd.Flags().StringVar(&compress, "compress", "", "tgz name if should be compressed")
+ cmd.Flags().BoolVar(&unpack, "unpack", false, "minio only: unpack the file if compressed")
+ cmd.Flags().StringVar(&compressCachePath, "compress-cache", "", "local cache path for passing compressed archive through")
+
+ return cmd
+}
diff --git a/cmd/tcl/testworkflow-toolkit/commands/clone.go b/cmd/tcl/testworkflow-toolkit/commands/clone.go
new file mode 100644
index 00000000000..08b9df2f064
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/commands/clone.go
@@ -0,0 +1,103 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package commands
+
+import (
+ "fmt"
+ "net/url"
+ "os"
+ "path/filepath"
+
+ "github.com/spf13/cobra"
+
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func NewCloneCmd() *cobra.Command {
+ var (
+ paths []string
+ username string
+ token string
+ authType string
+ revision string
+ )
+
+ cmd := &cobra.Command{
+ Use: "clone ",
+ Short: "Clone the Git repository",
+ Args: cobra.ExactArgs(2),
+
+ Run: func(cmd *cobra.Command, args []string) {
+ uri, err := url.Parse(args[0])
+ ui.ExitOnError("repository uri", err)
+ outputPath, err := filepath.Abs(args[1])
+ ui.ExitOnError("output path", err)
+
+ // Disable interactivity
+ os.Setenv("GIT_TERMINAL_PROMPT", "0")
+
+ authArgs := make([]string, 0)
+
+ if authType == "header" {
+ ui.Debug("auth type: header")
+ if token != "" {
+ authArgs = append(authArgs, "-c", fmt.Sprintf("http.extraHeader='%s'", "Authorization: Bearer "+token))
+ }
+ if username != "" {
+ uri.User = url.User(username)
+ }
+ } else {
+ ui.Debug("auth type: token")
+ if username != "" && token != "" {
+ uri.User = url.UserPassword(username, token)
+ } else if username != "" {
+ uri.User = url.User(username)
+ } else if token != "" {
+ uri.User = url.User(token)
+ }
+ }
+
+ // Mark directory as safe
+ configArgs := []string{"-c", fmt.Sprintf("safe.directory=%s", outputPath), "-c", "advice.detachedHead=false"}
+
+ // Clone repository
+ if len(paths) == 0 {
+ ui.Debug("full checkout")
+ err = Run("git", "clone", configArgs, authArgs, "--depth", 1, "--verbose", uri.String(), outputPath)
+ ui.ExitOnError("cloning repository", err)
+ } else {
+ ui.Debug("sparse checkout")
+ err = Run("git", "clone", configArgs, authArgs, "--filter=blob:none", "--no-checkout", "--sparse", "--depth", 1, "--verbose", uri.String(), outputPath)
+ ui.ExitOnError("cloning repository", err)
+ err = Run("git", "-C", outputPath, configArgs, "sparse-checkout", "set", "--no-cone", paths)
+ ui.ExitOnError("sparse checkout repository", err)
+ if revision != "" {
+ err = Run("git", "-C", outputPath, configArgs, "fetch", authArgs, "--depth", 1, "origin", revision)
+ ui.ExitOnError("fetching revision", err)
+ err = Run("git", "-C", outputPath, configArgs, "checkout", "FETCH_HEAD")
+ ui.ExitOnError("checking out head", err)
+ // TODO: Don't do it for commits
+ err = Run("git", "-C", outputPath, configArgs, "checkout", "-B", revision)
+ ui.ExitOnError("checking out the branch", err)
+ } else {
+ err = Run("git", "-C", outputPath, configArgs, "checkout")
+ ui.ExitOnError("fetching head", err)
+ }
+ }
+ },
+ }
+
+ cmd.Flags().StringSliceVarP(&paths, "paths", "p", nil, "paths for sparse checkout")
+ cmd.Flags().StringVarP(&username, "username", "u", "", "")
+ cmd.Flags().StringVarP(&token, "token", "t", "", "")
+ cmd.Flags().StringVarP(&authType, "authType", "a", "basic", "allowed: basic, header")
+ cmd.Flags().StringVarP(&revision, "revision", "r", "", "commit hash, branch name or tag")
+
+ return cmd
+}
diff --git a/cmd/tcl/testworkflow-toolkit/commands/execute.go b/cmd/tcl/testworkflow-toolkit/commands/execute.go
new file mode 100644
index 00000000000..4d4c648c22f
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/commands/execute.go
@@ -0,0 +1,286 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package commands
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/pkg/errors"
+ "github.com/spf13/cobra"
+
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-init/data"
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-toolkit/env"
+ "github.com/kubeshop/testkube/pkg/api/v1/client"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+type testExecutionDetails struct {
+ Id string `json:"id"`
+ Name string `json:"name"`
+ TestName string `json:"testName"`
+}
+
+type testWorkflowExecutionDetails struct {
+ Id string `json:"id"`
+ Name string `json:"name"`
+ TestWorkflowName string `json:"testWorkflowName"`
+}
+
+type executionResult struct {
+ Id string `json:"id"`
+ Status string `json:"status"`
+}
+
+func buildTestExecution(test string, async bool) (func() error, error) {
+ name, req, _ := strings.Cut(test, "=")
+ request := testkube.ExecutionRequest{}
+ if req != "" {
+ err := json.Unmarshal([]byte(req), &request)
+ if err != nil {
+ return nil, errors.Wrap(err, fmt.Sprintf("failed to unmarshal execution request: %s: %s", name, req))
+ }
+ }
+ if request.ExecutionLabels == nil {
+ request.ExecutionLabels = map[string]string{}
+ }
+
+ return func() (err error) {
+ c := env.Testkube()
+
+ exec, err := c.ExecuteTest(name, request.Name, client.ExecuteTestOptions{
+ RunningContext: &testkube.RunningContext{
+ Type_: "testworkflow",
+ Context: fmt.Sprintf("%s/executions/%s", env.WorkflowName(), env.ExecutionId()),
+ },
+ IsVariablesFileUploaded: request.IsVariablesFileUploaded,
+ ExecutionLabels: request.ExecutionLabels,
+ Command: request.Command,
+ Args: request.Args,
+ ArgsMode: request.ArgsMode,
+ Envs: request.Envs,
+ SecretEnvs: request.SecretEnvs,
+ HTTPProxy: request.HttpProxy,
+ HTTPSProxy: request.HttpsProxy,
+ Image: request.Image,
+ Uploads: request.Uploads,
+ BucketName: request.BucketName,
+ ArtifactRequest: request.ArtifactRequest,
+ JobTemplate: request.JobTemplate,
+ JobTemplateReference: request.JobTemplateReference,
+ ContentRequest: request.ContentRequest,
+ PreRunScriptContent: request.PreRunScript,
+ PostRunScriptContent: request.PostRunScript,
+ ExecutePostRunScriptBeforeScraping: request.ExecutePostRunScriptBeforeScraping,
+ SourceScripts: request.SourceScripts,
+ ScraperTemplate: request.ScraperTemplate,
+ ScraperTemplateReference: request.ScraperTemplateReference,
+ PvcTemplate: request.PvcTemplate,
+ PvcTemplateReference: request.PvcTemplateReference,
+ NegativeTest: request.NegativeTest,
+ IsNegativeTestChangedOnRun: request.IsNegativeTestChangedOnRun,
+ EnvConfigMaps: request.EnvConfigMaps,
+ EnvSecrets: request.EnvSecrets,
+ SlavePodRequest: request.SlavePodRequest,
+ ExecutionNamespace: request.ExecutionNamespace,
+ })
+ execName := exec.Name
+ if err != nil {
+ ui.Errf("failed to execute test: %s: %s", name, err)
+ return
+ }
+
+ data.PrintOutput(env.Ref(), "test-start", &testExecutionDetails{
+ Id: exec.Id,
+ Name: exec.Name,
+ TestName: exec.TestName,
+ })
+ fmt.Printf("%s • scheduled %s\n", ui.LightCyan(execName), ui.DarkGray("("+exec.Id+")"))
+
+ if async {
+ return
+ }
+
+ loop:
+ for {
+ time.Sleep(time.Second)
+ exec, err = c.GetExecution(exec.Id)
+ if err != nil {
+ ui.Errf("error while getting execution result: %s: %s", ui.LightCyan(execName), err.Error())
+ return
+ }
+ if exec.ExecutionResult != nil && exec.ExecutionResult.Status != nil {
+ status := *exec.ExecutionResult.Status
+ switch status {
+ case testkube.QUEUED_ExecutionStatus, testkube.RUNNING_ExecutionStatus:
+ continue
+ default:
+ break loop
+ }
+ }
+ }
+
+ status := *exec.ExecutionResult.Status
+ color := ui.Green
+
+ if status != testkube.PASSED_ExecutionStatus {
+ err = errors.New("test failed")
+ color = ui.Red
+ }
+
+ data.PrintOutput(env.Ref(), "test-end", &executionResult{Id: exec.Id, Status: string(status)})
+ fmt.Printf("%s • %s\n", color(execName), string(status))
+ return
+ }, nil
+}
+
+func buildWorkflowExecution(workflow string, async bool) (func() error, error) {
+ name, req, _ := strings.Cut(workflow, "=")
+ request := testkube.TestWorkflowExecutionRequest{}
+ if req != "" {
+ err := json.Unmarshal([]byte(req), &request)
+ if err != nil {
+ return nil, errors.Wrap(err, fmt.Sprintf("failed to unmarshal execution request: %s: %s", name, req))
+ }
+ }
+
+ return func() (err error) {
+ c := env.Testkube()
+
+ exec, err := c.ExecuteTestWorkflow(name, request)
+ execName := exec.Name
+ if err != nil {
+ ui.Errf("failed to execute test workflow: %s: %s", name, err.Error())
+ return
+ }
+
+ data.PrintOutput(env.Ref(), "testworkflow-start", &testWorkflowExecutionDetails{
+ Id: exec.Id,
+ Name: exec.Name,
+ TestWorkflowName: exec.Workflow.Name,
+ })
+ fmt.Printf("%s • scheduled %s\n", ui.LightCyan(execName), ui.DarkGray("("+exec.Id+")"))
+
+ if async {
+ return
+ }
+
+ loop:
+ for {
+ time.Sleep(100 * time.Millisecond)
+ exec, err = c.GetTestWorkflowExecution(exec.Id)
+ if err != nil {
+ ui.Errf("error while getting execution result: %s: %s", ui.LightCyan(execName), err.Error())
+ return
+ }
+ if exec.Result != nil && exec.Result.Status != nil {
+ status := *exec.Result.Status
+ switch status {
+ case testkube.QUEUED_TestWorkflowStatus, testkube.RUNNING_TestWorkflowStatus:
+ continue
+ default:
+ break loop
+ }
+ }
+ }
+
+ status := *exec.Result.Status
+ color := ui.Green
+
+ if status != testkube.PASSED_TestWorkflowStatus {
+ err = errors.New("test workflow failed")
+ color = ui.Red
+ }
+
+ data.PrintOutput(env.Ref(), "testworkflow-end", &executionResult{Id: exec.Id, Status: string(status)})
+ fmt.Printf("%s • %s\n", color(execName), string(status))
+ return
+ }, nil
+}
+
+func NewExecuteCmd() *cobra.Command {
+ var (
+ tests []string
+ workflows []string
+ parallelism int
+ async bool
+ )
+
+ cmd := &cobra.Command{
+ Use: "execute",
+ Short: "Execute other resources",
+ Args: cobra.ExactArgs(0),
+
+ Run: func(cmd *cobra.Command, _ []string) {
+ // Calculate parallelism
+ if parallelism <= 0 {
+ parallelism = 20
+ }
+
+ // Build operations to run
+ operations := make([]func() error, 0)
+ for _, t := range tests {
+ fn, err := buildTestExecution(t, async)
+ if err != nil {
+ ui.Fail(err)
+ }
+ operations = append(operations, fn)
+ }
+ for _, w := range workflows {
+ fn, err := buildWorkflowExecution(w, async)
+ if err != nil {
+ ui.Fail(err)
+ }
+ operations = append(operations, fn)
+ }
+
+ // Validate if there is anything to run
+ if len(operations) == 0 {
+ fmt.Printf("nothing to run\n")
+ os.Exit(0)
+ }
+
+ // Create channel for execution
+ var wg sync.WaitGroup
+ wg.Add(len(operations))
+ ch := make(chan struct{}, parallelism)
+ success := true
+
+ // Execute all operations
+ for _, op := range operations {
+ ch <- struct{}{}
+ go func(op func() error) {
+ if op() != nil {
+ success = false
+ }
+ <-ch
+ wg.Done()
+ }(op)
+ }
+ wg.Wait()
+
+ if !success {
+ os.Exit(1)
+ }
+ },
+ }
+
+ // TODO: Support test suites too
+ cmd.Flags().StringArrayVarP(&tests, "test", "t", nil, "tests to run; either test name, or test-name=json-execution-request")
+ cmd.Flags().StringArrayVarP(&workflows, "workflow", "w", nil, "workflows to run; either workflow name, or workflow-name=json-execution-request")
+ cmd.Flags().IntVarP(¶llelism, "parallelism", "p", 0, "how many items could be executed at once")
+ cmd.Flags().BoolVar(&async, "async", false, "should it wait for results")
+
+ return cmd
+}
diff --git a/cmd/tcl/testworkflow-toolkit/commands/root.go b/cmd/tcl/testworkflow-toolkit/commands/root.go
new file mode 100644
index 00000000000..52036cc4994
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/commands/root.go
@@ -0,0 +1,71 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package commands
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/pkg/errors"
+ "github.com/spf13/cobra"
+
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-toolkit/env"
+
+ "golang.org/x/sync/errgroup"
+)
+
+func init() {
+ RootCmd.AddCommand(NewCloneCmd())
+ RootCmd.AddCommand(NewExecuteCmd())
+ RootCmd.AddCommand(NewArtifactsCmd())
+}
+
+var RootCmd = &cobra.Command{
+ Use: "testworkflow-toolkit",
+ Short: "Orchestrating Testkube workflows",
+ CompletionOptions: cobra.CompletionOptions{
+ DisableDefaultCmd: true,
+ DisableNoDescFlag: true,
+ DisableDescriptions: true,
+ HiddenDefaultCmd: true,
+ },
+}
+
+func Execute() {
+ // Run services within an errgroup to propagate errors between services.
+ g, ctx := errgroup.WithContext(context.Background())
+
+ // Cancel the errgroup context on SIGINT and SIGTERM,
+ // which shuts everything down gracefully.
+ // Kill on recurring signal.
+ stopSignal := make(chan os.Signal, 1)
+ signal.Notify(stopSignal, syscall.SIGINT, syscall.SIGTERM)
+ g.Go(func() error {
+ select {
+ case <-ctx.Done():
+ return nil
+ case sig := <-stopSignal:
+ go func() {
+ <-stopSignal
+ os.Exit(137)
+ }()
+ return errors.Errorf("received signal: %v", sig)
+ }
+ })
+
+ RootCmd.PersistentFlags().BoolVar(&env.UseProxyValue, "proxy", false, "use Kubernetes proxy for TK access")
+
+ if err := RootCmd.ExecuteContext(ctx); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
diff --git a/cmd/tcl/testworkflow-toolkit/commands/utils.go b/cmd/tcl/testworkflow-toolkit/commands/utils.go
new file mode 100644
index 00000000000..50dfd4c9060
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/commands/utils.go
@@ -0,0 +1,43 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package commands
+
+import (
+ "os"
+ "os/exec"
+ "strconv"
+)
+
+func concat(args ...interface{}) []string {
+ result := make([]string, 0)
+ for _, a := range args {
+ switch a.(type) {
+ case string:
+ result = append(result, a.(string))
+ case int:
+ result = append(result, strconv.Itoa(a.(int)))
+ case []string:
+ result = append(result, a.([]string)...)
+ case []interface{}:
+ result = append(result, concat(a.([]interface{})...)...)
+ }
+ }
+ return result
+}
+
+func Comm(cmd string, args ...interface{}) *exec.Cmd {
+ return exec.Command(cmd, concat(args...)...)
+}
+
+func Run(c string, args ...interface{}) error {
+ sub := Comm(c, args...)
+ sub.Stdout = os.Stdout
+ sub.Stderr = os.Stderr
+ return sub.Run()
+}
diff --git a/cmd/tcl/testworkflow-toolkit/env/client.go b/cmd/tcl/testworkflow-toolkit/env/client.go
new file mode 100644
index 00000000000..5051de74908
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/env/client.go
@@ -0,0 +1,74 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package env
+
+import (
+ "context"
+ "fmt"
+
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+
+ "github.com/kubeshop/testkube/cmd/kubectl-testkube/config"
+ "github.com/kubeshop/testkube/pkg/agent"
+ "github.com/kubeshop/testkube/pkg/api/v1/client"
+ "github.com/kubeshop/testkube/pkg/cloud"
+ cloudexecutor "github.com/kubeshop/testkube/pkg/cloud/data/executor"
+ phttp "github.com/kubeshop/testkube/pkg/http"
+ "github.com/kubeshop/testkube/pkg/k8sclient"
+ "github.com/kubeshop/testkube/pkg/log"
+ "github.com/kubeshop/testkube/pkg/storage/minio"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func KubernetesConfig() *rest.Config {
+ c, err := rest.InClusterConfig()
+ if err != nil {
+ var fsErr error
+ c, fsErr = k8sclient.GetK8sClientConfig()
+ if fsErr != nil {
+ ui.Fail(fmt.Errorf("couldn't find Kubernetes config: %w and %w", err, fsErr))
+ }
+ }
+ return c
+}
+
+func Kubernetes() *kubernetes.Clientset {
+ c, err := kubernetes.NewForConfig(KubernetesConfig())
+ if err != nil {
+ ui.Fail(fmt.Errorf("couldn't instantiate Kubernetes client: %w", err))
+ }
+ return c
+}
+
+func Testkube() client.Client {
+ if UseProxy() {
+ return client.NewProxyAPIClient(Kubernetes(), client.NewAPIConfig(Namespace(), config.APIServerName, config.APIServerPort))
+ }
+ httpClient := phttp.NewClient(true)
+ sseClient := phttp.NewSSEClient(true)
+ return client.NewDirectAPIClient(httpClient, sseClient, fmt.Sprintf("http://%s:%d", config.APIServerName, config.APIServerPort), "")
+}
+
+func ObjectStorageClient() (*minio.Client, error) {
+ cfg := Config().ObjectStorage
+ opts := minio.GetTLSOptions(cfg.Ssl, cfg.SkipVerify, cfg.CertFile, cfg.KeyFile, cfg.CAFile)
+ c := minio.NewClient(cfg.Endpoint, cfg.AccessKeyID, cfg.SecretAccessKey, cfg.Region, cfg.Token, cfg.Bucket, opts...)
+ return c, c.Connect()
+}
+
+func Cloud(ctx context.Context) cloudexecutor.Executor {
+ cfg := Config().Cloud
+ grpcConn, err := agent.NewGRPCConnection(ctx, cfg.TlsInsecure, cfg.SkipVerify, cfg.Url, "", "", "", log.DefaultLogger)
+ if err != nil {
+ ui.Fail(fmt.Errorf("failed to connect with Cloud: %w", err))
+ }
+ grpcClient := cloud.NewTestKubeCloudAPIClient(grpcConn)
+ return cloudexecutor.NewCloudGRPCExecutor(grpcClient, grpcConn, cfg.ApiKey)
+}
diff --git a/cmd/tcl/testworkflow-toolkit/env/config.go b/cmd/tcl/testworkflow-toolkit/env/config.go
new file mode 100644
index 00000000000..3356f24ff34
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/env/config.go
@@ -0,0 +1,104 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package env
+
+import (
+ "github.com/kelseyhightower/envconfig"
+
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+var (
+ UseProxyValue = false
+)
+
+type envObjectStorageConfig struct {
+ Endpoint string `envconfig:"TK_OS_ENDPOINT"`
+ AccessKeyID string `envconfig:"TK_OS_ACCESSKEY"`
+ SecretAccessKey string `envconfig:"TK_OS_SECRETKEY"`
+ Region string `envconfig:"TK_OS_REGION"`
+ Token string `envconfig:"TK_OS_TOKEN"`
+ Bucket string `envconfig:"TK_OS_BUCKET"`
+ Ssl bool `envconfig:"TK_OS_SSL" default:"false"`
+ SkipVerify bool `envconfig:"TK_OS_SSL_SKIP_VERIFY" default:"false"`
+ CertFile string `envconfig:"TK_OS_CERT_FILE"`
+ KeyFile string `envconfig:"TK_OS_KEY_FILE"`
+ CAFile string `envconfig:"TK_OS_CA_FILE"`
+}
+
+type envCloudConfig struct {
+ Url string `envconfig:"TK_C_URL"`
+ ApiKey string `envconfig:"TK_C_KEY"`
+ SkipVerify bool `envconfig:"TK_C_SKIP_VERIFY" default:"false"`
+ TlsInsecure bool `envconfig:"TK_C_TLS_INSECURE" default:"false"`
+}
+
+type envExecutionConfig struct {
+ WorkflowName string `envconfig:"TK_WF"`
+ Id string `envconfig:"TK_EX"`
+}
+
+type envSystemConfig struct {
+ Debug string `envconfig:"DEBUG"`
+ Ref string `envconfig:"TK_REF"`
+ Namespace string `envconfig:"TK_NS"`
+}
+
+type envConfig struct {
+ System envSystemConfig
+ ObjectStorage envObjectStorageConfig
+ Cloud envCloudConfig
+ Execution envExecutionConfig
+}
+
+var cfg envConfig
+var cfgLoaded = false
+
+func Config() *envConfig {
+ if !cfgLoaded {
+ err := envconfig.Process("", &cfg.System)
+ ui.ExitOnError("configuring environment", err)
+ err = envconfig.Process("", &cfg.ObjectStorage)
+ ui.ExitOnError("configuring environment", err)
+ err = envconfig.Process("", &cfg.Cloud)
+ ui.ExitOnError("configuring environment", err)
+ err = envconfig.Process("", &cfg.Execution)
+ ui.ExitOnError("configuring environment", err)
+ }
+ cfgLoaded = true
+ return &cfg
+}
+
+func Debug() bool {
+ return Config().System.Debug == "1"
+}
+
+func CloudEnabled() bool {
+ return Config().Cloud.ApiKey != ""
+}
+
+func UseProxy() bool {
+ return UseProxyValue
+}
+
+func Ref() string {
+ return Config().System.Ref
+}
+
+func Namespace() string {
+ return Config().System.Namespace
+}
+
+func WorkflowName() string {
+ return Config().Execution.WorkflowName
+}
+
+func ExecutionId() string {
+ return Config().Execution.Id
+}
diff --git a/cmd/tcl/testworkflow-toolkit/main.go b/cmd/tcl/testworkflow-toolkit/main.go
new file mode 100644
index 00000000000..b75648b389b
--- /dev/null
+++ b/cmd/tcl/testworkflow-toolkit/main.go
@@ -0,0 +1,29 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package main
+
+import (
+ "errors"
+
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-toolkit/commands"
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-toolkit/env"
+ "github.com/kubeshop/testkube/pkg/ui"
+)
+
+func main() {
+ // Set verbosity
+ ui.SetVerbose(env.Debug())
+
+ // Validate provided data
+ if env.Namespace() == "" || env.Ref() == "" {
+ ui.Fail(errors.New("environment is misconfigured"))
+ }
+
+ commands.Execute()
+}
diff --git a/codecov.yaml b/codecov.yaml
index 186d462fe1a..1a3ad9a0358 100644
--- a/codecov.yaml
+++ b/codecov.yaml
@@ -1,5 +1,7 @@
ignore:
- - "cmd/****"
+ - "cmd/**/*"
+ - "**/*_mock.go"
+ - "**/mock_*.go"
coverage:
status:
diff --git a/config/job-container-template.yml b/config/job-container-template.yml
index f3372643357..93249d45aed 100644
--- a/config/job-container-template.yml
+++ b/config/job-container-template.yml
@@ -77,6 +77,11 @@ spec:
- name: {{ .CertificateSecret }}
mountPath: /etc/certs
{{- end }}
+ {{- if .AgentAPITLSSecret }}
+ - mountPath: /tmp/agent-cert
+ readOnly: true
+ name: {{ .AgentAPITLSSecret }}
+ {{- end }}
{{- if .ArtifactRequest }}
{{- if and .ArtifactRequest.VolumeMountPath .ArtifactRequest.StorageClassName }}
- name: artifact-volume
@@ -103,6 +108,11 @@ spec:
secret:
secretName: {{ .CertificateSecret }}
{{- end }}
+ {{- if .AgentAPITLSSecret }}
+ - name: { { .AgentAPITLSSecret } }
+ secret:
+ secretName: {{ .AgentAPITLSSecret }}
+ {{- end }}
{{- if .ArtifactRequest }}
{{- if and .ArtifactRequest.VolumeMountPath .ArtifactRequest.StorageClassName }}
- name: artifact-volume
diff --git a/config/job-template.yml b/config/job-template.yml
index 4525c298616..e4ca84162df 100644
--- a/config/job-template.yml
+++ b/config/job-template.yml
@@ -17,7 +17,7 @@ spec:
image: {{ .InitImage }}
{{- end }}
imagePullPolicy: IfNotPresent
- command:
+ command:
- "/bin/runner"
- '{{ .Jsn }}'
volumeMounts:
@@ -27,6 +27,11 @@ spec:
- name: {{ .CertificateSecret }}
mountPath: /etc/certs
{{- end }}
+ {{- if .AgentAPITLSSecret }}
+ - mountPath: /tmp/agent-cert
+ readOnly: true
+ name: {{ .AgentAPITLSSecret }}
+ {{- end }}
{{- if .ArtifactRequest }}
{{- if and .ArtifactRequest.VolumeMountPath .ArtifactRequest.StorageClassName }}
- name: artifact-volume
@@ -46,7 +51,7 @@ spec:
{{- end }}
{{- end }}
containers:
- {{ if .Features.LogsV2 -}}
+ {{ if .Features.LogsV2 -}}
- name: "{{ .Name }}-logs"
image: {{ .Registry }}/{{ .LogSidecarImage }}
env:
@@ -66,7 +71,7 @@ spec:
image: {{ .Image }}
{{- end }}
imagePullPolicy: IfNotPresent
- command:
+ command:
- "/bin/runner"
- '{{ .Jsn }}'
volumeMounts:
@@ -76,6 +81,11 @@ spec:
- name: {{ .CertificateSecret }}
mountPath: /etc/certs
{{- end }}
+ {{- if .AgentAPITLSSecret }}
+ - mountPath: /tmp/agent-cert
+ readOnly: true
+ name: {{ .AgentAPITLSSecret }}
+ {{- end }}
{{- if .ArtifactRequest }}
{{- if and .ArtifactRequest.VolumeMountPath .ArtifactRequest.StorageClassName }}
- name: artifact-volume
@@ -102,6 +112,11 @@ spec:
secret:
secretName: {{ .CertificateSecret }}
{{- end }}
+ {{- if .AgentAPITLSSecret }}
+ - name: {{ .AgentAPITLSSecret }}
+ secret:
+ secretName: {{ .AgentAPITLSSecret }}
+ {{- end }}
{{- if .ArtifactRequest }}
{{- if and .ArtifactRequest.VolumeMountPath .ArtifactRequest.StorageClassName }}
- name: artifact-volume
diff --git a/config/slave-pod-template.yml b/config/slave-pod-template.yml
index 0530fd864ac..33d6fd9f8fb 100644
--- a/config/slave-pod-template.yml
+++ b/config/slave-pod-template.yml
@@ -139,7 +139,7 @@ spec:
{{- if and .ArtifactRequest.VolumeMountPath .ArtifactRequest.StorageClassName }}
- name: artifact-volume
persistentVolumeClaim:
- claimName: {{ .Name }}-pvc
+ claimName: {{ .JobName }}-pvc
{{- end }}
{{- end }}
{{- range $configmap := .EnvConfigMaps }}
diff --git a/contrib/executor/artillery/README.md b/contrib/executor/artillery/README.md
index 0784b5d01ec..ad724060a8a 100644
--- a/contrib/executor/artillery/README.md
+++ b/contrib/executor/artillery/README.md
@@ -63,5 +63,5 @@ For more info go to [main testkube repo](https://github.com/kubeshop/testkube)
![Docker builds](https://img.shields.io/docker/automated/kubeshop/testkube-api-server) ![Code build](https://img.shields.io/github/workflow/status/kubeshop/testkube/Code%20build%20and%20checks) ![Release date](https://img.shields.io/github/release-date/kubeshop/testkube)
-![Twitter](https://img.shields.io/twitter/follow/thekubeshop?style=social) ![Discord](https://img.shields.io/discord/884464549347074049)
- #### [Documentation](https://docs.testkube.io) | [Discord](https://discord.gg/hfq44wtR6Q)
\ No newline at end of file
+![Twitter](https://img.shields.io/twitter/follow/thekubeshop?style=social) ![Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)
+ #### [Documentation](https://docs.testkube.io) | [Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)
\ No newline at end of file
diff --git a/contrib/executor/curl/pkg/runner/runner.go b/contrib/executor/curl/pkg/runner/runner.go
index 5d094589e1c..d0040d6999a 100644
--- a/contrib/executor/curl/pkg/runner/runner.go
+++ b/contrib/executor/curl/pkg/runner/runner.go
@@ -75,7 +75,7 @@ func (r *CurlRunner) Run(ctx context.Context, execution testkube.Execution) (res
if fileInfo.IsDir() {
scriptName := execution.Args[len(execution.Args)-1]
if workingDir != "" {
- path = filepath.Join(r.Params.DataDir, "repo")
+ path = ""
if execution.Content != nil && execution.Content.Repository != nil {
scriptName = filepath.Join(execution.Content.Repository.Path, scriptName)
}
diff --git a/contrib/executor/gradle/README.md b/contrib/executor/gradle/README.md
index 194b76818de..7a98d3f3d01 100644
--- a/contrib/executor/gradle/README.md
+++ b/contrib/executor/gradle/README.md
@@ -31,5 +31,5 @@ For more info go to [main testkube repo](https://github.com/kubeshop/testkube)
![Docker builds](https://img.shields.io/docker/automated/kubeshop/testkube-api-server) ![Code build](https://img.shields.io/github/workflow/status/kubeshop/testkube/Code%20build%20and%20checks) ![Release date](https://img.shields.io/github/release-date/kubeshop/testkube)
-![Twitter](https://img.shields.io/twitter/follow/thekubeshop?style=social) ![Discord](https://img.shields.io/discord/884464549347074049)
- #### [Documentation](https://docs.testkube.io) | [Discord](https://discord.gg/hfq44wtR6Q)
+![Twitter](https://img.shields.io/twitter/follow/thekubeshop?style=social) ![Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)
+ #### [Documentation](https://docs.testkube.io) | [Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)
diff --git a/contrib/executor/gradle/build/agent/Dockerfile.jdk11 b/contrib/executor/gradle/build/agent/Dockerfile.jdk11
index ed08d550d1f..8111390e8df 100644
--- a/contrib/executor/gradle/build/agent/Dockerfile.jdk11
+++ b/contrib/executor/gradle/build/agent/Dockerfile.jdk11
@@ -2,6 +2,9 @@
FROM gradle:8.5.0-jdk11
COPY gradle /bin/runner
+RUN chown -R 1001:1001 /home/gradle
+ENV GRADLE_USER_HOME /home/gradle
+
USER 1001
ENTRYPOINT ["/bin/runner"]
diff --git a/contrib/executor/gradle/build/agent/Dockerfile.jdk17 b/contrib/executor/gradle/build/agent/Dockerfile.jdk17
index 074ce893636..41353e9b66b 100644
--- a/contrib/executor/gradle/build/agent/Dockerfile.jdk17
+++ b/contrib/executor/gradle/build/agent/Dockerfile.jdk17
@@ -2,6 +2,9 @@
FROM gradle:8.5.0-jdk17
COPY gradle /bin/runner
+RUN chown -R 1001:1001 /home/gradle
+ENV GRADLE_USER_HOME /home/gradle
+
USER 1001
ENTRYPOINT ["/bin/runner"]
diff --git a/contrib/executor/gradle/build/agent/Dockerfile.jdk18 b/contrib/executor/gradle/build/agent/Dockerfile.jdk18
index 0a21cb34b13..e365ec77b03 100644
--- a/contrib/executor/gradle/build/agent/Dockerfile.jdk18
+++ b/contrib/executor/gradle/build/agent/Dockerfile.jdk18
@@ -2,6 +2,9 @@
FROM gradle:8.5.0-jdk18
COPY gradle /bin/runner
+RUN chown -R 1001:1001 /home/gradle
+ENV GRADLE_USER_HOME /home/gradle
+
USER 1001
ENTRYPOINT ["/bin/runner"]
diff --git a/contrib/executor/gradle/build/agent/Dockerfile.jdk8 b/contrib/executor/gradle/build/agent/Dockerfile.jdk8
index 463101d6afa..546000c545f 100644
--- a/contrib/executor/gradle/build/agent/Dockerfile.jdk8
+++ b/contrib/executor/gradle/build/agent/Dockerfile.jdk8
@@ -2,6 +2,9 @@
FROM gradle:8.5.0-jdk8
COPY gradle /bin/runner
+RUN chown -R 1001:1001 /home/gradle
+ENV GRADLE_USER_HOME /home/gradle
+
USER 1001
ENTRYPOINT ["/bin/runner"]
diff --git a/contrib/executor/gradle/pkg/runner/runner_integration_test.go b/contrib/executor/gradle/pkg/runner/runner_integration_test.go
index 06db2507ede..df2cf97dfc2 100644
--- a/contrib/executor/gradle/pkg/runner/runner_integration_test.go
+++ b/contrib/executor/gradle/pkg/runner/runner_integration_test.go
@@ -3,7 +3,6 @@ package runner
import (
"context"
- "fmt"
"os"
"path/filepath"
"testing"
@@ -88,8 +87,6 @@ func TestRunGradle_Integration(t *testing.T) {
// when
result, err := runner.Run(ctx, *execution)
- fmt.Printf("%+v\n", result)
-
// then
assert.NoError(t, err)
assert.Equal(t, result.Status, testkube.ExecutionStatusPassed)
diff --git a/contrib/executor/init/pkg/runner/runner.go b/contrib/executor/init/pkg/runner/runner.go
index 5db22700520..acd893dfee5 100755
--- a/contrib/executor/init/pkg/runner/runner.go
+++ b/contrib/executor/init/pkg/runner/runner.go
@@ -81,27 +81,50 @@ func (r *InitRunner) Run(ctx context.Context, execution testkube.Execution) (res
}
shebang := "#!" + shell + "\nset -e\n"
- entrypoint := shebang
+ // No set -e so that we can run the post-run script even if the command fails
+ entrypoint := "#!" + shell + "\n"
command := shebang
preRunScript := shebang
postRunScript := shebang
if execution.PreRunScript != "" {
+ if execution.SourceScripts {
+ entrypoint += ". "
+ }
+
entrypoint += strconv.Quote(filepath.Join(r.Params.DataDir, preRunScriptName)) + "\n"
+ entrypoint += "prerun_exit_code=$?\nif [ $prerun_exit_code -ne 0 ]; then\n exit $prerun_exit_code\nfi\n"
preRunScript += execution.PreRunScript
}
if len(execution.Command) != 0 {
+ if execution.SourceScripts {
+ entrypoint += ". "
+ }
+
entrypoint += strconv.Quote(filepath.Join(r.Params.DataDir, commandScriptName)) + " $@\n"
+ entrypoint += "command_exit_code=$?\n"
command += strings.Join(execution.Command, " ")
command += " \"$@\"\n"
}
if execution.PostRunScript != "" {
+ if execution.SourceScripts {
+ entrypoint += ". "
+ }
+
entrypoint += strconv.Quote(filepath.Join(r.Params.DataDir, postRunScriptName)) + "\n"
+ entrypoint += "postrun_exit_code=$?\n"
postRunScript += execution.PostRunScript
}
+ if len(execution.Command) != 0 {
+ entrypoint += "if [ $command_exit_code -ne 0 ]; then\n exit $command_exit_code\nfi\n"
+ }
+
+ if execution.PostRunScript != "" {
+ entrypoint += "exit $postrun_exit_code\n"
+ }
var scripts = []struct {
dir string
file string
@@ -130,13 +153,13 @@ func (r *InitRunner) Run(ctx context.Context, execution testkube.Execution) (res
}
// TODO: write a proper cloud implementation
- if r.Params.Endpoint != "" && !r.Params.CloudMode {
+ if r.Params.Endpoint != "" && !r.Params.ProMode {
output.PrintLogf("%s Fetching uploads from object store %s...", ui.IconFile, r.Params.Endpoint)
opts := minio.GetTLSOptions(r.Params.Ssl, r.Params.SkipVerify, r.Params.CertFile, r.Params.KeyFile, r.Params.CAFile)
minioClient := minio.NewClient(r.Params.Endpoint, r.Params.AccessKeyID, r.Params.SecretAccessKey, r.Params.Region, r.Params.Token, r.Params.Bucket, opts...)
fp := content.NewCopyFilesPlacer(minioClient)
fp.PlaceFiles(ctx, execution.TestName, execution.BucketName)
- } else if r.Params.CloudMode {
+ } else if r.Params.ProMode {
output.PrintLogf("%s Copy files functionality is currently not supported in cloud mode", ui.IconWarning)
}
diff --git a/contrib/executor/init/pkg/runner/runner_test.go b/contrib/executor/init/pkg/runner/runner_test.go
index 977c20213f7..ac06bfaed26 100755
--- a/contrib/executor/init/pkg/runner/runner_test.go
+++ b/contrib/executor/init/pkg/runner/runner_test.go
@@ -2,6 +2,7 @@ package runner
import (
"context"
+ "os"
"testing"
"github.com/stretchr/testify/assert"
@@ -31,4 +32,45 @@ func TestRun(t *testing.T) {
assert.Equal(t, result.Status, testkube.ExecutionStatusRunning)
})
+ t.Run("runner with pre and post run scripts should run test", func(t *testing.T) {
+ t.Parallel()
+
+ params := envs.Params{DataDir: "./testdir"}
+ runner := NewRunner(params)
+ execution := testkube.NewQueuedExecution()
+ execution.Content = testkube.NewStringTestContent("hello I'm test content")
+ execution.PreRunScript = "echo \"===== pre-run script\""
+ execution.Command = []string{"command.sh"}
+ execution.PostRunScript = "echo \"===== pre-run script\""
+
+ // when
+ result, err := runner.Run(ctx, *execution)
+
+ // then
+ assert.NoError(t, err)
+ assert.Equal(t, result.Status, testkube.ExecutionStatusRunning)
+
+ expected := `#!/bin/sh
+"testdir/prerun.sh"
+prerun_exit_code=$?
+if [ $prerun_exit_code -ne 0 ]; then
+ exit $prerun_exit_code
+fi
+"testdir/command.sh" $@
+command_exit_code=$?
+"testdir/postrun.sh"
+postrun_exit_code=$?
+if [ $command_exit_code -ne 0 ]; then
+ exit $command_exit_code
+fi
+exit $postrun_exit_code
+`
+
+ data, err := os.ReadFile("testdir/entrypoint.sh")
+ if err != nil {
+ t.Fatalf("Failed to read file: %v", err)
+ }
+ assert.Equal(t, string(data), expected)
+ })
+
}
diff --git a/contrib/executor/jmeter/pkg/runner/runner.go b/contrib/executor/jmeter/pkg/runner/runner.go
index 9cb299c7fd3..ca25d14b13e 100644
--- a/contrib/executor/jmeter/pkg/runner/runner.go
+++ b/contrib/executor/jmeter/pkg/runner/runner.go
@@ -75,7 +75,7 @@ func (r *JMeterRunner) Run(ctx context.Context, execution testkube.Execution) (r
if fileInfo.IsDir() {
scriptName := execution.Args[len(execution.Args)-1]
if workingDir != "" {
- path = filepath.Join(r.Params.DataDir, "repo")
+ path = ""
if execution.Content != nil && execution.Content.Repository != nil {
scriptName = filepath.Join(execution.Content.Repository.Path, scriptName)
}
diff --git a/contrib/executor/jmeterd/pkg/parser/mapper.go b/contrib/executor/jmeterd/pkg/parser/mapper.go
index bc077431bc0..aaaf4a47cd7 100644
--- a/contrib/executor/jmeterd/pkg/parser/mapper.go
+++ b/contrib/executor/jmeterd/pkg/parser/mapper.go
@@ -8,10 +8,11 @@ import (
func mapCSVResultsToExecutionResults(out []byte, results CSVResults) (result testkube.ExecutionResult) {
result = MakeSuccessExecution(out)
- if results.HasError {
- result.Status = testkube.ExecutionStatusFailed
- result.ErrorMessage = results.LastErrorMessage
- }
+ // TODO: Is it enough to just disable it here?
+ //if results.HasError {
+ // result.Status = testkube.ExecutionStatusFailed
+ // result.ErrorMessage = results.LastErrorMessage
+ //}
for _, r := range results.Results {
result.Steps = append(
@@ -43,12 +44,13 @@ func mapXMLResultsToExecutionResults(out []byte, results XMLResults) (result tes
samples := append(results.HTTPSamples, results.Samples...)
for _, r := range samples {
- if !r.Success {
- result.Status = testkube.ExecutionStatusFailed
- if r.AssertionResult != nil {
- result.ErrorMessage = r.AssertionResult.FailureMessage
- }
- }
+ // TODO: Is it enough to disable it here?
+ //if !r.Success {
+ // result.Status = testkube.ExecutionStatusFailed
+ // if r.AssertionResult != nil {
+ // result.ErrorMessage = r.AssertionResult.FailureMessage
+ // }
+ //}
result.Steps = append(
result.Steps,
diff --git a/contrib/executor/jmeterd/pkg/runner/helpers.go b/contrib/executor/jmeterd/pkg/runner/helpers.go
index 6dfa6b85fc7..e0cdb47df93 100644
--- a/contrib/executor/jmeterd/pkg/runner/helpers.go
+++ b/contrib/executor/jmeterd/pkg/runner/helpers.go
@@ -18,6 +18,11 @@ const (
envVarPrefix = "$"
)
+var (
+ ErrParamMissingValue = errors.New("no value found for parameter")
+ ErrMissingParam = errors.New("parameter not found")
+)
+
func getTestPathAndWorkingDir(fs filesystem.FileSystem, execution *testkube.Execution, dataDir string) (testPath string, workingDir, testFile string, err error) {
testPath, workingDir, err = content.GetPathAndWorkingDir(execution.Content, dataDir)
if err != nil {
@@ -29,21 +34,52 @@ func getTestPathAndWorkingDir(fs filesystem.FileSystem, execution *testkube.Exec
return "", "", "", err
}
- if fileInfo.IsDir() {
+ testFlag := ""
+ for i, arg := range execution.Args {
+ if arg == jmeterTestFileFlag {
+ if (i + 1) < len(execution.Args) {
+ if execution.Args[i+1] != "" {
+ testFlag = execution.Args[i+1]
+ i++
+ continue
+ }
+ }
+ }
+ }
+
+ if workingDir == "" {
+ workingDir = dataDir
+ }
+
+ sanityCheck := false
+ if testFlag != "" {
+ if filepath.IsAbs(testFlag) {
+ testPath = testFlag
+ } else {
+ testPath = filepath.Join(workingDir, testFlag)
+ }
+
+ testFile = filepath.Base(testPath)
+ sanityCheck = true
+ } else if fileInfo.IsDir() {
testFile, err = findTestFile(fs, execution, testPath, jmxExtension)
if err != nil {
return "", "", "", errors.Wrapf(err, "error searching for %s file in test path %s", jmxExtension, testPath)
}
- // sanity checking for test script
testPath = filepath.Join(testPath, testFile)
- fileInfo, err := fs.Stat(testPath)
+ sanityCheck = true
+ }
+
+ if sanityCheck {
+ // sanity checking for test script
+ fileInfo, err = fs.Stat(testPath)
if err != nil || fileInfo.IsDir() {
output.PrintLogf("%s Could not find file %s in the directory, error: %s", ui.IconCross, testFile, err)
return "", "", "", errors.Wrapf(err, "could not find file %s in the directory", testFile)
-
}
}
+
return
}
@@ -71,7 +107,7 @@ func findTestFile(fs filesystem.FileSystem, execution *testkube.Execution, testP
}
}
if testFile == "" {
- output.PrintLogf("%s %s file not found in args or test path!", ui.IconCross, testExtension)
+ output.PrintLogf("%s %s file not found in args or test path!", ui.IconCross, testExtension)
return "", errors.Errorf("no %s file found", testExtension)
}
return testFile, nil
@@ -114,3 +150,18 @@ func injectAndExpandEnvVars(args []string, params []string) []string {
return copied
}
+
+// getParamValue searches for a parameter in the args slice and returns its value.
+// It returns an error if the parameter is not found or if it does not have an associated value.
+func getParamValue(args []string, param string) (string, error) {
+ for i, arg := range args {
+ if arg == param {
+ // Check if the next element exists
+ if i+1 < len(args) {
+ return args[i+1], nil
+ }
+ return "", errors.WithStack(ErrParamMissingValue)
+ }
+ }
+ return "", errors.WithStack(ErrMissingParam)
+}
diff --git a/contrib/executor/jmeterd/pkg/runner/helpers_test.go b/contrib/executor/jmeterd/pkg/runner/helpers_test.go
index 5625effcc0c..c5f6ba70e6e 100644
--- a/contrib/executor/jmeterd/pkg/runner/helpers_test.go
+++ b/contrib/executor/jmeterd/pkg/runner/helpers_test.go
@@ -41,7 +41,7 @@ func TestGetTestPathAndWorkingDir(t *testing.T) {
dataDir: "/tmp/data",
},
wantTestPath: "/tmp/data/test-content",
- wantWorkingDir: "",
+ wantWorkingDir: "/tmp/data",
wantTestFile: "",
wantErr: false,
setup: func(fs *filesystem.MockFileSystem) {
@@ -59,7 +59,7 @@ func TestGetTestPathAndWorkingDir(t *testing.T) {
dataDir: "/tmp/data",
},
wantTestPath: "/tmp/data/repo/test.jmx",
- wantWorkingDir: "",
+ wantWorkingDir: "/tmp/data",
wantTestFile: "test.jmx",
wantErr: false,
setup: func(fs *filesystem.MockFileSystem) {
@@ -114,6 +114,112 @@ func TestGetTestPathAndWorkingDir(t *testing.T) {
fs.EXPECT().Stat(gomock.Any()).Return(nil, errors.New("stat error"))
},
},
+ {
+ name: "Get test path and working dir for -t absolute with working dir",
+ args: args{
+ fs: filesystem.NewMockFileSystem(mockCtrl),
+ execution: testkube.Execution{
+ Content: &testkube.TestContent{
+ Type_: string(testkube.TestContentTypeGitFile),
+ Repository: &testkube.Repository{
+ WorkingDir: "tests",
+ Path: "tests/test1",
+ },
+ },
+ Args: []string{"-t", "/tmp/data/repo/tests/test1/test.jmx"},
+ },
+ dataDir: "/tmp/data",
+ },
+ wantTestPath: "/tmp/data/repo/tests/test1/test.jmx",
+ wantWorkingDir: "/tmp/data/repo/tests",
+ wantTestFile: "test.jmx",
+ wantErr: false,
+ setup: func(fs *filesystem.MockFileSystem) {
+ gomock.InOrder(
+ fs.EXPECT().Stat("/tmp/data/repo/tests/test1").Return(&filesystem.MockFileInfo{FIsDir: true}, nil),
+ fs.EXPECT().Stat("/tmp/data/repo/tests/test1/test.jmx").Return(&filesystem.MockFileInfo{FIsDir: false}, nil),
+ )
+ },
+ },
+ {
+ name: "Get test path and working dir for -t absolute without working dir",
+ args: args{
+ fs: filesystem.NewMockFileSystem(mockCtrl),
+ execution: testkube.Execution{
+ Content: &testkube.TestContent{
+ Type_: string(testkube.TestContentTypeGitFile),
+ Repository: &testkube.Repository{
+ Path: "tests/test1",
+ },
+ },
+ Args: []string{"-t", "/tmp/data/repo/tests/test1/test.jmx"},
+ },
+ dataDir: "/tmp/data",
+ },
+ wantTestPath: "/tmp/data/repo/tests/test1/test.jmx",
+ wantWorkingDir: "/tmp/data",
+ wantTestFile: "test.jmx",
+ wantErr: false,
+ setup: func(fs *filesystem.MockFileSystem) {
+ gomock.InOrder(
+ fs.EXPECT().Stat("/tmp/data/repo/tests/test1").Return(&filesystem.MockFileInfo{FIsDir: true}, nil),
+ fs.EXPECT().Stat("/tmp/data/repo/tests/test1/test.jmx").Return(&filesystem.MockFileInfo{FIsDir: false}, nil),
+ )
+ },
+ },
+ {
+ name: "Get test path and working dir for -t relative with working dir",
+ args: args{
+ fs: filesystem.NewMockFileSystem(mockCtrl),
+ execution: testkube.Execution{
+ Content: &testkube.TestContent{
+ Type_: string(testkube.TestContentTypeGitFile),
+ Repository: &testkube.Repository{
+ WorkingDir: "tests",
+ Path: "tests/test1",
+ },
+ },
+ Args: []string{"-t", "test1/test.jmx"},
+ },
+ dataDir: "/tmp/data",
+ },
+ wantTestPath: "/tmp/data/repo/tests/test1/test.jmx",
+ wantWorkingDir: "/tmp/data/repo/tests",
+ wantTestFile: "test.jmx",
+ wantErr: false,
+ setup: func(fs *filesystem.MockFileSystem) {
+ gomock.InOrder(
+ fs.EXPECT().Stat("/tmp/data/repo/tests/test1").Return(&filesystem.MockFileInfo{FIsDir: true}, nil),
+ fs.EXPECT().Stat("/tmp/data/repo/tests/test1/test.jmx").Return(&filesystem.MockFileInfo{FIsDir: false}, nil),
+ )
+ },
+ },
+ {
+ name: "Get test path and working dir for -t relative without working dir",
+ args: args{
+ fs: filesystem.NewMockFileSystem(mockCtrl),
+ execution: testkube.Execution{
+ Content: &testkube.TestContent{
+ Type_: string(testkube.TestContentTypeGitFile),
+ Repository: &testkube.Repository{
+ Path: "tests/test1",
+ },
+ },
+ Args: []string{"-t", "repo/tests/test1/test.jmx"},
+ },
+ dataDir: "/tmp/data",
+ },
+ wantTestPath: "/tmp/data/repo/tests/test1/test.jmx",
+ wantWorkingDir: "/tmp/data",
+ wantTestFile: "test.jmx",
+ wantErr: false,
+ setup: func(fs *filesystem.MockFileSystem) {
+ gomock.InOrder(
+ fs.EXPECT().Stat("/tmp/data/repo/tests/test1").Return(&filesystem.MockFileInfo{FIsDir: true}, nil),
+ fs.EXPECT().Stat("/tmp/data/repo/tests/test1/test.jmx").Return(&filesystem.MockFileInfo{FIsDir: false}, nil),
+ )
+ },
+ },
}
for _, tt := range tests {
@@ -281,3 +387,34 @@ func TestInjectAndExpandEnvVars(t *testing.T) {
})
}
}
+
+func TestGetParamValue(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ name string
+ args []string
+ param string
+ expected string
+ wantErr error
+ }{
+ {name: "get last param successfully", args: []string{"-n", "-o", "/data", "-t", "/data/repo"}, param: "-t", expected: "/data/repo", wantErr: nil},
+ {name: "get middle param successfully", args: []string{"-n", "-o", "/data", "-t", "/data/repo"}, param: "-o", expected: "/data", wantErr: nil},
+ {name: "param missing value returns error", args: []string{"-n", "-o", "/data", "-t"}, param: "-t", expected: "", wantErr: ErrParamMissingValue},
+ {name: "param missing", args: []string{"-n", "-o", "/data", "-t", "/data/repo"}, param: "-x", expected: "", wantErr: ErrMissingParam},
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ value, err := getParamValue(tc.args, tc.param)
+ if tc.wantErr != nil {
+ assert.ErrorIs(t, err, tc.wantErr)
+ assert.Empty(t, value)
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, tc.expected, value)
+ }
+ })
+ }
+}
diff --git a/contrib/executor/jmeterd/pkg/runner/runner.go b/contrib/executor/jmeterd/pkg/runner/runner.go
index ea0c96d4004..9346bd0d70e 100644
--- a/contrib/executor/jmeterd/pkg/runner/runner.go
+++ b/contrib/executor/jmeterd/pkg/runner/runner.go
@@ -32,11 +32,10 @@ import (
type JMeterMode string
const (
- jmeterModeStandalone JMeterMode = "standalone"
- jmeterModeDistributed JMeterMode = "distributed"
- globalJMeterParamPrefix = "-G"
- standaloneJMeterParamPrefix = "-J"
- jmxExtension = "jmx"
+ jmeterModeStandalone JMeterMode = "standalone"
+ jmeterModeDistributed JMeterMode = "distributed"
+ jmxExtension = "jmx"
+ jmeterTestFileFlag = "-t"
)
// JMeterDRunner runner
@@ -118,11 +117,21 @@ func (r *JMeterDRunner) Run(ctx context.Context, execution testkube.Execution) (
// Add user plugins folder in slaves env variables
slavesEnvVariables["JMETER_PARENT_TEST_FOLDER"] = testkube.NewBasicVariable("JMETER_PARENT_TEST_FOLDER", parentTestFolder)
- runPath := r.Params.DataDir
- if workingDir != "" {
- runPath = workingDir
+ runPath := workingDir
+
+ outputDir := ""
+ if envVar, ok := envManager.Variables["RUNNER_ARTIFACTS_DIR"]; ok {
+ outputDir = envVar.Value
+ }
+
+ if outputDir == "" {
+ outputDir = filepath.Join(runPath, "output")
+ err = os.Setenv("RUNNER_ARTIFACTS_DIR", outputDir)
+ if err != nil {
+ output.PrintLogf("%s Failed to set output directory %s", ui.IconWarning, outputDir)
+ }
}
- outputDir := filepath.Join(runPath, "output")
+
// recreate output directory with wide permissions so JMeter can create report files
if err = os.Mkdir(outputDir, 0777); err != nil {
return *result.Err(errors.Wrapf(err, "error creating directory %s", outputDir)), nil
@@ -131,8 +140,8 @@ func (r *JMeterDRunner) Run(ctx context.Context, execution testkube.Execution) (
jtlPath := filepath.Join(outputDir, "report.jtl")
reportPath := filepath.Join(outputDir, "report")
jmeterLogPath := filepath.Join(outputDir, "jmeter.log")
- args := execution.Args
- hasJunit, hasReport := replacePlaceholderArgs(args, testPath, jtlPath, reportPath, jmeterLogPath)
+ args := mergeDuplicatedArgs(removeDuplicatedArgs(execution.Args))
+ hasJunit, hasReport := prepareArgs(args, testPath, jtlPath, reportPath, jmeterLogPath)
if mode == jmeterModeDistributed {
clientSet, err := k8sclient.ConnectToK8s()
@@ -151,6 +160,12 @@ func (r *JMeterDRunner) Run(ctx context.Context, execution testkube.Execution) (
args = injectAndExpandEnvVars(args, nil)
output.PrintLogf("%s Using arguments: %v", ui.IconWorld, envManager.ObfuscateStringSlice(args))
+ // TODO: this is a workaround, the check should be ideally performed in the getTestPathAndWorkingDir function
+ if err := checkIfTestFileExists(r.fs, args, workingDir); err != nil {
+ output.PrintLogf("%s Error validating test file exists: %v", ui.IconCross, err.Error())
+ return result, errors.WithStack(err)
+ }
+
entryPoint := getEntryPoint()
for i := range execution.Command {
if execution.Command[i] == "" {
@@ -224,10 +239,87 @@ func initSlaves(
return slaveClient.DeleteSlaves(ctx, slaveMeta)
}
return slaveMeta, cleanupFunc, nil
+}
+
+func checkIfTestFileExists(fs filesystem.FileSystem, args []string, workingDir string) error {
+ if len(args) == 0 {
+ return errors.New("no arguments provided")
+ }
+ testParamValue, err := getParamValue(args, jmeterTestFileFlag)
+ if err != nil {
+ return errors.Wrapf(err, "error extracting value for %s flag", jmeterTestFileFlag)
+ }
+ if !filepath.IsAbs(testParamValue) {
+ testParamValue = filepath.Join(workingDir, testParamValue)
+ }
+ info, err := fs.Stat(testParamValue)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ if info.IsDir() {
+ return errors.Errorf("test file %s is a directory", testParamValue)
+ }
+ return nil
}
-func replacePlaceholderArgs(args []string, path, jtlPath, reportPath, jmeterLogPath string) (hasJunit, hasReport bool) {
+func removeDuplicatedArgs(args []string) []string {
+ counters := make(map[string]int)
+ duplicates := make(map[string]string)
+ for _, arg := range args {
+ counters[arg] += 1
+ if counters[arg] > 1 {
+ switch arg {
+ case "-t":
+ duplicates[""] = arg
+ case "-l":
+ duplicates[""] = arg
+ case "-o":
+ duplicates[""] = arg
+ case "-j":
+ duplicates[""] = arg
+ }
+ }
+ }
+
+ for i := len(args) - 1; i >= 0; i-- {
+ if arg, ok := duplicates[args[i]]; ok {
+ args = append(args[:i], args[i+1:]...)
+ if i > 0 {
+ i--
+ if args[i] == arg {
+ args = append(args[:i], args[i+1:]...)
+ }
+ }
+ }
+ }
+
+ return args
+}
+
+func mergeDuplicatedArgs(args []string) []string {
+ // -n is mandatory regardless of args mode
+ args = append(args, "-n")
+ allowed := map[string]int{
+ "-e": 0,
+ "-n": 0,
+ }
+
+ for i := len(args) - 1; i >= 0; i-- {
+ if counter, ok := allowed[args[i]]; ok {
+ allowed[args[i]]++
+ if counter == 0 {
+ continue
+ }
+
+ args = append(args[:i], args[i+1:]...)
+ }
+ }
+
+ return args
+}
+
+func prepareArgs(args []string, path, jtlPath, reportPath, jmeterLogPath string) (hasJunit, hasReport bool) {
for i, arg := range args {
switch arg {
case "":
@@ -243,7 +335,7 @@ func replacePlaceholderArgs(args []string, path, jtlPath, reportPath, jmeterLogP
hasJunit = true
}
}
- return
+ return hasJunit, hasReport
}
func getEntryPoint() (entrypoint string) {
@@ -293,7 +385,10 @@ func runScraperIfEnabled(ctx context.Context, enabled bool, scraper scraper.Scra
directories := dirs
var masks []string
if execution.ArtifactRequest != nil {
- directories = append(directories, execution.ArtifactRequest.Dirs...)
+ if len(execution.ArtifactRequest.Dirs) != 0 {
+ directories = execution.ArtifactRequest.Dirs
+ }
+
masks = execution.ArtifactRequest.Masks
}
diff --git a/contrib/executor/jmeterd/pkg/runner/runner_test.go b/contrib/executor/jmeterd/pkg/runner/runner_test.go
index ee75d8d0f30..c45e2dde1e6 100644
--- a/contrib/executor/jmeterd/pkg/runner/runner_test.go
+++ b/contrib/executor/jmeterd/pkg/runner/runner_test.go
@@ -8,13 +8,80 @@ import (
"github.com/kubeshop/testkube/pkg/utils/test"
+ "github.com/golang/mock/gomock"
+ "github.com/pkg/errors"
+
+ "github.com/kubeshop/testkube/pkg/filesystem"
+
"github.com/stretchr/testify/assert"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/envs"
)
-func TestReplaceArgs(t *testing.T) {
+func TestCheckIfTestFileExists(t *testing.T) {
+ t.Parallel()
+
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ testCases := []struct {
+ name string
+ args []string
+ setupMock func(*filesystem.MockFileSystem)
+ expectError bool
+ }{
+ {
+ name: "no arguments",
+ args: []string{},
+ setupMock: func(mockFS *filesystem.MockFileSystem) {},
+ expectError: true,
+ },
+ {
+ name: "file does not exist",
+ args: []string{"-t", "test.txt"},
+ setupMock: func(mockFS *filesystem.MockFileSystem) {
+ mockFS.EXPECT().Stat("test.txt").Return(nil, errors.New("file not found"))
+ },
+ expectError: true,
+ },
+ {
+ name: "file is a directory",
+ args: []string{"-t", "testdir"},
+ setupMock: func(mockFS *filesystem.MockFileSystem) {
+ mockFileInfo := filesystem.MockFileInfo{FIsDir: true}
+ mockFS.EXPECT().Stat("testdir").Return(&mockFileInfo, nil)
+ },
+ expectError: true,
+ },
+ {
+ name: "file exists",
+ args: []string{"-t", "test.txt"},
+ setupMock: func(mockFS *filesystem.MockFileSystem) {
+ mockFileInfo := filesystem.MockFileInfo{FName: "test.txt", FSize: 100}
+ mockFS.EXPECT().Stat("test.txt").Return(&mockFileInfo, nil)
+ },
+ expectError: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ mockFS := filesystem.NewMockFileSystem(mockCtrl)
+ tc.setupMock(mockFS)
+ err := checkIfTestFileExists(mockFS, tc.args, "")
+ if tc.expectError {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
+func TestPrepareArgs(t *testing.T) {
t.Parallel()
tests := []struct {
@@ -57,7 +124,7 @@ func TestReplaceArgs(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
- hasJunit, hasReport := replacePlaceholderArgs(tt.args, tt.path, tt.jtlPath, tt.reportPath, tt.jmeterLogPath)
+ hasJunit, hasReport := prepareArgs(tt.args, tt.path, tt.jtlPath, tt.reportPath, tt.jmeterLogPath)
for i, arg := range tt.args {
assert.Equal(t, tt.expectedArgs[i], arg)
@@ -68,6 +135,106 @@ func TestReplaceArgs(t *testing.T) {
}
}
+func TestRemoveDuplicatedArgs(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ args []string
+ expectedArgs []string
+ }{
+ {
+ name: "Duplicated args",
+ args: []string{"-t", "", "-t", "path", "-l"},
+ expectedArgs: []string{"-t", "path", "-l"},
+ },
+ {
+ name: "Multiple duplicated args",
+ args: []string{"-t", "", "-o", "", "-t", "path", "-o", "output", "-l"},
+ expectedArgs: []string{"-t", "path", "-o", "output", "-l"},
+ },
+ {
+ name: "Non duplicated args",
+ args: []string{"-t", "path", "-l"},
+ expectedArgs: []string{"-t", "path", "-l"},
+ },
+ {
+ name: "Wrong arg order",
+ args: []string{"", "-t", "-t", "path", "-l"},
+ expectedArgs: []string{"-t", "-t", "path", "-l"},
+ },
+ {
+ name: "Missed template arg",
+ args: []string{"-t", "-t", "path", "-l"},
+ expectedArgs: []string{"-t", "-t", "path", "-l"},
+ },
+ {
+ name: "Wrong arg before template",
+ args: []string{"-d", "-o", "", "-t", "-t", "path", "-l"},
+ expectedArgs: []string{"-d", "-o", "-t", "-t", "path", "-l"},
+ },
+ {
+ name: "Duplicated not template args",
+ args: []string{"-t", "first", "-t", "second", "-l"},
+ expectedArgs: []string{"-t", "first", "-t", "second", "-l"},
+ },
+ }
+
+ for i := range tests {
+ tt := tests[i]
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ args := removeDuplicatedArgs(tt.args)
+
+ assert.Equal(t, len(args), len(tt.expectedArgs))
+ for j, arg := range args {
+ assert.Equal(t, tt.expectedArgs[j], arg)
+ }
+ })
+ }
+}
+
+func TestMergeDuplicatedArgs(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ args []string
+ expectedArgs []string
+ }{
+ {
+ name: "Duplicated args",
+ args: []string{"-e", "", "-e"},
+ expectedArgs: []string{"", "-e", "-n"},
+ },
+ {
+ name: "Multiple duplicated args",
+ args: []string{"", "-e", "-e", "-n", "-n", "-l"},
+ expectedArgs: []string{"", "-e", "-l", "-n"},
+ },
+ {
+ name: "Non duplicated args",
+ args: []string{"-e", "-n", "", "-l"},
+ expectedArgs: []string{"-e", "", "-l", "-n"},
+ },
+ }
+
+ for i := range tests {
+ tt := tests[i]
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ args := mergeDuplicatedArgs(tt.args)
+
+ assert.Equal(t, len(args), len(tt.expectedArgs))
+ for j, arg := range args {
+ assert.Equal(t, tt.expectedArgs[j], arg)
+ }
+ })
+ }
+}
+
func TestJMeterDRunner_Local_Integration(t *testing.T) {
test.IntegrationTest(t)
t.Parallel()
@@ -128,7 +295,7 @@ func TestJMeterDRunner_Local_Integration(t *testing.T) {
TestNamespace: "testkube",
Name: "test1",
Command: []string{"jmeter"},
- Args: []string{"-n", "-j", "", "-t", "", "-l", "", "-e", "-o", "", ""},
+ Args: []string{"-n", "-j", "", "-t", "", "-l", "", "-o", "", "-e", ""},
Content: &testkube.TestContent{
Type_: string(testkube.TestContentTypeString),
Data: tt.jmxContent,
@@ -368,54 +535,41 @@ log.info("=================================");
const failureJMX = `
-
- Kubeshop site simple perf test
+
+
false
- true
false
-
-
- PATH
- /pricing
- =
-
-
+
- continue
+ stopthread
false
1
1
1
+ 1668426657000
+ 1668426657000
false
true
-
+
-
-
- false
- $PATH
- =
- true
- PATH
-
-
+
- testkube.io
- 80
- https
+ testkube.kubeshop.io
+
+
- https://testkube.io
+
GET
true
false
@@ -425,18 +579,59 @@ const failureJMX = `
-
-
-
- SOME_NONExisting_String
-
-
- Assertion.response_data
- false
- 16
-
-
-
+
+
+
+ 418
+
+ Assertion.response_code
+ false
+ 8
+
+
+
+
+ false
+
+ saveConfig
+
+
+ true
+ true
+ true
+
+ true
+ true
+ true
+ true
+ false
+ true
+ true
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ 0
+ true
+ true
+
+
+
+
+
+
+ groovy
+
+
+ true
+ println "\nJMeter negative test - failing Thread Group in purpose with System.exit(1)\n";
+System.exit(1);
+
+
diff --git a/contrib/executor/k6/README.md b/contrib/executor/k6/README.md
index 15c9b6be3a7..6c352b38cee 100644
--- a/contrib/executor/k6/README.md
+++ b/contrib/executor/k6/README.md
@@ -60,5 +60,5 @@ For more info go to [main testkube repo](https://github.com/kubeshop/testkube)
![Docker builds](https://img.shields.io/docker/automated/kubeshop/testkube-api-server) ![Code build](https://img.shields.io/github/workflow/status/kubeshop/testkube/Code%20build%20and%20checks) ![Release date](https://img.shields.io/github/release-date/kubeshop/testkube)
-![Twitter](https://img.shields.io/twitter/follow/thekubeshop?style=social) ![Discord](https://img.shields.io/discord/884464549347074049)
- #### [Documentation](https://docs.testkube.io) | [Discord](https://discord.gg/hfq44wtR6Q)
\ No newline at end of file
+![Twitter](https://img.shields.io/twitter/follow/thekubeshop?style=social) ![Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)
+ #### [Documentation](https://docs.testkube.io) | [Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)
\ No newline at end of file
diff --git a/contrib/executor/k6/pkg/runner/runner.go b/contrib/executor/k6/pkg/runner/runner.go
index e80b203bb93..854f91671a0 100644
--- a/contrib/executor/k6/pkg/runner/runner.go
+++ b/contrib/executor/k6/pkg/runner/runner.go
@@ -110,6 +110,7 @@ func (r *K6Runner) Run(ctx context.Context, execution testkube.Execution) (resul
// in case of Git directory we will run k6 here and
// use the last argument as test file
+ changedArgs := false
if execution.Content.Type_ == string(testkube.TestContentTypeGitFile) ||
execution.Content.Type_ == string(testkube.TestContentTypeGitDir) ||
execution.Content.Type_ == string(testkube.TestContentTypeGit) {
@@ -130,7 +131,7 @@ func (r *K6Runner) Run(ctx context.Context, execution testkube.Execution) (resul
if fileInfo.IsDir() {
testPath = filepath.Join(path, args[len(args)-1])
args = append(args[:len(args)-1], args[len(args):]...)
-
+ changedArgs = true
} else {
testPath = path
}
@@ -144,6 +145,7 @@ func (r *K6Runner) Run(ctx context.Context, execution testkube.Execution) (resul
}
}
+ hasRunPath := false
for i := range args {
if args[i] == "" {
args[i] = k6Command
@@ -151,9 +153,14 @@ func (r *K6Runner) Run(ctx context.Context, execution testkube.Execution) (resul
if args[i] == "" {
args[i] = testPath
+ hasRunPath = true
}
}
+ if changedArgs && !hasRunPath {
+ args = append(args, testPath)
+ }
+
for i := range args {
if args[i] == "" {
newArgs := make([]string, len(args)+len(envVars)-1)
diff --git a/contrib/executor/kubepug/README.md b/contrib/executor/kubepug/README.md
index 85d1182ba66..cb74c5fddd4 100644
--- a/contrib/executor/kubepug/README.md
+++ b/contrib/executor/kubepug/README.md
@@ -120,6 +120,6 @@ For more info go to [main Testkube repo](https://github.com/kubeshop/testkube)
![Docker builds](https://img.shields.io/docker/automated/kubeshop/testkube-api-server) ![Code build](https://img.shields.io/github/workflow/status/kubeshop/testkube/Code%20build%20and%20checks) ![Release date](https://img.shields.io/github/release-date/kubeshop/testkube)
-![Twitter](https://img.shields.io/twitter/follow/thekubeshop?style=social) ![Discord](https://img.shields.io/discord/884464549347074049)
+![Twitter](https://img.shields.io/twitter/follow/thekubeshop?style=social) ![Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)
-#### [Documentation](https://docs.testkube.io) | [Discord](https://discord.gg/hfq44wtR6Q)
+#### [Documentation](https://docs.testkube.io) | [Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)
diff --git a/contrib/executor/maven/README.md b/contrib/executor/maven/README.md
index 3d1328f0828..92b0a10691d 100644
--- a/contrib/executor/maven/README.md
+++ b/contrib/executor/maven/README.md
@@ -30,5 +30,5 @@ For more info go to [main testkube repo](https://github.com/kubeshop/testkube)
![Docker builds](https://img.shields.io/docker/automated/kubeshop/testkube-api-server) ![Code build](https://img.shields.io/github/workflow/status/kubeshop/testkube/Code%20build%20and%20checks) ![Release date](https://img.shields.io/github/release-date/kubeshop/testkube)
-![Twitter](https://img.shields.io/twitter/follow/thekubeshop?style=social) ![Discord](https://img.shields.io/discord/884464549347074049)
- #### [Documentation](https://docs.testkube.io) | [Discord](https://discord.gg/hfq44wtR6Q)
+![Twitter](https://img.shields.io/twitter/follow/thekubeshop?style=social) ![Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)
+ #### [Documentation](https://docs.testkube.io) | [Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)
diff --git a/contrib/executor/postman/pkg/runner/newman/newman.go b/contrib/executor/postman/pkg/runner/newman/newman.go
index ba55be878e8..9cee312c887 100644
--- a/contrib/executor/postman/pkg/runner/newman/newman.go
+++ b/contrib/executor/postman/pkg/runner/newman/newman.go
@@ -69,7 +69,7 @@ func (r *NewmanRunner) Run(ctx context.Context, execution testkube.Execution) (r
if fileInfo.IsDir() {
scriptName := execution.Args[len(execution.Args)-1]
if workingDir != "" {
- path = filepath.Join(r.Params.DataDir, "repo")
+ path = ""
if execution.Content != nil && execution.Content.Repository != nil {
scriptName = filepath.Join(execution.Content.Repository.Path, scriptName)
}
diff --git a/contrib/executor/soapui/pkg/runner/runner.go b/contrib/executor/soapui/pkg/runner/runner.go
index f8b625418fb..08edebb6d14 100644
--- a/contrib/executor/soapui/pkg/runner/runner.go
+++ b/contrib/executor/soapui/pkg/runner/runner.go
@@ -72,7 +72,7 @@ func (r *SoapUIRunner) Run(ctx context.Context, execution testkube.Execution) (r
if fileInfo.IsDir() {
scriptName := execution.Args[len(execution.Args)-1]
if workingDir != "" {
- testFile = filepath.Join(r.Params.DataDir, "repo")
+ testFile = ""
if execution.Content != nil && execution.Content.Repository != nil {
scriptName = filepath.Join(execution.Content.Repository.Path, scriptName)
}
diff --git a/contrib/executor/template/README.md b/contrib/executor/template/README.md
index dc5135d4aea..1af1e5ea708 100644
--- a/contrib/executor/template/README.md
+++ b/contrib/executor/template/README.md
@@ -73,5 +73,5 @@ For more info go to [main testkube repo](https://github.com/kubeshop/testkube)
![Docker builds](https://img.shields.io/docker/automated/kubeshop/testkube-api-server) ![Code build](https://img.shields.io/github/workflow/status/kubeshop/testkube/Code%20build%20and%20checks) ![Release date](https://img.shields.io/github/release-date/kubeshop/testkube)
-![Twitter](https://img.shields.io/twitter/follow/thekubeshop?style=social) ![Discord](https://img.shields.io/discord/884464549347074049)
- #### [Documentation](https://docs.testkube.io/openapi) | [Discord](https://discord.gg/hfq44wtR6Q)
\ No newline at end of file
+![Twitter](https://img.shields.io/twitter/follow/thekubeshop?style=social) ![Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)
+ #### [Documentation](https://docs.testkube.io/openapi) | [Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)
\ No newline at end of file
diff --git a/contrib/executor/tracetest/README.md b/contrib/executor/tracetest/README.md
index 9186508d672..14623b38ea9 100644
--- a/contrib/executor/tracetest/README.md
+++ b/contrib/executor/tracetest/README.md
@@ -125,7 +125,7 @@ For more info go to [main testkube repo](https://github.com/kubeshop/testkube)
![Twitter](https://img.shields.io/twitter/follow/thekubeshop?style=social)
-#### [Documentation](https://kubeshop.github.io/testkube) | [Discord](https://discord.gg/hfq44wtR6Q)
+#### [Documentation](https://kubeshop.github.io/testkube) | [Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)
# Tracetest
@@ -133,4 +133,4 @@ For more info go to [main tracetest repo](https://github.com/kubeshop/tracetest)
![Twitter](https://img.shields.io/twitter/follow/tracetest_io?style=social)
-#### [Documentation](https://docs.tracetest.io/) | [Discord](https://discord.gg/6zupCZFQbe)
\ No newline at end of file
+#### [Documentation](https://docs.tracetest.io/) | [Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)
\ No newline at end of file
diff --git a/docs/docs/articles/argocd-integration.md b/docs/docs/articles/argocd-integration.md
index 0cffd2be9c5..6dca98a886a 100644
--- a/docs/docs/articles/argocd-integration.md
+++ b/docs/docs/articles/argocd-integration.md
@@ -219,7 +219,7 @@ spec:
namespace: testkube
```
-Notice that we have defined path `postman-collections` which is the test folder with our Postman collections from the steps earlier. With Testkube you can use multiple test executors like `curl`, for example, so it is convenient to have a folder for each. We have also defined the `.destination.namespace` to be `testkube`, which is where the tests should be deployed in our cluster.
+Notice that we have defined the path `postman-collections` which is the test folder with our Postman collections from the steps earlier. With Testkube you can use multiple test executors like `curl`, for example, so it is convenient to have a folder for each. We have also defined the `.destination.namespace` to be `testkube`, which is where the tests should be deployed in our cluster.
Now let’s create the application with:
@@ -297,12 +297,12 @@ And you will be able to see the results of the execution in the Executions tab a
We now have an automated test deployment and execution pipeline based on GitOps principles!
-### 11. Allow to add ownerReferences to CronJobs metadata for Tests and Test Suites
+### 11. Allow adding ownerReferences to CronJobs metadata for Tests and Test Suites
-You will need to enable helm chart variable `useArgoCDSync = true` in order to make CronJobs created for Tests and Test Suites syncronized in ArgoCD.
+You will need to enable the Helm chart variable `useArgoCDSync = true` in order to make CronJobs created for Tests and Test Suites syncronized in ArgoCD.
## GitOps Takeaways
Once fully realized - using GitOps for testing of Kubernetes applications as described above provides a powerful alternative to a more traditional approach where orchestration is tied to your current CI/CD tooling and not closely aligned with the lifecycle of Kubernetes applications.
-We would love to get your thoughts on the above approach - over-engineering done right? Waste of time? Let us know on [our Discord server](https://discord.com/channels/884464549347074049/885185660808474664)!
+We would love to get your thoughts on the above approach - over-engineering done right? Waste of time? Let us know on [our Slack Channel](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email)!
diff --git a/docs/docs/articles/azure-troubleshooting.md b/docs/docs/articles/azure-troubleshooting.md
new file mode 100644
index 00000000000..f36ab6722fe
--- /dev/null
+++ b/docs/docs/articles/azure-troubleshooting.md
@@ -0,0 +1,50 @@
+# Azure DevOps Troubleshooting
+
+## Testkube CLI and Git Integration issue
+
+When integrating Testkube with Azure DevOps, a common issue that users might encounter involves the --git flags in the Testkube CLI. This problem manifests as the process becoming stuck without displaying any error messages, ultimately leading to a timeout. This document provides a solution to circumvent this issue, ensuring a smoother integration and execution of tests within Azure DevOps pipelines.
+
+To avoid this issue, it is recommended to use the Git CLI directly for cloning the necessary repositories before executing Testkube CLI commands that reference the local copies of the test files or directories. This approach bypasses the complications associated with the --git flags in Testkube CLI within Azure DevOps environments.
+
+### Example Workflow Adjustment
+
+#### Before Adjustment (Issue Prone):
+```yaml
+trigger:
+- main
+
+pool:
+ vmImage: 'ubuntu-latest'
+
+stages:
+- stage: Test
+ jobs:
+ - job: RunTestkube
+ steps:
+ - task: SetupTestkube@1
+ - script: |
+ testkube create test --name test-name --test-content-type git-file --git-uri --git-path test-path
+ testkube run test test-name
+ displayName: Run Testkube Test
+```
+
+#### After Adjustment (Recommended Solution):
+```yaml
+trigger:
+- main
+
+pool:
+ vmImage: 'ubuntu-latest'
+
+stages:
+- stage: Test
+ jobs:
+ - job: RunTestkube
+ steps:
+ - task: SetupTestkube@1
+ - script: |
+ git clone
+ testkube create test --name test-name -f test-path
+ testkube run test test-name
+ displayName: Run Testkube Test
+```
diff --git a/docs/docs/articles/azure.md b/docs/docs/articles/azure.md
new file mode 100644
index 00000000000..98899041359
--- /dev/null
+++ b/docs/docs/articles/azure.md
@@ -0,0 +1,161 @@
+# Testkube Azure DevOps Pipelines
+
+Testkube's integration with Azure DevOps streamlines the installation of Testkube, enabling the execution of any [Testkube CLI](https://docs.testkube.io/cli/testkube) command within Azure DevOps pipelines. This integration can be effortlessly integrated into your Azure DevOps setup, enhancing your continuous integration and delivery processes.
+The Azure DevOps integration offers a versatile solution for managing your pipeline workflows and is compatible with Testkube Pro, Testkube Enterprise, and the open-source Testkube platform. It allows Azure DevOps users to effectively utilize Testkube's capabilities within their CI/CD pipelines, providing a robust and flexible framework for test execution and automation.
+
+### Azure DevOps Extension
+
+Install the Testkube CLI extension using the following url:
+[https://marketplace.visualstudio.com/items?itemName=Testkube.testkubecli](https://marketplace.visualstudio.com/items?itemName=Testkube.testkubecli)
+
+#### Troubleshooting
+For solutions to common issues, such as the `--git` flags causing timeouts, please refer to our [Troubleshooting article](./azure-troubleshooting.md).
+
+## Testkube Pro
+
+### How to configure Testkube CLI action for Testkube Pro and run a test
+
+To use Azure DevOps Pipelines for [Testkube Pro](https://app.testkube.io/), you need to create an [API token](https://docs.testkube.io/testkube-pro/articles/organization-management/#api-tokens).
+Then, pass the **organization** and **environment** IDs, along with the **token** and other parameters specific for your use case.
+
+If a test is already created, you can run it using the command `testkube run test test-name -f`. However, if you need to create a test in this workflow, you need to add a creation command, e.g.: `testkube create test --name test-name --file path_to_file.json`.
+
+You'll need to create a `azure-pipelines.yml`` file. This will include the stages, jobs and tasks necessary to execute the workflow
+
+```yaml
+trigger:
+- main
+
+pool:
+ vmImage: 'ubuntu-latest'
+
+stages:
+- stage: Test
+ jobs:
+ - job: RunTestkube
+ steps:
+ - task: SetupTestkube@1
+ inputs:
+ organization: '$(TK_ORG_ID)'
+ environment: '$(TK_ENV_ID)'
+ token: '$(TK_API_TOKEN)'
+ - script: testkube run test test-name -f
+ displayName: Run Testkube Test
+```
+
+## Testkube OSS
+
+### How to configure the Testkube CLI action for TK OSS and run a test
+
+To connect to the self-hosted instance, you need to have **kubectl** configured for accessing your Kubernetes cluster, and pass an optional namespace, if Testkube is not deployed in the default **testkube** namespace.
+
+If a test is already created, you can run it using the command `testkube run test test-name -f` . However, if you need to create a test in this workflow, please add a creation command, e.g.: `testkube create test --name test-name --file path_to_file.json`.
+
+```yaml
+trigger:
+- main
+
+pool:
+ vmImage: 'ubuntu-latest'
+
+stages:
+- stage: Test
+ jobs:
+ - job: RunTestkube
+ steps:
+ - task: SetupTestkube@1
+ inputs:
+ namespace: 'custom-testkube-namespace'
+ url: 'custom-testkube-url'
+ - script: testkube run test test-name -f
+ displayName: 'Run Testkube Test'
+```
+
+The steps to connect to your Kubernetes cluster differ for each provider. You should check the docs of your Cloud provider for how to connect to the Kubernetes cluster from Azure DevOps Pipelines
+
+### How to configure Testkube CLI action for TK OSS and run a test
+
+This workflow establishes a connection to the EKS cluster and creates and runs a test using TK CLI. In this example we also use variables not
+ to reveal sensitive data. Please make sure that the following points are satisfied:
+- The **AWS_ACCESS_KEY_ID**, **AWS_SECRET_ACCESS_KEY** secrets should contain your AWS IAM keys with proper permissions to connect to the EKS cluster.
+- The **AWS_REGION** secret should contain the AWS region where EKS is.
+- Tke **EKS_CLUSTER_NAME** secret points to the name of the EKS cluster you want to connect.
+
+```yaml
+trigger:
+- main
+
+pool:
+ vmImage: 'ubuntu-latest'
+
+stages:
+- stage: Test
+ jobs:
+ - job: SetupAndRunTestkube
+ steps:
+ - script: |
+ # Setting up AWS credentials
+ aws configure set aws_access_key_id $(AWS_ACCESS_KEY_ID)
+ aws configure set aws_secret_access_key $(AWS_SECRET_ACCESS_KEY)
+ aws configure set region $(AWS_REGION)
+
+ # Updating kubeconfig for EKS
+ aws eks update-kubeconfig --name $(EKS_CLUSTER_NAME) --region $(AWS_REGION)
+ displayName: 'Setup AWS and Testkube'
+
+ - task: SetupTestkube@1
+ inputs:
+ organization: '$(TK_ORG_ID)'
+ environment: '$(TK_ENV_ID)'
+ token: '$(TK_API_TOKEN)'
+
+ - script: testkube run test test-name -f
+ displayName: Run Testkube Test
+
+```
+
+### How to connect to GKE (Google Kubernetes Engine) cluster and run a test
+
+This example connects to a k8s cluster in Google Cloud and creates and runs a test using Testkube Azure DevOps pipeline. Please make sure that the following points are satisfied:
+- The **GKE Sevice Account** should have already been created in Google Cloud and added to pipeline variables along with **GKE_PROJECT** value.
+- The **GKE_CLUSTER_NAME** and **GKE_ZONE** can be added as environmental variables in the workflow.
+
+```yaml
+trigger:
+- main
+
+pool:
+ vmImage: 'ubuntu-latest'
+
+stages:
+- stage: SetupGKE
+ jobs:
+ - job: SetupGCloudAndKubectl
+ steps:
+ - task: DownloadSecureFile@1
+ name: gkeServiceAccount
+ inputs:
+ secureFile: 'gke-service-account-key.json'
+ - task: GoogleCloudSdkInstaller@0
+ inputs:
+ version: 'latest'
+ - script: |
+ gcloud auth activate-service-account --key-file $(gkeServiceAccount.secureFilePath)
+ gcloud config set project $(GKE_PROJECT)
+ gcloud container clusters get-credentials $(GKE_CLUSTER_NAME) --zone $(GKE_ZONE)
+ displayName: 'Setup GKE'
+
+- stage: Test
+ dependsOn: SetupGKE
+ jobs:
+ - job: RunTestkube
+ steps:
+ - task: SetupTestkube@1
+ inputs:
+ organization: '$(TK_ORG_ID)'
+ environment: '$(TK_ENV_ID)'
+ token: '$(TK_API_TOKEN)'
+ - script: |
+ testkube run test test-name -f
+ displayName: 'Run Testkube Test'
+```
diff --git a/docs/docs/articles/cicd-overview.md b/docs/docs/articles/cicd-overview.md
index 32aa3d1a3aa..76dfa386579 100644
--- a/docs/docs/articles/cicd-overview.md
+++ b/docs/docs/articles/cicd-overview.md
@@ -8,6 +8,10 @@ We have different tutorials for the options of being CI driven or using GitOps a
- [Github Actions - running Testkube CLI commands with setup-testkube-action](./github-actions.md)
- [Testkube Docker CLI](./testkube-cli-docker.md)
- [Gitlab CI](./gitlab.md)
+- [Jenkins Pipelines](./jenkins.md)
+- [Jenkins UI](./jenkins-ui.md)
+- [CircleCI](./circleci.md)
+- [Azure DevOps](./azure.md)
- [GitOps Testing](./gitops-overview.md)
- [Flux](./flux-integration.md)
- [ArgoCD](./argocd-integration.md)
diff --git a/docs/docs/articles/circleci.md b/docs/docs/articles/circleci.md
new file mode 100644
index 00000000000..02236401c71
--- /dev/null
+++ b/docs/docs/articles/circleci.md
@@ -0,0 +1,197 @@
+# Testkube CircleCI
+
+The Testkube CircleCI integration facilitates the installation of Testkube and allows the execution of any [Testkube CLI](https://docs.testkube.io/cli/testkube) command within a CircleCI pipeline. This integration can be seamlessly incorporated into your CircleCI repositories to enhance your CI/CD workflows.
+The integration offers a versatile approach to align with your pipeline requirements and is compatible with Testkube Pro, Testkube Enterprise, and the open-source Testkube platform. It enables CircleCI users to leverage the powerful features of Testkube directly within their CI/CD pipelines, ensuring efficient and flexible test execution.
+
+## Testkube Pro
+
+### How to configure Testkube CLI action for Testkube Pro and run a test
+
+To use CircleCI for [Testkube Pro](https://app.testkube.io/), you need to create an [API token](https://docs.testkube.io/testkube-pro/articles/organization-management/#api-tokens).
+Then, pass the **organization** and **environment** IDs, along with the **token** and other parameters specific for your use case.
+
+If a test is already created, you can run it using the command `testkube run test test-name -f` . However, if you need to create a test in this workflow, please add a creation command, e.g.: `testkube create test --name test-name --file path_to_file.json`.
+
+```yaml
+version: 2.1
+
+jobs:
+ run-tests:
+ docker:
+ - image: kubeshop/testkube-cli
+ working_directory: /.testkube
+ environment:
+ TESTKUBE_API_KEY: tkcapi_0123456789abcdef0123456789abcd
+ TESTKUBE_ORG_ID: tkcorg_0123456789abcdef
+ TESTKUBE_ENV_ID: tkcenv_fedcba9876543210
+ steps:
+ - run:
+ name: "Set Testkube Context"
+ command: "testkube set context --api-key $TESTKUBE_API_KEY --org $TESTKUBE_ORG_ID --env $TESTKUBE_ENV_ID --cloud-root-domain testkube.dev"
+ - run:
+ name: "Trigger testkube test"
+ command: "testkube run test test-name -f"
+
+workflows:
+ run-tests-workflow:
+ jobs:
+ - run-tests
+```
+
+It is recommended that sensitive values should never be stored as plaintext in workflow files, but rather as [project variables](https://circleci.com/docs/set-environment-variable/#set-an-environment-variable-in-a-project). Secrets can be configured at the organization or project level and allow you to store sensitive information in CircleCI.
+
+```yaml
+version: 2.1
+
+jobs:
+ run-tests:
+ docker:
+ - image: kubeshop/testkube-cli
+ working_directory: /.testkube
+ steps:
+ - run:
+ name: "Set Testkube Context"
+ command: "testkube set context --api-key $TESTKUBE_API_KEY --org $TESTKUBE_ORG_ID --env $TESTKUBE_ENV_ID --cloud-root-domain testkube.dev"
+ - run:
+ name: "Trigger testkube test"
+ command: "testkube run test test-name -f"
+
+workflows:
+ run-tests-workflow:
+ jobs:
+ - run-tests
+```
+## Testkube OSS
+
+### How to configure Testkube CLI action for TK OSS and run a test
+
+To connect to the self-hosted instance, you need to have **kubectl** configured for accessing your Kubernetes cluster and pass an optional namespace, if Testkube is not deployed in the default **testkube** namespace.
+
+If a test is already created, you can run it using the command `testkube run test test-name -f` . However, if you need to create a test in this workflow, please add a creation command, e.g.: `testkube create test --name test-name --file path_to_file.json`.
+
+In order to connect to your own cluster, you can put your kubeconfig file into CircleCI variable named KUBECONFIGFILE.
+
+```yaml
+version: 2.1
+
+jobs:
+ run-tests:
+ docker:
+ - image: kubeshop/testkube-cli
+ working_directory: /.testkube
+ steps:
+ - run:
+ name: "Export kubeconfig"
+ command: |
+ echo $KUBECONFIGFILE > /.testkube/tmp/kubeconfig/config
+ export KUBECONFIG=/.testkube/tmp/kubeconfig/config
+ - run:
+ name: "Set Testkube Context"
+ command: "testkube set context --api-key $TESTKUBE_API_KEY --org $TESTKUBE_ORG_ID --env $TESTKUBE_ENV_ID --cloud-root-domain testkube.dev"
+ - run:
+ name: "Trigger testkube test"
+ command: "testkube run test test-name -f"
+
+workflows:
+ run-tests-workflow:
+ jobs:
+ - run-tests
+```
+
+The steps to connect to your Kubernetes cluster differ for each provider. You should check the docs of your Cloud provider for how to connect to the Kubernetes cluster from CircleCI.
+
+### How to configure Testkube CLI action for TK OSS and run a test
+
+This workflow establishes a connection to the EKS cluster and creates and runs a test using TK CLI. In this example we also use CircleCI variables not to reveal sensitive data. Please make sure that the following points are satisfied:
+- The **_AwsAccessKeyId_**, **_AwsSecretAccessKeyId_** secrets should contain your AWS IAM keys with proper permissions to connect to EKS cluster.
+- The **_AwsRegion_** secret should contain the AWS region where EKS is.
+- Tke **EksClusterName** secret points to the name of the EKS cluster you want to connect.
+
+```yaml
+version: 2.1
+
+jobs:
+ setup-aws:
+ docker:
+ - image: amazon/aws-cli
+ steps:
+ - run:
+ name: "Configure AWS CLI"
+ command: |
+ mkdir -p /.testkube/tmp/kubeconfig/config
+ aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
+ aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY
+ aws configure set region $AWS_REGION
+ aws eks update-kubeconfig --name $EKS_CLUSTER_NAME --region $AWS_REGION --kubeconfig /.testkube/tmp/kubeconfig/config
+
+ run-testkube-on-aws:
+ docker:
+ - image: kubeshop/testkube-cli
+ working_directory: /.testkube
+ environment:
+ NAMESPACE: custom-testkube
+ steps:
+ - run:
+ name: "Run Testkube Test on EKS"
+ command: |
+ export KUBECONFIG=/.testkube/tmp/kubeconfig/config
+ testkube set context --kubeconfig --namespace $NAMESPACE
+ echo "Running Testkube test..."
+ testkube run test test-name -f
+
+workflows:
+ aws-testkube-workflow:
+ jobs:
+ - setup-aws
+ - run-testkube-on-aws:
+ requires:
+ - setup-aws
+```
+
+### How to connect to GKE (Google Kubernetes Engine) cluster and run a test
+
+This example connects to a k8s cluster in Google Cloud then creates and runs a test using Testkube CircleCI. Please make sure that the following points are satisfied:
+- The **_GKE Sevice Account_** should already be created in Google Cloud and added to CircleCI variables along with **_GKE Project_** value.
+- The **_GKE Cluster Name_** and **_GKE Zone_** can be added as environment variables in the workflow.
+
+
+```yaml
+version: 2.1
+
+jobs:
+ setup-gcp:
+ docker:
+ - image: google/cloud-sdk:latest
+ working_directory: /.testkube
+ steps:
+ - run:
+ name: "Setup GCP"
+ command: |
+ mkdir -p /.testkube/tmp/kubeconfig/config
+ export KUBECONFIG=$CI_PROJECT_DIR/tmp/kubeconfig/config
+ echo $GKE_SA_KEY | base64 -d > gke-sa-key.json
+ gcloud auth activate-service-account --key-file=gke-sa-key.json
+ gcloud config set project $GKE_PROJECT
+ gcloud --quiet auth configure-docker
+ gcloud container clusters get-credentials $GKE_CLUSTER_NAME --zone $GKE_ZONE
+
+ run-testkube-on-gcp:
+ docker:
+ - image: kubeshop/testkube-cli
+ working_directory: /.testkube
+ steps:
+ - run:
+ name: "Run Testkube Test on GKE"
+ command: |
+ export KUBECONFIG=/.testkube/tmp/kubeconfig/config
+ testkube set context --kubeconfig --namespace $NAMESPACE
+ testkube run test test-name -f
+
+workflows:
+ gke-testkube-workflow:
+ jobs:
+ - setup-gcp
+ - run-testkube-on-gcp:
+ requires:
+ - setup-gcp
+```
diff --git a/docs/docs/articles/common-issues.md b/docs/docs/articles/common-issues.md
index 3bf32777f85..2934b790069 100644
--- a/docs/docs/articles/common-issues.md
+++ b/docs/docs/articles/common-issues.md
@@ -83,11 +83,11 @@ Please stop the application that listens on 8080, 8088 ports.
## If You're Still Having Issues
-If these guides do not solve the issue that you encountered or you have other questions or comments, please contact us on [Discord](https://discord.com/invite/6zupCZFQbe).
+If these guides do not solve the issue that you encountered or you have other questions or comments, please contact us on [Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email).
## Other Installation Methods
-### Installation on OpenShift deployed on GCP
+### Installation on OpenShift Deployed on GCP
To install Testkube you need an empty OpenShift cluster. Once the cluster is up and running update `values.yaml` file, including the configuration below.
diff --git a/docs/docs/articles/creating-test-suites.md b/docs/docs/articles/creating-test-suites.md
index 672aaaca33e..0f127a65d24 100644
--- a/docs/docs/articles/creating-test-suites.md
+++ b/docs/docs/articles/creating-test-suites.md
@@ -107,3 +107,210 @@ spec:
```
Your `Test Suite` is defined and you can start running testing workflows.
+
+## Test Suite Steps
+
+Test Suite Steps are the individual components or actions that make up a Test Suite. They are typically a sequence of tests that are run in a specific order. There are two types of Test Suite Steps:
+
+Tests: These are the actual tests to be run. They could be unit tests, integration tests, functional tests, etc., depending on the context.
+
+Delays: These are time delays inserted between tests. They are used to wait for a certain period of time before proceeding to the next test. This can be useful in situations where you need to wait for some process to complete or some condition to be met before proceeding.
+
+Similar to running a Test, running a Test Suite Step based on a test allows for specific execution request parameters to be overwritten. Step level parameters overwrite Test Suite level parameters, which in turn overwrite Test level parameters. The Step level parameters are configurable only via CRDs at the moment.
+
+For details on which parameters are available in the CRDs, please consult the table below:
+
+| Parameter | Test | Test Suite | Test Step |
+| ---------------------------------- | ---- | ---------- | --------- |
+| name | ✓ | ✓ | |
+| testSuiteName | ✓ | | |
+| number | ✓ | | |
+| executionLabels | ✓ | ✓ | ✓ |
+| namespace | ✓ | ✓ | |
+| variablesFile | ✓ | | |
+| isVariablesFileUploaded | ✓ | | |
+| variables | ✓ | ✓ | |
+| testSecretUUID | ✓ | | |
+| testSuiteSecretUUID | ✓ | | |
+| args | ✓ | | ✓ |
+| argsMode | ✓ | | ✓ |
+| command | ✓ | | ✓ |
+| image | ✓ | | |
+| imagePullSecrets | ✓ | | |
+| sync | ✓ | ✓ | |
+| httpProxy | ✓ | ✓ | ✓ |
+| httpsProxy | ✓ | ✓ | ✓ |
+| negativeTest | ✓ | | |
+| activeDeadlineSeconds | ✓ | | |
+| artifactRequest | ✓ | | |
+| jobTemplate | ✓ | ✓ | ✓ |
+| jobTemplateReference | ✓ | ✓ | ✓ |
+| cronJobTemplate | ✓ | ✓ | ✓ |
+| cronJobTemplateReference | ✓ | ✓ | ✓ |
+| preRunScript | ✓ | | |
+| postRunScript | ✓ | | |
+| executePostRunScriptBeforeScraping | ✓ | | |
+| sourceScripts | ✓ | | |
+| scraperTemplate | ✓ | ✓ | ✓ |
+| scraperTemplateReference | ✓ | ✓ | ✓ |
+| pvcTemplate | ✓ | ✓ | ✓ |
+| pvcTemplateReference | ✓ | ✓ | ✓ |
+| envConfigMaps | ✓ | | |
+| envSecrets | ✓ | | |
+| runningContext | ✓ | ✓ | ✓ |
+| slavePodRequest | ✓ | | |
+| secretUUID | | ✓ | |
+| labels | | ✓ | |
+| timeout | | ✓ | |
+
+Similar to Tests and Test Suites, Test Suite Steps can also have a field of type `executionRequest` like in the example below:
+
+```yaml
+apiVersion: tests.testkube.io/v3
+kind: TestSuite
+metadata:
+ name: jmeter-special-cases
+ namespace: testkube
+ labels:
+ core-tests: special-cases
+spec:
+ description: "jmeter and jmeterd executor - special-cases"
+ steps:
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-custom-envs-replication
+ executionRequest:
+ args: ["-d", "-s"]
+ ...
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-env-value-in-args
+```
+
+The `Definition` section of each Test Suite in the Testkube UI offers the opportunity to directly edit the Test Suite CRDs. Besides that, consider also using `kubectl edit testsuite/jmeter-special-cases -n testkube` on the command line.
+
+### Usage Example
+
+An example of use case for test suite step parameters would be running the same K6 load test with different arguments and memory and CPU requirements.
+
+1. Create and Configure the Test
+
+Let's say our test CRD stored in the file `k6-test.yaml` looks the following:
+
+```yaml
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: k6-test-parallel
+ labels:
+ core-tests: executors
+ namespace: testkube
+spec:
+ type: k6/script
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/k6/executor-tests/
+ executionRequest:
+ args:
+ - k6-smoke-test-without-envs.js
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 128m\n"
+ activeDeadlineSeconds: 180
+```
+
+We can apply this from the command line using:
+
+```bash
+kubectl apply -f k6-test.yaml
+```
+
+2. Run the Test
+
+To run this test, execute:
+
+```bash
+testkube run test k6-test-parallel
+```
+
+A new Testkube execution will be created. If you investigate the new job assigned to this execution, you will see the memory and cpu limit specified in the job template was set. Checking the arguments from the `executionRequest` is also possible with:
+
+```bash
+kubectl testkube get execution k6-test-parallel-1
+```
+
+3. Create and Configure the Test Suite
+
+We are content with the test created, but we need to make sure our application works with different kinds of loads. We could create a new Test with different parameters, but that would come with the overhead of having to manage and sync two instances of the same test. Creating a test suite makes test orchestration a more robust operation.
+
+We have the following `k6-test-suite.yaml` file:
+
+```yaml
+apiVersion: tests.testkube.io/v3
+kind: TestSuite
+metadata:
+ name: k6-parallel
+ namespace: testkube
+spec:
+ description: "k6 parallel testsuite"
+ steps:
+ - stopOnFailure: false
+ execute:
+ - test: k6-test-parallel
+ executionRequest:
+ argsMode: override
+ args:
+ - -vu
+ - "1"
+ - k6-smoke-test-without-envs.js
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 64Mi\n cpu: 128m\n"
+ - test: k6-test-parallel
+ executionRequest:
+ argsMode: override
+ args:
+ - -vu
+ - "2"
+ - k6-smoke-test-without-envs.js
+```
+
+Note that there are two steps in there running the same test. The difference is in their `executionRequest`. The first step is setting the number of virtual users to one and updating the jobTemplate to use a different memory requirement. The second test updates the VUs to 2.
+
+Create the test suite with the command:
+
+```bash
+kubectl apply -f k6-test-suite.yaml
+```
+
+4. Run the Test Suite
+
+Run the test suite with:
+
+```bash
+kubectl testkube run testsuite k6-parallel
+```
+
+The output of both of the test runs can be examined with:
+
+```bash
+testkube get execution k6-parallel-k6-test-parallel-2
+
+testkube get execution k6-parallel-k6-test-parallel-3
+```
+
+The logs show the exact commands:
+
+```bash
+...
+🔬 Executing in directory /data/repo:
+ $ k6 run test/k6/executor-tests/k6-smoke-test-without-envs.js -vu 1
+...
+🔬 Executing in directory /data/repo:
+ $ k6 run test/k6/executor-tests/k6-smoke-test-without-envs.js -vu 2
+...
+```
+
+The job template configuration will be visible on the job level, running `kubectl get jobs -n testkube` and `kubectl get job ${job_id} -o yaml -n testkube` should be enough to check the settings.
+
+Now we know how to increase the flexibility, reusability and scalability of your tests using test suites. By setting parameters on test suite step levels, we are making our testing automation more robust and easier to manage.
diff --git a/docs/docs/articles/creating-tests.md b/docs/docs/articles/creating-tests.md
index c2343e43de8..f863f9f14bd 100644
--- a/docs/docs/articles/creating-tests.md
+++ b/docs/docs/articles/creating-tests.md
@@ -353,7 +353,7 @@ There are many differences between `--variables-file` and `--copy-files`. The fo
### Redefining the Prebuilt Executor Command and Arguments
-Each of Testkube Prebuilt executors has a default command and arguments it uses to execute the test. They are provided as a part of Executor CRD and can be either overidden or appended during test creation or execution, for example:
+Each of Testkube Prebuilt executors has a default command and arguments it uses to execute the test. They are provided as a part of Executor CRD and can be either overridden, replaced, or appended during test creation or execution, for example:
```sh
testkube create test --name maven-example-test --git-uri https://github.com/kubeshop/testkube-executor-maven.git --git-path examples/hello-maven --type maven/test --git-branch main --command "mvn" --args-mode "override" --executor-args="--settings -Duser.home "
@@ -381,13 +381,13 @@ There are two modes to pass arguments to the executor:
```sh
$ testkube create test --help
...
- --args-mode string usage mode for arguments. one of append|override (default "append")
+ --args-mode string usage mode for arguments. one of append|override|replace (default "append")
...
```
By default, `--args-mode` is set to `append`, which means that the default list will be kept, and whatever is set in `--executor-args` will be added to the end.
-The `override` mode will ignore the default arguments and use only what is set in `--executor-args`. If there are default values in between chevrons (`<>`), they can be reused in `--executor-args`.
+The `override` or `replace` mode will ignore the default arguments and use only what is set in `--executor-args`. If there are default values in between chevrons (`<>`), they can be reused in `--executor-args`.
When using `--args-mode` with `testkube run test ...` pay attention to set the arguments via the `--args` flag, not `--executor-args`.
@@ -517,6 +517,8 @@ Provide the script when you create or run the test using `--prerun-script` and `
testkube create test --file test/postman/LocalHealth.postman_collection.json --name script-test --type postman/collection --prerun-script pre_script.sh --postrun-script post_script.sh --secret-env SSL_CERT=your-k8s-secret
```
+For container executors you can define `--source-scripts` flag in order to run both scripts using `source` command in the same shell.
+
### Adjusting Scraping Parameters
For any executor type you can specify additional scraping parameters using CLI or CRD definition. For example, below we request to scrape report directories, use a custom bucket to store test artifacts and ask to avoid using separate artifact folders for each test execution
@@ -599,6 +601,43 @@ parameters when you create or run the test using the `--variable-configmap` and
testkube create test --file test/postman/LocalHealth.postman_collection.json --name var-test --type postman/collection --variable-configmap your_configmap --variable-secret your_secret
```
+### Run the Test in a Different Execution Namespace
+
+When you need to run the test in a namespace different from the Testkube installation one, you can use a special Test CRD field `executionNamespace`, for example:
+
+```yaml
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeter-smoke-test
+ namespace: testkube
+spec:
+ type: jmeter/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests/jmeter-executor-smoke.jmx
+ executionRequest:
+ executionNamespace: default
+```
+
+You need to define execution namespaces in your helm chart values. It's possible to generate all required RBAC or just manually supply them.
+
+```yaml
+ executionNamespaces: []
+ # -- Namespace for test execution
+ # - namespace: default
+ # -- Whether to generate RBAC for testkube api server or use manually provided
+ # generateAPIServerRBAC: true
+ # -- Job service account name for test jobs
+ # jobServiceAccountName: tests-job-default
+ # -- Whether to generate RBAC for test job or use manually provided
+ # generateTestJobRBAC: true
+```
+
## Summary
Tests are the main abstractions over test suites in Testkube, they can be created with different sources and used by executors to run on top of a particular test framework.
diff --git a/docs/docs/articles/deploying-in-aws.md b/docs/docs/articles/deploying-in-aws.md
index bf07bd96635..eceedeefa7e 100644
--- a/docs/docs/articles/deploying-in-aws.md
+++ b/docs/docs/articles/deploying-in-aws.md
@@ -49,7 +49,7 @@ uiIngress:
alb.ingress.kubernetes.io/healthcheck-port: "8088"
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:us-east-1:*******:certificate/*****"
- path: /results/v1
+ path: /v1
hosts:
- test-api.aws.testkube.io
```
@@ -77,7 +77,7 @@ path: /
:::caution
-Do not forget to add `apiServerEndpoint` to the `values.yaml` for `testkube-dashboard`, e.g.: `apiServerEndpoint: "test-api.aws.testkube.io/results/v1"`.
+Do not forget to add `apiServerEndpoint` to the `values.yaml` for `testkube-dashboard`, e.g.: `apiServerEndpoint: "test-api.aws.testkube.io/v1"`.
:::
@@ -134,7 +134,7 @@ spec:
- host: test-api.aws.testkube.io
http:
paths:
- - path: /results/v1
+ - path: /v1
pathType: Prefix
backend:
service:
@@ -169,7 +169,7 @@ service:
:::caution
-Do not forget to add `apiServerEndpoint` to the values.yaml for `testkube-dashboard`, e.g.: `apiServerEndpoint: "test-api.aws.testkube.io/results/v1"`.
+Do not forget to add `apiServerEndpoint` to the values.yaml for `testkube-dashboard`, e.g.: `apiServerEndpoint: "test-api.aws.testkube.io/v1"`.
:::
@@ -250,4 +250,4 @@ data "aws_iam_policy_document" "testkube" {
With just a few changes you can deploy Testkube into an EKS cluster and expose it to the outside world while all the necessary resources are created automatically.
-If you have any questions you can [join our Discord community](https://discord.com/invite/6zupCZFQbe) or, if you have any ideas for other useful features, you can create feature requests at our [GitHub Issues](https://github.com/kubeshop/testkube) page.
+If you have any questions you can [join our Slack Channel](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email) or, if you have any ideas for other useful features, you can create feature requests at our [GitHub Issues](https://github.com/kubeshop/testkube) page.
diff --git a/docs/docs/articles/getting-started.md b/docs/docs/articles/getting-started.md
index 2e57896ed44..f05c2869b22 100644
--- a/docs/docs/articles/getting-started.md
+++ b/docs/docs/articles/getting-started.md
@@ -51,7 +51,7 @@ By default, Testkube is installed in the `testkube` namespace.
## Need Help?
-- Join our community on [Discord](https://discord.com/invite/6zupCZFQbe).
+- Join our community on [Slack](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email).
- [Schedule a call](https://calendly.com/bryan-3pu/support-product-feedback-call?month=2023-10) with one of our experts.
- Check out our guides.
- [Integrating Testkube with your CI/CD](https://docs.testkube.io/articles/cicd-overview/).
diff --git a/docs/docs/articles/helm-chart.md b/docs/docs/articles/helm-chart.md
index a441a4f1ae6..cc8f6e27e5c 100644
--- a/docs/docs/articles/helm-chart.md
+++ b/docs/docs/articles/helm-chart.md
@@ -118,7 +118,8 @@ The following Helm defaults are used in the `testkube` chart:
| testkube-api.storage.compressArtifacts | yes | true |
| testkube-api.enableSecretsEndpoint | yes | false |
| testkube-api.disableMongoMigrations | yes | false |
-| testkube-api.enabledExecutors | no | "" |
+| testkube-api.enabledExecutors | yes | "" |
+| testkube-api.disableSecretCreation | yes | false |
>For more configuration parameters of a `MongoDB` chart please visit:
diff --git a/docs/docs/articles/jenkins-ui.md b/docs/docs/articles/jenkins-ui.md
new file mode 100644
index 00000000000..58c89c594d7
--- /dev/null
+++ b/docs/docs/articles/jenkins-ui.md
@@ -0,0 +1,33 @@
+# Testkube Jenkins UI
+
+The Testkube Jenkins integration streamlines the installation of Testkube, enabling the execution of any [Testkube CLI](https://docs.testkube.io/cli/testkube) command within Jenkins Pipelines or Freestyle Projects.
+
+If you're looking to use Pipelines and Groovy scripts, then look at examples from [Testkube Jenkins Pipelines](./jenkins.md).
+
+### Testkube CLI Jenkins Plugin
+
+Install the Testkube CLI plugin by searching for it in the "Available Plugins" section on Jenkins Plugins, or using the following url:
+[https://plugins.jenkins.io/testkube-cli](https://plugins.jenkins.io/testkube-cli)
+
+## Testkube Pro
+
+To use Jenkins CI/CD for [Testkube Pro](https://app.testkube.io/), you need to create an [API token](https://docs.testkube.io/testkube-pro/articles/organization-management/#api-tokens).
+
+
+### How to set up a Freestyle Project to run tests on Testkube Pro
+
+1. Create a new Freestyle Project.
+2. In General settings, configure the environment variables:
+ - TK_ORG
+ - TK_ENV
+ - TK_API_TOKEN
+
+![jenkins environment variables configuration](../img/jenkins-environment.png)
+
+3. Click on "Add Build Step" and select "Testkube Setup".
+![jenkins testkube setup build step](../img/jenkins-build-step.png)
+
+4. Specify a Testkube CLI version or leave it empty to use the latest version.
+
+5. Add a new "Execute Shell" Build Step and run one or multiple Testkube CLI commands.
+![jenkins execute shell](../img/jenkins-execute-shell.png)
diff --git a/docs/docs/articles/jenkins.md b/docs/docs/articles/jenkins.md
index 19ef0723a7b..3b6861e1e96 100644
--- a/docs/docs/articles/jenkins.md
+++ b/docs/docs/articles/jenkins.md
@@ -1,8 +1,13 @@
-# Testkube Jenkins
+# Testkube Jenkins Pipelines
The Testkube Jenkins integration streamlines the installation of Testkube, enabling the execution of any [Testkube CLI](https://docs.testkube.io/cli/testkube) command within Jenkins pipelines. This integration can be effortlessly integrated into your Jenkins setup, enhancing your continuous integration and delivery processes.
This Jenkins integration offers a versatile solution for managing your pipeline workflows and is compatible with Testkube Pro, Testkube Enterprise, and the open-source Testkube platform. It allows Jenkins users to effectively utilize Testkube's capabilities within their CI/CD pipelines, providing a robust and flexible framework for test execution and automation.
+### Testkube CLI Jenkins Plugin
+
+Install the Testkube CLI plugin by searching it in the "Available Plugins" section on Jenkins Plugins, or using the following url:
+[https://plugins.jenkins.io/testkube-cli](https://plugins.jenkins.io/testkube-cli)
+
## Testkube Pro
### How to configure Testkube CLI action for Testkube Pro and run a test
@@ -12,39 +17,31 @@ Then, pass the **organization** and **environment** IDs, along with the **token*
If a test is already created, you can run it using the command `testkube run test test-name -f` . However, if you need to create a test in this workflow, please add a creation command, e.g.: `testkube create test --name test-name --file path_to_file.json`.
-you'll need to create a Jenkinsfile. This Jenkinsfile should define the stages and steps necessary to execute the workflow
+You'll need to create a Jenkinsfile. This Jenkinsfile should define the stages and steps necessary to execute the workflow
```groovy
pipeline {
agent any
+ environment {
+ TK_ORG = credentials("TK_ORG")
+ TK_ENV = credentials("TK_ENV")
+ TK_API_TOKEN = credentials("TK_API_TOKEN")
+ }
stages {
- stage('Setup Testkube') {
+ stage('Example') {
steps {
script {
- // Retrieve credentials
- def apiKey = credentials('TESTKUBE_API_KEY')
- def orgId = credentials('TESTKUBE_ORG_ID')
- def envId = credentials('TESTKUBE_ENV_ID')
-
- // Install Testkube
- sh 'curl -sSLf https://get.testkube.io | sh'
-
- // Initialize Testkube
- sh "testkube set context --api-key ${apiKey} --org ${orgId} --env ${envId}"
+ // Setup the Testkube CLI
+ setupTestkube()
+ // Run testkube commands
+ sh 'testkube run test your-test'
+ sh 'testkube run testsuite your-test-suite --some-arg --other-arg'
}
}
}
-
- stage('Run Testkube Test') {
- steps {
- // Run a Testkube test
- sh 'testkube run test test-name -f'
- }
- }
}
}
-
```
## Testkube OSS
@@ -61,28 +58,18 @@ you'll need to create a Jenkinsfile. This Jenkinsfile should define the stages a
pipeline {
agent any
+ environment {
+ TK_NAMESPACE = 'custom-testkube-namespace'
+ }
stages {
- stage('Setup Testkube') {
+ stage('Example') {
steps {
script {
- // Retrieve credentials
- def namespace='custom-testkube'
-
- // Install Testkube
- sh 'curl -sSLf https://get.testkube.io | sh'
-
- // Initialize Testkube
- sh "testkube set context --kubeconfig --namespace ${namespace}"
+ setupTestkube()
+ sh 'testkube run test your-test'
}
}
}
-
- stage('Run Testkube Test') {
- steps {
- // Run a Testkube test
- sh 'testkube run test test-name -f'
- }
- }
}
}
```
@@ -104,6 +91,11 @@ you'll need to create a Jenkinsfile. This Jenkinsfile should define the stages a
pipeline {
agent any
+ environment {
+ TK_ORG = credentials("TK_ORG")
+ TK_ENV = credentials("TK_ENV")
+ TK_API_TOKEN = credentials("TK_API_TOKEN")
+ }
stages {
stage('Setup Testkube') {
steps {
@@ -120,17 +112,8 @@ pipeline {
sh 'aws eks update-kubeconfig --name $EKS_CLUSTER_NAME --region $AWS_REGION'
}
- // Installing Testkube
- sh 'curl -sSLf https://get.testkube.io | sh'
-
- // Initializing Testkube
- withCredentials([
- string(credentialsId: 'TestkubeApiKey', variable: 'TESTKUBE_API_KEY'),
- string(credentialsId: 'TestkubeOrgId', variable: 'TESTKUBE_ORG_ID'),
- string(credentialsId: 'TestkubeEnvId', variable: 'TESTKUBE_ENV_ID')
- ]) {
- sh 'testkube set context --api-key $TESTKUBE_API_KEY --org $TESTKUBE_ORG_ID --env $TESTKUBE_ENV_ID'
- }
+ // Installing and configuring Testkube based on env vars
+ setupTestkube()
// Running Testkube test
sh 'testkube run test test-name -f'
@@ -152,6 +135,11 @@ you'll need to create a Jenkinsfile. This Jenkinsfile should define the stages a
pipeline {
agent any
+ environment {
+ TK_ORG = credentials("TK_ORG")
+ TK_ENV = credentials("TK_ENV")
+ TK_API_TOKEN = credentials("TK_API_TOKEN")
+ }
stages {
stage('Deploy to GKE') {
steps {
@@ -177,15 +165,8 @@ pipeline {
sh 'gcloud container clusters get-credentials $GKE_CLUSTER_NAME --zone $GKE_ZONE'
}
- // Installing and initializing Testkube
- withCredentials([
- string(credentialsId: 'TESTKUBE_API_KEY', variable: 'TESTKUBE_API_KEY'),
- string(credentialsId: 'TESTKUBE_ORG_ID', variable: 'TESTKUBE_ORG_ID'),
- string(credentialsId: 'TESTKUBE_ENV_ID', variable: 'TESTKUBE_ENV_ID')
- ]) {
- sh 'curl -sSLf https://get.testkube.io | sh'
- sh 'testkube set context --api-key $TESTKUBE_API_KEY --org $TESTKUBE_ORG_ID --env $TESTKUBE_ENV_ID'
- }
+ // Installing and configuring Testkube based on env vars
+ setupTestkube()
// Running Testkube test
sh 'testkube run test test-name -f'
diff --git a/docs/docs/articles/run-tests-with-github-actions.md b/docs/docs/articles/run-tests-with-github-actions.md
index eccde46c9af..29ddda7e6af 100644
--- a/docs/docs/articles/run-tests-with-github-actions.md
+++ b/docs/docs/articles/run-tests-with-github-actions.md
@@ -1,5 +1,31 @@
# Run Tests with GitHub Actions
-**If you need more control over your flow or to access a private cluster, use [Testkube Action](https://github.com/marketplace/actions/testkube-action) instead.**
+**The `kubeshop/testkube-run-action` has been deprecated and won't receive further updates. Use the [Testkube Action](https://github.com/marketplace/actions/testkube-action) instead.**
+
+# Migrate from testkube-run-action to setup-testkube
+
+1. Change the `uses` property from `kubeshop/testkube-run-action@v1` to `kubeshop/setuo-testkube@v1`.
+
+```yaml
+uses: kubeshop/setuo-testkube@v1
+```
+2. Remove any usage of Test or Test Suite args from the `with` block.
+3. Use shell scripts to run testkube CLI commands directly:
+```yaml
+steps:
+ # Setup Testkube
+ - uses: kubeshop/setup-testkube@v1
+ # Pro and Enterprise args are still available
+ with:
+ organization: ${{ secrets.TkOrganization }}
+ environment: ${{ secrets.TkEnvironment }}
+ token: ${{ secrets.TkToken }}
+ # Use CLI with a shell script
+ - run: |
+ # Run one or multiple testkube CLI commands, passing any arguments you need
+ testkube run test some-test-name -f
+```
+
+# Deprecated usage information:
**Run on Testkube** is a GitHub Action for running tests on the Testkube platform.
diff --git a/docs/docs/articles/running-tests.md b/docs/docs/articles/running-tests.md
index 5d2a7490192..84dbaddb9e2 100644
--- a/docs/docs/articles/running-tests.md
+++ b/docs/docs/articles/running-tests.md
@@ -212,32 +212,40 @@ By default, there is a 10 second timeout limit on all requests on the client sid
The following environment variables are automatically injected into each executed test pod:
-DEBUG: if debug mode is on
-RUNNER_ENDPOINT: minio endpoint
-RUNNER_ACCESSKEYID: minio access key id
-RUNNER_SECRETACCESSKEY: minio secret access key
-RUNNER_REGION: minio region
-RUNNER_TOKEN: mnio token
-RUNNER_SSL: if minio ssl is on
-RUNNER_SCRAPPERENABLED: if scraping is on
-RUNNER_DATADIR: data directory
-RUNNER_CDEVENTS_TARGET: cd events target endpoint
-RUNNER_COMPRESSARTIFACTS: if artfifacts should be compressed
-RUNNER_CLOUD_MODE: cloud mode
-RUNNER_CLOUD_API_KEY: cloud api key
-RUNNER_CLOUD_API_TLS_INSECURE: if cloud connection is insecure
-RUNNER_CLOUD_API_URL: cloud api url
-RUNNER_DASHBOARD_URI: dashboard uri
-CI: ci flag
-RUNNER_CLUSTERID: cluster id
-RUNNER_BUCKET: minio bucket
-RUNNER_WORKINGDIR: working directory
-RUNNER_EXECUTIONID: test execution id
-RUNNER_TESTNAME: test name
-RUNNER_EXECUTIONNUMBER: test execution number
-RUNNER_CONTEXTTYPE: running context type
-RUNNER_CONTEXTDATA: running context data
-RUNNER_APIURI: api uri
+DEBUG: if debug mode is on
+RUNNER_ENDPOINT: minio endpoint
+RUNNER_ACCESSKEYID: minio access key id
+RUNNER_SECRETACCESSKEY: minio secret access key
+RUNNER_REGION: minio region
+RUNNER_TOKEN: minio token
+RUNNER_SSL: if minio ssl is on
+RUNNER_SCRAPPERENABLED: if scraping is on
+RUNNER_DATADIR: data directory
+RUNNER_CDEVENTS_TARGET: cd events target endpoint
+RUNNER_COMPRESSARTIFACTS: if artfifacts should be compressed
+RUNNER_PRO_MODE: pro mode
+RUNNER_PRO_API_KEY: pro api key
+RUNNER_PRO_API_TLS_INSECURE: if pro connection is insecure
+RUNNER_PRO_API_URL: pro api url
+RUNNER_PRO_CONNECTION_TIMEOUT: pro connection timeout limit
+RUNNER_PRO_API_SKIP_VERIFY: if pro connection tls verification is off
+RUNNER_CLOUD_MODE: DEPRECATED: please use RUNNER_PRO_MODE instead
+RUNNER_CLOUD_API_KEY: DEPRECATED: please use RUNNER_PRO_API_KEY instead
+RUNNER_CLOUD_API_TLS_INSECURE: DEPRECATED: please use RUNNER_PRO_API_TLS_INSECURE instead
+RUNNER_CLOUD_API_URL: DEPRECATED: please use RUNNER_PRO_API_URL instead
+RUNNER_CLOUD_CONNECTION_TIMEOUT: DEPRECATED: please use RUNNER_PRO_CONNECTION_TIMEOUT instead
+RUNNER_CLOUD_API_SKIP_VERIFY: DEPRECATED: please use RUNNER_PRO_API_SKIP_VERITY instead
+RUNNER_DASHBOARD_URI: dashboard uri
+CI: ci flag
+RUNNER_CLUSTERID: cluster id
+RUNNER_BUCKET: minio bucket
+RUNNER_WORKINGDIR: working directory
+RUNNER_EXECUTIONID: test execution id
+RUNNER_TESTNAME: test name
+RUNNER_EXECUTIONNUMBER: test execution number
+RUNNER_CONTEXTTYPE: running context type
+RUNNER_CONTEXTDATA: running context data
+RUNNER_APIURI: api uri
## Summary
diff --git a/docs/docs/articles/testkube-dependencies.md b/docs/docs/articles/testkube-dependencies.md
index 526b23c26c8..77ca55da70b 100644
--- a/docs/docs/articles/testkube-dependencies.md
+++ b/docs/docs/articles/testkube-dependencies.md
@@ -43,6 +43,8 @@ The keys of the fields can be modified. To set these variables on helm-charts le
### Amazon DocumentDB
+Warning: DocumentDB will not be supported in future releases. This is compatible with older releases of TestKube.
+
Testkube supports using [Amazon DocumentDB](https://aws.amazon.com/documentdb/), the managed version on MongoDB on AWS, as its database. Configuring it without TLS enabled is straightforward: add the connection string, and make sure the features that are not supported by DocumentDB are disabled. The parameters in the [helm-charts](https://github.com/kubeshop/helm-charts/blob/main/charts/testkube-api/values.yaml) are:
```bash
diff --git a/docs/docs/articles/testkube-licensing-FAQ.md b/docs/docs/articles/testkube-licensing-FAQ.md
new file mode 100644
index 00000000000..0ba56683639
--- /dev/null
+++ b/docs/docs/articles/testkube-licensing-FAQ.md
@@ -0,0 +1,65 @@
+# Testkube Licensing FAQ
+
+Testkube's software licensing is designed to be transparent and to support both open source and commercial use cases. This document aims to address common questions related to our licensing model.
+
+## Licenses
+
+Testkube software is distributed under two primary licenses:
+- **MIT License (MIT)**: A permissive open-source license that allows for broad freedom in usage and modification.
+- **Testkube Community License (TCL)**: A custom license designed to protect the Testkube community and ecosystem, covering specific advanced features.
+
+## Testkube Core
+
+Testkube Core is free to use. Most core features are licensed under the MIT license, but some core features are subject to the TCL.
+
+## Testkube Pro
+
+Testkube Pro features require a paid license from Testkube (see [pricing](https://testkube.io/pricing)) and are licensed under the Testkube Community License.
+
+:::note
+You can find any feature's license by checking the code's file header in the Testkube repository.
+:::
+
+### What is the TCL License?
+
+The Testkube Community License (TCL) is a custom license created by Testkube to cover certain aspects of the Testkube software. It was inspired by the [CockroachDB Community License](https://www.cockroachlabs.com/docs/stable/licensing-faqs#ccl) and designed to ensure that advanced features and proprietary extensions remain available and maintained for the community while allowing Testkube to sustain its development through commercial offerings.
+
+### Why does Testkube have a dual-licensing scheme with MIT / TCL?
+
+Testkube uses a dual license model to balance open source community participation with the ability to fund continued development. Core functionality is available under the permissive MIT license, while advanced features require a commercial license. This allows the community to benefit from an open source project while providing a sustainability model.
+
+### How does the TCL license apply to Testkube Core?
+
+Testkube core functionality is available under the MIT license, allowing free usage, modification and distribution. However, advanced pro features are covered under the more restrictive TCL. Contributions back to Testkube Core are welcomed, but modifications to TCL-licensed components may require reaching out to Testkube first.
+
+### Can I use Testkube Core for free?
+
+Yes, Testkube Core can be used for free. The majority of Testkube's core functionalities are available under the MIT license, which allows for free usage, modification, and distribution.
+
+### Does the TCL license restrict my usage of Testkube Core?
+
+No, the TCL license only applies to specific advanced features marked as "Pro" in the codebase. It does not restrict usage of the MIT-licensed open source components.
+
+### Can I make changes to Testkube Core for my own usage?
+
+Yes, you are free to make changes to Testkube Core components licensed under the MIT license for your own use. For components under the TCL, you must adhere to the terms of that license, which include restrictions on redistribution or commercial use, for this we advise you to reach out to us first.
+
+### Can I make contributions back to Testkube Core?
+
+Yes! Contributions are welcomed, whether bug fixes, enhancements or documentation. As long as you retain the existing MIT license, contributions can be made freely.
+
+## Feature Licensing
+
+The table below shows how certain core and pro features in the GitHub repository are licensed:
+
+| Feature | Core/MIT | Pro/TCL |
+| :--- | :----: | :---: |
+| Tests | x | |
+| Basic Testsuites | x | |
+| Triggers | x | |
+| Executors | x | |
+| Webhooks | x | |
+| Sources | x | |
+| Test Workflows | | x |
+| Adv Testsuites | | x |
+
diff --git a/docs/docs/articles/testkube-oss.md b/docs/docs/articles/testkube-oss.md
index 00d0d3d1ac9..2ea19ae3515 100644
--- a/docs/docs/articles/testkube-oss.md
+++ b/docs/docs/articles/testkube-oss.md
@@ -25,7 +25,6 @@ This command will set up the following components in your Kubernetes cluster:
- Create a Testkube namespace.
- Deploy the Testkube API.
- Use MongoDB for test results and Minio for artifact storage (optional; disable with --no-minio).
-- Testkube Dashboard to visually and manage all your tests (optional; disable with --no-dashboard flag).
- Testkube will listen and manage all the CRDs for Tests, TestSuites, Executors, etc… inside the Testkube namespace.
diff --git a/docs/docs/articles/webhooks.mdx b/docs/docs/articles/webhooks.mdx
index 0c8be2837a1..6a6a2e707ce 100644
--- a/docs/docs/articles/webhooks.mdx
+++ b/docs/docs/articles/webhooks.mdx
@@ -5,7 +5,26 @@ import TabItem from "@theme/TabItem";
Webhooks allow you to integrate Testkube with external systems by sending HTTP POST payloads containing information about Testkube executions and their current state when specific events occur. To set up webhooks in Testkube, you'll need to have an HTTPS endpoint to receive the events and a payload template to be sent along with the data.
+:::note
+Please visit our Blog, [Empowering Kubernetes Tests with Webhooks](https://testkube.io/blog/empowering-kubernetes-tests-with-webhooks) for a tutorial on setting up webhooks for Slack and Grafana Dashboard.
+:::
+
+## Benefits of using Webhooks in Testkube
+
+Testkube uses webhooks to integrate with external systems, allowing you to effortlessly synchronize your testing workflows with other tools and platforms. These webhooks are designed to carry critical information regarding your tests as HTTP POST payloads. The information can include the execution and real-time status depending on how you configure it.
+
+To leverage webhooks, you need to ensure that the platform that you want to send information to has an HTTPS endpoint to receive the events. Testkube also allows you to customize the payloads.
+
+You can create a webhook from the dashboard, use the CLI, or create it as a custom resource. Before we show how it’s done, let’s understand a few scenarios where Webhooks in Testkube shine:
+
+- Incident Management & Response: Webhooks can be used to create incidents and alert on-call teams when a critical test fails. This ensures a timely response and avoids any potential disruption due to failures and bugs. With Testkube, you can configure incident management tools like PagerDuty and OpsGenie to receive alerts based on critical events for your tests.
+
+- Communication and Collaboration: You can configure Webhooks in Testkube to send alerts to your teams in your communication tool. This will notify your team of any critical event that needs attention and attend to it before the issue escalates. Some of the popular communications tools like Slack and MS Teams can be configured to receive alerts from Testkube.
+
+- Monitoring and Observability: Webhooks can also be used to send alerts and notifications to your monitoring and observability tools like Prometheus and Grafana. This provides visibility into your tests, alerts you, and ensures that timely corrective actions can be taken.
+
## Creating a Webhook
+
The webhook can be created using the Dashboard, CLI, or a Custom Resource.
@@ -37,6 +56,7 @@ Webhooks can be created with Testkube CLI using the `create webhook` command.
```sh
testkube create webhook --name example-webhook --events start-test --events end-test-success --events end-test-failed --uri
```
+
`--name` - Your webhook name (in this case `example-webhook`).
`--events` - Event that will trigger a webhook. Multiple `--events` can be defined (in this case `--events start-test --events end-test-success --events end-test-failed`). All available trigger events can be found in the [Supported Event types](#supported-event-types) section.
`--uri` - The HTTPS endpoint where you want to receive the webhook events.
@@ -59,6 +79,7 @@ spec:
- end-test-failed
selector: ""
```
+
Where should be replaced with the HTTPS endpoint URL where you want to receive the webhook events.
And then apply with:
@@ -72,6 +93,7 @@ kubectl apply -f webhook.yaml
### Resource Selector (labels)
+
In order to limit webhook triggers to a specific resource, or resources, the Resource Selector can be used. It allows you to select the specific resource by label, or labels.
@@ -115,10 +137,10 @@ spec:
-
### Webhook Payload
Webhook payload can be configured - in this example, `event id`:
+
```
{"text": "event id {{ .Id }}"}
```
@@ -147,8 +169,8 @@ And set it with `--payload-template template.json`.
```sh
testkube create webhook --name example-webhook --events start-test --events end-test-passed --events end-test-failed --payload-template template.json --uri
```
-Where should be replaced with the HTTPS endpoint URL where you want to receive the webhook events.
+Where should be replaced with the HTTPS endpoint URL where you want to receive the webhook events.
```sh title="Expected output:"
Webhook created example-webhook 🥇
@@ -159,12 +181,14 @@ Webhook created example-webhook 🥇
Payload template can be configured with `spec.payloadTemplate`.
+
```
payloadTemplate: |
{"text": "event id {{ .Id }}"}
```
Example:
+
```
apiVersion: executor.testkube.io/v1
kind: Webhook
@@ -188,35 +212,43 @@ spec:
### Webhook Payload Variables
-Webhook payload can contain **event-specific** variables - they will be replaced with actual data when the events occurs. In the above examples, only the event `Id` is being sent.
+
+Webhook payload can contain **event-specific** variables - they will be replaced with actual data when the events occurs. In the above examples, only the event `Id` is being sent.
However, any of these [supported Event Variables](#supported-event-variables) can be used.
For example, the following payload:
+
```
{"text": "Event {{ .Type_ }} - Test '{{ .TestExecution.TestName }}' execution ({{ .TestExecution.Number }}) finished with '{{ .TestExecution.ExecutionResult.Status }}' status"}
```
+
will result in:
+
```
{"text": "Event end-test-success - Test 'postman-executor-smoke' execution (948) finished with 'passed' status"}
```
#### testkube-api-server ENV variables
+
In addition to event-specific variables, it's also possible to pass testkube-api-server ENV variables:
```sh title="template.txt"
-TESTKUBE_CLOUD_URL: {{ index .Envs "TESTKUBE_CLOUD_URL" }}
+TESTKUBE_PRO_URL: {{ index .Envs "TESTKUBE_PRO_URL" }}
```
### URI and HTTP Headers
+
You can add additional HTTP headers like `Authorization` or `x-api-key` to have a secret token.
It's possible to use golang based template string as header or uri value.
### Helper methods
+
We also provide special helper methods to use in the webhook template:
`executionstatustostring` is the method to convert a pointer to a execution status to a string type.
`testsuiteexecutionstatustostring` is the method to convert a pointer to a test suite execution status to a string type.
Usage example:
+
```yaml
- name: TEXT_COLOUR
value: {{ if eq (.TestSuiteExecution.Status | testsuiteexecutionstatustostring ) "passed" }}"00FF00"{{ else }}"FF0000"{{ end }}
@@ -269,7 +301,9 @@ spec:
## Supported Event types
+
Webhooks can be triggered on any of the following events:
+
- start-test
- end-test-success
- end-test-failed
@@ -285,6 +319,7 @@ Webhooks can be triggered on any of the following events:
- deleted
They can be triggered by the following resources:
+
- test
- testsuite
- executor
@@ -296,6 +331,7 @@ They can be triggered by the following resources:
## Supported Event Variables
### Event-specific variables:
+
- `Id` - event ID (for example, `2a20c7da-3b77-4ea9-a33d-403187d3e9e6`)
- `Resource`
- `ResourceId`
@@ -308,16 +344,17 @@ They can be triggered by the following resources:
The full Event Data Model can be found [here](https://github.com/kubeshop/testkube/blob/main/pkg/api/v1/testkube/model_event.go).
### TestExecution (Execution):
+
- `Id` - Execution ID (for example, `64f8cf3c712890925aea51ce`)
- `TestName` - Test Name (for example, `postman-executor-smoke`)
- `TestSuiteName` - Test Suite name (if run as a part of a Test Suite)
- `TestNamespace` - Execution namespace, where testkube is installed (for example, `testkube`)
- `TestType` - Test type (for example, `postman/collection`)
-- `Name` - Execution name (for example, `postman-executor-smoke-937)
+- `Name` - Execution name (for example, `postman-executor-smoke-937)
- `Number` - Execution number (for example, `937`)
-- `Envs` - List of ENV variables for specific Test (if defined)
+- `Envs` - List of ENV variables for specific Test (if defined)
- `Command` - Command executed inside the Pod (for example, `newman`)
-- `Args` - Command arguments (for example, `run -e --reporters cli,json --reporter-json-export `)
+- `Args` - Command arguments (for example, `run -e --reporters cli,json --reporter-json-export `)
- `Variables` - List of variables
- `Content` - Test content
- `StartTime` - Test start time (for example, `2023-09-06 19:23:34.543433547 +0000 UTC`)
@@ -350,10 +387,12 @@ The full TestSuiteExecution data model can be found [here](https://github.com/ku
## Additional Examples
### Microsoft Teams
+
Webhooks can also be used to send messages to Microsoft Teams channels.
First, you need to create an incoming webhook in Teams for a specific channel. You can see how to do it in the Teams Docs [here](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook?tabs=dotnet#create-incoming-webhooks-1). After your Teams incoming webhook is created, you can use it with Testkube webhooks - just use the URL provided (it will probably look like this: `https://xxxxx.webhook.office.com/xxxxxxxxx`).
In order to send the message when test execution finishes, the following Webhook can be used:
+
```
apiVersion: executor.testkube.io/v1
kind: Webhook
diff --git a/docs/docs/cli/testkube.md b/docs/docs/cli/testkube.md
index 6a8ae3ccc98..6383b0ae01f 100644
--- a/docs/docs/cli/testkube.md
+++ b/docs/docs/cli/testkube.md
@@ -26,7 +26,7 @@ testkube [flags]
* [testkube config](testkube_config.md) - Set feature configuration value
* [testkube create](testkube_create.md) - Create resource
* [testkube create-ticket](testkube_create-ticket.md) - Create bug ticket
-* [testkube dashboard](testkube_dashboard.md) - Open testkube dashboard
+* [testkube dashboard](testkube_dashboard.md) - Open Testkube Pro/Enterprise dashboard
* [testkube debug](testkube_debug.md) - Print environment information for debugging
* [testkube delete](testkube_delete.md) - Delete resources
* [testkube disable](testkube_disable.md) - Disable feature
diff --git a/docs/docs/cli/testkube_create.md b/docs/docs/cli/testkube_create.md
index 8ff3f3ddcfe..26dd58c808d 100644
--- a/docs/docs/cli/testkube_create.md
+++ b/docs/docs/cli/testkube_create.md
@@ -32,5 +32,7 @@ testkube create [flags]
* [testkube create test](testkube_create_test.md) - Create new Test
* [testkube create testsource](testkube_create_testsource.md) - Create new TestSource
* [testkube create testsuite](testkube_create_testsuite.md) - Create new TestSuite
+* [testkube create testworkflow](testkube_create_testworkflow.md) - Create test workflow
+* [testkube create testworkflowtemplate](testkube_create_testworkflowtemplate.md) - Create test workflow template
* [testkube create webhook](testkube_create_webhook.md) - Create new Webhook
diff --git a/docs/docs/cli/testkube_create_test.md b/docs/docs/cli/testkube_create_test.md
index 0015a3a7f70..d15c77f5a9c 100644
--- a/docs/docs/cli/testkube_create_test.md
+++ b/docs/docs/cli/testkube_create_test.md
@@ -13,7 +13,7 @@ testkube create test [flags]
### Options
```
- --args-mode string usage mode for arguments. one of append|override (default "append")
+ --args-mode string usage mode for arguments. one of append|override|replace (default "append")
--artifact-dir stringArray artifact dirs for scraping
--artifact-mask stringArray regexp to filter scraped artifacts, single or comma separated, like report/.* or .*\.json,.*\.js$
--artifact-omit-folder-per-execution don't store artifacts in execution folder
@@ -29,6 +29,7 @@ testkube create test [flags]
--env stringToString envs in a form of name1=val1 passed to executor (default [])
--execute-postrun-script-before-scraping whether to execute postrun scipt before scraping or not (prebuilt executor only)
--execution-name string execution name, if empty will be autogenerated
+ --execution-namespace string namespace for test execution (Pro edition only)
--executor-args stringArray executor binary additional arguments
-f, --file string test file - will be read from stdin if not specified
--git-auth-type string auth type for git requests one of basic|header (default "basic")
@@ -71,6 +72,7 @@ testkube create test [flags]
--slave-pod-template string slave pod template file path for extensions to slave pod template
--slave-pod-template-reference string reference to slave pod template to use for the test
--source string source name - will be used together with content parameters
+ --source-scripts run scripts using source command (container executor only)
--test-content-type string content type of test one of string|file-uri|git
--timeout int duration in seconds for test to timeout. 0 disables timeout.
-t, --type string test type
diff --git a/docs/docs/cli/testkube_create_testworkflow.md b/docs/docs/cli/testkube_create_testworkflow.md
new file mode 100644
index 00000000000..bdf9a07fae9
--- /dev/null
+++ b/docs/docs/cli/testkube_create_testworkflow.md
@@ -0,0 +1,33 @@
+## testkube create testworkflow
+
+Create test workflow
+
+```
+testkube create testworkflow [flags]
+```
+
+### Options
+
+```
+ -f, --file string file path to get the test workflow specification
+ -h, --help help for testworkflow
+ --name string test workflow name
+ --update update, if test workflow already exists
+```
+
+### Options inherited from parent commands
+
+```
+ -a, --api-uri string api uri, default value read from config if set (default "https://demo.testkube.io/results")
+ -c, --client string client used for connecting to Testkube API one of proxy|direct (default "proxy")
+ --crd-only generate only crd
+ --insecure insecure connection for direct client
+ --namespace string Kubernetes namespace, default value read from config if set (default "testkube")
+ --oauth-enabled enable oauth
+ --verbose show additional debug messages
+```
+
+### SEE ALSO
+
+* [testkube create](testkube_create.md) - Create resource
+
diff --git a/docs/docs/cli/testkube_create_testworkflowtemplate.md b/docs/docs/cli/testkube_create_testworkflowtemplate.md
new file mode 100644
index 00000000000..ffd31e3f39d
--- /dev/null
+++ b/docs/docs/cli/testkube_create_testworkflowtemplate.md
@@ -0,0 +1,33 @@
+## testkube create testworkflowtemplate
+
+Create test workflow template
+
+```
+testkube create testworkflowtemplate [flags]
+```
+
+### Options
+
+```
+ -f, --file string file path to get the test workflow template specification
+ -h, --help help for testworkflowtemplate
+ --name string test workflow template name
+ --update update, if test workflow template already exists
+```
+
+### Options inherited from parent commands
+
+```
+ -a, --api-uri string api uri, default value read from config if set (default "https://demo.testkube.io/results")
+ -c, --client string client used for connecting to Testkube API one of proxy|direct (default "proxy")
+ --crd-only generate only crd
+ --insecure insecure connection for direct client
+ --namespace string Kubernetes namespace, default value read from config if set (default "testkube")
+ --oauth-enabled enable oauth
+ --verbose show additional debug messages
+```
+
+### SEE ALSO
+
+* [testkube create](testkube_create.md) - Create resource
+
diff --git a/docs/docs/cli/testkube_dashboard.md b/docs/docs/cli/testkube_dashboard.md
index 4104b1dca31..60800d2bae5 100644
--- a/docs/docs/cli/testkube_dashboard.md
+++ b/docs/docs/cli/testkube_dashboard.md
@@ -1,10 +1,10 @@
## testkube dashboard
-Open testkube dashboard
+Open Testkube Pro/Enterprise dashboard
### Synopsis
-Open testkube dashboard
+Open Testkube Pro/Enterprise dashboard
```
testkube dashboard [flags]
@@ -13,14 +13,13 @@ testkube dashboard [flags]
### Options
```
- -h, --help help for dashboard
- --use-global-dashboard use global dashboard for viewing testkube results
+ -h, --help help for dashboard
```
### Options inherited from parent commands
```
- -a, --api-uri string api uri, default value read from config if set (default "https://demo.testkube.io/results")
+ -a, --api-uri string api uri, default value read from config if set (default "http://localhost:8088")
-c, --client string client used for connecting to Testkube API one of proxy|direct (default "proxy")
--insecure insecure connection for direct client
--namespace string Kubernetes namespace, default value read from config if set (default "testkube")
diff --git a/docs/docs/cli/testkube_delete.md b/docs/docs/cli/testkube_delete.md
index 7cdc4c177fb..c4d9bd7cff3 100644
--- a/docs/docs/cli/testkube_delete.md
+++ b/docs/docs/cli/testkube_delete.md
@@ -31,5 +31,7 @@ testkube delete [flags]
* [testkube delete test](testkube_delete_test.md) - Delete Test
* [testkube delete testsource](testkube_delete_testsource.md) - Delete test source
* [testkube delete testsuite](testkube_delete_testsuite.md) - Delete test suite
+* [testkube delete testworkflow](testkube_delete_testworkflow.md) - Delete test workflows
+* [testkube delete testworkflowtemplate](testkube_delete_testworkflowtemplate.md) - Delete test workflow templates
* [testkube delete webhook](testkube_delete_webhook.md) - Delete webhook
diff --git a/docs/docs/cli/testkube_delete_testworkflow.md b/docs/docs/cli/testkube_delete_testworkflow.md
new file mode 100644
index 00000000000..250eb3fc9d2
--- /dev/null
+++ b/docs/docs/cli/testkube_delete_testworkflow.md
@@ -0,0 +1,31 @@
+## testkube delete testworkflow
+
+Delete test workflows
+
+```
+testkube delete testworkflow [name] [flags]
+```
+
+### Options
+
+```
+ --all Delete all test workflows
+ -h, --help help for testworkflow
+ -l, --label strings label key value pair: --label key1=value1
+```
+
+### Options inherited from parent commands
+
+```
+ -a, --api-uri string api uri, default value read from config if set (default "https://demo.testkube.io/results")
+ -c, --client string Client used for connecting to testkube API one of proxy|direct (default "proxy")
+ --insecure insecure connection for direct client
+ --namespace string Kubernetes namespace, default value read from config if set (default "testkube")
+ --oauth-enabled enable oauth
+ --verbose should I show additional debug messages
+```
+
+### SEE ALSO
+
+* [testkube delete](testkube_delete.md) - Delete resources
+
diff --git a/docs/docs/cli/testkube_delete_testworkflowtemplate.md b/docs/docs/cli/testkube_delete_testworkflowtemplate.md
new file mode 100644
index 00000000000..4e5751a36d2
--- /dev/null
+++ b/docs/docs/cli/testkube_delete_testworkflowtemplate.md
@@ -0,0 +1,31 @@
+## testkube delete testworkflowtemplate
+
+Delete test workflow templates
+
+```
+testkube delete testworkflowtemplate [name] [flags]
+```
+
+### Options
+
+```
+ --all Delete all test workflow templates
+ -h, --help help for testworkflowtemplate
+ -l, --label strings label key value pair: --label key1=value1
+```
+
+### Options inherited from parent commands
+
+```
+ -a, --api-uri string api uri, default value read from config if set (default "https://demo.testkube.io/results")
+ -c, --client string Client used for connecting to testkube API one of proxy|direct (default "proxy")
+ --insecure insecure connection for direct client
+ --namespace string Kubernetes namespace, default value read from config if set (default "testkube")
+ --oauth-enabled enable oauth
+ --verbose should I show additional debug messages
+```
+
+### SEE ALSO
+
+* [testkube delete](testkube_delete.md) - Delete resources
+
diff --git a/docs/docs/cli/testkube_generate_tests-crds.md b/docs/docs/cli/testkube_generate_tests-crds.md
index f34efd49b9a..af583a785c0 100644
--- a/docs/docs/cli/testkube_generate_tests-crds.md
+++ b/docs/docs/cli/testkube_generate_tests-crds.md
@@ -13,7 +13,7 @@ testkube generate tests-crds [flags]
### Options
```
- --args-mode string usage mode for arguments. one of append|override (default "append")
+ --args-mode string usage mode for arguments. one of append|override|replace (default "append")
--artifact-dir stringArray artifact dirs for scraping
--artifact-mask stringArray regexp to filter scraped artifacts, single or comma separated, like report/.* or .*\.json,.*\.js$
--artifact-omit-folder-per-execution don't store artifacts in execution folder
@@ -29,6 +29,7 @@ testkube generate tests-crds [flags]
--env stringToString envs in a form of name1=val1 passed to executor (default [])
--execute-postrun-script-before-scraping whether to execute postrun scipt before scraping or not (prebuilt executor only)
--execution-name string execution name, if empty will be autogenerated
+ --execution-namespace string namespace for test execution (Pro edition only)
--executor-args stringArray executor binary additional arguments
-h, --help help for tests-crds
--http-proxy string http proxy for executor containers
@@ -57,6 +58,7 @@ testkube generate tests-crds [flags]
--slave-pod-requests-memory string slave pod resource requests memory
--slave-pod-template string slave pod template file path for extensions to slave pod template
--slave-pod-template-reference string reference to slave pod template to use for the test
+ --source-scripts run scripts using source command (container executor only)
--timeout int duration in seconds for test to timeout. 0 disables timeout.
-t, --type string test type
--upload-timeout string timeout to use when uploading files, example: 30s
diff --git a/docs/docs/cli/testkube_get.md b/docs/docs/cli/testkube_get.md
index c5d70315948..4eeab707ed3 100644
--- a/docs/docs/cli/testkube_get.md
+++ b/docs/docs/cli/testkube_get.md
@@ -41,5 +41,7 @@ testkube get [flags]
* [testkube get testsource](testkube_get_testsource.md) - Get test source details
* [testkube get testsuite](testkube_get_testsuite.md) - Get test suite by name
* [testkube get testsuiteexecution](testkube_get_testsuiteexecution.md) - Gets TestSuite Execution details
+* [testkube get testworkflow](testkube_get_testworkflow.md) - Get all available test workflows
+* [testkube get testworkflowtemplate](testkube_get_testworkflowtemplate.md) - Get all available test workflow templates
* [testkube get webhook](testkube_get_webhook.md) - Get webhook details
diff --git a/docs/docs/cli/testkube_get_testworkflow.md b/docs/docs/cli/testkube_get_testworkflow.md
new file mode 100644
index 00000000000..97b882b99e1
--- /dev/null
+++ b/docs/docs/cli/testkube_get_testworkflow.md
@@ -0,0 +1,37 @@
+## testkube get testworkflow
+
+Get all available test workflows
+
+### Synopsis
+
+Getting all available test workflows from given namespace - if no namespace given "testkube" namespace is used
+
+```
+testkube get testworkflow [name] [flags]
+```
+
+### Options
+
+```
+ --crd-only show only test workflow crd
+ -h, --help help for testworkflow
+ -l, --label strings label key value pair: --label key1=value1
+```
+
+### Options inherited from parent commands
+
+```
+ -a, --api-uri string api uri, default value read from config if set (default "https://demo.testkube.io/results")
+ -c, --client string client used for connecting to Testkube API one of proxy|direct (default "proxy")
+ --go-template string go template to render (default "{{.}}")
+ --insecure insecure connection for direct client
+ --namespace string Kubernetes namespace, default value read from config if set (default "testkube")
+ --oauth-enabled enable oauth
+ -o, --output string output type can be one of json|yaml|pretty|go-template (default "pretty")
+ --verbose show additional debug messages
+```
+
+### SEE ALSO
+
+* [testkube get](testkube_get.md) - Get resources
+
diff --git a/docs/docs/cli/testkube_get_testworkflowtemplate.md b/docs/docs/cli/testkube_get_testworkflowtemplate.md
new file mode 100644
index 00000000000..249f8284ba3
--- /dev/null
+++ b/docs/docs/cli/testkube_get_testworkflowtemplate.md
@@ -0,0 +1,37 @@
+## testkube get testworkflowtemplate
+
+Get all available test workflow templates
+
+### Synopsis
+
+Getting all available test workflow templates from given namespace - if no namespace given "testkube" namespace is used
+
+```
+testkube get testworkflowtemplate [name] [flags]
+```
+
+### Options
+
+```
+ --crd-only show only test workflow template crd
+ -h, --help help for testworkflowtemplate
+ -l, --label strings label key value pair: --label key1=value1
+```
+
+### Options inherited from parent commands
+
+```
+ -a, --api-uri string api uri, default value read from config if set (default "https://demo.testkube.io/results")
+ -c, --client string client used for connecting to Testkube API one of proxy|direct (default "proxy")
+ --go-template string go template to render (default "{{.}}")
+ --insecure insecure connection for direct client
+ --namespace string Kubernetes namespace, default value read from config if set (default "testkube")
+ --oauth-enabled enable oauth
+ -o, --output string output type can be one of json|yaml|pretty|go-template (default "pretty")
+ --verbose show additional debug messages
+```
+
+### SEE ALSO
+
+* [testkube get](testkube_get.md) - Get resources
+
diff --git a/docs/docs/cli/testkube_init.md b/docs/docs/cli/testkube_init.md
index f7a562d83cd..597ddbabcbf 100644
--- a/docs/docs/cli/testkube_init.md
+++ b/docs/docs/cli/testkube_init.md
@@ -21,7 +21,6 @@ testkube init [flags]
--name string installation name (usually you don't need to change it) (default "testkube")
--namespace string namespace where to install (default "testkube")
--no-confirm don't ask for confirmation - unatended installation mode
- --no-dashboard don't install dashboard
--no-minio don't install MinIO
--no-mongo don't install MongoDB
--org-id string Testkube Cloud organization id [required for centralized mode]
diff --git a/docs/docs/cli/testkube_install.md b/docs/docs/cli/testkube_install.md
index b51e91b60bd..a7921468c9c 100644
--- a/docs/docs/cli/testkube_install.md
+++ b/docs/docs/cli/testkube_install.md
@@ -12,7 +12,6 @@ testkube install [flags]
--chart string chart name (default "kubeshop/testkube")
-h, --help help for install
--name string installation name (default "testkube")
- --no-dashboard don't install dashboard
--no-minio don't install MinIO
--no-mongo don't install MongoDB
--values string path to Helm values file
diff --git a/docs/docs/cli/testkube_pro_connect.md b/docs/docs/cli/testkube_pro_connect.md
index 111d65b3d49..a2c3af447e3 100644
--- a/docs/docs/cli/testkube_pro_connect.md
+++ b/docs/docs/cli/testkube_pro_connect.md
@@ -24,7 +24,6 @@ testkube pro connect [flags]
--name string installation name (usually you don't need to change it) (default "testkube")
--namespace string namespace where to install (default "testkube")
--no-confirm don't ask for confirmation - unatended installation mode
- --no-dashboard don't install dashboard
--no-minio don't install MinIO
--no-mongo don't install MongoDB
--org-id string Testkube Cloud organization id [required for centralized mode]
diff --git a/docs/docs/cli/testkube_run_test.md b/docs/docs/cli/testkube_run_test.md
index 62b186732b0..221039072fb 100644
--- a/docs/docs/cli/testkube_run_test.md
+++ b/docs/docs/cli/testkube_run_test.md
@@ -14,7 +14,7 @@ testkube run test [flags]
```
--args stringArray executor binary additional arguments
- --args-mode string usage mode for argumnets. one of append|override (default "append")
+ --args-mode string usage mode for argumnets. one of append|override|replace (default "append")
--artifact-dir stringArray artifact dirs for scraping
--artifact-mask stringArray regexp to filter scraped artifacts, single or comma separated, like report/.* or .*\.json,.*\.js$
--artifact-omit-folder-per-execution don't store artifacts in execution folder
@@ -30,6 +30,7 @@ testkube run test [flags]
--download-dir string download dir (default "artifacts")
--execute-postrun-script-before-scraping whether to execute postrun scipt before scraping or not (prebuilt executor only)
--execution-label stringToString execution-label key value pair: --execution-label key1=value1 (default [])
+ --execution-namespace string namespace for test execution (Pro edition only)
--format string data format for storing files, one of folder|archive (default "folder")
--git-branch string if uri is git repository we can set additional branch parameter
--git-commit string if uri is git repository we can use commit id (sha) parameter
@@ -63,6 +64,7 @@ testkube run test [flags]
--slave-pod-requests-memory string slave pod resource requests memory
--slave-pod-template string slave pod template file path for extensions to slave pod template
--slave-pod-template-reference string reference to slave pod template to use for the test
+ --source-scripts run scripts using source command (container executor only)
--upload-timeout string timeout to use when uploading files, example: 30s
-v, --variable stringToString execution variable passed to executor (default [])
--variable-configmap stringArray config map name used to map all keys to basis variables
diff --git a/docs/docs/cli/testkube_update_test.md b/docs/docs/cli/testkube_update_test.md
index cc472b3af10..a44238fe963 100644
--- a/docs/docs/cli/testkube_update_test.md
+++ b/docs/docs/cli/testkube_update_test.md
@@ -13,7 +13,7 @@ testkube update test [flags]
### Options
```
- --args-mode string usage mode for arguments. one of append|override (default "append")
+ --args-mode string usage mode for arguments. one of append|override|replace (default "append")
--artifact-dir stringArray artifact dirs for scraping
--artifact-mask stringArray regexp to filter scraped artifacts, single or comma separated, like report/.* or .*\.json,.*\.js$
--artifact-omit-folder-per-execution don't store artifacts in execution folder
@@ -29,6 +29,7 @@ testkube update test [flags]
--env stringToString envs in a form of name1=val1 passed to executor (default [])
--execute-postrun-script-before-scraping whether to execute postrun scipt before scraping or not (prebuilt executor only)
--execution-name string execution name, if empty will be autogenerated
+ --execution-namespace string namespace for test execution (Pro edition only)
--executor-args stringArray executor binary additional arguments
-f, --file string test file - will try to read content from stdin if not specified
--git-auth-type string auth type for git requests one of basic|header (default "basic")
@@ -71,6 +72,7 @@ testkube update test [flags]
--slave-pod-template string slave pod template file path for extensions to slave pod template
--slave-pod-template-reference string reference to slave pod template to use for the test
--source string source name - will be used together with content parameters
+ --source-scripts run scripts using source command (container executor only)
--test-content-type string content type of test one of string|file-uri|git
--timeout int duration in seconds for test to timeout. 0 disables timeout.
-t, --type string test type
diff --git a/docs/docs/img/enterprise-download-form.png b/docs/docs/img/enterprise-download-form.png
new file mode 100644
index 00000000000..365321f5f50
Binary files /dev/null and b/docs/docs/img/enterprise-download-form.png differ
diff --git a/docs/docs/img/enterprise-download.png b/docs/docs/img/enterprise-download.png
new file mode 100644
index 00000000000..526ce62ecb1
Binary files /dev/null and b/docs/docs/img/enterprise-download.png differ
diff --git a/docs/docs/img/jenkins-build-step.png b/docs/docs/img/jenkins-build-step.png
new file mode 100644
index 00000000000..ed1820af215
Binary files /dev/null and b/docs/docs/img/jenkins-build-step.png differ
diff --git a/docs/docs/img/jenkins-environment.png b/docs/docs/img/jenkins-environment.png
new file mode 100644
index 00000000000..f6814e8e0ad
Binary files /dev/null and b/docs/docs/img/jenkins-environment.png differ
diff --git a/docs/docs/img/jenkins-execute-shell.png b/docs/docs/img/jenkins-execute-shell.png
new file mode 100644
index 00000000000..7db10a340d1
Binary files /dev/null and b/docs/docs/img/jenkins-execute-shell.png differ
diff --git a/docs/docs/img/keyword-highlights-configuration.png b/docs/docs/img/keyword-highlights-configuration.png
new file mode 100644
index 00000000000..ab9b898607b
Binary files /dev/null and b/docs/docs/img/keyword-highlights-configuration.png differ
diff --git a/docs/docs/index.mdx b/docs/docs/index.mdx
index 519e605ea00..70787ce2844 100644
--- a/docs/docs/index.mdx
+++ b/docs/docs/index.mdx
@@ -10,6 +10,11 @@ This is the place where you'll find everything you need to get ramped up and sta
Testkube is a Kubernetes-native testing framework for Testers, Developers, and DevOps practitioners that allows you to automate the executions of your existing testing tools inside your Kubernetes cluster, removing all the complexity from your CI/CD pipelines.
+
+
## Try It Out!
export const DocCardList = (input) => (
diff --git a/docs/docs/test-types/executor-curl.mdx b/docs/docs/test-types/executor-curl.mdx
index b47fbdf123a..c08c2e8fb56 100644
--- a/docs/docs/test-types/executor-curl.mdx
+++ b/docs/docs/test-types/executor-curl.mdx
@@ -83,7 +83,7 @@ For a File test source:
- `--file` (path to your curl test - in this case `test/curl/executor-tests/curl-smoke-test.json`)
```sh
-testkube create test --name curl-test --type curl/test --test-content-type file-uri --file test/curl/executor-tests/curl-smoke-test.json
+testkube create test --name curl-test --type curl/test --file test/curl/executor-tests/curl-smoke-test.json
```
```sh title="Expected output:"
diff --git a/docs/docs/testkube-enterprise/articles/migrating-from-oss-to-pro.md b/docs/docs/testkube-enterprise/articles/migrating-from-oss-to-pro.md
new file mode 100644
index 00000000000..b5cfe4c50c9
--- /dev/null
+++ b/docs/docs/testkube-enterprise/articles/migrating-from-oss-to-pro.md
@@ -0,0 +1,89 @@
+# How to Migrate from Testkube OSS to Testkube Enterprise
+
+It is possible to deploy Testkube Enterprise within the same k8s cluster where Testkube OSS is already running. To achieve this, you should install Testkube Enterprise in a different namespace and connect Testkube OSS as an Agent.
+
+:::note
+Please note that your test executions will not be migrated to Testkube Enterprise, only Test definitions.
+:::
+
+
+## License
+To start with Testkube Enterprise you need to request a license. Depending on your environment requirements it can be either an offline or an online license. Read more about these types of licenses [here](https://docs.testkube.io/testkube-enterprise/articles/usage-guide#license). If you require an online license, it can be acquired [here](https://testkube.io/download). If you need an offline license, please contact us using this [form](https://testkube.io/contact).
+There are multiple ways to integrate Testkube OSS into your Testkube Enterprise setup. We highly recommend creating a k8s secret, as it Enterprisevides a more secure way to store sensitive data.
+
+At this point there are two options to deploy Testkube Enterprise:
+
+**Multi-cluster Installation:**
+
+- *Description:* This option enables the connection of multiple Agents from different Kubernetes clusters. It allows you to consolidate all tests in a unified Dashboard, organized by Environments.
+
+- *Requirements:* A domain name and certificates are necessary as the installation exposes Testkube endpoints to the outside world.
+
+- *Benefit:* Offers a comprehensive view across clusters and environments.
+
+**One-cluster Installation:**
+
+- *Description:* With this option, you can connect only one Agent (e.g. your existing Testkube OSS) within the same Kubernetes cluster. Access to the Dashboard is achieved through port-forwarding to localhost.
+
+- *Requirements:* No domain names or certificates are required for this apEnterpriseach.
+
+- *Benefit:* Simplified setup suitable for a single-cluster environment without the need for external exposure.
+
+
+## Multi-cluster Installation
+
+If you decide to go with multiple-cluster installation, please ensure that you have the following prerequisites in place:
+
+- [cert-manager](https://cert-manager.io/docs/installation/) (version 1.14.2+ ) or have your own certificates in place;
+- [NGINX Controller](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/) (version 4.8.3+) or any other service of your choice to configure ingress traffic;
+- a domain for exposing Tetskube endpoints.
+
+### Ingress
+To make a central Testkube Enterprise cluster reachable for multiple Agents we need to expose [endpoints](https://docs.testkube.io/testkube-enterprise/articles/usage-guide#domain) and create certificates.
+Testkube Enterprise requires the NGINX Controller and it is the only supported Ingress Controller for now. By default, Testkube Enterprise integrates with cert-manager. However, if you choose to use your own certificates, provide them as specified [here](https://docs.testkube.io/testkube-enterprise/articles/usage-guide#tls).
+Create a `values.yaml` with your domain and certificate configuration. Additionally include a secretRef to the secret with the license that was created earlier:
+
+`values.yaml`
+```yaml
+global:
+ domain: you-domain.it.com
+ enterpriseLicenseSecretRef: testkube-enterprise-license
+
+ certificateProvider: "cert-manager"
+ certManager:
+ issuerRef: letsencrypt
+
+```
+
+###Auth
+Testkube Enterprise utilizes [Dex](https://dexidp.io/) for authentication and authorization. For detailed instruction on configuring Dex, please refer to the [Identity Provider](https://docs.testkube.io/testkube-enterprise/articles/auth) document. You may start with creating static users if you do not have any Identity Provider. Here is an example of usage:
+
+
+`values.yaml`
+```yaml
+dex:
+ configTemplate:
+ additionalConfig: |
+ enablePasswordDB: true
+ staticPasswords:
+ - email: "admin@example.com"
+ # bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2)
+ hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
+ username: "admin"
+ userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
+
+```
+
+### Deployment
+Now, let’s deploy Testkube Enterprise. Please refer to the installation commands [here](https://docs.testkube.io/testkube-enterprise/articles/usage-guide/#installation). Do not forget to pass your customized `values.yaml` file.
+
+It may take a few minutes for the certificates to be issued and for the pods to reach `Ready` status. Once everything is up and running, you may go to dashboard.your-domain.it.com and log in.
+
+The only thing that is remaining is to connect Testkube OSS as an Agent. [Create a new environment](https://docs.testkube.io/testkube-pro/articles/environment-management/#creating-a-new-environment) and duplicate the installation command. Execute this command in the cluster where Testkube OSS is deployed to seamlessly upgrade the existing installation to Agent mode. Pay attention to the namespace name, ensuring it aligns with the namespace of Testkube OSS.
+
+After running the command, navigate to the Dashboard and you will see all your tests available.
+
+
+## One-cluster Installation
+
+It is possible to deploy Testkube Enterprise and connect an Agent to it in the same k8s cluster without exposing endpoints to the outside world. By simply running `bash <(curl -sSLf https://download.testkube.io)` and entering the license key (for now it works with Online licenses only), you will have a working environment in just a few minutes. The script will ask you for the namespace where your Testkube OSS is running and automatically connect it as an Agent, preserving all created tests. Please check out the [official documentation](https://docs.testkube.io/testkube-enterprise/articles/usage-guide/#installation-of-testkube-enterprise-and-an-agent-in-the-same-cluster) for more detailed info.
\ No newline at end of file
diff --git a/docs/docs/testkube-enterprise/articles/testkube-enterprise.md b/docs/docs/testkube-enterprise/articles/testkube-enterprise.md
new file mode 100644
index 00000000000..b0671cefaa3
--- /dev/null
+++ b/docs/docs/testkube-enterprise/articles/testkube-enterprise.md
@@ -0,0 +1,13 @@
+# Testkube Enterprise
+
+If your company has security and compliance requirements and the capacity to manage your own Kubernetes deployments, visit https://testkube.io/get-started and click **Download** to sign up for Testkube Enterprise.
+
+![Download Testkube Enterprise](../../img/enterprise-download.png)
+
+Enter your information in the form and click **Download**.
+
+![Download Testkube Enterprise](../../img/enterprise-download-form.png)
+
+Once you submit the form you will receive an email with an Enterprise license and installion instructions.
+
+Visit our additional documentation to [install and use Helm Charts](https://docs.testkube.io/testkube-enterprise/articles/usage-guide) and [configure Identity Providers](https://docs.testkube.io/testkube-enterprise/articles/auth).
\ No newline at end of file
diff --git a/docs/docs/testkube-enterprise/articles/usage-guide.md b/docs/docs/testkube-enterprise/articles/usage-guide.md
index fa04802c7c5..269224880df 100644
--- a/docs/docs/testkube-enterprise/articles/usage-guide.md
+++ b/docs/docs/testkube-enterprise/articles/usage-guide.md
@@ -3,6 +3,8 @@
- [Testkube Enterprise Helm Chart Installation and Usage Guide](#testkube-enterprise-helm-chart-installation-and-usage-guide)
+ - [Installation of Testkube Enterprise and an Agent in the same cluster](#installation-of-testkube-enterprise-and-an-agent-in-the-same-cluster)
+ - [Installation of Testkube Enterprise and an Agent in multiple clusters](#installation-of-testkube-enterprise-and-an-agent-in-multiple-clusters)
- [Prerequisites](#prerequisites)
- [Configuration](#configuration)
- [Docker images](#docker-images)
@@ -34,6 +36,35 @@ Welcome to the Testkube Enterprise Helm chart installation and usage guide.
This comprehensive guide provides step-by-step instructions for installing and utilizing the Testkube Enterprise Helm chart.
Testkube Enterprise is a cutting-edge Kubernetes-native testing platform designed to optimize your testing and quality assurance processes with enterprise-grade features.
+## Installation of Testkube Enterprise and an Agent in the same cluster
+
+It is possible to deploy an instance of Testkube Enteprise and connect an Agent to it in the same k8s cluster without exposing endpoints to the outside world.
+This way gives you a more customizable set-up and allows you to have a working environment in just a few minutes. It can be deployed locally or in any other k8s cluster.
+
+For this you will need:
+- kubectl
+- connection to a running k8s cluster
+
+Simply run `bash <(curl -sSLf https://download.testkube.io)` and enter a license key, the script will do the rest.
+
+:::note
+The script will do a port-forward to the following ports: `8080`, `8090`, `5556` in the background mode. Please make sure they are available.
+:::
+
+The installation will take about 4-5 min, once it is completed you will have the Testkube Enterprise deployed in `testkube-enterprise` namespace and the Testkube Agent in `testkube` namespace. The UI is available at http://localhost:8080. We use Dex for authentication, so once you open the URL you will see a login page - use `admin@example.com` and `password` as a username and a password respectively.
+
+Voila! Now you can create tests, testsuites in both CLI and UI and explore the power of Testkube!
+
+:::note
+If you close a terminal you may do a port-forward with the following commands:
+```shell
+kubectl port-forward svc/testkube-enterprise-ui 8080:8080 --namespace testkube-enterprise &
+kubectl port-forward svc/testkube-enterprise-api 8090:8088 --namespace testkube-enterprise &
+kubectl port-forward svc/testkube-enterprise-dex 5556:5556 --namespace testkube-enterprise &
+```
+:::
+
+## Installation of Testkube Enterprise and an Agent in multiple clusters
## Prerequisites
Before you proceed with the installation, please ensure that you have the following prerequisites in place:
diff --git a/docs/docs/testkube-pro/articles/AI-test-insights.md b/docs/docs/testkube-pro/articles/AI-test-insights.md
index 788f6ad817f..6160d6395f6 100644
--- a/docs/docs/testkube-pro/articles/AI-test-insights.md
+++ b/docs/docs/testkube-pro/articles/AI-test-insights.md
@@ -82,7 +82,7 @@ Now if you execute the test again, it passes. Note that the AI Analysis tab is n
This was a simple demo to show you how to use Testkube’s AI Analysis feature to analyze logs and fix failing tests quickly. You can create complex tests to test your applications and infrastructure.
-If you have feedback or concerns using the AI analysis feature, do share them on our [Discord channel](https://discord.com/invite/6zupCZFQbe) for faster resolution.
+If you have feedback or concerns using the AI analysis feature, do share them on our [Slack Channel](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email) for faster resolution.
diff --git a/docs/docs/testkube-pro/articles/log-highlighting.md b/docs/docs/testkube-pro/articles/log-highlighting.md
index 9db9b20632c..5a60d300bc6 100644
--- a/docs/docs/testkube-pro/articles/log-highlighting.md
+++ b/docs/docs/testkube-pro/articles/log-highlighting.md
@@ -28,11 +28,34 @@ By default, all the categories are active.
![log-highlighting-filtering.png](../../img/log-highlighting-filtering.png)
-There are 4 categories at the moment, represented with few keywords each:
+
+
+## Configuring Keyword Categories
+
+There are 4 default categories, represented with a few keywords each:
| Category | Keywords |
|----------------------------|---------------------------------------------------------------------|
| **Error Keywords** | Error, Exception, Fail, Critical, Fatal |
| **Connection** | Connection, Disconnect, Lost, Timeout, Refused, Handshake, Retrying |
| **Resource Issues** | OutOfMemory, MemoryLeak, ResourceExhausted, LimitExceeded, Quota |
-| **Access & Authorization** | Denied, Unauthorized, Forbidden, Invalid, Invalid Token, Expired |
\ No newline at end of file
+| **Access & Authorization** | Denied, Unauthorized, Forbidden, Invalid, Invalid Token, Expired |
+
+To configure keyword categories for highlighting:
+
+1. Navigate to the Environment settings -> Keyword handling.
+2. Add a new category by providing a color, a group name, and an array of keywords.
+3. Save the changes.
+
+## Example Configuration
+
+![keyword-highlights-configuration.png](../../img/keyword-highlights-configuration.png)
+
+In the example above, there are default categories along with a new one:
+
+- **Custom Category** (New):
+ - **Color**: Green
+ - **Group Name**: Custom Category
+ - **Keywords**: [CustomKeyword1, CustomKeyword2, CustomKeyword3]
+
+These keywords will be highlighted in logs when the custom category is active.
\ No newline at end of file
diff --git a/docs/docs/testkube-pro/articles/running-parallel-tests-with-test-suite.md b/docs/docs/testkube-pro/articles/running-parallel-tests-with-test-suite.md
index c00a099bc19..a336bdafbfb 100644
--- a/docs/docs/testkube-pro/articles/running-parallel-tests-with-test-suite.md
+++ b/docs/docs/testkube-pro/articles/running-parallel-tests-with-test-suite.md
@@ -1,6 +1,6 @@
# Advanced Test Orchestration
-Creating Test Suites with Tracetest allows for the orchestration of tests. Individual tests that can be run at the same time, in parallel, helps to speed up overall testing.
+Creating Test Suites with Testkube allows for the orchestration of tests. Individual tests that can be run at the same time, in parallel, helps to speed up overall testing.
## Running Parallel Tests in a Test Suite
@@ -26,4 +26,4 @@ For this test suite, we have added 5 tests that all run in parallel:
Here is an example of a Test Suite sequence with 2 tests running in parallel and, when they complete, a single test runs, then 2 addtional parallel tests:
-![Test and Order of Execution](../../img/test-and-order-of-execution.png)
\ No newline at end of file
+![Test and Order of Execution](../../img/test-and-order-of-execution.png)
diff --git a/docs/docs/testkube-pro/articles/status-pages.md b/docs/docs/testkube-pro/articles/status-pages.md
index b5ae4bf4677..065d2ab754d 100644
--- a/docs/docs/testkube-pro/articles/status-pages.md
+++ b/docs/docs/testkube-pro/articles/status-pages.md
@@ -12,6 +12,8 @@ export const ProBadge = () => {
The Testkube status pages are designed to help both technical and non-technical users understand and utilize the results of tests run on Testkube effectively. Whether you're a developer, project manager, or simply a stakeholder interested in monitoring software project status via running tests, Testkube has you covered.
+You can see a live example of a Status Page [here](https://app.testkube.io/status/testkube).
+
## Overview
![status-page-main](../../img/status-page-main.png)
@@ -222,4 +224,4 @@ Custom Slugs: If applicable, configure custom slugs for your status pages to mat
These best practices will help you maximize the effectiveness of Testkube Status Pages, ensuring that it serves as a valuable communication tool for both technical and non-technical stakeholders. By following these guidelines, you can maintain transparency, respond efficiently to incidents, and provide a reliable source of information about the status of your software projects.
-If you have any questions or need assistance, our team is ready to assist you in our [Discord channel](https://discord.com/invite/6zupCZFQbe).
+If you have any questions or need assistance, our team is ready to assist you in our [Slack Channel](https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email).
diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js
index 93e6d0cca30..2a78b293129 100644
--- a/docs/docusaurus.config.js
+++ b/docs/docusaurus.config.js
@@ -114,8 +114,8 @@ const config = {
title: "Community",
items: [
{
- label: "Discord",
- href: "https://discord.com/invite/6zupCZFQbe",
+ label: "Slack",
+ href: "https://testkubeworkspace.slack.com/join/shared_invite/zt-2arhz5vmu-U2r3WZ69iPya5Fw0hMhRDg#/shared-invite/email",
},
{
label: "Twitter",
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 7c5ed49d3d0..5e36284d6ba 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -6712,16 +6712,15 @@
}
},
"node_modules/follow-redirects": {
- "version": "1.15.1",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
- "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
+ "version": "1.15.4",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
+ "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
- "license": "MIT",
"engines": {
"node": ">=4.0"
},
@@ -18513,9 +18512,9 @@
}
},
"follow-redirects": {
- "version": "1.15.1",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
- "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
+ "version": "1.15.4",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
+ "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw=="
},
"foreach": {
"version": "2.0.6",
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 2409cbc7ab0..db1f77229c0 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -72,7 +72,7 @@ const sidebars = {
"articles/webhooks",
"articles/test-sources",
"articles/test-executions",
- "articles/templates",
+ "articles/templates",
],
},
{
@@ -101,9 +101,26 @@ const sidebars = {
id: "articles/cicd-overview",
},
items: [
- "articles/github-actions",
+ {
+ type: "category",
+ label: "Github Actions",
+ link: {
+ type: "doc",
+ id: "articles/github-actions"
+ },
+ items: [
+ {
+ type: "doc",
+ id: "articles/run-tests-with-github-actions",
+ label: "Migrate from testkube-run-action"
+ }
+ ]
+ },
"articles/gitlab",
- "articles/run-tests-with-github-actions",
+ "articles/jenkins",
+ "articles/jenkins-ui",
+ "articles/azure",
+ "articles/circleci",
"articles/testkube-cli-docker",
{
type: "category",
@@ -133,7 +150,7 @@ const sidebars = {
"articles/generate-test-crds",
"articles/logging",
"articles/install-cli",
- "articles/uninstall"
+ "articles/uninstall",
],
},
{
@@ -161,7 +178,7 @@ const sidebars = {
"test-types/executor-tracetest",
"test-types/executor-zap",
"test-types/prebuilt-executor",
- "test-types/container-executor",
+ "test-types/container-executor",
"test-types/executor-distributed-jmeter",
],
},
@@ -191,9 +208,10 @@ const sidebars = {
type: "category",
label: "Testkube Enterprise",
items: [
- "testkube-enterprise/articles/usage-guide",
- "testkube-enterprise/articles/auth"
- ],
+ "testkube-enterprise/articles/testkube-enterprise",
+ "testkube-enterprise/articles/usage-guide",
+ "testkube-enterprise/articles/auth",
+ "testkube-enterprise/articles/migrating-from-oss-to-pro"],
},
"articles/testkube-oss",
{
@@ -242,6 +260,13 @@ const sidebars = {
},
],
},
+ {
+ type: "category",
+ label: "FAQs",
+ items: [
+ "articles/testkube-licensing-FAQ",
+ ],
+ },
],
// But you can create a sidebar manually
diff --git a/go.mod b/go.mod
index c1e3f72a865..6d36ecc5114 100644
--- a/go.mod
+++ b/go.mod
@@ -6,9 +6,10 @@ require (
github.com/99designs/gqlgen v0.17.27
github.com/Masterminds/semver v1.5.0
github.com/adhocore/gronx v1.6.3
+ github.com/bmatcuk/doublestar/v4 v4.6.1
github.com/cdevents/sdk-go v0.3.0
github.com/cli/cli/v2 v2.20.2
- github.com/cloudevents/sdk-go/v2 v2.14.0
+ github.com/cloudevents/sdk-go/v2 v2.15.2
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/creasty/defaults v1.7.0
github.com/denisbrodbeck/machineid v1.0.1
@@ -18,15 +19,17 @@ require (
github.com/gabriel-vasile/mimetype v1.4.1
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572
github.com/gofiber/adaptor/v2 v2.1.29
- github.com/gofiber/fiber/v2 v2.51.0
+ github.com/gofiber/fiber/v2 v2.52.1
github.com/gofiber/websocket/v2 v2.1.1
github.com/golang/mock v1.6.0
github.com/gookit/color v1.5.3
github.com/gorilla/websocket v1.5.0
+ github.com/h2non/filetype v1.1.3
github.com/joshdk/go-junit v1.0.0
+ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/kelseyhightower/envconfig v1.4.0
github.com/kubepug/kubepug v1.7.1
- github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20231214095624-483fef2d8731
+ github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240314074148-03a6d2dd1f3b
github.com/minio/minio-go/v7 v7.0.47
github.com/montanaflynn/stats v0.6.6
github.com/moogar0880/problems v0.1.1
@@ -37,7 +40,7 @@ require (
github.com/onsi/ginkgo/v2 v2.13.2
github.com/onsi/gomega v1.30.0
github.com/otiai10/copy v1.11.0
- github.com/prometheus/client_golang v1.16.0
+ github.com/prometheus/client_golang v1.18.0
github.com/pterm/pterm v0.12.62
github.com/segmentio/analytics-go/v3 v3.2.1
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
@@ -45,7 +48,7 @@ require (
github.com/spf13/afero v1.10.0
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
- github.com/valyala/fasthttp v1.50.0
+ github.com/valyala/fasthttp v1.51.0
github.com/vektah/gqlparser/v2 v2.5.2-0.20230422221642-25e09f9d292d
go.mongodb.org/mongo-driver v1.11.0
go.uber.org/zap v1.26.0
@@ -69,21 +72,21 @@ require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/briandowns/spinner v1.19.0 // indirect
- github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da // indirect
+ github.com/charmbracelet/glamour v0.6.0 // indirect
github.com/cli/browser v1.1.0 // indirect
github.com/cli/go-gh v0.1.3-0.20221102170023-e3ec45fb1d1b // indirect
github.com/cli/safeexec v1.0.0 // indirect
github.com/cli/shurcooL-graphql v0.0.2 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/dlclark/regexp2 v1.8.0 // indirect
- github.com/emicklei/go-restful/v3 v3.11.0 // indirect
+ github.com/emicklei/go-restful/v3 v3.11.2 // indirect
github.com/evanphx/json-patch/v5 v5.7.0 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
- github.com/go-playground/locales v0.14.0 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.1 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
@@ -92,10 +95,9 @@ require (
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.1 // indirect
github.com/henvic/httpretty v0.1.0 // indirect
- github.com/itchyny/gojq v0.12.9 // indirect
- github.com/itchyny/timefmt-go v0.1.4 // indirect
+ github.com/itchyny/gojq v0.12.14 // indirect
+ github.com/itchyny/timefmt-go v0.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
- github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
@@ -103,6 +105,7 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/microcosm-cc/bluemonday v1.0.21 // indirect
github.com/minio/highwayhash v1.0.2 // indirect
@@ -139,7 +142,7 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/dustin/go-humanize v1.0.1
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.3.0 // indirect
@@ -149,21 +152,20 @@ require (
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.6.0
github.com/google/gofuzz v1.2.0 // indirect
- github.com/google/uuid v1.4.0
+ github.com/google/uuid v1.5.0
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.2 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
- github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1
github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/prometheus/client_model v0.4.0 // indirect
- github.com/prometheus/common v0.44.0 // indirect
+ github.com/prometheus/client_model v0.5.0 // indirect
+ github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/robfig/cron v1.2.0
github.com/rs/xid v1.4.0 // indirect
diff --git a/go.sum b/go.sum
index abb930584df..af6a132170e 100644
--- a/go.sum
+++ b/go.sum
@@ -80,12 +80,15 @@ github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
+github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymanbagabas/go-osc52 v1.2.1 h1:q2sWUyDcozPLcLabEMd+a+7Ea2DitxZVN9hTxab9L4E=
github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
+github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E=
@@ -95,8 +98,8 @@ github.com/cdevents/sdk-go v0.3.0/go.mod h1:8EFl9VDZkxEmO/sr06Phzr501OiU6B5d04+e
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da h1:FGz53GWQRiKQ/5xUsoCCkewSQIC7u81Scaxx2nUy3nM=
-github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da/go.mod h1:HXz79SMFnF9arKxqeoHWxmo1BhplAH7wehlRhKQIL94=
+github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
+github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -111,8 +114,8 @@ github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5
github.com/cli/shurcooL-graphql v0.0.2 h1:rwP5/qQQ2fM0TzkUTwtt6E2LbIYf6R+39cUXTa04NYk=
github.com/cli/shurcooL-graphql v0.0.2/go.mod h1:tlrLmw/n5Q/+4qSvosT+9/W5zc8ZMjnJeYBxSdb4nWA=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cloudevents/sdk-go/v2 v2.14.0 h1:Nrob4FwVgi5L4tV9lhjzZcjYqFVyJzsA56CwPaPfv6s=
-github.com/cloudevents/sdk-go/v2 v2.14.0/go.mod h1:xDmKfzNjM8gBvjaF8ijFjM1VYOVUEeUfapHMUX1T5To=
+github.com/cloudevents/sdk-go/v2 v2.15.2 h1:54+I5xQEnI73RBhWHxbI1XJcqOFOVJN85vb41+8mHUc=
+github.com/cloudevents/sdk-go/v2 v2.15.2/go.mod h1:lL7kSWAE/V8VI4Wh0jbL2v/jvqsm6tjmaQBSvxcv4uE=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@@ -142,8 +145,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 h1:90Ly+6UfUypEF6vvvW5rQIv9opIL8CbmW9FT20LDQoY=
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8=
-github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
-github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU=
+github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -185,8 +188,9 @@ github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogB
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
@@ -198,8 +202,8 @@ github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3a
github.com/gofiber/adaptor/v2 v2.1.29 h1:JnYd6fbqVM9D4zPchk+kg89PfxyuKqZKhBWGQDHfKH4=
github.com/gofiber/adaptor/v2 v2.1.29/go.mod h1:z4mAV9mMsUgIEVGGS5Ii6ZMTJq4VdV1KWL1JAbsZdUA=
github.com/gofiber/fiber/v2 v2.39.0/go.mod h1:Cmuu+elPYGqlvQvdKyjtYsjGMi69PDp8a1AY2I5B2gM=
-github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ=
-github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U=
+github.com/gofiber/fiber/v2 v2.52.1 h1:1RoU2NS+b98o1L77sdl5mboGPiW+0Ypsi5oLmcYlgHI=
+github.com/gofiber/fiber/v2 v2.52.1/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/websocket/v2 v2.1.1 h1:Q88s88UL8B+elZTT/QB+ocDb1REhdMEmnysI0C9zzqs=
github.com/gofiber/websocket/v2 v2.1.1/go.mod h1:F0ES7DhlFrNyHtC2UGey2KYI+zdqIURRMbSF0C4qdGQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -281,8 +285,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
-github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
+github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
@@ -296,6 +300,8 @@ github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
+github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -312,10 +318,10 @@ github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/itchyny/gojq v0.12.9 h1:biKpbKwMxVYhCU1d6mR7qMr3f0Hn9F5k5YykCVb3gmM=
-github.com/itchyny/gojq v0.12.9/go.mod h1:T4Ip7AETUXeGpD+436m+UEl3m3tokRgajd5pRfsR5oE=
-github.com/itchyny/timefmt-go v0.1.4 h1:hFEfWVdwsEi+CY8xY2FtgWHGQaBaC3JeHd+cve0ynVM=
-github.com/itchyny/timefmt-go v0.1.4/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
+github.com/itchyny/gojq v0.12.14 h1:6k8vVtsrhQSYgSGg827AD+PVVaB1NLXEdX+dda2oZCc=
+github.com/itchyny/gojq v0.12.14/go.mod h1:y1G7oO7XkcR1LPZO59KyoCRy08T3j9vDYRV0GgYSS+s=
+github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
+github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
@@ -354,8 +360,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kubepug/kubepug v1.7.1 h1:LKhfSxS8Y5mXs50v+3Lpyec+cogErDLcV7CMUuiaisw=
github.com/kubepug/kubepug v1.7.1/go.mod h1:lv+HxD0oTFL7ZWjj0u6HKhMbbTIId3eG7aWIW0gyF8g=
-github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20231214095624-483fef2d8731 h1:otwyKLt+5Trz5c2EMS+yjMV33O26sdNlcJJkRSiu2tU=
-github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20231214095624-483fef2d8731/go.mod h1:P47tw1nKQFufdsZndyq2HG2MSa0zK/lU0XpRfZtEmIk=
+github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240312153624-07cd5e8ce0be h1:cs5m8bekmvEcyvFT37KgUEduv6XrdUfmX5WqZAbLUCY=
+github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240312153624-07cd5e8ce0be/go.mod h1:P47tw1nKQFufdsZndyq2HG2MSa0zK/lU0XpRfZtEmIk=
+github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240314074148-03a6d2dd1f3b h1:O41BWxgeUQJaBz4bcDlQhvHhiWLHPvsTxtvIKmwxjOs=
+github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240314074148-03a6d2dd1f3b/go.mod h1:P47tw1nKQFufdsZndyq2HG2MSa0zK/lU0XpRfZtEmIk=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
@@ -368,7 +376,6 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -380,12 +387,11 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
-github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
-github.com/microcosm-cc/bluemonday v1.0.19/go.mod h1:QNzV2UbLK2/53oIIwTOyLUSABMkjZ4tqiyC1g/DyqxE=
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
@@ -412,7 +418,7 @@ github.com/moogar0880/problems v0.1.1 h1:bktLhq8NDG/czU2ZziYNigBFksx13RaYe5AVdNm
github.com/moogar0880/problems v0.1.1/go.mod h1:5Dxrk2sD7BfBAgnOzQ1yaTiuCYdGPUh49L8Vhfky62c=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
-github.com/muesli/termenv v0.11.0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
+github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
github.com/muesli/termenv v0.14.0 h1:8x9NFfOe8lmIWK4pgy3IfVEy47f+ppe3tUqdPZG2Uy0=
github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -454,13 +460,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEnkRx9k=
github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
-github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
-github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
+github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
+github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
-github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
-github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
-github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
+github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
+github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
@@ -539,8 +545,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.33.0/go.mod h1:KJRK/MXx0J+yd0c5hlR+s1tIHD72sniU8ZJjl97LIw4=
github.com/valyala/fasthttp v1.40.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
-github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=
-github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
+github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
+github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/vektah/gqlparser/v2 v2.5.2-0.20230422221642-25e09f9d292d h1:ibuD+jp4yLoOY4w8+5+2fDq0ufJ/noPn/cPntJMWB1E=
@@ -569,8 +575,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
@@ -677,7 +683,6 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -685,6 +690,7 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
@@ -755,7 +761,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/goreleaser_files/.goreleaser-docker-build-logs-server.yml b/goreleaser_files/.goreleaser-docker-build-logs-server.yml
index 69754ae1a37..122f40df07a 100644
--- a/goreleaser_files/.goreleaser-docker-build-logs-server.yml
+++ b/goreleaser_files/.goreleaser-docker-build-logs-server.yml
@@ -5,12 +5,14 @@ env:
# https://github.com/goreleaser/goreleaser/pull/3199
# To use a builder other than "default", set this variable.
# Necessary for, e.g., GitHub actions cache integration.
+ - DOCKER_REPO={{ if index .Env "DOCKER_REPO" }}{{ .Env.DOCKER_REPO }}{{ else }}kubeshop{{ end }}
- DOCKER_BUILDX_BUILDER={{ if index .Env "DOCKER_BUILDX_BUILDER" }}{{ .Env.DOCKER_BUILDX_BUILDER }}{{ else }}default{{ end }}
# Setup to enable Docker to use, e.g., the GitHub actions cache; see
# https://docs.docker.com/build/building/cache/backends/
# https://github.com/moby/buildkit#export-cache
- DOCKER_BUILDX_CACHE_FROM={{ if index .Env "DOCKER_BUILDX_CACHE_FROM" }}{{ .Env.DOCKER_BUILDX_CACHE_FROM }}{{ else }}type=registry{{ end }}
- DOCKER_BUILDX_CACHE_TO={{ if index .Env "DOCKER_BUILDX_CACHE_TO" }}{{ .Env.DOCKER_BUILDX_CACHE_TO }}{{ else }}type=inline{{ end }}
+ - DOCKER_IMAGE_TAG={{ if index .Env "DOCKER_IMAGE_TAG" }}{{ .Env.DOCKER_IMAGE_TAG }}{{ else }}{{ end }}
builds:
- id: "linux"
main: ./cmd/logs
@@ -31,7 +33,8 @@ dockers:
goos: linux
goarch: amd64
image_templates:
- - "kubeshop/testkube-logs-server:{{ .Env.DOCKER_IMAGE_TAG }}-amd64"
+ - "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-server:{{ .Version }}-amd64{{ end }}"
+ - "{{ if .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-server:{{ .Env.DOCKER_IMAGE_TAG }}-amd64{{ end }}"
build_flag_templates:
- "--platform=linux/amd64"
- "--label=org.opencontainers.image.title={{ .ProjectName }}"
@@ -48,7 +51,8 @@ dockers:
goos: linux
goarch: arm64
image_templates:
- - "kubeshop/testkube-logs-server:{{ .Env.DOCKER_IMAGE_TAG }}-arm64v8"
+ - "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-server:{{ .Version }}-arm64v8{{ end }}"
+ - "{{ if .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-server:{{ .Env.DOCKER_IMAGE_TAG }}-arm64v8{{ end }}"
build_flag_templates:
- "--platform=linux/arm64/v8"
- "--label=org.opencontainers.image.created={{ .Date }}"
@@ -61,14 +65,14 @@ dockers:
- "--build-arg=ALPINE_IMAGE={{ .Env.ALPINE_IMAGE }}"
docker_manifests:
- - name_template: kubeshop/testkube-logs-server:{{ .Env.DOCKER_IMAGE_TAG }}
+ - name_template: "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-server:{{ .Version }}{{ end }}"
image_templates:
- - kubeshop/testkube-logs-server:{{ .Env.DOCKER_IMAGE_TAG }}-amd64
- - kubeshop/testkube-logs-server:{{ .Env.DOCKER_IMAGE_TAG }}-arm64v8
- - name_template: kubeshop/testkube-logs-server:latest
+ - "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-server:{{ .Version }}-amd64{{ end }}"
+ - "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-server:{{ .Version }}-arm64v8{{ end }}"
+ - name_template: "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-server:latest{{ end }}"
image_templates:
- - kubeshop/testkube-logs-server:{{ .Env.DOCKER_IMAGE_TAG }}-amd64
- - kubeshop/testkube-logs-server:{{ .Env.DOCKER_IMAGE_TAG }}-arm64v8
+ - "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-server:{{ .Version }}-amd64{{ end }}"
+ - "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-server:{{ .Version }}-arm64v8{{ end }}"
release:
disable: true
diff --git a/goreleaser_files/.goreleaser-docker-build-logs-sidecar.yml b/goreleaser_files/.goreleaser-docker-build-logs-sidecar.yml
index a11a8b20e35..4979267192e 100644
--- a/goreleaser_files/.goreleaser-docker-build-logs-sidecar.yml
+++ b/goreleaser_files/.goreleaser-docker-build-logs-sidecar.yml
@@ -5,12 +5,14 @@ env:
# https://github.com/goreleaser/goreleaser/pull/3199
# To use a builder other than "default", set this variable.
# Necessary for, e.g., GitHub actions cache integration.
+ - DOCKER_REPO={{ if index .Env "DOCKER_REPO" }}{{ .Env.DOCKER_REPO }}{{ else }}kubeshop{{ end }}
- DOCKER_BUILDX_BUILDER={{ if index .Env "DOCKER_BUILDX_BUILDER" }}{{ .Env.DOCKER_BUILDX_BUILDER }}{{ else }}default{{ end }}
# Setup to enable Docker to use, e.g., the GitHub actions cache; see
# https://docs.docker.com/build/building/cache/backends/
# https://github.com/moby/buildkit#export-cache
- DOCKER_BUILDX_CACHE_FROM={{ if index .Env "DOCKER_BUILDX_CACHE_FROM" }}{{ .Env.DOCKER_BUILDX_CACHE_FROM }}{{ else }}type=registry{{ end }}
- DOCKER_BUILDX_CACHE_TO={{ if index .Env "DOCKER_BUILDX_CACHE_TO" }}{{ .Env.DOCKER_BUILDX_CACHE_TO }}{{ else }}type=inline{{ end }}
+ - DOCKER_IMAGE_TAG={{ if index .Env "DOCKER_IMAGE_TAG" }}{{ .Env.DOCKER_IMAGE_TAG }}{{ else }}{{ end }}
builds:
- id: "linux"
main: ./cmd/sidecar
@@ -31,7 +33,8 @@ dockers:
goos: linux
goarch: amd64
image_templates:
- - "kubeshop/testkube-logs-sidecar:{{ .Env.DOCKER_IMAGE_TAG }}-amd64"
+ - "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-sidecar:{{ .Version }}-amd64{{ end }}"
+ - "{{ if .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-sidecar:{{ .Env.DOCKER_IMAGE_TAG }}-amd64{{ end }}"
build_flag_templates:
- "--platform=linux/amd64"
- "--label=org.opencontainers.image.title={{ .ProjectName }}"
@@ -48,7 +51,8 @@ dockers:
goos: linux
goarch: arm64
image_templates:
- - "kubeshop/testkube-logs-sidecar:{{ .Env.DOCKER_IMAGE_TAG }}-arm64v8"
+ - "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-sidecar:{{ .Version }}-arm64v8{{ end }}"
+ - "{{ if .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-sidecar:{{ .Env.DOCKER_IMAGE_TAG }}-arm64v8{{ end }}"
build_flag_templates:
- "--platform=linux/arm64/v8"
- "--label=org.opencontainers.image.created={{ .Date }}"
@@ -61,14 +65,15 @@ dockers:
- "--build-arg=ALPINE_IMAGE={{ .Env.ALPINE_IMAGE }}"
docker_manifests:
- - name_template: kubeshop/testkube-logs-sidecar:{{ .Env.DOCKER_IMAGE_TAG }}
+ - name_template: "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-sidecar:{{ .Version }}{{ end }}"
image_templates:
- - kubeshop/testkube-logs-sidecar:{{ .Env.DOCKER_IMAGE_TAG }}-amd64
- - kubeshop/testkube-logs-sidecar:{{ .Env.DOCKER_IMAGE_TAG }}-arm64v8
- - name_template: kubeshop/testkube-logs-sidecar:latest
+ - "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-sidecar:{{ .Version }}-amd64{{ end }}"
+ - "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-sidecar:{{ .Version }}-arm64v8{{ end }}"
+ - name_template: "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-sidecar:latest{{ end }}"
image_templates:
- - kubeshop/testkube-logs-sidecar:{{ .Env.DOCKER_IMAGE_TAG }}-amd64
- - kubeshop/testkube-logs-sidecar:{{ .Env.DOCKER_IMAGE_TAG }}-arm64v8
+ - "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-sidecar:{{ .Version }}-amd64{{ end }}"
+ - "{{ if not .Env.DOCKER_IMAGE_TAG }}{{ .Env.DOCKER_REPO }}/testkube-logs-sidecar:{{ .Version }}-arm64v8{{ end }}"
+
release:
disable: true
diff --git a/goreleaser_files/.goreleaser-docker-build-testworkflow-init.yml b/goreleaser_files/.goreleaser-docker-build-testworkflow-init.yml
new file mode 100644
index 00000000000..6aecfd57039
--- /dev/null
+++ b/goreleaser_files/.goreleaser-docker-build-testworkflow-init.yml
@@ -0,0 +1,93 @@
+project_name: testkube-tw-init
+
+env:
+ # Goreleaser always uses the docker buildx builder with name "default"; see
+ # https://github.com/goreleaser/goreleaser/pull/3199
+ # To use a builder other than "default", set this variable.
+ # Necessary for, e.g., GitHub actions cache integration.
+ - DOCKER_REPO={{ if index .Env "DOCKER_REPO" }}{{ .Env.DOCKER_REPO }}{{ else }}kubeshop{{ end }}
+ - DOCKER_BUILDX_BUILDER={{ if index .Env "DOCKER_BUILDX_BUILDER" }}{{ .Env.DOCKER_BUILDX_BUILDER }}{{ else }}default{{ end }}
+ # Setup to enable Docker to use, e.g., the GitHub actions cache; see
+ # https://docs.docker.com/build/building/cache/backends/
+ # https://github.com/moby/buildkit#export-cache
+ - DOCKER_BUILDX_CACHE_FROM={{ if index .Env "DOCKER_BUILDX_CACHE_FROM" }}{{ .Env.DOCKER_BUILDX_CACHE_FROM }}{{ else }}type=registry{{ end }}
+ - DOCKER_BUILDX_CACHE_TO={{ if index .Env "DOCKER_BUILDX_CACHE_TO" }}{{ .Env.DOCKER_BUILDX_CACHE_TO }}{{ else }}type=inline{{ end }}
+ # Build image with commit sha tag
+ - IMAGE_TAG_SHA={{ if index .Env "IMAGE_TAG_SHA" }}{{ .Env.IMAGE_TAG_SHA }}{{ else }}{{ end }}
+builds:
+ - id: "linux"
+ main: ./cmd/tcl/testworkflow-init
+ binary: testworkflow-init
+ env:
+ - CGO_ENABLED=0
+ goos:
+ - linux
+ goarch:
+ - amd64
+ - arm64
+ mod_timestamp: "{{ .CommitTimestamp }}"
+ ldflags:
+ -X github.com/kubeshop/testkube/pkg/version.Version={{ .Version }}
+ -X github.com/kubeshop/testkube/pkg/version.Commit={{ .FullCommit }}
+ -s -w
+dockers:
+ - dockerfile: ./build/testworkflow-init/Dockerfile
+ use: buildx
+ goos: linux
+ goarch: amd64
+ image_templates:
+ - "{{ if .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-init:{{ .ShortCommit }}{{ end }}"
+ - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-init:{{ .Version }}-amd64{{ end }}"
+ build_flag_templates:
+ - "--platform=linux/amd64"
+ - "--label=org.opencontainers.image.title={{ .ProjectName }}"
+ - "--label=org.opencontainers.image.created={{ .Date}}"
+ - "--label=org.opencontainers.image.revision={{ .FullCommit }}"
+ - "--label=org.opencontainers.image.version={{ .Version }}"
+ - "--builder={{ .Env.DOCKER_BUILDX_BUILDER }}"
+ - "--cache-to={{ .Env.DOCKER_BUILDX_CACHE_TO }}"
+ - "--cache-from={{ .Env.DOCKER_BUILDX_CACHE_FROM }}"
+ - "--build-arg=ALPINE_IMAGE={{ .Env.ALPINE_IMAGE }}"
+
+ - dockerfile: ./build/testworkflow-init/Dockerfile
+ use: buildx
+ goos: linux
+ goarch: arm64
+ image_templates:
+ - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-init:{{ .Version }}-arm64v8{{ end }}"
+ build_flag_templates:
+ - "--platform=linux/arm64/v8"
+ - "--label=org.opencontainers.image.created={{ .Date }}"
+ - "--label=org.opencontainers.image.title={{ .ProjectName }}"
+ - "--label=org.opencontainers.image.revision={{ .FullCommit }}"
+ - "--label=org.opencontainers.image.version={{ .Version }}"
+ - "--builder={{ .Env.DOCKER_BUILDX_BUILDER }}"
+ - "--cache-to={{ .Env.DOCKER_BUILDX_CACHE_TO }}"
+ - "--cache-from={{ .Env.DOCKER_BUILDX_CACHE_FROM }}"
+ - "--build-arg=ALPINE_IMAGE={{ .Env.ALPINE_IMAGE }}"
+
+docker_manifests:
+ - name_template: "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-init:{{ .Version }}{{ end }}"
+ image_templates:
+ - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-init:{{ .Version }}-amd64{{ end }}"
+ - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-init:{{ .Version }}-arm64v8{{ end }}"
+ - name_template: "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-init:latest{{ end }}"
+ image_templates:
+ - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-init:{{ .Version }}-amd64{{ end }}"
+ - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-init:{{ .Version }}-arm64v8{{ end }}"
+
+
+release:
+ disable: true
+
+docker_signs:
+ - cmd: cosign
+ artifacts: all
+ output: true
+ args:
+ - "sign"
+ - "${artifact}"
+ - "--yes"
+
+snapshot:
+ name_template: "{{ .ShortCommit }}"
diff --git a/goreleaser_files/.goreleaser-docker-build-testworkflow-toolkit.yml b/goreleaser_files/.goreleaser-docker-build-testworkflow-toolkit.yml
new file mode 100644
index 00000000000..cec195644d7
--- /dev/null
+++ b/goreleaser_files/.goreleaser-docker-build-testworkflow-toolkit.yml
@@ -0,0 +1,93 @@
+project_name: testkube-tw-toolkit
+
+env:
+ # Goreleaser always uses the docker buildx builder with name "default"; see
+ # https://github.com/goreleaser/goreleaser/pull/3199
+ # To use a builder other than "default", set this variable.
+ # Necessary for, e.g., GitHub actions cache integration.
+ - DOCKER_REPO={{ if index .Env "DOCKER_REPO" }}{{ .Env.DOCKER_REPO }}{{ else }}kubeshop{{ end }}
+ - DOCKER_BUILDX_BUILDER={{ if index .Env "DOCKER_BUILDX_BUILDER" }}{{ .Env.DOCKER_BUILDX_BUILDER }}{{ else }}default{{ end }}
+ # Setup to enable Docker to use, e.g., the GitHub actions cache; see
+ # https://docs.docker.com/build/building/cache/backends/
+ # https://github.com/moby/buildkit#export-cache
+ - DOCKER_BUILDX_CACHE_FROM={{ if index .Env "DOCKER_BUILDX_CACHE_FROM" }}{{ .Env.DOCKER_BUILDX_CACHE_FROM }}{{ else }}type=registry{{ end }}
+ - DOCKER_BUILDX_CACHE_TO={{ if index .Env "DOCKER_BUILDX_CACHE_TO" }}{{ .Env.DOCKER_BUILDX_CACHE_TO }}{{ else }}type=inline{{ end }}
+ # Build image with commit sha tag
+ - IMAGE_TAG_SHA={{ if index .Env "IMAGE_TAG_SHA" }}{{ .Env.IMAGE_TAG_SHA }}{{ else }}{{ end }}
+builds:
+ - id: "linux"
+ main: ./cmd/tcl/testworkflow-toolkit
+ binary: testworkflow-toolkit
+ env:
+ - CGO_ENABLED=0
+ goos:
+ - linux
+ goarch:
+ - amd64
+ - arm64
+ mod_timestamp: "{{ .CommitTimestamp }}"
+ ldflags:
+ -X github.com/kubeshop/testkube/pkg/version.Version={{ .Version }}
+ -X github.com/kubeshop/testkube/pkg/version.Commit={{ .FullCommit }}
+ -s -w
+dockers:
+ - dockerfile: ./build/testworkflow-toolkit/Dockerfile
+ use: buildx
+ goos: linux
+ goarch: amd64
+ image_templates:
+ - "{{ if .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-toolkit:{{ .ShortCommit }}{{ end }}"
+ - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-toolkit:{{ .Version }}-amd64{{ end }}"
+ build_flag_templates:
+ - "--platform=linux/amd64"
+ - "--label=org.opencontainers.image.title={{ .ProjectName }}"
+ - "--label=org.opencontainers.image.created={{ .Date}}"
+ - "--label=org.opencontainers.image.revision={{ .FullCommit }}"
+ - "--label=org.opencontainers.image.version={{ .Version }}"
+ - "--builder={{ .Env.DOCKER_BUILDX_BUILDER }}"
+ - "--cache-to={{ .Env.DOCKER_BUILDX_CACHE_TO }}"
+ - "--cache-from={{ .Env.DOCKER_BUILDX_CACHE_FROM }}"
+ - "--build-arg=ALPINE_IMAGE={{ .Env.ALPINE_IMAGE }}"
+
+ - dockerfile: ./build/testworkflow-toolkit/Dockerfile
+ use: buildx
+ goos: linux
+ goarch: arm64
+ image_templates:
+ - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-toolkit:{{ .Version }}-arm64v8{{ end }}"
+ build_flag_templates:
+ - "--platform=linux/arm64/v8"
+ - "--label=org.opencontainers.image.created={{ .Date }}"
+ - "--label=org.opencontainers.image.title={{ .ProjectName }}"
+ - "--label=org.opencontainers.image.revision={{ .FullCommit }}"
+ - "--label=org.opencontainers.image.version={{ .Version }}"
+ - "--builder={{ .Env.DOCKER_BUILDX_BUILDER }}"
+ - "--cache-to={{ .Env.DOCKER_BUILDX_CACHE_TO }}"
+ - "--cache-from={{ .Env.DOCKER_BUILDX_CACHE_FROM }}"
+ - "--build-arg=ALPINE_IMAGE={{ .Env.ALPINE_IMAGE }}"
+
+docker_manifests:
+ - name_template: "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-toolkit:{{ .Version }}{{ end }}"
+ image_templates:
+ - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-toolkit:{{ .Version }}-amd64{{ end }}"
+ - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-toolkit:{{ .Version }}-arm64v8{{ end }}"
+ - name_template: "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-toolkit:latest{{ end }}"
+ image_templates:
+ - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-toolkit:{{ .Version }}-amd64{{ end }}"
+ - "{{ if not .Env.IMAGE_TAG_SHA }}{{ .Env.DOCKER_REPO }}/testkube-tw-toolkit:{{ .Version }}-arm64v8{{ end }}"
+
+
+release:
+ disable: true
+
+docker_signs:
+ - cmd: cosign
+ artifacts: all
+ output: true
+ args:
+ - "sign"
+ - "${artifact}"
+ - "--yes"
+
+snapshot:
+ name_template: "{{ .ShortCommit }}"
diff --git a/internal/app/api/metrics/metrics.go b/internal/app/api/metrics/metrics.go
index 28cf64c22a5..e29365a9a93 100644
--- a/internal/app/api/metrics/metrics.go
+++ b/internal/app/api/metrics/metrics.go
@@ -14,12 +14,12 @@ import (
var testExecutionCount = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "testkube_test_executions_count",
Help: "The total number of test executions",
-}, []string{"type", "name", "result", "labels", "test_uri", "execution_uri"})
+}, []string{"type", "name", "result", "labels", "test_uri"})
var testSuiteExecutionCount = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "testkube_testsuite_executions_count",
Help: "The total number of test suite executions",
-}, []string{"name", "result", "labels", "testsuite_uri", "execution_uri"})
+}, []string{"name", "result", "labels", "testsuite_uri"})
var testCreationCount = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "testkube_test_creations_count",
@@ -71,36 +71,78 @@ var testAbortCount = promauto.NewCounterVec(prometheus.CounterOpts{
Help: "The total number of tests aborted by type events",
}, []string{"type", "result"})
+var testWorkflowCreationCount = promauto.NewCounterVec(prometheus.CounterOpts{
+ Name: "testkube_testworkflow_creations_count",
+ Help: "The total number of test workflow created by type events",
+}, []string{"result"})
+
+var testWorkflowUpdatesCount = promauto.NewCounterVec(prometheus.CounterOpts{
+ Name: "testkube_testworkflow_updates_count",
+ Help: "The total number of test workflow updated by type events",
+}, []string{"result"})
+
+var testWorkflowDeletesCount = promauto.NewCounterVec(prometheus.CounterOpts{
+ Name: "testkube_testworkflow_deletes_count",
+ Help: "The total number of test workflow deleted events",
+}, []string{"result"})
+
+var testWorkflowTemplateCreationCount = promauto.NewCounterVec(prometheus.CounterOpts{
+ Name: "testkube_testworkflowtemplate_creations_count",
+ Help: "The total number of test workflow template created by type events",
+}, []string{"result"})
+
+var testWorkflowTemplateUpdatesCount = promauto.NewCounterVec(prometheus.CounterOpts{
+ Name: "testkube_testworkflowtemplate_updates_count",
+ Help: "The total number of test workflow template updated by type events",
+}, []string{"result"})
+
+var testWorkflowTemplateDeletesCount = promauto.NewCounterVec(prometheus.CounterOpts{
+ Name: "testkube_testworkflowtemplate_deletes_count",
+ Help: "The total number of test workflow template deleted events",
+}, []string{"result"})
+
func NewMetrics() Metrics {
return Metrics{
- TestExecutions: testExecutionCount,
- TestSuiteExecutions: testSuiteExecutionCount,
- TestCreations: testCreationCount,
- TestSuiteCreations: testSuiteCreationCount,
- TestUpdates: testUpdatesCount,
- TestSuiteUpdates: testSuiteUpdatesCount,
- TestTriggerCreations: testTriggerCreationCount,
- TestTriggerUpdates: testTriggerUpdatesCount,
- TestTriggerDeletes: testTriggerDeletesCount,
- TestTriggerBulkUpdates: testTriggerBulkUpdatesCount,
- TestTriggerBulkDeletes: testTriggerBulkDeletesCount,
- TestAbort: testAbortCount,
+ TestExecutions: testExecutionCount,
+ TestSuiteExecutions: testSuiteExecutionCount,
+ TestCreations: testCreationCount,
+ TestSuiteCreations: testSuiteCreationCount,
+ TestUpdates: testUpdatesCount,
+ TestSuiteUpdates: testSuiteUpdatesCount,
+ TestTriggerCreations: testTriggerCreationCount,
+ TestTriggerUpdates: testTriggerUpdatesCount,
+ TestTriggerDeletes: testTriggerDeletesCount,
+ TestTriggerBulkUpdates: testTriggerBulkUpdatesCount,
+ TestTriggerBulkDeletes: testTriggerBulkDeletesCount,
+ TestAbort: testAbortCount,
+ TestWorkflowCreations: testWorkflowCreationCount,
+ TestWorkflowUpdates: testWorkflowUpdatesCount,
+ TestWorkflowDeletes: testWorkflowDeletesCount,
+ TestWorkflowTemplateCreations: testWorkflowTemplateCreationCount,
+ TestWorkflowTemplateUpdates: testWorkflowTemplateUpdatesCount,
+ TestWorkflowTemplateDeletes: testWorkflowTemplateDeletesCount,
}
}
type Metrics struct {
- TestExecutions *prometheus.CounterVec
- TestSuiteExecutions *prometheus.CounterVec
- TestCreations *prometheus.CounterVec
- TestSuiteCreations *prometheus.CounterVec
- TestUpdates *prometheus.CounterVec
- TestSuiteUpdates *prometheus.CounterVec
- TestTriggerCreations *prometheus.CounterVec
- TestTriggerUpdates *prometheus.CounterVec
- TestTriggerDeletes *prometheus.CounterVec
- TestTriggerBulkUpdates *prometheus.CounterVec
- TestTriggerBulkDeletes *prometheus.CounterVec
- TestAbort *prometheus.CounterVec
+ TestExecutions *prometheus.CounterVec
+ TestSuiteExecutions *prometheus.CounterVec
+ TestCreations *prometheus.CounterVec
+ TestSuiteCreations *prometheus.CounterVec
+ TestUpdates *prometheus.CounterVec
+ TestSuiteUpdates *prometheus.CounterVec
+ TestTriggerCreations *prometheus.CounterVec
+ TestTriggerUpdates *prometheus.CounterVec
+ TestTriggerDeletes *prometheus.CounterVec
+ TestTriggerBulkUpdates *prometheus.CounterVec
+ TestTriggerBulkDeletes *prometheus.CounterVec
+ TestAbort *prometheus.CounterVec
+ TestWorkflowCreations *prometheus.CounterVec
+ TestWorkflowUpdates *prometheus.CounterVec
+ TestWorkflowDeletes *prometheus.CounterVec
+ TestWorkflowTemplateCreations *prometheus.CounterVec
+ TestWorkflowTemplateUpdates *prometheus.CounterVec
+ TestWorkflowTemplateDeletes *prometheus.CounterVec
}
func (m Metrics) IncExecuteTest(execution testkube.Execution, dashboardURI string) {
@@ -121,8 +163,6 @@ func (m Metrics) IncExecuteTest(execution testkube.Execution, dashboardURI strin
"result": status,
"labels": strings.Join(labels, ","),
"test_uri": fmt.Sprintf("%s/tests/%s", dashboardURI, execution.TestName),
- "execution_uri": fmt.Sprintf("%s/tests/%s/executions/%s", dashboardURI,
- execution.TestName, execution.Id),
}).Inc()
}
@@ -153,8 +193,6 @@ func (m Metrics) IncExecuteTestSuite(execution testkube.TestSuiteExecution, dash
"result": status,
"labels": strings.Join(labels, ","),
"testsuite_uri": fmt.Sprintf("%s/test-suites/%s", dashboardURI, testSuiteName),
- "execution_uri": fmt.Sprintf("%s/test-suites/%s/executions/%s", dashboardURI,
- testSuiteName, execution.Id),
}).Inc()
}
@@ -270,3 +308,69 @@ func (m Metrics) IncAbortTest(testType string, failed bool) {
"result": result,
}).Inc()
}
+
+func (m Metrics) IncCreateTestWorkflow(err error) {
+ result := "created"
+ if err != nil {
+ result = "error"
+ }
+
+ m.TestWorkflowCreations.With(map[string]string{
+ "result": result,
+ }).Inc()
+}
+
+func (m Metrics) IncUpdateTestWorkflow(err error) {
+ result := "updated"
+ if err != nil {
+ result = "error"
+ }
+
+ m.TestWorkflowUpdates.With(map[string]string{
+ "result": result,
+ }).Inc()
+}
+
+func (m Metrics) IncDeleteTestWorkflow(err error) {
+ result := "deleted"
+ if err != nil {
+ result = "error"
+ }
+
+ m.TestWorkflowDeletes.With(map[string]string{
+ "result": result,
+ }).Inc()
+}
+
+func (m Metrics) IncCreateTestWorkflowTemplate(err error) {
+ result := "created"
+ if err != nil {
+ result = "error"
+ }
+
+ m.TestWorkflowTemplateCreations.With(map[string]string{
+ "result": result,
+ }).Inc()
+}
+
+func (m Metrics) IncUpdateTestWorkflowTemplate(err error) {
+ result := "updated"
+ if err != nil {
+ result = "error"
+ }
+
+ m.TestWorkflowTemplateUpdates.With(map[string]string{
+ "result": result,
+ }).Inc()
+}
+
+func (m Metrics) IncDeleteTestWorkflowTemplate(err error) {
+ result := "deleted"
+ if err != nil {
+ result = "error"
+ }
+
+ m.TestWorkflowTemplateDeletes.With(map[string]string{
+ "result": result,
+ }).Inc()
+}
diff --git a/internal/app/api/v1/executions.go b/internal/app/api/v1/executions.go
index 16a029bee9c..0d01a3cb3d2 100644
--- a/internal/app/api/v1/executions.go
+++ b/internal/app/api/v1/executions.go
@@ -10,9 +10,11 @@ import (
"net/url"
"os"
"strconv"
+ "strings"
"k8s.io/apimachinery/pkg/api/errors"
+ "github.com/kubeshop/testkube/pkg/logs/events"
"github.com/kubeshop/testkube/pkg/repository/result"
"github.com/gofiber/fiber/v2"
@@ -86,7 +88,7 @@ func (s *TestkubeAPI) ExecuteTestsHandler() fiber.Handler {
}
var results []testkube.Execution
if len(tests) != 0 {
- request.TestExecutionName = c.Query("testExecutionName")
+ request.TestExecutionName = strings.Clone(c.Query("testExecutionName"))
concurrencyLevel, err := strconv.Atoi(c.Query("concurrency", strconv.Itoa(scheduler.DefaultConcurrencyLevel)))
if err != nil {
return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: can't detect concurrency level: %w", errPrefix, err))
@@ -170,7 +172,7 @@ func (s *TestkubeAPI) GetLogsStream(ctx context.Context, executionID string) (ch
return nil, fmt.Errorf("can't get executor for test type %s: %w", execution.TestType, err)
}
- logs, err := executor.Logs(ctx, executionID)
+ logs, err := executor.Logs(ctx, executionID, execution.TestNamespace)
if err != nil {
return nil, fmt.Errorf("can't get executor logs: %w", err)
}
@@ -180,6 +182,10 @@ func (s *TestkubeAPI) GetLogsStream(ctx context.Context, executionID string) (ch
func (s *TestkubeAPI) ExecutionLogsStreamHandler() fiber.Handler {
return websocket.New(func(c *websocket.Conn) {
+ if s.featureFlags.LogsV2 {
+ return
+ }
+
executionID := c.Params("executionID")
l := s.Log.With("executionID", executionID)
@@ -199,9 +205,47 @@ func (s *TestkubeAPI) ExecutionLogsStreamHandler() fiber.Handler {
})
}
+func (s *TestkubeAPI) ExecutionLogsStreamHandlerV2() fiber.Handler {
+ return websocket.New(func(c *websocket.Conn) {
+ if !s.featureFlags.LogsV2 {
+ return
+ }
+
+ executionID := c.Params("executionID")
+ l := s.Log.With("executionID", executionID)
+
+ l.Debugw("getting logs from grpc log server and passing to websocket",
+ "id", c.Params("id"), "locals", c.Locals, "remoteAddr", c.RemoteAddr(), "localAddr", c.LocalAddr())
+
+ defer c.Conn.Close()
+
+ logs, err := s.logGrpcClient.Get(context.Background(), executionID)
+ if err != nil {
+ l.Errorw("can't get logs fom grpc", "error", err)
+ return
+ }
+
+ for logLine := range logs {
+ if logLine.Error != nil {
+ l.Errorw("can't get log line", "error", logLine.Error)
+ continue
+ }
+
+ l.Debugw("sending log line to websocket", "line", logLine.Log)
+ _ = c.WriteJSON(logLine.Log)
+ }
+
+ l.Debug("stream stopped in v2 logs handler")
+ })
+}
+
// ExecutionLogsHandler streams the logs from a test execution
func (s *TestkubeAPI) ExecutionLogsHandler() fiber.Handler {
return func(c *fiber.Ctx) error {
+ if s.featureFlags.LogsV2 {
+ return nil
+ }
+
executionID := c.Params("executionID")
s.Log.Debug("getting logs", "executionID", executionID)
@@ -235,7 +279,43 @@ func (s *TestkubeAPI) ExecutionLogsHandler() fiber.Handler {
return
}
- s.streamLogsFromJob(ctx, executionID, execution.TestType, w)
+ s.streamLogsFromJob(ctx, executionID, execution.TestType, execution.TestNamespace, w)
+ })
+
+ return nil
+ }
+}
+
+// ExecutionLogsHandlerV2 streams the logs from a test execution version 2
+func (s *TestkubeAPI) ExecutionLogsHandlerV2() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ if !s.featureFlags.LogsV2 {
+ return nil
+ }
+
+ executionID := c.Params("executionID")
+
+ s.Log.Debugw("getting logs", "executionID", executionID)
+
+ ctx := c.Context()
+
+ ctx.SetContentType("text/event-stream")
+ ctx.Response.Header.Set("Cache-Control", "no-cache")
+ ctx.Response.Header.Set("Connection", "keep-alive")
+ ctx.Response.Header.Set("Transfer-Encoding", "chunked")
+
+ ctx.SetBodyStreamWriter(func(w *bufio.Writer) {
+ s.Log.Debug("start streaming logs")
+ _ = w.Flush()
+
+ s.Log.Infow("getting logs from grpc log server")
+ logs, err := s.logGrpcClient.Get(ctx, executionID)
+ if err != nil {
+ s.Log.Errorw("can't get logs from grpc", "error", err)
+ return
+ }
+
+ s.streamLogsFromLogServer(logs, w)
})
return nil
@@ -361,7 +441,7 @@ func (s *TestkubeAPI) GetArtifactHandler() fiber.Handler {
var file io.Reader
var bucket string
- artifactsStorage := s.artifactsStorage
+ artifactsStorage := s.ArtifactsStorage
folder := execution.Id
if execution.ArtifactRequest != nil {
bucket = execution.ArtifactRequest.StorageBucket
@@ -377,7 +457,7 @@ func (s *TestkubeAPI) GetArtifactHandler() fiber.Handler {
}
}
- file, err = artifactsStorage.DownloadFile(c.Context(), fileName, folder, execution.TestName, execution.TestSuiteName)
+ file, err = artifactsStorage.DownloadFile(c.Context(), fileName, folder, execution.TestName, execution.TestSuiteName, "")
if err != nil {
return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not download file: %w", errPrefix, err))
}
@@ -409,7 +489,7 @@ func (s *TestkubeAPI) GetArtifactArchiveHandler() fiber.Handler {
var archive io.Reader
var bucket string
- artifactsStorage := s.artifactsStorage
+ artifactsStorage := s.ArtifactsStorage
folder := execution.Id
if execution.ArtifactRequest != nil {
bucket = execution.ArtifactRequest.StorageBucket
@@ -452,7 +532,7 @@ func (s *TestkubeAPI) ListArtifactsHandler() fiber.Handler {
var files []testkube.Artifact
var bucket string
- artifactsStorage := s.artifactsStorage
+ artifactsStorage := s.ArtifactsStorage
folder := execution.Id
if execution.ArtifactRequest != nil {
bucket = execution.ArtifactRequest.StorageBucket
@@ -468,7 +548,7 @@ func (s *TestkubeAPI) ListArtifactsHandler() fiber.Handler {
}
}
- files, err = artifactsStorage.ListFiles(c.Context(), folder, execution.TestName, execution.TestSuiteName)
+ files, err = artifactsStorage.ListFiles(c.Context(), folder, execution.TestName, execution.TestSuiteName, "")
if err != nil {
return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: storage client could not list files %w", errPrefix, err))
}
@@ -503,7 +583,7 @@ func (s *TestkubeAPI) streamLogsFromResult(executionResult *testkube.ExecutionRe
}
// streamLogsFromJob streams logs in chunks to writer from the running execution
-func (s *TestkubeAPI) streamLogsFromJob(ctx context.Context, executionID, testType string, w *bufio.Writer) {
+func (s *TestkubeAPI) streamLogsFromJob(ctx context.Context, executionID, testType, namespace string, w *bufio.Writer) {
enc := json.NewEncoder(w)
s.Log.Infow("getting logs from Kubernetes job")
@@ -515,7 +595,7 @@ func (s *TestkubeAPI) streamLogsFromJob(ctx context.Context, executionID, testTy
return
}
- logs, err := executor.Logs(ctx, executionID)
+ logs, err := executor.Logs(ctx, executionID, namespace)
s.Log.Debugw("waiting for jobs channel", "channelSize", len(logs))
if err != nil {
output.PrintError(os.Stdout, err)
@@ -594,11 +674,30 @@ func (s *TestkubeAPI) getNewestExecutions(ctx context.Context) ([]testkube.Execu
// getExecutionLogs returns logs from an execution
func (s *TestkubeAPI) getExecutionLogs(ctx context.Context, execution testkube.Execution) ([]string, error) {
var res []string
+
+ if s.featureFlags.LogsV2 {
+ logs, err := s.logGrpcClient.Get(ctx, execution.Id)
+ if err != nil {
+ return []string{}, fmt.Errorf("could not get logs for grpc %s: %w", execution.Id, err)
+ }
+
+ for out := range logs {
+ if out.Error != nil {
+ s.Log.Errorw("can't get log line", "error", out.Error)
+ continue
+ }
+
+ res = append(res, out.Log.Content)
+ }
+
+ return res, nil
+ }
+
if execution.ExecutionResult.IsCompleted() {
return append(res, execution.ExecutionResult.Output), nil
}
- logs, err := s.Executor.Logs(ctx, execution.Id)
+ logs, err := s.Executor.Logs(ctx, execution.Id, execution.TestNamespace)
if err != nil {
return []string{}, fmt.Errorf("could not get logs for execution %s: %w", execution.Id, err)
}
@@ -625,7 +724,7 @@ func (s *TestkubeAPI) getExecutorByTestType(testType string) (client.Executor, e
func (s *TestkubeAPI) getArtifactStorage(bucket string) (storage.ArtifactsStorage, error) {
if s.mode == common.ModeAgent {
- return s.artifactsStorage, nil
+ return s.ArtifactsStorage, nil
}
opts := minio.GetTLSOptions(s.storageParams.SSL, s.storageParams.SkipVerify, s.storageParams.CertFile, s.storageParams.KeyFile, s.storageParams.CAFile)
@@ -644,3 +743,29 @@ func (s *TestkubeAPI) getArtifactStorage(bucket string) (storage.ArtifactsStorag
return minio.NewMinIOArtifactClient(minioClient), nil
}
+
+// streamLogsFromLogServer writes logs from the output of log server to the writer
+func (s *TestkubeAPI) streamLogsFromLogServer(logs chan events.LogResponse, w *bufio.Writer) {
+ enc := json.NewEncoder(w)
+ s.Log.Infow("looping through logs channel")
+ // loop through grpc server log lines - it's blocking channel
+ // and pass single log output as sse data chunk
+ for out := range logs {
+ if out.Error != nil {
+ s.Log.Errorw("can't get log line", "error", out.Error)
+ continue
+ }
+
+ s.Log.Debugw("got log line from grpc log server", "out", out.Log)
+ _, _ = fmt.Fprintf(w, "data: ")
+ err := enc.Encode(out.Log)
+ if err != nil {
+ s.Log.Infow("Encode", "error", err)
+ }
+ // enc.Encode adds \n and we need \n\n after `data: {}` chunk
+ _, _ = fmt.Fprintf(w, "\n")
+ _ = w.Flush()
+ }
+
+ s.Log.Debugw("logs streaming stopped")
+}
diff --git a/internal/app/api/v1/executions_test.go b/internal/app/api/v1/executions_test.go
index 16b5afc725f..0d8be318ee7 100644
--- a/internal/app/api/v1/executions_test.go
+++ b/internal/app/api/v1/executions_test.go
@@ -7,9 +7,11 @@ import (
"testing"
"time"
+ "github.com/kubeshop/testkube/pkg/featureflags"
"github.com/kubeshop/testkube/pkg/repository/result"
"github.com/gofiber/fiber/v2"
+ "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@@ -19,9 +21,11 @@ import (
executorv1 "github.com/kubeshop/testkube-operator/api/executor/v1"
executorsclientv1 "github.com/kubeshop/testkube-operator/pkg/client/executors/v1"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
- "github.com/kubeshop/testkube/pkg/executor/client"
+ executorclient "github.com/kubeshop/testkube/pkg/executor/client"
"github.com/kubeshop/testkube/pkg/executor/output"
"github.com/kubeshop/testkube/pkg/log"
+ logclient "github.com/kubeshop/testkube/pkg/logs/client"
+ "github.com/kubeshop/testkube/pkg/logs/events"
"github.com/kubeshop/testkube/pkg/server"
)
@@ -120,6 +124,76 @@ func TestTestkubeAPI_ExecutionLogsHandler(t *testing.T) {
}
}
+func TestTestkubeAPI_ExecutionLogsHandlerV2(t *testing.T) {
+ app := fiber.New()
+
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ grpcClient := logclient.NewMockStreamGetter(mockCtrl)
+
+ eventLog := events.Log{
+ Content: "storage logs",
+ Source: events.SourceJobPod,
+ Version: string(events.LogVersionV2),
+ }
+
+ out := make(chan events.LogResponse)
+ go func() {
+ defer func() {
+ close(out)
+ }()
+
+ out <- events.LogResponse{Log: eventLog}
+ }()
+
+ grpcClient.EXPECT().Get(gomock.Any(), "test-execution-1").Return(out, nil)
+ s := &TestkubeAPI{
+ HTTPServer: server.HTTPServer{
+ Mux: app,
+ Log: log.DefaultLogger,
+ },
+ featureFlags: featureflags.FeatureFlags{LogsV2: true},
+ logGrpcClient: grpcClient,
+ }
+ app.Get("/executions/:executionID/logs/v2", s.ExecutionLogsHandlerV2())
+
+ tests := []struct {
+ name string
+ route string
+ expectedCode int
+ eventLog events.Log
+ }{
+ {
+ name: "Test getting logs from grpc client",
+ route: "/executions/test-execution-1/logs/v2",
+ expectedCode: 200,
+ eventLog: eventLog,
+ },
+ }
+
+ responsePrefix := "data: "
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ req := httptest.NewRequest("GET", tt.route, nil)
+ resp, err := app.Test(req, -1)
+ assert.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, tt.expectedCode, resp.StatusCode, tt.name)
+
+ b := make([]byte, len(responsePrefix))
+ resp.Body.Read(b)
+ assert.Equal(t, responsePrefix, string(b))
+
+ var res events.Log
+ err = json.NewDecoder(resp.Body).Decode(&res)
+ assert.NoError(t, err)
+ assert.Equal(t, tt.eventLog, res)
+ })
+ }
+}
+
type MockExecutionResultsRepository struct {
GetFn func(ctx context.Context, id string) (testkube.Execution, error)
}
@@ -131,6 +205,13 @@ func (r MockExecutionResultsRepository) Get(ctx context.Context, id string) (tes
return r.GetFn(ctx, id)
}
+func (r MockExecutionResultsRepository) GetExecution(ctx context.Context, id string) (testkube.Execution, error) {
+ if r.GetFn == nil {
+ panic("not implemented")
+ }
+ return r.GetFn(ctx, id)
+}
+
func (r MockExecutionResultsRepository) GetByNameAndTest(ctx context.Context, name, testName string) (testkube.Execution, error) {
panic("not implemented")
}
@@ -207,11 +288,15 @@ func (r MockExecutionResultsRepository) GetTestMetrics(ctx context.Context, name
panic("not implemented")
}
+func (r MockExecutionResultsRepository) Count(ctx context.Context, filter result.Filter) (int64, error) {
+ panic("not implemented")
+}
+
type MockExecutor struct {
LogsFn func(id string) (chan output.Output, error)
}
-func (e MockExecutor) Execute(ctx context.Context, execution *testkube.Execution, options client.ExecuteOptions) (*testkube.ExecutionResult, error) {
+func (e MockExecutor) Execute(ctx context.Context, execution *testkube.Execution, options executorclient.ExecuteOptions) (*testkube.ExecutionResult, error) {
panic("not implemented")
}
@@ -219,7 +304,7 @@ func (e MockExecutor) Abort(ctx context.Context, execution *testkube.Execution)
panic("not implemented")
}
-func (e MockExecutor) Logs(ctx context.Context, id string) (chan output.Output, error) {
+func (e MockExecutor) Logs(ctx context.Context, id, namespace string) (chan output.Output, error) {
if e.LogsFn == nil {
panic("not implemented")
}
diff --git a/internal/app/api/v1/executor.go b/internal/app/api/v1/executor.go
index 797ce5fdcf9..a5d601ccf83 100644
--- a/internal/app/api/v1/executor.go
+++ b/internal/app/api/v1/executor.go
@@ -196,3 +196,29 @@ func (s TestkubeAPI) DeleteExecutorsHandler() fiber.Handler {
return nil
}
}
+
+func (s TestkubeAPI) GetExecutorByTestTypeHandler() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ errPrefix := "failed to get executor by test type"
+
+ testType := c.Query("testType", "")
+ if testType == "" {
+ return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not fine test type", errPrefix))
+ }
+
+ item, err := s.ExecutorsClient.GetByType(testType)
+ if err != nil {
+ return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not get executor: %w", errPrefix, err))
+ }
+
+ if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML {
+ result := executorsmapper.MapCRDToAPI(*item)
+ result.QuoteExecutorTextFields()
+ data, err := crd.GenerateYAML(crd.TemplateExecutor, []testkube.ExecutorUpsertRequest{result})
+ return s.getCRDs(c, data, err)
+ }
+
+ result := executorsmapper.MapExecutorCRDToExecutorDetails(*item)
+ return c.JSON(result)
+ }
+}
diff --git a/internal/app/api/v1/handlers.go b/internal/app/api/v1/handlers.go
index 3db757e5d1c..a4a700f17fa 100644
--- a/internal/app/api/v1/handlers.go
+++ b/internal/app/api/v1/handlers.go
@@ -3,16 +3,14 @@ package v1
import (
"fmt"
"net/http"
- "os"
"strings"
- "github.com/kubeshop/testkube/pkg/version"
-
"github.com/gofiber/fiber/v2"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/k8sclient"
"github.com/kubeshop/testkube/pkg/oauth"
+ "github.com/kubeshop/testkube/pkg/version"
)
const (
@@ -23,11 +21,6 @@ const (
// mediaTypeYAML is yaml media type
mediaTypeYAML = "text/yaml"
- // env names for cloud context
- cloudApiKeyEnvName = "TESTKUBE_CLOUD_API_KEY"
- cloudEnvIdEnvName = "TESTKUBE_CLOUD_ENV_ID"
- cloudOrgIdEnvName = "TESTKUBE_CLOUD_ORG_ID"
-
// contextCloud is cloud context
contextCloud = "cloud"
// contextOSS is oss context
@@ -58,19 +51,28 @@ func (s *TestkubeAPI) AuthHandler() fiber.Handler {
// InfoHandler is a handler to get info
func (s *TestkubeAPI) InfoHandler() fiber.Handler {
apiContext := contextOSS
- if os.Getenv(cloudApiKeyEnvName) != "" {
+ if s.proContext != nil && s.proContext.APIKey != "" {
apiContext = contextCloud
}
+ var envID, orgID string
+ if s.proContext != nil {
+ envID = s.proContext.EnvID
+ orgID = s.proContext.OrgID
+ }
return func(c *fiber.Ctx) error {
return c.JSON(testkube.ServerInfo{
- Commit: version.Commit,
- Version: version.Version,
- Namespace: s.Namespace,
- Context: apiContext,
- EnvId: os.Getenv(cloudEnvIdEnvName),
- OrgId: os.Getenv(cloudOrgIdEnvName),
- HelmchartVersion: s.helmchartVersion,
- DashboardUri: s.dashboardURI,
+ Commit: version.Commit,
+ Version: version.Version,
+ Namespace: s.Namespace,
+ Context: apiContext,
+ EnvId: envID,
+ OrgId: orgID,
+ HelmchartVersion: s.helmchartVersion,
+ DashboardUri: s.dashboardURI,
+ DisableSecretCreation: s.disableSecretCreation,
+ Features: &testkube.Features{
+ LogsV2: s.featureFlags.LogsV2,
+ },
})
}
}
diff --git a/internal/app/api/v1/labels.go b/internal/app/api/v1/labels.go
index 10b16f21a9a..6e1d84d3269 100644
--- a/internal/app/api/v1/labels.go
+++ b/internal/app/api/v1/labels.go
@@ -5,50 +5,38 @@ import (
"net/http"
"github.com/gofiber/fiber/v2"
-
- "github.com/kubeshop/testkube/pkg/data/set"
)
func (s TestkubeAPI) ListLabelsHandler() fiber.Handler {
return func(c *fiber.Ctx) error {
- errPrefix := "failed to list labels"
- testSuitesLabels, err := s.TestsSuitesClient.ListLabels()
- if err != nil {
- return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client failed to list labels for test suites: %w", errPrefix, err))
- }
+ labels := make(map[string][]string)
+ sources := append(*s.LabelSources, s.TestsClient, s.TestsSuitesClient)
- labels, err := s.TestsClient.ListLabels()
- if err != nil {
- return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client failed to list labels for tests: %w", errPrefix, err))
- }
-
- for key, testValues := range testSuitesLabels {
- if values, ok := labels[key]; !ok {
- labels[key] = testValues
- } else {
- valuesMap := map[string]struct{}{}
- for _, v := range values {
- valuesMap[v] = struct{}{}
- }
+ for _, source := range sources {
+ nextLabels, err := source.ListLabels()
+ if err != nil {
+ return s.Error(c, http.StatusBadGateway, fmt.Errorf("failed to list labels: %w", err))
+ }
- testValuesMap := map[string]struct{}{}
- for _, v := range testValues {
- testValuesMap[v] = struct{}{}
- }
+ for key, testValues := range nextLabels {
+ if values, ok := labels[key]; !ok {
+ labels[key] = testValues
+ } else {
+ valuesMap := map[string]struct{}{}
+ for _, v := range values {
+ valuesMap[v] = struct{}{}
+ }
- for k := range testValuesMap {
- if _, ok := valuesMap[k]; !ok {
- labels[key] = append(labels[key], k)
+ for _, label := range testValues {
+ if _, ok := valuesMap[label]; !ok {
+ labels[key] = append(labels[key], label)
+ valuesMap[label] = struct{}{}
+ }
}
}
}
}
- // make labels unique
- for key, list := range labels {
- labels[key] = set.Of(list...).ToArray()
- }
-
return c.JSON(labels)
}
}
diff --git a/internal/app/api/v1/server.go b/internal/app/api/v1/server.go
index 6204fbdbb19..a8e941b35dd 100644
--- a/internal/app/api/v1/server.go
+++ b/internal/app/api/v1/server.go
@@ -13,8 +13,11 @@ import (
"github.com/pkg/errors"
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/internal/config"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
- "github.com/kubeshop/testkube/pkg/repository/config"
+ repoConfig "github.com/kubeshop/testkube/pkg/repository/config"
+ "github.com/kubeshop/testkube/pkg/tcl/checktcl"
"github.com/kubeshop/testkube/pkg/version"
@@ -36,7 +39,6 @@ import (
testsuitesclientv3 "github.com/kubeshop/testkube-operator/pkg/client/testsuites/v3"
testkubeclientset "github.com/kubeshop/testkube-operator/pkg/clientset/versioned"
"github.com/kubeshop/testkube/internal/app/api/metrics"
- "github.com/kubeshop/testkube/internal/featureflags"
"github.com/kubeshop/testkube/pkg/event"
"github.com/kubeshop/testkube/pkg/event/bus"
"github.com/kubeshop/testkube/pkg/event/kind/cdevent"
@@ -44,6 +46,8 @@ import (
"github.com/kubeshop/testkube/pkg/event/kind/webhook"
ws "github.com/kubeshop/testkube/pkg/event/kind/websocket"
"github.com/kubeshop/testkube/pkg/executor/client"
+ "github.com/kubeshop/testkube/pkg/featureflags"
+ logsclient "github.com/kubeshop/testkube/pkg/logs/client"
"github.com/kubeshop/testkube/pkg/oauth"
"github.com/kubeshop/testkube/pkg/scheduler"
"github.com/kubeshop/testkube/pkg/secret"
@@ -70,7 +74,7 @@ func NewTestkubeAPI(
clientset kubernetes.Interface,
testkubeClientset testkubeclientset.Interface,
testsourcesClient *testsourcesclientv1.TestSourcesClient,
- configMap config.Repository,
+ configMap repoConfig.Repository,
clusterId string,
eventsEmitter *event.Emitter,
executor client.Executor,
@@ -89,6 +93,10 @@ func NewTestkubeAPI(
eventsBus bus.Bus,
enableSecretsEndpoint bool,
ff featureflags.FeatureFlags,
+ logsStream logsclient.Stream,
+ logGrpcClient logsclient.StreamGetter,
+ disableSecretCreation bool,
+ subscriptionChecker checktcl.SubscriptionChecker,
) TestkubeAPI {
var httpConfig server.Config
@@ -126,7 +134,7 @@ func NewTestkubeAPI(
slackLoader: slackLoader,
Storage: storage,
graphqlPort: graphqlPort,
- artifactsStorage: artifactsStorage,
+ ArtifactsStorage: artifactsStorage,
TemplatesClient: templatesClient,
dashboardURI: dashboardURI,
helmchartVersion: helmchartVersion,
@@ -134,6 +142,11 @@ func NewTestkubeAPI(
eventsBus: eventsBus,
enableSecretsEndpoint: enableSecretsEndpoint,
featureFlags: ff,
+ logsStream: logsStream,
+ logGrpcClient: logGrpcClient,
+ disableSecretCreation: disableSecretCreation,
+ SubscriptionChecker: subscriptionChecker,
+ LabelSources: common.Ptr(make([]LabelSource, 0)),
}
// will be reused in websockets handler
@@ -178,12 +191,12 @@ type TestkubeAPI struct {
oauthParams oauthParams
WebsocketLoader *ws.WebsocketLoader
Events *event.Emitter
- ConfigMap config.Repository
+ ConfigMap repoConfig.Repository
scheduler *scheduler.Scheduler
Clientset kubernetes.Interface
slackLoader *slack.SlackLoader
graphqlPort string
- artifactsStorage storage.ArtifactsStorage
+ ArtifactsStorage storage.ArtifactsStorage
TemplatesClient *templatesclientv1.TemplatesClient
dashboardURI string
helmchartVersion string
@@ -191,6 +204,12 @@ type TestkubeAPI struct {
eventsBus bus.Bus
enableSecretsEndpoint bool
featureFlags featureflags.FeatureFlags
+ logsStream logsclient.Stream
+ logGrpcClient logsclient.StreamGetter
+ proContext *config.ProContext
+ disableSecretCreation bool
+ SubscriptionChecker checktcl.SubscriptionChecker
+ LabelSources *[]LabelSource
}
type storageParams struct {
@@ -219,6 +238,14 @@ func (s *TestkubeAPI) WithFeatureFlags(ff featureflags.FeatureFlags) *TestkubeAP
return s
}
+type LabelSource interface {
+ ListLabels() (map[string][]string, error)
+}
+
+func (s *TestkubeAPI) WithLabelSources(l ...LabelSource) {
+ *s.LabelSources = append(*s.LabelSources, l...)
+}
+
// SendTelemetryStartEvent sends anonymous start event to telemetry trackers
func (s TestkubeAPI) SendTelemetryStartEvent(ctx context.Context, ch chan struct{}) {
go func() {
@@ -275,6 +302,9 @@ func (s *TestkubeAPI) InitRoutes() {
executors.Delete("/:name", s.DeleteExecutorHandler())
executors.Delete("/", s.DeleteExecutorsHandler())
+ executorByTypes := root.Group("/executor-by-types")
+ executorByTypes.Get("/", s.GetExecutorByTestTypeHandler())
+
webhooks := root.Group("/webhooks")
webhooks.Post("/", s.CreateWebhookHandler())
@@ -292,6 +322,8 @@ func (s *TestkubeAPI) InitRoutes() {
executions.Get("/:executionID/artifacts", s.ListArtifactsHandler())
executions.Get("/:executionID/logs", s.ExecutionLogsHandler())
executions.Get("/:executionID/logs/stream", s.ExecutionLogsStreamHandler())
+ executions.Get("/:executionID/logs/v2", s.ExecutionLogsHandlerV2())
+ executions.Get("/:executionID/logs/stream/v2", s.ExecutionLogsStreamHandlerV2())
executions.Get("/:executionID/artifacts/:filename", s.GetArtifactHandler())
executions.Get("/:executionID/artifact-archive", s.GetArtifactArchiveHandler())
@@ -576,3 +608,16 @@ func getFilterFromRequest(c *fiber.Ctx) result.Filter {
return filter
}
+
+// WithProContext sets pro context for the API
+func (s *TestkubeAPI) WithProContext(proContext *config.ProContext) *TestkubeAPI {
+ s.proContext = proContext
+ return s
+}
+
+// WithSubscriptionChecker sets subscription checker for the API
+// This is used to check if Pro/Enterprise subscription is valid
+func (s *TestkubeAPI) WithSubscriptionChecker(subscriptionChecker checktcl.SubscriptionChecker) *TestkubeAPI {
+ s.SubscriptionChecker = subscriptionChecker
+ return s
+}
diff --git a/internal/app/api/v1/tests.go b/internal/app/api/v1/tests.go
index aa7df7d6beb..2316e076cb4 100644
--- a/internal/app/api/v1/tests.go
+++ b/internal/app/api/v1/tests.go
@@ -363,7 +363,7 @@ func (s TestkubeAPI) CreateTestHandler() fiber.Handler {
test = testsmapper.MapUpsertToSpec(request)
test.Namespace = s.Namespace
- if request.Content != nil && request.Content.Repository != nil {
+ if request.Content != nil && request.Content.Repository != nil && !s.disableSecretCreation {
secrets = createTestSecretsData(request.Content.Repository.Username, request.Content.Repository.Token)
}
}
@@ -439,7 +439,7 @@ func (s TestkubeAPI) UpdateTestHandler() fiber.Handler {
if request.Content != nil && (*request.Content) != nil && (*request.Content).Repository != nil && *(*request.Content).Repository != nil {
username := (*(*request.Content).Repository).Username
token := (*(*request.Content).Repository).Token
- if username != nil || token != nil {
+ if (username != nil || token != nil) && !s.disableSecretCreation {
data, err := s.SecretClient.Get(secret.GetMetadataName(name, client.SecretTest))
if err != nil && !errors.IsNotFound(err) {
return s.Error(c, http.StatusBadGateway, err)
@@ -449,7 +449,7 @@ func (s TestkubeAPI) UpdateTestHandler() fiber.Handler {
}
}
- if testSpec.Spec.Content != nil && testSpec.Spec.Content.Repository != nil && testSpec.Spec.Content.Repository.Uri == "" {
+ if isRepositoryEmpty(testSpec.Spec) {
testSpec.Spec.Content.Repository = nil
}
@@ -470,6 +470,17 @@ func (s TestkubeAPI) UpdateTestHandler() fiber.Handler {
}
}
+func isRepositoryEmpty(s testsv3.TestSpec) bool {
+ return s.Content != nil &&
+ s.Content.Repository != nil &&
+ s.Content.Repository.Type_ == "" &&
+ s.Content.Repository.Uri == "" &&
+ s.Content.Repository.Branch == "" &&
+ s.Content.Repository.Path == "" &&
+ s.Content.Repository.Commit == "" &&
+ s.Content.Repository.WorkingDir == ""
+}
+
// DeleteTestHandler is a method for deleting a test with id
func (s TestkubeAPI) DeleteTestHandler() fiber.Handler {
return func(c *fiber.Ctx) error {
diff --git a/internal/app/api/v1/testsource.go b/internal/app/api/v1/testsource.go
index d75cf1bf9cf..e5e71624e9a 100644
--- a/internal/app/api/v1/testsource.go
+++ b/internal/app/api/v1/testsource.go
@@ -47,7 +47,7 @@ func (s TestkubeAPI) CreateTestSourceHandler() fiber.Handler {
testSource = testsourcesmapper.MapAPIToCRD(request)
testSource.Namespace = s.Namespace
- if request.Repository != nil {
+ if request.Repository != nil && !s.disableSecretCreation {
secrets = createTestSecretsData(request.Repository.Username, request.Repository.Token)
}
}
@@ -104,7 +104,7 @@ func (s TestkubeAPI) UpdateTestSourceHandler() fiber.Handler {
if request.Repository != nil && (*request.Repository) != nil {
username := (*request.Repository).Username
token := (*request.Repository).Token
- if username != nil || token != nil {
+ if (username != nil || token != nil) && !s.disableSecretCreation {
data, err := s.SecretClient.Get(secret.GetMetadataName(name, client.SecretSource))
if err != nil && !errors.IsNotFound(err) {
return s.Error(c, http.StatusBadGateway, err)
@@ -246,7 +246,7 @@ func (s TestkubeAPI) ProcessTestSourceBatchHandler() fiber.Handler {
for name, item := range testSourceBatch {
testSource := testsourcesmapper.MapAPIToCRD(item)
var username, token string
- if item.Repository != nil {
+ if item.Repository != nil && !s.disableSecretCreation {
username = item.Repository.Username
token = item.Repository.Token
}
diff --git a/internal/app/api/v1/testsuites.go b/internal/app/api/v1/testsuites.go
index 566d7f2cdea..8e64af8cd03 100644
--- a/internal/app/api/v1/testsuites.go
+++ b/internal/app/api/v1/testsuites.go
@@ -43,7 +43,6 @@ func (s TestkubeAPI) CreateTestSuiteHandler() fiber.Handler {
if err := decoder.Decode(&testSuite); err != nil {
return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse yaml request: %w", errPrefix, err))
}
-
errPrefix = errPrefix + " " + testSuite.Name
} else {
var request testkube.TestSuiteUpsertRequest
@@ -116,7 +115,6 @@ func (s TestkubeAPI) UpdateTestSuiteHandler() fiber.Handler {
if err := decoder.Decode(&testSuite); err != nil {
return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse yaml request: %w", errPrefix, err))
}
-
request = testsuitesmapper.MapTestSuiteTestCRDToUpdateRequest(&testSuite)
} else {
data := c.Body()
@@ -564,7 +562,6 @@ func (s TestkubeAPI) ExecuteTestSuitesHandler() fiber.Handler {
return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could get test suite: %w", errPrefix, err))
}
-
testSuites = append(testSuites, *testSuite)
} else {
testSuiteList, err := s.TestsSuitesClient.List(selector)
@@ -577,7 +574,7 @@ func (s TestkubeAPI) ExecuteTestSuitesHandler() fiber.Handler {
var results []testkube.TestSuiteExecution
if len(testSuites) != 0 {
- request.TestSuiteExecutionName = c.Query("testSuiteExecutionName")
+ request.TestSuiteExecutionName = strings.Clone(c.Query("testSuiteExecutionName"))
concurrencyLevel, err := strconv.Atoi(c.Query("concurrency", strconv.Itoa(scheduler.DefaultConcurrencyLevel)))
if err != nil {
return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: can't detect concurrency level: %w", errPrefix, err))
@@ -695,38 +692,26 @@ func (s TestkubeAPI) ListTestSuiteArtifactsHandler() fiber.Handler {
var artifacts []testkube.Artifact
for _, stepResult := range execution.StepResults {
- if stepResult.Execution.Id == "" {
+ if stepResult.Execution == nil || stepResult.Execution.Id == "" {
continue
}
- var stepArtifacts []testkube.Artifact
- var bucket string
- artifactsStorage := s.artifactsStorage
- folder := stepResult.Execution.Id
- if stepResult.Execution.ArtifactRequest != nil {
- bucket = stepResult.Execution.ArtifactRequest.StorageBucket
- if stepResult.Execution.ArtifactRequest.OmitFolderPerExecution {
- folder = ""
- }
+ artifacts, err = s.getExecutionArtfacts(c.Context(), stepResult.Execution, artifacts)
+ if err != nil {
+ continue
}
+ }
- if bucket != "" {
- artifactsStorage, err = s.getArtifactStorage(bucket)
- if err != nil {
- s.Log.Warnw("can't get artifact storage", "executionID", stepResult.Execution.Id, "error", err)
+ for _, stepResults := range execution.ExecuteStepResults {
+ for _, stepResult := range stepResults.Execute {
+ if stepResult.Execution == nil || stepResult.Execution.Id == "" {
continue
}
- }
- stepArtifacts, err = artifactsStorage.ListFiles(c.Context(), folder, stepResult.Execution.TestName, stepResult.Execution.TestSuiteName)
- if err != nil {
- s.Log.Warnw("can't list artifacts", "executionID", stepResult.Execution.Id, "error", err)
- continue
- }
- s.Log.Debugw("listing artifacts for step", "executionID", stepResult.Execution.Id, "artifacts", stepArtifacts)
- for i := range stepArtifacts {
- stepArtifacts[i].ExecutionName = stepResult.Execution.Name
- artifacts = append(artifacts, stepArtifacts[i])
+ artifacts, err = s.getExecutionArtfacts(c.Context(), stepResult.Execution, artifacts)
+ if err != nil {
+ continue
+ }
}
}
@@ -738,6 +723,44 @@ func (s TestkubeAPI) ListTestSuiteArtifactsHandler() fiber.Handler {
}
}
+func (s TestkubeAPI) getExecutionArtfacts(ctx context.Context, execution *testkube.Execution,
+ artifacts []testkube.Artifact) ([]testkube.Artifact, error) {
+ var stepArtifacts []testkube.Artifact
+ var bucket string
+
+ artifactsStorage := s.ArtifactsStorage
+ folder := execution.Id
+ if execution.ArtifactRequest != nil {
+ bucket = execution.ArtifactRequest.StorageBucket
+ if execution.ArtifactRequest.OmitFolderPerExecution {
+ folder = ""
+ }
+ }
+
+ var err error
+ if bucket != "" {
+ artifactsStorage, err = s.getArtifactStorage(bucket)
+ if err != nil {
+ s.Log.Warnw("can't get artifact storage", "executionID", execution.Id, "error", err)
+ return artifacts, err
+ }
+ }
+
+ stepArtifacts, err = artifactsStorage.ListFiles(ctx, folder, execution.TestName, execution.TestSuiteName, "")
+ if err != nil {
+ s.Log.Warnw("can't list artifacts", "executionID", execution.Id, "error", err)
+ return artifacts, err
+ }
+
+ s.Log.Debugw("listing artifacts for step", "executionID", execution.Id, "artifacts", stepArtifacts)
+ for i := range stepArtifacts {
+ stepArtifacts[i].ExecutionName = execution.Name
+ artifacts = append(artifacts, stepArtifacts[i])
+ }
+
+ return artifacts, nil
+}
+
// AbortTestSuiteHandler for aborting a TestSuite with id
func (s TestkubeAPI) AbortTestSuiteHandler() fiber.Handler {
return func(c *fiber.Ctx) error {
diff --git a/internal/app/api/v1/uploads.go b/internal/app/api/v1/uploads.go
index d0226c85460..6508fbca088 100644
--- a/internal/app/api/v1/uploads.go
+++ b/internal/app/api/v1/uploads.go
@@ -24,7 +24,7 @@ func (s TestkubeAPI) UploadFiles() fiber.Handler {
return s.Error(c, fiber.StatusBadRequest, fmt.Errorf("%s: wrong input: filePath cannot be empty", errPrefix))
}
- bucketName := s.artifactsStorage.GetValidBucketName(parentType, parentName)
+ bucketName := s.ArtifactsStorage.GetValidBucketName(parentType, parentName)
file, err := c.FormFile("attachment")
if err != nil {
return s.Error(c, fiber.StatusBadRequest, fmt.Errorf("%s: unable to upload file: %w", errPrefix, err))
@@ -35,7 +35,7 @@ func (s TestkubeAPI) UploadFiles() fiber.Handler {
}
defer f.Close()
- err = s.artifactsStorage.UploadFile(c.Context(), bucketName, filePath, f, file.Size)
+ err = s.ArtifactsStorage.UploadFile(c.Context(), bucketName, filePath, f, file.Size)
if err != nil {
return s.Error(c, fiber.StatusInternalServerError, fmt.Errorf("%s: could not save uploaded file: %w", errPrefix, err))
}
diff --git a/internal/app/api/v1/uploads_test.go b/internal/app/api/v1/uploads_test.go
index aa93947c4b0..6fad514363a 100644
--- a/internal/app/api/v1/uploads_test.go
+++ b/internal/app/api/v1/uploads_test.go
@@ -34,7 +34,7 @@ func TestTestkubeAPI_UploadCopyFiles(t *testing.T) {
Mux: app,
Log: log.DefaultLogger,
},
- artifactsStorage: mockArtifactsStorage,
+ ArtifactsStorage: mockArtifactsStorage,
}
route := "/uploads"
diff --git a/internal/common/common.go b/internal/common/common.go
index d584554e008..a27568f5bea 100644
--- a/internal/common/common.go
+++ b/internal/common/common.go
@@ -10,3 +10,97 @@ func MergeMaps(ms ...map[string]string) map[string]string {
}
return res
}
+
+func Ptr[T any](v T) *T {
+ return &v
+}
+
+func MapPtr[T any, U any](v *T, fn func(T) U) *U {
+ if v == nil {
+ return nil
+ }
+ return Ptr(fn(*v))
+}
+
+func PtrOrNil[T comparable](v T) *T {
+ var zero T
+ if zero == v {
+ return nil
+ }
+ return &v
+}
+
+func ResolvePtr[T any](v *T, def T) T {
+ if v == nil {
+ return def
+ }
+ return *v
+}
+
+func MapSlice[T any, U any](s []T, fn func(T) U) []U {
+ if len(s) == 0 {
+ return nil
+ }
+ result := make([]U, len(s))
+ for i := range s {
+ result[i] = fn(s[i])
+ }
+ return result
+}
+
+func FilterSlice[T any](s []T, fn func(T) bool) []T {
+ if len(s) == 0 {
+ return nil
+ }
+ result := make([]T, 0)
+ for i := range s {
+ if fn(s[i]) {
+ result = append(result, s[i])
+ }
+ }
+ return result
+}
+
+func UniqueSlice[T comparable](s []T) []T {
+ if len(s) == 0 {
+ return nil
+ }
+ result := make([]T, 0)
+ seen := map[T]struct{}{}
+ for i := range s {
+ _, ok := seen[s[i]]
+ if !ok {
+ seen[s[i]] = struct{}{}
+ result = append(result, s[i])
+ }
+ }
+ return result
+}
+
+func MapMap[T any, U any](m map[string]T, fn func(T) U) map[string]U {
+ if len(m) == 0 {
+ return nil
+ }
+ res := make(map[string]U, len(m))
+ for k, v := range m {
+ res[k] = fn(v)
+ }
+ return res
+}
+
+func GetMapValue[T any, K comparable](m map[K]T, k K, def T) T {
+ v, ok := m[k]
+ if ok {
+ return v
+ }
+ return def
+}
+
+func GetOr(v ...string) string {
+ for i := range v {
+ if v[i] != "" {
+ return v[i]
+ }
+ }
+ return ""
+}
diff --git a/internal/common/crd.go b/internal/common/crd.go
new file mode 100644
index 00000000000..ed2ff77fcca
--- /dev/null
+++ b/internal/common/crd.go
@@ -0,0 +1,126 @@
+package common
+
+import (
+ "encoding/json"
+ "reflect"
+ "regexp"
+
+ "gopkg.in/yaml.v2"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/client-go/kubernetes/scheme"
+)
+
+type SerializeOptions struct {
+ OmitCreationTimestamp bool
+ CleanMeta bool
+ Kind string
+ GroupVersion *schema.GroupVersion
+}
+
+type ObjectWithTypeMeta interface {
+ SetGroupVersionKind(schema.GroupVersionKind)
+}
+
+func AppendTypeMeta(kind string, version schema.GroupVersion, crs ...ObjectWithTypeMeta) {
+ for _, cr := range crs {
+ cr.SetGroupVersionKind(schema.GroupVersionKind{
+ Group: version.Group,
+ Version: version.Version,
+ Kind: kind,
+ })
+ }
+}
+
+func CleanObjectMeta(crs ...metav1.Object) {
+ for _, cr := range crs {
+ cr.SetGeneration(0)
+ cr.SetResourceVersion("")
+ cr.SetSelfLink("")
+ cr.SetUID("")
+ cr.SetFinalizers(nil)
+ cr.SetOwnerReferences(nil)
+ cr.SetManagedFields(nil)
+
+ annotations := cr.GetAnnotations()
+ delete(annotations, "kubectl.kubernetes.io/last-applied-configuration")
+ cr.SetAnnotations(annotations)
+ }
+}
+
+var creationTsNullRegex = regexp.MustCompile(`\n\s+creationTimestamp: null`)
+var creationTsRegex = regexp.MustCompile(`\n\s+creationTimestamp:[^\n]*`)
+
+func SerializeCRD(cr interface{}, opts SerializeOptions) ([]byte, error) {
+ if opts.CleanMeta || (opts.Kind != "" && opts.GroupVersion != nil) {
+ // For simplicity, support both direct struct (as in *List.Items), as well as the pointer itself
+ if reflect.ValueOf(cr).Kind() == reflect.Struct {
+ v := reflect.ValueOf(cr)
+ p := reflect.New(v.Type())
+ p.Elem().Set(v)
+ cr = p.Interface()
+ }
+
+ // Deep copy object, as it will have modifications
+ switch cr.(type) {
+ case runtime.Object:
+ cr = cr.(runtime.Object).DeepCopyObject()
+ }
+
+ // Clean messy metadata
+ if opts.CleanMeta {
+ if v, ok := cr.(metav1.Object); ok {
+ CleanObjectMeta(v)
+ cr = v
+ }
+ }
+
+ // Append metadata when expected
+ if opts.Kind != "" && opts.GroupVersion != nil {
+ if v, ok := cr.(ObjectWithTypeMeta); ok {
+ AppendTypeMeta(opts.Kind, *opts.GroupVersion, v)
+ cr = v
+ }
+ }
+ }
+
+ out, err := json.Marshal(cr)
+ if err != nil {
+ return nil, err
+ }
+ m := yaml.MapSlice{}
+ _ = yaml.Unmarshal(out, &m)
+ b, _ := yaml.Marshal(m)
+ if opts.OmitCreationTimestamp {
+ b = creationTsRegex.ReplaceAll(b, nil)
+ } else {
+ b = creationTsNullRegex.ReplaceAll(b, nil)
+ }
+ return b, err
+}
+
+var crdSeparator = []byte("---\n")
+
+// SerializeCRDs builds a serialized version of CRD,
+// persisting the order of properties from the struct.
+func SerializeCRDs[T interface{}](crs []T, opts SerializeOptions) ([]byte, error) {
+ result := []byte(nil)
+ for _, cr := range crs {
+ b, err := SerializeCRD(cr, opts)
+ if err != nil {
+ return nil, err
+ }
+ if len(result) > 0 {
+ result = append(append(result, crdSeparator...), b...)
+ } else {
+ result = b
+ }
+ }
+ return result, nil
+}
+
+func DeserializeCRD(cr runtime.Object, content []byte) error {
+ _, _, err := scheme.Codecs.UniversalDeserializer().Decode(content, nil, cr)
+ return err
+}
diff --git a/internal/common/crd_test.go b/internal/common/crd_test.go
new file mode 100644
index 00000000000..5d3e27d1ac3
--- /dev/null
+++ b/internal/common/crd_test.go
@@ -0,0 +1,224 @@
+package common
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ testsv3 "github.com/kubeshop/testkube-operator/api/tests/v3"
+)
+
+var (
+ time1 = time.Now().UTC()
+ testBare = testsv3.Test{
+ TypeMeta: metav1.TypeMeta{},
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-name",
+ },
+ Spec: testsv3.TestSpec{
+ Description: "some-description",
+ },
+ }
+ testWithCreationTimestamp = testsv3.Test{
+ TypeMeta: metav1.TypeMeta{},
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-name",
+ CreationTimestamp: metav1.Time{Time: time1},
+ },
+ Spec: testsv3.TestSpec{
+ Description: "some-description",
+ },
+ }
+ testWrongOrder = testsv3.Test{
+ TypeMeta: metav1.TypeMeta{},
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-name",
+ },
+ // Use keys that are not alphabetically ordered
+ Spec: testsv3.TestSpec{
+ Schedule: "abc",
+ Name: "example-name",
+ Description: "some-description",
+ },
+ }
+ testMessyData = testsv3.Test{
+ TypeMeta: metav1.TypeMeta{},
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-name",
+ ManagedFields: []metav1.ManagedFieldsEntry{
+ {
+ Manager: "some-manager",
+ Operation: "some-operation",
+ APIVersion: "v1",
+ FieldsType: "blah",
+ Subresource: "meh",
+ },
+ },
+ },
+ // Use keys that are not alphabetically ordered
+ Spec: testsv3.TestSpec{
+ Description: "some-description",
+ },
+ }
+)
+
+func TestSerializeCRDNoMutations(t *testing.T) {
+ value := testBare.DeepCopy()
+ _, _ = SerializeCRD(value, SerializeOptions{
+ CleanMeta: true,
+ OmitCreationTimestamp: true,
+ Kind: "Test",
+ GroupVersion: &testsv3.GroupVersion,
+ })
+
+ assert.Equal(t, value.TypeMeta, testBare.TypeMeta)
+ assert.Equal(t, value.ObjectMeta, testBare.ObjectMeta)
+}
+
+func TestSerializeCRD(t *testing.T) {
+ b, err := SerializeCRD(testBare.DeepCopy(), SerializeOptions{})
+ b2, err2 := SerializeCRD(testWithCreationTimestamp.DeepCopy(), SerializeOptions{OmitCreationTimestamp: true})
+ want := strings.TrimSpace(`
+metadata:
+ name: test-name
+spec:
+ description: some-description
+status: {}
+`)
+ assert.NoError(t, err)
+ assert.Equal(t, want+"\n", string(b))
+ assert.NoError(t, err2)
+ assert.Equal(t, want+"\n", string(b2))
+}
+
+func TestSerializeCRDWithCreationTimestamp(t *testing.T) {
+ b, err := SerializeCRD(testWithCreationTimestamp.DeepCopy(), SerializeOptions{})
+ want := strings.TrimSpace(`
+metadata:
+ name: test-name
+ creationTimestamp: "%s"
+spec:
+ description: some-description
+status: {}
+`)
+ want = fmt.Sprintf(want, time1.Format(time.RFC3339))
+ assert.NoError(t, err)
+ assert.Equal(t, want+"\n", string(b))
+}
+
+func TestSerializeCRDWithMessyData(t *testing.T) {
+ b, err := SerializeCRD(testMessyData.DeepCopy(), SerializeOptions{})
+ b2, err2 := SerializeCRD(testMessyData.DeepCopy(), SerializeOptions{CleanMeta: true})
+ want := strings.TrimSpace(`
+metadata:
+ name: test-name
+ managedFields:
+ - manager: some-manager
+ operation: some-operation
+ apiVersion: v1
+ fieldsType: blah
+ subresource: meh
+spec:
+ description: some-description
+status: {}
+`)
+ want2 := strings.TrimSpace(`
+metadata:
+ name: test-name
+spec:
+ description: some-description
+status: {}
+`)
+ assert.NoError(t, err)
+ assert.Equal(t, want+"\n", string(b))
+ assert.NoError(t, err2)
+ assert.Equal(t, want2+"\n", string(b2))
+}
+
+func TestSerializeCRDKeepOrder(t *testing.T) {
+ b, err := SerializeCRD(*testWrongOrder.DeepCopy(), SerializeOptions{})
+ want := strings.TrimSpace(`
+metadata:
+ name: test-name
+spec:
+ name: example-name
+ description: some-description
+ schedule: abc
+status: {}
+`)
+ assert.NoError(t, err)
+ assert.Equal(t, want+"\n", string(b))
+}
+
+func TestSerializeCRDs(t *testing.T) {
+ b, err := SerializeCRDs([]testsv3.Test{
+ *testWrongOrder.DeepCopy(),
+ *testBare.DeepCopy(),
+ }, SerializeOptions{})
+ want := strings.TrimSpace(`
+metadata:
+ name: test-name
+spec:
+ name: example-name
+ description: some-description
+ schedule: abc
+status: {}
+---
+metadata:
+ name: test-name
+spec:
+ description: some-description
+status: {}
+`)
+ assert.NoError(t, err)
+ assert.Equal(t, want+"\n", string(b))
+}
+
+func TestSerializeCRDsFullCleanup(t *testing.T) {
+ list := testsv3.TestList{
+ Items: []testsv3.Test{
+ *testWrongOrder.DeepCopy(),
+ *testBare.DeepCopy(),
+ *testWithCreationTimestamp.DeepCopy(),
+ },
+ }
+ b, err := SerializeCRDs(list.Items, SerializeOptions{
+ CleanMeta: true,
+ OmitCreationTimestamp: true,
+ Kind: "Test",
+ GroupVersion: &testsv3.GroupVersion,
+ })
+ want := strings.TrimSpace(`
+kind: Test
+apiVersion: tests.testkube.io/v3
+metadata:
+ name: test-name
+spec:
+ name: example-name
+ description: some-description
+ schedule: abc
+status: {}
+---
+kind: Test
+apiVersion: tests.testkube.io/v3
+metadata:
+ name: test-name
+spec:
+ description: some-description
+status: {}
+---
+kind: Test
+apiVersion: tests.testkube.io/v3
+metadata:
+ name: test-name
+spec:
+ description: some-description
+status: {}
+`)
+ assert.NoError(t, err)
+ assert.Equal(t, want+"\n", string(b))
+}
diff --git a/internal/config/config.go b/internal/config/config.go
index 01d1f9c9bb7..ac1378c7016 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -7,81 +7,102 @@ 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:""`
- NatsURI string `envconfig:"NATS_URI" default:"nats://localhost:4222"`
- NatsSecure bool `envconfig:"NATS_SECURE" default:"false"`
- NatsSkipVerify bool `envconfig:"NATS_SKIP_VERIFY" default:"false"`
- NatsCertFile string `envconfig:"NATS_CERT_FILE" default:""`
- NatsKeyFile string `envconfig:"NATS_KEY_FILE" default:""`
- NatsCAFile string `envconfig:"NATS_CA_FILE" default:""`
- JobServiceAccountName string `envconfig:"JOB_SERVICE_ACCOUNT_NAME" default:""`
- JobTemplateFile string `envconfig:"JOB_TEMPLATE_FILE" default:""`
- DisableTestTriggers bool `envconfig:"DISABLE_TEST_TRIGGERS" default:"false"`
- TestkubeDefaultExecutors string `envconfig:"TESTKUBE_DEFAULT_EXECUTORS" default:""`
- TestkubeEnabledExecutors string `envconfig:"TESTKUBE_ENABLED_EXECUTORS" default:""`
- TestkubeTemplateJob string `envconfig:"TESTKUBE_TEMPLATE_JOB" default:""`
- TestkubeContainerTemplateJob string `envconfig:"TESTKUBE_CONTAINER_TEMPLATE_JOB" default:""`
- TestkubeContainerTemplateScraper string `envconfig:"TESTKUBE_CONTAINER_TEMPLATE_SCRAPER" default:""`
- TestkubeContainerTemplatePVC string `envconfig:"TESTKUBE_CONTAINER_TEMPLATE_PVC" default:""`
- TestkubeTemplateSlavePod string `envconfig:"TESTKUBE_TEMPLATE_SLAVE_POD" default:""`
- TestkubeConfigDir string `envconfig:"TESTKUBE_CONFIG_DIR" default:"config"`
- TestkubeAnalyticsEnabled bool `envconfig:"TESTKUBE_ANALYTICS_ENABLED" default:"false"`
- TestkubeReadonlyExecutors bool `envconfig:"TESTKUBE_READONLY_EXECUTORS" default:"false"`
- TestkubeNamespace string `envconfig:"TESTKUBE_NAMESPACE" default:"testkube"`
- TestkubeOAuthClientID string `envconfig:"TESTKUBE_OAUTH_CLIENTID" default:""`
- TestkubeOAuthClientSecret string `envconfig:"TESTKUBE_OAUTH_CLIENTSECRET" default:""`
- TestkubeOAuthProvider string `envconfig:"TESTKUBE_OAUTH_PROVIDER" default:""`
- TestkubeOAuthScopes string `envconfig:"TESTKUBE_OAUTH_SCOPES" default:""`
- TestkubeProAPIKey string `envconfig:"TESTKUBE_PRO_API_KEY" default:""`
- TestkubeProURL string `envconfig:"TESTKUBE_PRO_URL" default:""`
- TestkubeProTLSInsecure bool `envconfig:"TESTKUBE_PRO_TLS_INSECURE" default:"false"`
- TestkubeProWorkerCount int `envconfig:"TESTKUBE_PRO_WORKER_COUNT" default:"50"`
- TestkubeProLogStreamWorkerCount int `envconfig:"TESTKUBE_PRO_LOG_STREAM_WORKER_COUNT" default:"25"`
- TestkubeProSkipVerify bool `envconfig:"TESTKUBE_PRO_SKIP_VERIFY" default:"false"`
- TestkubeWatcherNamespaces string `envconfig:"TESTKUBE_WATCHER_NAMESPACES" default:""`
- GraphqlPort string `envconfig:"TESTKUBE_GRAPHQL_PORT" default:"8070"`
- TestkubeRegistry string `envconfig:"TESTKUBE_REGISTRY" default:""`
- TestkubePodStartTimeout time.Duration `envconfig:"TESTKUBE_POD_START_TIMEOUT" default:"30m"`
- CDEventsTarget string `envconfig:"CDEVENTS_TARGET" default:""`
- TestkubeDashboardURI string `envconfig:"TESTKUBE_DASHBOARD_URI" default:""`
- DisableReconciler bool `envconfig:"DISABLE_RECONCILER" default:"false"`
- TestkubeClusterName string `envconfig:"TESTKUBE_CLUSTER_NAME" default:""`
- CompressArtifacts bool `envconfig:"COMPRESSARTIFACTS" default:"false"`
- TestkubeHelmchartVersion string `envconfig:"TESTKUBE_HELMCHART_VERSION" default:""`
- DebugListenAddr string `envconfig:"DEBUG_LISTEN_ADDR" default:"0.0.0.0:1337"`
- EnableDebugServer bool `envconfig:"ENABLE_DEBUG_SERVER" default:"false"`
- EnableSecretsEndpoint bool `envconfig:"ENABLE_SECRETS_ENDPOINT" default:"false"`
- DisableMongoMigrations bool `envconfig:"DISABLE_MONGO_MIGRATIONS" default:"false"`
- Debug bool `envconfig:"DEBUG" default:"false"`
+ 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:""`
+ NatsURI string `envconfig:"NATS_URI" default:"nats://localhost:4222"`
+ NatsSecure bool `envconfig:"NATS_SECURE" default:"false"`
+ NatsSkipVerify bool `envconfig:"NATS_SKIP_VERIFY" default:"false"`
+ NatsCertFile string `envconfig:"NATS_CERT_FILE" default:""`
+ NatsKeyFile string `envconfig:"NATS_KEY_FILE" default:""`
+ NatsCAFile string `envconfig:"NATS_CA_FILE" default:""`
+ NatsConnectTimeout time.Duration `envconfig:"NATS_CONNECT_TIMEOUT" default:"5s"`
+ JobServiceAccountName string `envconfig:"JOB_SERVICE_ACCOUNT_NAME" default:""`
+ JobTemplateFile string `envconfig:"JOB_TEMPLATE_FILE" default:""`
+ DisableTestTriggers bool `envconfig:"DISABLE_TEST_TRIGGERS" default:"false"`
+ TestkubeDefaultExecutors string `envconfig:"TESTKUBE_DEFAULT_EXECUTORS" default:""`
+ TestkubeEnabledExecutors string `envconfig:"TESTKUBE_ENABLED_EXECUTORS" default:""`
+ TestkubeTemplateJob string `envconfig:"TESTKUBE_TEMPLATE_JOB" default:""`
+ TestkubeContainerTemplateJob string `envconfig:"TESTKUBE_CONTAINER_TEMPLATE_JOB" default:""`
+ TestkubeContainerTemplateScraper string `envconfig:"TESTKUBE_CONTAINER_TEMPLATE_SCRAPER" default:""`
+ TestkubeContainerTemplatePVC string `envconfig:"TESTKUBE_CONTAINER_TEMPLATE_PVC" default:""`
+ TestkubeTemplateSlavePod string `envconfig:"TESTKUBE_TEMPLATE_SLAVE_POD" default:""`
+ TestkubeConfigDir string `envconfig:"TESTKUBE_CONFIG_DIR" default:"config"`
+ TestkubeAnalyticsEnabled bool `envconfig:"TESTKUBE_ANALYTICS_ENABLED" default:"false"`
+ TestkubeReadonlyExecutors bool `envconfig:"TESTKUBE_READONLY_EXECUTORS" default:"false"`
+ TestkubeNamespace string `envconfig:"TESTKUBE_NAMESPACE" default:"testkube"`
+ TestkubeOAuthClientID string `envconfig:"TESTKUBE_OAUTH_CLIENTID" default:""`
+ TestkubeOAuthClientSecret string `envconfig:"TESTKUBE_OAUTH_CLIENTSECRET" default:""`
+ TestkubeOAuthProvider string `envconfig:"TESTKUBE_OAUTH_PROVIDER" default:""`
+ TestkubeOAuthScopes string `envconfig:"TESTKUBE_OAUTH_SCOPES" default:""`
+ TestkubeProAPIKey string `envconfig:"TESTKUBE_PRO_API_KEY" default:""`
+ TestkubeProURL string `envconfig:"TESTKUBE_PRO_URL" default:""`
+ TestkubeProLogsPath string `envconfig:"TESTKUBE_PRO_LOGS_PATH" default:"/logs"`
+ TestkubeProTLSInsecure bool `envconfig:"TESTKUBE_PRO_TLS_INSECURE" default:"false"`
+ TestkubeProWorkerCount int `envconfig:"TESTKUBE_PRO_WORKER_COUNT" default:"50"`
+ TestkubeProLogStreamWorkerCount int `envconfig:"TESTKUBE_PRO_LOG_STREAM_WORKER_COUNT" default:"25"`
+ TestkubeProWorkflowNotificationsWorkerCount int `envconfig:"TESTKUBE_PRO_WORKFLOW_NOTIFICATIONS_STREAM_WORKER_COUNT" default:"25"`
+ TestkubeProSkipVerify bool `envconfig:"TESTKUBE_PRO_SKIP_VERIFY" default:"false"`
+ TestkubeProEnvID string `envconfig:"TESTKUBE_PRO_ENV_ID" default:""`
+ TestkubeProOrgID string `envconfig:"TESTKUBE_PRO_ORG_ID" default:""`
+ TestkubeProMigrate string `envconfig:"TESTKUBE_PRO_MIGRATE" default:"false"`
+ TestkubeProConnectionTimeout int `envconfig:"TESTKUBE_PRO_CONNECTION_TIMEOUT" default:"10"`
+ TestkubeProCertFile string `envconfig:"TESTKUBE_PRO_CERT_FILE" default:""`
+ TestkubeProKeyFile string `envconfig:"TESTKUBE_PRO_KEY_FILE" default:""`
+ TestkubeProCAFile string `envconfig:"TESTKUBE_PRO_CA_FILE" default:""`
+ TestkubeProTLSSecret string `envconfig:"TESTKUBE_PRO_TLS_SECRET" default:""`
+ TestkubeWatcherNamespaces string `envconfig:"TESTKUBE_WATCHER_NAMESPACES" default:""`
+ GraphqlPort string `envconfig:"TESTKUBE_GRAPHQL_PORT" default:"8070"`
+ TestkubeRegistry string `envconfig:"TESTKUBE_REGISTRY" default:""`
+ TestkubePodStartTimeout time.Duration `envconfig:"TESTKUBE_POD_START_TIMEOUT" default:"30m"`
+ CDEventsTarget string `envconfig:"CDEVENTS_TARGET" default:""`
+ TestkubeDashboardURI string `envconfig:"TESTKUBE_DASHBOARD_URI" default:""`
+ DisableReconciler bool `envconfig:"DISABLE_RECONCILER" default:"false"`
+ TestkubeClusterName string `envconfig:"TESTKUBE_CLUSTER_NAME" default:""`
+ CompressArtifacts bool `envconfig:"COMPRESSARTIFACTS" default:"false"`
+ TestkubeHelmchartVersion string `envconfig:"TESTKUBE_HELMCHART_VERSION" default:""`
+ DebugListenAddr string `envconfig:"DEBUG_LISTEN_ADDR" default:"0.0.0.0:1337"`
+ EnableDebugServer bool `envconfig:"ENABLE_DEBUG_SERVER" default:"false"`
+ EnableSecretsEndpoint bool `envconfig:"ENABLE_SECRETS_ENDPOINT" default:"false"`
+ DisableMongoMigrations bool `envconfig:"DISABLE_MONGO_MIGRATIONS" default:"false"`
+ Debug bool `envconfig:"DEBUG" default:"false"`
+ EnableImageDataPersistentCache bool `envconfig:"TESTKUBE_ENABLE_IMAGE_DATA_PERSISTENT_CACHE" default:"false"`
+ ImageDataPersistentCacheKey string `envconfig:"TESTKUBE_IMAGE_DATA_PERSISTENT_CACHE_KEY" default:"testkube-image-cache"`
+ LogServerGrpcAddress string `envconfig:"LOG_SERVER_GRPC_ADDRESS" default:":9090"`
+ LogServerSecure bool `envconfig:"LOG_SERVER_SECURE" default:"false"`
+ LogServerSkipVerify bool `envconfig:"LOG_SERVER_SKIP_VERIFY" default:"false"`
+ LogServerCertFile string `envconfig:"LOG_SERVER_CERT_FILE" default:""`
+ LogServerKeyFile string `envconfig:"LOG_SERVER_KEY_FILE" default:""`
+ LogServerCAFile string `envconfig:"LOG_SERVER_CA_FILE" default:""`
+ DisableSecretCreation bool `envconfig:"DISABLE_SECRET_CREATION" default:"false"`
+ TestkubeExecutionNamespaces string `envconfig:"TESTKUBE_EXECUTION_NAMESPACES" default:""`
// DEPRECATED: Use TestkubeProAPIKey instead
TestkubeCloudAPIKey string `envconfig:"TESTKUBE_CLOUD_API_KEY" default:""`
@@ -93,6 +114,12 @@ type Config struct {
TestkubeCloudWorkerCount int `envconfig:"TESTKUBE_CLOUD_WORKER_COUNT" default:"50"`
// DEPRECATED: Use TestkubeProLogStreamWorkerCount instead
TestkubeCloudLogStreamWorkerCount int `envconfig:"TESTKUBE_CLOUD_LOG_STREAM_WORKER_COUNT" default:"25"`
+ // DEPRECATED: Use TestkubeProEnvID instead
+ TestkubeCloudEnvID string `envconfig:"TESTKUBE_CLOUD_ENV_ID" default:""`
+ // DEPRECATED: Use TestkubeProOrgID instead
+ TestkubeCloudOrgID string `envconfig:"TESTKUBE_CLOUD_ORG_ID" default:""`
+ // DEPRECATED: Use TestkubeProMigrate instead
+ TestkubeCloudMigrate string `envconfig:"TESTKUBE_CLOUD_MIGRATE" default:"false"`
}
func Get() (*Config, error) {
@@ -124,4 +151,16 @@ func (c *Config) CleanLegacyVars() {
if c.TestkubeProLogStreamWorkerCount == 0 && c.TestkubeCloudLogStreamWorkerCount != 0 {
c.TestkubeProLogStreamWorkerCount = c.TestkubeCloudLogStreamWorkerCount
}
+
+ if c.TestkubeProEnvID == "" && c.TestkubeCloudEnvID != "" {
+ c.TestkubeProEnvID = c.TestkubeCloudEnvID
+ }
+
+ if c.TestkubeProOrgID == "" && c.TestkubeCloudOrgID != "" {
+ c.TestkubeProOrgID = c.TestkubeCloudOrgID
+ }
+
+ if c.TestkubeProMigrate == "" && c.TestkubeCloudMigrate != "" {
+ c.TestkubeProMigrate = c.TestkubeCloudMigrate
+ }
}
diff --git a/internal/config/procontext.go b/internal/config/procontext.go
new file mode 100644
index 00000000000..2429a4a61b1
--- /dev/null
+++ b/internal/config/procontext.go
@@ -0,0 +1,16 @@
+package config
+
+type ProContext struct {
+ APIKey string
+ URL string
+ LogsPath string
+ TLSInsecure bool
+ WorkerCount int
+ LogStreamWorkerCount int
+ WorkflowNotificationsWorkerCount int
+ SkipVerify bool
+ EnvID string
+ OrgID string
+ Migrate string
+ ConnectionTimeout int
+}
diff --git a/internal/db-migrations/03_testworkflow_indexes.down.json b/internal/db-migrations/03_testworkflow_indexes.down.json
new file mode 100644
index 00000000000..9cf60fefb58
--- /dev/null
+++ b/internal/db-migrations/03_testworkflow_indexes.down.json
@@ -0,0 +1,14 @@
+[
+ {
+ "dropIndexes": "workflowresults",
+ "index": [
+ "workflow.name_1_statusat-1",
+ "workflow.name_1_scheduledat-1",
+ "id_1",
+ "name_1",
+ "result.status_1",
+ "statusat_-1",
+ "scheduledat_-1"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/internal/db-migrations/03_testworkflow_indexes.up.json b/internal/db-migrations/03_testworkflow_indexes.up.json
new file mode 100644
index 00000000000..6026007c98b
--- /dev/null
+++ b/internal/db-migrations/03_testworkflow_indexes.up.json
@@ -0,0 +1,35 @@
+[
+ {
+ "createIndexes": "workflowresults",
+ "indexes": [
+ {
+ "key": {"workflow.name": 1, "statusat": -1},
+ "name": "workflow.name_1_statusat-1"
+ },
+ {
+ "key": {"workflow.name": 1, "scheduledat": -1},
+ "name": "workflow.name_1_scheduledat-1"
+ },
+ {
+ "key": {"id": 1},
+ "name": "id_1"
+ },
+ {
+ "key": {"name": 1},
+ "name": "name_1"
+ },
+ {
+ "key": {"result.status": 1},
+ "name": "result.status_1"
+ },
+ {
+ "key": {"statusat": -1},
+ "name": "statusat_-1"
+ },
+ {
+ "key": {"scheduledat": -1},
+ "name": "scheduledat_-1"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/licenses/MIT.txt b/licenses/MIT.txt
new file mode 100644
index 00000000000..3ad02909079
--- /dev/null
+++ b/licenses/MIT.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Kubeshop
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/licenses/TCL.txt b/licenses/TCL.txt
new file mode 100644
index 00000000000..ee5b74c2386
--- /dev/null
+++ b/licenses/TCL.txt
@@ -0,0 +1,391 @@
+Testkube Community License Agreement
+
+Please read this Testkube Community License Agreement (the “Agreement”)
+carefully before using Testkube (as defined below), which is offered by
+Testkube or its affiliated Legal Entities (“Testkube”).
+
+By accessing, installing, downloading or in any manner using Testkube,
+You agree that You have read and agree to be bound by the terms of this
+Agreement. If You are accessing Testkube on behalf of a Legal Entity,
+You represent and warrant that You have the authority to agree to these
+terms on its behalf and the right to bind that Legal Entity to this
+Agreement. Use of Testkube is expressly conditioned upon Your assent to
+all the terms of this Agreement, as well as the other Testkube agreements,
+including the Testkube Privacy Policy and Testkube Terms and Conditions,
+accessible at: https://testkube.io/privacy-policy and
+https://testkube.io/terms-and-conditions.
+
+1. Definitions. In addition to other terms defined elsewhere in this Agreement
+and in the other Testkube agreements, the terms below have the following
+meanings.
+
+(a) “Testkube” shall mean the Test Orchestration and Execution software
+provided by Testkube, including both Testkube Core and Testkube Pro, as
+defined below.
+
+(b) “Testkube Core” shall mean the version and features of Testkube designated
+as free of charge at https://testkube.io/pricing and available at
+https://github.com/kubeshop/testkube pursuant to the terms of the MIT license.
+
+(c) “Testkube Pro” shall mean the version of Testkube which includes the
+additional paid features of Testkube designated at
+https://testkube.io/pricing and made available by Testkube, also at
+https://github.com/kubeshop/testkube, the use of which is subject to additional
+terms set out below.
+
+(d) “Contribution” shall mean any work of authorship, including the original
+version of the Work and any modifications or additions to that Work or
+Derivative Works thereof, that is intentionally submitted to Testkube for
+inclusion in the Work by the copyright owner or by an individual or Legal Entity
+authorized to submit on behalf of the copyright owner. For the purposes of this
+definition, “submitted” means any form of electronic, verbal, or written
+communication sent to Testkube or its representatives, including but not
+limited to communication on electronic mailing lists, source code control
+systems, and issue tracking systems that are managed by, or on behalf of,
+Testkube for the purpose of discussing and improving the Work, but excluding
+communication that is conspicuously marked or otherwise designated in writing
+by the copyright owner as “Not a Contribution.”
+
+(e) “Contributor” shall mean any copyright owner or individual or Legal Entity
+authorized by the copyright owner, other than Testkube, from whom Testkube
+receives a Contribution that Testkube subsequently incorporates within the Work.
+
+(f) “Derivative Works” shall mean any work, whether in Source or Object form,
+that is based on (or derived from) the Work, such as a translation, abridgement,
+condensation, or any other recasting, transformation, or adaptation for which
+the editorial revisions, annotations, elaborations, or other modifications
+represent, as a whole, an original work of authorship. For the purposes of this
+License, Derivative Works shall not include works that remain separable from, or
+merely link (or bind by name) to the interfaces of, the Work and Derivative
+Works thereof. You may create certain Derivative Works of Testkube Pro (“Pro
+Derivative Works”, as defined below) provided that such Pro Derivative Works
+are solely created, distributed, and accessed for Your internal use, and are
+not created, distributed, or accessed in such a way that the Pro Derivative
+Works would modify, circumvent, or otherwise bypass controls implemented, if
+any, to ensure that Testkube Pro users comply with the terms of the Paid Pro
+License. Notwithstanding anything contained herein to the contrary, You may not
+modify or alter the Source of Testkube Pro absent Testkube’s prior express
+written permission. If You have any questions about creating Pro Derivative
+Works or otherwise modifying or redistributing Testkube Pro, please contact
+Testkube at support@testkube.io.
+
+(g) “Legal Entity” shall mean the union of the acting entity and all other
+entities that control, are controlled by, or are under common control with that
+entity. For the purposes of this definition, “control” means (i) the power,
+direct or indirect, to cause the direction or management of such entity, whether
+by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of
+the outstanding shares, or (iii) beneficial ownership of such entity.
+
+(h) “License” shall mean the terms and conditions for use, reproduction, and
+distribution of a Work as defined by this Agreement.
+
+(i) “Licensor” shall mean Testkube or a Contributor, as applicable.
+
+(j) “Object” form shall mean any form resulting from mechanical transformation
+or translation of a Source form, including but not limited to compiled object
+code, generated documentation, and conversions to other media types.
+
+(k) “Source” form shall mean the preferred form for making modifications,
+including but not limited to software source code, documentation source, and
+configuration files.
+
+(l) “Third Party Works” shall mean Works, including Contributions, and other
+technology owned by a person or Legal Entity other than Testkube, as indicated
+by a copyright notice that is included in or attached to such Works or technology.
+
+(m) “Work” shall mean the work of authorship, whether in Source or Object form,
+made available under a License, as indicated by a copyright notice that is
+included in or attached to the work.
+
+(n) “You” (or “Your”) shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+2. Licenses.
+
+(a) License to Testkube Core. The License for the applicable version of
+Testkube Core can be found on the Testkube Licensing FAQs page and in the
+applicable license file within the Testkube GitHub repository(ies). Testkube
+Core is a no-cost, entry-level license and as such, contains the following
+disclaimers: NOTWITHSTANDING ANYTHING TO THE CONTRARY HEREIN, TESTKUBE CORE
+IS PROVIDED “AS IS” AND “AS AVAILABLE”, AND ALL EXPRESS OR IMPLIED WARRANTIES
+ARE EXCLUDED AND DISCLAIMED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND ANY
+WARRANTIES ARISING BY STATUTE OR OTHERWISE IN LAW OR FROM COURSE OF DEALING,
+COURSE OF PERFORMANCE, OR USE IN TRADE. For clarity, the terms of this Agreement,
+other than the relevant definitions in Section 1 and this Section 2(a) do not
+apply to Testkube Core.
+
+(b) License to Testkube Pro.
+
+(i) Grant of Copyright License: Subject to the terms of this Agreement, Licensor
+hereby grants to You a worldwide, non-exclusive, non-transferable limited
+license to reproduce, prepare Pro Derivative Works (as defined below) of,
+publicly display, publicly perform, sublicense, and distribute Testkube Pro for
+Your business purposes, for so long as You are not in violation of this
+Section 2(b) and are current on all payments required by Section 4 below.
+
+(ii) Grant of Patent License: Subject to the terms of this Agreement, Licensor
+hereby grants to You a worldwide, non-exclusive, non-transferable limited patent
+license to make, have made, use, and import Testkube Pro, where such license
+applies only to those patent claims licensable by Licensor that are necessarily
+infringed by their Contribution(s) alone or by combination of their
+Contribution(s) with the Work to which such Contribution(s) was submitted. If You
+institute patent litigation against any entity (including a cross-claim or
+counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated
+within the Work constitutes direct or contributory patent infringement, then any
+patent licenses granted to You under this License for that Work shall terminate
+as of the date such litigation is filed.
+
+(iii) License to Third Party Works: From time to time Testkube may use, or
+provide You access to, Third Party Works in connection with Testkube Pro. You
+acknowledge and agree that in addition to this Agreement, Your use of Third Party
+Works is subject to all other terms and conditions set forth in the License
+provided with or contained in such Third Party Works. Some Third Party Works may
+be licensed to You solely for use with Testkube Pro under the terms of a third
+party License, or as otherwise notified by Testkube, and not under the terms of
+this Agreement. You agree that the owners and third party licensors of Third
+Party Works are intended third party beneficiaries to this Agreement, and You
+agree to abide by all third party terms and conditions and licenses.
+
+3. Support. From time to time, in its sole discretion, Testkube may offer
+professional services or support for Testkube, which may now or in the future be
+subject to additional fees, as outlined at https://testkube.io/pricing.
+
+4. Fees for Testkube Pro or Testkube Support.
+
+(a) Fees. The License to Testkube Pro is conditioned upon Your entering into a
+subscription agreement with Testkube for its use (a “Paid Pro License”) and
+timely paying Testkube for such Paid Pro License; provided that features of
+Testkube Pro that are features of Testkube Core and are not designated as “Pro
+features” at https://testkube.io/pricing may be used for free under the terms of
+the Agreement without a Paid Pro License. Testkube Pro may at its discretion
+include within Testkube Pro certain Source code solely intended to determine
+Your compliance with the Paid Pro License which may be accessed without a Paid
+Pro License, provided that under no circumstances may You modify Testkube Pro
+to circumvent the Paid Pro License requirement. Any professional services or
+support for Testkube may also be subject to Your payment of fees, which will be
+specified by Testkube when you sign up to receive such professional services or
+support. Testkube reserves the right to change the fees at any time with prior
+written notice; for recurring fees, any such adjustments will take effect as of
+the next pay period.
+
+(b) Overdue Payments and Taxes. Overdue payments are subject to a service charge
+equal to the lesser of 1.5% per month or the maximum legal interest rate allowed
+by law, and You shall pay all Testkube reasonable costs of collection, including
+court costs and attorneys’ fees. Fees are stated and payable in U.S. dollars and
+are exclusive of all sales, use, value added and similar taxes, duties,
+withholdings and other governmental assessments (but excluding taxes based on
+Testkube income) that may be levied on the transactions contemplated by this
+Agreement in any jurisdiction, all of which are Your responsibility unless you
+have provided Testkube with a valid tax-exempt certificate. If You owe Testkube
+overdue payments, Testkube reserves the right to revoke any license(s) granted
+by this Agreement and revoke to Your access to Testkube Core and to Testkube Pro.
+
+(c) Record-keeping and Audit. If fees for Testkube Pro are based on the number
+of environments running on Testkube Pro or another use-based unit of measurement,
+including number of users, You must maintain complete and accurate records with
+respect Your use of Testkube Pro and will provide such records to Testkube for
+inspection or audit upon Testkube’s reasonable request. If an inspection or
+audit uncovers additional usage by You for which fees are owed under this
+Agreement, then You shall pay for such additional usage at Testkube’s
+then-current rates.
+
+5. Trial License. If You have signed up for a trial or evaluation of Testkube
+Pro, Your License to Testkube Pro is granted without charge for the trial or
+evaluation period specified when You signed up, or if no term was specified, for
+forty-five (45) calendar days, provided that Your License is granted solely for
+purposes of Your internal evaluation of Testkube Pro during the trial or
+evaluation period (a “Trial License”). You may not use Testkube Pro or any
+Testkube Pro features under a Trial License more than once in any twelve (12)
+month period. Testkube may revoke a Trial License at any time and for any reason.
+Sections 3, 4, 9 and 11 of this Agreement do not apply to Trial Licenses.
+
+6. Redistribution. You may reproduce and distribute copies of the Work or
+Derivative Works thereof in any medium, with or without modifications, and in
+Source or Object form, provided that You meet the following conditions:
+
+(a) You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+
+(b) You must cause any modified files to carry prominent notices stating that
+You changed the files; and
+
+(c) You must retain, in the Source form of any Derivative Works that You
+distribute, including for internal purposes at Your Legal Entities, all
+copyright, patent, trademark, and attribution notices from the Source form of
+the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+
+(d) If the Work includes a “NOTICE” or equivalent text file as part of its
+distribution, then any Derivative Works that You distribute must include a
+readable copy of the attribution notices contained within such NOTICE file,
+excluding those notices that do not pertain to any part of the Derivative Works,
+in at least one of the following places: within a NOTICE text file distributed
+as part of the Derivative Works; within the Source form or documentation, if
+provided along with the Derivative Works; or, within a display generated by the
+Derivative Works, if and wherever such third-party notices normally appear. The
+contents of the NOTICE or equivalent files are for informational purposes only
+and do not modify the License. You may add Your own attribution notices within
+Derivative Works that You distribute for Your internal use, alongside or as an
+addendum to the NOTICE text from the Work, provided that such additional
+attribution notices cannot be construed as modifying the License.
+
+You may not create Derivative Works, including Pro Derivative Works (as defined
+below), which add Your own copyright statements or provide additional or
+different license terms and conditions for use, reproduction, or distribution of
+Your modifications, or for any such Derivative Works as a whole. All Derivative
+Works, including Your use, reproduction, and distribution of the Work, must
+comply in all respects with the conditions stated in this License.
+
+(e) Pro Derivative Works: Derivative Works of Testkube Pro (“Pro Derivative
+Works”) may only be made, reproduced and distributed, without modifications, in
+Source or Object form, provided that such Pro Derivative Works are solely for
+Your internal use. Each Pro Derivative Work shall be governed by this Agreement,
+shall include a License to Testkube Pro, and thus will be subject to the payment
+of fees to Testkube by any user of the Pro Derivative Work.
+
+7. Submission of Contributions. Unless You explicitly state otherwise, any
+Contribution submitted for inclusion in Testkube Pro by You to Testkube shall be
+under the terms and conditions of this Agreement, without any additional terms
+or conditions, payments of royalties or otherwise to Your benefit. Testkube may
+at any time, at its sole discretion, elect for the Contribution to be subject to
+the Paid Pro License. If You wish to reserve any rights regarding Your
+Contribution, You must contact Testkube at support@testkube.io prior to
+submitting the Contribution.
+
+8. Trademarks. This License does not grant permission to use the trade names,
+trademarks, service marks, or product names of Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and reproducing
+the content of the NOTICE or equivalent file.
+
+9. Limited Warranty.
+
+(a) Warranties. Subject to the terms of the Paid Pro License, or any other
+agreement between You and Testkube which governs the terms of Your access to
+Testkube Pro, Testkube warrants to You that: (i) Testkube Pro will materially
+perform in accordance with the applicable documentation for thirty (30) days
+after initial delivery to You; and (ii) any professional services performed by
+Testkube under this Agreement will be performed in a workmanlike manner, in
+accordance with general industry standards.
+
+(b) Exclusions. Testkube’s warranties in this Section 9 do not extend to problems
+that result from: (i) Your failure to implement updates issued by Testkube during
+the warranty period; (ii) any alterations or additions (including Pro Derivative
+Works and Contributions) to Testkube not performed by or at the direction of
+Testkube; (iii) failures that are not reproducible by Testkube; (iv) operation
+of Testkube Pro in violation of this Agreement or not in accordance with its
+documentation; (v) failures caused by software, hardware, or products not
+licensed or provided by Testkube hereunder; or (vi) Third Party Works.
+
+(c) Remedies. In the event of a breach of a warranty under this Section 9,
+Testkube will, at its discretion and cost, either repair, replace or re-perform
+the applicable Works or services or refund a portion of fees previously paid to
+Testkube that are associated with the defective Works or services. This is Your
+exclusive remedy, and Testkube’s sole liability, arising in connection with the
+limited warranties herein and shall, in all cases, be limited to the fees paid
+to Testkube in the three (3) months preceding the delivery of the defective Works
+or services.
+
+10. Disclaimer of Warranty. Except as set out in Section 9, unless required by
+applicable law, Licensor provides the Work (and each Contributor provides its
+Contributions) on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+either express or implied, arising out of course of dealing, course of
+performance, or usage in trade, including, without limitation, any warranties
+or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, CORRECTNESS,
+RELIABILITY, or FITNESS FOR A PARTICULAR PURPOSE, all of which are hereby
+disclaimed. You are solely responsible for determining the appropriateness of
+using or redistributing Works and assume any risks associated with Your exercise
+of permissions under the applicable License for such Works.
+
+11. Limited Indemnity.
+
+(a) Indemnity. Testkube will defend, indemnify and hold You harmless against
+any third party claims, liabilities or expenses incurred (including reasonable
+attorneys’ fees), as well as amounts finally awarded in a settlement or a
+non-appealable judgement by a court (“Losses”), to the extent arising from any
+claim or allegation by a third party that Testkube Pro infringes or
+misappropriates a valid United States patent, copyright, or trade secret right
+of a third party; provided that You give Testkube: (i) prompt written notice of
+any such claim or allegation; (ii) sole control of the defense and settlement
+thereof; and (iii) reasonable cooperation and assistance in such defense or
+settlement. If any Work within Testkube Pro becomes or in Testkube’s opinion is
+likely to become, the subject of an injunction, Testkube may, at its option,
+(A) procure for You the right to continue using such Work, (B) replace or modify
+such Work so that it becomes non-infringing without substantially compromising
+its functionality, or, if (A) and (B) are not commercially practicable, then (C)
+terminate Your license to the allegedly infringing Work and refund to You a
+prorated portion of the prepaid and unearned fees for such infringing Work. The
+foregoing comprises the entire liability of Testkube with respect to infringement
+of patents, copyrights, trade secrets, or other intellectual property rights.
+
+(b) Exclusions. The foregoing obligations on Testkube shall not apply to: (i)
+Works modified by any party other than Testkube (including Pro Derivative Works
+and Contributions) where the alleged infringement relates to such modification,
+(ii) Works combined or bundled with any products, processes, or materials not
+provided by Testkube where the alleged infringement relates to such combination,
+(iii) use of a version of Testkube Pro other than the version that was current at
+the time of such use, as long as a non-infringing version had been released at
+the time of the alleged infringement, (iv) any Works created to Your
+specifications, (v) infringement or misappropriation of any proprietary or
+intellectual property right in which You have an interest, or (vi) Third Party
+Works. You will defend, indemnify, and hold Testkube harmless against any Losses
+arising from any such claim or allegation as described in the scenarios in this
+Section 11(b), subject to conditions reciprocal to those in Section 11(a).
+
+12. Limitation of Liability. In no event and under no legal or equitable theory,
+whether in tort (including negligence), contract, or otherwise, unless required
+by applicable law (such as deliberate and grossly negligent acts), and
+notwithstanding anything in this Agreement to the contrary, shall Licensor or
+any Contributor be liable to You for (i) any amounts in excess, in the aggregate,
+of the fees paid by You to Testkube under this Agreement in the twelve (12)
+months preceding the date the first cause of liability arose, or (ii) any
+indirect, special, incidental, punitive, exemplary, reliance, or consequential
+damages of any character arising as a result of this Agreement or out of the use
+or inability to use the Work (including but not limited to damages for loss of
+goodwill, profits, data or data use, work stoppage, computer failure or
+malfunction, cost of procurement of substitute goods, technology or services,
+or any and all other commercial damages or losses), even if such Licensor or
+Contributor has been advised of the possibility of such damages. THESE
+LIMITATIONS SHALL APPLY NOTWITHSTANDING THE FAILURE OF THE ESSENTIAL PURPOSE OF
+ANY LIMITED REMEDY.
+
+13. General.
+
+(a) Relationship of Parties. You and Testkube are independent contractors, and
+nothing herein shall be deemed to constitute either party as the agent or
+representative of the other or both parties as joint venturers or partners for
+any purpose.
+
+(b) Export Control. You shall comply with the U.S. Foreign Corrupt Practices Act
+and all applicable export laws, restrictions and regulations of the U.S.
+Department of Commerce, U.S. Department of Treasury, and any other applicable
+U.S. and foreign authority(ies).
+
+(c) Assignment. This Agreement and the rights and obligations herein may not be
+assigned or transferred, in whole or in part, by You without the prior written
+consent of Testkube. Any assignment in violation of this provision is void. This
+Agreement shall be binding upon, and inure to the benefit of, the successors and
+permitted assigns of the parties.
+
+(d) Governing Law. This Agreement shall be governed by and construed under the
+laws of the State of Delaware and the United States without regard to conflicts
+of laws provisions thereof, and without regard to the Uniform Computer
+Information Transactions Act.
+
+(e) Attorneys’ Fees. In any action or proceeding to enforce rights under this
+Agreement, the prevailing party shall be entitled to recover its costs, expenses,
+and attorneys’ fees.
+
+(f) Severability. If any provision of this Agreement is held to be invalid,
+illegal, or unenforceable in any respect, that provision shall be limited or
+eliminated to the minimum extent necessary so that this Agreement otherwise
+remains in full force and effect and enforceable.
+
+(g) Entire Agreement; Waivers; Modification. This Agreement constitutes the
+entire agreement between the parties relating to the subject matter hereof and
+supersedes all proposals, understandings, or discussions, whether written or
+oral, relating to the subject matter of this Agreement and all past dealing or
+industry custom. The failure of either party to enforce its rights under this
+Agreement at any time for any period shall not be construed as a waiver of such
+rights. No changes, modifications or waivers to this Agreement will be effective
+unless in writing and signed by both parties.
diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go
index 69219565377..b6d6c7466f6 100644
--- a/pkg/agent/agent.go
+++ b/pkg/agent/agent.go
@@ -3,6 +3,7 @@ package agent
import (
"context"
"crypto/tls"
+ "crypto/x509"
"fmt"
"math"
"os"
@@ -24,8 +25,10 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
+ "github.com/kubeshop/testkube/internal/config"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/cloud"
+ "github.com/kubeshop/testkube/pkg/featureflags"
)
const (
@@ -36,22 +39,37 @@ const (
orgIdMeta = "environment-id"
envIdMeta = "organization-id"
healthcheckCommand = "healthcheck"
-
- cloudMigrateEnvName = "TESTKUBE_CLOUD_MIGRATE"
- cloudEnvIdEnvName = "TESTKUBE_CLOUD_ENV_ID"
- cloudOrgIdEnvName = "TESTKUBE_CLOUD_ORG_ID"
)
// buffer up to five messages per worker
const bufferSizePerWorker = 5
-func NewGRPCConnection(ctx context.Context, isInsecure bool, skipVerify bool, server string, logger *zap.SugaredLogger) (*grpc.ClientConn, error) {
+func NewGRPCConnection(
+ ctx context.Context,
+ isInsecure bool,
+ skipVerify bool,
+ server string,
+ certFile, keyFile, caFile string,
+ logger *zap.SugaredLogger,
+) (*grpc.ClientConn, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
- var tlsConfig *tls.Config
+ tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
if skipVerify {
tlsConfig = &tls.Config{InsecureSkipVerify: true}
+ } else {
+ if certFile != "" && keyFile != "" {
+ if err := clientCert(tlsConfig, certFile, keyFile); err != nil {
+ return nil, err
+ }
+ }
+ if caFile != "" {
+ if err := rootCAs(tlsConfig, caFile); err != nil {
+ return nil, err
+ }
+ }
}
+
creds := credentials.NewTLS(tlsConfig)
if isInsecure {
creds = insecure.NewCredentials()
@@ -79,6 +97,35 @@ func NewGRPCConnection(ctx context.Context, isInsecure bool, skipVerify bool, se
)
}
+func rootCAs(tlsConfig *tls.Config, file ...string) error {
+ pool := x509.NewCertPool()
+ for _, f := range file {
+ rootPEM, err := os.ReadFile(f)
+ if err != nil || rootPEM == nil {
+ return fmt.Errorf("agent: error loading or parsing rootCA file: %v", err)
+ }
+ ok := pool.AppendCertsFromPEM(rootPEM)
+ if !ok {
+ return fmt.Errorf("agent: failed to parse root certificate from %q", f)
+ }
+ }
+ tlsConfig.RootCAs = pool
+ return nil
+}
+
+func clientCert(tlsConfig *tls.Config, certFile, keyFile string) error {
+ cert, err := tls.LoadX509KeyPair(certFile, keyFile)
+ if err != nil {
+ return fmt.Errorf("agent: error loading client certificate: %v", err)
+ }
+ cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
+ if err != nil {
+ return fmt.Errorf("agent: error parsing client certificate: %v", err)
+ }
+ tlsConfig.Certificates = []tls.Certificate{cert}
+ return nil
+}
+
type Agent struct {
client cloud.TestKubeCloudAPIClient
handler fasthttp.RequestHandler
@@ -94,6 +141,11 @@ type Agent struct {
logStreamResponseBuffer chan *cloud.LogsStreamResponse
logStreamFunc func(ctx context.Context, executionID string) (chan output.Output, error)
+ testWorkflowNotificationsWorkerCount int
+ testWorkflowNotificationsRequestBuffer chan *cloud.TestWorkflowNotificationsRequest
+ testWorkflowNotificationsResponseBuffer chan *cloud.TestWorkflowNotificationsResponse
+ testWorkflowNotificationsFunc func(ctx context.Context, executionID string) (chan testkube.TestWorkflowExecutionNotification, error)
+
events chan testkube.Event
sendTimeout time.Duration
receiveTimeout time.Duration
@@ -102,38 +154,47 @@ type Agent struct {
clusterID string
clusterName string
envs map[string]string
+ features featureflags.FeatureFlags
+
+ proContext config.ProContext
}
func NewAgent(logger *zap.SugaredLogger,
handler fasthttp.RequestHandler,
- apiKey string,
client cloud.TestKubeCloudAPIClient,
- workerCount int,
- logStreamWorkerCount int,
logStreamFunc func(ctx context.Context, executionID string) (chan output.Output, error),
+ workflowNotificationsFunc func(ctx context.Context, executionID string) (chan testkube.TestWorkflowExecutionNotification, error),
clusterID string,
clusterName string,
envs map[string]string,
+ features featureflags.FeatureFlags,
+ proContext config.ProContext,
) (*Agent, error) {
return &Agent{
- handler: handler,
- logger: logger,
- apiKey: apiKey,
- client: client,
- events: make(chan testkube.Event),
- workerCount: workerCount,
- requestBuffer: make(chan *cloud.ExecuteRequest, bufferSizePerWorker*workerCount),
- responseBuffer: make(chan *cloud.ExecuteResponse, bufferSizePerWorker*workerCount),
- receiveTimeout: 5 * time.Minute,
- sendTimeout: 30 * time.Second,
- healthcheckInterval: 30 * time.Second,
- logStreamWorkerCount: logStreamWorkerCount,
- logStreamRequestBuffer: make(chan *cloud.LogsStreamRequest, bufferSizePerWorker*logStreamWorkerCount),
- logStreamResponseBuffer: make(chan *cloud.LogsStreamResponse, bufferSizePerWorker*logStreamWorkerCount),
- logStreamFunc: logStreamFunc,
- clusterID: clusterID,
- clusterName: clusterName,
- envs: envs,
+ handler: handler,
+ logger: logger,
+ apiKey: proContext.APIKey,
+ client: client,
+ events: make(chan testkube.Event),
+ workerCount: proContext.WorkerCount,
+ requestBuffer: make(chan *cloud.ExecuteRequest, bufferSizePerWorker*proContext.WorkerCount),
+ responseBuffer: make(chan *cloud.ExecuteResponse, bufferSizePerWorker*proContext.WorkerCount),
+ receiveTimeout: 5 * time.Minute,
+ sendTimeout: 30 * time.Second,
+ healthcheckInterval: 30 * time.Second,
+ logStreamWorkerCount: proContext.LogStreamWorkerCount,
+ logStreamRequestBuffer: make(chan *cloud.LogsStreamRequest, bufferSizePerWorker*proContext.LogStreamWorkerCount),
+ logStreamResponseBuffer: make(chan *cloud.LogsStreamResponse, bufferSizePerWorker*proContext.LogStreamWorkerCount),
+ logStreamFunc: logStreamFunc,
+ testWorkflowNotificationsWorkerCount: proContext.WorkflowNotificationsWorkerCount,
+ testWorkflowNotificationsRequestBuffer: make(chan *cloud.TestWorkflowNotificationsRequest, bufferSizePerWorker*proContext.WorkflowNotificationsWorkerCount),
+ testWorkflowNotificationsResponseBuffer: make(chan *cloud.TestWorkflowNotificationsResponse, bufferSizePerWorker*proContext.WorkflowNotificationsWorkerCount),
+ testWorkflowNotificationsFunc: workflowNotificationsFunc,
+ clusterID: clusterID,
+ clusterName: clusterName,
+ envs: envs,
+ features: features,
+ proContext: proContext,
}, nil
}
@@ -165,11 +226,20 @@ func (ag *Agent) run(ctx context.Context) (err error) {
return ag.runEventLoop(groupCtx)
})
+ if !ag.features.LogsV2 {
+ g.Go(func() error {
+ return ag.runLogStreamLoop(groupCtx)
+ })
+ g.Go(func() error {
+ return ag.runLogStreamWorker(groupCtx, ag.logStreamWorkerCount)
+ })
+ }
+
g.Go(func() error {
- return ag.runLogStreamLoop(groupCtx)
+ return ag.runTestWorkflowNotificationsLoop(groupCtx)
})
g.Go(func() error {
- return ag.runLogStreamWorker(groupCtx, ag.logStreamWorkerCount)
+ return ag.runTestWorkflowNotificationsWorker(groupCtx, ag.testWorkflowNotificationsWorkerCount)
})
err = g.Wait()
@@ -238,14 +308,14 @@ func (ag *Agent) receiveCommand(ctx context.Context, stream cloud.TestKubeCloudA
}
func (ag *Agent) runCommandLoop(ctx context.Context) error {
- ctx = AddAPIKeyMeta(ctx, ag.apiKey)
+ ctx = AddAPIKeyMeta(ctx, ag.proContext.APIKey)
ctx = metadata.AppendToOutgoingContext(ctx, clusterIDMeta, ag.clusterID)
- ctx = metadata.AppendToOutgoingContext(ctx, cloudMigrateMeta, os.Getenv(cloudMigrateEnvName))
- ctx = metadata.AppendToOutgoingContext(ctx, envIdMeta, os.Getenv(cloudEnvIdEnvName))
- ctx = metadata.AppendToOutgoingContext(ctx, orgIdMeta, os.Getenv(cloudOrgIdEnvName))
+ ctx = metadata.AppendToOutgoingContext(ctx, cloudMigrateMeta, ag.proContext.Migrate)
+ ctx = metadata.AppendToOutgoingContext(ctx, envIdMeta, ag.proContext.EnvID)
+ ctx = metadata.AppendToOutgoingContext(ctx, orgIdMeta, ag.proContext.OrgID)
- ag.logger.Infow("initiating streaming connection with Cloud API")
+ ag.logger.Infow("initiating streaming connection with Pro API")
// creates a new Stream from the client side. ctx is used for the lifetime of the stream.
opts := []grpc.CallOption{grpc.UseCompressor(gzip.Name), grpc.MaxCallRecvMsgSize(math.MaxInt32)}
stream, err := ag.client.ExecuteAsync(ctx, opts...)
diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go
index 4cd3eebfaac..f6c5cee62eb 100644
--- a/pkg/agent/agent_test.go
+++ b/pkg/agent/agent_test.go
@@ -8,6 +8,7 @@ import (
"testing"
"time"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/executor/output"
"github.com/kubeshop/testkube/pkg/log"
"github.com/kubeshop/testkube/pkg/ui"
@@ -19,8 +20,10 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
+ "github.com/kubeshop/testkube/internal/config"
"github.com/kubeshop/testkube/pkg/agent"
"github.com/kubeshop/testkube/pkg/cloud"
+ "github.com/kubeshop/testkube/pkg/featureflags"
)
func TestCommandExecution(t *testing.T) {
@@ -47,16 +50,18 @@ func TestCommandExecution(t *testing.T) {
atomic.AddInt32(&msgCnt, 1)
}
- grpcConn, err := agent.NewGRPCConnection(context.Background(), true, false, url, log.DefaultLogger)
+ grpcConn, err := agent.NewGRPCConnection(context.Background(), true, false, url, "", "", "", log.DefaultLogger)
ui.ExitOnError("error creating gRPC connection", err)
defer grpcConn.Close()
grpcClient := cloud.NewTestKubeCloudAPIClient(grpcConn)
var logStreamFunc func(ctx context.Context, executionID string) (chan output.Output, error)
+ var workflowNotificationsStreamFunc func(ctx context.Context, executionID string) (chan testkube.TestWorkflowExecutionNotification, error)
logger, _ := zap.NewDevelopment()
- agent, err := agent.NewAgent(logger.Sugar(), m, "api-key", grpcClient, 5, 5, logStreamFunc, "", "", nil)
+ proContext := config.ProContext{APIKey: "api-key", WorkerCount: 5, LogStreamWorkerCount: 5, WorkflowNotificationsWorkerCount: 5}
+ agent, err := agent.NewAgent(logger.Sugar(), m, grpcClient, logStreamFunc, workflowNotificationsStreamFunc, "", "", nil, featureflags.FeatureFlags{}, proContext)
if err != nil {
t.Fatal(err)
}
@@ -85,6 +90,12 @@ func (cs *CloudServer) GetLogsStream(srv cloud.TestKubeCloudAPI_GetLogsStreamSer
return nil
}
+func (cs *CloudServer) GetTestWorkflowNotificationsStream(srv cloud.TestKubeCloudAPI_GetTestWorkflowNotificationsStreamServer) error {
+ <-cs.ctx.Done()
+
+ return nil
+}
+
func (cs *CloudServer) ExecuteAsync(srv cloud.TestKubeCloudAPI_ExecuteAsyncServer) error {
md, ok := metadata.FromIncomingContext(srv.Context())
if !ok {
diff --git a/pkg/agent/events_test.go b/pkg/agent/events_test.go
index 7eacca223a5..990c6664673 100644
--- a/pkg/agent/events_test.go
+++ b/pkg/agent/events_test.go
@@ -17,9 +17,11 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
+ "github.com/kubeshop/testkube/internal/config"
"github.com/kubeshop/testkube/pkg/agent"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/cloud"
+ "github.com/kubeshop/testkube/pkg/featureflags"
)
func TestEventLoop(t *testing.T) {
@@ -45,14 +47,17 @@ func TestEventLoop(t *testing.T) {
logger, _ := zap.NewDevelopment()
- grpcConn, err := agent.NewGRPCConnection(context.Background(), true, false, url, log.DefaultLogger)
+ grpcConn, err := agent.NewGRPCConnection(context.Background(), true, false, url, "", "", "", log.DefaultLogger)
ui.ExitOnError("error creating gRPC connection", err)
defer grpcConn.Close()
grpcClient := cloud.NewTestKubeCloudAPIClient(grpcConn)
var logStreamFunc func(ctx context.Context, executionID string) (chan output.Output, error)
- agent, err := agent.NewAgent(logger.Sugar(), nil, "api-key", grpcClient, 5, 5, logStreamFunc, "", "", nil)
+ var workflowNotificationsStreamFunc func(ctx context.Context, executionID string) (chan testkube.TestWorkflowExecutionNotification, error)
+
+ proContext := config.ProContext{APIKey: "api-key", WorkerCount: 5, LogStreamWorkerCount: 5, WorkflowNotificationsWorkerCount: 5}
+ agent, err := agent.NewAgent(logger.Sugar(), nil, grpcClient, logStreamFunc, workflowNotificationsStreamFunc, "", "", nil, featureflags.FeatureFlags{}, proContext)
assert.NoError(t, err)
go func() {
l, err := agent.Load()
@@ -98,6 +103,12 @@ func (cws *CloudEventServer) GetLogsStream(srv cloud.TestKubeCloudAPI_GetLogsStr
return nil
}
+func (cws *CloudEventServer) GetTestWorkflowNotificationsStream(srv cloud.TestKubeCloudAPI_GetTestWorkflowNotificationsStreamServer) error {
+ <-cws.ctx.Done()
+
+ return nil
+}
+
func (cws *CloudEventServer) Send(srv cloud.TestKubeCloudAPI_SendServer) error {
md, ok := metadata.FromIncomingContext(srv.Context())
if !ok {
diff --git a/pkg/agent/logs_test.go b/pkg/agent/logs_test.go
index 1c45a303382..4847c88fba9 100644
--- a/pkg/agent/logs_test.go
+++ b/pkg/agent/logs_test.go
@@ -7,9 +7,12 @@ import (
"testing"
"time"
+ "github.com/kubeshop/testkube/internal/config"
"github.com/kubeshop/testkube/pkg/agent"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/cloud"
"github.com/kubeshop/testkube/pkg/executor/output"
+ "github.com/kubeshop/testkube/pkg/featureflags"
"github.com/kubeshop/testkube/pkg/log"
"github.com/kubeshop/testkube/pkg/ui"
@@ -43,7 +46,7 @@ func TestLogStream(t *testing.T) {
fmt.Fprintf(ctx, "Hi there! RequestURI is %q", ctx.RequestURI())
}
- grpcConn, err := agent.NewGRPCConnection(context.Background(), true, false, url, log.DefaultLogger)
+ grpcConn, err := agent.NewGRPCConnection(context.Background(), true, false, url, "", "", "", log.DefaultLogger)
ui.ExitOnError("error creating gRPC connection", err)
defer grpcConn.Close()
@@ -61,9 +64,11 @@ func TestLogStream(t *testing.T) {
msgCnt++
return ch, nil
}
+ var workflowNotificationsStreamFunc func(ctx context.Context, executionID string) (chan testkube.TestWorkflowExecutionNotification, error)
logger, _ := zap.NewDevelopment()
- agent, err := agent.NewAgent(logger.Sugar(), m, "api-key", grpcClient, 5, 5, logStreamFunc, "", "", nil)
+ proContext := config.ProContext{APIKey: "api-key", WorkerCount: 5, LogStreamWorkerCount: 5, WorkflowNotificationsWorkerCount: 5}
+ agent, err := agent.NewAgent(logger.Sugar(), m, grpcClient, logStreamFunc, workflowNotificationsStreamFunc, "", "", nil, featureflags.FeatureFlags{}, proContext)
if err != nil {
t.Fatal(err)
}
@@ -90,6 +95,12 @@ func (cs *CloudLogsServer) ExecuteAsync(srv cloud.TestKubeCloudAPI_ExecuteAsyncS
<-cs.ctx.Done()
return nil
}
+
+func (cs *CloudLogsServer) GetTestWorkflowNotificationsStream(srv cloud.TestKubeCloudAPI_GetTestWorkflowNotificationsStreamServer) error {
+ <-cs.ctx.Done()
+ return nil
+}
+
func (cs *CloudLogsServer) GetLogsStream(srv cloud.TestKubeCloudAPI_GetLogsStreamServer) error {
md, ok := metadata.FromIncomingContext(srv.Context())
if !ok {
diff --git a/pkg/agent/testworkflows.go b/pkg/agent/testworkflows.go
new file mode 100644
index 00000000000..e5439e675cf
--- /dev/null
+++ b/pkg/agent/testworkflows.go
@@ -0,0 +1,215 @@
+package agent
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "math"
+ "time"
+
+ "github.com/pkg/errors"
+ "golang.org/x/sync/errgroup"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/encoding/gzip"
+
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/cloud"
+)
+
+const testWorkflowNotificationsRetryCount = 10
+
+func getTestWorkflowNotificationType(n testkube.TestWorkflowExecutionNotification) cloud.TestWorkflowNotificationType {
+ if n.Result != nil {
+ return cloud.TestWorkflowNotificationType_WORKFLOW_STREAM_RESULT
+ } else if n.Output != nil {
+ return cloud.TestWorkflowNotificationType_WORKFLOW_STREAM_OUTPUT
+ }
+ return cloud.TestWorkflowNotificationType_WORKFLOW_STREAM_LOG
+}
+
+func (ag *Agent) runTestWorkflowNotificationsLoop(ctx context.Context) error {
+ ctx = AddAPIKeyMeta(ctx, ag.apiKey)
+
+ ag.logger.Infow("initiating workflow notifications streaming connection with Cloud API")
+ // creates a new Stream from the client side. ctx is used for the lifetime of the stream.
+ opts := []grpc.CallOption{grpc.UseCompressor(gzip.Name), grpc.MaxCallRecvMsgSize(math.MaxInt32)}
+ stream, err := ag.client.GetTestWorkflowNotificationsStream(ctx, opts...)
+ if err != nil {
+ ag.logger.Errorf("failed to execute: %w", err)
+ return errors.Wrap(err, "failed to setup stream")
+ }
+
+ // GRPC stream have special requirements for concurrency on SendMsg, and RecvMsg calls.
+ // Please check https://github.com/grpc/grpc-go/blob/master/Documentation/concurrency.md
+ g, groupCtx := errgroup.WithContext(ctx)
+ g.Go(func() error {
+ for {
+ cmd, err := ag.receiveTestWorkflowNotificationsRequest(groupCtx, stream)
+ if err != nil {
+ return err
+ }
+
+ ag.testWorkflowNotificationsRequestBuffer <- cmd
+ }
+ })
+
+ g.Go(func() error {
+ for {
+ select {
+ case resp := <-ag.testWorkflowNotificationsResponseBuffer:
+ err := ag.sendTestWorkflowNotificationsResponse(groupCtx, stream, resp)
+ if err != nil {
+ return err
+ }
+ case <-groupCtx.Done():
+ return groupCtx.Err()
+ }
+ }
+ })
+
+ err = g.Wait()
+
+ return err
+}
+
+func (ag *Agent) runTestWorkflowNotificationsWorker(ctx context.Context, numWorkers int) error {
+ g, groupCtx := errgroup.WithContext(ctx)
+ for i := 0; i < numWorkers; i++ {
+ g.Go(func() error {
+ for {
+ select {
+ case req := <-ag.testWorkflowNotificationsRequestBuffer:
+ if req.RequestType == cloud.TestWorkflowNotificationsRequestType_WORKFLOW_STREAM_HEALTH_CHECK {
+ ag.testWorkflowNotificationsResponseBuffer <- &cloud.TestWorkflowNotificationsResponse{
+ StreamId: req.StreamId,
+ SeqNo: 0,
+ }
+ break
+ }
+
+ err := ag.executeWorkflowNotificationsRequest(groupCtx, req)
+ if err != nil {
+ ag.logger.Errorf("error executing workflow notifications request: %s", err.Error())
+ }
+ case <-groupCtx.Done():
+ return groupCtx.Err()
+ }
+ }
+ })
+ }
+ return g.Wait()
+}
+
+func (ag *Agent) executeWorkflowNotificationsRequest(ctx context.Context, req *cloud.TestWorkflowNotificationsRequest) error {
+ notificationsCh, err := ag.testWorkflowNotificationsFunc(ctx, req.ExecutionId)
+ for i := 0; i < testWorkflowNotificationsRetryCount; i++ {
+ if err != nil {
+ // We have a race condition here
+ // Cloud sometimes slow to insert execution or test
+ // while WorkflowNotifications request from websockets comes in faster
+ // so we retry up to testWorkflowNotificationsRetryCount times.
+ time.Sleep(100 * time.Millisecond)
+ notificationsCh, err = ag.testWorkflowNotificationsFunc(ctx, req.ExecutionId)
+ }
+ }
+ if err != nil {
+ message := fmt.Sprintf("cannot get pod logs: %s", err.Error())
+ ag.testWorkflowNotificationsResponseBuffer <- &cloud.TestWorkflowNotificationsResponse{
+ StreamId: req.StreamId,
+ SeqNo: 0,
+ Type: cloud.TestWorkflowNotificationType_WORKFLOW_STREAM_ERROR,
+ Message: message,
+ }
+ return nil
+ }
+
+ for {
+ var i uint32
+ select {
+ case n, ok := <-notificationsCh:
+ if !ok {
+ return nil
+ }
+ t := getTestWorkflowNotificationType(n)
+ msg := &cloud.TestWorkflowNotificationsResponse{
+ StreamId: req.StreamId,
+ SeqNo: i,
+ Timestamp: n.Ts.Format(time.RFC3339Nano),
+ Ref: n.Ref,
+ Type: t,
+ }
+ if n.Result != nil {
+ m, _ := json.Marshal(n.Result)
+ msg.Message = string(m)
+ } else if n.Output != nil {
+ m, _ := json.Marshal(n.Output)
+ msg.Message = string(m)
+ } else {
+ msg.Message = n.Log
+ }
+ i++
+
+ select {
+ case ag.testWorkflowNotificationsResponseBuffer <- msg:
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+}
+
+func (ag *Agent) receiveTestWorkflowNotificationsRequest(ctx context.Context, stream cloud.TestKubeCloudAPI_GetTestWorkflowNotificationsStreamClient) (*cloud.TestWorkflowNotificationsRequest, error) {
+ respChan := make(chan testWorkflowNotificationsRequest, 1)
+ go func() {
+ cmd, err := stream.Recv()
+ respChan <- testWorkflowNotificationsRequest{resp: cmd, err: err}
+ }()
+
+ var cmd *cloud.TestWorkflowNotificationsRequest
+ select {
+ case resp := <-respChan:
+ cmd = resp.resp
+ err := resp.err
+
+ if err != nil {
+ ag.logger.Errorf("agent stream receive: %v", err)
+ return nil, err
+ }
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+
+ return cmd, nil
+}
+
+type testWorkflowNotificationsRequest struct {
+ resp *cloud.TestWorkflowNotificationsRequest
+ err error
+}
+
+func (ag *Agent) sendTestWorkflowNotificationsResponse(ctx context.Context, stream cloud.TestKubeCloudAPI_GetTestWorkflowNotificationsStreamClient, resp *cloud.TestWorkflowNotificationsResponse) error {
+ errChan := make(chan error, 1)
+ go func() {
+ errChan <- stream.Send(resp)
+ close(errChan)
+ }()
+
+ t := time.NewTimer(ag.sendTimeout)
+ select {
+ case err := <-errChan:
+ if !t.Stop() {
+ <-t.C
+ }
+ return err
+ case <-ctx.Done():
+ if !t.Stop() {
+ <-t.C
+ }
+
+ return ctx.Err()
+ case <-t.C:
+ return errors.New("send response too slow")
+ }
+}
diff --git a/pkg/api/v1/client/api.go b/pkg/api/v1/client/api.go
index 73ed3ba089b..bcd0c27a11e 100644
--- a/pkg/api/v1/client/api.go
+++ b/pkg/api/v1/client/api.go
@@ -38,6 +38,14 @@ func NewProxyAPIClient(client kubernetes.Interface, config APIConfig) APIClient
TestSourceClient: NewTestSourceClient(NewProxyClient[testkube.TestSource](client, config)),
CopyFileClient: NewCopyFileProxyClient(client, config),
TemplateClient: NewTemplateClient(NewProxyClient[testkube.Template](client, config)),
+ TestWorkflowClient: NewTestWorkflowClient(
+ NewProxyClient[testkube.TestWorkflow](client, config),
+ NewProxyClient[testkube.TestWorkflowWithExecution](client, config),
+ NewProxyClient[testkube.TestWorkflowExecution](client, config),
+ NewProxyClient[testkube.TestWorkflowExecutionsResult](client, config),
+ NewProxyClient[testkube.Artifact](client, config),
+ ),
+ TestWorkflowTemplateClient: NewTestWorkflowTemplateClient(NewProxyClient[testkube.TestWorkflowTemplate](client, config)),
}
}
@@ -68,6 +76,14 @@ func NewDirectAPIClient(httpClient *http.Client, sseClient *http.Client, apiURI,
TestSourceClient: NewTestSourceClient(NewDirectClient[testkube.TestSource](httpClient, apiURI, apiPathPrefix)),
CopyFileClient: NewCopyFileDirectClient(httpClient, apiURI, apiPathPrefix),
TemplateClient: NewTemplateClient(NewDirectClient[testkube.Template](httpClient, apiURI, apiPathPrefix)),
+ TestWorkflowClient: NewTestWorkflowClient(
+ NewDirectClient[testkube.TestWorkflow](httpClient, apiURI, apiPathPrefix),
+ NewDirectClient[testkube.TestWorkflowWithExecution](httpClient, apiURI, apiPathPrefix),
+ NewDirectClient[testkube.TestWorkflowExecution](httpClient, apiURI, apiPathPrefix),
+ NewDirectClient[testkube.TestWorkflowExecutionsResult](httpClient, apiURI, apiPathPrefix),
+ NewDirectClient[testkube.Artifact](httpClient, apiURI, apiPathPrefix),
+ ),
+ TestWorkflowTemplateClient: NewTestWorkflowTemplateClient(NewDirectClient[testkube.TestWorkflowTemplate](httpClient, apiURI, apiPathPrefix)),
}
}
@@ -98,6 +114,14 @@ func NewCloudAPIClient(httpClient *http.Client, sseClient *http.Client, apiURI,
TestSourceClient: NewTestSourceClient(NewCloudClient[testkube.TestSource](httpClient, apiURI, apiPathPrefix)),
CopyFileClient: NewCopyFileDirectClient(httpClient, apiURI, apiPathPrefix),
TemplateClient: NewTemplateClient(NewCloudClient[testkube.Template](httpClient, apiURI, apiPathPrefix)),
+ TestWorkflowClient: NewTestWorkflowClient(
+ NewCloudClient[testkube.TestWorkflow](httpClient, apiURI, apiPathPrefix).WithSSEClient(sseClient),
+ NewCloudClient[testkube.TestWorkflowWithExecution](httpClient, apiURI, apiPathPrefix),
+ NewCloudClient[testkube.TestWorkflowExecution](httpClient, apiURI, apiPathPrefix),
+ NewCloudClient[testkube.TestWorkflowExecutionsResult](httpClient, apiURI, apiPathPrefix),
+ NewCloudClient[testkube.Artifact](httpClient, apiURI, apiPathPrefix),
+ ),
+ TestWorkflowTemplateClient: NewTestWorkflowTemplateClient(NewCloudClient[testkube.TestWorkflowTemplate](httpClient, apiURI, apiPathPrefix)),
}
}
@@ -111,4 +135,6 @@ type APIClient struct {
TestSourceClient
CopyFileClient
TemplateClient
+ TestWorkflowClient
+ TestWorkflowTemplateClient
}
diff --git a/pkg/api/v1/client/common.go b/pkg/api/v1/client/common.go
index f9c86bda82f..95fdc0c14ea 100644
--- a/pkg/api/v1/client/common.go
+++ b/pkg/api/v1/client/common.go
@@ -7,7 +7,9 @@ import (
"fmt"
"io"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/executor/output"
+ "github.com/kubeshop/testkube/pkg/logs/events"
"github.com/kubeshop/testkube/pkg/utils"
)
@@ -32,9 +34,10 @@ func StreamToLogsChannel(resp io.Reader, logs chan output.Output) {
for {
b, err := utils.ReadLongLine(reader)
if err != nil {
- if err == io.EOF {
- err = nil
+ if err != io.EOF {
+ fmt.Printf("Read long line error: %+v' \n", err)
}
+
break
}
chunk := trimDataChunk(b)
@@ -56,6 +59,70 @@ func StreamToLogsChannel(resp io.Reader, logs chan output.Output) {
}
}
+// StreamToLogsChannelV2 converts io.Reader with SSE data like `data: {"type": "event", "message":"something"}`
+// to channel of output.Output objects, helps with logs version 2 streaming from SSE endpoint (passed from job executor)
+func StreamToLogsChannelV2(resp io.Reader, logs chan events.Log) {
+ reader := bufio.NewReader(resp)
+
+ for {
+ b, err := utils.ReadLongLine(reader)
+ if err != nil {
+ if err != io.EOF {
+ fmt.Printf("Read long line error: %+v' \n", err)
+ }
+
+ break
+ }
+ chunk := trimDataChunk(b)
+
+ // ignore lines which are not JSON objects
+ if len(chunk) < 2 || chunk[0] != '{' {
+ continue
+ }
+
+ // convert to events.Log object
+ out := events.Log{}
+ err = json.Unmarshal(chunk, &out)
+ if err != nil {
+ fmt.Printf("Unmarshal chunk error: %+v, json:'%s' \n", err, chunk)
+ continue
+ }
+
+ logs <- out
+ }
+}
+
+// StreamToTestWorkflowExecutionNotificationsChannel converts io.Reader with SSE data to channel of actual notifications
+func StreamToTestWorkflowExecutionNotificationsChannel(resp io.Reader, notifications chan testkube.TestWorkflowExecutionNotification) {
+ reader := bufio.NewReader(resp)
+
+ for {
+ b, err := utils.ReadLongLine(reader)
+ if err != nil {
+ if err != io.EOF {
+ fmt.Printf("Read long line error: %+v' \n", err)
+ }
+
+ break
+ }
+ chunk := trimDataChunk(b)
+
+ // ignore lines which are not JSON objects
+ if len(chunk) < 2 || chunk[0] != '{' {
+ continue
+ }
+
+ out := testkube.TestWorkflowExecutionNotification{}
+ err = json.Unmarshal(chunk, &out)
+ if err != nil {
+ fmt.Printf("Unmarshal chunk error: %+v, json:'%s' \n", err, chunk)
+ continue
+ }
+
+ notifications <- out
+ }
+}
+
// trimDataChunk remove data: and newlines from incoming SSE data line
func trimDataChunk(in []byte) []byte {
prefix := []byte("data: ")
diff --git a/pkg/api/v1/client/direct_client.go b/pkg/api/v1/client/direct_client.go
index 4f7c2254114..13842c57d6d 100644
--- a/pkg/api/v1/client/direct_client.go
+++ b/pkg/api/v1/client/direct_client.go
@@ -12,7 +12,9 @@ import (
"github.com/pkg/errors"
"golang.org/x/oauth2"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/executor/output"
+ "github.com/kubeshop/testkube/pkg/logs/events"
"github.com/kubeshop/testkube/pkg/oauth"
"github.com/kubeshop/testkube/pkg/problem"
)
@@ -182,6 +184,56 @@ func (t DirectClient[A]) GetLogs(uri string, logs chan output.Output) error {
return nil
}
+// GetLogsV2 returns logs stream version 2 from log server, based on job pods logs
+func (t DirectClient[A]) GetLogsV2(uri string, logs chan events.Log) error {
+ req, err := http.NewRequest(http.MethodGet, uri, nil)
+ if err != nil {
+ return err
+ }
+
+ req.Header.Set("Accept", "text/event-stream")
+ resp, err := t.sseClient.Do(req)
+ if err != nil {
+ return err
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return errors.New("error getting logs, invalid status code: " + resp.Status)
+ }
+
+ go func() {
+ defer close(logs)
+ defer resp.Body.Close()
+
+ StreamToLogsChannelV2(resp.Body, logs)
+ }()
+
+ return nil
+}
+
+// GetTestWorkflowExecutionNotifications returns logs stream from job pods, based on job pods logs
+func (t DirectClient[A]) GetTestWorkflowExecutionNotifications(uri string, notifications chan testkube.TestWorkflowExecutionNotification) error {
+ req, err := http.NewRequest(http.MethodGet, uri, nil)
+ if err != nil {
+ return err
+ }
+
+ req.Header.Set("Accept", "text/event-stream")
+ resp, err := t.sseClient.Do(req)
+ if err != nil {
+ return err
+ }
+
+ go func() {
+ defer close(notifications)
+ defer resp.Body.Close()
+
+ StreamToTestWorkflowExecutionNotificationsChannel(resp.Body, notifications)
+ }()
+
+ return nil
+}
+
// GetFile returns file artifact
func (t DirectClient[A]) GetFile(uri, fileName, destination string, params map[string][]string) (name string, err error) {
req, err := http.NewRequest(http.MethodGet, uri, nil)
diff --git a/pkg/api/v1/client/interface.go b/pkg/api/v1/client/interface.go
index 33c46305682..f5111ea7414 100644
--- a/pkg/api/v1/client/interface.go
+++ b/pkg/api/v1/client/interface.go
@@ -5,6 +5,7 @@ import (
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/executor/output"
+ "github.com/kubeshop/testkube/pkg/logs/events"
)
// Client is the Testkube API client abstraction
@@ -20,6 +21,9 @@ type Client interface {
TestSourceAPI
CopyFileAPI
TemplateAPI
+ TestWorkflowAPI
+ TestWorkflowExecutionAPI
+ TestWorkflowTemplateAPI
}
// TestAPI describes test api methods
@@ -35,6 +39,7 @@ type TestAPI interface {
ExecuteTest(id, executionName string, options ExecuteTestOptions) (executions testkube.Execution, err error)
ExecuteTests(selector string, concurrencyLevel int, options ExecuteTestOptions) (executions []testkube.Execution, err error)
Logs(id string) (logs chan output.Output, err error)
+ LogsV2(id string) (logs chan events.Log, err error)
}
// ExecutionAPI describes execution api methods
@@ -124,6 +129,41 @@ type TestSourceAPI interface {
DeleteTestSources(selector string) (err error)
}
+// TestWorkflowAPI describes test workflow api methods
+type TestWorkflowAPI interface {
+ GetTestWorkflow(id string) (testkube.TestWorkflow, error)
+ GetTestWorkflowWithExecution(id string) (testkube.TestWorkflowWithExecution, error)
+ ListTestWorkflows(selector string) (testkube.TestWorkflows, error)
+ ListTestWorkflowWithExecutions(selector string) (testkube.TestWorkflowWithExecutions, error)
+ DeleteTestWorkflows(selector string) error
+ CreateTestWorkflow(workflow testkube.TestWorkflow) (testkube.TestWorkflow, error)
+ UpdateTestWorkflow(workflow testkube.TestWorkflow) (testkube.TestWorkflow, error)
+ DeleteTestWorkflow(name string) error
+ ExecuteTestWorkflow(name string, request testkube.TestWorkflowExecutionRequest) (testkube.TestWorkflowExecution, error)
+ GetTestWorkflowExecutionNotifications(id string) (chan testkube.TestWorkflowExecutionNotification, error)
+}
+
+// TestWorkflowExecutionAPI describes test workflow api methods
+type TestWorkflowExecutionAPI interface {
+ GetTestWorkflowExecution(executionID string) (execution testkube.TestWorkflowExecution, err error)
+ ListTestWorkflowExecutions(id string, limit int, selector string) (executions testkube.TestWorkflowExecutionsResult, err error)
+ AbortTestWorkflowExecution(workflow string, id string) error
+ AbortTestWorkflowExecutions(workflow string) error
+ GetTestWorkflowExecutionArtifacts(executionID string) (artifacts testkube.Artifacts, err error)
+ DownloadTestWorkflowArtifact(executionID, fileName, destination string) (artifact string, err error)
+ DownloadTestWorkflowArtifactArchive(executionID, destination string, masks []string) (archive string, err error)
+}
+
+// TestWorkflowTemplateAPI describes test workflow api methods
+type TestWorkflowTemplateAPI interface {
+ GetTestWorkflowTemplate(id string) (testkube.TestWorkflowTemplate, error)
+ ListTestWorkflowTemplates(selector string) (testkube.TestWorkflowTemplates, error)
+ DeleteTestWorkflowTemplates(selector string) error
+ CreateTestWorkflowTemplate(workflow testkube.TestWorkflowTemplate) (testkube.TestWorkflowTemplate, error)
+ UpdateTestWorkflowTemplate(workflow testkube.TestWorkflowTemplate) (testkube.TestWorkflowTemplate, error)
+ DeleteTestWorkflowTemplate(name string) error
+}
+
// CopyFileAPI describes methods to handle files in the object storage
type CopyFileAPI interface {
UploadFile(parentName string, parentType TestingType, filePath string, fileContent []byte, timeout time.Duration) error
@@ -195,6 +235,7 @@ type ExecuteTestOptions struct {
PreRunScriptContent string
PostRunScriptContent string
ExecutePostRunScriptBeforeScraping bool
+ SourceScripts bool
ScraperTemplate string
ScraperTemplateReference string
PvcTemplate string
@@ -205,6 +246,7 @@ type ExecuteTestOptions struct {
EnvSecrets []testkube.EnvReference
RunningContext *testkube.RunningContext
SlavePodRequest *testkube.PodRequest
+ ExecutionNamespace string
}
// ExecuteTestSuiteOptions contains test suite run options
@@ -229,13 +271,14 @@ type Gettable interface {
testkube.Test | testkube.TestSuite | testkube.ExecutorDetails |
testkube.Webhook | testkube.TestWithExecution | testkube.TestSuiteWithExecution | testkube.TestWithExecutionSummary |
testkube.TestSuiteWithExecutionSummary | testkube.Artifact | testkube.ServerInfo | testkube.Config | testkube.DebugInfo |
- testkube.TestSource | testkube.Template
+ testkube.TestSource | testkube.Template |
+ testkube.TestWorkflow | testkube.TestWorkflowWithExecution | testkube.TestWorkflowTemplate | testkube.TestWorkflowExecution
}
// Executable is an interface of executable objects
type Executable interface {
- testkube.Execution | testkube.TestSuiteExecution |
- testkube.ExecutionsResult | testkube.TestSuiteExecutionsResult
+ testkube.Execution | testkube.TestSuiteExecution | testkube.TestWorkflowExecution |
+ testkube.ExecutionsResult | testkube.TestSuiteExecutionsResult | testkube.TestWorkflowExecutionsResult
}
// All is an interface of all objects
@@ -251,5 +294,7 @@ type Transport[A All] interface {
ExecuteMethod(method, uri, selector string, isContentExpected bool) error
GetURI(pathTemplate string, params ...interface{}) string
GetLogs(uri string, logs chan output.Output) error
+ GetLogsV2(uri string, logs chan events.Log) error
+ GetTestWorkflowExecutionNotifications(uri string, notifications chan testkube.TestWorkflowExecutionNotification) error
GetFile(uri, fileName, destination string, params map[string][]string) (name string, err error)
}
diff --git a/pkg/api/v1/client/proxy_client.go b/pkg/api/v1/client/proxy_client.go
index 11f1439004e..63551e491d4 100644
--- a/pkg/api/v1/client/proxy_client.go
+++ b/pkg/api/v1/client/proxy_client.go
@@ -14,7 +14,9 @@ import (
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/executor/output"
+ "github.com/kubeshop/testkube/pkg/logs/events"
"github.com/kubeshop/testkube/pkg/problem"
)
@@ -149,6 +151,46 @@ func (t ProxyClient[A]) GetLogs(uri string, logs chan output.Output) error {
return nil
}
+// GetLogsV2 returns logs version 2 stream from log server, based on job pods logs
+func (t ProxyClient[A]) GetLogsV2(uri string, logs chan events.Log) error {
+ resp, err := t.getProxy(http.MethodGet).
+ Suffix(uri).
+ SetHeader("Accept", "text/event-stream").
+ Stream(context.Background())
+ if err != nil {
+ return err
+ }
+
+ go func() {
+ defer close(logs)
+ defer resp.Close()
+
+ StreamToLogsChannelV2(resp, logs)
+ }()
+
+ return nil
+}
+
+// GetTestWorkflowExecutionNotifications returns logs stream from job pods, based on job pods logs
+func (t ProxyClient[A]) GetTestWorkflowExecutionNotifications(uri string, notifications chan testkube.TestWorkflowExecutionNotification) error {
+ resp, err := t.getProxy(http.MethodGet).
+ Suffix(uri).
+ SetHeader("Accept", "text/event-stream").
+ Stream(context.Background())
+ if err != nil {
+ return err
+ }
+
+ go func() {
+ defer close(notifications)
+ defer resp.Close()
+
+ StreamToTestWorkflowExecutionNotificationsChannel(resp, notifications)
+ }()
+
+ return nil
+}
+
// GetFile returns file artifact
func (t ProxyClient[A]) GetFile(uri, fileName, destination string, params map[string][]string) (name string, err error) {
req := t.getProxy(http.MethodGet).
diff --git a/pkg/api/v1/client/test.go b/pkg/api/v1/client/test.go
index b689d0708df..d4f9ce565b0 100644
--- a/pkg/api/v1/client/test.go
+++ b/pkg/api/v1/client/test.go
@@ -9,6 +9,7 @@ import (
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/executor/output"
+ "github.com/kubeshop/testkube/pkg/logs/events"
)
// NewTestClient creates new Test client
@@ -158,6 +159,7 @@ func (c TestClient) ExecuteTest(id, executionName string, options ExecuteTestOpt
PreRunScript: options.PreRunScriptContent,
PostRunScript: options.PostRunScriptContent,
ExecutePostRunScriptBeforeScraping: options.ExecutePostRunScriptBeforeScraping,
+ SourceScripts: options.SourceScripts,
ScraperTemplate: options.ScraperTemplate,
ScraperTemplateReference: options.ScraperTemplateReference,
PvcTemplate: options.PvcTemplate,
@@ -168,6 +170,7 @@ func (c TestClient) ExecuteTest(id, executionName string, options ExecuteTestOpt
EnvSecrets: options.EnvSecrets,
RunningContext: options.RunningContext,
SlavePodRequest: options.SlavePodRequest,
+ ExecutionNamespace: options.ExecutionNamespace,
}
body, err := json.Marshal(request)
@@ -202,6 +205,7 @@ func (c TestClient) ExecuteTests(selector string, concurrencyLevel int, options
PreRunScript: options.PreRunScriptContent,
PostRunScript: options.PostRunScriptContent,
ExecutePostRunScriptBeforeScraping: options.ExecutePostRunScriptBeforeScraping,
+ SourceScripts: options.SourceScripts,
ScraperTemplate: options.ScraperTemplate,
ScraperTemplateReference: options.ScraperTemplateReference,
PvcTemplate: options.PvcTemplate,
@@ -210,6 +214,7 @@ func (c TestClient) ExecuteTests(selector string, concurrencyLevel int, options
IsNegativeTestChangedOnRun: options.IsNegativeTestChangedOnRun,
RunningContext: options.RunningContext,
SlavePodRequest: options.SlavePodRequest,
+ ExecutionNamespace: options.ExecutionNamespace,
}
body, err := json.Marshal(request)
@@ -260,6 +265,14 @@ func (c TestClient) Logs(id string) (logs chan output.Output, err error) {
return logs, err
}
+// LogsV2 returns logs version 2 stream from log sever, based on job pods logs
+func (c TestClient) LogsV2(id string) (logs chan events.Log, err error) {
+ logs = make(chan events.Log)
+ uri := c.testTransport.GetURI("/executions/%s/logs/v2", id)
+ err = c.testTransport.GetLogsV2(uri, logs)
+ return logs, err
+}
+
// GetExecutionArtifacts returns execution artifacts
func (c TestClient) GetExecutionArtifacts(executionID string) (artifacts testkube.Artifacts, err error) {
uri := c.artifactTransport.GetURI("/executions/%s/artifacts", executionID)
diff --git a/pkg/api/v1/client/testworkflow.go b/pkg/api/v1/client/testworkflow.go
new file mode 100644
index 00000000000..dc89783b738
--- /dev/null
+++ b/pkg/api/v1/client/testworkflow.go
@@ -0,0 +1,179 @@
+package client
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+)
+
+// NewTestWorkflowClient creates new TestWorkflow client
+func NewTestWorkflowClient(
+ testWorkflowTransport Transport[testkube.TestWorkflow],
+ testWorkflowWithExecutionTransport Transport[testkube.TestWorkflowWithExecution],
+ testWorkflowExecutionTransport Transport[testkube.TestWorkflowExecution],
+ testWorkflowExecutionsResultTransport Transport[testkube.TestWorkflowExecutionsResult],
+ artifactTransport Transport[testkube.Artifact],
+) TestWorkflowClient {
+ return TestWorkflowClient{
+ testWorkflowTransport: testWorkflowTransport,
+ testWorkflowWithExecutionTransport: testWorkflowWithExecutionTransport,
+ testWorkflowExecutionTransport: testWorkflowExecutionTransport,
+ testWorkflowExecutionsResultTransport: testWorkflowExecutionsResultTransport,
+ artifactTransport: artifactTransport,
+ }
+}
+
+// TestWorkflowClient is a client for test workflows
+type TestWorkflowClient struct {
+ testWorkflowTransport Transport[testkube.TestWorkflow]
+ testWorkflowWithExecutionTransport Transport[testkube.TestWorkflowWithExecution]
+ testWorkflowExecutionTransport Transport[testkube.TestWorkflowExecution]
+ testWorkflowExecutionsResultTransport Transport[testkube.TestWorkflowExecutionsResult]
+ artifactTransport Transport[testkube.Artifact]
+}
+
+// GetTestWorkflow returns single test workflow by id
+func (c TestWorkflowClient) GetTestWorkflow(id string) (testkube.TestWorkflow, error) {
+ uri := c.testWorkflowTransport.GetURI("/test-workflows/%s", id)
+ return c.testWorkflowTransport.Execute(http.MethodGet, uri, nil, nil)
+}
+
+// GetTestWorkflowWithExecution returns single test workflow with execution by id
+func (c TestWorkflowClient) GetTestWorkflowWithExecution(id string) (testkube.TestWorkflowWithExecution, error) {
+ uri := c.testWorkflowWithExecutionTransport.GetURI("/test-workflow-with-executions/%s", id)
+ return c.testWorkflowWithExecutionTransport.Execute(http.MethodGet, uri, nil, nil)
+}
+
+// ListTestWorkflows list all test workflows
+func (c TestWorkflowClient) ListTestWorkflows(selector string) (testkube.TestWorkflows, error) {
+ uri := c.testWorkflowTransport.GetURI("/test-workflows")
+ params := map[string]string{"selector": selector}
+ return c.testWorkflowTransport.ExecuteMultiple(http.MethodGet, uri, nil, params)
+}
+
+// ListTestWorkflowWithExecutions list all test workflows with their latest executions
+func (c TestWorkflowClient) ListTestWorkflowWithExecutions(selector string) (testkube.TestWorkflowWithExecutions, error) {
+ uri := c.testWorkflowWithExecutionTransport.GetURI("/test-workflow-with-executions")
+ params := map[string]string{"selector": selector}
+ return c.testWorkflowWithExecutionTransport.ExecuteMultiple(http.MethodGet, uri, nil, params)
+}
+
+// DeleteTestWorkflows deletes multiple test workflows by labels
+func (c TestWorkflowClient) DeleteTestWorkflows(selector string) error {
+ uri := c.testWorkflowTransport.GetURI("/test-workflows")
+ return c.testWorkflowTransport.Delete(uri, selector, true)
+}
+
+// CreateTestWorkflow creates new TestWorkflow Custom Resource
+func (c TestWorkflowClient) CreateTestWorkflow(workflow testkube.TestWorkflow) (result testkube.TestWorkflow, err error) {
+ uri := c.testWorkflowTransport.GetURI("/test-workflows")
+
+ body, err := json.Marshal(workflow)
+ if err != nil {
+ return result, err
+ }
+
+ return c.testWorkflowTransport.Execute(http.MethodPost, uri, body, nil)
+}
+
+// UpdateTestWorkflow updates TestWorkflow Custom Resource
+func (c TestWorkflowClient) UpdateTestWorkflow(workflow testkube.TestWorkflow) (result testkube.TestWorkflow, err error) {
+ if workflow.Name == "" {
+ return result, fmt.Errorf("test workflow name '%s' is not valid", workflow.Name)
+ }
+
+ uri := c.testWorkflowTransport.GetURI("/test-workflows/%s", workflow.Name)
+
+ body, err := json.Marshal(workflow)
+ if err != nil {
+ return result, err
+ }
+
+ return c.testWorkflowTransport.Execute(http.MethodPut, uri, body, nil)
+}
+
+// DeleteTestWorkflow deletes single test by name
+func (c TestWorkflowClient) DeleteTestWorkflow(name string) error {
+ if name == "" {
+ return fmt.Errorf("test workflow name '%s' is not valid", name)
+ }
+
+ uri := c.testWorkflowTransport.GetURI("/test-workflows/%s", name)
+ return c.testWorkflowTransport.Delete(uri, "", true)
+}
+
+// ExecuteTestWorkflow starts new TestWorkflow execution
+func (c TestWorkflowClient) ExecuteTestWorkflow(name string, request testkube.TestWorkflowExecutionRequest) (result testkube.TestWorkflowExecution, err error) {
+ if name == "" {
+ return result, fmt.Errorf("test workflow name '%s' is not valid", name)
+ }
+
+ uri := c.testWorkflowExecutionTransport.GetURI("/test-workflows/%s/executions", name)
+
+ body, err := json.Marshal(request)
+ if err != nil {
+ return result, err
+ }
+
+ return c.testWorkflowExecutionTransport.Execute(http.MethodPost, uri, body, nil)
+}
+
+// GetTestWorkflowExecutionNotifications returns events stream from job pods, based on job pods logs
+func (c TestWorkflowClient) GetTestWorkflowExecutionNotifications(id string) (notifications chan testkube.TestWorkflowExecutionNotification, err error) {
+ notifications = make(chan testkube.TestWorkflowExecutionNotification)
+ uri := c.testWorkflowTransport.GetURI("/test-workflow-executions/%s/notifications", id)
+ err = c.testWorkflowTransport.GetTestWorkflowExecutionNotifications(uri, notifications)
+ return notifications, err
+}
+
+// GetTestWorkflowExecution returns single test workflow execution by id
+func (c TestWorkflowClient) GetTestWorkflowExecution(id string) (testkube.TestWorkflowExecution, error) {
+ uri := c.testWorkflowExecutionTransport.GetURI("/test-workflow-executions/%s", id)
+ return c.testWorkflowExecutionTransport.Execute(http.MethodGet, uri, nil, nil)
+}
+
+// ListTestWorkflowExecutions list test workflow executions for selected workflow
+func (c TestWorkflowClient) ListTestWorkflowExecutions(id string, limit int, selector string) (testkube.TestWorkflowExecutionsResult, error) {
+ uri := c.testWorkflowExecutionsResultTransport.GetURI("/test-workflow-executions/")
+ if id != "" {
+ uri = c.testWorkflowExecutionsResultTransport.GetURI(fmt.Sprintf("/test-workflows/%s/executions", id))
+ }
+ params := map[string]string{
+ "selector": selector,
+ "pageSize": fmt.Sprintf("%d", limit),
+ }
+ return c.testWorkflowExecutionsResultTransport.Execute(http.MethodGet, uri, nil, params)
+}
+
+// AbortTestWorkflowExecution aborts selected execution
+func (c TestWorkflowClient) AbortTestWorkflowExecution(workflow, id string) error {
+ uri := c.testWorkflowTransport.GetURI("/test-workflows/%s/executions/%s/abort", workflow, id)
+ return c.testWorkflowTransport.ExecuteMethod(http.MethodPost, uri, "", false)
+}
+
+// AbortTestWorkflowExecutions aborts all workflow executions
+func (c TestWorkflowClient) AbortTestWorkflowExecutions(workflow string) error {
+ uri := c.testWorkflowTransport.GetURI("/test-workflows/%s/abort", workflow)
+ return c.testWorkflowTransport.ExecuteMethod(http.MethodPost, uri, "", false)
+}
+
+// GetTestWorkflowExecutionArtifacts returns execution artifacts
+func (c TestWorkflowClient) GetTestWorkflowExecutionArtifacts(executionID string) (artifacts testkube.Artifacts, err error) {
+ uri := c.artifactTransport.GetURI("/test-workflow-executions/%s/artifacts", executionID)
+ return c.artifactTransport.ExecuteMultiple(http.MethodGet, uri, nil, nil)
+}
+
+// DownloadTestWorkflowArtifact downloads file
+func (c TestWorkflowClient) DownloadTestWorkflowArtifact(executionID, fileName, destination string) (artifact string, err error) {
+ uri := c.testWorkflowExecutionTransport.GetURI("/test-workflow-executions/%s/artifacts/%s", executionID, url.QueryEscape(fileName))
+ return c.testWorkflowExecutionTransport.GetFile(uri, fileName, destination, nil)
+}
+
+// DownloadTestWorkflowArtifactArchive downloads archive
+func (c TestWorkflowClient) DownloadTestWorkflowArtifactArchive(executionID, destination string, masks []string) (archive string, err error) {
+ uri := c.testWorkflowExecutionTransport.GetURI("/test-workflow-executions/%s/artifact-archive", executionID)
+ return c.testWorkflowExecutionTransport.GetFile(uri, fmt.Sprintf("%s.tar.gz", executionID), destination, map[string][]string{"mask": masks})
+}
diff --git a/pkg/api/v1/client/testworkflowtemplate.go b/pkg/api/v1/client/testworkflowtemplate.go
new file mode 100644
index 00000000000..3928666c2ed
--- /dev/null
+++ b/pkg/api/v1/client/testworkflowtemplate.go
@@ -0,0 +1,84 @@
+package client
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+)
+
+// NewTestWorkflowTemplateClient creates new TestWorkflowTemplate client
+func NewTestWorkflowTemplateClient(
+ testWorkflowTemplateTransport Transport[testkube.TestWorkflowTemplate],
+) TestWorkflowTemplateClient {
+ return TestWorkflowTemplateClient{
+ testWorkflowTemplateTransport: testWorkflowTemplateTransport,
+ }
+}
+
+// TestWorkflowTemplateClient is a client for tests
+type TestWorkflowTemplateClient struct {
+ testWorkflowTemplateTransport Transport[testkube.TestWorkflowTemplate]
+}
+
+// GetTestWorkflowTemplate returns single test by id
+func (c TestWorkflowTemplateClient) GetTestWorkflowTemplate(id string) (testkube.TestWorkflowTemplate, error) {
+ id = strings.ReplaceAll(id, "/", "--")
+ uri := c.testWorkflowTemplateTransport.GetURI("/test-workflow-templates/%s", id)
+ return c.testWorkflowTemplateTransport.Execute(http.MethodGet, uri, nil, nil)
+}
+
+// ListTestWorkflowTemplates list all tests
+func (c TestWorkflowTemplateClient) ListTestWorkflowTemplates(selector string) (testkube.TestWorkflowTemplates, error) {
+ params := map[string]string{"selector": selector}
+ uri := c.testWorkflowTemplateTransport.GetURI("/test-workflow-templates")
+ return c.testWorkflowTemplateTransport.ExecuteMultiple(http.MethodGet, uri, nil, params)
+}
+
+// DeleteTestWorkflowTemplates deletes multiple test workflow templates by labels
+func (c TestWorkflowTemplateClient) DeleteTestWorkflowTemplates(selector string) error {
+ uri := c.testWorkflowTemplateTransport.GetURI("/test-workflow-templates")
+ return c.testWorkflowTemplateTransport.Delete(uri, selector, true)
+}
+
+// CreateTestWorkflowTemplate creates new TestWorkflowTemplate Custom Resource
+func (c TestWorkflowTemplateClient) CreateTestWorkflowTemplate(template testkube.TestWorkflowTemplate) (result testkube.TestWorkflowTemplate, err error) {
+ template.Name = strings.ReplaceAll(template.Name, "/", "--")
+
+ body, err := json.Marshal(template)
+ if err != nil {
+ return result, err
+ }
+
+ uri := c.testWorkflowTemplateTransport.GetURI("/test-workflow-templates")
+ return c.testWorkflowTemplateTransport.Execute(http.MethodPost, uri, body, nil)
+}
+
+// UpdateTestWorkflowTemplate updates TestWorkflowTemplate Custom Resource
+func (c TestWorkflowTemplateClient) UpdateTestWorkflowTemplate(template testkube.TestWorkflowTemplate) (result testkube.TestWorkflowTemplate, err error) {
+ if template.Name == "" {
+ return result, fmt.Errorf("test workflow template name '%s' is not valid", template.Name)
+ }
+ template.Name = strings.ReplaceAll(template.Name, "/", "--")
+
+ body, err := json.Marshal(template)
+ if err != nil {
+ return result, err
+ }
+
+ uri := c.testWorkflowTemplateTransport.GetURI("/test-workflow-templates/%s", template.Name)
+ return c.testWorkflowTemplateTransport.Execute(http.MethodPut, uri, body, nil)
+}
+
+// DeleteTestWorkflowTemplate deletes single test by name
+func (c TestWorkflowTemplateClient) DeleteTestWorkflowTemplate(name string) error {
+ if name == "" {
+ return fmt.Errorf("test workflow template name '%s' is not valid", name)
+ }
+ name = strings.ReplaceAll(name, "/", "--")
+
+ uri := c.testWorkflowTemplateTransport.GetURI("/test-workflow-templates/%s", name)
+ return c.testWorkflowTemplateTransport.Delete(uri, "", true)
+}
diff --git a/pkg/api/v1/client/uploads.go b/pkg/api/v1/client/uploads.go
index dabd6396924..b54893620cc 100644
--- a/pkg/api/v1/client/uploads.go
+++ b/pkg/api/v1/client/uploads.go
@@ -9,6 +9,7 @@ import (
"mime/multipart"
"net/http"
"path/filepath"
+ "strings"
"time"
"k8s.io/client-go/kubernetes"
@@ -81,7 +82,7 @@ func (c CopyFileDirectClient) UploadFile(parentName string, parentType TestingTy
}
func (c CopyFileDirectClient) getUri() string {
- return c.apiPathPrefix + uri
+ return strings.Join([]string{c.apiPathPrefix, c.apiURI, "/", Version, uri}, "")
}
// UploadFile uploads a copy file to the API server
diff --git a/pkg/api/v1/testkube/model_artifact.go b/pkg/api/v1/testkube/model_artifact.go
index 41111f66d2f..062b341e8a3 100644
--- a/pkg/api/v1/testkube/model_artifact.go
+++ b/pkg/api/v1/testkube/model_artifact.go
@@ -17,4 +17,5 @@ type Artifact struct {
Size int32 `json:"size,omitempty"`
// execution name that produced the artifact
ExecutionName string `json:"executionName,omitempty"`
+ Status string `json:"status,omitempty"`
}
diff --git a/pkg/api/v1/testkube/model_aws_elastic_block_store_volume_source.go b/pkg/api/v1/testkube/model_aws_elastic_block_store_volume_source.go
new file mode 100644
index 00000000000..f0599c5197b
--- /dev/null
+++ b/pkg/api/v1/testkube/model_aws_elastic_block_store_volume_source.go
@@ -0,0 +1,22 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// awsElasticBlockStore represents an AWS Disk resource that is attached to a kubelet's host machine and then exposed to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore
+type AwsElasticBlockStoreVolumeSource struct {
+ // fsType is the filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore TODO: how do we prevent errors in the filesystem from compromising the machine
+ FsType string `json:"fsType,omitempty"`
+ // partition is the partition in the volume that you want to mount. If omitted, the default is to mount by volume name. Examples: For volume /dev/sda1, you specify the partition as \"1\". Similarly, the volume partition for /dev/sda is \"0\" (or you can leave the property empty).
+ Partition int32 `json:"partition,omitempty"`
+ // readOnly value true will force the readOnly setting in VolumeMounts. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore
+ ReadOnly bool `json:"readOnly,omitempty"`
+ // volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore
+ VolumeID string `json:"volumeID"`
+}
diff --git a/pkg/api/v1/testkube/model_azure_disk_volume_source.go b/pkg/api/v1/testkube/model_azure_disk_volume_source.go
new file mode 100644
index 00000000000..2501927724c
--- /dev/null
+++ b/pkg/api/v1/testkube/model_azure_disk_volume_source.go
@@ -0,0 +1,23 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// azureDisk represents an Azure Data Disk mount on the host and bind mount to the pod.
+type AzureDiskVolumeSource struct {
+ CachingMode *BoxedString `json:"cachingMode,omitempty"`
+ // diskName is the Name of the data disk in the blob storage
+ DiskName string `json:"diskName"`
+ // diskURI is the URI of data disk in the blob storage
+ DiskURI string `json:"diskURI"`
+ FsType *BoxedString `json:"fsType,omitempty"`
+ Kind *BoxedString `json:"kind,omitempty"`
+ // readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.
+ ReadOnly bool `json:"readOnly,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_azure_file_volume_source.go b/pkg/api/v1/testkube/model_azure_file_volume_source.go
new file mode 100644
index 00000000000..4b712c8f3e2
--- /dev/null
+++ b/pkg/api/v1/testkube/model_azure_file_volume_source.go
@@ -0,0 +1,20 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// azureFile represents an Azure File Service mount on the host and bind mount to the pod.
+type AzureFileVolumeSource struct {
+ // readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.
+ ReadOnly bool `json:"readOnly,omitempty"`
+ // secretName is the name of secret that contains Azure Storage Account Name and Key
+ SecretName string `json:"secretName"`
+ // shareName is the azure share Name
+ ShareName string `json:"shareName"`
+}
diff --git a/pkg/api/v1/testkube/model_boxed_boolean.go b/pkg/api/v1/testkube/model_boxed_boolean.go
new file mode 100644
index 00000000000..b13d178fdaa
--- /dev/null
+++ b/pkg/api/v1/testkube/model_boxed_boolean.go
@@ -0,0 +1,14 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type BoxedBoolean struct {
+ Value bool `json:"value"`
+}
diff --git a/pkg/api/v1/testkube/model_boxed_integer.go b/pkg/api/v1/testkube/model_boxed_integer.go
new file mode 100644
index 00000000000..85d66f73c8a
--- /dev/null
+++ b/pkg/api/v1/testkube/model_boxed_integer.go
@@ -0,0 +1,14 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type BoxedInteger struct {
+ Value int32 `json:"value"`
+}
diff --git a/pkg/api/v1/testkube/model_boxed_string.go b/pkg/api/v1/testkube/model_boxed_string.go
new file mode 100644
index 00000000000..a1aa9a4ad93
--- /dev/null
+++ b/pkg/api/v1/testkube/model_boxed_string.go
@@ -0,0 +1,14 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type BoxedString struct {
+ Value string `json:"value"`
+}
diff --git a/pkg/api/v1/testkube/model_boxed_string_list.go b/pkg/api/v1/testkube/model_boxed_string_list.go
new file mode 100644
index 00000000000..087040470c6
--- /dev/null
+++ b/pkg/api/v1/testkube/model_boxed_string_list.go
@@ -0,0 +1,14 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type BoxedStringList struct {
+ Value []string `json:"value"`
+}
diff --git a/pkg/api/v1/testkube/model_ceph_fs_volume_source.go b/pkg/api/v1/testkube/model_ceph_fs_volume_source.go
new file mode 100644
index 00000000000..e5de0e33de1
--- /dev/null
+++ b/pkg/api/v1/testkube/model_ceph_fs_volume_source.go
@@ -0,0 +1,25 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// cephFS represents a Ceph FS mount on the host that shares a pod's lifetime
+type CephFsVolumeSource struct {
+ // monitors is Required: Monitors is a collection of Ceph monitors More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it
+ Monitors []string `json:"monitors"`
+ // path is Optional: Used as the mounted root, rather than the full Ceph tree, default is /
+ Path string `json:"path,omitempty"`
+ // readOnly is Optional: Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it
+ ReadOnly bool `json:"readOnly,omitempty"`
+ // secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it
+ SecretFile string `json:"secretFile,omitempty"`
+ SecretRef *LocalObjectReference `json:"secretRef,omitempty"`
+ // user is optional: User is the rados user name, default is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it
+ User string `json:"user,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_config_map_env_source.go b/pkg/api/v1/testkube/model_config_map_env_source.go
new file mode 100644
index 00000000000..2a9ae4c2b67
--- /dev/null
+++ b/pkg/api/v1/testkube/model_config_map_env_source.go
@@ -0,0 +1,15 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type ConfigMapEnvSource struct {
+ Name string `json:"name"`
+ Optional bool `json:"optional,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_config_map_volume_source.go b/pkg/api/v1/testkube/model_config_map_volume_source.go
new file mode 100644
index 00000000000..a9934bcf8e9
--- /dev/null
+++ b/pkg/api/v1/testkube/model_config_map_volume_source.go
@@ -0,0 +1,21 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// configMap represents a configMap that should populate this volume
+type ConfigMapVolumeSource struct {
+ DefaultMode *BoxedInteger `json:"defaultMode,omitempty"`
+ // items if unspecified, each key-value pair in the Data field of the referenced ConfigMap will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the ConfigMap, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.
+ Items []SecretVolumeSourceItems `json:"items,omitempty"`
+ // Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?
+ Name string `json:"name,omitempty"`
+ // optional specify whether the ConfigMap or its keys must be defined
+ Optional bool `json:"optional,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_config_map_volume_source_items.go b/pkg/api/v1/testkube/model_config_map_volume_source_items.go
new file mode 100644
index 00000000000..383c2748fa0
--- /dev/null
+++ b/pkg/api/v1/testkube/model_config_map_volume_source_items.go
@@ -0,0 +1,19 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// Maps a string key to a path within a volume.
+type ConfigMapVolumeSourceItems struct {
+ // key is the key to project.
+ Key string `json:"key"`
+ Mode *BoxedInteger `json:"mode,omitempty"`
+ // path is the relative path of the file to map the key to. May not be an absolute path. May not contain the path element '..'. May not start with the string '..'.
+ Path string `json:"path"`
+}
diff --git a/pkg/api/v1/testkube/model_content_git_auth_type.go b/pkg/api/v1/testkube/model_content_git_auth_type.go
new file mode 100644
index 00000000000..039ff182d94
--- /dev/null
+++ b/pkg/api/v1/testkube/model_content_git_auth_type.go
@@ -0,0 +1,19 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// ContentGitAuthType : auth type for git requests
+type ContentGitAuthType string
+
+// List of ContentGitAuthType
+const (
+ BASIC_ContentGitAuthType ContentGitAuthType = "basic"
+ HEADER_ContentGitAuthType ContentGitAuthType = "header"
+)
diff --git a/pkg/api/v1/testkube/model_empty_dir_volume_source.go b/pkg/api/v1/testkube/model_empty_dir_volume_source.go
new file mode 100644
index 00000000000..dd549c57020
--- /dev/null
+++ b/pkg/api/v1/testkube/model_empty_dir_volume_source.go
@@ -0,0 +1,17 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// emptyDir represents a temporary directory that shares a pod's lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir
+type EmptyDirVolumeSource struct {
+ // medium represents what type of storage medium should back this directory. The default is \"\" which means to use the node's default medium. Must be an empty string (default) or Memory. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir
+ Medium string `json:"medium,omitempty"`
+ SizeLimit *BoxedString `json:"sizeLimit,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_env_from_source.go b/pkg/api/v1/testkube/model_env_from_source.go
new file mode 100644
index 00000000000..d63a456a9b0
--- /dev/null
+++ b/pkg/api/v1/testkube/model_env_from_source.go
@@ -0,0 +1,16 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type EnvFromSource struct {
+ Prefix string `json:"prefix,omitempty"`
+ ConfigMapRef *ConfigMapEnvSource `json:"configMapRef,omitempty"`
+ SecretRef *SecretEnvSource `json:"secretRef,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_env_var.go b/pkg/api/v1/testkube/model_env_var.go
new file mode 100644
index 00000000000..ea07fac4d77
--- /dev/null
+++ b/pkg/api/v1/testkube/model_env_var.go
@@ -0,0 +1,16 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type EnvVar struct {
+ Name string `json:"name,omitempty"`
+ Value string `json:"value,omitempty"`
+ ValueFrom *EnvVarSource `json:"valueFrom,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_env_var_source.go b/pkg/api/v1/testkube/model_env_var_source.go
new file mode 100644
index 00000000000..bce205fa969
--- /dev/null
+++ b/pkg/api/v1/testkube/model_env_var_source.go
@@ -0,0 +1,18 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// EnvVarSource represents a source for the value of an EnvVar.
+type EnvVarSource struct {
+ ConfigMapKeyRef *EnvVarSourceConfigMapKeyRef `json:"configMapKeyRef,omitempty"`
+ FieldRef *EnvVarSourceFieldRef `json:"fieldRef,omitempty"`
+ ResourceFieldRef *EnvVarSourceResourceFieldRef `json:"resourceFieldRef,omitempty"`
+ SecretKeyRef *EnvVarSourceSecretKeyRef `json:"secretKeyRef,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_env_var_source_config_map_key_ref.go b/pkg/api/v1/testkube/model_env_var_source_config_map_key_ref.go
new file mode 100644
index 00000000000..66855009267
--- /dev/null
+++ b/pkg/api/v1/testkube/model_env_var_source_config_map_key_ref.go
@@ -0,0 +1,20 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// Selects a key of a ConfigMap.
+type EnvVarSourceConfigMapKeyRef struct {
+ // The key to select.
+ Key string `json:"key"`
+ // Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?
+ Name string `json:"name,omitempty"`
+ // Specify whether the ConfigMap or its key must be defined
+ Optional bool `json:"optional,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_env_var_source_field_ref.go b/pkg/api/v1/testkube/model_env_var_source_field_ref.go
new file mode 100644
index 00000000000..4a69682e3ba
--- /dev/null
+++ b/pkg/api/v1/testkube/model_env_var_source_field_ref.go
@@ -0,0 +1,18 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.
+type EnvVarSourceFieldRef struct {
+ // Version of the schema the FieldPath is written in terms of, defaults to \"v1\".
+ ApiVersion string `json:"apiVersion,omitempty"`
+ // Path of the field to select in the specified API version.
+ FieldPath string `json:"fieldPath"`
+}
diff --git a/pkg/api/v1/testkube/model_env_var_source_resource_field_ref.go b/pkg/api/v1/testkube/model_env_var_source_resource_field_ref.go
new file mode 100644
index 00000000000..01f0fb66fdd
--- /dev/null
+++ b/pkg/api/v1/testkube/model_env_var_source_resource_field_ref.go
@@ -0,0 +1,19 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.
+type EnvVarSourceResourceFieldRef struct {
+ // Container name: required for volumes, optional for env vars
+ ContainerName string `json:"containerName,omitempty"`
+ Divisor string `json:"divisor,omitempty"`
+ // Required: resource to select
+ Resource string `json:"resource"`
+}
diff --git a/pkg/api/v1/testkube/model_env_var_source_secret_key_ref.go b/pkg/api/v1/testkube/model_env_var_source_secret_key_ref.go
new file mode 100644
index 00000000000..551fdd65de7
--- /dev/null
+++ b/pkg/api/v1/testkube/model_env_var_source_secret_key_ref.go
@@ -0,0 +1,20 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// Selects a key of a secret in the pod's namespace
+type EnvVarSourceSecretKeyRef struct {
+ // The key of the secret to select from. Must be a valid secret key.
+ Key string `json:"key"`
+ // Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?
+ Name string `json:"name,omitempty"`
+ // Specify whether the Secret or its key must be defined
+ Optional bool `json:"optional,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_event.go b/pkg/api/v1/testkube/model_event.go
index 3a31df3d205..1e6e58cc61f 100644
--- a/pkg/api/v1/testkube/model_event.go
+++ b/pkg/api/v1/testkube/model_event.go
@@ -12,13 +12,16 @@ package testkube
// Event data
type Event struct {
// UUID of event
- Id string `json:"id"`
- Resource *EventResource `json:"resource"`
+ Id string `json:"id"`
+ // stream topic
+ StreamTopic string `json:"streamTopic,omitempty"`
+ Resource *EventResource `json:"resource"`
// ID of resource
- ResourceId string `json:"resourceId"`
- Type_ *EventType `json:"type"`
- TestExecution *Execution `json:"testExecution,omitempty"`
- TestSuiteExecution *TestSuiteExecution `json:"testSuiteExecution,omitempty"`
+ ResourceId string `json:"resourceId"`
+ Type_ *EventType `json:"type"`
+ TestExecution *Execution `json:"testExecution,omitempty"`
+ TestSuiteExecution *TestSuiteExecution `json:"testSuiteExecution,omitempty"`
+ TestWorkflowExecution *TestWorkflowExecution `json:"testWorkflowExecution,omitempty"`
// cluster name of event
ClusterName string `json:"clusterName,omitempty"`
// environment variables
diff --git a/pkg/api/v1/testkube/model_event_extended.go b/pkg/api/v1/testkube/model_event_extended.go
index a01edfbb288..332e9f43fcb 100644
--- a/pkg/api/v1/testkube/model_event_extended.go
+++ b/pkg/api/v1/testkube/model_event_extended.go
@@ -7,6 +7,19 @@ import (
"k8s.io/apimachinery/pkg/labels"
)
+const (
+ TestStartSubject = "events.test.start"
+ TestStopSubject = "events.test.stop"
+)
+
+// check if Event implements model generic event type
+var _ Trigger = Event{}
+
+// Trigger for generic events
+type Trigger interface {
+ GetResourceId() string
+}
+
func NewEvent(t *EventType, resource *EventResource, id string) Event {
return Event{
Id: uuid.NewString(),
@@ -21,6 +34,8 @@ func NewEventStartTest(execution *Execution) Event {
Id: uuid.NewString(),
Type_: EventStartTest,
TestExecution: execution,
+ StreamTopic: TestStartSubject,
+ ResourceId: execution.Id,
}
}
@@ -29,6 +44,8 @@ func NewEventEndTestSuccess(execution *Execution) Event {
Id: uuid.NewString(),
Type_: EventEndTestSuccess,
TestExecution: execution,
+ StreamTopic: TestStopSubject,
+ ResourceId: execution.Id,
}
}
@@ -37,6 +54,8 @@ func NewEventEndTestFailed(execution *Execution) Event {
Id: uuid.NewString(),
Type_: EventEndTestFailed,
TestExecution: execution,
+ StreamTopic: TestStopSubject,
+ ResourceId: execution.Id,
}
}
@@ -45,6 +64,8 @@ func NewEventEndTestAborted(execution *Execution) Event {
Id: uuid.NewString(),
Type_: EventEndTestAborted,
TestExecution: execution,
+ StreamTopic: TestStopSubject,
+ ResourceId: execution.Id,
}
}
@@ -53,6 +74,8 @@ func NewEventEndTestTimeout(execution *Execution) Event {
Id: uuid.NewString(),
Type_: EventEndTestTimeout,
TestExecution: execution,
+ StreamTopic: TestStopSubject,
+ ResourceId: execution.Id,
}
}
@@ -96,6 +119,46 @@ func NewEventEndTestSuiteTimeout(execution *TestSuiteExecution) Event {
}
}
+func NewEventQueueTestWorkflow(execution *TestWorkflowExecution) Event {
+ return Event{
+ Id: uuid.NewString(),
+ Type_: EventQueueTestWorkflow,
+ TestWorkflowExecution: execution,
+ }
+}
+
+func NewEventStartTestWorkflow(execution *TestWorkflowExecution) Event {
+ return Event{
+ Id: uuid.NewString(),
+ Type_: EventStartTestWorkflow,
+ TestWorkflowExecution: execution,
+ }
+}
+
+func NewEventEndTestWorkflowSuccess(execution *TestWorkflowExecution) Event {
+ return Event{
+ Id: uuid.NewString(),
+ Type_: EventEndTestWorkflowSuccess,
+ TestWorkflowExecution: execution,
+ }
+}
+
+func NewEventEndTestWorkflowFailed(execution *TestWorkflowExecution) Event {
+ return Event{
+ Id: uuid.NewString(),
+ Type_: EventEndTestWorkflowFailed,
+ TestWorkflowExecution: execution,
+ }
+}
+
+func NewEventEndTestWorkflowAborted(execution *TestWorkflowExecution) Event {
+ return Event{
+ Id: uuid.NewString(),
+ Type_: EventEndTestWorkflowAborted,
+ TestWorkflowExecution: execution,
+ }
+}
+
func (e Event) Type() EventType {
if e.Type_ != nil {
return *e.Type_
@@ -184,6 +247,10 @@ func (e Event) Valid(selector string, types []EventType) (valid bool) {
// Topic returns topic for event based on resource and resource id
// or fallback to global "events" topic
func (e Event) Topic() string {
+ if e.StreamTopic != "" {
+ return e.StreamTopic
+ }
+
if e.Resource == nil {
return "events.all"
}
@@ -194,3 +261,8 @@ func (e Event) Topic() string {
return "events." + string(*e.Resource) + "." + e.ResourceId
}
+
+// GetResourceId implmenents generic event trigger
+func (e Event) GetResourceId() string {
+ return e.ResourceId
+}
diff --git a/pkg/api/v1/testkube/model_event_extended_test.go b/pkg/api/v1/testkube/model_event_extended_test.go
index 8e0741beab1..b7853bdbe68 100644
--- a/pkg/api/v1/testkube/model_event_extended_test.go
+++ b/pkg/api/v1/testkube/model_event_extended_test.go
@@ -116,6 +116,11 @@ func TestEvent_IsSuccess(t *testing.T) {
func TestEvent_Topic(t *testing.T) {
+ t.Run("should return events topic if explicitly set", func(t *testing.T) {
+ evt := Event{Type_: EventStartTest, StreamTopic: "topic"}
+ assert.Equal(t, "topic", evt.Topic())
+ })
+
t.Run("should return events topic if not resource set", func(t *testing.T) {
evt := Event{Type_: EventStartTest, Resource: nil}
assert.Equal(t, "events.all", evt.Topic())
diff --git a/pkg/api/v1/testkube/model_event_resource.go b/pkg/api/v1/testkube/model_event_resource.go
index 34acdae7ffc..91779782cb3 100644
--- a/pkg/api/v1/testkube/model_event_resource.go
+++ b/pkg/api/v1/testkube/model_event_resource.go
@@ -13,12 +13,14 @@ type EventResource string
// List of EventResource
const (
- TEST_EventResource EventResource = "test"
- TESTSUITE_EventResource EventResource = "testsuite"
- EXECUTOR_EventResource EventResource = "executor"
- TRIGGER_EventResource EventResource = "trigger"
- WEBHOOK_EventResource EventResource = "webhook"
- TESTEXECUTION_EventResource EventResource = "testexecution"
- TESTSUITEEXECUTION_EventResource EventResource = "testsuiteexecution"
- TESTSOURCE_EventResource EventResource = "testsource"
+ TEST_EventResource EventResource = "test"
+ TESTSUITE_EventResource EventResource = "testsuite"
+ EXECUTOR_EventResource EventResource = "executor"
+ TRIGGER_EventResource EventResource = "trigger"
+ WEBHOOK_EventResource EventResource = "webhook"
+ TESTEXECUTION_EventResource EventResource = "testexecution"
+ TESTSUITEEXECUTION_EventResource EventResource = "testsuiteexecution"
+ TESTSOURCE_EventResource EventResource = "testsource"
+ TESTWORKFLOW_EventResource EventResource = "testworkflow"
+ TESTWORKFLOWEXECUTION_EventResource EventResource = "testworkflowexecution"
)
diff --git a/pkg/api/v1/testkube/model_event_type.go b/pkg/api/v1/testkube/model_event_type.go
index 3a8b78a09db..1fcd825675d 100644
--- a/pkg/api/v1/testkube/model_event_type.go
+++ b/pkg/api/v1/testkube/model_event_type.go
@@ -13,17 +13,22 @@ type EventType string
// List of EventType
const (
- START_TEST_EventType EventType = "start-test"
- END_TEST_SUCCESS_EventType EventType = "end-test-success"
- END_TEST_FAILED_EventType EventType = "end-test-failed"
- END_TEST_ABORTED_EventType EventType = "end-test-aborted"
- END_TEST_TIMEOUT_EventType EventType = "end-test-timeout"
- START_TESTSUITE_EventType EventType = "start-testsuite"
- END_TESTSUITE_SUCCESS_EventType EventType = "end-testsuite-success"
- END_TESTSUITE_FAILED_EventType EventType = "end-testsuite-failed"
- END_TESTSUITE_ABORTED_EventType EventType = "end-testsuite-aborted"
- END_TESTSUITE_TIMEOUT_EventType EventType = "end-testsuite-timeout"
- CREATED_EventType EventType = "created"
- UPDATED_EventType EventType = "updated"
- DELETED_EventType EventType = "deleted"
+ START_TEST_EventType EventType = "start-test"
+ END_TEST_SUCCESS_EventType EventType = "end-test-success"
+ END_TEST_FAILED_EventType EventType = "end-test-failed"
+ END_TEST_ABORTED_EventType EventType = "end-test-aborted"
+ END_TEST_TIMEOUT_EventType EventType = "end-test-timeout"
+ START_TESTSUITE_EventType EventType = "start-testsuite"
+ END_TESTSUITE_SUCCESS_EventType EventType = "end-testsuite-success"
+ END_TESTSUITE_FAILED_EventType EventType = "end-testsuite-failed"
+ END_TESTSUITE_ABORTED_EventType EventType = "end-testsuite-aborted"
+ END_TESTSUITE_TIMEOUT_EventType EventType = "end-testsuite-timeout"
+ QUEUE_TESTWORKFLOW_EventType EventType = "queue-testworkflow"
+ START_TESTWORKFLOW_EventType EventType = "start-testworkflow"
+ END_TESTWORKFLOW_SUCCESS_EventType EventType = "end-testworkflow-success"
+ END_TESTWORKFLOW_FAILED_EventType EventType = "end-testworkflow-failed"
+ END_TESTWORKFLOW_ABORTED_EventType EventType = "end-testworkflow-aborted"
+ CREATED_EventType EventType = "created"
+ UPDATED_EventType EventType = "updated"
+ DELETED_EventType EventType = "deleted"
)
diff --git a/pkg/api/v1/testkube/model_event_type_extended.go b/pkg/api/v1/testkube/model_event_type_extended.go
index c21b74d5a70..8d4e1733890 100644
--- a/pkg/api/v1/testkube/model_event_type_extended.go
+++ b/pkg/api/v1/testkube/model_event_type_extended.go
@@ -11,6 +11,11 @@ var AllEventTypes = []EventType{
END_TESTSUITE_FAILED_EventType,
END_TESTSUITE_ABORTED_EventType,
END_TESTSUITE_TIMEOUT_EventType,
+ QUEUE_TESTWORKFLOW_EventType,
+ START_TESTWORKFLOW_EventType,
+ END_TESTWORKFLOW_SUCCESS_EventType,
+ END_TESTWORKFLOW_FAILED_EventType,
+ END_TESTWORKFLOW_ABORTED_EventType,
CREATED_EventType,
DELETED_EventType,
UPDATED_EventType,
@@ -25,19 +30,24 @@ func EventTypePtr(t EventType) *EventType {
}
var (
- EventStartTest = EventTypePtr(START_TEST_EventType)
- EventEndTestSuccess = EventTypePtr(END_TEST_SUCCESS_EventType)
- EventEndTestFailed = EventTypePtr(END_TEST_FAILED_EventType)
- EventEndTestAborted = EventTypePtr(END_TEST_ABORTED_EventType)
- EventEndTestTimeout = EventTypePtr(END_TEST_TIMEOUT_EventType)
- EventStartTestSuite = EventTypePtr(START_TESTSUITE_EventType)
- EventEndTestSuiteSuccess = EventTypePtr(END_TESTSUITE_SUCCESS_EventType)
- EventEndTestSuiteFailed = EventTypePtr(END_TESTSUITE_FAILED_EventType)
- EventEndTestSuiteAborted = EventTypePtr(END_TESTSUITE_ABORTED_EventType)
- EventEndTestSuiteTimeout = EventTypePtr(END_TESTSUITE_TIMEOUT_EventType)
- EventCreated = EventTypePtr(CREATED_EventType)
- EventDeleted = EventTypePtr(DELETED_EventType)
- EventUpdated = EventTypePtr(UPDATED_EventType)
+ EventStartTest = EventTypePtr(START_TEST_EventType)
+ EventEndTestSuccess = EventTypePtr(END_TEST_SUCCESS_EventType)
+ EventEndTestFailed = EventTypePtr(END_TEST_FAILED_EventType)
+ EventEndTestAborted = EventTypePtr(END_TEST_ABORTED_EventType)
+ EventEndTestTimeout = EventTypePtr(END_TEST_TIMEOUT_EventType)
+ EventStartTestSuite = EventTypePtr(START_TESTSUITE_EventType)
+ EventEndTestSuiteSuccess = EventTypePtr(END_TESTSUITE_SUCCESS_EventType)
+ EventEndTestSuiteFailed = EventTypePtr(END_TESTSUITE_FAILED_EventType)
+ EventEndTestSuiteAborted = EventTypePtr(END_TESTSUITE_ABORTED_EventType)
+ EventEndTestSuiteTimeout = EventTypePtr(END_TESTSUITE_TIMEOUT_EventType)
+ EventQueueTestWorkflow = EventTypePtr(QUEUE_TESTWORKFLOW_EventType)
+ EventStartTestWorkflow = EventTypePtr(START_TESTWORKFLOW_EventType)
+ EventEndTestWorkflowSuccess = EventTypePtr(END_TESTWORKFLOW_SUCCESS_EventType)
+ EventEndTestWorkflowFailed = EventTypePtr(END_TESTWORKFLOW_FAILED_EventType)
+ EventEndTestWorkflowAborted = EventTypePtr(END_TESTWORKFLOW_ABORTED_EventType)
+ EventCreated = EventTypePtr(CREATED_EventType)
+ EventDeleted = EventTypePtr(DELETED_EventType)
+ EventUpdated = EventTypePtr(UPDATED_EventType)
)
func EventTypesFromSlice(types []string) []EventType {
diff --git a/pkg/api/v1/testkube/model_execution.go b/pkg/api/v1/testkube/model_execution.go
index 77cc094d517..87ae7711bf3 100644
--- a/pkg/api/v1/testkube/model_execution.go
+++ b/pkg/api/v1/testkube/model_execution.go
@@ -69,8 +69,10 @@ type Execution struct {
// script to run after test execution
PostRunScript string `json:"postRunScript,omitempty"`
// execute post run script before scraping (prebuilt executor only)
- ExecutePostRunScriptBeforeScraping bool `json:"executePostRunScriptBeforeScraping,omitempty"`
- RunningContext *RunningContext `json:"runningContext,omitempty"`
+ ExecutePostRunScriptBeforeScraping bool `json:"executePostRunScriptBeforeScraping,omitempty"`
+ // run scripts using source command (container executor only)
+ SourceScripts bool `json:"sourceScripts,omitempty"`
+ RunningContext *RunningContext `json:"runningContext,omitempty"`
// shell used in container executor
ContainerShell string `json:"containerShell,omitempty"`
// test execution name started the test execution
@@ -80,4 +82,6 @@ type Execution struct {
// test names for artifacts to download from latest executions
DownloadArtifactTestNames []string `json:"downloadArtifactTestNames,omitempty"`
SlavePodRequest *PodRequest `json:"slavePodRequest,omitempty"`
+ // namespace for test execution (Pro edition only)
+ ExecutionNamespace string `json:"executionNamespace,omitempty"`
}
diff --git a/pkg/api/v1/testkube/model_execution_request.go b/pkg/api/v1/testkube/model_execution_request.go
index e6a8334c3af..391cb3d31de 100644
--- a/pkg/api/v1/testkube/model_execution_request.go
+++ b/pkg/api/v1/testkube/model_execution_request.go
@@ -80,6 +80,8 @@ type ExecutionRequest struct {
PostRunScript string `json:"postRunScript,omitempty"`
// execute post run script before scraping (prebuilt executor only)
ExecutePostRunScriptBeforeScraping bool `json:"executePostRunScriptBeforeScraping,omitempty"`
+ // run scripts using source command (container executor only)
+ SourceScripts bool `json:"sourceScripts,omitempty"`
// scraper template extensions
ScraperTemplate string `json:"scraperTemplate,omitempty"`
// name of the template resource
@@ -100,4 +102,6 @@ type ExecutionRequest struct {
// test names for artifacts to download from latest executions
DownloadArtifactTestNames []string `json:"downloadArtifactTestNames,omitempty"`
SlavePodRequest *PodRequest `json:"slavePodRequest,omitempty"`
+ // namespace for test execution (Pro edition only)
+ ExecutionNamespace string `json:"executionNamespace,omitempty"`
}
diff --git a/pkg/api/v1/testkube/model_execution_update_request.go b/pkg/api/v1/testkube/model_execution_update_request.go
index 13f762d2087..d0ae7893f36 100644
--- a/pkg/api/v1/testkube/model_execution_update_request.go
+++ b/pkg/api/v1/testkube/model_execution_update_request.go
@@ -80,6 +80,8 @@ type ExecutionUpdateRequest struct {
PostRunScript *string `json:"postRunScript,omitempty"`
// execute post run script before scraping (prebuilt executor only)
ExecutePostRunScriptBeforeScraping *bool `json:"executePostRunScriptBeforeScraping,omitempty"`
+ // run scripts using source command (container executor only)
+ SourceScripts *bool `json:"sourceScripts,omitempty"`
// scraper template extensions
ScraperTemplate *string `json:"scraperTemplate,omitempty"`
// name of the template resource
@@ -100,4 +102,6 @@ type ExecutionUpdateRequest struct {
// test names for artifacts to download from latest executions
DownloadArtifactTestNames *[]string `json:"downloadArtifactTestNames,omitempty"`
SlavePodRequest **PodUpdateRequest `json:"slavePodRequest,omitempty"`
+ // namespace for test execution (Pro edition only)
+ ExecutionNamespace *string `json:"executionNamespace,omitempty"`
}
diff --git a/pkg/api/v1/testkube/model_features.go b/pkg/api/v1/testkube/model_features.go
new file mode 100644
index 00000000000..68b3bc1ad29
--- /dev/null
+++ b/pkg/api/v1/testkube/model_features.go
@@ -0,0 +1,15 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type Features struct {
+ // Log processing version 2
+ LogsV2 bool `json:"logsV2"`
+}
diff --git a/pkg/api/v1/testkube/model_gce_persistent_disk_volume_source.go b/pkg/api/v1/testkube/model_gce_persistent_disk_volume_source.go
new file mode 100644
index 00000000000..6369325274c
--- /dev/null
+++ b/pkg/api/v1/testkube/model_gce_persistent_disk_volume_source.go
@@ -0,0 +1,22 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// gcePersistentDisk represents a GCE Disk resource that is attached to a kubelet's host machine and then exposed to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk
+type GcePersistentDiskVolumeSource struct {
+ // fsType is filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk TODO: how do we prevent errors in the filesystem from compromising the machine
+ FsType string `json:"fsType,omitempty"`
+ // partition is the partition in the volume that you want to mount. If omitted, the default is to mount by volume name. Examples: For volume /dev/sda1, you specify the partition as \"1\". Similarly, the volume partition for /dev/sda is \"0\" (or you can leave the property empty). More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk
+ Partition int32 `json:"partition,omitempty"`
+ // pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk
+ PdName string `json:"pdName"`
+ // readOnly here will force the ReadOnly setting in VolumeMounts. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk
+ ReadOnly bool `json:"readOnly,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_host_path_volume_source.go b/pkg/api/v1/testkube/model_host_path_volume_source.go
new file mode 100644
index 00000000000..edefe82fd89
--- /dev/null
+++ b/pkg/api/v1/testkube/model_host_path_volume_source.go
@@ -0,0 +1,17 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// hostPath represents a pre-existing file or directory on the host machine that is directly exposed to the container. This is generally used for system agents or other privileged things that are allowed to see the host machine. Most containers will NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath --- TODO(jonesdl) We need to restrict who can use host directory mounts and who can/can not mount host directories as read/write.
+type HostPathVolumeSource struct {
+ // path of the directory on the host. If the path is a symlink, it will follow the link to the real path. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath
+ Path string `json:"path"`
+ Type_ *BoxedString `json:"type,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_image_pull_policy.go b/pkg/api/v1/testkube/model_image_pull_policy.go
new file mode 100644
index 00000000000..0f698dec386
--- /dev/null
+++ b/pkg/api/v1/testkube/model_image_pull_policy.go
@@ -0,0 +1,19 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type ImagePullPolicy string
+
+// List of ImagePullPolicy
+const (
+ ALWAYS_ImagePullPolicy ImagePullPolicy = "Always"
+ NEVER_ImagePullPolicy ImagePullPolicy = "Never"
+ IF_NOT_PRESENT_ImagePullPolicy ImagePullPolicy = "IfNotPresent"
+)
diff --git a/pkg/api/v1/testkube/model_log_v1.go b/pkg/api/v1/testkube/model_log_v1.go
new file mode 100644
index 00000000000..2f87bc1f8b5
--- /dev/null
+++ b/pkg/api/v1/testkube/model_log_v1.go
@@ -0,0 +1,15 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// Log format version 1
+type LogV1 struct {
+ Result *ExecutionResult `json:"result,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_log_v2.go b/pkg/api/v1/testkube/model_log_v2.go
new file mode 100644
index 00000000000..fd435936f09
--- /dev/null
+++ b/pkg/api/v1/testkube/model_log_v2.go
@@ -0,0 +1,33 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+import (
+ "time"
+)
+
+// Log format version 2
+type LogV2 struct {
+ // Timestamp of log
+ Time time.Time `json:"time,omitempty"`
+ // Message/event data passed from executor (like log lines etc)
+ Content string `json:"content,omitempty"`
+ // One of possible log types
+ Type_ string `json:"type,omitempty"`
+ // One of possible log sources
+ Source string `json:"source"`
+ // indicates a log error
+ Error_ bool `json:"error,omitempty"`
+ // One of possible log versions
+ Version string `json:"version,omitempty"`
+ // additional log details
+ Metadata map[string]string `json:"metadata,omitempty"`
+ V1 *LogV1 `json:"v1,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_nfs_volume_source.go b/pkg/api/v1/testkube/model_nfs_volume_source.go
new file mode 100644
index 00000000000..1e8b200cb74
--- /dev/null
+++ b/pkg/api/v1/testkube/model_nfs_volume_source.go
@@ -0,0 +1,20 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// nfs represents an NFS mount on the host that shares a pod's lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs
+type NfsVolumeSource struct {
+ // path that is exported by the NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs
+ Path string `json:"path"`
+ // readOnly here will force the NFS export to be mounted with read-only permissions. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs
+ ReadOnly bool `json:"readOnly,omitempty"`
+ // server is the hostname or IP address of the NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs
+ Server string `json:"server"`
+}
diff --git a/pkg/api/v1/testkube/model_persistent_volume_claim_volume_source.go b/pkg/api/v1/testkube/model_persistent_volume_claim_volume_source.go
new file mode 100644
index 00000000000..bfedb173bd7
--- /dev/null
+++ b/pkg/api/v1/testkube/model_persistent_volume_claim_volume_source.go
@@ -0,0 +1,18 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// persistentVolumeClaimVolumeSource represents a reference to a PersistentVolumeClaim in the same namespace. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims
+type PersistentVolumeClaimVolumeSource struct {
+ // claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims
+ ClaimName string `json:"claimName"`
+ // readOnly Will force the ReadOnly setting in VolumeMounts. Default false.
+ ReadOnly bool `json:"readOnly,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_repository_extended.go b/pkg/api/v1/testkube/model_repository_extended.go
index 5bfef92f3b3..42891051506 100644
--- a/pkg/api/v1/testkube/model_repository_extended.go
+++ b/pkg/api/v1/testkube/model_repository_extended.go
@@ -41,5 +41,11 @@ func (r *Repository) WithAuthType(authType GitAuthType) *Repository {
// IsEmpty returns true if repository is empty
func (r *Repository) IsEmpty() bool {
- return r == nil || r.Uri == ""
+ return r == nil ||
+ (r.Type_ == "" &&
+ r.Uri == "" &&
+ r.Branch == "" &&
+ r.Path == "" &&
+ r.Commit == "" &&
+ r.WorkingDir == "")
}
diff --git a/pkg/api/v1/testkube/model_secret_env_source.go b/pkg/api/v1/testkube/model_secret_env_source.go
new file mode 100644
index 00000000000..0255909b9ff
--- /dev/null
+++ b/pkg/api/v1/testkube/model_secret_env_source.go
@@ -0,0 +1,15 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type SecretEnvSource struct {
+ Name string `json:"name"`
+ Optional bool `json:"optional,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_secret_volume_source.go b/pkg/api/v1/testkube/model_secret_volume_source.go
new file mode 100644
index 00000000000..5deede9f2f1
--- /dev/null
+++ b/pkg/api/v1/testkube/model_secret_volume_source.go
@@ -0,0 +1,21 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// secret represents a secret that should populate this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret
+type SecretVolumeSource struct {
+ DefaultMode *BoxedInteger `json:"defaultMode,omitempty"`
+ // items If unspecified, each key-value pair in the Data field of the referenced Secret will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the Secret, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.
+ Items []SecretVolumeSourceItems `json:"items,omitempty"`
+ // optional field specify whether the Secret or its keys must be defined
+ Optional bool `json:"optional,omitempty"`
+ // secretName is the name of the secret in the pod's namespace to use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret
+ SecretName string `json:"secretName,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_secret_volume_source_items.go b/pkg/api/v1/testkube/model_secret_volume_source_items.go
new file mode 100644
index 00000000000..8afdaacf46e
--- /dev/null
+++ b/pkg/api/v1/testkube/model_secret_volume_source_items.go
@@ -0,0 +1,19 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// Maps a string key to a path within a volume.
+type SecretVolumeSourceItems struct {
+ // key is the key to project.
+ Key string `json:"key"`
+ Mode *BoxedInteger `json:"mode,omitempty"`
+ // path is the relative path of the file to map the key to. May not be an absolute path. May not contain the path element '..'. May not start with the string '..'.
+ Path string `json:"path"`
+}
diff --git a/pkg/api/v1/testkube/model_security_context.go b/pkg/api/v1/testkube/model_security_context.go
new file mode 100644
index 00000000000..af41964a42f
--- /dev/null
+++ b/pkg/api/v1/testkube/model_security_context.go
@@ -0,0 +1,19 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type SecurityContext struct {
+ Privileged *BoxedBoolean `json:"privileged,omitempty"`
+ RunAsUser *BoxedInteger `json:"runAsUser,omitempty"`
+ RunAsGroup *BoxedInteger `json:"runAsGroup,omitempty"`
+ RunAsNonRoot *BoxedBoolean `json:"runAsNonRoot,omitempty"`
+ ReadOnlyRootFilesystem *BoxedBoolean `json:"readOnlyRootFilesystem,omitempty"`
+ AllowPrivilegeEscalation *BoxedBoolean `json:"allowPrivilegeEscalation,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_server_info.go b/pkg/api/v1/testkube/model_server_info.go
index 38e0ff0051f..5f3f0193b96 100644
--- a/pkg/api/v1/testkube/model_server_info.go
+++ b/pkg/api/v1/testkube/model_server_info.go
@@ -27,4 +27,7 @@ type ServerInfo struct {
HelmchartVersion string `json:"helmchartVersion,omitempty"`
// dashboard uri
DashboardUri string `json:"dashboardUri,omitempty"`
+ // disable secret creation for tests and test sources
+ DisableSecretCreation bool `json:"disableSecretCreation,omitempty"`
+ Features *Features `json:"features,omitempty"`
}
diff --git a/pkg/api/v1/testkube/model_test_content_extended.go b/pkg/api/v1/testkube/model_test_content_extended.go
index b37ec7f0807..a18a91192be 100644
--- a/pkg/api/v1/testkube/model_test_content_extended.go
+++ b/pkg/api/v1/testkube/model_test_content_extended.go
@@ -19,6 +19,7 @@ type ArgsModeType string
const (
ArgsModeTypeAppend ArgsModeType = "append"
ArgsModeTypeOverride ArgsModeType = "override"
+ ArgsModeTypeReplace ArgsModeType = "replace"
)
var ErrTestContentTypeNotFile = fmt.Errorf("unsupported content type use one of: file-uri, git-file, string")
diff --git a/pkg/api/v1/testkube/model_test_suite_base_extended.go b/pkg/api/v1/testkube/model_test_suite_base_extended.go
index 298fdc6ac0f..07f98351c06 100644
--- a/pkg/api/v1/testkube/model_test_suite_base_extended.go
+++ b/pkg/api/v1/testkube/model_test_suite_base_extended.go
@@ -79,4 +79,52 @@ func (t *TestSuite) QuoteTestSuiteTextFields() {
}
}
}
+ for i := range t.Before {
+ for j := range t.Before[i].Execute {
+ if t.Before[i].Execute[j].ExecutionRequest != nil {
+ t.Before[i].Execute[j].ExecutionRequest.QuoteTestSuiteStepExecutionRequestTextFields()
+ }
+ }
+ }
+ for i := range t.After {
+ for j := range t.After[i].Execute {
+ if t.After[i].Execute[j].ExecutionRequest != nil {
+ t.After[i].Execute[j].ExecutionRequest.QuoteTestSuiteStepExecutionRequestTextFields()
+ }
+ }
+ }
+ for i := range t.Steps {
+ for j := range t.Steps[i].Execute {
+ if t.Steps[i].Execute[j].ExecutionRequest != nil {
+ t.Steps[i].Execute[j].ExecutionRequest.QuoteTestSuiteStepExecutionRequestTextFields()
+ }
+ }
+ }
+}
+
+func (request *TestSuiteStepExecutionRequest) QuoteTestSuiteStepExecutionRequestTextFields() {
+ for i := range request.Args {
+ if request.Args[i] != "" {
+ request.Args[i] = fmt.Sprintf("%q", request.Args[i])
+ }
+ }
+
+ for i := range request.Command {
+ if request.Command[i] != "" {
+ request.Command[i] = fmt.Sprintf("%q", request.Command[i])
+ }
+ }
+
+ var fields = []*string{
+ &request.JobTemplate,
+ &request.CronJobTemplate,
+ &request.ScraperTemplate,
+ &request.PvcTemplate,
+ }
+
+ for _, field := range fields {
+ if *field != "" {
+ *field = fmt.Sprintf("%q", *field)
+ }
+ }
}
diff --git a/pkg/api/v1/testkube/model_test_suite_step.go b/pkg/api/v1/testkube/model_test_suite_step.go
index 1e1020dce2f..66efe7a308c 100644
--- a/pkg/api/v1/testkube/model_test_suite_step.go
+++ b/pkg/api/v1/testkube/model_test_suite_step.go
@@ -13,5 +13,6 @@ type TestSuiteStep struct {
// object name
Test string `json:"test,omitempty"`
// delay duration in time units
- Delay string `json:"delay,omitempty"`
+ Delay string `json:"delay,omitempty"`
+ ExecutionRequest *TestSuiteStepExecutionRequest `json:"executionRequest,omitempty"`
}
diff --git a/pkg/api/v1/testkube/model_test_suite_step_execution_request.go b/pkg/api/v1/testkube/model_test_suite_step_execution_request.go
new file mode 100644
index 00000000000..85d64b5e1b6
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_suite_step_execution_request.go
@@ -0,0 +1,48 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// test step execution request body
+type TestSuiteStepExecutionRequest struct {
+ // test execution labels
+ ExecutionLabels map[string]string `json:"executionLabels,omitempty"`
+ Variables map[string]Variable `json:"variables,omitempty"`
+ // executor image command
+ Command []string `json:"command,omitempty"`
+ // additional executor binary arguments
+ Args []string `json:"args,omitempty"`
+ // usage mode for arguments
+ ArgsMode string `json:"args_mode,omitempty"`
+ // whether to start execution sync or async
+ Sync bool `json:"sync,omitempty"`
+ // http proxy for executor containers
+ HttpProxy string `json:"httpProxy,omitempty"`
+ // https proxy for executor containers
+ HttpsProxy string `json:"httpsProxy,omitempty"`
+ // whether to run test as negative test
+ NegativeTest bool `json:"negativeTest,omitempty"`
+ // job template extensions
+ JobTemplate string `json:"jobTemplate,omitempty"`
+ // name of the template resource
+ JobTemplateReference string `json:"jobTemplateReference,omitempty"`
+ // cron job template extensions
+ CronJobTemplate string `json:"cronJobTemplate,omitempty"`
+ // name of the template resource
+ CronJobTemplateReference string `json:"cronJobTemplateReference,omitempty"`
+ // scraper template extensions
+ ScraperTemplate string `json:"scraperTemplate,omitempty"`
+ // name of the template resource
+ ScraperTemplateReference string `json:"scraperTemplateReference,omitempty"`
+ // pvc template extensions
+ PvcTemplate string `json:"pvcTemplate,omitempty"`
+ // name of the template resource
+ PvcTemplateReference string `json:"pvcTemplateReference,omitempty"`
+ RunningContext *RunningContext `json:"runningContext,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow.go b/pkg/api/v1/testkube/model_test_workflow.go
new file mode 100644
index 00000000000..0e237900d00
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow.go
@@ -0,0 +1,29 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+import (
+ "time"
+)
+
+type TestWorkflow struct {
+ // kubernetes resource name
+ Name string `json:"name,omitempty"`
+ // kubernetes namespace
+ Namespace string `json:"namespace,omitempty"`
+ // human-readable description
+ Description string `json:"description,omitempty"`
+ // test workflow labels
+ Labels map[string]string `json:"labels,omitempty"`
+ // test workflow annotations
+ Annotations map[string]string `json:"annotations,omitempty"`
+ Created time.Time `json:"created,omitempty"`
+ Spec *TestWorkflowSpec `json:"spec,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_container_config.go b/pkg/api/v1/testkube/model_test_workflow_container_config.go
new file mode 100644
index 00000000000..e070501ed9b
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_container_config.go
@@ -0,0 +1,27 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowContainerConfig struct {
+ WorkingDir *BoxedString `json:"workingDir,omitempty"`
+ // image to be used for the container
+ Image string `json:"image,omitempty"`
+ ImagePullPolicy *ImagePullPolicy `json:"imagePullPolicy,omitempty"`
+ // environment variables to append to the container
+ Env []EnvVar `json:"env,omitempty"`
+ // external environment variables to append to the container
+ EnvFrom []EnvFromSource `json:"envFrom,omitempty"`
+ Command *BoxedStringList `json:"command,omitempty"`
+ Args *BoxedStringList `json:"args,omitempty"`
+ Resources *TestWorkflowResources `json:"resources,omitempty"`
+ SecurityContext *SecurityContext `json:"securityContext,omitempty"`
+ // volumes to mount to the container
+ VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_content.go b/pkg/api/v1/testkube/model_test_workflow_content.go
new file mode 100644
index 00000000000..b659a86b3ae
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_content.go
@@ -0,0 +1,15 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowContent struct {
+ Git *TestWorkflowContentGit `json:"git,omitempty"`
+ Files []TestWorkflowContentFile `json:"files,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_content_file.go b/pkg/api/v1/testkube/model_test_workflow_content_file.go
new file mode 100644
index 00000000000..c1ddfd048ee
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_content_file.go
@@ -0,0 +1,19 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowContentFile struct {
+ // path where the file should be accessible at
+ Path string `json:"path"`
+ // plain-text content to put inside
+ Content string `json:"content,omitempty"`
+ ContentFrom *EnvVarSource `json:"contentFrom,omitempty"`
+ Mode *BoxedInteger `json:"mode,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_content_git.go b/pkg/api/v1/testkube/model_test_workflow_content_git.go
new file mode 100644
index 00000000000..24ec57835a1
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_content_git.go
@@ -0,0 +1,28 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowContentGit struct {
+ // uri for the Git repository
+ Uri string `json:"uri,omitempty"`
+ // branch, commit or a tag name to fetch
+ Revision string `json:"revision,omitempty"`
+ // plain text username to fetch with
+ Username string `json:"username,omitempty"`
+ UsernameFrom *EnvVarSource `json:"usernameFrom,omitempty"`
+ // plain text token to fetch with
+ Token string `json:"token,omitempty"`
+ TokenFrom *EnvVarSource `json:"tokenFrom,omitempty"`
+ AuthType *ContentGitAuthType `json:"authType,omitempty"`
+ // where to mount the fetched repository contents (defaults to \"repo\" directory in the data volume)
+ MountPath string `json:"mountPath,omitempty"`
+ // paths to fetch for the sparse checkout
+ Paths []string `json:"paths,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_execution.go b/pkg/api/v1/testkube/model_test_workflow_execution.go
new file mode 100644
index 00000000000..1ed8c872b8a
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_execution.go
@@ -0,0 +1,34 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+import (
+ "time"
+)
+
+type TestWorkflowExecution struct {
+ // unique execution identifier
+ Id string `json:"id"`
+ // execution name
+ Name string `json:"name"`
+ // sequence number for the execution
+ Number int32 `json:"number,omitempty"`
+ // when the execution has been scheduled to run
+ ScheduledAt time.Time `json:"scheduledAt,omitempty"`
+ // when the execution result's status has changed last time (queued, passed, failed)
+ StatusAt time.Time `json:"statusAt,omitempty"`
+ // structured tree of steps
+ Signature []TestWorkflowSignature `json:"signature,omitempty"`
+ Result *TestWorkflowResult `json:"result,omitempty"`
+ // additional information from the steps, like referenced executed tests or artifacts
+ Output []TestWorkflowOutput `json:"output,omitempty"`
+ Workflow *TestWorkflow `json:"workflow"`
+ ResolvedWorkflow *TestWorkflow `json:"resolvedWorkflow,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_execution_extended.go b/pkg/api/v1/testkube/model_test_workflow_execution_extended.go
new file mode 100644
index 00000000000..4257004480c
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_execution_extended.go
@@ -0,0 +1,40 @@
+package testkube
+
+import "github.com/kubeshop/testkube/pkg/utils"
+
+type TestWorkflowExecutions []TestWorkflowExecution
+
+func (executions TestWorkflowExecutions) Table() (header []string, output [][]string) {
+ header = []string{"Id", "Name", "Test Workflow Name", "Status", "Labels"}
+
+ for _, e := range executions {
+ status := "unknown"
+ if e.Result != nil && e.Result.Status != nil {
+ status = string(*e.Result.Status)
+ }
+
+ output = append(output, []string{
+ e.Id,
+ e.Name,
+ e.Workflow.Name,
+ status,
+ MapToString(e.Workflow.Labels),
+ })
+ }
+
+ return
+}
+
+func (e *TestWorkflowExecution) ConvertDots(fn func(string) string) *TestWorkflowExecution {
+ e.Workflow.ConvertDots(fn)
+ e.ResolvedWorkflow.ConvertDots(fn)
+ return e
+}
+
+func (e *TestWorkflowExecution) EscapeDots() *TestWorkflowExecution {
+ return e.ConvertDots(utils.EscapeDots)
+}
+
+func (e *TestWorkflowExecution) UnscapeDots() *TestWorkflowExecution {
+ return e.ConvertDots(utils.UnescapeDots)
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_execution_notification.go b/pkg/api/v1/testkube/model_test_workflow_execution_notification.go
new file mode 100644
index 00000000000..97d3ee69626
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_execution_notification.go
@@ -0,0 +1,25 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+import (
+ "time"
+)
+
+type TestWorkflowExecutionNotification struct {
+ // timestamp for the notification if available
+ Ts time.Time `json:"ts,omitempty"`
+ Result *TestWorkflowResult `json:"result,omitempty"`
+ // step reference, if related to some specific step
+ Ref string `json:"ref,omitempty"`
+ // log content, if it's just a log. note, that it includes 30 chars timestamp + space
+ Log string `json:"log,omitempty"`
+ Output *TestWorkflowOutput `json:"output,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_execution_request.go b/pkg/api/v1/testkube/model_test_workflow_execution_request.go
new file mode 100644
index 00000000000..ccd4d0db8a4
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_execution_request.go
@@ -0,0 +1,16 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowExecutionRequest struct {
+ // custom execution name
+ Name string `json:"name,omitempty"`
+ Config map[string]string `json:"config,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_execution_summary.go b/pkg/api/v1/testkube/model_test_workflow_execution_summary.go
new file mode 100644
index 00000000000..15e3f4c61aa
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_execution_summary.go
@@ -0,0 +1,29 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+import (
+ "time"
+)
+
+type TestWorkflowExecutionSummary struct {
+ // unique execution identifier
+ Id string `json:"id"`
+ // execution name
+ Name string `json:"name"`
+ // sequence number for the execution
+ Number int32 `json:"number,omitempty"`
+ // when the execution has been scheduled to run
+ ScheduledAt time.Time `json:"scheduledAt,omitempty"`
+ // when the execution result's status has changed last time (queued, passed, failed)
+ StatusAt time.Time `json:"statusAt,omitempty"`
+ Result *TestWorkflowResultSummary `json:"result,omitempty"`
+ Workflow *TestWorkflowSummary `json:"workflow"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_execution_summary_extended.go b/pkg/api/v1/testkube/model_test_workflow_execution_summary_extended.go
new file mode 100644
index 00000000000..5f68c5367e7
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_execution_summary_extended.go
@@ -0,0 +1,41 @@
+package testkube
+
+import (
+ "github.com/kubeshop/testkube/pkg/utils"
+)
+
+type TestWorkflowExecutionSummaries []TestWorkflowExecutionSummary
+
+func (executions TestWorkflowExecutionSummaries) Table() (header []string, output [][]string) {
+ header = []string{"Id", "Name", "Test Workflow Name", "Status", "Labels"}
+
+ for _, e := range executions {
+ status := "unknown"
+ if e.Result != nil && e.Result.Status != nil {
+ status = string(*e.Result.Status)
+ }
+
+ output = append(output, []string{
+ e.Id,
+ e.Name,
+ e.Workflow.Name,
+ status,
+ MapToString(e.Workflow.Labels),
+ })
+ }
+
+ return
+}
+
+func (e *TestWorkflowExecutionSummary) ConvertDots(fn func(string) string) *TestWorkflowExecutionSummary {
+ e.Workflow.ConvertDots(fn)
+ return e
+}
+
+func (e *TestWorkflowExecutionSummary) EscapeDots() *TestWorkflowExecutionSummary {
+ return e.ConvertDots(utils.EscapeDots)
+}
+
+func (e *TestWorkflowExecutionSummary) UnscapeDots() *TestWorkflowExecutionSummary {
+ return e.ConvertDots(utils.UnescapeDots)
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_executions_result.go b/pkg/api/v1/testkube/model_test_workflow_executions_result.go
new file mode 100644
index 00000000000..c6dad65d0a5
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_executions_result.go
@@ -0,0 +1,16 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowExecutionsResult struct {
+ Totals *ExecutionsTotals `json:"totals"`
+ Filtered *ExecutionsTotals `json:"filtered"`
+ Results []TestWorkflowExecutionSummary `json:"results"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_extended.go b/pkg/api/v1/testkube/model_test_workflow_extended.go
new file mode 100644
index 00000000000..6b79acb1245
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_extended.go
@@ -0,0 +1,78 @@
+package testkube
+
+import "github.com/kubeshop/testkube/pkg/utils"
+
+type TestWorkflows []TestWorkflow
+
+func (t TestWorkflows) Table() (header []string, output [][]string) {
+ header = []string{"Name", "Description", "Created", "Labels"}
+ for _, e := range t {
+ output = append(output, []string{
+ e.Name,
+ e.Description,
+ e.Created.String(),
+ MapToString(e.Labels),
+ })
+ }
+
+ return
+}
+
+func convertDotsInMap[T any](m map[string]T, fn func(string) string) map[string]T {
+ result := make(map[string]T)
+ for key, value := range m {
+ result[fn(key)] = value
+ }
+ return result
+}
+
+func (w *TestWorkflow) ConvertDots(fn func(string) string) *TestWorkflow {
+ if w == nil {
+ return w
+ }
+ if w.Labels == nil {
+ w.Labels = convertDotsInMap(w.Labels, fn)
+ }
+ if w.Spec.Pod != nil {
+ w.Spec.Pod.Labels = convertDotsInMap(w.Spec.Pod.Labels, fn)
+ w.Spec.Pod.Annotations = convertDotsInMap(w.Spec.Pod.Annotations, fn)
+ w.Spec.Pod.NodeSelector = convertDotsInMap(w.Spec.Pod.NodeSelector, fn)
+ }
+ if w.Spec.Job != nil {
+ w.Spec.Job.Labels = convertDotsInMap(w.Spec.Job.Labels, fn)
+ w.Spec.Job.Annotations = convertDotsInMap(w.Spec.Job.Annotations, fn)
+ }
+ for i := range w.Spec.Use {
+ if w.Spec.Use[i].Config != nil {
+ w.Spec.Use[i].Config = convertDotsInMap(w.Spec.Use[i].Config, fn)
+ }
+ }
+ for i := range w.Spec.Setup {
+ w.Spec.Setup[i].ConvertDots(fn)
+ }
+ for i := range w.Spec.Steps {
+ w.Spec.Steps[i].ConvertDots(fn)
+ }
+ for i := range w.Spec.After {
+ w.Spec.After[i].ConvertDots(fn)
+ }
+ return w
+}
+
+func (w *TestWorkflow) EscapeDots() *TestWorkflow {
+ return w.ConvertDots(utils.EscapeDots)
+}
+
+func (w *TestWorkflow) UnscapeDots() *TestWorkflow {
+ return w.ConvertDots(utils.UnescapeDots)
+}
+
+func (w *TestWorkflow) GetObjectRef() *ObjectRef {
+ return &ObjectRef{
+ Name: w.Name,
+ Namespace: w.Namespace,
+ }
+}
+
+func (w *TestWorkflow) QuoteWorkflowTextFields() {
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_independent_step.go b/pkg/api/v1/testkube/model_test_workflow_independent_step.go
new file mode 100644
index 00000000000..566b33cb92d
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_independent_step.go
@@ -0,0 +1,38 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowIndependentStep struct {
+ // readable name for the step
+ Name string `json:"name,omitempty"`
+ // expression to declare under which conditions the step should be run; defaults to \"passed\", except artifacts where it defaults to \"always\"
+ Condition string `json:"condition,omitempty"`
+ // is the step expected to fail
+ Negative bool `json:"negative,omitempty"`
+ // is the step optional, so the failure won't affect the TestWorkflow result
+ Optional bool `json:"optional,omitempty"`
+ Retry *TestWorkflowRetryPolicy `json:"retry,omitempty"`
+ // maximum time this step may take
+ Timeout string `json:"timeout,omitempty"`
+ // delay before the step
+ Delay string `json:"delay,omitempty"`
+ Content *TestWorkflowContent `json:"content,omitempty"`
+ // script to run in a default shell for the container
+ Shell string `json:"shell,omitempty"`
+ Run *TestWorkflowContainerConfig `json:"run,omitempty"`
+ WorkingDir *BoxedString `json:"workingDir,omitempty"`
+ Container *TestWorkflowContainerConfig `json:"container,omitempty"`
+ Execute *TestWorkflowStepExecute `json:"execute,omitempty"`
+ Artifacts *TestWorkflowStepArtifacts `json:"artifacts,omitempty"`
+ // nested setup steps to run
+ Setup []TestWorkflowIndependentStep `json:"setup,omitempty"`
+ // nested steps to run
+ Steps []TestWorkflowIndependentStep `json:"steps,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_job_config.go b/pkg/api/v1/testkube/model_test_workflow_job_config.go
new file mode 100644
index 00000000000..387db9b7e6c
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_job_config.go
@@ -0,0 +1,17 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowJobConfig struct {
+ // labels to attach to the job
+ Labels map[string]string `json:"labels,omitempty"`
+ // annotations to attach to the job
+ Annotations map[string]string `json:"annotations,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_output.go b/pkg/api/v1/testkube/model_test_workflow_output.go
new file mode 100644
index 00000000000..503068db64d
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_output.go
@@ -0,0 +1,19 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowOutput struct {
+ // step reference
+ Ref string `json:"ref,omitempty"`
+ // output kind name
+ Name string `json:"name,omitempty"`
+ // value returned
+ Value map[string]interface{} `json:"value,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_parameter_schema.go b/pkg/api/v1/testkube/model_test_workflow_parameter_schema.go
new file mode 100644
index 00000000000..5d3e22617d0
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_parameter_schema.go
@@ -0,0 +1,32 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowParameterSchema struct {
+ // human-readable description for the property
+ Description string `json:"description,omitempty"`
+ Type_ *TestWorkflowParameterType `json:"type"`
+ // list of acceptable values
+ Enum []string `json:"enum,omitempty"`
+ // example value for the parameter
+ Example string `json:"example,omitempty"`
+ Default_ *BoxedString `json:"default,omitempty"`
+ // predefined format for the string
+ Format string `json:"format,omitempty"`
+ // regular expression to match
+ Pattern string `json:"pattern,omitempty"`
+ MinLength *BoxedInteger `json:"minLength,omitempty"`
+ MaxLength *BoxedInteger `json:"maxLength,omitempty"`
+ Minimum *BoxedInteger `json:"minimum,omitempty"`
+ Maximum *BoxedInteger `json:"maximum,omitempty"`
+ ExclusiveMinimum *BoxedInteger `json:"exclusiveMinimum,omitempty"`
+ ExclusiveMaximum *BoxedInteger `json:"exclusiveMaximum,omitempty"`
+ MultipleOf *BoxedInteger `json:"multipleOf,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_parameter_type.go b/pkg/api/v1/testkube/model_test_workflow_parameter_type.go
new file mode 100644
index 00000000000..7e680454cd2
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_parameter_type.go
@@ -0,0 +1,21 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// TestWorkflowParameterType : type of the config parameter
+type TestWorkflowParameterType string
+
+// List of TestWorkflowParameterType
+const (
+ STRING__TestWorkflowParameterType TestWorkflowParameterType = "string"
+ INTEGER_TestWorkflowParameterType TestWorkflowParameterType = "integer"
+ NUMBER_TestWorkflowParameterType TestWorkflowParameterType = "number"
+ BOOLEAN_TestWorkflowParameterType TestWorkflowParameterType = "boolean"
+)
diff --git a/pkg/api/v1/testkube/model_test_workflow_pod_config.go b/pkg/api/v1/testkube/model_test_workflow_pod_config.go
new file mode 100644
index 00000000000..0fd8e70ea89
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_pod_config.go
@@ -0,0 +1,25 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowPodConfig struct {
+ // labels to attach to the pod
+ Labels map[string]string `json:"labels,omitempty"`
+ // annotations to attach to the pod
+ Annotations map[string]string `json:"annotations,omitempty"`
+ // secret references for pulling images
+ ImagePullSecrets []LocalObjectReference `json:"imagePullSecrets,omitempty"`
+ // default service account name for the containers
+ ServiceAccountName string `json:"serviceAccountName,omitempty"`
+ // label selector for node that the pod should land on
+ NodeSelector map[string]string `json:"nodeSelector,omitempty"`
+ // volumes to append to the pod
+ Volumes []Volume `json:"volumes,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_ref.go b/pkg/api/v1/testkube/model_test_workflow_ref.go
new file mode 100644
index 00000000000..ce3eebef629
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_ref.go
@@ -0,0 +1,16 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowRef struct {
+ // TestWorkflow name to include
+ Name string `json:"name"`
+ Config map[string]string `json:"config,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_resources.go b/pkg/api/v1/testkube/model_test_workflow_resources.go
new file mode 100644
index 00000000000..645b69634eb
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_resources.go
@@ -0,0 +1,15 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowResources struct {
+ Limits *TestWorkflowResourcesList `json:"limits,omitempty"`
+ Requests *TestWorkflowResourcesList `json:"requests,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_resources_list.go b/pkg/api/v1/testkube/model_test_workflow_resources_list.go
new file mode 100644
index 00000000000..f491e82bc3b
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_resources_list.go
@@ -0,0 +1,21 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowResourcesList struct {
+ // number of CPUs
+ Cpu string `json:"cpu,omitempty"`
+ // size of RAM memory
+ Memory string `json:"memory,omitempty"`
+ // storage size
+ Storage string `json:"storage,omitempty"`
+ // ephemeral storage size
+ EphemeralStorage string `json:"ephemeral-storage,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_result.go b/pkg/api/v1/testkube/model_test_workflow_result.go
new file mode 100644
index 00000000000..864645be9bc
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_result.go
@@ -0,0 +1,31 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+import (
+ "time"
+)
+
+type TestWorkflowResult struct {
+ Status *TestWorkflowStatus `json:"status"`
+ PredictedStatus *TestWorkflowStatus `json:"predictedStatus"`
+ // when the pod was created
+ QueuedAt time.Time `json:"queuedAt,omitempty"`
+ // when the pod has been successfully assigned
+ StartedAt time.Time `json:"startedAt,omitempty"`
+ // when the pod has been completed
+ FinishedAt time.Time `json:"finishedAt,omitempty"`
+ // Go-formatted (human-readable) duration
+ Duration string `json:"duration,omitempty"`
+ // Duration in milliseconds
+ DurationMs int32 `json:"durationMs,omitempty"`
+ Initialization *TestWorkflowStepResult `json:"initialization,omitempty"`
+ Steps map[string]TestWorkflowStepResult `json:"steps,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_result_extended.go b/pkg/api/v1/testkube/model_test_workflow_result_extended.go
new file mode 100644
index 00000000000..05f79cc79aa
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_result_extended.go
@@ -0,0 +1,382 @@
+package testkube
+
+import (
+ "time"
+
+ "github.com/kubeshop/testkube/internal/common"
+)
+
+func (r *TestWorkflowResult) IsFinished() bool {
+ return !r.IsStatus(QUEUED_TestWorkflowStatus) && !r.IsStatus(RUNNING_TestWorkflowStatus)
+}
+
+func (r *TestWorkflowResult) IsStatus(s TestWorkflowStatus) bool {
+ if r == nil || r.Status == nil {
+ return s == QUEUED_TestWorkflowStatus
+ }
+ return *r.Status == s
+}
+
+func (r *TestWorkflowResult) IsQueued() bool {
+ return r.IsStatus(QUEUED_TestWorkflowStatus)
+}
+
+func (r *TestWorkflowResult) IsFailed() bool {
+ return r.IsStatus(FAILED_TestWorkflowStatus)
+}
+
+func (r *TestWorkflowResult) IsAborted() bool {
+ return r.IsStatus(ABORTED_TestWorkflowStatus)
+}
+
+func (r *TestWorkflowResult) IsPassed() bool {
+ return r.IsStatus(PASSED_TestWorkflowStatus)
+}
+
+func (r *TestWorkflowResult) IsAnyError() bool {
+ return r.IsFinished() && !r.IsStatus(PASSED_TestWorkflowStatus)
+}
+
+func (r *TestWorkflowResult) Fatal(err error, aborted bool, ts time.Time) {
+ r.Initialization.ErrorMessage = err.Error()
+ r.Status = common.Ptr(FAILED_TestWorkflowStatus)
+ r.PredictedStatus = r.Status
+ if aborted {
+ r.Status = common.Ptr(ABORTED_TestWorkflowStatus)
+ }
+ if r.FinishedAt.IsZero() {
+ r.FinishedAt = ts.UTC()
+ }
+ if r.Initialization.Status == nil || (*r.Initialization.Status == QUEUED_TestWorkflowStepStatus) || (*r.Initialization.Status == RUNNING_TestWorkflowStepStatus) {
+ r.Initialization.Status = common.Ptr(FAILED_TestWorkflowStepStatus)
+ if aborted {
+ r.Initialization.Status = common.Ptr(ABORTED_TestWorkflowStepStatus)
+ }
+ r.Initialization.FinishedAt = r.FinishedAt
+ }
+ for i := range r.Steps {
+ if r.Steps[i].Status == nil || (*r.Steps[i].Status == QUEUED_TestWorkflowStepStatus) {
+ s := r.Steps[i]
+ s.Status = common.Ptr(SKIPPED_TestWorkflowStepStatus)
+ r.Steps[i] = s
+ } else if *r.Steps[i].Status == RUNNING_TestWorkflowStepStatus {
+ s := r.Steps[i]
+ s.Status = common.Ptr(FAILED_TestWorkflowStepStatus)
+ if aborted {
+ s.Status = common.Ptr(ABORTED_TestWorkflowStepStatus)
+ }
+ r.Steps[i] = s
+ }
+ }
+ r.RecomputeDuration()
+}
+
+func (r *TestWorkflowResult) Clone() *TestWorkflowResult {
+ if r == nil {
+ return nil
+ }
+ steps := make(map[string]TestWorkflowStepResult, len(r.Steps))
+ for k, v := range r.Steps {
+ steps[k] = *v.Clone()
+ }
+ return &TestWorkflowResult{
+ Status: r.Status,
+ PredictedStatus: r.PredictedStatus,
+ QueuedAt: r.QueuedAt,
+ StartedAt: r.StartedAt,
+ FinishedAt: r.FinishedAt,
+ Duration: r.Duration,
+ DurationMs: r.DurationMs,
+ Initialization: r.Initialization.Clone(),
+ Steps: steps,
+ }
+}
+
+func getTestWorkflowStepStatus(result TestWorkflowStepResult) TestWorkflowStepStatus {
+ if result.Status == nil {
+ return QUEUED_TestWorkflowStepStatus
+ }
+ return *result.Status
+}
+
+func (r *TestWorkflowResult) UpdateStepResult(sig []TestWorkflowSignature, ref string, result TestWorkflowStepResult, scheduledAt time.Time) TestWorkflowStepResult {
+ v := r.Steps[ref]
+ v.Merge(result)
+ r.Steps[ref] = v
+ r.Recompute(sig, scheduledAt)
+ return v
+}
+
+func (r *TestWorkflowResult) RecomputeDuration() {
+ if !r.FinishedAt.IsZero() {
+ r.Duration = r.FinishedAt.Sub(r.QueuedAt).Round(time.Millisecond).String()
+ r.DurationMs = int32(r.FinishedAt.Sub(r.QueuedAt).Milliseconds())
+ }
+}
+
+func (r *TestWorkflowResult) Recompute(sig []TestWorkflowSignature, scheduledAt time.Time) {
+ // Recompute steps
+ for _, ch := range sig {
+ r.RecomputeStep(ch)
+ }
+
+ // Compute the duration
+ r.RecomputeDuration()
+
+ // Build status on the internal failure
+ if getTestWorkflowStepStatus(*r.Initialization) == ABORTED_TestWorkflowStepStatus {
+ r.Status = common.Ptr(ABORTED_TestWorkflowStatus)
+ r.PredictedStatus = r.Status
+ return
+ } else if getTestWorkflowStepStatus(*r.Initialization) == FAILED_TestWorkflowStepStatus {
+ r.Status = common.Ptr(FAILED_TestWorkflowStatus)
+ r.PredictedStatus = r.Status
+ return
+ }
+
+ // Calibrate the execution time initially
+ r.QueuedAt = adjustMinimumTime(r.QueuedAt, scheduledAt)
+ r.StartedAt = adjustMinimumTime(r.StartedAt, r.QueuedAt)
+ r.FinishedAt = adjustMinimumTime(r.FinishedAt, r.StartedAt)
+ initialDate := r.StartedAt
+ if initialDate.IsZero() {
+ initialDate = r.QueuedAt
+ }
+
+ // Calibrate the initialization step
+ if r.Initialization != nil {
+ r.Initialization.QueuedAt = adjustMinimumTime(r.Initialization.QueuedAt, initialDate)
+ r.Initialization.StartedAt = adjustMinimumTime(r.Initialization.StartedAt, r.Initialization.QueuedAt)
+ r.Initialization.FinishedAt = adjustMinimumTime(r.Initialization.FinishedAt, r.Initialization.StartedAt)
+ initialDate = getLastDate(*r.Initialization, initialDate)
+ }
+
+ // Prepare sequential list of container steps
+ type step struct {
+ ref string
+ result TestWorkflowStepResult
+ }
+ seq := make([]step, 0)
+ walkSteps(sig, func(s TestWorkflowSignature) {
+ if len(s.Children) > 0 {
+ return
+ }
+ seq = append(seq, step{ref: s.Ref, result: r.Steps[s.Ref]})
+ })
+
+ // Calibrate the clock for container steps
+ for i := 0; i < len(seq); i++ {
+ if i != 0 {
+ initialDate = getLastDate(seq[i-1].result, initialDate)
+ }
+ seq[i].result.QueuedAt = initialDate
+ seq[i].result.StartedAt = adjustMinimumTime(seq[i].result.StartedAt, seq[i].result.QueuedAt)
+ seq[i].result.FinishedAt = adjustMinimumTime(seq[i].result.FinishedAt, seq[i].result.StartedAt)
+ }
+ for _, s := range seq {
+ r.Steps[s.ref] = s.result
+ }
+
+ // Calibrate the clock for group steps
+ walkSteps(sig, func(s TestWorkflowSignature) {
+ if len(s.Children) == 0 {
+ return
+ }
+ first := getFirstContainer(s.Children)
+ last := getLastContainer(s.Children)
+ if first == nil || last == nil {
+ return
+ }
+ res := r.Steps[s.Ref]
+ res.QueuedAt = r.Steps[first.Ref].QueuedAt
+ res.StartedAt = r.Steps[first.Ref].StartedAt
+ res.FinishedAt = r.Steps[last.Ref].FinishedAt
+ r.Steps[s.Ref] = res
+ })
+
+ // Calibrate execution clock
+ if r.Initialization != nil {
+ if r.Initialization.QueuedAt.Before(r.QueuedAt) {
+ r.QueuedAt = r.Initialization.QueuedAt
+ }
+ if r.Initialization.StartedAt.Before(r.StartedAt) {
+ r.StartedAt = r.Initialization.StartedAt
+ }
+ }
+ last := r.Steps[sig[len(sig)-1].Ref]
+ r.FinishedAt = adjustMinimumTime(r.FinishedAt, last.FinishedAt)
+
+ // Recompute the TestWorkflow status
+ totalSig := TestWorkflowSignature{Children: sig}
+ result, _ := predictTestWorkflowStepStatus(TestWorkflowStepResult{}, totalSig, r)
+ status := common.Ptr(FAILED_TestWorkflowStatus)
+ switch result {
+ case ABORTED_TestWorkflowStepStatus:
+ status = common.Ptr(ABORTED_TestWorkflowStatus)
+ case PASSED_TestWorkflowStepStatus, SKIPPED_TestWorkflowStepStatus:
+ status = common.Ptr(PASSED_TestWorkflowStatus)
+ }
+ r.PredictedStatus = status
+ if !r.FinishedAt.IsZero() || *status == ABORTED_TestWorkflowStatus {
+ r.Status = r.PredictedStatus
+ }
+}
+
+func (r *TestWorkflowResult) RecomputeStep(sig TestWorkflowSignature) {
+ children := sig.Children
+ if len(children) == 0 {
+ return
+ }
+
+ // Compute nested steps
+ for _, ch := range children {
+ r.RecomputeStep(ch)
+ }
+
+ // Simplify accessing value
+ v := r.Steps[sig.Ref]
+ defer func() {
+ r.Steps[sig.Ref] = v
+ }()
+
+ // Compute time
+ v = recomputeTestWorkflowStepResult(v, sig, r)
+}
+
+func walkSteps(sig []TestWorkflowSignature, fn func(signature TestWorkflowSignature)) {
+ for _, s := range sig {
+ walkSteps(s.Children, fn)
+ fn(s)
+ }
+}
+
+func getFirstContainer(sig []TestWorkflowSignature) *TestWorkflowSignature {
+ for i := 0; i < len(sig); i++ {
+ s := sig[i]
+ if len(s.Children) == 0 {
+ return &s
+ }
+ c := getFirstContainer(s.Children)
+ if c != nil {
+ return c
+ }
+ }
+ return nil
+}
+
+func getLastContainer(sig []TestWorkflowSignature) *TestWorkflowSignature {
+ for i := len(sig) - 1; i >= 0; i-- {
+ s := sig[i]
+ if len(s.Children) == 0 {
+ return &s
+ }
+ c := getLastContainer(s.Children)
+ if c != nil {
+ return c
+ }
+ }
+ return nil
+}
+
+func getLastDate(r TestWorkflowStepResult, def time.Time) time.Time {
+ if !r.FinishedAt.IsZero() {
+ return r.FinishedAt
+ }
+ if !r.StartedAt.IsZero() {
+ return r.StartedAt
+ }
+ if !r.QueuedAt.IsZero() {
+ return r.QueuedAt
+ }
+ return def
+}
+
+func adjustMinimumTime(dst, min time.Time) time.Time {
+ if dst.IsZero() {
+ return dst
+ }
+ if min.After(dst) {
+ return min
+ }
+ return dst
+}
+
+func predictTestWorkflowStepStatus(v TestWorkflowStepResult, sig TestWorkflowSignature, r *TestWorkflowResult) (TestWorkflowStepStatus, bool) {
+ children := sig.Children
+ if len(children) == 0 {
+ if getTestWorkflowStepStatus(v) == QUEUED_TestWorkflowStepStatus || getTestWorkflowStepStatus(v) == RUNNING_TestWorkflowStepStatus {
+ return PASSED_TestWorkflowStepStatus, false
+ }
+ return *v.Status, true
+ }
+
+ // Compute the status
+ skipped := true
+ aborted := false
+ failed := false
+ finished := true
+ for _, ch := range children {
+ status := getTestWorkflowStepStatus(r.Steps[ch.Ref])
+ if status != SKIPPED_TestWorkflowStepStatus {
+ skipped = false
+ }
+ if status == ABORTED_TestWorkflowStepStatus {
+ aborted = true
+ }
+ if !ch.Optional && (status == FAILED_TestWorkflowStepStatus || status == TIMEOUT_TestWorkflowStepStatus) {
+ failed = true
+ }
+ if status == QUEUED_TestWorkflowStepStatus || status == RUNNING_TestWorkflowStepStatus {
+ finished = false
+ }
+ }
+
+ if getTestWorkflowStepStatus(v) == FAILED_TestWorkflowStepStatus {
+ return FAILED_TestWorkflowStepStatus, finished
+ } else if aborted {
+ return ABORTED_TestWorkflowStepStatus, finished
+ } else if (failed && !sig.Negative) || (!failed && sig.Negative) {
+ return FAILED_TestWorkflowStepStatus, finished
+ } else if skipped {
+ return SKIPPED_TestWorkflowStepStatus, finished
+ } else {
+ return PASSED_TestWorkflowStepStatus, finished
+ }
+}
+
+func recomputeTestWorkflowStepResult(v TestWorkflowStepResult, sig TestWorkflowSignature, r *TestWorkflowResult) TestWorkflowStepResult {
+ children := sig.Children
+ if len(children) == 0 {
+ return v
+ }
+
+ // Compute nested steps
+ for _, ch := range children {
+ r.RecomputeStep(ch)
+ }
+
+ // Compute time
+ v.QueuedAt = r.Steps[children[0].Ref].QueuedAt
+ v.StartedAt = r.Steps[children[0].Ref].StartedAt
+ v.FinishedAt = r.Steps[children[len(children)-1].Ref].StartedAt
+
+ // It has been already marked as failed internally from some step below
+ if getTestWorkflowStepStatus(v) == FAILED_TestWorkflowStepStatus {
+ return v
+ }
+
+ // It is finished already
+ if !v.FinishedAt.IsZero() {
+ predicted, finished := predictTestWorkflowStepStatus(v, sig, r)
+ if finished {
+ v.Status = common.Ptr(predicted)
+ }
+ return v
+ }
+
+ if !v.StartedAt.IsZero() {
+ v.Status = common.Ptr(RUNNING_TestWorkflowStepStatus)
+ }
+
+ return v
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_result_summary.go b/pkg/api/v1/testkube/model_test_workflow_result_summary.go
new file mode 100644
index 00000000000..7311584fec8
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_result_summary.go
@@ -0,0 +1,29 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+import (
+ "time"
+)
+
+type TestWorkflowResultSummary struct {
+ Status *TestWorkflowStatus `json:"status"`
+ PredictedStatus *TestWorkflowStatus `json:"predictedStatus"`
+ // when the pod was created
+ QueuedAt time.Time `json:"queuedAt,omitempty"`
+ // when the pod has been successfully assigned
+ StartedAt time.Time `json:"startedAt,omitempty"`
+ // when the pod has been completed
+ FinishedAt time.Time `json:"finishedAt,omitempty"`
+ // Go-formatted (human-readable) duration
+ Duration string `json:"duration,omitempty"`
+ // Duration in milliseconds
+ DurationMs int32 `json:"durationMs,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_retry_policy.go b/pkg/api/v1/testkube/model_test_workflow_retry_policy.go
new file mode 100644
index 00000000000..93c16008b32
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_retry_policy.go
@@ -0,0 +1,17 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowRetryPolicy struct {
+ // how many times at most it should retry
+ Count int32 `json:"count"`
+ // until when it should retry (defaults to \"passed\")
+ Until string `json:"until,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_signature.go b/pkg/api/v1/testkube/model_test_workflow_signature.go
new file mode 100644
index 00000000000..8fd25943e29
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_signature.go
@@ -0,0 +1,24 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowSignature struct {
+ // step reference
+ Ref string `json:"ref,omitempty"`
+ // step name
+ Name string `json:"name,omitempty"`
+ // step category, that may be used as name fallback
+ Category string `json:"category,omitempty"`
+ // is the step/group meant to be optional
+ Optional bool `json:"optional,omitempty"`
+ // is the step/group meant to be negative
+ Negative bool `json:"negative,omitempty"`
+ Children []TestWorkflowSignature `json:"children,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_spec.go b/pkg/api/v1/testkube/model_test_workflow_spec.go
new file mode 100644
index 00000000000..8d051514dca
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_spec.go
@@ -0,0 +1,22 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowSpec struct {
+ Use []TestWorkflowTemplateRef `json:"use,omitempty"`
+ Config map[string]TestWorkflowParameterSchema `json:"config,omitempty"`
+ Content *TestWorkflowContent `json:"content,omitempty"`
+ Container *TestWorkflowContainerConfig `json:"container,omitempty"`
+ Job *TestWorkflowJobConfig `json:"job,omitempty"`
+ Pod *TestWorkflowPodConfig `json:"pod,omitempty"`
+ Setup []TestWorkflowStep `json:"setup,omitempty"`
+ Steps []TestWorkflowStep `json:"steps,omitempty"`
+ After []TestWorkflowStep `json:"after,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_status.go b/pkg/api/v1/testkube/model_test_workflow_status.go
new file mode 100644
index 00000000000..b0c655faec6
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_status.go
@@ -0,0 +1,21 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowStatus string
+
+// List of TestWorkflowStatus
+const (
+ QUEUED_TestWorkflowStatus TestWorkflowStatus = "queued"
+ RUNNING_TestWorkflowStatus TestWorkflowStatus = "running"
+ PASSED_TestWorkflowStatus TestWorkflowStatus = "passed"
+ FAILED_TestWorkflowStatus TestWorkflowStatus = "failed"
+ ABORTED_TestWorkflowStatus TestWorkflowStatus = "aborted"
+)
diff --git a/pkg/api/v1/testkube/model_test_workflow_status_extended.go b/pkg/api/v1/testkube/model_test_workflow_status_extended.go
new file mode 100644
index 00000000000..d56ba0d1e1c
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_status_extended.go
@@ -0,0 +1,46 @@
+package testkube
+
+import (
+ "fmt"
+ "strings"
+)
+
+// TestWorkflowStatuses is an array of TestWorkflowStatus
+type TestWorkflowStatuses []TestWorkflowStatus
+
+// ToMap generates map from TestWorkflowStatuses
+func (statuses TestWorkflowStatuses) ToMap() map[TestWorkflowStatus]struct{} {
+ statusMap := map[TestWorkflowStatus]struct{}{}
+ for _, status := range statuses {
+ statusMap[status] = struct{}{}
+ }
+
+ return statusMap
+}
+
+// ParseTestWorkflowStatusList parse a list of workflow execution statuses from string
+func ParseTestWorkflowStatusList(source, separator string) (statusList TestWorkflowStatuses, err error) {
+ statusMap := map[TestWorkflowStatus]struct{}{
+ ABORTED_TestWorkflowStatus: {},
+ FAILED_TestWorkflowStatus: {},
+ PASSED_TestWorkflowStatus: {},
+ QUEUED_TestWorkflowStatus: {},
+ RUNNING_TestWorkflowStatus: {},
+ }
+
+ if source == "" {
+ return nil, nil
+ }
+
+ values := strings.Split(source, separator)
+ for _, value := range values {
+ status := TestWorkflowStatus(value)
+ if _, ok := statusMap[status]; ok {
+ statusList = append(statusList, status)
+ } else {
+ return nil, fmt.Errorf("unknown test workflow execution status %v", status)
+ }
+ }
+
+ return statusList, nil
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_step.go b/pkg/api/v1/testkube/model_test_workflow_step.go
new file mode 100644
index 00000000000..31a404c9f33
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_step.go
@@ -0,0 +1,41 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowStep struct {
+ // readable name for the step
+ Name string `json:"name,omitempty"`
+ // expression to declare under which conditions the step should be run; defaults to \"passed\", except artifacts where it defaults to \"always\"
+ Condition string `json:"condition,omitempty"`
+ // is the step expected to fail
+ Negative bool `json:"negative,omitempty"`
+ // is the step optional, so the failure won't affect the TestWorkflow result
+ Optional bool `json:"optional,omitempty"`
+ // list of TestWorkflowTemplates to use
+ Use []TestWorkflowTemplateRef `json:"use,omitempty"`
+ Template *TestWorkflowTemplateRef `json:"template,omitempty"`
+ Retry *TestWorkflowRetryPolicy `json:"retry,omitempty"`
+ // maximum time this step may take
+ Timeout string `json:"timeout,omitempty"`
+ // delay before the step
+ Delay string `json:"delay,omitempty"`
+ Content *TestWorkflowContent `json:"content,omitempty"`
+ // script to run in a default shell for the container
+ Shell string `json:"shell,omitempty"`
+ Run *TestWorkflowContainerConfig `json:"run,omitempty"`
+ WorkingDir *BoxedString `json:"workingDir,omitempty"`
+ Container *TestWorkflowContainerConfig `json:"container,omitempty"`
+ Execute *TestWorkflowStepExecute `json:"execute,omitempty"`
+ Artifacts *TestWorkflowStepArtifacts `json:"artifacts,omitempty"`
+ // nested setup steps to run
+ Setup []TestWorkflowStep `json:"setup,omitempty"`
+ // nested steps to run
+ Steps []TestWorkflowStep `json:"steps,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_step_artifacts.go b/pkg/api/v1/testkube/model_test_workflow_step_artifacts.go
new file mode 100644
index 00000000000..4897d0cae0a
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_step_artifacts.go
@@ -0,0 +1,17 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowStepArtifacts struct {
+ WorkingDir *BoxedString `json:"workingDir,omitempty"`
+ Compress *TestWorkflowStepArtifactsCompression `json:"compress,omitempty"`
+ // file paths to fetch from the container
+ Paths []string `json:"paths"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_step_artifacts_compression.go b/pkg/api/v1/testkube/model_test_workflow_step_artifacts_compression.go
new file mode 100644
index 00000000000..e0dc9a7e171
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_step_artifacts_compression.go
@@ -0,0 +1,15 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowStepArtifactsCompression struct {
+ // artifact name
+ Name string `json:"name,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_step_execute.go b/pkg/api/v1/testkube/model_test_workflow_step_execute.go
new file mode 100644
index 00000000000..1fe93dcb222
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_step_execute.go
@@ -0,0 +1,21 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowStepExecute struct {
+ // how many resources could be scheduled in parallel
+ Parallelism int32 `json:"parallelism,omitempty"`
+ // only schedule the resources, don't watch for the results (unless it is needed for parallelism)
+ Async bool `json:"async,omitempty"`
+ // tests to schedule
+ Tests []TestWorkflowStepExecuteTestRef `json:"tests,omitempty"`
+ // workflows to schedule
+ Workflows []TestWorkflowRef `json:"workflows,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_step_execute_test_ref.go b/pkg/api/v1/testkube/model_test_workflow_step_execute_test_ref.go
new file mode 100644
index 00000000000..a31c446d497
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_step_execute_test_ref.go
@@ -0,0 +1,15 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowStepExecuteTestRef struct {
+ // test name to schedule
+ Name string `json:"name,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_step_extended.go b/pkg/api/v1/testkube/model_test_workflow_step_extended.go
new file mode 100644
index 00000000000..daf74da4b29
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_step_extended.go
@@ -0,0 +1,29 @@
+package testkube
+
+import "github.com/kubeshop/testkube/pkg/utils"
+
+func (w *TestWorkflowStep) ConvertDots(fn func(string) string) *TestWorkflowStep {
+ if w == nil {
+ return w
+ }
+ for i := range w.Use {
+ if w.Use[i].Config != nil {
+ w.Use[i].Config = convertDotsInMap(w.Use[i].Config, fn)
+ }
+ }
+ if w.Template != nil && w.Template.Config != nil {
+ w.Template.Config = convertDotsInMap(w.Template.Config, fn)
+ }
+ for i := range w.Steps {
+ w.Steps[i].ConvertDots(fn)
+ }
+ return w
+}
+
+func (w *TestWorkflowStep) EscapeDots() *TestWorkflowStep {
+ return w.ConvertDots(utils.EscapeDots)
+}
+
+func (w *TestWorkflowStep) UnscapeDots() *TestWorkflowStep {
+ return w.ConvertDots(utils.UnescapeDots)
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_step_result.go b/pkg/api/v1/testkube/model_test_workflow_step_result.go
new file mode 100644
index 00000000000..021ee2c8230
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_step_result.go
@@ -0,0 +1,26 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+import (
+ "time"
+)
+
+type TestWorkflowStepResult struct {
+ ErrorMessage string `json:"errorMessage,omitempty"`
+ Status *TestWorkflowStepStatus `json:"status,omitempty"`
+ ExitCode float64 `json:"exitCode,omitempty"`
+ // when the container was created
+ QueuedAt time.Time `json:"queuedAt,omitempty"`
+ // when the container was started
+ StartedAt time.Time `json:"startedAt,omitempty"`
+ // when the container was finished
+ FinishedAt time.Time `json:"finishedAt,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_step_result_extended.go b/pkg/api/v1/testkube/model_test_workflow_step_result_extended.go
new file mode 100644
index 00000000000..e207318ce35
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_step_result_extended.go
@@ -0,0 +1,36 @@
+package testkube
+
+func (r *TestWorkflowStepResult) Clone() *TestWorkflowStepResult {
+ if r == nil {
+ return nil
+ }
+ return &TestWorkflowStepResult{
+ ErrorMessage: r.ErrorMessage,
+ Status: r.Status,
+ ExitCode: r.ExitCode,
+ QueuedAt: r.QueuedAt,
+ StartedAt: r.StartedAt,
+ FinishedAt: r.FinishedAt,
+ }
+}
+
+func (r *TestWorkflowStepResult) Merge(next TestWorkflowStepResult) {
+ if next.ErrorMessage != "" {
+ r.ErrorMessage = next.ErrorMessage
+ }
+ if next.Status != nil {
+ r.Status = next.Status
+ }
+ if next.ExitCode != 0 && (r.ExitCode == 0 || r.ExitCode == -1) {
+ r.ExitCode = next.ExitCode
+ }
+ if !next.QueuedAt.IsZero() {
+ r.QueuedAt = next.QueuedAt
+ }
+ if !next.StartedAt.IsZero() {
+ r.StartedAt = next.StartedAt
+ }
+ if !next.FinishedAt.IsZero() {
+ r.FinishedAt = next.FinishedAt
+ }
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_step_status.go b/pkg/api/v1/testkube/model_test_workflow_step_status.go
new file mode 100644
index 00000000000..1c70bbb1efd
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_step_status.go
@@ -0,0 +1,23 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowStepStatus string
+
+// List of TestWorkflowStepStatus
+const (
+ QUEUED_TestWorkflowStepStatus TestWorkflowStepStatus = "queued"
+ RUNNING_TestWorkflowStepStatus TestWorkflowStepStatus = "running"
+ PASSED_TestWorkflowStepStatus TestWorkflowStepStatus = "passed"
+ FAILED_TestWorkflowStepStatus TestWorkflowStepStatus = "failed"
+ TIMEOUT_TestWorkflowStepStatus TestWorkflowStepStatus = "timeout"
+ SKIPPED_TestWorkflowStepStatus TestWorkflowStepStatus = "skipped"
+ ABORTED_TestWorkflowStepStatus TestWorkflowStepStatus = "aborted"
+)
diff --git a/pkg/api/v1/testkube/model_test_workflow_summary.go b/pkg/api/v1/testkube/model_test_workflow_summary.go
new file mode 100644
index 00000000000..751bca8fac1
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_summary.go
@@ -0,0 +1,17 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowSummary struct {
+ Name string `json:"name,omitempty"`
+ Namespace string `json:"namespace,omitempty"`
+ Labels map[string]string `json:"labels,omitempty"`
+ Annotations map[string]string `json:"annotations,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_summary_extended.go b/pkg/api/v1/testkube/model_test_workflow_summary_extended.go
new file mode 100644
index 00000000000..0a47f30a5e2
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_summary_extended.go
@@ -0,0 +1,33 @@
+package testkube
+
+import (
+ "github.com/kubeshop/testkube/pkg/utils"
+)
+
+func (w *TestWorkflowSummary) ConvertDots(fn func(string) string) *TestWorkflowSummary {
+ if w == nil || w.Labels == nil {
+ return w
+ }
+ if w.Labels != nil {
+ w.Labels = convertDotsInMap(w.Labels, fn)
+ }
+ return w
+}
+
+func (w *TestWorkflowSummary) EscapeDots() *TestWorkflowSummary {
+ return w.ConvertDots(utils.EscapeDots)
+}
+
+func (w *TestWorkflowSummary) UnscapeDots() *TestWorkflowSummary {
+ return w.ConvertDots(utils.UnescapeDots)
+}
+
+func (w *TestWorkflowSummary) GetObjectRef() *ObjectRef {
+ return &ObjectRef{
+ Name: w.Name,
+ Namespace: w.Namespace,
+ }
+}
+
+func (w *TestWorkflowSummary) QuoteWorkflowTextFields() {
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_template.go b/pkg/api/v1/testkube/model_test_workflow_template.go
new file mode 100644
index 00000000000..b0fb09424a8
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_template.go
@@ -0,0 +1,29 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+import (
+ "time"
+)
+
+type TestWorkflowTemplate struct {
+ // kubernetes resource name
+ Name string `json:"name,omitempty"`
+ // kubernetes namespace
+ Namespace string `json:"namespace,omitempty"`
+ // human-readable description
+ Description string `json:"description,omitempty"`
+ // test workflow labels
+ Labels map[string]string `json:"labels,omitempty"`
+ // test workflow annotations
+ Annotations map[string]string `json:"annotations,omitempty"`
+ Created time.Time `json:"created,omitempty"`
+ Spec *TestWorkflowTemplateSpec `json:"spec,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_template_extended.go b/pkg/api/v1/testkube/model_test_workflow_template_extended.go
new file mode 100644
index 00000000000..601c6addc0c
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_template_extended.go
@@ -0,0 +1,19 @@
+package testkube
+
+import "strings"
+
+type TestWorkflowTemplates []TestWorkflowTemplate
+
+func (t TestWorkflowTemplates) Table() (header []string, output [][]string) {
+ header = []string{"Name", "Description", "Created", "Labels"}
+ for _, e := range t {
+ output = append(output, []string{
+ strings.ReplaceAll(e.Name, "--", "/"),
+ e.Description,
+ e.Created.String(),
+ MapToString(e.Labels),
+ })
+ }
+
+ return
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_template_ref.go b/pkg/api/v1/testkube/model_test_workflow_template_ref.go
new file mode 100644
index 00000000000..ef405df3932
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_template_ref.go
@@ -0,0 +1,16 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowTemplateRef struct {
+ // TestWorkflowTemplate name to include
+ Name string `json:"name"`
+ Config map[string]string `json:"config,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_template_spec.go b/pkg/api/v1/testkube/model_test_workflow_template_spec.go
new file mode 100644
index 00000000000..97c560da6ec
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_template_spec.go
@@ -0,0 +1,21 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowTemplateSpec struct {
+ Config map[string]TestWorkflowParameterSchema `json:"config,omitempty"`
+ Content *TestWorkflowContent `json:"content,omitempty"`
+ Container *TestWorkflowContainerConfig `json:"container,omitempty"`
+ Job *TestWorkflowJobConfig `json:"job,omitempty"`
+ Pod *TestWorkflowPodConfig `json:"pod,omitempty"`
+ Setup []TestWorkflowIndependentStep `json:"setup,omitempty"`
+ Steps []TestWorkflowIndependentStep `json:"steps,omitempty"`
+ After []TestWorkflowIndependentStep `json:"after,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_with_execution.go b/pkg/api/v1/testkube/model_test_workflow_with_execution.go
new file mode 100644
index 00000000000..736b4560c77
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_with_execution.go
@@ -0,0 +1,15 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowWithExecution struct {
+ Workflow *TestWorkflow `json:"workflow,omitempty"`
+ LatestExecution *TestWorkflowExecution `json:"latestExecution,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_with_execution_extended.go b/pkg/api/v1/testkube/model_test_workflow_with_execution_extended.go
new file mode 100644
index 00000000000..1ec43935143
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_with_execution_extended.go
@@ -0,0 +1,28 @@
+package testkube
+
+type TestWorkflowWithExecutions []TestWorkflowWithExecution
+
+func (t TestWorkflowWithExecutions) Table() (header []string, output [][]string) {
+ header = []string{"Name", "Description", "Created", "Labels", "Status", "Execution ID"}
+ for _, e := range t {
+ status := ""
+ executionID := ""
+ if e.LatestExecution != nil {
+ executionID = e.LatestExecution.Id
+ if e.LatestExecution.Result != nil && e.LatestExecution.Result.Status != nil {
+ status = string(*e.LatestExecution.Result.Status)
+ }
+ }
+
+ output = append(output, []string{
+ e.Workflow.Name,
+ e.Workflow.Description,
+ e.Workflow.Created.String(),
+ MapToString(e.Workflow.Labels),
+ status,
+ executionID,
+ })
+ }
+
+ return
+}
diff --git a/pkg/api/v1/testkube/model_test_workflow_with_execution_summary.go b/pkg/api/v1/testkube/model_test_workflow_with_execution_summary.go
new file mode 100644
index 00000000000..ce1c8646aad
--- /dev/null
+++ b/pkg/api/v1/testkube/model_test_workflow_with_execution_summary.go
@@ -0,0 +1,15 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type TestWorkflowWithExecutionSummary struct {
+ Workflow *TestWorkflow `json:"workflow,omitempty"`
+ LatestExecution *TestWorkflowExecutionSummary `json:"latestExecution,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_volume.go b/pkg/api/v1/testkube/model_volume.go
new file mode 100644
index 00000000000..bf6fb1bae89
--- /dev/null
+++ b/pkg/api/v1/testkube/model_volume.go
@@ -0,0 +1,26 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// Volume represents a named volume in a pod that may be accessed by any container in the pod.
+type Volume struct {
+ Name string `json:"name"`
+ HostPath *HostPathVolumeSource `json:"hostPath,omitempty"`
+ EmptyDir *EmptyDirVolumeSource `json:"emptyDir,omitempty"`
+ GcePersistentDisk *GcePersistentDiskVolumeSource `json:"gcePersistentDisk,omitempty"`
+ AwsElasticBlockStore *AwsElasticBlockStoreVolumeSource `json:"awsElasticBlockStore,omitempty"`
+ Secret *SecretVolumeSource `json:"secret,omitempty"`
+ Nfs *NfsVolumeSource `json:"nfs,omitempty"`
+ PersistentVolumeClaim *PersistentVolumeClaimVolumeSource `json:"persistentVolumeClaim,omitempty"`
+ Cephfs *CephFsVolumeSource `json:"cephfs,omitempty"`
+ AzureFile *AzureFileVolumeSource `json:"azureFile,omitempty"`
+ AzureDisk *AzureDiskVolumeSource `json:"azureDisk,omitempty"`
+ ConfigMap *ConfigMapVolumeSource `json:"configMap,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_volume_mount.go b/pkg/api/v1/testkube/model_volume_mount.go
new file mode 100644
index 00000000000..2aa74c5913e
--- /dev/null
+++ b/pkg/api/v1/testkube/model_volume_mount.go
@@ -0,0 +1,25 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+// VolumeMount describes a mounting of a Volume within a container.
+type VolumeMount struct {
+ // Path within the container at which the volume should be mounted. Must not contain ':'.
+ MountPath string `json:"mountPath"`
+ MountPropagation *BoxedString `json:"mountPropagation,omitempty"`
+ // This must match the Name of a Volume.
+ Name string `json:"name"`
+ // Mounted read-only if true, read-write otherwise (false or unspecified). Defaults to false.
+ ReadOnly bool `json:"readOnly,omitempty"`
+ // Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root).
+ SubPath string `json:"subPath,omitempty"`
+ // Expanded path within the volume from which the container's volume should be mounted. Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. Defaults to \"\" (volume's root). SubPathExpr and SubPath are mutually exclusive.
+ SubPathExpr string `json:"subPathExpr,omitempty"`
+}
diff --git a/pkg/api/v1/testkube/model_volume_source.go b/pkg/api/v1/testkube/model_volume_source.go
new file mode 100644
index 00000000000..c7793075834
--- /dev/null
+++ b/pkg/api/v1/testkube/model_volume_source.go
@@ -0,0 +1,13 @@
+/*
+ * Testkube API
+ *
+ * Testkube provides a Kubernetes-native framework for test definition, execution and results
+ *
+ * API version: 1.0.0
+ * Contact: testkube@kubeshop.io
+ * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
+ */
+package testkube
+
+type VolumeSource struct {
+}
diff --git a/pkg/cloud/data/artifact/artifacts_storage.go b/pkg/cloud/data/artifact/artifacts_storage.go
index d29f94dacf3..a6410a86eb3 100644
--- a/pkg/cloud/data/artifact/artifacts_storage.go
+++ b/pkg/cloud/data/artifact/artifacts_storage.go
@@ -1,7 +1,7 @@
package artifact
import (
- context "context"
+ "context"
"encoding/json"
"io"
"net/http"
@@ -25,11 +25,12 @@ func NewCloudArtifactsStorage(cloudClient cloud.TestKubeCloudAPIClient, grpcConn
return &CloudArtifactsStorage{executor: executor.NewCloudGRPCExecutor(cloudClient, grpcConn, apiKey)}
}
-func (c *CloudArtifactsStorage) ListFiles(ctx context.Context, executionID, testName, testSuiteName string) ([]testkube.Artifact, error) {
+func (c *CloudArtifactsStorage) ListFiles(ctx context.Context, executionID, testName, testSuiteName, testWorkflowName string) ([]testkube.Artifact, error) {
req := ListFilesRequest{
- ExecutionID: executionID,
- TestName: testName,
- TestSuiteName: testSuiteName,
+ ExecutionID: executionID,
+ TestName: testName,
+ TestSuiteName: testSuiteName,
+ TestWorkflowName: testWorkflowName,
}
response, err := c.executor.Execute(ctx, CmdArtifactsListFiles, req)
if err != nil {
@@ -43,12 +44,13 @@ func (c *CloudArtifactsStorage) ListFiles(ctx context.Context, executionID, test
return commandResponse.Artifacts, nil
}
-func (c *CloudArtifactsStorage) DownloadFile(ctx context.Context, file, executionID, testName, testSuiteName string) (io.Reader, error) {
+func (c *CloudArtifactsStorage) DownloadFile(ctx context.Context, file, executionID, testName, testSuiteName, testWorkflowName string) (io.Reader, error) {
req := DownloadFileRequest{
- File: file,
- ExecutionID: executionID,
- TestName: testName,
- TestSuiteName: testSuiteName,
+ File: file,
+ ExecutionID: executionID,
+ TestName: testName,
+ TestSuiteName: testSuiteName,
+ TestWorkflowName: testWorkflowName,
}
response, err := c.executor.Execute(ctx, CmdArtifactsDownloadFile, req)
if err != nil {
diff --git a/pkg/cloud/data/artifact/artifacts_storage_models.go b/pkg/cloud/data/artifact/artifacts_storage_models.go
index 36a9b4cff8f..54ca1661a65 100644
--- a/pkg/cloud/data/artifact/artifacts_storage_models.go
+++ b/pkg/cloud/data/artifact/artifacts_storage_models.go
@@ -3,9 +3,10 @@ package artifact
import "github.com/kubeshop/testkube/pkg/api/v1/testkube"
type ListFilesRequest struct {
- ExecutionID string
- TestName string
- TestSuiteName string
+ ExecutionID string
+ TestName string
+ TestSuiteName string
+ TestWorkflowName string
}
type ListFilesResponse struct {
@@ -13,10 +14,11 @@ type ListFilesResponse struct {
}
type DownloadFileRequest struct {
- File string
- ExecutionID string
- TestName string
- TestSuiteName string
+ File string
+ ExecutionID string
+ TestName string
+ TestSuiteName string
+ TestWorkflowName string
}
type DownloadFileResponse struct {
diff --git a/pkg/cloud/data/artifact/scraper_integration_test.go b/pkg/cloud/data/artifact/scraper_integration_test.go
index 0eda10921bb..8c132aaddef 100644
--- a/pkg/cloud/data/artifact/scraper_integration_test.go
+++ b/pkg/cloud/data/artifact/scraper_integration_test.go
@@ -61,7 +61,7 @@ func TestCloudScraper_ArchiveFilesystemExtractor_Integration(t *testing.T) {
defer testServer.Close()
mockExecutor := executor.NewMockExecutor(mockCtrl)
- cloudLoader := cloudscraper.NewCloudUploader(mockExecutor)
+ cloudLoader := cloudscraper.NewCloudUploader(mockExecutor, false)
req := &cloudscraper.PutObjectSignedURLRequest{
Object: "artifacts.tar.gz",
ExecutionID: "my-execution-id",
@@ -148,7 +148,7 @@ func TestCloudScraper_RecursiveFilesystemExtractor_Integration(t *testing.T) {
defer testServer.Close()
mockExecutor := executor.NewMockExecutor(mockCtrl)
- cloudLoader := cloudscraper.NewCloudUploader(mockExecutor)
+ cloudLoader := cloudscraper.NewCloudUploader(mockExecutor, false)
req1 := &cloudscraper.PutObjectSignedURLRequest{
Object: "file1.txt",
ExecutionID: "my-execution-id",
diff --git a/pkg/cloud/data/artifact/scraper_model.go b/pkg/cloud/data/artifact/scraper_model.go
index 537624c0254..f1b73c9a8f2 100644
--- a/pkg/cloud/data/artifact/scraper_model.go
+++ b/pkg/cloud/data/artifact/scraper_model.go
@@ -7,10 +7,12 @@ const (
)
type PutObjectSignedURLRequest struct {
- Object string `json:"object"`
- ExecutionID string `json:"executionId"`
- TestName string `json:"testName"`
- TestSuiteName string `json:"testSuiteName"`
+ Object string `json:"object"`
+ ContentType string `json:"contentType,omitempty"`
+ ExecutionID string `json:"executionId"`
+ TestName string `json:"testName"`
+ TestSuiteName string `json:"testSuiteName"`
+ TestWorkflowName string `json:"testWorkflowName"`
}
type PutObjectSignedURLResponse struct {
diff --git a/pkg/cloud/data/artifact/uploader.go b/pkg/cloud/data/artifact/uploader.go
index fa1f522de1c..b7a34c70805 100644
--- a/pkg/cloud/data/artifact/uploader.go
+++ b/pkg/cloud/data/artifact/uploader.go
@@ -2,6 +2,7 @@ package artifact
import (
"context"
+ "crypto/tls"
"encoding/json"
"io"
"net/http"
@@ -18,10 +19,12 @@ import (
type CloudUploader struct {
executor executor.Executor
+ // skipVerify is used to skip TLS verification when artifacts
+ skipVerify bool
}
-func NewCloudUploader(executor executor.Executor) *CloudUploader {
- return &CloudUploader{executor: executor}
+func NewCloudUploader(executor executor.Executor, skipVerify bool) *CloudUploader {
+ return &CloudUploader{executor: executor, skipVerify: skipVerify}
}
func (u *CloudUploader) Upload(ctx context.Context, object *scraper.Object, execution testkube.Execution) error {
@@ -63,7 +66,10 @@ func (u *CloudUploader) putObject(ctx context.Context, url string, data io.Reade
return err
}
req.Header.Set("Content-Type", "application/octet-stream")
- rsp, err := http.DefaultClient.Do(req)
+ tr := http.DefaultTransport.(*http.Transport).Clone()
+ tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: u.skipVerify}
+ client := &http.Client{Transport: tr}
+ rsp, err := client.Do(req)
if err != nil {
return errors.Wrap(err, "failed to send file to cloud")
}
diff --git a/pkg/cloud/data/artifact/uploader_test.go b/pkg/cloud/data/artifact/uploader_test.go
index 3b10730ba6f..2a78448920d 100644
--- a/pkg/cloud/data/artifact/uploader_test.go
+++ b/pkg/cloud/data/artifact/uploader_test.go
@@ -60,7 +60,7 @@ func TestCloudLoader_Load(t *testing.T) {
}
mockExecutor.EXPECT().Execute(gomock.Any(), cloudscraper.CmdScraperPutObjectSignedURL, gomock.Eq(req)).Return([]byte(`{"URL":"`+testServer.URL+`/dummy"}`), nil).Times(1)
- return cloudscraper.NewCloudUploader(mockExecutor)
+ return cloudscraper.NewCloudUploader(mockExecutor, false)
},
putErr: nil,
wantErr: false,
@@ -82,7 +82,7 @@ func TestCloudLoader_Load(t *testing.T) {
}
mockExecutor.EXPECT().Execute(gomock.Any(), cloudscraper.CmdScraperPutObjectSignedURL, gomock.Eq(req)).Return(nil, errors.New("connection error")).Times(1)
- return cloudscraper.NewCloudUploader(mockExecutor)
+ return cloudscraper.NewCloudUploader(mockExecutor, false)
},
wantErr: true,
errContains: "failed to get signed URL for object [my-object]: connection error",
diff --git a/pkg/cloud/data/config/commands.go b/pkg/cloud/data/config/commands.go
index b6a2668880b..e4ab0d4bdb5 100644
--- a/pkg/cloud/data/config/commands.go
+++ b/pkg/cloud/data/config/commands.go
@@ -7,4 +7,5 @@ const (
CmdConfigGetTelemetryEnabled executor.Command = "get_telemetry_enabled"
CmdConfigGet executor.Command = "get"
CmdConfigUpsert executor.Command = "upsert"
+ CmdConfigGetOrganizationPlan executor.Command = "get_org_plan"
)
diff --git a/pkg/cloud/data/result/result.go b/pkg/cloud/data/result/result.go
index a0e41f38564..5ee06ad7aaa 100644
--- a/pkg/cloud/data/result/result.go
+++ b/pkg/cloud/data/result/result.go
@@ -42,6 +42,19 @@ func (r *CloudRepository) GetNextExecutionNumber(ctx context.Context, testName s
return commandResponse.TestNumber, nil
}
+func (r *CloudRepository) GetExecution(ctx context.Context, id string) (testkube.Execution, error) {
+ req := GetRequest{ID: id}
+ response, err := r.executor.Execute(ctx, CmdResultGet, req)
+ if err != nil {
+ return testkube.Execution{}, err
+ }
+ var commandResponse GetResponse
+ if err := json.Unmarshal(response, &commandResponse); err != nil {
+ return testkube.Execution{}, err
+ }
+ return commandResponse.Execution, nil
+}
+
func (r *CloudRepository) Get(ctx context.Context, id string) (testkube.Execution, error) {
req := GetRequest{ID: id}
response, err := r.executor.Execute(ctx, CmdResultGet, req)
@@ -316,3 +329,7 @@ func (r *CloudRepository) GetTestMetrics(ctx context.Context, name string, limit
}
return commandResponse.Metrics, nil
}
+
+func (r *CloudRepository) Count(ctx context.Context, filter result.Filter) (int64, error) {
+ return 0, nil
+}
diff --git a/pkg/cloud/data/testresult/testresult.go b/pkg/cloud/data/testresult/testresult.go
index 6a67eb20ed2..37425ca1ba5 100644
--- a/pkg/cloud/data/testresult/testresult.go
+++ b/pkg/cloud/data/testresult/testresult.go
@@ -234,3 +234,7 @@ func (r *CloudRepository) GetTestSuiteMetrics(ctx context.Context, name string,
}
return commandResponse.Metrics, nil
}
+
+func (r *CloudRepository) Count(ctx context.Context, filter testresult.Filter) (int64, error) {
+ return 0, nil
+}
diff --git a/pkg/cloud/service.pb.go b/pkg/cloud/service.pb.go
index 6e2ef74812d..0d8b4a7d29d 100644
--- a/pkg/cloud/service.pb.go
+++ b/pkg/cloud/service.pb.go
@@ -1,19 +1,18 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
-// protoc-gen-go v1.27.1
+// protoc-gen-go v1.28.1
// protoc v3.19.4
// source: proto/service.proto
package cloud
import (
- reflect "reflect"
- sync "sync"
-
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
structpb "google.golang.org/protobuf/types/known/structpb"
+ reflect "reflect"
+ sync "sync"
)
const (
@@ -69,6 +68,104 @@ func (LogsStreamRequestType) EnumDescriptor() ([]byte, []int) {
return file_proto_service_proto_rawDescGZIP(), []int{0}
}
+type TestWorkflowNotificationsRequestType int32
+
+const (
+ TestWorkflowNotificationsRequestType_WORKFLOW_STREAM_LOG_MESSAGE TestWorkflowNotificationsRequestType = 0
+ TestWorkflowNotificationsRequestType_WORKFLOW_STREAM_HEALTH_CHECK TestWorkflowNotificationsRequestType = 1
+)
+
+// Enum value maps for TestWorkflowNotificationsRequestType.
+var (
+ TestWorkflowNotificationsRequestType_name = map[int32]string{
+ 0: "WORKFLOW_STREAM_LOG_MESSAGE",
+ 1: "WORKFLOW_STREAM_HEALTH_CHECK",
+ }
+ TestWorkflowNotificationsRequestType_value = map[string]int32{
+ "WORKFLOW_STREAM_LOG_MESSAGE": 0,
+ "WORKFLOW_STREAM_HEALTH_CHECK": 1,
+ }
+)
+
+func (x TestWorkflowNotificationsRequestType) Enum() *TestWorkflowNotificationsRequestType {
+ p := new(TestWorkflowNotificationsRequestType)
+ *p = x
+ return p
+}
+
+func (x TestWorkflowNotificationsRequestType) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (TestWorkflowNotificationsRequestType) Descriptor() protoreflect.EnumDescriptor {
+ return file_proto_service_proto_enumTypes[1].Descriptor()
+}
+
+func (TestWorkflowNotificationsRequestType) Type() protoreflect.EnumType {
+ return &file_proto_service_proto_enumTypes[1]
+}
+
+func (x TestWorkflowNotificationsRequestType) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use TestWorkflowNotificationsRequestType.Descriptor instead.
+func (TestWorkflowNotificationsRequestType) EnumDescriptor() ([]byte, []int) {
+ return file_proto_service_proto_rawDescGZIP(), []int{1}
+}
+
+type TestWorkflowNotificationType int32
+
+const (
+ TestWorkflowNotificationType_WORKFLOW_STREAM_ERROR TestWorkflowNotificationType = 0
+ TestWorkflowNotificationType_WORKFLOW_STREAM_LOG TestWorkflowNotificationType = 1
+ TestWorkflowNotificationType_WORKFLOW_STREAM_RESULT TestWorkflowNotificationType = 2
+ TestWorkflowNotificationType_WORKFLOW_STREAM_OUTPUT TestWorkflowNotificationType = 3
+)
+
+// Enum value maps for TestWorkflowNotificationType.
+var (
+ TestWorkflowNotificationType_name = map[int32]string{
+ 0: "WORKFLOW_STREAM_ERROR",
+ 1: "WORKFLOW_STREAM_LOG",
+ 2: "WORKFLOW_STREAM_RESULT",
+ 3: "WORKFLOW_STREAM_OUTPUT",
+ }
+ TestWorkflowNotificationType_value = map[string]int32{
+ "WORKFLOW_STREAM_ERROR": 0,
+ "WORKFLOW_STREAM_LOG": 1,
+ "WORKFLOW_STREAM_RESULT": 2,
+ "WORKFLOW_STREAM_OUTPUT": 3,
+ }
+)
+
+func (x TestWorkflowNotificationType) Enum() *TestWorkflowNotificationType {
+ p := new(TestWorkflowNotificationType)
+ *p = x
+ return p
+}
+
+func (x TestWorkflowNotificationType) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (TestWorkflowNotificationType) Descriptor() protoreflect.EnumDescriptor {
+ return file_proto_service_proto_enumTypes[2].Descriptor()
+}
+
+func (TestWorkflowNotificationType) Type() protoreflect.EnumType {
+ return &file_proto_service_proto_enumTypes[2]
+}
+
+func (x TestWorkflowNotificationType) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use TestWorkflowNotificationType.Descriptor instead.
+func (TestWorkflowNotificationType) EnumDescriptor() ([]byte, []int) {
+ return file_proto_service_proto_rawDescGZIP(), []int{2}
+}
+
type Opcode int32
const (
@@ -105,11 +202,11 @@ func (x Opcode) String() string {
}
func (Opcode) Descriptor() protoreflect.EnumDescriptor {
- return file_proto_service_proto_enumTypes[1].Descriptor()
+ return file_proto_service_proto_enumTypes[3].Descriptor()
}
func (Opcode) Type() protoreflect.EnumType {
- return &file_proto_service_proto_enumTypes[1]
+ return &file_proto_service_proto_enumTypes[3]
}
func (x Opcode) Number() protoreflect.EnumNumber {
@@ -118,7 +215,7 @@ func (x Opcode) Number() protoreflect.EnumNumber {
// Deprecated: Use Opcode.Descriptor instead.
func (Opcode) EnumDescriptor() ([]byte, []int) {
- return file_proto_service_proto_rawDescGZIP(), []int{1}
+ return file_proto_service_proto_rawDescGZIP(), []int{3}
}
type LogsStreamRequest struct {
@@ -436,6 +533,156 @@ func (x *ExecuteRequest) GetMessageId() string {
return ""
}
+type TestWorkflowNotificationsRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ StreamId string `protobuf:"bytes,1,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"`
+ ExecutionId string `protobuf:"bytes,2,opt,name=execution_id,json=executionId,proto3" json:"execution_id,omitempty"`
+ RequestType TestWorkflowNotificationsRequestType `protobuf:"varint,3,opt,name=request_type,json=requestType,proto3,enum=cloud.TestWorkflowNotificationsRequestType" json:"request_type,omitempty"`
+}
+
+func (x *TestWorkflowNotificationsRequest) Reset() {
+ *x = TestWorkflowNotificationsRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_proto_service_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *TestWorkflowNotificationsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TestWorkflowNotificationsRequest) ProtoMessage() {}
+
+func (x *TestWorkflowNotificationsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_service_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use TestWorkflowNotificationsRequest.ProtoReflect.Descriptor instead.
+func (*TestWorkflowNotificationsRequest) Descriptor() ([]byte, []int) {
+ return file_proto_service_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *TestWorkflowNotificationsRequest) GetStreamId() string {
+ if x != nil {
+ return x.StreamId
+ }
+ return ""
+}
+
+func (x *TestWorkflowNotificationsRequest) GetExecutionId() string {
+ if x != nil {
+ return x.ExecutionId
+ }
+ return ""
+}
+
+func (x *TestWorkflowNotificationsRequest) GetRequestType() TestWorkflowNotificationsRequestType {
+ if x != nil {
+ return x.RequestType
+ }
+ return TestWorkflowNotificationsRequestType_WORKFLOW_STREAM_LOG_MESSAGE
+}
+
+type TestWorkflowNotificationsResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ StreamId string `protobuf:"bytes,1,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"`
+ SeqNo uint32 `protobuf:"varint,2,opt,name=seq_no,json=seqNo,proto3" json:"seq_no,omitempty"`
+ Timestamp string `protobuf:"bytes,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+ Ref string `protobuf:"bytes,4,opt,name=ref,proto3" json:"ref,omitempty"`
+ Type TestWorkflowNotificationType `protobuf:"varint,5,opt,name=type,proto3,enum=cloud.TestWorkflowNotificationType" json:"type,omitempty"`
+ Message string `protobuf:"bytes,6,opt,name=message,proto3" json:"message,omitempty"` // based on type: log/error = inline, others = serialized to JSON
+}
+
+func (x *TestWorkflowNotificationsResponse) Reset() {
+ *x = TestWorkflowNotificationsResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_proto_service_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *TestWorkflowNotificationsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TestWorkflowNotificationsResponse) ProtoMessage() {}
+
+func (x *TestWorkflowNotificationsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_service_proto_msgTypes[6]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use TestWorkflowNotificationsResponse.ProtoReflect.Descriptor instead.
+func (*TestWorkflowNotificationsResponse) Descriptor() ([]byte, []int) {
+ return file_proto_service_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *TestWorkflowNotificationsResponse) GetStreamId() string {
+ if x != nil {
+ return x.StreamId
+ }
+ return ""
+}
+
+func (x *TestWorkflowNotificationsResponse) GetSeqNo() uint32 {
+ if x != nil {
+ return x.SeqNo
+ }
+ return 0
+}
+
+func (x *TestWorkflowNotificationsResponse) GetTimestamp() string {
+ if x != nil {
+ return x.Timestamp
+ }
+ return ""
+}
+
+func (x *TestWorkflowNotificationsResponse) GetRef() string {
+ if x != nil {
+ return x.Ref
+ }
+ return ""
+}
+
+func (x *TestWorkflowNotificationsResponse) GetType() TestWorkflowNotificationType {
+ if x != nil {
+ return x.Type
+ }
+ return TestWorkflowNotificationType_WORKFLOW_STREAM_ERROR
+}
+
+func (x *TestWorkflowNotificationsResponse) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
type HeaderValue struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -447,7 +694,7 @@ type HeaderValue struct {
func (x *HeaderValue) Reset() {
*x = HeaderValue{}
if protoimpl.UnsafeEnabled {
- mi := &file_proto_service_proto_msgTypes[5]
+ mi := &file_proto_service_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -460,7 +707,7 @@ func (x *HeaderValue) String() string {
func (*HeaderValue) ProtoMessage() {}
func (x *HeaderValue) ProtoReflect() protoreflect.Message {
- mi := &file_proto_service_proto_msgTypes[5]
+ mi := &file_proto_service_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -473,7 +720,7 @@ func (x *HeaderValue) ProtoReflect() protoreflect.Message {
// Deprecated: Use HeaderValue.ProtoReflect.Descriptor instead.
func (*HeaderValue) Descriptor() ([]byte, []int) {
- return file_proto_service_proto_rawDescGZIP(), []int{5}
+ return file_proto_service_proto_rawDescGZIP(), []int{7}
}
func (x *HeaderValue) GetHeader() []string {
@@ -497,7 +744,7 @@ type ExecuteResponse struct {
func (x *ExecuteResponse) Reset() {
*x = ExecuteResponse{}
if protoimpl.UnsafeEnabled {
- mi := &file_proto_service_proto_msgTypes[6]
+ mi := &file_proto_service_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -510,7 +757,7 @@ func (x *ExecuteResponse) String() string {
func (*ExecuteResponse) ProtoMessage() {}
func (x *ExecuteResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_service_proto_msgTypes[6]
+ mi := &file_proto_service_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -523,7 +770,7 @@ func (x *ExecuteResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExecuteResponse.ProtoReflect.Descriptor instead.
func (*ExecuteResponse) Descriptor() ([]byte, []int) {
- return file_proto_service_proto_rawDescGZIP(), []int{6}
+ return file_proto_service_proto_rawDescGZIP(), []int{8}
}
func (x *ExecuteResponse) GetStatus() int64 {
@@ -566,7 +813,7 @@ type WebsocketData struct {
func (x *WebsocketData) Reset() {
*x = WebsocketData{}
if protoimpl.UnsafeEnabled {
- mi := &file_proto_service_proto_msgTypes[7]
+ mi := &file_proto_service_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -579,7 +826,7 @@ func (x *WebsocketData) String() string {
func (*WebsocketData) ProtoMessage() {}
func (x *WebsocketData) ProtoReflect() protoreflect.Message {
- mi := &file_proto_service_proto_msgTypes[7]
+ mi := &file_proto_service_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -592,7 +839,7 @@ func (x *WebsocketData) ProtoReflect() protoreflect.Message {
// Deprecated: Use WebsocketData.ProtoReflect.Descriptor instead.
func (*WebsocketData) Descriptor() ([]byte, []int) {
- return file_proto_service_proto_rawDescGZIP(), []int{7}
+ return file_proto_service_proto_rawDescGZIP(), []int{9}
}
func (x *WebsocketData) GetOpcode() Opcode {
@@ -660,60 +907,109 @@ var file_proto_service_proto_rawDesc = []byte{
0x6b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x12, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65,
0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
- 0x01, 0x22, 0x25, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65,
- 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09,
- 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x22, 0xeb, 0x01, 0x0a, 0x0f, 0x45, 0x78, 0x65,
- 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06,
- 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x73, 0x74,
- 0x61, 0x74, 0x75, 0x73, 0x12, 0x3d, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18,
- 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x45, 0x78,
- 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x48, 0x65,
- 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64,
- 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28,
- 0x0c, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x65, 0x73, 0x73, 0x61,
- 0x67, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d, 0x65, 0x73,
- 0x73, 0x61, 0x67, 0x65, 0x49, 0x64, 0x1a, 0x4e, 0x0a, 0x0c, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72,
- 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
- 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e,
- 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c,
- 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x4a, 0x0a, 0x0d, 0x57, 0x65, 0x62, 0x73, 0x6f, 0x63,
- 0x6b, 0x65, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x25, 0x0a, 0x06, 0x6f, 0x70, 0x63, 0x6f, 0x64,
- 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0d, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e,
- 0x4f, 0x70, 0x63, 0x6f, 0x64, 0x65, 0x52, 0x06, 0x6f, 0x70, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x12,
- 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x62, 0x6f,
- 0x64, 0x79, 0x2a, 0x48, 0x0a, 0x15, 0x4c, 0x6f, 0x67, 0x73, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d,
- 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x53,
- 0x54, 0x52, 0x45, 0x41, 0x4d, 0x5f, 0x4c, 0x4f, 0x47, 0x5f, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47,
- 0x45, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x53, 0x54, 0x52, 0x45, 0x41, 0x4d, 0x5f, 0x48, 0x45,
- 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x10, 0x01, 0x2a, 0x4c, 0x0a, 0x06,
- 0x4f, 0x70, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43,
- 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x54, 0x45, 0x58, 0x54, 0x5f, 0x46,
- 0x52, 0x41, 0x4d, 0x45, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x42, 0x49, 0x4e, 0x41, 0x52, 0x59,
- 0x5f, 0x46, 0x52, 0x41, 0x4d, 0x45, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x48, 0x45, 0x41, 0x4c,
- 0x54, 0x48, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x10, 0x03, 0x32, 0xcc, 0x02, 0x0a, 0x10, 0x54,
- 0x65, 0x73, 0x74, 0x4b, 0x75, 0x62, 0x65, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x41, 0x50, 0x49, 0x12,
- 0x3c, 0x0a, 0x07, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x63, 0x6c, 0x6f,
- 0x75, 0x64, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
- 0x73, 0x65, 0x1a, 0x15, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75,
- 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, 0x01, 0x12, 0x36, 0x0a,
- 0x04, 0x53, 0x65, 0x6e, 0x64, 0x12, 0x14, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x57, 0x65,
- 0x62, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x44, 0x61, 0x74, 0x61, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
- 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
- 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x35, 0x0a, 0x04, 0x43, 0x61, 0x6c, 0x6c, 0x12, 0x15, 0x2e,
- 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71,
- 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x43, 0x6f, 0x6d,
- 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c,
- 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x41, 0x73, 0x79, 0x6e, 0x63, 0x12, 0x16, 0x2e, 0x63,
- 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x15, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x45, 0x78, 0x65,
- 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, 0x01, 0x12,
- 0x48, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d,
- 0x12, 0x19, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x73, 0x53, 0x74, 0x72,
- 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x18, 0x2e, 0x63, 0x6c,
- 0x6f, 0x75, 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x73, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, 0x01, 0x42, 0x0b, 0x5a, 0x09, 0x70, 0x6b, 0x67,
- 0x2f, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x01, 0x22, 0xb2, 0x01, 0x0a, 0x20, 0x54, 0x65, 0x73, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
+ 0x6f, 0x77, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d,
+ 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x74, 0x72, 0x65, 0x61,
+ 0x6d, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e,
+ 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x78, 0x65, 0x63, 0x75,
+ 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x4e, 0x0a, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x63,
+ 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x54, 0x65, 0x73, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f,
+ 0x77, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0b, 0x72, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x22, 0xda, 0x01, 0x0a, 0x21, 0x54, 0x65, 0x73, 0x74, 0x57,
+ 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74,
+ 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09,
+ 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x08, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x73, 0x65, 0x71,
+ 0x5f, 0x6e, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x65, 0x71, 0x4e, 0x6f,
+ 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x10,
+ 0x0a, 0x03, 0x72, 0x65, 0x66, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x65, 0x66,
+ 0x12, 0x37, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23,
+ 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x54, 0x65, 0x73, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x66,
+ 0x6c, 0x6f, 0x77, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54,
+ 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73,
+ 0x73, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73,
+ 0x61, 0x67, 0x65, 0x22, 0x25, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x56, 0x61, 0x6c,
+ 0x75, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03,
+ 0x28, 0x09, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x22, 0xeb, 0x01, 0x0a, 0x0f, 0x45,
+ 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16,
+ 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06,
+ 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x3d, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72,
+ 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e,
+ 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e,
+ 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x68, 0x65,
+ 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x0c, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x65, 0x73,
+ 0x73, 0x61, 0x67, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6d,
+ 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49, 0x64, 0x1a, 0x4e, 0x0a, 0x0c, 0x48, 0x65, 0x61, 0x64,
+ 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x05, 0x76, 0x61,
+ 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x63, 0x6c, 0x6f, 0x75,
+ 0x64, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76,
+ 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x4a, 0x0a, 0x0d, 0x57, 0x65, 0x62, 0x73,
+ 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, 0x25, 0x0a, 0x06, 0x6f, 0x70, 0x63,
+ 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0d, 0x2e, 0x63, 0x6c, 0x6f, 0x75,
+ 0x64, 0x2e, 0x4f, 0x70, 0x63, 0x6f, 0x64, 0x65, 0x52, 0x06, 0x6f, 0x70, 0x63, 0x6f, 0x64, 0x65,
+ 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04,
+ 0x62, 0x6f, 0x64, 0x79, 0x2a, 0x48, 0x0a, 0x15, 0x4c, 0x6f, 0x67, 0x73, 0x53, 0x74, 0x72, 0x65,
+ 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a,
+ 0x12, 0x53, 0x54, 0x52, 0x45, 0x41, 0x4d, 0x5f, 0x4c, 0x4f, 0x47, 0x5f, 0x4d, 0x45, 0x53, 0x53,
+ 0x41, 0x47, 0x45, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x53, 0x54, 0x52, 0x45, 0x41, 0x4d, 0x5f,
+ 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x10, 0x01, 0x2a, 0x69,
+ 0x0a, 0x24, 0x54, 0x65, 0x73, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4e, 0x6f,
+ 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x57, 0x4f, 0x52, 0x4b, 0x46, 0x4c,
+ 0x4f, 0x57, 0x5f, 0x53, 0x54, 0x52, 0x45, 0x41, 0x4d, 0x5f, 0x4c, 0x4f, 0x47, 0x5f, 0x4d, 0x45,
+ 0x53, 0x53, 0x41, 0x47, 0x45, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, 0x57, 0x4f, 0x52, 0x4b, 0x46,
+ 0x4c, 0x4f, 0x57, 0x5f, 0x53, 0x54, 0x52, 0x45, 0x41, 0x4d, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54,
+ 0x48, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x10, 0x01, 0x2a, 0x8a, 0x01, 0x0a, 0x1c, 0x54, 0x65,
+ 0x73, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69,
+ 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x15, 0x57, 0x4f,
+ 0x52, 0x4b, 0x46, 0x4c, 0x4f, 0x57, 0x5f, 0x53, 0x54, 0x52, 0x45, 0x41, 0x4d, 0x5f, 0x45, 0x52,
+ 0x52, 0x4f, 0x52, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x57, 0x4f, 0x52, 0x4b, 0x46, 0x4c, 0x4f,
+ 0x57, 0x5f, 0x53, 0x54, 0x52, 0x45, 0x41, 0x4d, 0x5f, 0x4c, 0x4f, 0x47, 0x10, 0x01, 0x12, 0x1a,
+ 0x0a, 0x16, 0x57, 0x4f, 0x52, 0x4b, 0x46, 0x4c, 0x4f, 0x57, 0x5f, 0x53, 0x54, 0x52, 0x45, 0x41,
+ 0x4d, 0x5f, 0x52, 0x45, 0x53, 0x55, 0x4c, 0x54, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x57, 0x4f,
+ 0x52, 0x4b, 0x46, 0x4c, 0x4f, 0x57, 0x5f, 0x53, 0x54, 0x52, 0x45, 0x41, 0x4d, 0x5f, 0x4f, 0x55,
+ 0x54, 0x50, 0x55, 0x54, 0x10, 0x03, 0x2a, 0x4c, 0x0a, 0x06, 0x4f, 0x70, 0x63, 0x6f, 0x64, 0x65,
+ 0x12, 0x0e, 0x0a, 0x0a, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00,
+ 0x12, 0x0e, 0x0a, 0x0a, 0x54, 0x45, 0x58, 0x54, 0x5f, 0x46, 0x52, 0x41, 0x4d, 0x45, 0x10, 0x01,
+ 0x12, 0x10, 0x0a, 0x0c, 0x42, 0x49, 0x4e, 0x41, 0x52, 0x59, 0x5f, 0x46, 0x52, 0x41, 0x4d, 0x45,
+ 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x43, 0x48, 0x45,
+ 0x43, 0x4b, 0x10, 0x03, 0x32, 0xc9, 0x03, 0x0a, 0x10, 0x54, 0x65, 0x73, 0x74, 0x4b, 0x75, 0x62,
+ 0x65, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x41, 0x50, 0x49, 0x12, 0x3c, 0x0a, 0x07, 0x45, 0x78, 0x65,
+ 0x63, 0x75, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x45, 0x78, 0x65,
+ 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x15, 0x2e, 0x63,
+ 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, 0x01, 0x12, 0x36, 0x0a, 0x04, 0x53, 0x65, 0x6e, 0x64, 0x12,
+ 0x14, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x57, 0x65, 0x62, 0x73, 0x6f, 0x63, 0x6b, 0x65,
+ 0x74, 0x44, 0x61, 0x74, 0x61, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12,
+ 0x35, 0x0a, 0x04, 0x43, 0x61, 0x6c, 0x6c, 0x12, 0x15, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e,
+ 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16,
+ 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74,
+ 0x65, 0x41, 0x73, 0x79, 0x6e, 0x63, 0x12, 0x16, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x45,
+ 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x15,
+ 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, 0x01, 0x12, 0x48, 0x0a, 0x0d, 0x47, 0x65, 0x74,
+ 0x4c, 0x6f, 0x67, 0x73, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x19, 0x2e, 0x63, 0x6c, 0x6f,
+ 0x75, 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x73, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x18, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x4c, 0x6f,
+ 0x67, 0x73, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x28,
+ 0x01, 0x30, 0x01, 0x12, 0x7b, 0x0a, 0x22, 0x47, 0x65, 0x74, 0x54, 0x65, 0x73, 0x74, 0x57, 0x6f,
+ 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69,
+ 0x6f, 0x6e, 0x73, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x28, 0x2e, 0x63, 0x6c, 0x6f, 0x75,
+ 0x64, 0x2e, 0x54, 0x65, 0x73, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4e, 0x6f,
+ 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x1a, 0x27, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x54, 0x65, 0x73, 0x74,
+ 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
+ 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, 0x01,
+ 0x42, 0x0b, 0x5a, 0x09, 0x70, 0x6b, 0x67, 0x2f, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x62, 0x06, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -728,47 +1024,55 @@ func file_proto_service_proto_rawDescGZIP() []byte {
return file_proto_service_proto_rawDescData
}
-var file_proto_service_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
-var file_proto_service_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
+var file_proto_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
+var file_proto_service_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_proto_service_proto_goTypes = []interface{}{
- (LogsStreamRequestType)(0), // 0: cloud.LogsStreamRequestType
- (Opcode)(0), // 1: cloud.Opcode
- (*LogsStreamRequest)(nil), // 2: cloud.LogsStreamRequest
- (*LogsStreamResponse)(nil), // 3: cloud.LogsStreamResponse
- (*CommandRequest)(nil), // 4: cloud.CommandRequest
- (*CommandResponse)(nil), // 5: cloud.CommandResponse
- (*ExecuteRequest)(nil), // 6: cloud.ExecuteRequest
- (*HeaderValue)(nil), // 7: cloud.HeaderValue
- (*ExecuteResponse)(nil), // 8: cloud.ExecuteResponse
- (*WebsocketData)(nil), // 9: cloud.WebsocketData
- nil, // 10: cloud.ExecuteRequest.HeadersEntry
- nil, // 11: cloud.ExecuteResponse.HeadersEntry
- (*structpb.Struct)(nil), // 12: google.protobuf.Struct
- (*emptypb.Empty)(nil), // 13: google.protobuf.Empty
+ (LogsStreamRequestType)(0), // 0: cloud.LogsStreamRequestType
+ (TestWorkflowNotificationsRequestType)(0), // 1: cloud.TestWorkflowNotificationsRequestType
+ (TestWorkflowNotificationType)(0), // 2: cloud.TestWorkflowNotificationType
+ (Opcode)(0), // 3: cloud.Opcode
+ (*LogsStreamRequest)(nil), // 4: cloud.LogsStreamRequest
+ (*LogsStreamResponse)(nil), // 5: cloud.LogsStreamResponse
+ (*CommandRequest)(nil), // 6: cloud.CommandRequest
+ (*CommandResponse)(nil), // 7: cloud.CommandResponse
+ (*ExecuteRequest)(nil), // 8: cloud.ExecuteRequest
+ (*TestWorkflowNotificationsRequest)(nil), // 9: cloud.TestWorkflowNotificationsRequest
+ (*TestWorkflowNotificationsResponse)(nil), // 10: cloud.TestWorkflowNotificationsResponse
+ (*HeaderValue)(nil), // 11: cloud.HeaderValue
+ (*ExecuteResponse)(nil), // 12: cloud.ExecuteResponse
+ (*WebsocketData)(nil), // 13: cloud.WebsocketData
+ nil, // 14: cloud.ExecuteRequest.HeadersEntry
+ nil, // 15: cloud.ExecuteResponse.HeadersEntry
+ (*structpb.Struct)(nil), // 16: google.protobuf.Struct
+ (*emptypb.Empty)(nil), // 17: google.protobuf.Empty
}
var file_proto_service_proto_depIdxs = []int32{
0, // 0: cloud.LogsStreamRequest.request_type:type_name -> cloud.LogsStreamRequestType
- 12, // 1: cloud.CommandRequest.payload:type_name -> google.protobuf.Struct
- 10, // 2: cloud.ExecuteRequest.headers:type_name -> cloud.ExecuteRequest.HeadersEntry
- 11, // 3: cloud.ExecuteResponse.headers:type_name -> cloud.ExecuteResponse.HeadersEntry
- 1, // 4: cloud.WebsocketData.opcode:type_name -> cloud.Opcode
- 7, // 5: cloud.ExecuteRequest.HeadersEntry.value:type_name -> cloud.HeaderValue
- 7, // 6: cloud.ExecuteResponse.HeadersEntry.value:type_name -> cloud.HeaderValue
- 8, // 7: cloud.TestKubeCloudAPI.Execute:input_type -> cloud.ExecuteResponse
- 9, // 8: cloud.TestKubeCloudAPI.Send:input_type -> cloud.WebsocketData
- 4, // 9: cloud.TestKubeCloudAPI.Call:input_type -> cloud.CommandRequest
- 8, // 10: cloud.TestKubeCloudAPI.ExecuteAsync:input_type -> cloud.ExecuteResponse
- 3, // 11: cloud.TestKubeCloudAPI.GetLogsStream:input_type -> cloud.LogsStreamResponse
- 6, // 12: cloud.TestKubeCloudAPI.Execute:output_type -> cloud.ExecuteRequest
- 13, // 13: cloud.TestKubeCloudAPI.Send:output_type -> google.protobuf.Empty
- 5, // 14: cloud.TestKubeCloudAPI.Call:output_type -> cloud.CommandResponse
- 6, // 15: cloud.TestKubeCloudAPI.ExecuteAsync:output_type -> cloud.ExecuteRequest
- 2, // 16: cloud.TestKubeCloudAPI.GetLogsStream:output_type -> cloud.LogsStreamRequest
- 12, // [12:17] is the sub-list for method output_type
- 7, // [7:12] is the sub-list for method input_type
- 7, // [7:7] is the sub-list for extension type_name
- 7, // [7:7] is the sub-list for extension extendee
- 0, // [0:7] is the sub-list for field type_name
+ 16, // 1: cloud.CommandRequest.payload:type_name -> google.protobuf.Struct
+ 14, // 2: cloud.ExecuteRequest.headers:type_name -> cloud.ExecuteRequest.HeadersEntry
+ 1, // 3: cloud.TestWorkflowNotificationsRequest.request_type:type_name -> cloud.TestWorkflowNotificationsRequestType
+ 2, // 4: cloud.TestWorkflowNotificationsResponse.type:type_name -> cloud.TestWorkflowNotificationType
+ 15, // 5: cloud.ExecuteResponse.headers:type_name -> cloud.ExecuteResponse.HeadersEntry
+ 3, // 6: cloud.WebsocketData.opcode:type_name -> cloud.Opcode
+ 11, // 7: cloud.ExecuteRequest.HeadersEntry.value:type_name -> cloud.HeaderValue
+ 11, // 8: cloud.ExecuteResponse.HeadersEntry.value:type_name -> cloud.HeaderValue
+ 12, // 9: cloud.TestKubeCloudAPI.Execute:input_type -> cloud.ExecuteResponse
+ 13, // 10: cloud.TestKubeCloudAPI.Send:input_type -> cloud.WebsocketData
+ 6, // 11: cloud.TestKubeCloudAPI.Call:input_type -> cloud.CommandRequest
+ 12, // 12: cloud.TestKubeCloudAPI.ExecuteAsync:input_type -> cloud.ExecuteResponse
+ 5, // 13: cloud.TestKubeCloudAPI.GetLogsStream:input_type -> cloud.LogsStreamResponse
+ 10, // 14: cloud.TestKubeCloudAPI.GetTestWorkflowNotificationsStream:input_type -> cloud.TestWorkflowNotificationsResponse
+ 8, // 15: cloud.TestKubeCloudAPI.Execute:output_type -> cloud.ExecuteRequest
+ 17, // 16: cloud.TestKubeCloudAPI.Send:output_type -> google.protobuf.Empty
+ 7, // 17: cloud.TestKubeCloudAPI.Call:output_type -> cloud.CommandResponse
+ 8, // 18: cloud.TestKubeCloudAPI.ExecuteAsync:output_type -> cloud.ExecuteRequest
+ 4, // 19: cloud.TestKubeCloudAPI.GetLogsStream:output_type -> cloud.LogsStreamRequest
+ 9, // 20: cloud.TestKubeCloudAPI.GetTestWorkflowNotificationsStream:output_type -> cloud.TestWorkflowNotificationsRequest
+ 15, // [15:21] is the sub-list for method output_type
+ 9, // [9:15] is the sub-list for method input_type
+ 9, // [9:9] is the sub-list for extension type_name
+ 9, // [9:9] is the sub-list for extension extendee
+ 0, // [0:9] is the sub-list for field type_name
}
func init() { file_proto_service_proto_init() }
@@ -838,7 +1142,7 @@ func file_proto_service_proto_init() {
}
}
file_proto_service_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*HeaderValue); i {
+ switch v := v.(*TestWorkflowNotificationsRequest); i {
case 0:
return &v.state
case 1:
@@ -850,7 +1154,7 @@ func file_proto_service_proto_init() {
}
}
file_proto_service_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*ExecuteResponse); i {
+ switch v := v.(*TestWorkflowNotificationsResponse); i {
case 0:
return &v.state
case 1:
@@ -862,6 +1166,30 @@ func file_proto_service_proto_init() {
}
}
file_proto_service_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*HeaderValue); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_proto_service_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ExecuteResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_proto_service_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*WebsocketData); i {
case 0:
return &v.state
@@ -879,8 +1207,8 @@ func file_proto_service_proto_init() {
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_service_proto_rawDesc,
- NumEnums: 2,
- NumMessages: 10,
+ NumEnums: 4,
+ NumMessages: 12,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/pkg/cloud/service_grpc.pb.go b/pkg/cloud/service_grpc.pb.go
index 18f07f2e7f9..5ced9df6368 100644
--- a/pkg/cloud/service_grpc.pb.go
+++ b/pkg/cloud/service_grpc.pb.go
@@ -1,10 +1,13 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.2.0
+// - protoc v3.19.4
+// source: proto/service.proto
package cloud
import (
context "context"
-
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
@@ -27,6 +30,7 @@ type TestKubeCloudAPIClient interface {
Call(ctx context.Context, in *CommandRequest, opts ...grpc.CallOption) (*CommandResponse, error)
ExecuteAsync(ctx context.Context, opts ...grpc.CallOption) (TestKubeCloudAPI_ExecuteAsyncClient, error)
GetLogsStream(ctx context.Context, opts ...grpc.CallOption) (TestKubeCloudAPI_GetLogsStreamClient, error)
+ GetTestWorkflowNotificationsStream(ctx context.Context, opts ...grpc.CallOption) (TestKubeCloudAPI_GetTestWorkflowNotificationsStreamClient, error)
}
type testKubeCloudAPIClient struct {
@@ -173,6 +177,37 @@ func (x *testKubeCloudAPIGetLogsStreamClient) Recv() (*LogsStreamRequest, error)
return m, nil
}
+func (c *testKubeCloudAPIClient) GetTestWorkflowNotificationsStream(ctx context.Context, opts ...grpc.CallOption) (TestKubeCloudAPI_GetTestWorkflowNotificationsStreamClient, error) {
+ stream, err := c.cc.NewStream(ctx, &TestKubeCloudAPI_ServiceDesc.Streams[4], "/cloud.TestKubeCloudAPI/GetTestWorkflowNotificationsStream", opts...)
+ if err != nil {
+ return nil, err
+ }
+ x := &testKubeCloudAPIGetTestWorkflowNotificationsStreamClient{stream}
+ return x, nil
+}
+
+type TestKubeCloudAPI_GetTestWorkflowNotificationsStreamClient interface {
+ Send(*TestWorkflowNotificationsResponse) error
+ Recv() (*TestWorkflowNotificationsRequest, error)
+ grpc.ClientStream
+}
+
+type testKubeCloudAPIGetTestWorkflowNotificationsStreamClient struct {
+ grpc.ClientStream
+}
+
+func (x *testKubeCloudAPIGetTestWorkflowNotificationsStreamClient) Send(m *TestWorkflowNotificationsResponse) error {
+ return x.ClientStream.SendMsg(m)
+}
+
+func (x *testKubeCloudAPIGetTestWorkflowNotificationsStreamClient) Recv() (*TestWorkflowNotificationsRequest, error) {
+ m := new(TestWorkflowNotificationsRequest)
+ if err := x.ClientStream.RecvMsg(m); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
// TestKubeCloudAPIServer is the server API for TestKubeCloudAPI service.
// All implementations must embed UnimplementedTestKubeCloudAPIServer
// for forward compatibility
@@ -184,6 +219,7 @@ type TestKubeCloudAPIServer interface {
Call(context.Context, *CommandRequest) (*CommandResponse, error)
ExecuteAsync(TestKubeCloudAPI_ExecuteAsyncServer) error
GetLogsStream(TestKubeCloudAPI_GetLogsStreamServer) error
+ GetTestWorkflowNotificationsStream(TestKubeCloudAPI_GetTestWorkflowNotificationsStreamServer) error
mustEmbedUnimplementedTestKubeCloudAPIServer()
}
@@ -206,6 +242,9 @@ func (UnimplementedTestKubeCloudAPIServer) ExecuteAsync(TestKubeCloudAPI_Execute
func (UnimplementedTestKubeCloudAPIServer) GetLogsStream(TestKubeCloudAPI_GetLogsStreamServer) error {
return status.Errorf(codes.Unimplemented, "method GetLogsStream not implemented")
}
+func (UnimplementedTestKubeCloudAPIServer) GetTestWorkflowNotificationsStream(TestKubeCloudAPI_GetTestWorkflowNotificationsStreamServer) error {
+ return status.Errorf(codes.Unimplemented, "method GetTestWorkflowNotificationsStream not implemented")
+}
func (UnimplementedTestKubeCloudAPIServer) mustEmbedUnimplementedTestKubeCloudAPIServer() {}
// UnsafeTestKubeCloudAPIServer may be embedded to opt out of forward compatibility for this service.
@@ -341,6 +380,32 @@ func (x *testKubeCloudAPIGetLogsStreamServer) Recv() (*LogsStreamResponse, error
return m, nil
}
+func _TestKubeCloudAPI_GetTestWorkflowNotificationsStream_Handler(srv interface{}, stream grpc.ServerStream) error {
+ return srv.(TestKubeCloudAPIServer).GetTestWorkflowNotificationsStream(&testKubeCloudAPIGetTestWorkflowNotificationsStreamServer{stream})
+}
+
+type TestKubeCloudAPI_GetTestWorkflowNotificationsStreamServer interface {
+ Send(*TestWorkflowNotificationsRequest) error
+ Recv() (*TestWorkflowNotificationsResponse, error)
+ grpc.ServerStream
+}
+
+type testKubeCloudAPIGetTestWorkflowNotificationsStreamServer struct {
+ grpc.ServerStream
+}
+
+func (x *testKubeCloudAPIGetTestWorkflowNotificationsStreamServer) Send(m *TestWorkflowNotificationsRequest) error {
+ return x.ServerStream.SendMsg(m)
+}
+
+func (x *testKubeCloudAPIGetTestWorkflowNotificationsStreamServer) Recv() (*TestWorkflowNotificationsResponse, error) {
+ m := new(TestWorkflowNotificationsResponse)
+ if err := x.ServerStream.RecvMsg(m); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
// TestKubeCloudAPI_ServiceDesc is the grpc.ServiceDesc for TestKubeCloudAPI service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -377,6 +442,12 @@ var TestKubeCloudAPI_ServiceDesc = grpc.ServiceDesc{
ServerStreams: true,
ClientStreams: true,
},
+ {
+ StreamName: "GetTestWorkflowNotificationsStream",
+ Handler: _TestKubeCloudAPI_GetTestWorkflowNotificationsStream_Handler,
+ ServerStreams: true,
+ ClientStreams: true,
+ },
},
Metadata: "proto/service.proto",
}
diff --git a/pkg/configmap/client.go b/pkg/configmap/client.go
index c14ecd9c78c..68a432d7db5 100644
--- a/pkg/configmap/client.go
+++ b/pkg/configmap/client.go
@@ -15,7 +15,7 @@ import (
//go:generate mockgen -destination=./mock_client.go -package=configmap "github.com/kubeshop/testkube/pkg/configmap" Interface
type Interface interface {
- Get(ctx context.Context, id string) (map[string]string, error)
+ Get(ctx context.Context, id string, namespace ...string) (map[string]string, error)
Create(ctx context.Context, id string, stringData map[string]string) error
Apply(ctx context.Context, id string, stringData map[string]string) error
Update(ctx context.Context, id string, stringData map[string]string) error
@@ -55,8 +55,13 @@ func (c *Client) Create(ctx context.Context, id string, stringData map[string]st
}
// Get is a method to retrieve an existing configmap
-func (c *Client) Get(ctx context.Context, id string) (map[string]string, error) {
- configMapsClient := c.ClientSet.CoreV1().ConfigMaps(c.Namespace)
+func (c *Client) Get(ctx context.Context, id string, namespace ...string) (map[string]string, error) {
+ ns := c.Namespace
+ if len(namespace) != 0 {
+ ns = namespace[0]
+ }
+
+ configMapsClient := c.ClientSet.CoreV1().ConfigMaps(ns)
configMapSpec, err := configMapsClient.Get(ctx, id, metav1.GetOptions{})
if err != nil {
diff --git a/pkg/configmap/mock_client.go b/pkg/configmap/mock_client.go
index b97d70bb80d..69c69266959 100644
--- a/pkg/configmap/mock_client.go
+++ b/pkg/configmap/mock_client.go
@@ -63,18 +63,23 @@ func (mr *MockInterfaceMockRecorder) Create(arg0, arg1, arg2 interface{}) *gomoc
}
// Get mocks base method.
-func (m *MockInterface) Get(arg0 context.Context, arg1 string) (map[string]string, error) {
+func (m *MockInterface) Get(arg0 context.Context, arg1 string, arg2 ...string) (map[string]string, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Get", arg0, arg1)
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Get", varargs...)
ret0, _ := ret[0].(map[string]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get.
-func (mr *MockInterfaceMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockInterfaceMockRecorder) Get(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), arg0, arg1)
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), varargs...)
}
// Update mocks base method.
diff --git a/pkg/crd/crd_test.go b/pkg/crd/crd_test.go
index 890b9654c59..5db775ba338 100644
--- a/pkg/crd/crd_test.go
+++ b/pkg/crd/crd_test.go
@@ -113,7 +113,7 @@ func TestGenerateYAML(t *testing.T) {
})
t.Run("generate test CRD yaml", func(t *testing.T) {
// given
- expected := "apiVersion: tests.testkube.io/v3\nkind: Test\nmetadata:\n name: name1\n namespace: namespace1\n labels:\n key1: value1\nspec:\n executionRequest:\n name: execution-name\n args:\n - -v\n - test\n image: docker.io/curlimages/curl:latest\n command:\n - curl\n imagePullSecrets:\n - name: secret-name\n negativeTest: true\n activeDeadlineSeconds: 10\n executePostRunScriptBeforeScraping: false\n"
+ expected := "apiVersion: tests.testkube.io/v3\nkind: Test\nmetadata:\n name: name1\n namespace: namespace1\n labels:\n key1: value1\nspec:\n executionRequest:\n name: execution-name\n args:\n - -v\n - test\n image: docker.io/curlimages/curl:latest\n command:\n - curl\n imagePullSecrets:\n - name: secret-name\n negativeTest: true\n activeDeadlineSeconds: 10\n"
tests := []testkube.TestUpsertRequest{
{
Name: "name1",
diff --git a/pkg/crd/templates/test.tmpl b/pkg/crd/templates/test.tmpl
index 0eb3e951765..666f03b9eee 100644
--- a/pkg/crd/templates/test.tmpl
+++ b/pkg/crd/templates/test.tmpl
@@ -82,7 +82,7 @@ spec:
schedule: {{ .Schedule }}
{{- end }}
{{- if .ExecutionRequest }}
- {{- if or (.ExecutionRequest.Name) (.ExecutionRequest.NegativeTest) (.ExecutionRequest.VariablesFile) (.ExecutionRequest.HttpProxy) (.ExecutionRequest.HttpsProxy) (ne (len .ExecutionRequest.Variables) 0) (ne (len .ExecutionRequest.Args) 0) (ne (len .ExecutionRequest.Envs) 0) (ne (len .ExecutionRequest.SecretEnvs) 0) (.ExecutionRequest.Image) (ne (len .ExecutionRequest.Command) 0) (.ExecutionRequest.ArgsMode) (ne (len .ExecutionRequest.ImagePullSecrets) 0) (ne .ExecutionRequest.ActiveDeadlineSeconds 0) (.ExecutionRequest.ArtifactRequest) (.ExecutionRequest.JobTemplate) (.ExecutionRequest.JobTemplateReference) (.ExecutionRequest.CronJobTemplate) (.ExecutionRequest.CronJobTemplateReference) (.ExecutionRequest.PreRunScript) (.ExecutionRequest.PostRunScript) (.ExecutionRequest.ExecutePostRunScriptBeforeScraping) (.ExecutionRequest.ScraperTemplate) (.ExecutionRequest.ScraperTemplateReference) (.ExecutionRequest.PvcTemplate) (.ExecutionRequest.PvcTemplateReference) (ne (len .ExecutionRequest.EnvConfigMaps) 0) (ne (len .ExecutionRequest.EnvSecrets) 0) (.ExecutionRequest.SlavePodRequest)}}
+ {{- if or (.ExecutionRequest.Name) (.ExecutionRequest.NegativeTest) (.ExecutionRequest.VariablesFile) (.ExecutionRequest.HttpProxy) (.ExecutionRequest.HttpsProxy) (ne (len .ExecutionRequest.Variables) 0) (ne (len .ExecutionRequest.Args) 0) (ne (len .ExecutionRequest.Envs) 0) (ne (len .ExecutionRequest.SecretEnvs) 0) (.ExecutionRequest.Image) (ne (len .ExecutionRequest.Command) 0) (.ExecutionRequest.ArgsMode) (ne (len .ExecutionRequest.ImagePullSecrets) 0) (ne .ExecutionRequest.ActiveDeadlineSeconds 0) (.ExecutionRequest.ArtifactRequest) (.ExecutionRequest.JobTemplate) (.ExecutionRequest.JobTemplateReference) (.ExecutionRequest.CronJobTemplate) (.ExecutionRequest.CronJobTemplateReference) (.ExecutionRequest.PreRunScript) (.ExecutionRequest.PostRunScript) (.ExecutionRequest.ExecutePostRunScriptBeforeScraping) (.ExecutionRequest.SourceScripts) (.ExecutionRequest.ScraperTemplate) (.ExecutionRequest.ScraperTemplateReference) (.ExecutionRequest.PvcTemplate) (.ExecutionRequest.PvcTemplateReference) (ne (len .ExecutionRequest.EnvConfigMaps) 0) (ne (len .ExecutionRequest.EnvSecrets) 0) (.ExecutionRequest.SlavePodRequest) (.ExecutionRequest.ExecutionNamespace)}}
executionRequest:
{{- if .ExecutionRequest.Name }}
name: {{ .ExecutionRequest.Name }}
@@ -222,7 +222,12 @@ spec:
{{- if .ExecutionRequest.PostRunScript }}
postRunScript: {{ .ExecutionRequest.PostRunScript }}
{{- end }}
+ {{- if .ExecutionRequest.ExecutePostRunScriptBeforeScraping }}
executePostRunScriptBeforeScraping: {{ .ExecutionRequest.ExecutePostRunScriptBeforeScraping }}
+ {{- end }}
+ {{- if .ExecutionRequest.SourceScripts }}
+ sourceScripts: {{ .ExecutionRequest.SourceScripts }}
+ {{- end }}
{{- if .ExecutionRequest.ScraperTemplate }}
scraperTemplate: {{ .ExecutionRequest.ScraperTemplate }}
{{- end }}
@@ -235,6 +240,9 @@ spec:
{{- if .ExecutionRequest.PvcTemplateReference }}
pvcTemplateReference: {{ .ExecutionRequest.PvcTemplateReference }}
{{- end }}
+ {{- if .ExecutionRequest.ExecutionNamespace }}
+ executionNamespace: {{ .ExecutionRequest.ExecutionNamespace }}
+ {{- end }}
{{- if .ExecutionRequest.SlavePodRequest }}
slavePodRequest:
{{- if .ExecutionRequest.SlavePodRequest.Resources }}
diff --git a/pkg/crd/templates/testsuite.tmpl b/pkg/crd/templates/testsuite.tmpl
index 6e26a4bf19f..cf473739cb2 100644
--- a/pkg/crd/templates/testsuite.tmpl
+++ b/pkg/crd/templates/testsuite.tmpl
@@ -38,6 +38,104 @@ spec:
{{- range .Execute }}
{{- if .Test }}
- test: {{ .Test }}
+ {{- if .ExecutionRequest }}
+ {{- if or (ne (len .ExecutionRequest.ExecutionLabels) 0) (ne (len .ExecutionRequest.Variables) 0) (ne (len .ExecutionRequest.Args) 0) (.ExecutionRequest.ArgsMode) (ne (len .ExecutionRequest.Command) 0) (.ExecutionRequest.HttpProxy) (.ExecutionRequest.HttpsProxy) (.ExecutionRequest.JobTemplate) (.ExecutionRequest.JobTemplateReference) (.ExecutionRequest.CronJobTemplate) (.ExecutionRequest.CronJobTemplateReference) (.ExecutionRequest.ScraperTemplate) (.ExecutionRequest.ScraperTemplateReference) (.ExecutionRequest.PvcTemplate) (.ExecutionRequest.PvcTemplateReference) (.ExecutionRequest.RunningContext)}}
+ executionRequest:
+ {{- if ne (len .ExecutionRequest.ExecutionLabels) 0 }}
+ executionLabels:
+ {{- range $key, $value := .ExecutionRequest.ExecutionLabels }}
+ {{ $key }}: {{ $value }}
+ {{- end }}
+ {{- end }}
+ {{- if ne (len .ExecutionRequest.Variables) 0 }}
+ variables:
+ {{- range $key, $value := .ExecutionRequest.Variables }}
+ {{ $key }}:
+ name: {{ $key }}
+ {{- if $value.Value }}
+ value: {{ $value.Value }}
+ {{- end }}
+ {{- if $value.Type_ }}
+ type: {{ $value.Type_ }}
+ {{- end }}
+ {{- if $value.SecretRef }}
+ valueFrom:
+ secretKeyRef:
+ {{- if $value.SecretRef.Name }}
+ name: {{ $value.SecretRef.Name }}
+ {{- end }}
+ {{- if $value.SecretRef.Key }}
+ key: {{ $value.SecretRef.Key }}
+ {{- end }}
+ {{- end }}
+ {{- if $value.ConfigMapRef }}
+ valueFrom:
+ configMapKeyRef:
+ {{- if $value.ConfigMapRef.Name }}
+ name: {{ $value.ConfigMapRef.Name }}
+ {{- end }}
+ {{- if $value.ConfigMapRef.Key }}
+ key: {{ $value.ConfigMapRef.Key }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ {{- if ne (len .ExecutionRequest.Args) 0 }}
+ args:
+ {{- range .ExecutionRequest.Args }}
+ - {{ . }}
+ {{- end }}
+ {{- end }}
+ {{- if .ExecutionRequest.ArgsMode }}
+ argsMode: {{ .ExecutionRequest.ArgsMode }}
+ {{- end }}
+ {{- if gt (len .ExecutionRequest.Command) 0 }}
+ command:
+ {{- range $cmd := .ExecutionRequest.Command }}
+ - {{ $cmd -}}
+ {{- end }}
+ {{- end -}}
+ {{- if .ExecutionRequest.Sync }}
+ sync: {{ .ExecutionRequest.Sync }}
+ {{- end }}
+ {{- if .ExecutionRequest.HttpProxy }}
+ httpProxy: {{ .ExecutionRequest.HttpProxy }}
+ {{- end }}
+ {{- if .ExecutionRequest.HttpsProxy }}
+ httpsProxy: {{ .ExecutionRequest.HttpsProxy }}
+ {{- end }}
+ {{- if .ExecutionRequest.NegativeTest }}
+ negativeTest: {{ .ExecutionRequest.NegativeTest }}
+ {{- end }}
+ {{- if .ExecutionRequest.JobTemplate }}
+ jobTemplate: {{ .ExecutionRequest.JobTemplate }}
+ {{- end }}
+ {{- if .ExecutionRequest.JobTemplateReference }}
+ jobTemplateReference: {{ .ExecutionRequest.JobTemplateReference }}
+ {{- end }}
+ {{- if .ExecutionRequest.CronJobTemplate }}
+ cronJobTemplate: {{ .ExecutionRequest.CronJobTemplate }}
+ {{- end }}
+ {{- if .ExecutionRequest.CronJobTemplateReference }}
+ cronJobTemplateReference: {{ .ExecutionRequest.CronJobTemplateReference }}
+ {{- end }}
+ {{- if .ExecutionRequest.ScraperTemplate }}
+ scraperTemplate: {{ .ExecutionRequest.ScraperTemplate }}
+ {{- end }}
+ {{- if .ExecutionRequest.ScraperTemplateReference }}
+ scraperTemplateReference: {{ .ExecutionRequest.ScraperTemplateReference }}
+ {{- end }}
+ {{- if .ExecutionRequest.PvcTemplate }}
+ pvcTemplate: {{ .ExecutionRequest.PvcTemplate }}
+ {{- end }}
+ {{- if .ExecutionRequest.PvcTemplateReference }}
+ pvcTemplateReference: {{ .ExecutionRequest.PvcTemplateReference }}
+ {{- end }}
+ {{- if .ExecutionRequest.RunningContext }}
+ runningContext: {{ .ExecutionRequest.RunningContext }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
{{- end }}
{{- if .Delay }}
- delay: {{ .Delay }}
@@ -71,6 +169,104 @@ spec:
{{- range .Execute }}
{{- if .Test }}
- test: {{ .Test }}
+ {{- if .ExecutionRequest }}
+ {{- if or (ne (len .ExecutionRequest.ExecutionLabels) 0) (ne (len .ExecutionRequest.Variables) 0) (ne (len .ExecutionRequest.Args) 0) (.ExecutionRequest.ArgsMode) (ne (len .ExecutionRequest.Command) 0) (.ExecutionRequest.HttpProxy) (.ExecutionRequest.HttpsProxy) (.ExecutionRequest.JobTemplate) (.ExecutionRequest.JobTemplateReference) (.ExecutionRequest.CronJobTemplate) (.ExecutionRequest.CronJobTemplateReference) (.ExecutionRequest.ScraperTemplate) (.ExecutionRequest.ScraperTemplateReference) (.ExecutionRequest.PvcTemplate) (.ExecutionRequest.PvcTemplateReference) (.ExecutionRequest.RunningContext)}}
+ executionRequest:
+ {{- if ne (len .ExecutionRequest.ExecutionLabels) 0 }}
+ executionLabels:
+ {{- range $key, $value := .ExecutionRequest.ExecutionLabels }}
+ {{ $key }}: {{ $value }}
+ {{- end }}
+ {{- end }}
+ {{- if ne (len .ExecutionRequest.Variables) 0 }}
+ variables:
+ {{- range $key, $value := .ExecutionRequest.Variables }}
+ {{ $key }}:
+ name: {{ $key }}
+ {{- if $value.Value }}
+ value: {{ $value.Value }}
+ {{- end }}
+ {{- if $value.Type_ }}
+ type: {{ $value.Type_ }}
+ {{- end }}
+ {{- if $value.SecretRef }}
+ valueFrom:
+ secretKeyRef:
+ {{- if $value.SecretRef.Name }}
+ name: {{ $value.SecretRef.Name }}
+ {{- end }}
+ {{- if $value.SecretRef.Key }}
+ key: {{ $value.SecretRef.Key }}
+ {{- end }}
+ {{- end }}
+ {{- if $value.ConfigMapRef }}
+ valueFrom:
+ configMapKeyRef:
+ {{- if $value.ConfigMapRef.Name }}
+ name: {{ $value.ConfigMapRef.Name }}
+ {{- end }}
+ {{- if $value.ConfigMapRef.Key }}
+ key: {{ $value.ConfigMapRef.Key }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ {{- if ne (len .ExecutionRequest.Args) 0 }}
+ args:
+ {{- range .ExecutionRequest.Args }}
+ - {{ . }}
+ {{- end }}
+ {{- end }}
+ {{- if .ExecutionRequest.ArgsMode }}
+ argsMode: {{ .ExecutionRequest.ArgsMode }}
+ {{- end }}
+ {{- if gt (len .ExecutionRequest.Command) 0 }}
+ command:
+ {{- range $cmd := .ExecutionRequest.Command }}
+ - {{ $cmd -}}
+ {{- end }}
+ {{- end -}}
+ {{- if .ExecutionRequest.Sync }}
+ sync: {{ .ExecutionRequest.Sync }}
+ {{- end }}
+ {{- if .ExecutionRequest.HttpProxy }}
+ httpProxy: {{ .ExecutionRequest.HttpProxy }}
+ {{- end }}
+ {{- if .ExecutionRequest.HttpsProxy }}
+ httpsProxy: {{ .ExecutionRequest.HttpsProxy }}
+ {{- end }}
+ {{- if .ExecutionRequest.NegativeTest }}
+ negativeTest: {{ .ExecutionRequest.NegativeTest }}
+ {{- end }}
+ {{- if .ExecutionRequest.JobTemplate }}
+ jobTemplate: {{ .ExecutionRequest.JobTemplate }}
+ {{- end }}
+ {{- if .ExecutionRequest.JobTemplateReference }}
+ jobTemplateReference: {{ .ExecutionRequest.JobTemplateReference }}
+ {{- end }}
+ {{- if .ExecutionRequest.CronJobTemplate }}
+ cronJobTemplate: {{ .ExecutionRequest.CronJobTemplate }}
+ {{- end }}
+ {{- if .ExecutionRequest.CronJobTemplateReference }}
+ cronJobTemplateReference: {{ .ExecutionRequest.CronJobTemplateReference }}
+ {{- end }}
+ {{- if .ExecutionRequest.ScraperTemplate }}
+ scraperTemplate: {{ .ExecutionRequest.ScraperTemplate }}
+ {{- end }}
+ {{- if .ExecutionRequest.ScraperTemplateReference }}
+ scraperTemplateReference: {{ .ExecutionRequest.ScraperTemplateReference }}
+ {{- end }}
+ {{- if .ExecutionRequest.PvcTemplate }}
+ pvcTemplate: {{ .ExecutionRequest.PvcTemplate }}
+ {{- end }}
+ {{- if .ExecutionRequest.PvcTemplateReference }}
+ pvcTemplateReference: {{ .ExecutionRequest.PvcTemplateReference }}
+ {{- end }}
+ {{- if .ExecutionRequest.RunningContext }}
+ runningContext: {{ .ExecutionRequest.RunningContext }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
{{- end }}
{{- if .Delay }}
- delay: {{ .Delay }}
@@ -104,10 +300,108 @@ spec:
{{- range .Execute }}
{{- if .Test }}
- test: {{ .Test }}
+ {{- if .ExecutionRequest }}
+ {{- if or (ne (len .ExecutionRequest.ExecutionLabels) 0) (ne (len .ExecutionRequest.Variables) 0) (ne (len .ExecutionRequest.Args) 0) (.ExecutionRequest.ArgsMode) (ne (len .ExecutionRequest.Command) 0) (.ExecutionRequest.HttpProxy) (.ExecutionRequest.HttpsProxy) (.ExecutionRequest.JobTemplate) (.ExecutionRequest.JobTemplateReference) (.ExecutionRequest.CronJobTemplate) (.ExecutionRequest.CronJobTemplateReference) (.ExecutionRequest.ScraperTemplate) (.ExecutionRequest.ScraperTemplateReference) (.ExecutionRequest.PvcTemplate) (.ExecutionRequest.PvcTemplateReference) (.ExecutionRequest.RunningContext)}}
+ executionRequest:
+ {{- if ne (len .ExecutionRequest.ExecutionLabels) 0 }}
+ executionLabels:
+ {{- range $key, $value := .ExecutionRequest.ExecutionLabels }}
+ {{ $key }}: {{ $value }}
+ {{- end }}
+ {{- end }}
+ {{- if ne (len .ExecutionRequest.Variables) 0 }}
+ variables:
+ {{- range $key, $value := .ExecutionRequest.Variables }}
+ {{ $key }}:
+ name: {{ $key }}
+ {{- if $value.Value }}
+ value: {{ $value.Value }}
+ {{- end }}
+ {{- if $value.Type_ }}
+ type: {{ $value.Type_ }}
+ {{- end }}
+ {{- if $value.SecretRef }}
+ valueFrom:
+ secretKeyRef:
+ {{- if $value.SecretRef.Name }}
+ name: {{ $value.SecretRef.Name }}
+ {{- end }}
+ {{- if $value.SecretRef.Key }}
+ key: {{ $value.SecretRef.Key }}
+ {{- end }}
+ {{- end }}
+ {{- if $value.ConfigMapRef }}
+ valueFrom:
+ configMapKeyRef:
+ {{- if $value.ConfigMapRef.Name }}
+ name: {{ $value.ConfigMapRef.Name }}
+ {{- end }}
+ {{- if $value.ConfigMapRef.Key }}
+ key: {{ $value.ConfigMapRef.Key }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ {{- if ne (len .ExecutionRequest.Args) 0 }}
+ args:
+ {{- range .ExecutionRequest.Args }}
+ - {{ . }}
+ {{- end }}
+ {{- end }}
+ {{- if .ExecutionRequest.ArgsMode }}
+ argsMode: {{ .ExecutionRequest.ArgsMode }}
+ {{- end }}
+ {{- if gt (len .ExecutionRequest.Command) 0 }}
+ command:
+ {{- range $cmd := .ExecutionRequest.Command }}
+ - {{ $cmd -}}
+ {{- end }}
+ {{- end -}}
+ {{- if .ExecutionRequest.Sync }}
+ sync: {{ .ExecutionRequest.Sync }}
+ {{- end }}
+ {{- if .ExecutionRequest.HttpProxy }}
+ httpProxy: {{ .ExecutionRequest.HttpProxy }}
+ {{- end }}
+ {{- if .ExecutionRequest.HttpsProxy }}
+ httpsProxy: {{ .ExecutionRequest.HttpsProxy }}
+ {{- end }}
+ {{- if .ExecutionRequest.NegativeTest }}
+ negativeTest: {{ .ExecutionRequest.NegativeTest }}
+ {{- end }}
+ {{- if .ExecutionRequest.JobTemplate }}
+ jobTemplate: {{ .ExecutionRequest.JobTemplate }}
+ {{- end }}
+ {{- if .ExecutionRequest.JobTemplateReference }}
+ jobTemplateReference: {{ .ExecutionRequest.JobTemplateReference }}
+ {{- end }}
+ {{- if .ExecutionRequest.CronJobTemplate }}
+ cronJobTemplate: {{ .ExecutionRequest.CronJobTemplate }}
+ {{- end }}
+ {{- if .ExecutionRequest.CronJobTemplateReference }}
+ cronJobTemplateReference: {{ .ExecutionRequest.CronJobTemplateReference }}
+ {{- end }}
+ {{- if .ExecutionRequest.ScraperTemplate }}
+ scraperTemplate: {{ .ExecutionRequest.ScraperTemplate }}
+ {{- end }}
+ {{- if .ExecutionRequest.ScraperTemplateReference }}
+ scraperTemplateReference: {{ .ExecutionRequest.ScraperTemplateReference }}
+ {{- end }}
+ {{- if .ExecutionRequest.PvcTemplate }}
+ pvcTemplate: {{ .ExecutionRequest.PvcTemplate }}
+ {{- end }}
+ {{- if .ExecutionRequest.PvcTemplateReference }}
+ pvcTemplateReference: {{ .ExecutionRequest.PvcTemplateReference }}
+ {{- end }}
+ {{- if .ExecutionRequest.RunningContext }}
+ runningContext: {{ .ExecutionRequest.RunningContext }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
{{- end }}
{{- if .Delay }}
- delay: {{ .Delay }}
- {{- end }}
+ {{- end }}
{{- end }}
{{- end }}
{{- end }}
diff --git a/pkg/envs/variables.go b/pkg/envs/variables.go
index 292f0bfb9e5..1afb930fd50 100644
--- a/pkg/envs/variables.go
+++ b/pkg/envs/variables.go
@@ -42,6 +42,15 @@ type Params struct {
CloudAPIURL string `envconfig:"RUNNER_CLOUD_API_URL"` // RUNNER_CLOUD_API_URL
CloudConnectionTimeoutSec int `envconfig:"RUNNER_CLOUD_CONNECTION_TIMEOUT" default:"10"` // RUNNER_CLOUD_CONNECTION_TIMEOUT
CloudAPISkipVerify bool `envconfig:"RUNNER_CLOUD_API_SKIP_VERIFY" default:"false"` // RUNNER_CLOUD_API_SKIP_VERIFY
+ ProMode bool `envconfig:"RUNNER_PRO_MODE"` // RUNNER_PRO_MODE
+ ProAPIKey string `envconfig:"RUNNER_PRO_API_KEY"` // RUNNER_PRO_API_KEY
+ ProAPITLSInsecure bool `envconfig:"RUNNER_PRO_API_TLS_INSECURE"` // RUNNER_PRO_API_TLS_INSECURE
+ ProAPIURL string `envconfig:"RUNNER_PRO_API_URL"` // RUNNER_PRO_API_URL
+ ProConnectionTimeoutSec int `envconfig:"RUNNER_PRO_CONNECTION_TIMEOUT" default:"10"` // RUNNER_PRO_CONNECTION_TIMEOUT
+ ProAPISkipVerify bool `envconfig:"RUNNER_PRO_API_SKIP_VERIFY" default:"false"` // RUNNER_PRO_API_SKIP_VERIFY
+ ProAPICertFile string `envconfig:"RUNNER_PRO_API_CERT_FILE"` // RUNNER_PRO_API_CERT_FILE
+ ProAPIKeyFile string `envconfig:"RUNNER_PRO_API_KEY_FILE"` // RUNNER_PRO_API_KEY_FILE
+ ProAPICAFile string `envconfig:"RUNNER_PRO_API_CA_FILE"` // RUNNER_PRO_API_CA_FILE
SlavesConfigs string `envconfig:"RUNNER_SLAVES_CONFIGS"` // RUNNER_SLAVES_CONFIGS
}
@@ -52,7 +61,7 @@ func LoadTestkubeVariables() (Params, error) {
if err != nil {
return params, errors.Errorf("failed to read environment variables: %v", err)
}
-
+ cleanDeprecatedParams(¶ms)
return params, nil
}
@@ -81,12 +90,19 @@ func PrintParams(params Params) {
output.PrintLogf("RUNNER_CLUSTERID=\"%s\"", params.ClusterID)
output.PrintLogf("RUNNER_CDEVENTS_TARGET=\"%s\"", params.CDEventsTarget)
output.PrintLogf("RUNNER_DASHBOARD_URI=\"%s\"", params.DashboardURI)
- output.PrintLogf("RUNNER_CLOUD_MODE=\"%t\"", params.CloudMode)
- output.PrintLogf("RUNNER_CLOUD_API_TLS_INSECURE=\"%t\"", params.CloudAPITLSInsecure)
- output.PrintLogf("RUNNER_CLOUD_API_URL=\"%s\"", params.CloudAPIURL)
- printSensitiveParam("RUNNER_CLOUD_API_KEY", params.CloudAPIKey)
- output.PrintLogf("RUNNER_CLOUD_CONNECTION_TIMEOUT=%d", params.CloudConnectionTimeoutSec)
- output.PrintLogf("RUNNER_CLOUD_API_SKIP_VERIFY=\"%t\"", params.CloudAPISkipVerify)
+ output.PrintLogf("RUNNER_CLOUD_MODE=\"%t\" - DEPRECATED: please use RUNNER_PRO_MODE instead", params.CloudMode)
+ output.PrintLogf("RUNNER_CLOUD_API_TLS_INSECURE=\"%t\" - DEPRECATED: please use RUNNER_PRO_API_TLS_INSECURE instead", params.CloudAPITLSInsecure)
+ output.PrintLogf("RUNNER_CLOUD_API_URL=\"%s\" - DEPRECATED: please use RUNNER_PRO_API_URL instead", params.CloudAPIURL)
+ printSensitiveDeprecatedParam("RUNNER_CLOUD_API_KEY", params.CloudAPIKey, "RUNNER_PRO_API_KEY")
+ output.PrintLogf("RUNNER_CLOUD_CONNECTION_TIMEOUT=%d - DEPRECATED: please use RUNNER_PRO_CONNECTION_TIMEOUT instead", params.CloudConnectionTimeoutSec)
+ output.PrintLogf("RUNNER_CLOUD_API_SKIP_VERIFY=\"%t\" - DEPRECATED: please use RUNNER_PRO_API_SKIP_VERIFY instead", params.CloudAPISkipVerify)
+ output.PrintLogf("RUNNER_PRO_MODE=\"%t\"", params.ProMode)
+ output.PrintLogf("RUNNER_PRO_API_TLS_INSECURE=\"%t\"", params.ProAPITLSInsecure)
+ output.PrintLogf("RUNNER_PRO_API_URL=\"%s\"", params.ProAPIURL)
+ printSensitiveParam("RUNNER_PRO_API_KEY", params.ProAPIKey)
+ output.PrintLogf("RUNNER_PRO_CONNECTION_TIMEOUT=%d", params.ProConnectionTimeoutSec)
+ output.PrintLogf("RUNNER_PRO_API_SKIP_VERIFY=\"%t\"", params.ProAPISkipVerify)
+
}
// printSensitiveParam shows in logs if a parameter is set or not
@@ -97,3 +113,39 @@ func printSensitiveParam(name string, value string) {
output.PrintLogf("%s=\"********\"", name)
}
}
+
+// printSensitiveDeprecatedParam shows in logs if a parameter is set or not
+func printSensitiveDeprecatedParam(name string, value string, newName string) {
+ if len(value) == 0 {
+ output.PrintLogf("%s=\"\" - DEPRECATED: please use %s instead", name, newName)
+ } else {
+ output.PrintLogf("%s=\"********\" - DEPRECATED: please use %s instead", name, newName)
+ }
+}
+
+// cleanDeprecatedParams makes sure deprecated parameter values are set in replacements
+func cleanDeprecatedParams(params *Params) {
+ if !params.ProMode && params.CloudMode {
+ params.ProMode = params.CloudMode
+ }
+
+ if params.ProAPIKey == "" && params.CloudAPIKey != "" {
+ params.ProAPIKey = params.CloudAPIKey
+ }
+
+ if !params.ProAPITLSInsecure && params.CloudAPITLSInsecure {
+ params.ProAPITLSInsecure = params.CloudAPITLSInsecure
+ }
+
+ if params.ProAPIURL == "" && params.CloudAPIURL != "" {
+ params.ProAPIURL = params.CloudAPIURL
+ }
+
+ if params.ProConnectionTimeoutSec == 0 && params.CloudConnectionTimeoutSec != 0 {
+ params.ProConnectionTimeoutSec = params.CloudConnectionTimeoutSec
+ }
+
+ if !params.ProAPISkipVerify && params.CloudAPISkipVerify {
+ params.ProAPISkipVerify = params.CloudAPISkipVerify
+ }
+}
diff --git a/pkg/event/bus/nats.go b/pkg/event/bus/nats.go
index 87e8dbd852a..484b9b4a14c 100644
--- a/pkg/event/bus/nats.go
+++ b/pkg/event/bus/nats.go
@@ -1,8 +1,10 @@
package bus
import (
+ "crypto/tls"
"fmt"
"sync"
+ "time"
"github.com/nats-io/nats.go"
@@ -22,18 +24,40 @@ const (
InternalSubscribeTopic = "internal.>"
)
-func NewNATSConnection(uri string, opts ...nats.Option) (*nats.Conn, error) {
- nc, err := nats.Connect(uri, opts...)
- if err != nil {
- log.DefaultLogger.Fatalw("error connecting to nats", "error", err)
- return nil, err
+type ConnectionConfig struct {
+ NatsURI string
+ NatsSecure bool
+ NatsSkipVerify bool
+ NatsCertFile string
+ NatsKeyFile string
+ NatsCAFile string
+ NatsConnectTimeout time.Duration
+}
+
+func optsFromConfig(cfg ConnectionConfig) (opts []nats.Option) {
+ opts = []nats.Option{}
+ if cfg.NatsSecure {
+ if cfg.NatsSkipVerify {
+ opts = append(opts, nats.Secure(&tls.Config{InsecureSkipVerify: true}))
+ } else {
+ opts = append(opts, nats.ClientCert(cfg.NatsCertFile, cfg.NatsKeyFile))
+ if cfg.NatsCAFile != "" {
+ opts = append(opts, nats.RootCAs(cfg.NatsCAFile))
+ }
+ }
}
- return nc, nil
+ if cfg.NatsConnectTimeout > 0 {
+ opts = append(opts, nats.Timeout(cfg.NatsConnectTimeout))
+ }
+
+ return opts
}
-func NewNATSEncoddedConnection(uri string, opts ...nats.Option) (*nats.EncodedConn, error) {
- nc, err := nats.Connect(uri, opts...)
+func NewNATSEncodedConnection(cfg ConnectionConfig, opts ...nats.Option) (*nats.EncodedConn, error) {
+ opts = append(opts, optsFromConfig(cfg)...)
+
+ nc, err := NewNATSConnection(cfg, opts...)
if err != nil {
log.DefaultLogger.Fatalw("error connecting to nats", "error", err)
return nil, err
@@ -46,9 +70,25 @@ func NewNATSEncoddedConnection(uri string, opts ...nats.Option) (*nats.EncodedCo
return nil, err
}
+ if err != nil {
+ log.DefaultLogger.Errorw("error creating NATS connection", "error", err)
+ }
+
return ec, nil
}
+func NewNATSConnection(cfg ConnectionConfig, opts ...nats.Option) (*nats.Conn, error) {
+ opts = append(opts, optsFromConfig(cfg)...)
+
+ nc, err := nats.Connect(cfg.NatsURI, opts...)
+ if err != nil {
+ log.DefaultLogger.Fatalw("error connecting to nats", "error", err)
+ return nil, err
+ }
+
+ return nc, nil
+}
+
func NewNATSBus(nc *nats.EncodedConn) *NATSBus {
return &NATSBus{
nc: nc,
diff --git a/pkg/event/emitter.go b/pkg/event/emitter.go
index f14ec650491..3a49a04b612 100644
--- a/pkg/event/emitter.go
+++ b/pkg/event/emitter.go
@@ -38,7 +38,7 @@ type Emitter struct {
Listeners common.Listeners
Loader *Loader
Log *zap.SugaredLogger
- mutex sync.Mutex
+ mutex sync.RWMutex
Bus bus.Bus
ClusterName string
Envs map[string]string
@@ -195,8 +195,20 @@ func (e *Emitter) Reconcile(ctx context.Context) {
default:
listeners := e.Loader.Reconcile()
e.UpdateListeners(listeners)
- e.Log.Debugw("reconciled listeners", e.Listeners.Log()...)
+ e.Log.Debugw("reconciled listeners", e.Logs()...)
time.Sleep(reconcileInterval)
}
}
}
+
+func (e *Emitter) Logs() []any {
+ e.mutex.Lock()
+ defer e.mutex.Unlock()
+ return e.Listeners.Log()
+}
+
+func (e *Emitter) GetListeners() common.Listeners {
+ e.mutex.RLock()
+ defer e.mutex.RUnlock()
+ return e.Listeners
+}
diff --git a/pkg/event/emitter_integration_test.go b/pkg/event/emitter_integration_test.go
index a15a5e1c23d..b699e5ede6e 100644
--- a/pkg/event/emitter_integration_test.go
+++ b/pkg/event/emitter_integration_test.go
@@ -20,7 +20,10 @@ import (
func GetTestNATSEmitter() *Emitter {
os.Setenv("DEBUG", "true")
// configure NATS event bus
- nc, err := bus.NewNATSEncoddedConnection("http://localhost:4222")
+ nc, err := bus.NewNATSEncodedConnection(bus.ConnectionConfig{
+ NatsURI: "http://localhost:4222",
+ })
+
if err != nil {
panic(err)
}
diff --git a/pkg/event/emitter_test.go b/pkg/event/emitter_test.go
index 52e802dc83a..52198f5f2a9 100644
--- a/pkg/event/emitter_test.go
+++ b/pkg/event/emitter_test.go
@@ -140,7 +140,7 @@ func TestEmitter_Reconcile(t *testing.T) {
go emitter.Reconcile(ctx)
time.Sleep(100 * time.Millisecond)
- assert.Len(t, emitter.Listeners, 4)
+ assert.Len(t, emitter.GetListeners(), 4)
cancel()
@@ -155,7 +155,7 @@ func TestEmitter_Reconcile(t *testing.T) {
// then each reconciler (3 reconcilers) should load 2 listeners
time.Sleep(100 * time.Millisecond)
- assert.Len(t, emitter.Listeners, 6)
+ assert.Len(t, emitter.GetListeners(), 6)
cancel()
})
diff --git a/pkg/event/kind/cdevent/listener.go b/pkg/event/kind/cdevent/listener.go
index ec82d80df5d..e4493887024 100644
--- a/pkg/event/kind/cdevent/listener.go
+++ b/pkg/event/kind/cdevent/listener.go
@@ -62,7 +62,12 @@ func (l *CDEventListener) Metadata() map[string]string {
func (l *CDEventListener) Notify(event testkube.Event) (result testkube.EventResult) {
// Create the base event
- ev, err := cde.MapTestkubeEventToCDEvent(event, l.clusterID, l.defaultNamespace, l.dashboardURI)
+ namespace := l.defaultNamespace
+ if event.TestExecution != nil {
+ namespace = event.TestExecution.TestNamespace
+ }
+
+ ev, err := cde.MapTestkubeEventToCDEvent(event, l.clusterID, namespace, l.dashboardURI)
if err != nil {
return testkube.NewFailedEventResult(event.Id, err)
}
diff --git a/pkg/executor/client/common.go b/pkg/executor/client/common.go
index a88be2ff854..246e87f5e46 100644
--- a/pkg/executor/client/common.go
+++ b/pkg/executor/client/common.go
@@ -13,8 +13,8 @@ import (
executorv1 "github.com/kubeshop/testkube-operator/api/executor/v1"
testsv3 "github.com/kubeshop/testkube-operator/api/tests/v3"
- "github.com/kubeshop/testkube/internal/featureflags"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/featureflags"
"github.com/kubeshop/testkube/pkg/utils"
)
@@ -23,18 +23,20 @@ const (
)
type ExecuteOptions struct {
- ID string
- TestName string
- Namespace string
- TestSpec testsv3.TestSpec
- ExecutorName string
- ExecutorSpec executorv1.ExecutorSpec
- Request testkube.ExecutionRequest
- Sync bool
- Labels map[string]string
- UsernameSecret *testkube.SecretRef
- TokenSecret *testkube.SecretRef
- CertificateSecret string
+ ID string
+ TestName string
+ Namespace string
+ TestSpec testsv3.TestSpec
+ ExecutorName string
+ ExecutorSpec executorv1.ExecutorSpec
+ Request testkube.ExecutionRequest
+ Sync bool
+ Labels map[string]string
+ UsernameSecret *testkube.SecretRef
+ TokenSecret *testkube.SecretRef
+ CertificateSecret string
+ // AgentAPITLSSecret is a secret name that contains TLS certificate for Agent (gRPC) API
+ AgentAPITLSSecret string
ImagePullSecretNames []string
Features featureflags.FeatureFlags
}
diff --git a/pkg/executor/client/interface.go b/pkg/executor/client/interface.go
index 18e8f27eea1..3b6b4f3dba7 100644
--- a/pkg/executor/client/interface.go
+++ b/pkg/executor/client/interface.go
@@ -26,7 +26,7 @@ type Executor interface {
// Abort aborts pending execution, do nothing when there is no pending execution
Abort(ctx context.Context, execution *testkube.Execution) (result *testkube.ExecutionResult, err error)
- Logs(ctx context.Context, id string) (logs chan output.Output, err error)
+ Logs(ctx context.Context, id, namespace string) (logs chan output.Output, err error)
}
// HTTPClient interface for getting REST based requests
diff --git a/pkg/executor/client/job.go b/pkg/executor/client/job.go
index 26f1813f96a..aa9bdb03221 100644
--- a/pkg/executor/client/job.go
+++ b/pkg/executor/client/job.go
@@ -13,7 +13,7 @@ import (
"text/template"
"time"
- "github.com/kubeshop/testkube/internal/featureflags"
+ "github.com/kubeshop/testkube/pkg/featureflags"
"github.com/kubeshop/testkube/pkg/repository/config"
"github.com/pkg/errors"
@@ -44,6 +44,8 @@ import (
"github.com/kubeshop/testkube/pkg/executor/env"
"github.com/kubeshop/testkube/pkg/executor/output"
"github.com/kubeshop/testkube/pkg/log"
+ logsclient "github.com/kubeshop/testkube/pkg/logs/client"
+ "github.com/kubeshop/testkube/pkg/logs/events"
testexecutionsmapper "github.com/kubeshop/testkube/pkg/mapper/testexecutions"
testsmapper "github.com/kubeshop/testkube/pkg/mapper/tests"
"github.com/kubeshop/testkube/pkg/telemetry"
@@ -75,10 +77,9 @@ const (
// NewJobExecutor creates new job executor
func NewJobExecutor(
repo result.Repository,
- namespace string,
images executor.Images,
templates executor.Templates,
- serviceAccountName string,
+ serviceAccountNames map[string]string,
metrics ExecutionCounter,
emiter *event.Emitter,
configMap config.Repository,
@@ -93,15 +94,20 @@ func NewJobExecutor(
apiURI string,
natsURI string,
debug bool,
+ logsStream logsclient.Stream,
+ features featureflags.FeatureFlags,
) (client *JobExecutor, err error) {
+ if serviceAccountNames == nil {
+ serviceAccountNames = make(map[string]string)
+ }
+
return &JobExecutor{
ClientSet: clientset,
Repository: repo,
Log: log.DefaultLogger,
- Namespace: namespace,
images: images,
templates: templates,
- serviceAccountName: serviceAccountName,
+ serviceAccountNames: serviceAccountNames,
metrics: metrics,
Emitter: emiter,
configMap: configMap,
@@ -115,6 +121,8 @@ func NewJobExecutor(
apiURI: apiURI,
natsURI: natsURI,
debug: debug,
+ logsStream: logsStream,
+ features: features,
}, nil
}
@@ -127,11 +135,10 @@ type JobExecutor struct {
Repository result.Repository
Log *zap.SugaredLogger
ClientSet kubernetes.Interface
- Namespace string
Cmd string
images executor.Images
templates executor.Templates
- serviceAccountName string
+ serviceAccountNames map[string]string
metrics ExecutionCounter
Emitter *event.Emitter
configMap config.Repository
@@ -145,6 +152,8 @@ type JobExecutor struct {
apiURI string
natsURI string
debug bool
+ logsStream logsclient.Stream
+ features featureflags.FeatureFlags
}
type JobOptions struct {
@@ -163,6 +172,7 @@ type JobOptions struct {
UsernameSecret *testkube.SecretRef
TokenSecret *testkube.SecretRef
CertificateSecret string
+ AgentAPITLSSecret string
Variables map[string]testkube.Variable
ActiveDeadlineSeconds int64
ServiceAccountName string
@@ -188,7 +198,7 @@ type JobOptions struct {
}
// Logs returns job logs stream channel using kubernetes api
-func (c *JobExecutor) Logs(ctx context.Context, id string) (out chan output.Output, err error) {
+func (c *JobExecutor) Logs(ctx context.Context, id, namespace string) (out chan output.Output, err error) {
out = make(chan output.Output)
logs := make(chan []byte)
@@ -198,7 +208,7 @@ func (c *JobExecutor) Logs(ctx context.Context, id string) (out chan output.Outp
close(out)
}()
- if err := c.TailJobLogs(ctx, id, logs); err != nil {
+ if err := c.TailJobLogs(ctx, id, namespace, logs); err != nil {
out <- output.NewOutputError(err)
return
}
@@ -225,11 +235,14 @@ func (c *JobExecutor) Execute(ctx context.Context, execution *testkube.Execution
if err != nil {
return result.Err(err), err
}
+
+ c.streamLog(ctx, execution.Id, events.NewLog("created kubernetes job").WithSource(events.SourceJobExecutor))
+
if !options.Sync {
- go c.MonitorJobForTimeout(ctx, execution.Id)
+ go c.MonitorJobForTimeout(ctx, execution.Id, execution.TestNamespace)
}
- podsClient := c.ClientSet.CoreV1().Pods(c.Namespace)
+ podsClient := c.ClientSet.CoreV1().Pods(execution.TestNamespace)
pods, err := executor.GetJobPods(ctx, podsClient, execution.Id, 1, 10)
if err != nil {
return result.Err(err), err
@@ -237,6 +250,8 @@ func (c *JobExecutor) Execute(ctx context.Context, execution *testkube.Execution
l := c.Log.With("executionID", execution.Id, "type", "async")
+ c.streamLog(ctx, execution.Id, events.NewLog("waiting for pod to spin up").WithSource(events.SourceJobExecutor))
+
for _, pod := range pods.Items {
if pod.Status.Phase != corev1.PodRunning && pod.Labels["job-name"] == execution.Id {
// for sync block and complete
@@ -258,10 +273,10 @@ func (c *JobExecutor) Execute(ctx context.Context, execution *testkube.Execution
l.Debugw("no pods was found", "totalPodsCount", len(pods.Items))
- return testkube.NewRunningExecutionResult(), nil
+ return result, nil
}
-func (c *JobExecutor) MonitorJobForTimeout(ctx context.Context, jobName string) {
+func (c *JobExecutor) MonitorJobForTimeout(ctx context.Context, jobName, namespace string) {
ticker := time.NewTicker(pollJobStatus)
l := c.Log.With("jobName", jobName)
for {
@@ -270,7 +285,7 @@ func (c *JobExecutor) MonitorJobForTimeout(ctx context.Context, jobName string)
l.Infow("context done, stopping job timeout monitor")
return
case <-ticker.C:
- jobs, err := c.ClientSet.BatchV1().Jobs(c.Namespace).List(ctx, metav1.ListOptions{LabelSelector: "job-name=" + jobName})
+ jobs, err := c.ClientSet.BatchV1().Jobs(namespace).List(ctx, metav1.ListOptions{LabelSelector: "job-name=" + jobName})
if err != nil {
l.Errorw("could not get jobs", "error", err)
return
@@ -282,7 +297,7 @@ func (c *JobExecutor) MonitorJobForTimeout(ctx context.Context, jobName string)
job := jobs.Items[0]
if job.Status.Succeeded > 0 {
- l.Debugw("job succeeded", "status")
+ l.Debugw("job succeeded", "status", "succeded")
return
}
@@ -308,9 +323,9 @@ func (c *JobExecutor) MonitorJobForTimeout(ctx context.Context, jobName string)
// CreateJob creates new Kubernetes job based on execution and execute options
func (c *JobExecutor) CreateJob(ctx context.Context, execution testkube.Execution, options ExecuteOptions) error {
- jobs := c.ClientSet.BatchV1().Jobs(c.Namespace)
+ jobs := c.ClientSet.BatchV1().Jobs(execution.TestNamespace)
jobOptions, err := NewJobOptions(c.Log, c.templatesClient, c.images, c.templates,
- c.serviceAccountName, c.registry, c.clusterID, c.apiURI, execution, options, c.natsURI, c.debug)
+ c.serviceAccountNames, c.registry, c.clusterID, c.apiURI, execution, options, c.natsURI, c.debug)
if err != nil {
return err
}
@@ -318,7 +333,7 @@ func (c *JobExecutor) CreateJob(ctx context.Context, execution testkube.Executio
if jobOptions.ArtifactRequest != nil &&
jobOptions.ArtifactRequest.StorageClassName != "" {
c.Log.Debug("creating persistent volume claim with options", "options", jobOptions)
- pvcsClient := c.ClientSet.CoreV1().PersistentVolumeClaims(c.Namespace)
+ pvcsClient := c.ClientSet.CoreV1().PersistentVolumeClaims(execution.TestNamespace)
pvcSpec, err := NewPersistentVolumeClaimSpec(c.Log, NewPVCOptionsFromJobOptions(jobOptions))
if err != nil {
return err
@@ -347,29 +362,34 @@ func (c *JobExecutor) updateResultsFromPod(ctx context.Context, pod corev1.Pod,
// save stop time and final state
defer func() {
if err := c.stopExecution(ctx, l, execution, execution.ExecutionResult, isNegativeTest, err); err != nil {
+ c.streamLog(ctx, execution.Id, events.NewErrorLog(err))
l.Errorw("error stopping execution after updating results from pod", "error", err)
}
}()
// wait for pod to be loggable
- if err = wait.PollUntilContextTimeout(ctx, pollInterval, c.podStartTimeout, true, executor.IsPodLoggable(c.ClientSet, pod.Name, c.Namespace)); err != nil {
+ if err = wait.PollUntilContextTimeout(ctx, pollInterval, c.podStartTimeout, true, executor.IsPodLoggable(c.ClientSet, pod.Name, execution.TestNamespace)); err != nil {
+ c.streamLog(ctx, execution.Id, events.NewErrorLog(errors.Wrap(err, "can't start test job pod")))
l.Errorw("waiting for pod started error", "error", err)
}
l.Debug("poll immediate waiting for pod")
// wait for pod
- if err = wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, executor.IsPodReady(c.ClientSet, pod.Name, c.Namespace)); err != nil {
+ if err = wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, executor.IsPodReady(c.ClientSet, pod.Name, execution.TestNamespace)); err != nil {
// continue on poll err and try to get logs later
+ c.streamLog(ctx, execution.Id, events.NewErrorLog(errors.Wrap(err, "can't read data from pod, pod was not completed")))
l.Errorw("waiting for pod complete error", "error", err)
}
+
if err != nil {
execution.ExecutionResult.Err(err)
}
l.Debug("poll immediate end")
+ c.streamLog(ctx, execution.Id, events.NewLog("analyzing test results and artfacts"))
if execution.ArtifactRequest != nil &&
execution.ArtifactRequest.StorageClassName != "" {
- pvcsClient := c.ClientSet.CoreV1().PersistentVolumeClaims(c.Namespace)
+ pvcsClient := c.ClientSet.CoreV1().PersistentVolumeClaims(execution.TestNamespace)
err = pvcsClient.Delete(ctx, execution.Id+"-pvc", metav1.DeleteOptions{})
if err != nil {
return execution.ExecutionResult, err
@@ -377,16 +397,20 @@ func (c *JobExecutor) updateResultsFromPod(ctx context.Context, pod corev1.Pod,
}
var logs []byte
- logs, err = executor.GetPodLogs(ctx, c.ClientSet, c.Namespace, pod)
+ logs, err = executor.GetPodLogs(ctx, c.ClientSet, execution.TestNamespace, pod)
if err != nil {
l.Errorw("get pod logs error", "error", err)
+ c.streamLog(ctx, execution.Id, events.NewErrorLog(err))
return execution.ExecutionResult, err
}
+ // don't attach logs if logs v2 is enabled - they will be streamed through the logs service
+ attachLogs := !c.features.LogsV2
// parse job output log (JSON stream)
- execution.ExecutionResult, err = output.ParseRunnerOutput(logs)
+ execution.ExecutionResult, err = output.ParseRunnerOutput(logs, attachLogs)
if err != nil {
l.Errorw("parse output error", "error", err)
+ c.streamLog(ctx, execution.Id, events.NewErrorLog(errors.Wrap(err, "can't get test execution job output")))
return execution.ExecutionResult, err
}
@@ -397,6 +421,10 @@ func (c *JobExecutor) updateResultsFromPod(ctx context.Context, pod corev1.Pod,
}
execution.ExecutionResult.ErrorMessage = errorMessage
+
+ c.streamLog(ctx, execution.Id, events.NewErrorLog(errors.Wrap(err, "test execution finished with failed state")))
+ } else {
+ c.streamLog(ctx, execution.Id, events.NewLog("test execution finshed").WithMetadataEntry("status", string(*execution.ExecutionResult.Status)))
}
// saving result in the defer function
@@ -409,21 +437,28 @@ func (c *JobExecutor) stopExecution(ctx context.Context, l *zap.SugaredLogger, e
l.Errorw("get execution error", "error", err)
return err
}
+
+ logEvent := events.NewLog().WithSource(events.SourceJobExecutor)
+
l.Debugw("stopping execution", "executionId", execution.Id, "status", result.Status, "executionStatus", execution.ExecutionResult.Status, "passedError", passedErr, "savedExecutionStatus", savedExecution.ExecutionResult.Status)
+ c.streamLog(ctx, execution.Id, logEvent.WithContent("stopping execution"))
+ defer c.streamLog(ctx, execution.Id, logEvent.WithContent("execution stopped"))
+
if savedExecution.IsCanceled() || savedExecution.IsTimeout() {
+ c.streamLog(ctx, execution.Id, logEvent.WithContent("execution is cancelled"))
return nil
}
execution.Stop()
if isNegativeTest {
if result.IsFailed() {
- l.Infow("test run was expected to fail, and it failed as expected", "test", execution.TestName)
+ l.Debugw("test run was expected to fail, and it failed as expected", "test", execution.TestName)
execution.ExecutionResult.Status = testkube.ExecutionStatusPassed
result.Status = testkube.ExecutionStatusPassed
result.Output = result.Output + "\nTest run was expected to fail, and it failed as expected"
} else {
- l.Infow("test run was expected to fail - the result will be reversed", "test", execution.TestName)
+ l.Debugw("test run was expected to fail - the result will be reversed", "test", execution.TestName)
execution.ExecutionResult.Status = testkube.ExecutionStatusFailed
result.Status = testkube.ExecutionStatusFailed
result.Output = result.Output + "\nTest run was expected to fail, the result will be reversed"
@@ -594,9 +629,9 @@ func NewJobOptionsFromExecutionOptions(options ExecuteOptions) JobOptions {
}
// TailJobLogs - locates logs for job pod(s)
-func (c *JobExecutor) TailJobLogs(ctx context.Context, id string, logs chan []byte) (err error) {
+func (c *JobExecutor) TailJobLogs(ctx context.Context, id, namespace string, logs chan []byte) (err error) {
- podsClient := c.ClientSet.CoreV1().Pods(c.Namespace)
+ podsClient := c.ClientSet.CoreV1().Pods(namespace)
pods, err := executor.GetJobPods(ctx, podsClient, id, 1, 10)
if err != nil {
@@ -622,7 +657,7 @@ func (c *JobExecutor) TailJobLogs(ctx context.Context, id string, logs chan []by
default:
l.Debugw("tailing job logs: waiting for pod to be ready")
- if err = wait.PollUntilContextTimeout(ctx, pollInterval, c.podStartTimeout, true, executor.IsPodLoggable(c.ClientSet, pod.Name, c.Namespace)); err != nil {
+ if err = wait.PollUntilContextTimeout(ctx, pollInterval, c.podStartTimeout, true, executor.IsPodLoggable(c.ClientSet, pod.Name, namespace)); err != nil {
l.Errorw("poll immediate error when tailing logs", "error", err)
return err
}
@@ -659,7 +694,7 @@ func (c *JobExecutor) TailPodLogs(ctx context.Context, pod corev1.Pod, logs chan
}
podLogRequest := c.ClientSet.CoreV1().
- Pods(c.Namespace).
+ Pods(pod.Namespace).
GetLogs(pod.Name, &podLogOptions)
stream, err := podLogRequest.Stream(ctx)
@@ -693,7 +728,7 @@ 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) {
// error line should be last one
- return executor.GetPodLogs(ctx, c.ClientSet, c.Namespace, pod, 1)
+ return executor.GetPodLogs(ctx, c.ClientSet, pod.Namespace, pod, 1)
}
// GetLastLogLineError return error if last line is failed
@@ -719,7 +754,7 @@ func (c *JobExecutor) GetLastLogLineError(ctx context.Context, pod corev1.Pod) e
// Abort aborts K8S by job name
func (c *JobExecutor) Abort(ctx context.Context, execution *testkube.Execution) (result *testkube.ExecutionResult, err error) {
l := c.Log.With("execution", execution.Id)
- result, err = executor.AbortJob(ctx, c.ClientSet, c.Namespace, execution.Id)
+ result, err = executor.AbortJob(ctx, c.ClientSet, execution.TestNamespace, execution.Id)
if err != nil {
l.Errorw("error aborting job", "execution", execution.Id, "error", err)
}
@@ -738,6 +773,9 @@ func (c *JobExecutor) Timeout(ctx context.Context, jobName string) (result *test
l.Errorw("error getting execution", "error", err)
return
}
+
+ c.streamLog(ctx, execution.Id, events.NewLog("execution took too long, pod deadline exceeded"))
+
result = &testkube.ExecutionResult{
Status: testkube.ExecutionStatusTimeout,
}
@@ -748,6 +786,12 @@ func (c *JobExecutor) Timeout(ctx context.Context, jobName string) (result *test
return
}
+func (c *JobExecutor) streamLog(ctx context.Context, id string, log *events.Log) {
+ if c.features.LogsV2 {
+ c.logsStream.Push(ctx, id, log)
+ }
+}
+
// NewJobSpec is a method to create new job spec
func NewJobSpec(log *zap.SugaredLogger, options JobOptions) (*batchv1.Job, error) {
envManager := env.NewManager()
@@ -842,7 +886,7 @@ func NewJobSpec(log *zap.SugaredLogger, options JobOptions) (*batchv1.Job, error
}
func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface, images executor.Images,
- templates executor.Templates, serviceAccountName, registry, clusterID, apiURI string,
+ templates executor.Templates, serviceAccountNames map[string]string, registry, clusterID, apiURI string,
execution testkube.Execution, options ExecuteOptions, natsURI string, debug bool) (jobOptions JobOptions, err error) {
jsn, err := json.Marshal(execution)
if err != nil {
@@ -896,6 +940,11 @@ func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface
}
jobOptions.Variables = execution.Variables
+ serviceAccountName, ok := serviceAccountNames[execution.TestNamespace]
+ if !ok {
+ return jobOptions, fmt.Errorf("not supported namespace %s", execution.TestNamespace)
+ }
+
jobOptions.ServiceAccountName = serviceAccountName
jobOptions.Registry = registry
jobOptions.ClusterID = clusterID
@@ -951,6 +1000,11 @@ func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface
if err != nil {
return jobOptions, err
}
+
+ if jobOptions.Variables == nil {
+ jobOptions.Variables = make(map[string]testkube.Variable)
+ }
+
jobOptions.Variables[executor.SlavesConfigsEnv] = testkube.NewBasicVariable(executor.SlavesConfigsEnv, string(slvesConfigs))
}
@@ -968,6 +1022,9 @@ func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface
}
}
+ // used for adding custom certificates for Agent (gRPC) API
+ jobOptions.AgentAPITLSSecret = options.AgentAPITLSSecret
+
return
}
diff --git a/pkg/executor/client/mock_executor.go b/pkg/executor/client/mock_executor.go
index 9e066b38b8f..100f0a44938 100644
--- a/pkg/executor/client/mock_executor.go
+++ b/pkg/executor/client/mock_executor.go
@@ -67,16 +67,16 @@ func (mr *MockExecutorMockRecorder) Execute(arg0, arg1, arg2 interface{}) *gomoc
}
// Logs mocks base method.
-func (m *MockExecutor) Logs(arg0 context.Context, arg1 string) (chan output.Output, error) {
+func (m *MockExecutor) Logs(arg0 context.Context, arg1, arg2 string) (chan output.Output, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Logs", arg0, arg1)
+ ret := m.ctrl.Call(m, "Logs", arg0, arg1, arg2)
ret0, _ := ret[0].(chan output.Output)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Logs indicates an expected call of Logs.
-func (mr *MockExecutorMockRecorder) Logs(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockExecutorMockRecorder) Logs(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockExecutor)(nil).Logs), arg0, arg1)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockExecutor)(nil).Logs), arg0, arg1, arg2)
}
diff --git a/pkg/executor/common.go b/pkg/executor/common.go
index e832c8f8acf..a76d0aa0ee6 100644
--- a/pkg/executor/common.go
+++ b/pkg/executor/common.go
@@ -23,6 +23,7 @@ import (
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/log"
executorsmapper "github.com/kubeshop/testkube/pkg/mapper/executors"
+ "github.com/kubeshop/testkube/pkg/utils"
)
var ErrPodInitializing = errors.New("PodInitializing")
@@ -44,7 +45,7 @@ const (
var RunnerEnvVars = []corev1.EnvVar{
{
Name: "DEBUG",
- Value: os.Getenv("DEBUG"),
+ Value: getOr("DEBUG", "false"),
},
{
Name: "RUNNER_ENDPOINT",
@@ -103,25 +104,41 @@ var RunnerEnvVars = []corev1.EnvVar{
Value: getOr("COMPRESSARTIFACTS", "false"),
},
{
- Name: "RUNNER_CLOUD_MODE",
- Value: getRunnerCloudMode(),
+ Name: "RUNNER_PRO_MODE",
+ Value: getRunnerProMode(),
},
{
- Name: "RUNNER_CLOUD_API_KEY",
- Value: os.Getenv("TESTKUBE_CLOUD_API_KEY"),
+ Name: "RUNNER_PRO_API_KEY",
+ Value: utils.GetEnvVarWithDeprecation("TESTKUBE_PRO_API_KEY", "TESTKUBE_CLOUD_API_KEY", ""),
},
{
- Name: "RUNNER_CLOUD_API_TLS_INSECURE",
- Value: getOr("TESTKUBE_CLOUD_TLS_INSECURE", "false"),
+ Name: "RUNNER_PRO_API_TLS_INSECURE",
+ Value: utils.GetEnvVarWithDeprecation("TESTKUBE_PRO_TLS_INSECURE", "TESTKUBE_CLOUD_TLS_INSECURE", "false"),
},
{
- Name: "RUNNER_CLOUD_API_URL",
- Value: os.Getenv("TESTKUBE_CLOUD_URL"),
+ Name: "RUNNER_PRO_API_URL",
+ Value: utils.GetEnvVarWithDeprecation("TESTKUBE_PRO_URL", "TESTKUBE_CLOUD_URL", ""),
},
{
- Name: "RUNNER_CLOUD_API_SKIP_VERIFY",
+ Name: "RUNNER_PRO_API_SKIP_VERIFY",
Value: getOr("TESTKUBE_PRO_SKIP_VERIFY", "false"),
},
+ {
+ Name: "RUNNER_PRO_CONNECTION_TIMEOUT",
+ Value: getOr("TESTKUBE_PRO_CONNECTION_TIMEOUT", "10"),
+ },
+ {
+ Name: "RUNNER_PRO_API_CERT_FILE",
+ Value: os.Getenv("TESTKUBE_PRO_CERT_FILE"),
+ },
+ {
+ Name: "RUNNER_PRO_API_KEY_FILE",
+ Value: os.Getenv("TESTKUBE_PRO_KEY_FILE"),
+ },
+ {
+ Name: "RUNNER_PRO_API_CA_FILE",
+ Value: os.Getenv("TESTKUBE_PRO_CA_FILE"),
+ },
{
Name: "RUNNER_DASHBOARD_URI",
Value: os.Getenv("TESTKUBE_DASHBOARD_URI"),
@@ -130,6 +147,31 @@ var RunnerEnvVars = []corev1.EnvVar{
Name: "CI",
Value: "1",
},
+ // DEPRECATED: Use RUNNER_PRO_MODE instead
+ {
+ Name: "RUNNER_CLOUD_MODE",
+ Value: getRunnerProMode(),
+ },
+ // DEPRECATED: Use RUNNER_PRO_API_KEY instead
+ {
+ Name: "RUNNER_CLOUD_API_KEY",
+ Value: utils.GetEnvVarWithDeprecation("TESTKUBE_PRO_API_KEY", "TESTKUBE_CLOUD_API_KEY", ""),
+ },
+ // DEPRECATED: Use RUNNER_PRO_API_TLS_INSECURE instead
+ {
+ Name: "RUNNER_CLOUD_API_TLS_INSECURE",
+ Value: utils.GetEnvVarWithDeprecation("TESTKUBE_PRO_TLS_INSECURE", "TESTKUBE_CLOUD_TLS_INSECURE", "false"),
+ },
+ // DEPRECATED: Use RUNNER_PRO_API_URL instead
+ {
+ Name: "RUNNER_CLOUD_API_URL",
+ Value: utils.GetEnvVarWithDeprecation("TESTKUBE_PRO_URL", "TESTKUBE_CLOUD_URL", ""),
+ },
+ // DEPRECATED: Use RUNNER_PRO_API_SKIP_VERIFY instead
+ {
+ Name: "RUNNER_CLOUD_API_SKIP_VERIFY",
+ Value: getOr("TESTKUBE_PRO_SKIP_VERIFY", "false"),
+ },
}
type SlavesConfigs struct {
@@ -183,9 +225,9 @@ func getOr(key, defaultVal string) string {
return defaultVal
}
-func getRunnerCloudMode() string {
+func getRunnerProMode() string {
val := "false"
- if os.Getenv("TESTKUBE_CLOUD_API_KEY") != "" {
+ if utils.GetEnvVarWithDeprecation("TESTKUBE_PRO_API_KEY", "TESTKUBE_CLOUD_API_KEY", "") != "" {
val = "true"
}
return val
diff --git a/pkg/executor/containerexecutor/containerexecutor.go b/pkg/executor/containerexecutor/containerexecutor.go
index 875825fed1e..f7d8b0185b7 100644
--- a/pkg/executor/containerexecutor/containerexecutor.go
+++ b/pkg/executor/containerexecutor/containerexecutor.go
@@ -6,8 +6,12 @@ import (
"path/filepath"
"time"
- "github.com/kubeshop/testkube/internal/featureflags"
+ "github.com/pkg/errors"
+
+ "github.com/kubeshop/testkube/pkg/featureflags"
+ "github.com/kubeshop/testkube/pkg/imageinspector"
"github.com/kubeshop/testkube/pkg/repository/config"
+ "github.com/kubeshop/testkube/pkg/secret"
"github.com/kubeshop/testkube/pkg/utils"
"github.com/kubeshop/testkube/pkg/repository/result"
@@ -31,6 +35,7 @@ import (
"github.com/kubeshop/testkube/pkg/executor/output"
"github.com/kubeshop/testkube/pkg/k8sclient"
"github.com/kubeshop/testkube/pkg/log"
+ logsclient "github.com/kubeshop/testkube/pkg/logs/client"
testexecutionsmapper "github.com/kubeshop/testkube/pkg/mapper/testexecutions"
testsmapper "github.com/kubeshop/testkube/pkg/mapper/tests"
"github.com/kubeshop/testkube/pkg/telemetry"
@@ -53,10 +58,10 @@ type EventEmitter interface {
// NewContainerExecutor creates new job executor
func NewContainerExecutor(
repo result.Repository,
- namespace string,
images executor.Images,
templates executor.Templates,
- serviceAccountName string,
+ imageInspector imageinspector.Inspector,
+ serviceAccountNames map[string]string,
metrics ExecutionCounter,
emiter EventEmitter,
configMap config.Repository,
@@ -71,21 +76,27 @@ func NewContainerExecutor(
apiURI string,
natsUri string,
debug bool,
+ logsStream logsclient.Stream,
+ features featureflags.FeatureFlags,
) (client *ContainerExecutor, err error) {
clientSet, err := k8sclient.ConnectToK8s()
if err != nil {
return client, err
}
+ if serviceAccountNames == nil {
+ serviceAccountNames = make(map[string]string)
+ }
+
return &ContainerExecutor{
clientSet: clientSet,
repository: repo,
log: log.DefaultLogger,
- namespace: namespace,
images: images,
templates: templates,
+ imageInspector: imageInspector,
configMap: configMap,
- serviceAccountName: serviceAccountName,
+ serviceAccountNames: serviceAccountNames,
metrics: metrics,
emitter: emiter,
testsClient: testsClient,
@@ -99,6 +110,8 @@ func NewContainerExecutor(
apiURI: apiURI,
natsURI: natsUri,
debug: debug,
+ logsStream: logsStream,
+ features: features,
}, nil
}
@@ -111,13 +124,13 @@ type ContainerExecutor struct {
repository result.Repository
log *zap.SugaredLogger
clientSet kubernetes.Interface
- namespace string
images executor.Images
templates executor.Templates
+ imageInspector imageinspector.Inspector
metrics ExecutionCounter
emitter EventEmitter
configMap config.Repository
- serviceAccountName string
+ serviceAccountNames map[string]string
testsClient testsv3.Interface
executorsClient executorsclientv1.Interface
testExecutionsClient testexecutionsv1.Interface
@@ -129,6 +142,8 @@ type ContainerExecutor struct {
apiURI string
natsURI string
debug bool
+ logsStream logsclient.Stream
+ features featureflags.FeatureFlags
}
type JobOptions struct {
@@ -153,6 +168,7 @@ type JobOptions struct {
UsernameSecret *testkube.SecretRef
TokenSecret *testkube.SecretRef
CertificateSecret string
+ AgentAPITLSSecret string
Variables map[string]testkube.Variable
ActiveDeadlineSeconds int64
ArtifactRequest *testkube.ArtifactRequest
@@ -177,7 +193,7 @@ type JobOptions struct {
}
// Logs returns job logs stream channel using kubernetes api
-func (c *ContainerExecutor) Logs(ctx context.Context, id string) (out chan output.Output, err error) {
+func (c *ContainerExecutor) Logs(ctx context.Context, id, namespace string) (out chan output.Output, err error) {
out = make(chan output.Output)
go func() {
@@ -215,7 +231,7 @@ func (c *ContainerExecutor) Logs(ctx context.Context, id string) (out chan outpu
for _, podName := range ids {
logs := make(chan []byte)
- if err := c.TailJobLogs(ctx, podName, logs); err != nil {
+ if err := c.TailJobLogs(ctx, podName, namespace, logs); err != nil {
out <- output.NewOutputError(err)
return
}
@@ -242,7 +258,7 @@ func (c *ContainerExecutor) Execute(ctx context.Context, execution *testkube.Exe
return executionResult, err
}
- podsClient := c.clientSet.CoreV1().Pods(c.namespace)
+ podsClient := c.clientSet.CoreV1().Pods(execution.TestNamespace)
pods, err := executor.GetJobPods(ctx, podsClient, execution.Id, 1, 10)
if err != nil {
executionResult.Err(err)
@@ -254,12 +270,12 @@ func (c *ContainerExecutor) Execute(ctx context.Context, execution *testkube.Exe
for _, pod := range pods.Items {
if pod.Status.Phase != corev1.PodRunning && pod.Labels["job-name"] == execution.Id {
if options.Sync {
- return c.updateResultsFromPod(ctx, pod, l, execution, jobOptions)
+ return c.updateResultsFromPod(ctx, pod, l, execution, jobOptions, options.Request.NegativeTest)
}
// async wait for complete status or error
go func(pod corev1.Pod) {
- _, err := c.updateResultsFromPod(ctx, pod, l, execution, jobOptions)
+ _, err := c.updateResultsFromPod(ctx, pod, l, execution, jobOptions, options.Request.NegativeTest)
if err != nil {
l.Errorw("update results from jobs pod error", "error", err)
}
@@ -276,10 +292,20 @@ func (c *ContainerExecutor) Execute(ctx context.Context, execution *testkube.Exe
// createJob creates new Kubernetes job based on execution and execute options
func (c *ContainerExecutor) createJob(ctx context.Context, execution testkube.Execution, options client.ExecuteOptions) (*JobOptions, error) {
- jobsClient := c.clientSet.BatchV1().Jobs(c.namespace)
+ jobsClient := c.clientSet.BatchV1().Jobs(execution.TestNamespace)
- jobOptions, err := NewJobOptions(c.log, c.templatesClient, c.images, c.templates, c.serviceAccountName,
- c.registry, c.clusterID, c.apiURI, execution, options, c.natsURI, c.debug)
+ // Fallback to one-time inspector when non-default namespace is needed
+ inspector := c.imageInspector
+ if len(options.ImagePullSecretNames) > 0 && options.Namespace != "" && execution.TestNamespace != options.Namespace {
+ secretClient, err := secret.NewClient(options.Namespace)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to build secrets client")
+ }
+ inspector = imageinspector.NewInspector(c.registry, imageinspector.NewSkopeoFetcher(), imageinspector.NewSecretFetcher(secretClient))
+ }
+
+ jobOptions, err := NewJobOptions(c.log, c.templatesClient, c.images, c.templates, inspector,
+ c.serviceAccountNames, c.registry, c.clusterID, c.apiURI, execution, options, c.natsURI, c.debug)
if err != nil {
return nil, err
}
@@ -287,7 +313,7 @@ func (c *ContainerExecutor) createJob(ctx context.Context, execution testkube.Ex
if jobOptions.ArtifactRequest != nil &&
jobOptions.ArtifactRequest.StorageClassName != "" {
c.log.Debug("creating persistent volume claim with options", "options", jobOptions)
- pvcsClient := c.clientSet.CoreV1().PersistentVolumeClaims(c.namespace)
+ pvcsClient := c.clientSet.CoreV1().PersistentVolumeClaims(execution.TestNamespace)
pvcSpec, err := client.NewPersistentVolumeClaimSpec(c.log, NewPVCOptionsFromJobOptions(*jobOptions))
if err != nil {
return nil, err
@@ -316,17 +342,18 @@ func (c *ContainerExecutor) updateResultsFromPod(
l *zap.SugaredLogger,
execution *testkube.Execution,
jobOptions *JobOptions,
+ isNegativeTest bool,
) (*testkube.ExecutionResult, error) {
var err error
// save stop time and final state
- defer c.stopExecution(ctx, execution, execution.ExecutionResult)
+ defer c.stopExecution(ctx, execution, execution.ExecutionResult, isNegativeTest)
// wait for pod
l.Debug("poll immediate waiting for executor pod")
- if err = wait.PollUntilContextTimeout(ctx, pollInterval, c.podStartTimeout, true, executor.IsPodLoggable(c.clientSet, executorPod.Name, c.namespace)); err != nil {
+ if err = wait.PollUntilContextTimeout(ctx, pollInterval, c.podStartTimeout, true, executor.IsPodLoggable(c.clientSet, executorPod.Name, execution.TestNamespace)); err != nil {
l.Errorw("waiting for executor pod started error", "error", err)
- } else if err = wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, executor.IsPodReady(c.clientSet, executorPod.Name, c.namespace)); err != nil {
+ } else if err = wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, executor.IsPodReady(c.clientSet, executorPod.Name, execution.TestNamespace)); err != nil {
// continue on poll err and try to get logs later
l.Errorw("waiting for executor pod complete error", "error", err)
}
@@ -336,7 +363,7 @@ func (c *ContainerExecutor) updateResultsFromPod(
l.Debug("poll executor immediate end")
// we need to retrieve the Pod to get its latest status
- podsClient := c.clientSet.CoreV1().Pods(c.namespace)
+ podsClient := c.clientSet.CoreV1().Pods(execution.TestNamespace)
latestExecutorPod, err := podsClient.Get(context.Background(), executorPod.Name, metav1.GetOptions{})
if err != nil {
return execution.ExecutionResult, err
@@ -346,7 +373,7 @@ func (c *ContainerExecutor) updateResultsFromPod(
if jobOptions.ArtifactRequest != nil &&
jobOptions.ArtifactRequest.StorageClassName != "" {
c.log.Debug("creating scraper job with options", "options", jobOptions)
- jobsClient := c.clientSet.BatchV1().Jobs(c.namespace)
+ jobsClient := c.clientSet.BatchV1().Jobs(execution.TestNamespace)
scraperSpec, err := NewScraperJobSpec(c.log, jobOptions)
if err != nil {
return execution.ExecutionResult, err
@@ -367,9 +394,9 @@ func (c *ContainerExecutor) updateResultsFromPod(
for _, scraperPod := range scraperPods.Items {
if scraperPod.Status.Phase != corev1.PodRunning && scraperPod.Labels["job-name"] == scraperPodName {
l.Debug("poll immediate waiting for scraper pod to succeed")
- if err = wait.PollUntilContextTimeout(ctx, pollInterval, c.podStartTimeout, true, executor.IsPodLoggable(c.clientSet, scraperPod.Name, c.namespace)); err != nil {
+ if err = wait.PollUntilContextTimeout(ctx, pollInterval, c.podStartTimeout, true, executor.IsPodLoggable(c.clientSet, scraperPod.Name, execution.TestNamespace)); err != nil {
l.Errorw("waiting for scraper pod started error", "error", err)
- } else if err = wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, executor.IsPodReady(c.clientSet, scraperPod.Name, c.namespace)); err != nil {
+ } else if err = wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, executor.IsPodReady(c.clientSet, scraperPod.Name, execution.TestNamespace)); err != nil {
// continue on poll err and try to get logs later
l.Errorw("waiting for scraper pod complete error", "error", err)
}
@@ -380,7 +407,7 @@ func (c *ContainerExecutor) updateResultsFromPod(
return execution.ExecutionResult, err
}
- pvcsClient := c.clientSet.CoreV1().PersistentVolumeClaims(c.namespace)
+ pvcsClient := c.clientSet.CoreV1().PersistentVolumeClaims(execution.TestNamespace)
err = pvcsClient.Delete(ctx, execution.Id+"-pvc", metav1.DeleteOptions{})
if err != nil {
return execution.ExecutionResult, err
@@ -393,7 +420,7 @@ func (c *ContainerExecutor) updateResultsFromPod(
execution.ExecutionResult.Error()
}
- scraperLogs, err = executor.GetPodLogs(ctx, c.clientSet, c.namespace, *latestScraperPod)
+ scraperLogs, err = executor.GetPodLogs(ctx, c.clientSet, execution.TestNamespace, *latestScraperPod)
if err != nil {
l.Errorw("get scraper pod logs error", "error", err)
return execution.ExecutionResult, err
@@ -413,7 +440,7 @@ func (c *ContainerExecutor) updateResultsFromPod(
}
}
- executorLogs, err := executor.GetPodLogs(ctx, c.clientSet, c.namespace, *latestExecutorPod)
+ executorLogs, err := executor.GetPodLogs(ctx, c.clientSet, execution.TestNamespace, *latestExecutorPod)
if err != nil {
l.Errorw("get executor pod logs error", "error", err)
execution.ExecutionResult.Err(err)
@@ -442,7 +469,12 @@ func (c *ContainerExecutor) updateResultsFromPod(
if executionResult != nil {
execution.ExecutionResult = executionResult
}
- execution.ExecutionResult.Output = output
+
+ // don't attach logs if logs v2 is enabled - they will be streamed through the logs service
+ attachLogs := !c.features.LogsV2
+ if attachLogs {
+ execution.ExecutionResult.Output = output
+ }
if execution.ExecutionResult.IsFailed() {
errorMessage := execution.ExecutionResult.ErrorMessage
@@ -461,9 +493,33 @@ func (c *ContainerExecutor) updateResultsFromPod(
return execution.ExecutionResult, nil
}
-func (c *ContainerExecutor) stopExecution(ctx context.Context, execution *testkube.Execution, result *testkube.ExecutionResult) {
- c.log.Debug("stopping execution")
+func (c *ContainerExecutor) stopExecution(ctx context.Context,
+ execution *testkube.Execution,
+ result *testkube.ExecutionResult,
+ isNegativeTest bool,
+) {
+ c.log.Debugw("stopping execution", "isNegativeTest", isNegativeTest, "test", execution.TestName)
execution.Stop()
+
+ if isNegativeTest {
+ if result.IsFailed() {
+ c.log.Debugw("test run was expected to fail, and it failed as expected", "test", execution.TestName)
+ execution.ExecutionResult.Status = testkube.ExecutionStatusPassed
+ result.Status = testkube.ExecutionStatusPassed
+ result.Output = result.Output + "\nTest run was expected to fail, and it failed as expected"
+ } else {
+ c.log.Debugw("test run was expected to fail - the result will be reversed", "test", execution.TestName)
+ execution.ExecutionResult.Status = testkube.ExecutionStatusFailed
+ result.Status = testkube.ExecutionStatusFailed
+ result.Output = result.Output + "\nTest run was expected to fail, the result will be reversed"
+ }
+
+ err := c.repository.UpdateResult(ctx, execution.Id, *execution)
+ if err != nil {
+ c.log.Errorw("Update execution result error", "error", err)
+ }
+ }
+
err := c.repository.EndExecution(ctx, *execution)
if err != nil {
c.log.Errorw("Update execution result error", "error", err)
@@ -557,43 +613,7 @@ func (c *ContainerExecutor) stopExecution(ctx context.Context, execution *testku
// NewJobOptionsFromExecutionOptions compose JobOptions based on ExecuteOptions
func NewJobOptionsFromExecutionOptions(options client.ExecuteOptions) *JobOptions {
- // for args, command and image, HTTP request takes priority, then test spec, then executor
- var args []string
- argsMode := options.Request.ArgsMode
- if options.TestSpec.ExecutionRequest != nil && argsMode == "" {
- argsMode = string(options.TestSpec.ExecutionRequest.ArgsMode)
- }
-
- if argsMode == string(testkube.ArgsModeTypeAppend) || argsMode == "" {
- args = options.Request.Args
- if options.TestSpec.ExecutionRequest != nil && len(args) == 0 {
- args = options.TestSpec.ExecutionRequest.Args
- }
-
- args = append(options.ExecutorSpec.Args, args...)
- }
-
- if argsMode == string(testkube.ArgsModeTypeOverride) {
- args = options.Request.Args
- if options.TestSpec.ExecutionRequest != nil && len(args) == 0 {
- args = options.TestSpec.ExecutionRequest.Args
- }
- }
-
- var command []string
- if len(options.ExecutorSpec.Command) != 0 {
- command = options.ExecutorSpec.Command
- }
-
- if options.TestSpec.ExecutionRequest != nil &&
- len(options.TestSpec.ExecutionRequest.Command) != 0 {
- command = options.TestSpec.ExecutionRequest.Command
- }
-
- if len(options.Request.Command) != 0 {
- command = options.Request.Command
- }
-
+ // for image, HTTP request takes priority, then test spec, then executor
var image string
if options.ExecutorSpec.Image != "" {
image = options.ExecutorSpec.Image
@@ -652,8 +672,8 @@ func NewJobOptionsFromExecutionOptions(options client.ExecuteOptions) *JobOption
return &JobOptions{
Image: image,
ImagePullSecrets: options.ImagePullSecretNames,
- Args: args,
- Command: command,
+ Args: options.Request.Args,
+ Command: options.Request.Command,
WorkingDir: workingDir,
TestName: options.TestName,
Namespace: options.Namespace,
@@ -664,6 +684,7 @@ func NewJobOptionsFromExecutionOptions(options client.ExecuteOptions) *JobOption
UsernameSecret: options.UsernameSecret,
TokenSecret: options.TokenSecret,
CertificateSecret: options.CertificateSecret,
+ AgentAPITLSSecret: options.AgentAPITLSSecret,
ActiveDeadlineSeconds: options.Request.ActiveDeadlineSeconds,
ArtifactRequest: artifactRequest,
DelaySeconds: jobDelaySeconds,
@@ -683,7 +704,7 @@ func NewJobOptionsFromExecutionOptions(options client.ExecuteOptions) *JobOption
// Abort K8sJob aborts K8S by job name
func (c *ContainerExecutor) Abort(ctx context.Context, execution *testkube.Execution) (*testkube.ExecutionResult, error) {
- return executor.AbortJob(ctx, c.clientSet, c.namespace, execution.Id)
+ return executor.AbortJob(ctx, c.clientSet, execution.TestNamespace, execution.Id)
}
func NewPVCOptionsFromJobOptions(options JobOptions) client.PVCOptions {
diff --git a/pkg/executor/containerexecutor/containerexecutor_test.go b/pkg/executor/containerexecutor/containerexecutor_test.go
index 78222674972..b5079748bee 100644
--- a/pkg/executor/containerexecutor/containerexecutor_test.go
+++ b/pkg/executor/containerexecutor/containerexecutor_test.go
@@ -17,10 +17,11 @@ import (
testsv3 "github.com/kubeshop/testkube-operator/api/tests/v3"
templatesclientv1 "github.com/kubeshop/testkube-operator/pkg/client/templates/v1"
v3 "github.com/kubeshop/testkube-operator/pkg/client/tests/v3"
- "github.com/kubeshop/testkube/internal/featureflags"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/executor"
"github.com/kubeshop/testkube/pkg/executor/client"
+ "github.com/kubeshop/testkube/pkg/featureflags"
+ "github.com/kubeshop/testkube/pkg/imageinspector"
"github.com/kubeshop/testkube/pkg/repository/result"
)
@@ -30,15 +31,15 @@ func TestExecuteAsync(t *testing.T) {
t.Parallel()
ce := ContainerExecutor{
- clientSet: getFakeClient("1"),
- log: logger(),
- repository: FakeResultRepository{},
- metrics: FakeMetricCounter{},
- emitter: FakeEmitter{},
- namespace: "default",
- configMap: FakeConfigRepository{},
- testsClient: FakeTestsClient{},
- executorsClient: FakeExecutorsClient{},
+ clientSet: getFakeClient("1"),
+ log: logger(),
+ repository: FakeResultRepository{},
+ metrics: FakeMetricCounter{},
+ emitter: FakeEmitter{},
+ configMap: FakeConfigRepository{},
+ testsClient: FakeTestsClient{},
+ executorsClient: FakeExecutorsClient{},
+ serviceAccountNames: map[string]string{"": ""},
}
execution := &testkube.Execution{Id: "1"}
@@ -56,18 +57,18 @@ func TestExecuteSync(t *testing.T) {
t.Parallel()
ce := ContainerExecutor{
- clientSet: getFakeClient("1"),
- log: logger(),
- repository: FakeResultRepository{},
- metrics: FakeMetricCounter{},
- emitter: FakeEmitter{},
- namespace: "default",
- configMap: FakeConfigRepository{},
- testsClient: FakeTestsClient{},
- executorsClient: FakeExecutorsClient{},
+ clientSet: getFakeClient("1"),
+ log: logger(),
+ repository: FakeResultRepository{},
+ metrics: FakeMetricCounter{},
+ emitter: FakeEmitter{},
+ configMap: FakeConfigRepository{},
+ testsClient: FakeTestsClient{},
+ executorsClient: FakeExecutorsClient{},
+ serviceAccountNames: map[string]string{"default": ""},
}
- execution := &testkube.Execution{Id: "1"}
+ execution := &testkube.Execution{Id: "1", TestNamespace: "default"}
options := client.ExecuteOptions{
ImagePullSecretNames: []string{"secret-name1"},
Sync: true,
@@ -129,7 +130,7 @@ func TestNewExecutorJobSpecWithArgs(t *testing.T) {
assert.NotNil(t, spec)
wantEnvs := []corev1.EnvVar{
- {Name: "DEBUG", Value: ""},
+ {Name: "DEBUG", Value: "false"},
{Name: "RUNNER_ENDPOINT", Value: ""},
{Name: "RUNNER_ACCESSKEYID", Value: ""},
{Name: "RUNNER_SECRETACCESSKEY", Value: ""},
@@ -153,12 +154,21 @@ func TestNewExecutorJobSpecWithArgs(t *testing.T) {
{Name: "RUNNER_CONTEXTTYPE", Value: ""},
{Name: "RUNNER_CONTEXTDATA", Value: ""},
{Name: "RUNNER_APIURI", Value: ""},
- {Name: "RUNNER_CLOUD_MODE", Value: "false"},
- {Name: "RUNNER_CLOUD_API_KEY", Value: ""},
- {Name: "RUNNER_CLOUD_API_URL", Value: ""},
- {Name: "RUNNER_CLOUD_API_TLS_INSECURE", Value: "false"},
- {Name: "RUNNER_CLOUD_API_SKIP_VERIFY", Value: "false"},
+ {Name: "RUNNER_PRO_MODE", Value: "false"},
+ {Name: "RUNNER_PRO_API_KEY", Value: ""},
+ {Name: "RUNNER_PRO_API_URL", Value: ""},
+ {Name: "RUNNER_PRO_API_TLS_INSECURE", Value: "false"},
+ {Name: "RUNNER_PRO_API_SKIP_VERIFY", Value: "false"},
+ {Name: "RUNNER_PRO_CONNECTION_TIMEOUT", Value: "10"},
+ {Name: "RUNNER_CLOUD_MODE", Value: "false"}, // DEPRECATED
+ {Name: "RUNNER_CLOUD_API_KEY", Value: ""}, // DEPRECATED
+ {Name: "RUNNER_CLOUD_API_URL", Value: ""}, // DEPRECATED
+ {Name: "RUNNER_CLOUD_API_TLS_INSECURE", Value: "false"}, // DEPRECATED
+ {Name: "RUNNER_CLOUD_API_SKIP_VERIFY", Value: "false"}, // DEPRECATED
{Name: "RUNNER_CLUSTERID", Value: ""},
+ {Name: "RUNNER_PRO_API_CERT_FILE", Value: ""},
+ {Name: "RUNNER_PRO_API_KEY_FILE", Value: ""},
+ {Name: "RUNNER_PRO_API_CA_FILE", Value: ""},
{Name: "CI", Value: "1"},
{Name: "key", Value: "value"},
{Name: "aa", Value: "bb"},
@@ -197,13 +207,15 @@ func TestNewExecutorJobSpecWithWorkingDirRelative(t *testing.T) {
defer mockCtrl.Finish()
mockTemplatesClient := templatesclientv1.NewMockInterface(mockCtrl)
+ mockInspector := imageinspector.NewMockInspector(mockCtrl)
jobOptions, _ := NewJobOptions(
logger(),
mockTemplatesClient,
executor.Images{},
executor.Templates{},
- "",
+ mockInspector,
+ map[string]string{},
"",
"",
"",
@@ -242,13 +254,15 @@ func TestNewExecutorJobSpecWithWorkingDirAbsolute(t *testing.T) {
defer mockCtrl.Finish()
mockTemplatesClient := templatesclientv1.NewMockInterface(mockCtrl)
+ mockInspector := imageinspector.NewMockInspector(mockCtrl)
jobOptions, _ := NewJobOptions(
logger(),
mockTemplatesClient,
executor.Images{},
executor.Templates{},
- "",
+ mockInspector,
+ map[string]string{},
"",
"",
"",
@@ -286,13 +300,15 @@ func TestNewExecutorJobSpecWithoutWorkingDir(t *testing.T) {
defer mockCtrl.Finish()
mockTemplatesClient := templatesclientv1.NewMockInterface(mockCtrl)
+ mockInspector := imageinspector.NewMockInspector(mockCtrl)
jobOptions, _ := NewJobOptions(
logger(),
mockTemplatesClient,
executor.Images{},
executor.Templates{},
- "",
+ mockInspector,
+ map[string]string{},
"",
"",
"",
@@ -450,6 +466,15 @@ func (r FakeResultRepository) GetTestMetrics(ctx context.Context, name string, l
panic("implement me")
}
+func (r FakeResultRepository) Count(ctx context.Context, filter result.Filter) (count int64, err error) {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (FakeResultRepository) GetExecution(ctx context.Context, id string) (testkube.Execution, error) {
+ return testkube.Execution{}, nil
+}
+
func (FakeResultRepository) Get(ctx context.Context, id string) (testkube.Execution, error) {
return testkube.Execution{}, nil
}
diff --git a/pkg/executor/containerexecutor/logs.go b/pkg/executor/containerexecutor/logs.go
index beb482c45a8..8e3f3934a9c 100644
--- a/pkg/executor/containerexecutor/logs.go
+++ b/pkg/executor/containerexecutor/logs.go
@@ -17,8 +17,8 @@ import (
// TailJobLogs - locates logs for job pod(s)
// These methods here are similar to Job executor, but they don't require the json structure.
-func (c *ContainerExecutor) TailJobLogs(ctx context.Context, id string, logs chan []byte) (err error) {
- podsClient := c.clientSet.CoreV1().Pods(c.namespace)
+func (c *ContainerExecutor) TailJobLogs(ctx context.Context, id, namespace string, logs chan []byte) (err error) {
+ podsClient := c.clientSet.CoreV1().Pods(namespace)
pods, err := executor.GetJobPods(ctx, podsClient, id, 1, 10)
if err != nil {
close(logs)
@@ -34,7 +34,7 @@ func (c *ContainerExecutor) TailJobLogs(ctx context.Context, id string, logs cha
case corev1.PodRunning:
l.Debug("tailing pod logs: immediately")
- return tailPodLogs(c.log, c.clientSet, c.namespace, pod, logs)
+ return tailPodLogs(c.log, c.clientSet, namespace, pod, logs)
case corev1.PodFailed:
err := fmt.Errorf("can't get pod logs, pod failed: %s/%s", pod.Namespace, pod.Name)
@@ -43,13 +43,13 @@ func (c *ContainerExecutor) TailJobLogs(ctx context.Context, id string, logs cha
default:
l.Debugw("tailing job logs: waiting for pod to be ready")
- if err = wait.PollUntilContextTimeout(ctx, pollInterval, c.podStartTimeout, true, executor.IsPodLoggable(c.clientSet, pod.Name, c.namespace)); err != nil {
+ if err = wait.PollUntilContextTimeout(ctx, pollInterval, c.podStartTimeout, true, executor.IsPodLoggable(c.clientSet, pod.Name, namespace)); err != nil {
l.Errorw("poll immediate error when tailing logs", "error", err)
return err
}
l.Debug("tailing pod logs")
- return tailPodLogs(c.log, c.clientSet, c.namespace, pod, logs)
+ return tailPodLogs(c.log, c.clientSet, namespace, pod, logs)
}
}
}
diff --git a/pkg/executor/containerexecutor/tmpl.go b/pkg/executor/containerexecutor/tmpl.go
index 31dbe9bae35..2ba9bb0866b 100644
--- a/pkg/executor/containerexecutor/tmpl.go
+++ b/pkg/executor/containerexecutor/tmpl.go
@@ -2,14 +2,14 @@ package containerexecutor
import (
"bytes"
+ "context"
+ _ "embed"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
- _ "embed"
-
"go.uber.org/zap"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
@@ -22,8 +22,7 @@ import (
"github.com/kubeshop/testkube/pkg/executor"
"github.com/kubeshop/testkube/pkg/executor/client"
"github.com/kubeshop/testkube/pkg/executor/env"
- "github.com/kubeshop/testkube/pkg/secret"
- "github.com/kubeshop/testkube/pkg/skopeo"
+ "github.com/kubeshop/testkube/pkg/imageinspector"
"github.com/kubeshop/testkube/pkg/utils"
)
@@ -203,56 +202,22 @@ func NewScraperJobSpec(log *zap.SugaredLogger, options *JobOptions) (*batchv1.Jo
return &job, nil
}
-// InspectDockerImage inspects docker image
-func InspectDockerImage(namespace, registry, image string, imageSecrets []string) ([]string, string, error) {
- inspector := skopeo.NewClient()
- if len(imageSecrets) != 0 {
- secretClient, err := secret.NewClient(namespace)
- if err != nil {
- return nil, "", err
- }
-
- var secrets []corev1.Secret
- for _, imageSecret := range imageSecrets {
- object, err := secretClient.GetObject(imageSecret)
- if err != nil {
- return nil, "", err
- }
-
- secrets = append(secrets, *object)
- }
-
- inspector, err = skopeo.NewClientFromSecrets(secrets, registry)
- if err != nil {
- return nil, "", err
- }
- }
-
- dockerImage, err := inspector.Inspect(image)
- if err != nil {
- return nil, "", err
- }
-
- return append(dockerImage.Config.Entrypoint, dockerImage.Config.Cmd...), dockerImage.Shell, nil
-}
-
// TODO refactor JobOptions to use builder pattern
// TODO extract JobOptions for both container and job executor to common package in separate PR
// NewJobOptions provides job options for templates
func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface, images executor.Images,
- templates executor.Templates, serviceAccountName, registry, clusterID, apiURI string,
+ templates executor.Templates, inspector imageinspector.Inspector, serviceAccountNames map[string]string, registry, clusterID, apiURI string,
execution testkube.Execution, options client.ExecuteOptions, natsUri string, debug bool) (*JobOptions, error) {
jobOptions := NewJobOptionsFromExecutionOptions(options)
if execution.PreRunScript != "" || execution.PostRunScript != "" {
jobOptions.Command = []string{filepath.Join(executor.VolumeDir, EntrypointScriptName)}
if jobOptions.Image != "" {
- cmd, shell, err := InspectDockerImage(jobOptions.Namespace, registry, jobOptions.Image, jobOptions.ImagePullSecrets)
+ info, err := inspector.Inspect(context.Background(), registry, jobOptions.Image, corev1.PullIfNotPresent, jobOptions.ImagePullSecrets)
if err == nil {
if len(execution.Command) == 0 {
- execution.Command = cmd
+ execution.Command = info.Cmd
}
-
- execution.ContainerShell = shell
+ execution.ContainerShell = info.Shell
} else {
log.Errorw("Docker image inspection error", "error", err)
}
@@ -341,6 +306,11 @@ func NewJobOptions(log *zap.SugaredLogger, templatesClient templatesv1.Interface
}
jobOptions.Variables = execution.Variables
+ serviceAccountName, ok := serviceAccountNames[execution.TestNamespace]
+ if !ok {
+ return jobOptions, fmt.Errorf("not supported namespace %s", execution.TestNamespace)
+ }
+
jobOptions.ServiceAccountName = serviceAccountName
jobOptions.Registry = registry
jobOptions.ClusterID = clusterID
diff --git a/pkg/executor/content/fetcher_test.go b/pkg/executor/content/fetcher_test.go
index 2dbceb20439..68b9bbaa28b 100644
--- a/pkg/executor/content/fetcher_test.go
+++ b/pkg/executor/content/fetcher_test.go
@@ -14,9 +14,29 @@ import (
)
// this content is also saved in test repo
-// in https://github.com/kubeshop/testkube-examples/blob/main/example.json
+// in https:///github.com/kubeshop/testkube-docker-action/blob/main/action.yaml
// file with \n on end
-const fileContent = `{"some":"json","file":"with content"}
+const fileContent = `MIT License
+
+Copyright (c) 2022 kubeshop
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
`
func TestFetcher_Integration(t *testing.T) {
@@ -47,8 +67,8 @@ func TestFetcher_Integration(t *testing.T) {
content := &testkube.TestContent{
Type_: string(testkube.TestContentTypeGitFile),
- Repository: testkube.NewGitRepository("https://github.com/kubeshop/testkube-examples.git", "main").
- WithPath("example.json"),
+ Repository: testkube.NewGitRepository("https://github.com/kubeshop/testkube-docker-action.git", "main").
+ WithPath("LICENSE"),
}
path, err := f.Fetch(content)
@@ -65,15 +85,15 @@ func TestFetcher_Integration(t *testing.T) {
content := &testkube.TestContent{
Type_: string(testkube.TestContentTypeGitDir),
- Repository: testkube.NewGitRepository("https://github.com/kubeshop/testkube-examples.git", "main").
+ Repository: testkube.NewGitRepository("https://github.com/kubeshop/testkube-docker-action.git", "main").
WithPath(""),
}
path, err := f.Fetch(content)
assert.NoError(t, err)
- assert.FileExists(t, filepath.Join(path, "example.json"))
+ assert.FileExists(t, filepath.Join(path, "action.yaml"))
assert.FileExists(t, filepath.Join(path, "README.md"))
- assert.FileExists(t, filepath.Join(path, "subdir/example.json"))
+ assert.FileExists(t, filepath.Join(path, ".github/update-major-version.yml"))
})
@@ -82,14 +102,13 @@ func TestFetcher_Integration(t *testing.T) {
content := &testkube.TestContent{
Type_: string(testkube.TestContentTypeGitDir),
- Repository: testkube.NewGitRepository("https://github.com/kubeshop/testkube-examples.git", "main").
- WithPath("subdir"),
+ Repository: testkube.NewGitRepository("https://github.com/kubeshop/testkube-docker-action.git", "main").
+ WithPath(".github"),
}
path, err := f.Fetch(content)
assert.NoError(t, err)
- assert.FileExists(t, filepath.Join(path, "example.json"))
- assert.FileExists(t, filepath.Join(path, "example2.json"))
+ assert.FileExists(t, filepath.Join(path, "update-major-version.yml"))
})
t.Run("test fetch no content", func(t *testing.T) {
@@ -110,7 +129,7 @@ func TestFetcher_Integration(t *testing.T) {
t.Run("with file", func(t *testing.T) {
t.Parallel()
- repo := testkube.NewGitRepository("https://github.com/kubeshop/testkube-examples.git", "main").WithPath("example.json")
+ repo := testkube.NewGitRepository("https://github.com/kubeshop/testkube-docker-action.git", "main").WithPath("action.yaml")
contentType, err := f.CalculateGitContentType(*repo)
assert.NoError(t, err)
@@ -120,7 +139,7 @@ func TestFetcher_Integration(t *testing.T) {
t.Run("with dir", func(t *testing.T) {
t.Parallel()
- repo := testkube.NewGitRepository("https://github.com/kubeshop/testkube-examples.git", "main").WithPath("subdir")
+ repo := testkube.NewGitRepository("https://github.com/kubeshop/testkube-docker-action.git", "main").WithPath(".github")
contentType, err := f.CalculateGitContentType(*repo)
assert.NoError(t, err)
diff --git a/pkg/executor/output/parser.go b/pkg/executor/output/parser.go
index c3445334e8f..8372d09dd94 100644
--- a/pkg/executor/output/parser.go
+++ b/pkg/executor/output/parser.go
@@ -36,11 +36,13 @@ func GetLogEntry(b []byte) (out Output, err error) {
// {"type": "line", "message": "runner execution started ------------", "time": "..."}
// {"type": "line", "message": "GET /results", "time": "..."}
// {"type": "result", "result": {"id": "2323", "output": "-----"}, "time": "..."}
-func ParseRunnerOutput(b []byte) (*testkube.ExecutionResult, error) {
+func ParseRunnerOutput(b []byte, attachLogs bool) (*testkube.ExecutionResult, error) {
result := &testkube.ExecutionResult{}
if len(b) == 0 {
errMessage := "no logs found"
- result.Output = errMessage
+ if attachLogs {
+ result.Output = errMessage
+ }
return result.Err(errors.New(errMessage)), nil
}
logs, err := parseLogs(b)
@@ -69,7 +71,10 @@ func ParseRunnerOutput(b []byte) (*testkube.ExecutionResult, error) {
default:
result.Err(fmt.Errorf("wrong log type was found as last log: %v", log))
}
- result.Output = sanitizeLogs(logs)
+
+ if attachLogs {
+ result.Output = sanitizeLogs(logs)
+ }
return result, nil
}
@@ -308,7 +313,7 @@ func getResultMessage(result testkube.ExecutionResult) string {
return result.Output
}
- return fmt.Sprintf("%s", *result.Status)
+ return string(*result.Status)
}
// sameSeverity decides if a and b are of the same severity type
diff --git a/pkg/executor/output/parser_test.go b/pkg/executor/output/parser_test.go
index 045d3f033b3..cb842bb1f67 100644
--- a/pkg/executor/output/parser_test.go
+++ b/pkg/executor/output/parser_test.go
@@ -64,7 +64,7 @@ func TestParseRunnerOutput(t *testing.T) {
t.Run("Empty runner output", func(t *testing.T) {
t.Parallel()
- result, err := ParseRunnerOutput([]byte{})
+ result, err := ParseRunnerOutput([]byte{}, true)
assert.Equal(t, "no logs found", result.Output)
assert.NoError(t, err)
@@ -75,7 +75,7 @@ func TestParseRunnerOutput(t *testing.T) {
t.Parallel()
invalidOutput := []byte(`{not a json}`)
- result, err := ParseRunnerOutput(invalidOutput)
+ result, err := ParseRunnerOutput(invalidOutput, true)
expectedErrMessage := "ERROR can't get log entry: invalid character 'n' looking for beginning of object key string, ((({not a json})))"
assert.Equal(t, expectedErrMessage+"\n", result.Output)
@@ -100,7 +100,7 @@ func TestParseRunnerOutput(t *testing.T) {
{"type":"line","content":"\n # failure detail \n \n 1. Error \n connect ECONNREFUSED 127.0.0.1:8088 \n at request \n inside \"Health\" \n \n 2. AssertionError Status code is 200 \n expected { Object (id, _details, ...) } to have property 'code' \n at assertion:0 in test-script \n inside \"Health\" \n"}
{"type":"result","result":{"status":"failed","startTime":"2021-10-29T11:35:35.759Z","endTime":"2021-10-29T11:35:36.771Z","output":"newman\n\nLocal-API-Health\n\n→ Health\n GET http://localhost:8088/health [errored]\n connect ECONNREFUSED 127.0.0.1:8088\n 2. Status code is 200\n\n┌─────────────────────────┬──────────┬──────────┐\n│ │ executed │ failed │\n├─────────────────────────┼──────────┼──────────┤\n│ iterations │ 1 │ 0 │\n├─────────────────────────┼──────────┼──────────┤\n│ requests │ 1 │ 1 │\n├─────────────────────────┼──────────┼──────────┤\n│ test-scripts │ 1 │ 0 │\n├─────────────────────────┼──────────┼──────────┤\n│ prerequest-scripts │ 0 │ 0 │\n├─────────────────────────┼──────────┼──────────┤\n│ assertions │ 1 │ 1 │\n├─────────────────────────┴──────────┴──────────┤\n│ total run duration: 1012ms │\n├───────────────────────────────────────────────┤\n│ total data received: 0B (approx) │\n└───────────────────────────────────────────────┘\n\n # failure detail \n \n 1. Error \n connect ECONNREFUSED 127.0.0.1:8088 \n at request \n inside \"Health\" \n \n 2. AssertionError Status code is 200 \n expected { Object (id, _details, ...) } to have property 'code' \n at assertion:0 in test-script \n inside \"Health\" \n","outputType":"text/plain","errorMessage":"process error: exit status 1","steps":[{"name":"Health","duration":"0s","status":"failed","assertionResults":[{"name":"Status code is 200","status":"failed","errorMessage":"expected { Object (id, _details, ...) } to have property 'code'"}]}]}}
`)
- result, err := ParseRunnerOutput(exampleOutput)
+ result, err := ParseRunnerOutput(exampleOutput, true)
assert.Len(t, result.Output, 4624)
assert.NoError(t, err)
@@ -150,7 +150,7 @@ func TestParseRunnerOutput(t *testing.T) {
{"type":"line","content":"✅ Got Newman result successfully","time":"2023-07-18T19:12:46.126116248Z"}
{"type":"line","content":"✅ Mapped Newman result successfully","time":"2023-07-18T19:12:46.126152021Z"}
{"type":"result","result":{"status":"passed","output":"newman\n\nCore App Tests - WebPlayer\n\n→ core-eks-test.poppcore.co client=testdb sign=testct1 company=41574150-b952-413b-898b-dc5336b4bd12\n GET https://na.com/v6-wplt/?client=testdb\u0026sign=testct1\u0026company=41574150-b952-413b-898b-dc5336b4bd12 [200 OK, 33.9kB, 326ms]\n ✓ Status code is 200\n\n┌─────────────────────────┬────────────────────┬───────────────────┐\n│ │ executed │ failed │\n├─────────────────────────┼────────────────────┼───────────────────┤\n│ iterations │ 1 │ 0 │\n├─────────────────────────┼────────────────────┼───────────────────┤\n│ requests │ 1 │ 0 │\n├─────────────────────────┼────────────────────┼───────────────────┤\n│ test-scripts │ 1 │ 0 │\n├─────────────────────────┼────────────────────┼───────────────────┤\n│ prerequest-scripts │ 0 │ 0 │\n├─────────────────────────┼────────────────────┼───────────────────┤\n│ assertions │ 1 │ 0 │\n├─────────────────────────┴────────────────────┴───────────────────┤\n│ total run duration: 429ms │\n├──────────────────────────────────────────────────────────────────┤\n│ total data received: 33.45kB (approx) │\n├──────────────────────────────────────────────────────────────────┤\n│ average response time: 326ms [min: 326ms, max: 326ms, s.d.: 0µs] │\n└──────────────────────────────────────────────────────────────────┘\n","outputType":"text/plain","steps":[{"name":"na.com client=testdb sign=testct1 company=41574150-b952-413b-898b-dc5336b4bd12","duration":"326ms","status":"passed","assertionResults":[{"name":"Status code is 200","status":"passed"}]}]},"time":"2023-07-18T19:12:46.12615853Z"}`)
- result, err := ParseRunnerOutput(exampleOutput)
+ result, err := ParseRunnerOutput(exampleOutput, true)
assert.Len(t, result.Output, 7304)
assert.NoError(t, err)
@@ -203,7 +203,7 @@ can't find branch or commit in params, repo:&{Type_:git-file Uri:https://github.
running test [63c6bec1790802b7e3e57048]
🚚 Preparing for test run
`
- result, err := ParseRunnerOutput(unorderedOutput)
+ result, err := ParseRunnerOutput(unorderedOutput, true)
assert.Equal(t, expectedOutput, result.Output)
assert.NoError(t, err)
@@ -221,7 +221,7 @@ running test [63c6bec1790802b7e3e57048]
Running [ ./zap-api-scan.py [-t https://www.example.com/openapi.json -f openapi -c examples/zap-api.conf -d -D 5 -I -l INFO -n examples/context.config -S -T 60 -U anonymous -O https://www.example.com -z -config aaa=bbb -r api-test-report.html]]
could not start process: fork/exec ./zap-api-scan.py: no such file or directory
`
- result, err := ParseRunnerOutput(output)
+ result, err := ParseRunnerOutput(output, true)
assert.Equal(t, expectedOutput, result.Output)
assert.NoError(t, err)
@@ -276,7 +276,7 @@ running test [63c960287104b0fa0b7a45ef]
can't find branch or commit in params, repo:&{Type_:git-file Uri:https://github.com/kubeshop/testkube.git Branch: Commit: Path:test/cypress/executor-smoke/cypress-11 Username: Token: UsernameSecret: TokenSecret: WorkingDir:}
`
- result, err := ParseRunnerOutput(output)
+ result, err := ParseRunnerOutput(output, true)
assert.Equal(t, expectedOutput, result.Output)
assert.NoError(t, err)
@@ -337,7 +337,7 @@ running test [63c960287104b0fa0b7a45ef]
can't find branch or commit in params, repo:&{Type_:git-file Uri:https://github.com/kubeshop/testkube.git Branch: Commit: Path:test/cypress/executor-smoke/cypress-11 Username: Token: UsernameSecret: TokenSecret: WorkingDir:}
`
- result, err := ParseRunnerOutput(output)
+ result, err := ParseRunnerOutput(output, true)
assert.Equal(t, expectedOutput, result.Output)
assert.NoError(t, err)
@@ -392,7 +392,7 @@ running test [63ca8c8988564860327a16b5]
❌ can't find branch or commit in params, repo:&{Type_:git-file Uri:https://github.com/kubeshop/testkube.git Branch: Commit: Path:test/cypress/executor-smoke/cypress-11 Username: Token: UsernameSecret: TokenSecret: WorkingDir:}
`
- result, err := ParseRunnerOutput(output)
+ result, err := ParseRunnerOutput(output, true)
assert.Equal(t, expectedOutput, result.Output)
assert.NoError(t, err)
diff --git a/pkg/executor/scraper/extractor.go b/pkg/executor/scraper/extractor.go
index 63ce3b5ed04..2acf4e81fa3 100644
--- a/pkg/executor/scraper/extractor.go
+++ b/pkg/executor/scraper/extractor.go
@@ -41,4 +41,7 @@ type FilesMeta struct {
type FileStat struct {
Name string `json:"name"`
Size int64 `json:"size"`
+ // Status shows if file is ready to be downloaded
+ // One of: ready, processing, error
+ Status string `json:"status,omitempty"`
}
diff --git a/pkg/executor/scraper/factory/factory.go b/pkg/executor/scraper/factory/factory.go
index e9b95968a7e..1b8d89bff00 100644
--- a/pkg/executor/scraper/factory/factory.go
+++ b/pkg/executor/scraper/factory/factory.go
@@ -33,7 +33,7 @@ const (
func TryGetScrapper(ctx context.Context, params envs.Params) (scraper.Scraper, error) {
if params.ScrapperEnabled {
uploader := MinIOUploader
- if params.CloudMode {
+ if params.ProMode {
uploader = CloudUploader
}
extractor := RecursiveFilesystemExtractor
@@ -58,7 +58,7 @@ func GetScraper(ctx context.Context, params envs.Params, extractorType Extractor
extractor = scraper.NewRecursiveFilesystemExtractor(filesystem.NewOSFileSystem())
case ArchiveFilesystemExtractor:
var opts []scraper.ArchiveFilesystemExtractorOpts
- if params.CloudMode {
+ if params.ProMode {
opts = append(opts, scraper.GenerateTarballMetaFile())
}
extractor = scraper.NewArchiveFilesystemExtractor(filesystem.NewOSFileSystem(), opts...)
@@ -96,21 +96,30 @@ func GetScraper(ctx context.Context, params envs.Params, extractorType Extractor
func getRemoteStorageUploader(ctx context.Context, params envs.Params) (uploader *cloudscraper.CloudUploader, err error) {
// timeout blocking connection to cloud
- ctxTimeout, cancel := context.WithTimeout(ctx, time.Duration(params.CloudConnectionTimeoutSec)*time.Second)
+ ctxTimeout, cancel := context.WithTimeout(ctx, time.Duration(params.ProConnectionTimeoutSec)*time.Second)
defer cancel()
output.PrintLogf(
- "%s Uploading artifacts using Remote Storage Uploader (timeout:%ds, insecure:%v, skipVerify: %v, url: %s)",
- ui.IconCheckMark, params.CloudConnectionTimeoutSec, params.CloudAPITLSInsecure, params.CloudAPISkipVerify, params.CloudAPIURL)
- grpcConn, err := agent.NewGRPCConnection(ctxTimeout, params.CloudAPITLSInsecure, params.CloudAPISkipVerify, params.CloudAPIURL, log.DefaultLogger)
+ "%s Uploading artifacts using Remote Storage Uploader (timeout:%ds, agentInsecure:%v, agentSkipVerify: %v, url: %s, scraperSkipVerify: %v)",
+ ui.IconCheckMark, params.ProConnectionTimeoutSec, params.ProAPITLSInsecure, params.ProAPISkipVerify, params.ProAPIURL, params.SkipVerify)
+ grpcConn, err := agent.NewGRPCConnection(
+ ctxTimeout,
+ params.ProAPITLSInsecure,
+ params.ProAPISkipVerify,
+ params.ProAPIURL,
+ params.ProAPICertFile,
+ params.ProAPIKeyFile,
+ params.ProAPICAFile,
+ log.DefaultLogger,
+ )
if err != nil {
return nil, err
}
output.PrintLogf("%s Connected to Agent API", ui.IconCheckMark)
grpcClient := cloud.NewTestKubeCloudAPIClient(grpcConn)
- cloudExecutor := cloudexecutor.NewCloudGRPCExecutor(grpcClient, grpcConn, params.CloudAPIKey)
- return cloudscraper.NewCloudUploader(cloudExecutor), nil
+ cloudExecutor := cloudexecutor.NewCloudGRPCExecutor(grpcClient, grpcConn, params.ProAPIKey)
+ return cloudscraper.NewCloudUploader(cloudExecutor, params.SkipVerify), nil
}
func getMinIOUploader(params envs.Params) (*scraper.MinIOUploader, error) {
diff --git a/pkg/executor/scraper/mock_scraper.go b/pkg/executor/scraper/mock_scraper.go
index 5092e04414a..f2df1ab1a8d 100644
--- a/pkg/executor/scraper/mock_scraper.go
+++ b/pkg/executor/scraper/mock_scraper.go
@@ -9,7 +9,6 @@ import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
-
testkube "github.com/kubeshop/testkube/pkg/api/v1/testkube"
)
diff --git a/pkg/executor/scraper/mock_uploader.go b/pkg/executor/scraper/mock_uploader.go
index f1ab4091a45..638901f69de 100644
--- a/pkg/executor/scraper/mock_uploader.go
+++ b/pkg/executor/scraper/mock_uploader.go
@@ -9,7 +9,6 @@ import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
-
testkube "github.com/kubeshop/testkube/pkg/api/v1/testkube"
)
diff --git a/internal/featureflags/featueflags.go b/pkg/featureflags/featureflags.go
similarity index 100%
rename from internal/featureflags/featueflags.go
rename to pkg/featureflags/featureflags.go
diff --git a/internal/featureflags/featureflags_test.go b/pkg/featureflags/featureflags_test.go
similarity index 100%
rename from internal/featureflags/featureflags_test.go
rename to pkg/featureflags/featureflags_test.go
diff --git a/pkg/imageinspector/configmapstorage.go b/pkg/imageinspector/configmapstorage.go
new file mode 100644
index 00000000000..6e66e615644
--- /dev/null
+++ b/pkg/imageinspector/configmapstorage.go
@@ -0,0 +1,145 @@
+package imageinspector
+
+import (
+ "context"
+ "encoding/json"
+ "slices"
+ "sync"
+ "time"
+
+ "github.com/pkg/errors"
+ k8serrors "k8s.io/apimachinery/pkg/api/errors"
+
+ "github.com/kubeshop/testkube/pkg/configmap"
+)
+
+type configmapStorage struct {
+ client configmap.Interface
+ name string
+ avoidDirectGet bool // if there is memory storage prior to this one, all the contents will be copied there anyway
+ mu sync.RWMutex
+}
+
+func NewConfigMapStorage(client configmap.Interface, name string, avoidDirectGet bool) StorageWithTransfer {
+ return &configmapStorage{
+ client: client,
+ name: name,
+ avoidDirectGet: avoidDirectGet,
+ }
+}
+
+func (c *configmapStorage) fetch(ctx context.Context) (map[string]string, error) {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+ cache, err := c.client.Get(ctx, c.name)
+ if err != nil && !k8serrors.IsNotFound(err) {
+ return nil, errors.Wrap(err, "getting configmap cache")
+ }
+ if cache == nil {
+ cache = map[string]string{}
+ }
+ return cache, nil
+}
+
+func cleanOldRecords(currentData *map[string]string) {
+ // Unmarshal the fetched date for the records
+ type Entry struct {
+ time time.Time
+ name string
+ }
+ dates := make([]Entry, 0, len(*currentData))
+ var vv Info
+ for k := range *currentData {
+ _ = json.Unmarshal([]byte((*currentData)[k]), &vv)
+ dates = append(dates, Entry{time: vv.FetchedAt, name: k})
+ }
+ slices.SortFunc(dates, func(a, b Entry) int {
+ if a.time.Before(b.time) {
+ return -1
+ }
+ return 1
+ })
+
+ // Delete half of the records
+ for i := 0; i < len(*currentData)/2; i++ {
+ delete(*currentData, dates[i].name)
+ }
+}
+
+func (c *configmapStorage) save(ctx context.Context, serializedData map[string]string) error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ // Save data
+ err := c.client.Apply(ctx, c.name, serializedData)
+
+ // When the cache is too big, delete the oldest items and try again
+ if err != nil && k8serrors.IsRequestEntityTooLargeError(err) {
+ cleanOldRecords(&serializedData)
+ err = c.client.Apply(ctx, c.name, serializedData)
+ }
+ return err
+}
+
+func (c *configmapStorage) StoreMany(ctx context.Context, data map[Hash]Info) (err error) {
+ if data == nil {
+ return
+ }
+ serialized, err := c.fetch(ctx)
+ if err != nil {
+ return
+ }
+ for k, v := range data {
+ serialized[string(k)], err = marshalInfo(v)
+ if err != nil {
+ return
+ }
+ }
+ return c.save(ctx, serialized)
+}
+
+func (c *configmapStorage) CopyTo(ctx context.Context, other ...StorageTransfer) (err error) {
+ serialized, err := c.fetch(ctx)
+ if err != nil {
+ return
+ }
+ data := make(map[Hash]Info, len(serialized))
+ for k, v := range serialized {
+ data[Hash(k)], err = unmarshalInfo(v)
+ if err != nil {
+ return
+ }
+ }
+ for _, v := range other {
+ err = v.StoreMany(ctx, data)
+ if err != nil {
+ return
+ }
+ }
+ return
+}
+
+func (c *configmapStorage) Store(ctx context.Context, request RequestBase, info Info) error {
+ return c.StoreMany(ctx, map[Hash]Info{
+ hash(request.Registry, request.Image): info,
+ })
+}
+
+func (c *configmapStorage) Get(ctx context.Context, request RequestBase) (*Info, error) {
+ if c.avoidDirectGet {
+ return nil, nil
+ }
+ data, err := c.fetch(ctx)
+ if err != nil {
+ return nil, err
+ }
+ value, ok := data[string(hash(request.Registry, request.Image))]
+ if !ok {
+ return nil, nil
+ }
+ v, err := unmarshalInfo(value)
+ if err != nil {
+ return nil, err
+ }
+ return &v, nil
+}
diff --git a/pkg/imageinspector/configmapstorage_test.go b/pkg/imageinspector/configmapstorage_test.go
new file mode 100644
index 00000000000..24820d95a03
--- /dev/null
+++ b/pkg/imageinspector/configmapstorage_test.go
@@ -0,0 +1,179 @@
+package imageinspector
+
+import (
+ "context"
+ "maps"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/stretchr/testify/assert"
+ k8serrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+
+ "github.com/kubeshop/testkube/pkg/configmap"
+)
+
+func mustMarshalInfo(v Info) string {
+ s, e := marshalInfo(v)
+ if e != nil {
+ panic(e)
+ }
+ return s
+}
+
+func TestConfigMapStorageGet(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ client := configmap.NewMockInterface(ctrl)
+ m := NewConfigMapStorage(client, "dummy", false)
+ value := map[string]string{
+ string(hash(req1.Registry, req1.Image)): mustMarshalInfo(info1),
+ string(hash(req2.Registry, req2.Image)): mustMarshalInfo(info2),
+ }
+
+ client.EXPECT().Get(gomock.Any(), "dummy").Return(value, nil)
+
+ v1, err1 := m.Get(context.Background(), req1)
+ assert.NoError(t, err1)
+ assert.Equal(t, &info1, v1)
+}
+
+func TestConfigMapStorageGetEmpty(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ client := configmap.NewMockInterface(ctrl)
+ m := NewConfigMapStorage(client, "dummy", false)
+
+ client.EXPECT().Get(gomock.Any(), "dummy").
+ Return(nil, k8serrors.NewNotFound(schema.GroupResource{}, "dummy"))
+
+ v1, err1 := m.Get(context.Background(), req1)
+ assert.NoError(t, err1)
+ assert.Equal(t, noInfoPtr, v1)
+}
+
+func TestConfigMapStorageStore(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ client := configmap.NewMockInterface(ctrl)
+ m := NewConfigMapStorage(client, "dummy", false)
+ value := map[string]string{
+ string(hash(req1.Registry, req1.Image)): mustMarshalInfo(info1),
+ }
+ expected := map[string]string{
+ string(hash(req2.Registry, req2.Image)): mustMarshalInfo(info2),
+ }
+ maps.Copy(expected, value)
+
+ client.EXPECT().Get(gomock.Any(), "dummy").Return(value, nil)
+ client.EXPECT().Apply(gomock.Any(), "dummy", expected).Return(nil)
+
+ err1 := m.Store(context.Background(), req2, info2)
+ assert.NoError(t, err1)
+}
+
+func TestConfigMapStorageStoreTooLarge(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ client := configmap.NewMockInterface(ctrl)
+ m := NewConfigMapStorage(client, "dummy", false)
+ value := map[string]string{
+ string(hash(req1.Registry, req1.Image)): mustMarshalInfo(info1),
+ string(hash(req1.Registry+"A", req1.Image)): mustMarshalInfo(info1),
+ string(hash(req2.Registry, req2.Image)): mustMarshalInfo(info2),
+ string(hash(req2.Registry+"A", req2.Image)): mustMarshalInfo(info2),
+ }
+ initial := map[string]string{
+ string(hash(req1.Registry, req1.Image)): mustMarshalInfo(info1),
+ string(hash(req1.Registry+"A", req1.Image)): mustMarshalInfo(info1),
+ string(hash(req2.Registry, req2.Image)): mustMarshalInfo(info2),
+ string(hash(req2.Registry+"A", req2.Image)): mustMarshalInfo(info2),
+ string(hash(req3.Registry, req3.Image)): mustMarshalInfo(info3),
+ }
+ expected := map[string]string{
+ string(hash(req2.Registry, req2.Image)): mustMarshalInfo(info2),
+ string(hash(req2.Registry+"A", req2.Image)): mustMarshalInfo(info2),
+ string(hash(req3.Registry, req3.Image)): mustMarshalInfo(info3),
+ }
+
+ client.EXPECT().Get(gomock.Any(), "dummy").Return(value, nil)
+ client.EXPECT().Apply(gomock.Any(), "dummy", initial).Return(k8serrors.NewRequestEntityTooLargeError("test"))
+ client.EXPECT().Apply(gomock.Any(), "dummy", expected).Return(nil)
+
+ err1 := m.Store(context.Background(), req3, info3)
+ assert.NoError(t, err1)
+}
+
+func TestConfigMapStorageStoreMany(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ client := configmap.NewMockInterface(ctrl)
+ m := NewConfigMapStorage(client, "dummy", false)
+ value := map[string]string{
+ string(hash(req1.Registry, req1.Image)): mustMarshalInfo(info1),
+ }
+ expected := map[string]string{
+ string(hash(req2.Registry, req2.Image)): mustMarshalInfo(info2),
+ string(hash(req3.Registry, req3.Image)): mustMarshalInfo(info3),
+ }
+ maps.Copy(expected, value)
+
+ client.EXPECT().Get(gomock.Any(), "dummy").Return(value, nil)
+ client.EXPECT().Apply(gomock.Any(), "dummy", expected).Return(nil)
+
+ err1 := m.StoreMany(context.Background(), map[Hash]Info{
+ hash(req2.Registry, req2.Image): info2,
+ hash(req3.Registry, req3.Image): info3,
+ })
+ assert.NoError(t, err1)
+}
+
+func TestConfigMapStorageStoreManyTooLarge(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ client := configmap.NewMockInterface(ctrl)
+ m := NewConfigMapStorage(client, "dummy", false)
+ value := map[string]string{
+ string(hash(req1.Registry, req1.Image)): mustMarshalInfo(info1),
+ string(hash(req1.Registry+"A", req1.Image)): mustMarshalInfo(info1),
+ string(hash(req2.Registry, req2.Image)): mustMarshalInfo(info2),
+ }
+ initial := map[string]string{
+ string(hash(req1.Registry, req1.Image)): mustMarshalInfo(info1),
+ string(hash(req1.Registry+"A", req1.Image)): mustMarshalInfo(info1),
+ string(hash(req2.Registry, req2.Image)): mustMarshalInfo(info2),
+ string(hash(req2.Registry+"A", req2.Image)): mustMarshalInfo(info2),
+ string(hash(req3.Registry, req3.Image)): mustMarshalInfo(info3),
+ }
+ expected := map[string]string{
+ string(hash(req2.Registry, req2.Image)): mustMarshalInfo(info2),
+ string(hash(req2.Registry+"A", req2.Image)): mustMarshalInfo(info2),
+ string(hash(req3.Registry, req3.Image)): mustMarshalInfo(info3),
+ }
+
+ client.EXPECT().Get(gomock.Any(), "dummy").Return(value, nil)
+ client.EXPECT().Apply(gomock.Any(), "dummy", initial).Return(k8serrors.NewRequestEntityTooLargeError("test"))
+ client.EXPECT().Apply(gomock.Any(), "dummy", expected).Return(nil)
+
+ err1 := m.StoreMany(context.Background(), map[Hash]Info{
+ hash(req2.Registry+"A", req2.Image): info2,
+ hash(req3.Registry, req3.Image): info3,
+ })
+ assert.NoError(t, err1)
+}
+
+func TestConfigMapStorageCopyTo(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ client := configmap.NewMockInterface(ctrl)
+ s := NewMockStorageWithTransfer(ctrl)
+ m := NewConfigMapStorage(client, "dummy", false)
+ value := map[string]string{
+ string(hash(req1.Registry, req1.Image)): mustMarshalInfo(info1),
+ string(hash(req2.Registry, req2.Image)): mustMarshalInfo(info2),
+ string(hash(req3.Registry, req3.Image)): mustMarshalInfo(info3),
+ }
+ expected := map[Hash]Info{
+ hash(req1.Registry, req1.Image): info1,
+ hash(req2.Registry, req2.Image): info2,
+ hash(req3.Registry, req3.Image): info3,
+ }
+ client.EXPECT().Get(gomock.Any(), "dummy").Return(value, nil)
+ s.EXPECT().StoreMany(gomock.Any(), expected).Return(nil)
+
+ err1 := m.CopyTo(context.Background(), s)
+ assert.NoError(t, err1)
+}
diff --git a/pkg/imageinspector/inspector.go b/pkg/imageinspector/inspector.go
new file mode 100644
index 00000000000..129a2721d0d
--- /dev/null
+++ b/pkg/imageinspector/inspector.go
@@ -0,0 +1,100 @@
+package imageinspector
+
+import (
+ "context"
+ "fmt"
+ "path/filepath"
+
+ "github.com/pkg/errors"
+ corev1 "k8s.io/api/core/v1"
+
+ "github.com/kubeshop/testkube/pkg/log"
+)
+
+type inspector struct {
+ defaultRegistry string
+ fetcher InfoFetcher
+ secrets SecretFetcher
+ storage []Storage
+}
+
+func NewInspector(defaultRegistry string, infoFetcher InfoFetcher, secretFetcher SecretFetcher, storage ...Storage) Inspector {
+ return &inspector{
+ defaultRegistry: defaultRegistry,
+ fetcher: infoFetcher,
+ secrets: secretFetcher,
+ storage: storage,
+ }
+}
+
+func (i *inspector) get(ctx context.Context, registry, image string) *Info {
+ for _, s := range i.storage {
+ v, err := s.Get(ctx, RequestBase{Registry: registry, Image: image})
+ if err != nil && !errors.Is(err, context.Canceled) {
+ log.DefaultLogger.Warnw("error while getting image details from cache", "registry", registry, "image", image, "error", err)
+ }
+ if v != nil {
+ return v
+ }
+ }
+ return nil
+}
+
+func (i *inspector) fetch(ctx context.Context, registry, image string, pullSecretNames []string) (*Info, error) {
+ // Fetch the secrets
+ secrets := make([]corev1.Secret, len(pullSecretNames))
+ for idx, name := range pullSecretNames {
+ secret, err := i.secrets.Get(ctx, name)
+ if err != nil {
+ return nil, errors.Wrap(err, fmt.Sprintf("fetching '%s' pull secret", name))
+ }
+ secrets[idx] = *secret
+ }
+
+ // Load the image details
+ info, err := i.fetcher.Fetch(ctx, registry, image, secrets)
+ if err != nil {
+ return nil, errors.Wrap(err, fmt.Sprintf("fetching '%s' image from '%s' registry", image, registry))
+ } else if info == nil {
+ return nil, fmt.Errorf("unknown problem with fetching '%s' image from '%s' registry", image, registry)
+ }
+ if info.Shell != "" && !filepath.IsAbs(info.Shell) {
+ info.Shell = ""
+ }
+ return info, err
+}
+
+func (i *inspector) save(ctx context.Context, registry, image string, info *Info) {
+ if info == nil {
+ return
+ }
+ for _, s := range i.storage {
+ if err := s.Store(ctx, RequestBase{Registry: registry, Image: image}, *info); err != nil {
+ log.DefaultLogger.Warnw("error while saving image details in the cache", "registry", registry, "image", image, "error", err)
+ }
+ }
+}
+
+func (i *inspector) Inspect(ctx context.Context, registry, image string, pullPolicy corev1.PullPolicy, pullSecretNames []string) (*Info, error) {
+ // Load from cache
+ if pullPolicy != corev1.PullAlways {
+ value := i.get(ctx, registry, image)
+ if value != nil {
+ return value, nil
+ }
+ }
+
+ // Fetch the data
+ value, err := i.fetch(ctx, registry, image, pullSecretNames)
+ if err != nil {
+ return nil, errors.Wrap(err, fmt.Sprintf("inspecting image: '%s' at '%s' registry", image, registry))
+ }
+ if value == nil {
+ return nil, fmt.Errorf("not found image details for: '%s' at '%s' registry", image, registry)
+ }
+
+ // Save asynchronously
+ go i.save(context.Background(), registry, image, value)
+
+ return value, nil
+}
diff --git a/pkg/imageinspector/inspector_test.go b/pkg/imageinspector/inspector_test.go
new file mode 100644
index 00000000000..e005d7a51d0
--- /dev/null
+++ b/pkg/imageinspector/inspector_test.go
@@ -0,0 +1,56 @@
+package imageinspector
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/golang/mock/gomock"
+ "github.com/stretchr/testify/assert"
+ corev1 "k8s.io/api/core/v1"
+)
+
+func TestInspectorInspect(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ infos := NewMockInfoFetcher(ctrl)
+ secrets := NewMockSecretFetcher(ctrl)
+ storage1 := NewMockStorageWithTransfer(ctrl)
+ storage2 := NewMockStorageWithTransfer(ctrl)
+ inspector := NewInspector("default", infos, secrets, storage1, storage2)
+
+ sec := corev1.Secret{StringData: map[string]string{"foo": "bar"}}
+ req := RequestBase{Registry: "regname", Image: "imgname"}
+ storage1.EXPECT().Get(gomock.Any(), req).Return(nil, nil)
+ storage2.EXPECT().Get(gomock.Any(), req).Return(nil, nil)
+ secrets.EXPECT().Get(gomock.Any(), "secname").Return(&sec, nil)
+ infos.EXPECT().Fetch(gomock.Any(), req.Registry, req.Image, []corev1.Secret{sec}).Return(&info1, nil)
+
+ storage1.EXPECT().Store(gomock.Any(), req, info1).Return(nil)
+ storage2.EXPECT().Store(gomock.Any(), req, info1).Return(nil)
+
+ v, err := inspector.Inspect(context.Background(), req.Registry, req.Image, corev1.PullIfNotPresent, []string{"secname"})
+ assert.NoError(t, err)
+ assert.Equal(t, &info1, v)
+
+ // Wait until asynchronous storage will be done
+ <-time.After(10 * time.Millisecond)
+}
+
+func TestInspectorInspectWithCache(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ infos := NewMockInfoFetcher(ctrl)
+ secrets := NewMockSecretFetcher(ctrl)
+ storage1 := NewMockStorageWithTransfer(ctrl)
+ storage2 := NewMockStorageWithTransfer(ctrl)
+ inspector := NewInspector("default", infos, secrets, storage1, storage2)
+
+ req := RequestBase{Registry: "regname", Image: "imgname"}
+ storage1.EXPECT().Get(gomock.Any(), req).Return(&info1, nil)
+
+ v, err := inspector.Inspect(context.Background(), req.Registry, req.Image, corev1.PullIfNotPresent, []string{"secname"})
+ assert.NoError(t, err)
+ assert.Equal(t, &info1, v)
+
+ // Wait until asynchronous storage will be done
+ <-time.After(10 * time.Millisecond)
+}
diff --git a/pkg/imageinspector/memorystorage.go b/pkg/imageinspector/memorystorage.go
new file mode 100644
index 00000000000..c9ddfdc115a
--- /dev/null
+++ b/pkg/imageinspector/memorystorage.go
@@ -0,0 +1,59 @@
+package imageinspector
+
+import (
+ "context"
+ "sync"
+)
+
+type memoryStorage struct {
+ data map[Hash]Info
+ mu sync.RWMutex
+}
+
+func NewMemoryStorage() StorageWithTransfer {
+ return &memoryStorage{
+ data: make(map[Hash]Info),
+ }
+}
+
+func (m *memoryStorage) StoreMany(_ context.Context, data map[Hash]Info) error {
+ if data == nil {
+ return nil
+ }
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ for k, v := range data {
+ if vv, ok := m.data[k]; !ok || v.FetchedAt.After(vv.FetchedAt) {
+ m.data[k] = v
+ }
+ }
+ return nil
+}
+
+func (m *memoryStorage) CopyTo(ctx context.Context, other ...StorageTransfer) (err error) {
+ if len(other) == 0 {
+ return
+ }
+ for _, v := range other {
+ err = v.StoreMany(ctx, m.data)
+ if err != nil {
+ return
+ }
+ }
+ return
+}
+
+func (m *memoryStorage) Store(ctx context.Context, request RequestBase, info Info) error {
+ return m.StoreMany(ctx, map[Hash]Info{
+ hash(request.Registry, request.Image): info,
+ })
+}
+
+func (m *memoryStorage) Get(_ context.Context, request RequestBase) (*Info, error) {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+ if v, ok := m.data[hash(request.Registry, request.Image)]; ok {
+ return &v, nil
+ }
+ return nil, nil
+}
diff --git a/pkg/imageinspector/memorystorage_test.go b/pkg/imageinspector/memorystorage_test.go
new file mode 100644
index 00000000000..c43bae44f62
--- /dev/null
+++ b/pkg/imageinspector/memorystorage_test.go
@@ -0,0 +1,114 @@
+package imageinspector
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/golang/mock/gomock"
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ time1 = time.Now().UTC().Add(-4 * time.Minute)
+ time2 = time.Now().UTC().Add(-2 * time.Minute)
+ time3 = time.Now().UTC()
+ noInfoPtr *Info
+ info1 = Info{
+ FetchedAt: time1,
+ Entrypoint: []string{"en", "try"},
+ Cmd: []string{"c", "md"},
+ Shell: "/bin/shell",
+ WorkingDir: "some-wd",
+ }
+ info2 = Info{
+ FetchedAt: time2,
+ Entrypoint: []string{"en", "try2"},
+ Cmd: []string{"c", "md2"},
+ Shell: "/bin/shell",
+ WorkingDir: "some-wd",
+ }
+ info3 = Info{
+ FetchedAt: time3,
+ Entrypoint: []string{"en", "try3"},
+ Cmd: []string{"c", "md3"},
+ Shell: "/bin/shell",
+ WorkingDir: "some-wd",
+ }
+ req1 = RequestBase{
+ Registry: "foo",
+ Image: "bar",
+ }
+ req1Copy = RequestBase{
+ Registry: "foo",
+ Image: "bar",
+ }
+ req2 = RequestBase{
+ Registry: "foo2",
+ Image: "bar2",
+ }
+ req3 = RequestBase{
+ Registry: "foo3",
+ Image: "bar3",
+ }
+)
+
+func TestMemoryStorageGetAndStore(t *testing.T) {
+ m := NewMemoryStorage()
+ err1 := m.Store(context.Background(), req1, info1)
+ err2 := m.Store(context.Background(), req2, info2)
+ v1, gErr1 := m.Get(context.Background(), req1)
+ v2, gErr2 := m.Get(context.Background(), req2)
+ v3, gErr3 := m.Get(context.Background(), req1Copy)
+ v4, gErr4 := m.Get(context.Background(), req3)
+ assert.NoError(t, err1)
+ assert.NoError(t, err2)
+ assert.NoError(t, gErr1)
+ assert.NoError(t, gErr2)
+ assert.NoError(t, gErr3)
+ assert.NoError(t, gErr4)
+ assert.Equal(t, &info1, v1)
+ assert.Equal(t, &info2, v2)
+ assert.Equal(t, &info1, v3)
+ assert.Equal(t, noInfoPtr, v4)
+}
+
+func TestMemoryStorageStoreManyAndGet(t *testing.T) {
+ m := NewMemoryStorage()
+ err1 := m.StoreMany(context.Background(), map[Hash]Info{
+ hash(req1.Registry, req1.Image): info1,
+ hash(req2.Registry, req2.Image): info2,
+ })
+ v1, gErr1 := m.Get(context.Background(), req1)
+ v2, gErr2 := m.Get(context.Background(), req2)
+ v3, gErr3 := m.Get(context.Background(), req1Copy)
+ v4, gErr4 := m.Get(context.Background(), req3)
+ assert.NoError(t, err1)
+ assert.NoError(t, gErr1)
+ assert.NoError(t, gErr2)
+ assert.NoError(t, gErr3)
+ assert.NoError(t, gErr4)
+ assert.Equal(t, &info1, v1)
+ assert.Equal(t, &info2, v2)
+ assert.Equal(t, &info1, v3)
+ assert.Equal(t, noInfoPtr, v4)
+}
+
+func TestMemoryStorageFillAndCopyTo(t *testing.T) {
+ m := NewMemoryStorage()
+ value := map[Hash]Info{
+ hash(req1.Registry, req1.Image): info1,
+ hash(req2.Registry, req2.Image): info2,
+ }
+ err1 := m.StoreMany(context.Background(), value)
+
+ ctrl := gomock.NewController(t)
+ mockStorage1 := NewMockStorageWithTransfer(ctrl)
+ mockStorage2 := NewMockStorageWithTransfer(ctrl)
+ mockStorage1.EXPECT().StoreMany(gomock.Any(), value)
+ mockStorage2.EXPECT().StoreMany(gomock.Any(), value)
+ err2 := m.CopyTo(context.Background(), mockStorage1, mockStorage2)
+
+ assert.NoError(t, err1)
+ assert.NoError(t, err2)
+}
diff --git a/pkg/imageinspector/mock_infofetcher.go b/pkg/imageinspector/mock_infofetcher.go
new file mode 100644
index 00000000000..ba2b092cf12
--- /dev/null
+++ b/pkg/imageinspector/mock_infofetcher.go
@@ -0,0 +1,51 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/imageinspector (interfaces: InfoFetcher)
+
+// Package imageinspector is a generated GoMock package.
+package imageinspector
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ v1 "k8s.io/api/core/v1"
+)
+
+// MockInfoFetcher is a mock of InfoFetcher interface.
+type MockInfoFetcher struct {
+ ctrl *gomock.Controller
+ recorder *MockInfoFetcherMockRecorder
+}
+
+// MockInfoFetcherMockRecorder is the mock recorder for MockInfoFetcher.
+type MockInfoFetcherMockRecorder struct {
+ mock *MockInfoFetcher
+}
+
+// NewMockInfoFetcher creates a new mock instance.
+func NewMockInfoFetcher(ctrl *gomock.Controller) *MockInfoFetcher {
+ mock := &MockInfoFetcher{ctrl: ctrl}
+ mock.recorder = &MockInfoFetcherMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockInfoFetcher) EXPECT() *MockInfoFetcherMockRecorder {
+ return m.recorder
+}
+
+// Fetch mocks base method.
+func (m *MockInfoFetcher) Fetch(arg0 context.Context, arg1, arg2 string, arg3 []v1.Secret) (*Info, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Fetch", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*Info)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Fetch indicates an expected call of Fetch.
+func (mr *MockInfoFetcherMockRecorder) Fetch(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockInfoFetcher)(nil).Fetch), arg0, arg1, arg2, arg3)
+}
diff --git a/pkg/imageinspector/mock_inspector.go b/pkg/imageinspector/mock_inspector.go
new file mode 100644
index 00000000000..944c9b6a230
--- /dev/null
+++ b/pkg/imageinspector/mock_inspector.go
@@ -0,0 +1,51 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/imageinspector (interfaces: Inspector)
+
+// Package imageinspector is a generated GoMock package.
+package imageinspector
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ v1 "k8s.io/api/core/v1"
+)
+
+// MockInspector is a mock of Inspector interface.
+type MockInspector struct {
+ ctrl *gomock.Controller
+ recorder *MockInspectorMockRecorder
+}
+
+// MockInspectorMockRecorder is the mock recorder for MockInspector.
+type MockInspectorMockRecorder struct {
+ mock *MockInspector
+}
+
+// NewMockInspector creates a new mock instance.
+func NewMockInspector(ctrl *gomock.Controller) *MockInspector {
+ mock := &MockInspector{ctrl: ctrl}
+ mock.recorder = &MockInspectorMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockInspector) EXPECT() *MockInspectorMockRecorder {
+ return m.recorder
+}
+
+// Inspect mocks base method.
+func (m *MockInspector) Inspect(arg0 context.Context, arg1, arg2 string, arg3 v1.PullPolicy, arg4 []string) (*Info, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Inspect", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(*Info)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Inspect indicates an expected call of Inspect.
+func (mr *MockInspectorMockRecorder) Inspect(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Inspect", reflect.TypeOf((*MockInspector)(nil).Inspect), arg0, arg1, arg2, arg3, arg4)
+}
diff --git a/pkg/imageinspector/mock_secretfetcher.go b/pkg/imageinspector/mock_secretfetcher.go
new file mode 100644
index 00000000000..d4ff8588512
--- /dev/null
+++ b/pkg/imageinspector/mock_secretfetcher.go
@@ -0,0 +1,51 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/imageinspector (interfaces: SecretFetcher)
+
+// Package imageinspector is a generated GoMock package.
+package imageinspector
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ v1 "k8s.io/api/core/v1"
+)
+
+// MockSecretFetcher is a mock of SecretFetcher interface.
+type MockSecretFetcher struct {
+ ctrl *gomock.Controller
+ recorder *MockSecretFetcherMockRecorder
+}
+
+// MockSecretFetcherMockRecorder is the mock recorder for MockSecretFetcher.
+type MockSecretFetcherMockRecorder struct {
+ mock *MockSecretFetcher
+}
+
+// NewMockSecretFetcher creates a new mock instance.
+func NewMockSecretFetcher(ctrl *gomock.Controller) *MockSecretFetcher {
+ mock := &MockSecretFetcher{ctrl: ctrl}
+ mock.recorder = &MockSecretFetcherMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockSecretFetcher) EXPECT() *MockSecretFetcherMockRecorder {
+ return m.recorder
+}
+
+// Get mocks base method.
+func (m *MockSecretFetcher) Get(arg0 context.Context, arg1 string) (*v1.Secret, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Get", arg0, arg1)
+ ret0, _ := ret[0].(*v1.Secret)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockSecretFetcherMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSecretFetcher)(nil).Get), arg0, arg1)
+}
diff --git a/pkg/imageinspector/mock_storage.go b/pkg/imageinspector/mock_storage.go
new file mode 100644
index 00000000000..22c00a993a3
--- /dev/null
+++ b/pkg/imageinspector/mock_storage.go
@@ -0,0 +1,97 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/imageinspector (interfaces: StorageWithTransfer)
+
+// Package imageinspector is a generated GoMock package.
+package imageinspector
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+)
+
+// MockStorageWithTransfer is a mock of StorageWithTransfer interface.
+type MockStorageWithTransfer struct {
+ ctrl *gomock.Controller
+ recorder *MockStorageWithTransferMockRecorder
+}
+
+// MockStorageWithTransferMockRecorder is the mock recorder for MockStorageWithTransfer.
+type MockStorageWithTransferMockRecorder struct {
+ mock *MockStorageWithTransfer
+}
+
+// NewMockStorageWithTransfer creates a new mock instance.
+func NewMockStorageWithTransfer(ctrl *gomock.Controller) *MockStorageWithTransfer {
+ mock := &MockStorageWithTransfer{ctrl: ctrl}
+ mock.recorder = &MockStorageWithTransferMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockStorageWithTransfer) EXPECT() *MockStorageWithTransferMockRecorder {
+ return m.recorder
+}
+
+// CopyTo mocks base method.
+func (m *MockStorageWithTransfer) CopyTo(arg0 context.Context, arg1 ...StorageTransfer) error {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "CopyTo", varargs...)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// CopyTo indicates an expected call of CopyTo.
+func (mr *MockStorageWithTransferMockRecorder) CopyTo(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyTo", reflect.TypeOf((*MockStorageWithTransfer)(nil).CopyTo), varargs...)
+}
+
+// Get mocks base method.
+func (m *MockStorageWithTransfer) Get(arg0 context.Context, arg1 RequestBase) (*Info, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Get", arg0, arg1)
+ ret0, _ := ret[0].(*Info)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockStorageWithTransferMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStorageWithTransfer)(nil).Get), arg0, arg1)
+}
+
+// Store mocks base method.
+func (m *MockStorageWithTransfer) Store(arg0 context.Context, arg1 RequestBase, arg2 Info) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Store", arg0, arg1, arg2)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Store indicates an expected call of Store.
+func (mr *MockStorageWithTransferMockRecorder) Store(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Store", reflect.TypeOf((*MockStorageWithTransfer)(nil).Store), arg0, arg1, arg2)
+}
+
+// StoreMany mocks base method.
+func (m *MockStorageWithTransfer) StoreMany(arg0 context.Context, arg1 map[Hash]Info) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StoreMany", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// StoreMany indicates an expected call of StoreMany.
+func (mr *MockStorageWithTransferMockRecorder) StoreMany(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreMany", reflect.TypeOf((*MockStorageWithTransfer)(nil).StoreMany), arg0, arg1)
+}
diff --git a/pkg/imageinspector/secretfetcher.go b/pkg/imageinspector/secretfetcher.go
new file mode 100644
index 00000000000..44b81f51159
--- /dev/null
+++ b/pkg/imageinspector/secretfetcher.go
@@ -0,0 +1,50 @@
+package imageinspector
+
+import (
+ "context"
+ "sync"
+
+ "github.com/pkg/errors"
+ corev1 "k8s.io/api/core/v1"
+
+ "github.com/kubeshop/testkube/pkg/secret"
+)
+
+type secretFetcher struct {
+ client secret.Interface
+ cache map[string]*corev1.Secret
+ mu sync.RWMutex
+}
+
+func NewSecretFetcher(client secret.Interface) SecretFetcher {
+ return &secretFetcher{
+ client: client,
+ cache: make(map[string]*corev1.Secret),
+ }
+}
+
+func (s *secretFetcher) Get(ctx context.Context, name string) (*corev1.Secret, error) {
+ // Get cached secret
+ s.mu.RLock()
+ if v, ok := s.cache[name]; ok {
+ s.mu.RUnlock()
+ return v, nil
+ }
+ s.mu.RUnlock()
+
+ // Load secret from the Kubernetes
+ obj, err := s.client.GetObject(name)
+ if err != nil {
+ return nil, errors.Wrap(err, "fetching image pull secret")
+ }
+
+ // Save in cache
+ s.mu.Lock()
+ s.cache[name] = obj
+ s.mu.Unlock()
+
+ if ctx.Err() != nil {
+ return nil, ctx.Err()
+ }
+ return obj, nil
+}
diff --git a/pkg/imageinspector/secretfetcher_test.go b/pkg/imageinspector/secretfetcher_test.go
new file mode 100644
index 00000000000..e60e7cd5ee6
--- /dev/null
+++ b/pkg/imageinspector/secretfetcher_test.go
@@ -0,0 +1,66 @@
+package imageinspector
+
+import (
+ "context"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/stretchr/testify/assert"
+ corev1 "k8s.io/api/core/v1"
+ k8serrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+
+ "github.com/kubeshop/testkube/pkg/secret"
+)
+
+func TestSecretFetcherGetExisting(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ client := secret.NewMockInterface(ctrl)
+ fetcher := NewSecretFetcher(client)
+
+ expected := corev1.Secret{
+ StringData: map[string]string{"key": "value"},
+ }
+ client.EXPECT().GetObject("dummy").Return(&expected, nil)
+
+ result, err := fetcher.Get(context.Background(), "dummy")
+ assert.NoError(t, err)
+ assert.Equal(t, &expected, result)
+}
+
+func TestSecretFetcherGetCache(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ client := secret.NewMockInterface(ctrl)
+ fetcher := NewSecretFetcher(client)
+
+ expected := corev1.Secret{
+ StringData: map[string]string{"key": "value"},
+ }
+ client.EXPECT().GetObject("dummy").Return(&expected, nil)
+
+ result1, err1 := fetcher.Get(context.Background(), "dummy")
+ result2, err2 := fetcher.Get(context.Background(), "dummy")
+ assert.NoError(t, err1)
+ assert.NoError(t, err2)
+ assert.Equal(t, &expected, result1)
+ assert.Equal(t, &expected, result2)
+}
+
+func TestSecretFetcherGetError(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ client := secret.NewMockInterface(ctrl)
+ fetcher := NewSecretFetcher(client)
+
+ client.EXPECT().GetObject("dummy").Return(nil, k8serrors.NewNotFound(schema.GroupResource{}, "dummy"))
+ client.EXPECT().GetObject("dummy").Return(nil, k8serrors.NewNotFound(schema.GroupResource{}, "dummy"))
+
+ result1, err1 := fetcher.Get(context.Background(), "dummy")
+ result2, err2 := fetcher.Get(context.Background(), "dummy")
+ var noSecret *corev1.Secret
+ assert.Error(t, err1)
+ assert.Error(t, err2)
+ assert.True(t, k8serrors.IsNotFound(err1))
+ assert.True(t, k8serrors.IsNotFound(err2))
+ assert.Equal(t, noSecret, result1)
+ assert.Equal(t, noSecret, result2)
+}
diff --git a/pkg/imageinspector/serialization.go b/pkg/imageinspector/serialization.go
new file mode 100644
index 00000000000..9a319d7f6d0
--- /dev/null
+++ b/pkg/imageinspector/serialization.go
@@ -0,0 +1,29 @@
+package imageinspector
+
+import (
+ "encoding/json"
+ "fmt"
+ "regexp"
+ "strings"
+)
+
+type Hash string
+
+var hashKeyRe = regexp.MustCompile("[^a-zA-Z0-9-_]")
+
+func hash(registry, image string) Hash {
+ return Hash(hashKeyRe.ReplaceAllString(strings.ReplaceAll(fmt.Sprintf("%s/%s", registry, image), "/", "."), "_-"))
+}
+
+func marshalInfo(v Info) (string, error) {
+ res, err := json.Marshal(v)
+ if err != nil {
+ return "", err
+ }
+ return string(res), nil
+}
+
+func unmarshalInfo(s string) (v Info, err error) {
+ err = json.Unmarshal([]byte(s), &v)
+ return
+}
diff --git a/pkg/imageinspector/skopeofetcher.go b/pkg/imageinspector/skopeofetcher.go
new file mode 100644
index 00000000000..f043aafb319
--- /dev/null
+++ b/pkg/imageinspector/skopeofetcher.go
@@ -0,0 +1,53 @@
+package imageinspector
+
+import (
+ "context"
+ "strconv"
+ "strings"
+ "time"
+
+ corev1 "k8s.io/api/core/v1"
+
+ "github.com/kubeshop/testkube/pkg/skopeo"
+)
+
+type skopeoFetcher struct {
+}
+
+func NewSkopeoFetcher() InfoFetcher {
+ return &skopeoFetcher{}
+}
+
+func (s *skopeoFetcher) Fetch(ctx context.Context, registry, image string, pullSecrets []corev1.Secret) (*Info, error) {
+ client, err := skopeo.NewClientFromSecrets(pullSecrets, registry)
+ if err != nil {
+ return nil, err
+ }
+ info, err := client.Inspect(image) // TODO: Support passing context
+ if err != nil {
+ return nil, err
+ }
+ user, group := determineUserGroupPair(info.Config.User)
+ return &Info{
+ FetchedAt: time.Now(),
+ Entrypoint: info.Config.Entrypoint,
+ Cmd: info.Config.Cmd,
+ Shell: info.Shell,
+ WorkingDir: info.Config.WorkingDir,
+ User: user,
+ Group: group,
+ }, nil
+}
+
+func determineUserGroupPair(userGroupStr string) (int64, int64) {
+ if userGroupStr == "" {
+ userGroupStr = "0"
+ }
+ userStr, groupStr, _ := strings.Cut(userGroupStr, ":")
+ if groupStr == "" {
+ groupStr = userStr
+ }
+ user, _ := strconv.Atoi(userStr)
+ group, _ := strconv.Atoi(groupStr)
+ return int64(user), int64(group)
+}
diff --git a/pkg/imageinspector/types.go b/pkg/imageinspector/types.go
new file mode 100644
index 00000000000..99987ba2b2a
--- /dev/null
+++ b/pkg/imageinspector/types.go
@@ -0,0 +1,59 @@
+package imageinspector
+
+import (
+ "context"
+ "time"
+
+ corev1 "k8s.io/api/core/v1"
+)
+
+//go:generate mockgen -destination=./mock_inspector.go -package=imageinspector "github.com/kubeshop/testkube/pkg/imageinspector" Inspector
+type Inspector interface {
+ Inspect(ctx context.Context, registry, image string, pullPolicy corev1.PullPolicy, pullSecretNames []string) (*Info, error)
+}
+
+type StorageTransfer interface {
+ StoreMany(ctx context.Context, data map[Hash]Info) error
+ CopyTo(ctx context.Context, other ...StorageTransfer) error
+}
+
+type Storage interface {
+ Store(ctx context.Context, request RequestBase, info Info) error
+ Get(ctx context.Context, request RequestBase) (*Info, error)
+}
+
+//go:generate mockgen -destination=./mock_storage.go -package=imageinspector "github.com/kubeshop/testkube/pkg/imageinspector" StorageWithTransfer
+type StorageWithTransfer interface {
+ StorageTransfer
+ Storage
+}
+
+//go:generate mockgen -destination=./mock_secretfetcher.go -package=imageinspector "github.com/kubeshop/testkube/pkg/imageinspector" SecretFetcher
+type SecretFetcher interface {
+ Get(ctx context.Context, name string) (*corev1.Secret, error)
+}
+
+//go:generate mockgen -destination=./mock_infofetcher.go -package=imageinspector "github.com/kubeshop/testkube/pkg/imageinspector" InfoFetcher
+type InfoFetcher interface {
+ Fetch(ctx context.Context, registry, image string, pullSecrets []corev1.Secret) (*Info, error)
+}
+
+type Info struct {
+ FetchedAt time.Time `json:"a,omitempty"`
+ Entrypoint []string `json:"e,omitempty"`
+ Cmd []string `json:"c,omitempty"`
+ Shell string `json:"s,omitempty"`
+ WorkingDir string `json:"w,omitempty"`
+ User int64 `json:"u,omitempty"`
+ Group int64 `json:"g,omitempty"`
+}
+
+type RequestBase struct {
+ Image string
+ Registry string
+}
+
+type Request struct {
+ RequestBase
+ PullPolicy corev1.PullPolicy
+}
diff --git a/pkg/logs/adapter/cloud.go b/pkg/logs/adapter/cloud.go
index 1c2075a24b5..1e89bccaa32 100644
--- a/pkg/logs/adapter/cloud.go
+++ b/pkg/logs/adapter/cloud.go
@@ -1,26 +1,86 @@
package adapter
-import "github.com/kubeshop/testkube/pkg/logs/events"
+import (
+ "context"
+ "sync"
+
+ "github.com/pkg/errors"
+ "go.uber.org/zap"
+ "google.golang.org/grpc/metadata"
+
+ "github.com/kubeshop/testkube/pkg/log"
+ "github.com/kubeshop/testkube/pkg/logs/events"
+ "github.com/kubeshop/testkube/pkg/logs/pb"
+)
var _ Adapter = &CloudAdapter{}
// NewCloudConsumer creates new CloudSubscriber which will send data to local MinIO bucket
-func NewCloudConsumer() *CloudAdapter {
- return &CloudAdapter{}
+func NewCloudAdapter(grpcConn pb.CloudLogsServiceClient, agentApiKey string) *CloudAdapter {
+ return &CloudAdapter{
+ client: grpcConn,
+ agentApiKey: agentApiKey,
+ logger: log.DefaultLogger.With("service", "logs-cloud-adapter"),
+ }
}
type CloudAdapter struct {
- Bucket string
+ client pb.CloudLogsServiceClient
+ streams sync.Map
+ agentApiKey string
+ logger *zap.SugaredLogger
+}
+
+func (s *CloudAdapter) Init(ctx context.Context, id string) error {
+
+ // write metadata to the stream context
+ md := metadata.Pairs("api-key", s.agentApiKey, "execution-id", id)
+ ctx = metadata.NewOutgoingContext(ctx, md)
+
+ stream, err := s.client.Stream(ctx)
+ if err != nil {
+ return errors.Wrap(err, "can't init stream")
+ }
+
+ s.streams.Store(id, stream)
+
+ return nil
}
-func (s *CloudAdapter) Notify(id string, e events.Log) error {
- panic("not implemented")
+func (s *CloudAdapter) Notify(ctx context.Context, id string, e events.Log) error {
+ c, err := s.getStreamClient(id)
+ if err != nil {
+ return errors.Wrap(err, "can't get stream client for id: "+id)
+ }
+
+ return c.Send(pb.MapToPB(e))
}
-func (s *CloudAdapter) Stop(id string) error {
- panic("not implemented")
+func (s *CloudAdapter) Stop(ctx context.Context, id string) error {
+ c, err := s.getStreamClient(id)
+ if err != nil {
+ return errors.Wrap(err, "can't get stream client for id: "+id)
+ }
+
+ resp, err := c.CloseAndRecv()
+ if err != nil {
+ return errors.Wrap(err, "closing log stream error")
+ }
+ s.logger.Debugw("closing response", "resp", resp, "id", id)
+
+ s.streams.Delete(id)
+ return nil
}
func (s *CloudAdapter) Name() string {
return "cloud"
}
+
+func (s *CloudAdapter) getStreamClient(id string) (client pb.CloudLogsService_StreamClient, err error) {
+ c, ok := s.streams.Load(id)
+ if !ok {
+ return nil, errors.New("can't find initialized stream")
+ }
+
+ return c.(pb.CloudLogsService_StreamClient), nil
+}
diff --git a/pkg/logs/adapter/cloud_test.go b/pkg/logs/adapter/cloud_test.go
new file mode 100644
index 00000000000..54a8866c805
--- /dev/null
+++ b/pkg/logs/adapter/cloud_test.go
@@ -0,0 +1,319 @@
+package adapter
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "math"
+ "math/rand"
+ "net"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/pkg/errors"
+ "github.com/stretchr/testify/assert"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/credentials/insecure"
+ "google.golang.org/grpc/metadata"
+ "google.golang.org/grpc/status"
+
+ "github.com/kubeshop/testkube/pkg/agent"
+ "github.com/kubeshop/testkube/pkg/log"
+ "github.com/kubeshop/testkube/pkg/logs/events"
+ "github.com/kubeshop/testkube/pkg/logs/pb"
+)
+
+func TestCloudAdapter(t *testing.T) {
+
+ t.Run("GRPC server receives log data", func(t *testing.T) {
+ // given grpc test server
+ ts := NewTestServer().WithRandomPort()
+ go ts.Run()
+
+ ctx := context.Background()
+ id := "id1"
+
+ // and connection
+ grpcConn, err := agent.NewGRPCConnection(ctx, true, true, ts.Url, "", "", "", log.DefaultLogger)
+ assert.NoError(t, err)
+ defer grpcConn.Close()
+
+ // and log stream client
+ grpcClient := pb.NewCloudLogsServiceClient(grpcConn)
+ a := NewCloudAdapter(grpcClient, "APIKEY")
+
+ // when stream is initialized
+ err = a.Init(ctx, id)
+ assert.NoError(t, err)
+ // and data is sent to it
+ err = a.Notify(ctx, id, *events.NewLog("log1"))
+ assert.NoError(t, err)
+ err = a.Notify(ctx, id, *events.NewLog("log2"))
+ assert.NoError(t, err)
+ err = a.Notify(ctx, id, *events.NewLog("log3"))
+ assert.NoError(t, err)
+ err = a.Notify(ctx, id, *events.NewLog("log4"))
+ assert.NoError(t, err)
+ // and stream is stopped after sending logs to it
+ err = a.Stop(ctx, id)
+ assert.NoError(t, err)
+
+ // cooldown
+ time.Sleep(time.Millisecond * 100)
+
+ // then all messahes should be delivered to the GRPC server
+ ts.AssertMessagesProcessed(t, id, 4)
+ })
+
+ t.Run("cleaning GRPC connections in adapter on Stop", func(t *testing.T) {
+ // given new test server
+ ts := NewTestServer().WithRandomPort()
+ go ts.Run()
+
+ ctx := context.Background()
+ id1 := "id1"
+ id2 := "id2"
+ id3 := "id3"
+
+ // and connection
+ grpcConn, err := agent.NewGRPCConnection(ctx, true, true, ts.Url, "", "", "", log.DefaultLogger)
+ assert.NoError(t, err)
+ defer grpcConn.Close()
+ grpcClient := pb.NewCloudLogsServiceClient(grpcConn)
+ a := NewCloudAdapter(grpcClient, "APIKEY")
+
+ // when 3 streams are initialized, data is sent, and then stopped
+ err = a.Init(ctx, id1)
+ assert.NoError(t, err)
+ err = a.Notify(ctx, id1, *events.NewLog("log1"))
+ assert.NoError(t, err)
+ err = a.Stop(ctx, id1)
+ assert.NoError(t, err)
+
+ err = a.Init(ctx, id2)
+ assert.NoError(t, err)
+ err = a.Notify(ctx, id2, *events.NewLog("log2"))
+ assert.NoError(t, err)
+ err = a.Stop(ctx, id2)
+ assert.NoError(t, err)
+
+ err = a.Init(ctx, id3)
+ assert.NoError(t, err)
+ err = a.Notify(ctx, id3, *events.NewLog("log3"))
+ assert.NoError(t, err)
+ err = a.Stop(ctx, id3)
+ assert.NoError(t, err)
+
+ // cooldown
+ time.Sleep(time.Millisecond * 100)
+
+ // then messages should be delivered
+ ts.AssertMessagesProcessed(t, id1, 1)
+ ts.AssertMessagesProcessed(t, id2, 1)
+ ts.AssertMessagesProcessed(t, id3, 1)
+
+ // and no stream are registered anymore in cloud adapter
+ assertNoStreams(t, a)
+ })
+
+ t.Run("Send and receive a lot of messages", func(t *testing.T) {
+ // given test server
+ ts := NewTestServer().WithRandomPort()
+ go ts.Run()
+
+ ctx := context.Background()
+ id := "id1M"
+
+ // and grpc connetion to the server
+ grpcConn, err := agent.NewGRPCConnection(ctx, true, true, ts.Url, "", "", "", log.DefaultLogger)
+ assert.NoError(t, err)
+ defer grpcConn.Close()
+
+ // and logs stream client
+ grpcClient := pb.NewCloudLogsServiceClient(grpcConn)
+ a := NewCloudAdapter(grpcClient, "APIKEY")
+
+ // when streams are initialized
+ err = a.Init(ctx, id)
+ assert.NoError(t, err)
+
+ messageCount := 10_000
+ for i := 0; i < messageCount; i++ {
+ // and data is sent
+ err = a.Notify(ctx, id, *events.NewLog("log1"))
+ assert.NoError(t, err)
+ }
+
+ // cooldown
+ time.Sleep(time.Millisecond * 100)
+
+ // then messages should be delivered to GRPC server
+ ts.AssertMessagesProcessed(t, id, messageCount)
+ })
+
+ t.Run("Send to a lot of streams in parallel", func(t *testing.T) {
+ // given test server
+ ts := NewTestServer().WithRandomPort()
+ go ts.Run()
+
+ ctx := context.Background()
+
+ // and grpc connetion to the server
+ grpcConn, err := agent.NewGRPCConnection(ctx, true, true, ts.Url, "", "", "", log.DefaultLogger)
+ assert.NoError(t, err)
+ defer grpcConn.Close()
+
+ // and logs stream client
+ grpcClient := pb.NewCloudLogsServiceClient(grpcConn)
+ a := NewCloudAdapter(grpcClient, "APIKEY")
+
+ streamsCount := 100
+ messageCount := 1_000
+
+ // when streams are initialized
+ var wg sync.WaitGroup
+ wg.Add(streamsCount)
+ for j := 0; j < streamsCount; j++ {
+ err = a.Init(ctx, fmt.Sprintf("id%d", j))
+ assert.NoError(t, err)
+
+ go func(j int) {
+ defer wg.Done()
+ for i := 0; i < messageCount; i++ {
+ // and when data are sent
+ err = a.Notify(ctx, fmt.Sprintf("id%d", j), *events.NewLog("log1"))
+ assert.NoError(t, err)
+ }
+ }(j)
+ }
+
+ wg.Wait()
+
+ // and wait for cooldown
+ time.Sleep(time.Millisecond * 100)
+
+ // then each stream should receive valid data amount
+ for j := 0; j < streamsCount; j++ {
+ ts.AssertMessagesProcessed(t, fmt.Sprintf("id%d", j), messageCount)
+ }
+ })
+
+}
+
+func assertNoStreams(t *testing.T, a *CloudAdapter) {
+ t.Helper()
+ // no stream are registered anymore
+ count := 0
+ a.streams.Range(func(key, value any) bool {
+ count++
+ return true
+ })
+ assert.Equal(t, count, 0)
+}
+
+// Cloud Logs server mock
+func NewTestServer() *TestServer {
+ return &TestServer{
+ Received: make(map[string][]*pb.Log),
+ }
+}
+
+type TestServer struct {
+ Url string
+ pb.UnimplementedCloudLogsServiceServer
+ Received map[string][]*pb.Log
+ lock sync.Mutex
+}
+
+func getVal(ctx context.Context, key string) (string, error) {
+ md, ok := metadata.FromIncomingContext(ctx)
+ if !ok {
+ return "", status.Error(codes.Unauthenticated, "api-key header is missing")
+ }
+ apiKeyMeta := md.Get(key)
+ if len(apiKeyMeta) != 1 {
+ return "", status.Error(codes.Unauthenticated, "api-key header is empty")
+ }
+ if apiKeyMeta[0] == "" {
+ return "", status.Error(codes.Unauthenticated, "api-key header value is empty")
+ }
+
+ return apiKeyMeta[0], nil
+}
+
+func (s *TestServer) Stream(stream pb.CloudLogsService_StreamServer) error {
+ ctx := stream.Context()
+ v, err := getVal(ctx, "execution-id")
+ if err != nil {
+ return err
+ }
+ id := v
+
+ s.lock.Lock()
+ s.Received[id] = []*pb.Log{}
+ s.lock.Unlock()
+
+ for {
+ in, err := stream.Recv()
+ if err != nil {
+ if err == io.EOF {
+ err := stream.SendAndClose(&pb.StreamResponse{Message: "completed"})
+ if err != nil {
+ return status.Error(codes.Internal, "can't close stream: "+err.Error())
+ }
+ return nil
+ }
+ return status.Error(codes.Internal, "can't receive stream: "+err.Error())
+ }
+
+ s.lock.Lock()
+ s.Received[id] = append(s.Received[id], in)
+ s.lock.Unlock()
+ }
+}
+
+func (s *TestServer) WithRandomPort() *TestServer {
+ port := rand.Intn(1000) + 18000
+ s.Url = fmt.Sprintf("127.0.0.1:%d", port)
+ return s
+}
+
+func (s *TestServer) Run() (err error) {
+ lis, err := net.Listen("tcp", s.Url)
+ if err != nil {
+ return errors.Errorf("net listen: %v", err)
+ }
+
+ var opts []grpc.ServerOption
+ creds := insecure.NewCredentials()
+ opts = append(opts, grpc.Creds(creds), grpc.MaxRecvMsgSize(math.MaxInt32))
+
+ // register server logs
+ srv := grpc.NewServer(opts...)
+ srv.RegisterService(&pb.CloudLogsService_ServiceDesc, s)
+ srv.Serve(lis)
+
+ if err != nil {
+ return errors.Wrap(err, "grpc server error")
+ }
+ return nil
+}
+
+func (s *TestServer) AssertMessagesProcessed(t *testing.T, id string, messageCount int) {
+ var received int
+
+ for i := 0; i < 100; i++ {
+ s.lock.Lock()
+ received = len(s.Received[id])
+ s.lock.Unlock()
+
+ if received == messageCount {
+ return
+ }
+ time.Sleep(time.Millisecond * 10)
+ }
+
+ assert.Equal(t, messageCount, received)
+}
diff --git a/pkg/logs/adapter/debug.go b/pkg/logs/adapter/debug.go
new file mode 100644
index 00000000000..cd2f27e7205
--- /dev/null
+++ b/pkg/logs/adapter/debug.go
@@ -0,0 +1,42 @@
+package adapter
+
+import (
+ "context"
+
+ "go.uber.org/zap"
+
+ "github.com/kubeshop/testkube/pkg/log"
+ "github.com/kubeshop/testkube/pkg/logs/events"
+)
+
+var _ Adapter = &DebugAdapter{}
+
+// NewDebugAdapter creates new DebugAdapter which will write logs to stdout
+func NewDebugAdapter() *DebugAdapter {
+ return &DebugAdapter{
+ l: log.DefaultLogger,
+ }
+}
+
+type DebugAdapter struct {
+ l *zap.SugaredLogger
+}
+
+func (s *DebugAdapter) Init(ctx context.Context, id string) error {
+ s.l.Debugw("Initializing", "id", id)
+ return nil
+}
+
+func (s *DebugAdapter) Notify(ctx context.Context, id string, e events.Log) error {
+ s.l.Debugw("got event", "id", id, "event", e)
+ return nil
+}
+
+func (s *DebugAdapter) Stop(ctx context.Context, id string) error {
+ s.l.Debugw("Stopping", "id", id)
+ return nil
+}
+
+func (s *DebugAdapter) Name() string {
+ return "dummy"
+}
diff --git a/pkg/logs/adapter/dummy.go b/pkg/logs/adapter/dummy.go
deleted file mode 100644
index 993e47254cc..00000000000
--- a/pkg/logs/adapter/dummy.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package adapter
-
-import (
- "fmt"
-
- "github.com/kubeshop/testkube/pkg/logs/events"
-)
-
-var _ Adapter = &DummyAdapter{}
-
-// NewS3Subscriber creates new DummySubscriber which will send data to local MinIO bucket
-func NewDummyAdapter() *DummyAdapter {
- return &DummyAdapter{}
-}
-
-type DummyAdapter struct {
- Bucket string
-}
-
-func (s *DummyAdapter) Notify(id string, e events.Log) error {
- fmt.Printf("%s %+v\n", id, e)
- return nil
-}
-
-func (s *DummyAdapter) Stop(id string) error {
- fmt.Printf("stopping %s \n", id)
- return nil
-}
-
-func (s *DummyAdapter) Name() string {
- return "dummy"
-}
diff --git a/pkg/logs/adapter/interface.go b/pkg/logs/adapter/interface.go
index 2081d3b9e78..accf5989aff 100644
--- a/pkg/logs/adapter/interface.go
+++ b/pkg/logs/adapter/interface.go
@@ -1,13 +1,19 @@
package adapter
-import "github.com/kubeshop/testkube/pkg/logs/events"
+import (
+ "context"
+
+ "github.com/kubeshop/testkube/pkg/logs/events"
+)
// Adapter will listen to log chunks, and handle them based on log id (execution Id)
type Adapter interface {
+ // Init will init for given id
+ Init(ctx context.Context, id string) error
// Notify will send data log events for particaular execution id
- Notify(id string, event events.Log) error
+ Notify(ctx context.Context, id string, event events.Log) error
// Stop will stop listening subscriber from sending data
- Stop(id string) error
+ Stop(ctx context.Context, id string) error
// Name subscriber name
Name() string
}
diff --git a/pkg/logs/adapter/minio.go b/pkg/logs/adapter/minio.go
new file mode 100644
index 00000000000..12a48708709
--- /dev/null
+++ b/pkg/logs/adapter/minio.go
@@ -0,0 +1,246 @@
+package adapter
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "sync"
+
+ "github.com/minio/minio-go/v7"
+ "go.uber.org/zap"
+
+ "github.com/kubeshop/testkube/pkg/log"
+ "github.com/kubeshop/testkube/pkg/logs/events"
+ minioconnecter "github.com/kubeshop/testkube/pkg/storage/minio"
+)
+
+const (
+ defaultBufferSize = 1024 * 100 // 100KB
+ defaultWriteSize = 1024 * 80 // 80KB
+)
+
+var _ Adapter = &MinioAdapter{}
+
+type ErrMinioAdapterDisconnected struct {
+}
+
+func (e ErrMinioAdapterDisconnected) Error() string {
+ return "minio consumer disconnected"
+}
+
+type ErrIdNotFound struct {
+ Id string
+}
+
+func (e ErrIdNotFound) Error() string {
+ return fmt.Sprintf("id %s not found", e.Id)
+}
+
+type ErrChunckTooBig struct {
+ Length int
+}
+
+func (e ErrChunckTooBig) Error() string {
+ return fmt.Sprintf("chunk too big: %d", e.Length)
+}
+
+type BufferInfo struct {
+ Buffer *bytes.Buffer
+ Part int
+}
+
+// NewMinioAdapter creates new MinioAdapter which will send data to local MinIO bucket
+func NewMinioAdapter(endpoint, accessKeyID, secretAccessKey, region, token, bucket string, ssl, skipVerify bool, certFile, keyFile, caFile string) (*MinioAdapter, error) {
+ ctx := context.TODO()
+ opts := minioconnecter.GetTLSOptions(ssl, skipVerify, certFile, keyFile, caFile)
+ c := &MinioAdapter{
+ minioConnecter: minioconnecter.NewConnecter(endpoint, accessKeyID, secretAccessKey, region, token, bucket, log.DefaultLogger, opts...),
+ Log: log.DefaultLogger,
+ bucket: bucket,
+ region: region,
+ disconnected: false,
+ buffInfos: make(map[string]BufferInfo),
+ }
+ minioClient, err := c.minioConnecter.GetClient()
+ if err != nil {
+ c.Log.Errorw("error connecting to minio", "err", err)
+ return c, err
+ }
+
+ c.minioClient = minioClient
+ exists, err := c.minioClient.BucketExists(ctx, c.bucket)
+ if err != nil {
+ c.Log.Errorw("error checking if bucket exists", "err", err)
+ return c, err
+ }
+
+ if !exists {
+ err = c.minioClient.MakeBucket(ctx, c.bucket,
+ minio.MakeBucketOptions{Region: c.region})
+ if err != nil {
+ c.Log.Errorw("error creating bucket", "err", err)
+ return c, err
+ }
+ }
+ return c, nil
+}
+
+type MinioAdapter struct {
+ minioConnecter *minioconnecter.Connecter
+ minioClient *minio.Client
+ bucket string
+ region string
+ Log *zap.SugaredLogger
+ disconnected bool
+ buffInfos map[string]BufferInfo
+ mapLock sync.RWMutex
+}
+
+func (s *MinioAdapter) Init(ctx context.Context, id string) error {
+ return nil
+}
+
+func (s *MinioAdapter) Notify(ctx context.Context, id string, e events.Log) error {
+ s.Log.Debugw("minio consumer notify", "id", id, "event", e)
+ if s.disconnected {
+ s.Log.Debugw("minio consumer disconnected", "id", id)
+ return ErrMinioAdapterDisconnected{}
+ }
+
+ buffInfo, ok := s.GetBuffInfo(id)
+ if !ok {
+ buffInfo = BufferInfo{Buffer: bytes.NewBuffer(make([]byte, 0, defaultBufferSize)), Part: 0}
+ s.UpdateBuffInfo(id, buffInfo)
+ }
+
+ chunckToAdd, err := json.Marshal(e)
+ if err != nil {
+ return err
+ }
+
+ if len(chunckToAdd) > defaultWriteSize {
+ s.Log.Warnw("chunck too big", "length", len(chunckToAdd))
+ return ErrChunckTooBig{len(chunckToAdd)}
+ }
+
+ chunckToAdd = append(chunckToAdd, []byte("\n")...)
+
+ writer := buffInfo.Buffer
+ _, err = writer.Write(chunckToAdd)
+ if err != nil {
+ return err
+ }
+
+ if writer.Len() > defaultWriteSize {
+ buffInfo.Buffer = bytes.NewBuffer(make([]byte, 0, defaultBufferSize))
+ name := id + "-" + strconv.Itoa(buffInfo.Part)
+ buffInfo.Part++
+ s.UpdateBuffInfo(id, buffInfo)
+ go s.putData(context.TODO(), name, writer)
+ }
+
+ return nil
+}
+
+func (s *MinioAdapter) putData(ctx context.Context, name string, buffer *bytes.Buffer) {
+ if buffer != nil && buffer.Len() != 0 {
+ _, err := s.minioClient.PutObject(ctx, s.bucket, name, buffer, int64(buffer.Len()), minio.PutObjectOptions{ContentType: "application/octet-stream"})
+ if err != nil {
+ s.Log.Errorw("error putting object", "err", err)
+ }
+ s.Log.Debugw("put object successfully", "name", name, "s.bucket", s.bucket)
+ } else {
+ s.Log.Warn("empty buffer for name: ", name)
+ }
+
+}
+
+func (s *MinioAdapter) combineData(ctxt context.Context, minioClient *minio.Client, id string, parts int, deleteIntermediaryData bool) error {
+ var returnedError []error
+ returnedError = nil
+ buffer := bytes.NewBuffer(make([]byte, 0, parts*defaultBufferSize))
+ for i := 0; i < parts; i++ {
+ objectName := fmt.Sprintf("%s-%d", id, i)
+ if s.objectExists(objectName) {
+ objInfo, err := minioClient.GetObject(ctxt, s.bucket, objectName, minio.GetObjectOptions{})
+ if err != nil {
+ s.Log.Errorw("error getting object", "err", err)
+ returnedError = append(returnedError, err)
+ }
+ _, err = buffer.ReadFrom(objInfo)
+ if err != nil {
+ s.Log.Errorw("error reading object", "err", err)
+ returnedError = append(returnedError, err)
+ }
+ }
+ }
+
+ info, err := minioClient.PutObject(ctxt, s.bucket, id, buffer, int64(buffer.Len()), minio.PutObjectOptions{ContentType: "application/octet-stream"})
+ if err != nil {
+ s.Log.Errorw("error putting object", "err", err)
+ return err
+ }
+ s.Log.Debugw("put object successfully", "id", id, "s.bucket", s.bucket, "parts", parts, "uploadInfo", info)
+
+ if deleteIntermediaryData {
+ for i := 0; i < parts; i++ {
+ objectName := fmt.Sprintf("%s-%d", id, i)
+ if s.objectExists(objectName) {
+ err = minioClient.RemoveObject(ctxt, s.bucket, objectName, minio.RemoveObjectOptions{})
+ if err != nil {
+ s.Log.Errorw("error removing object", "err", err)
+ returnedError = append(returnedError, err)
+ }
+ }
+ }
+ }
+
+ buffer.Reset()
+ if len(returnedError) == 0 {
+ return nil
+ }
+ return fmt.Errorf("executed with errors: %v", returnedError)
+}
+
+func (s *MinioAdapter) objectExists(objectName string) bool {
+ _, err := s.minioClient.StatObject(context.Background(), s.bucket, objectName, minio.StatObjectOptions{})
+ return err == nil
+}
+
+func (s *MinioAdapter) Stop(ctx context.Context, id string) error {
+ s.Log.Debugw("minio consumer stop", "id", id)
+ buffInfo, ok := s.GetBuffInfo(id)
+ if !ok {
+ return ErrIdNotFound{id}
+ }
+ name := id + "-" + strconv.Itoa(buffInfo.Part)
+ s.putData(ctx, name, buffInfo.Buffer)
+ parts := buffInfo.Part + 1
+ s.DeleteBuffInfo(id)
+ return s.combineData(ctx, s.minioClient, id, parts, true)
+}
+
+func (s *MinioAdapter) Name() string {
+ return "minio"
+}
+
+func (s *MinioAdapter) GetBuffInfo(id string) (BufferInfo, bool) {
+ s.mapLock.RLock()
+ defer s.mapLock.RUnlock()
+ buffInfo, ok := s.buffInfos[id]
+ return buffInfo, ok
+}
+
+func (s *MinioAdapter) UpdateBuffInfo(id string, buffInfo BufferInfo) {
+ s.mapLock.Lock()
+ defer s.mapLock.Unlock()
+ s.buffInfos[id] = buffInfo
+}
+
+func (s *MinioAdapter) DeleteBuffInfo(id string) {
+ s.mapLock.Lock()
+ defer s.mapLock.Unlock()
+ delete(s.buffInfos, id)
+}
diff --git a/pkg/logs/adapter/minio_test.go b/pkg/logs/adapter/minio_test.go
new file mode 100644
index 00000000000..eb941de82c7
--- /dev/null
+++ b/pkg/logs/adapter/minio_test.go
@@ -0,0 +1,186 @@
+package adapter
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "math/rand"
+ "strconv"
+ "sync"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/minio/minio-go/v7"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/kubeshop/testkube/pkg/logs/events"
+ "github.com/kubeshop/testkube/pkg/utils"
+)
+
+const hugeString = "82vbUcyQ0chpR665zbXY2mySOk7DGDFQCF1iLFjDNUYtNV8oQNaX3IYgJR30zBVhmDVjoZDJXO479tGSirHilZWEbzhjKJOdUwGb2HWOSOOjGh5r5wH0EHxRiOp8mBJv2rwdB2SoKF7JTBFgRt9M8F0JKp2Zx5kqh8eOGB1DGj64NLmwIpfuevJSv0wbDLrls5kEL5hHkszXPsuufVjJBsjNrxCoafuk93L2jE3ivVrMlkmLd9XAWKdop0oo0yRMJ9Vs1T5SZTkM6KXJB5hY3c14NsoPiG9Ay4EZmXrGpzGWI3RLAU6snXL8kV9sVLCG5DuRDnW047VR8eb78fpVj8YY3o9xpZd7xYPAhsmK0SwznHfrb0etAqdjQO6LFS9Blwre3G94DG5scVFH8RfteVNgKJXa8lTp8kKjtQLKNNA9mqyWfJ7uy8yjnVKwl7rodKqdtU6wjH2hf597MXA3roIS2xVhFpsCAVDybo9TVvZpoGfE9povhApoUR6Rmae9zvXPRoDbClOrvDElFkfgkJFzuoY2rPoV3dKuiTNwhYgPm36WPRk3SeFf2NiBQnWJBvjbRMIk5DsGfxcEiXQBfDvY4hgFctjwZ3USvWGriqT1cPsJ90LMLxbp38TRD1KVJ8ZgpqdvKTTi8dBqgEtob7okhdrkOahHJ3EKPtqV4PmaHvXSaIJvDG9c8jza64wxYBwMkHGt22i3HhCcIi8KmmfVo1ruqQLqKvINJg8eD5rKGV1mX9IipQcnrqADYnAj1wls7NSxsL0VZZm2pxRaGN494o2LCicHGEcOYkVLHufXY4Gv3friOIZSrT1r3NUgDBufpXWiG2b02TrRyFhgwRSS1a2OyMjHkT9tALmlIwFGF5HdaZphN6Mo5TFGdJyp65YU1scnlSGAVXzVdhsoD0RDZPSetdK2fzJC20kncaujAujHtSKnXrJNIhObnOjgMhCkx5E4z0oIH26DlfrbxS7k5SBQb1Zo3papQOk4uTNIdMBW4cE3V7AB8r6v4en3"
+
+func init() {
+ rand.New(rand.NewSource(time.Now().UnixNano()))
+}
+
+var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
+
+func RandString(n int) string {
+ b := make([]rune, n)
+ for i := range b {
+ b[i] = letterRunes[rand.Intn(len(letterRunes))]
+ }
+ return string(b)
+}
+
+func TestLogs(t *testing.T) {
+ t.Skip("skipping test")
+ ctx := context.Background()
+ consumer, _ := NewMinioAdapter("localhost:9000", "minio", "minio123", "", "", "test-1", false, false, "", "", "")
+ id := "test-bla"
+ for i := 0; i < 1000; i++ {
+ fmt.Println("sending", i)
+ consumer.Notify(ctx, id, events.Log{Time: time.Now(),
+ Content: fmt.Sprintf("Test %d: %s", i, hugeString),
+ Type_: "test", Source: strconv.Itoa(i)})
+ time.Sleep(100 * time.Millisecond)
+ }
+ err := consumer.Stop(ctx, id)
+ assert.NoError(t, err)
+}
+
+func BenchmarkLogs(b *testing.B) {
+ ctx := context.Background()
+ randomString := RandString(5)
+ bucket := "test-bench"
+ consumer, _ := NewMinioAdapter("localhost:9000", "minio", "minio123", "", "", bucket, false, false, "", "", "")
+ id := "test-bench" + "-" + randomString + "-" + strconv.Itoa(b.N)
+ totalSize := 0
+ for i := 0; i < b.N; i++ {
+ consumer.Notify(ctx, id, events.Log{Time: time.Now(),
+ Content: fmt.Sprintf("Test %d: %s", i, hugeString),
+ Type_: "test", Source: strconv.Itoa(i)})
+ totalSize += len(hugeString)
+ }
+ sizeInMB := float64(totalSize) / 1024 / 1024
+ err := consumer.Stop(ctx, id)
+ assert.NoError(b, err)
+ b.Logf("Total size for %s logs is %f MB", id, sizeInMB)
+}
+
+func BenchmarkLogs2(b *testing.B) {
+ bucket := "test-bench"
+ consumer, _ := NewMinioAdapter("localhost:9000", "minio", "minio123", "", "", bucket, false, false, "", "", "")
+ idChan := make(chan string, 100)
+ go verifyConsumer(idChan, bucket, consumer.minioClient)
+ var counter atomic.Int32
+ var wg sync.WaitGroup
+ for i := 0; i < 10; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ randomString := strconv.Itoa(int(counter.Add(1)))
+ id := "test-bench" + "-" + randomString
+ testOneConsumer(consumer, id)
+ idChan <- id
+ }()
+ }
+ wg.Wait()
+}
+
+func testOneConsumer(consumer *MinioAdapter, id string) {
+ ctx := context.Background()
+ fmt.Println("#####starting", id)
+ totalSize := 0
+ numberOFLogs := rand.Intn(100000)
+ for i := 0; i < numberOFLogs; i++ {
+ consumer.Notify(ctx, id, events.Log{Time: time.Now(),
+ Content: fmt.Sprintf("Test %d: %s", i, hugeString),
+ Type_: "test", Source: strconv.Itoa(i)})
+ totalSize += len(hugeString)
+ time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond)
+ }
+ sizeInMB := float64(totalSize) / 1024 / 1024
+ err := consumer.Stop(ctx, id)
+ if err != nil {
+ fmt.Println("#####error stopping", err)
+ }
+ fmt.Printf("#####Total size for %s logs is %f MB\n\n\n", id, sizeInMB)
+}
+
+func verifyConsumer(idChan chan string, bucket string, minioClient *minio.Client) {
+ okSlice := make([]string, 0)
+ notOkSlice := make([]string, 0)
+ for id := range idChan {
+ reader, err := minioClient.GetObject(context.Background(), bucket, id, minio.GetObjectOptions{})
+ if err != nil {
+ fmt.Println("######error getting object", err)
+ }
+ count := 0
+
+ r := bufio.NewReader(reader)
+ isOk := true
+ for {
+ line, err := utils.ReadLongLine(r)
+ if err != nil {
+ if err == io.EOF {
+ err = nil
+ }
+ break
+ }
+ var LogChunk events.Log
+ err = json.Unmarshal(line, &LogChunk)
+ if err != nil {
+ fmt.Printf("for id %s error %v unmarshalling %s\n\n\n", id, err, string(line))
+ isOk = false
+ break
+ }
+ if LogChunk.Source == "" || LogChunk.Source != strconv.Itoa(count) {
+ fmt.Printf("for id %s not equal for count %d line %s \n logChunk %+v\n\n\n", id, count, string(line), LogChunk)
+ isOk = false
+ break
+ }
+ count++
+ }
+ if isOk {
+ okSlice = append(okSlice, id)
+ } else {
+ notOkSlice = append(notOkSlice, id)
+ }
+ }
+ fmt.Println("##### number of ok", len(okSlice))
+ fmt.Println("#####verified ok", okSlice)
+ fmt.Println("##### number of not ok", len(notOkSlice))
+ fmt.Println("#####verified not ok", notOkSlice)
+}
+
+func DoRunBenchmark() {
+ numberOfConsumers := 100
+ bucket := "test-bench"
+ consumer, _ := NewMinioAdapter("testkube-minio-service-testkube:9000", "minio", "minio123", "", "", bucket, false, false, "", "", "")
+
+ idChan := make(chan string, numberOfConsumers)
+ DoRunBenchmark2(idChan, numberOfConsumers, consumer)
+ verifyConsumer(idChan, bucket, consumer.minioClient)
+}
+
+func DoRunBenchmark2(idChan chan string, numberOfConsumers int, consumer *MinioAdapter) {
+ var counter atomic.Int32
+ var wg sync.WaitGroup
+ for i := 0; i < numberOfConsumers; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ randomString := strconv.Itoa(int(counter.Add(1)))
+ id := "test-bench" + "-" + randomString
+ testOneConsumer(consumer, id)
+ idChan <- id
+ }()
+ }
+ wg.Wait()
+ close(idChan)
+ fmt.Printf("#####Done buffInfo is %+v\n\n\n", consumer.buffInfos)
+}
diff --git a/pkg/logs/adapter/s3.go b/pkg/logs/adapter/s3.go
deleted file mode 100644
index 33ba08dea19..00000000000
--- a/pkg/logs/adapter/s3.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package adapter
-
-import "github.com/kubeshop/testkube/pkg/logs/events"
-
-var _ Adapter = &S3Adapter{}
-
-// NewS3Adapter creates new S3Subscriber which will send data to local MinIO bucket
-func NewS3Adapter() *S3Adapter {
- return &S3Adapter{}
-}
-
-type S3Adapter struct {
- Bucket string
-}
-
-func (s *S3Adapter) Notify(id string, e events.Log) error {
- panic("not implemented")
-}
-
-func (s *S3Adapter) Stop(id string) error {
- panic("not implemented")
-}
-
-func (s *S3Adapter) Name() string {
- return "s3"
-}
diff --git a/pkg/logs/client/client.go b/pkg/logs/client/client.go
index a955ffd15bd..d537a28b847 100644
--- a/pkg/logs/client/client.go
+++ b/pkg/logs/client/client.go
@@ -2,11 +2,16 @@ package client
import (
"context"
+ "crypto/tls"
+ "crypto/x509"
+ "fmt"
"io"
+ "os"
"time"
"go.uber.org/zap"
"google.golang.org/grpc"
+ "google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"github.com/kubeshop/testkube/pkg/log"
@@ -15,35 +20,46 @@ import (
)
const (
- buffer = 100
+ buffer = 100
+ requestDeadline = time.Minute * 5
)
-func NewGrpcClient(address string) Client {
+// NewGrpcClient imlpements getter interface for log stream for given ID
+func NewGrpcClient(address string, creds credentials.TransportCredentials) StreamGetter {
return &GrpcClient{
log: log.DefaultLogger.With("service", "logs-grpc-client"),
+ creds: creds,
address: address,
}
}
type GrpcClient struct {
log *zap.SugaredLogger
+ creds credentials.TransportCredentials
address string
}
// Get returns channel with log stream chunks for given execution id connects through GRPC to log service
-func (c GrpcClient) Get(ctx context.Context, id string) chan events.LogResponse {
+func (c GrpcClient) Get(ctx context.Context, id string) (chan events.LogResponse, error) {
ch := make(chan events.LogResponse, buffer)
+
log := c.log.With("id", id)
log.Debugw("getting logs", "address", c.address)
+
go func() {
// Contact the server and print out its response.
- ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ ctx, cancel := context.WithTimeout(context.Background(), requestDeadline)
defer cancel()
defer close(ch)
// TODO add TLS to GRPC client
- conn, err := grpc.Dial(c.address, grpc.WithTransportCredentials(insecure.NewCredentials()))
+ creds := insecure.NewCredentials()
+ if c.creds != nil {
+ creds = c.creds
+ }
+
+ conn, err := grpc.Dial(c.address, grpc.WithTransportCredentials(creds))
if err != nil {
ch <- events.LogResponse{Error: err}
return
@@ -61,22 +77,83 @@ func (c GrpcClient) Get(ctx context.Context, id string) chan events.LogResponse
}
log.Debugw("client start streaming")
+ defer func() {
+ log.Debugw("client stopped streaming")
+ }()
+
for {
l, err := r.Recv()
- log.Debugw("received log chunk from client", "log", l, "error", err)
if err == io.EOF {
- log.Debugw("client stream finished", "error", err)
- break
+ log.Infow("client stream finished", "error", err)
+ return
} else if err != nil {
- log.Errorw("error receiving log response", "error", err)
ch <- events.LogResponse{Error: err}
- continue
+ log.Errorw("error receiving log response", "error", err)
+ return
}
+ logChunk := pb.MapFromPB(l)
+
+ // catch finish event
+ if events.IsFinished(&logChunk) {
+ log.Infow("received finish", "log", l)
+ return
+ }
+
+ log.Debugw("grpc client log", "log", l)
// send to the channel
- ch <- events.LogResponse{Log: pb.MapFromPB(l)}
+ ch <- events.LogResponse{Log: logChunk}
}
}()
- return ch
+ return ch, nil
+}
+
+// GrpcConnectionConfig contains GRPC connection parameters
+type GrpcConnectionConfig struct {
+ Secure bool
+ SkipVerify bool
+ CertFile string
+ KeyFile string
+ CAFile string
+}
+
+// GetGrpcTransportCredentials returns transport credentials for GRPC connection config
+func GetGrpcTransportCredentials(cfg GrpcConnectionConfig) (credentials.TransportCredentials, error) {
+ var creds credentials.TransportCredentials
+
+ if cfg.Secure {
+ var tlsConfig tls.Config
+
+ if cfg.SkipVerify {
+ tlsConfig.InsecureSkipVerify = true
+ } else {
+ if cfg.CertFile != "" && cfg.KeyFile != "" {
+ cert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile)
+ if err != nil {
+ return nil, err
+ }
+
+ tlsConfig.Certificates = []tls.Certificate{cert}
+ }
+
+ if cfg.CAFile != "" {
+ caCertificate, err := os.ReadFile(cfg.CAFile)
+ if err != nil {
+ return nil, err
+ }
+
+ certPool := x509.NewCertPool()
+ if !certPool.AppendCertsFromPEM(caCertificate) {
+ return nil, fmt.Errorf("failed to add server CA's certificate")
+ }
+
+ tlsConfig.RootCAs = certPool
+ }
+ }
+
+ creds = credentials.NewTLS(&tlsConfig)
+ }
+
+ return creds, nil
}
diff --git a/pkg/logs/client/interface.go b/pkg/logs/client/interface.go
index 6f85f45fb68..d942682f6c7 100644
--- a/pkg/logs/client/interface.go
+++ b/pkg/logs/client/interface.go
@@ -13,15 +13,26 @@ const (
StopSubject = "events.logs.stop"
)
-type Client interface {
- Get(ctx context.Context, id string) chan events.LogResponse
-}
-
+//go:generate mockgen -destination=./mock_stream.go -package=client "github.com/kubeshop/testkube/pkg/logs/client" Stream
type Stream interface {
StreamInitializer
StreamPusher
StreamTrigger
StreamGetter
+ StreamFinisher
+ StreamNamer
+}
+
+//go:generate mockgen -destination=./mock_initializedstreampusher.go -package=client "github.com/kubeshop/testkube/pkg/logs/client" InitializedStreamPusher
+type InitializedStreamPusher interface {
+ StreamInitializer
+ StreamPusher
+}
+
+//go:generate mockgen -destination=./mock_initializedstreamgetter.go -package=client "github.com/kubeshop/testkube/pkg/logs/client" InitializedStreamGetter
+type InitializedStreamGetter interface {
+ StreamInitializer
+ StreamGetter
}
type StreamMetadata struct {
@@ -30,20 +41,33 @@ type StreamMetadata struct {
type StreamInitializer interface {
// Init creates or updates stream on demand
- Init(ctx context.Context) (meta StreamMetadata, err error)
+ Init(ctx context.Context, id string) (meta StreamMetadata, err error)
}
type StreamPusher interface {
// Push sends logs to log stream
- Push(ctx context.Context, chunk events.Log) error
+ Push(ctx context.Context, id string, log *events.Log) error
// PushBytes sends RAW bytes to log stream, developer is responsible for marshaling valid data
- PushBytes(ctx context.Context, chunk []byte) error
+ PushBytes(ctx context.Context, id string, bytes []byte) error
+}
+
+type StreamFinisher interface {
+ // Finish sends termination log message
+ Finish(ctx context.Context, id string) error
+}
+
+//go:generate mockgen -destination=./mock_namer.go -package=client "github.com/kubeshop/testkube/pkg/logs/client" StreamNamer
+type StreamNamer interface {
+ // Name returns stream name based on possible name groups
+ Name(parts ...string) string
}
-// LogStream is a single log stream chunk with possible errors
+// StreamGetter interface for getting logs stream channel
+//
+//go:generate mockgen -destination=./mock_streamgetter.go -package=client "github.com/kubeshop/testkube/pkg/logs/client" StreamGetter
type StreamGetter interface {
// Init creates or updates stream on demand
- Get(ctx context.Context) (chan events.LogResponse, error)
+ Get(ctx context.Context, id string) (chan events.LogResponse, error)
}
type StreamConfigurer interface {
@@ -63,7 +87,7 @@ type StreamResponse struct {
type StreamTrigger interface {
// Trigger start event
- Start(ctx context.Context) (StreamResponse, error)
+ Start(ctx context.Context, id string) (StreamResponse, error)
// Trigger stop event
- Stop(ctx context.Context) (StreamResponse, error)
+ Stop(ctx context.Context, id string) (StreamResponse, error)
}
diff --git a/pkg/logs/client/mock_client.go b/pkg/logs/client/mock_client.go
new file mode 100644
index 00000000000..a87031ea1af
--- /dev/null
+++ b/pkg/logs/client/mock_client.go
@@ -0,0 +1,50 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/logs/client (interfaces: Client)
+
+// Package client is a generated GoMock package.
+package client
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ events "github.com/kubeshop/testkube/pkg/logs/events"
+)
+
+// MockClient is a mock of Client interface.
+type MockClient struct {
+ ctrl *gomock.Controller
+ recorder *MockClientMockRecorder
+}
+
+// MockClientMockRecorder is the mock recorder for MockClient.
+type MockClientMockRecorder struct {
+ mock *MockClient
+}
+
+// NewMockClient creates a new mock instance.
+func NewMockClient(ctrl *gomock.Controller) *MockClient {
+ mock := &MockClient{ctrl: ctrl}
+ mock.recorder = &MockClientMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockClient) EXPECT() *MockClientMockRecorder {
+ return m.recorder
+}
+
+// Get mocks base method.
+func (m *MockClient) Get(arg0 context.Context, arg1 string) chan events.LogResponse {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Get", arg0, arg1)
+ ret0, _ := ret[0].(chan events.LogResponse)
+ return ret0
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockClientMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClient)(nil).Get), arg0, arg1)
+}
diff --git a/pkg/logs/client/mock_initializedstreamgetter.go b/pkg/logs/client/mock_initializedstreamgetter.go
new file mode 100644
index 00000000000..911f3f3f5cd
--- /dev/null
+++ b/pkg/logs/client/mock_initializedstreamgetter.go
@@ -0,0 +1,66 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/logs/client (interfaces: InitializedStreamGetter)
+
+// Package client is a generated GoMock package.
+package client
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ events "github.com/kubeshop/testkube/pkg/logs/events"
+)
+
+// MockInitializedStreamGetter is a mock of InitializedStreamGetter interface.
+type MockInitializedStreamGetter struct {
+ ctrl *gomock.Controller
+ recorder *MockInitializedStreamGetterMockRecorder
+}
+
+// MockInitializedStreamGetterMockRecorder is the mock recorder for MockInitializedStreamGetter.
+type MockInitializedStreamGetterMockRecorder struct {
+ mock *MockInitializedStreamGetter
+}
+
+// NewMockInitializedStreamGetter creates a new mock instance.
+func NewMockInitializedStreamGetter(ctrl *gomock.Controller) *MockInitializedStreamGetter {
+ mock := &MockInitializedStreamGetter{ctrl: ctrl}
+ mock.recorder = &MockInitializedStreamGetterMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockInitializedStreamGetter) EXPECT() *MockInitializedStreamGetterMockRecorder {
+ return m.recorder
+}
+
+// Get mocks base method.
+func (m *MockInitializedStreamGetter) Get(arg0 context.Context, arg1 string) (chan events.LogResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Get", arg0, arg1)
+ ret0, _ := ret[0].(chan events.LogResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockInitializedStreamGetterMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInitializedStreamGetter)(nil).Get), arg0, arg1)
+}
+
+// Init mocks base method.
+func (m *MockInitializedStreamGetter) Init(arg0 context.Context, arg1 string) (StreamMetadata, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Init", arg0, arg1)
+ ret0, _ := ret[0].(StreamMetadata)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Init indicates an expected call of Init.
+func (mr *MockInitializedStreamGetterMockRecorder) Init(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockInitializedStreamGetter)(nil).Init), arg0, arg1)
+}
diff --git a/pkg/logs/client/mock_initializedstreampusher.go b/pkg/logs/client/mock_initializedstreampusher.go
new file mode 100644
index 00000000000..8d67710a751
--- /dev/null
+++ b/pkg/logs/client/mock_initializedstreampusher.go
@@ -0,0 +1,79 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/logs/client (interfaces: InitializedStreamPusher)
+
+// Package client is a generated GoMock package.
+package client
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ events "github.com/kubeshop/testkube/pkg/logs/events"
+)
+
+// MockInitializedStreamPusher is a mock of InitializedStreamPusher interface.
+type MockInitializedStreamPusher struct {
+ ctrl *gomock.Controller
+ recorder *MockInitializedStreamPusherMockRecorder
+}
+
+// MockInitializedStreamPusherMockRecorder is the mock recorder for MockInitializedStreamPusher.
+type MockInitializedStreamPusherMockRecorder struct {
+ mock *MockInitializedStreamPusher
+}
+
+// NewMockInitializedStreamPusher creates a new mock instance.
+func NewMockInitializedStreamPusher(ctrl *gomock.Controller) *MockInitializedStreamPusher {
+ mock := &MockInitializedStreamPusher{ctrl: ctrl}
+ mock.recorder = &MockInitializedStreamPusherMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockInitializedStreamPusher) EXPECT() *MockInitializedStreamPusherMockRecorder {
+ return m.recorder
+}
+
+// Init mocks base method.
+func (m *MockInitializedStreamPusher) Init(arg0 context.Context, arg1 string) (StreamMetadata, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Init", arg0, arg1)
+ ret0, _ := ret[0].(StreamMetadata)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Init indicates an expected call of Init.
+func (mr *MockInitializedStreamPusherMockRecorder) Init(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockInitializedStreamPusher)(nil).Init), arg0, arg1)
+}
+
+// Push mocks base method.
+func (m *MockInitializedStreamPusher) Push(arg0 context.Context, arg1 string, arg2 *events.Log) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Push", arg0, arg1, arg2)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Push indicates an expected call of Push.
+func (mr *MockInitializedStreamPusherMockRecorder) Push(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Push", reflect.TypeOf((*MockInitializedStreamPusher)(nil).Push), arg0, arg1, arg2)
+}
+
+// PushBytes mocks base method.
+func (m *MockInitializedStreamPusher) PushBytes(arg0 context.Context, arg1 string, arg2 []byte) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PushBytes", arg0, arg1, arg2)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// PushBytes indicates an expected call of PushBytes.
+func (mr *MockInitializedStreamPusherMockRecorder) PushBytes(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PushBytes", reflect.TypeOf((*MockInitializedStreamPusher)(nil).PushBytes), arg0, arg1, arg2)
+}
diff --git a/pkg/logs/client/mock_namer.go b/pkg/logs/client/mock_namer.go
new file mode 100644
index 00000000000..93e588865cc
--- /dev/null
+++ b/pkg/logs/client/mock_namer.go
@@ -0,0 +1,52 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/logs/client (interfaces: StreamNamer)
+
+// Package client is a generated GoMock package.
+package client
+
+import (
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+)
+
+// MockStreamNamer is a mock of StreamNamer interface.
+type MockStreamNamer struct {
+ ctrl *gomock.Controller
+ recorder *MockStreamNamerMockRecorder
+}
+
+// MockStreamNamerMockRecorder is the mock recorder for MockStreamNamer.
+type MockStreamNamerMockRecorder struct {
+ mock *MockStreamNamer
+}
+
+// NewMockStreamNamer creates a new mock instance.
+func NewMockStreamNamer(ctrl *gomock.Controller) *MockStreamNamer {
+ mock := &MockStreamNamer{ctrl: ctrl}
+ mock.recorder = &MockStreamNamerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockStreamNamer) EXPECT() *MockStreamNamerMockRecorder {
+ return m.recorder
+}
+
+// Name mocks base method.
+func (m *MockStreamNamer) Name(arg0 ...string) string {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Name", varargs...)
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// Name indicates an expected call of Name.
+func (mr *MockStreamNamerMockRecorder) Name(arg0 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockStreamNamer)(nil).Name), arg0...)
+}
diff --git a/pkg/logs/client/mock_stream.go b/pkg/logs/client/mock_stream.go
new file mode 100644
index 00000000000..012fd03b3c5
--- /dev/null
+++ b/pkg/logs/client/mock_stream.go
@@ -0,0 +1,156 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/logs/client (interfaces: Stream)
+
+// Package client is a generated GoMock package.
+package client
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ events "github.com/kubeshop/testkube/pkg/logs/events"
+)
+
+// MockStream is a mock of Stream interface.
+type MockStream struct {
+ ctrl *gomock.Controller
+ recorder *MockStreamMockRecorder
+}
+
+// MockStreamMockRecorder is the mock recorder for MockStream.
+type MockStreamMockRecorder struct {
+ mock *MockStream
+}
+
+// NewMockStream creates a new mock instance.
+func NewMockStream(ctrl *gomock.Controller) *MockStream {
+ mock := &MockStream{ctrl: ctrl}
+ mock.recorder = &MockStreamMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockStream) EXPECT() *MockStreamMockRecorder {
+ return m.recorder
+}
+
+// Finish mocks base method.
+func (m *MockStream) Finish(arg0 context.Context, arg1 string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Finish", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Finish indicates an expected call of Finish.
+func (mr *MockStreamMockRecorder) Finish(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Finish", reflect.TypeOf((*MockStream)(nil).Finish), arg0, arg1)
+}
+
+// Get mocks base method.
+func (m *MockStream) Get(arg0 context.Context, arg1 string) (chan events.LogResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Get", arg0, arg1)
+ ret0, _ := ret[0].(chan events.LogResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockStreamMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStream)(nil).Get), arg0, arg1)
+}
+
+// Init mocks base method.
+func (m *MockStream) Init(arg0 context.Context, arg1 string) (StreamMetadata, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Init", arg0, arg1)
+ ret0, _ := ret[0].(StreamMetadata)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Init indicates an expected call of Init.
+func (mr *MockStreamMockRecorder) Init(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockStream)(nil).Init), arg0, arg1)
+}
+
+// Name mocks base method.
+func (m *MockStream) Name(arg0 ...string) string {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Name", varargs...)
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// Name indicates an expected call of Name.
+func (mr *MockStreamMockRecorder) Name(arg0 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockStream)(nil).Name), arg0...)
+}
+
+// Push mocks base method.
+func (m *MockStream) Push(arg0 context.Context, arg1 string, arg2 *events.Log) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Push", arg0, arg1, arg2)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Push indicates an expected call of Push.
+func (mr *MockStreamMockRecorder) Push(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Push", reflect.TypeOf((*MockStream)(nil).Push), arg0, arg1, arg2)
+}
+
+// PushBytes mocks base method.
+func (m *MockStream) PushBytes(arg0 context.Context, arg1 string, arg2 []byte) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PushBytes", arg0, arg1, arg2)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// PushBytes indicates an expected call of PushBytes.
+func (mr *MockStreamMockRecorder) PushBytes(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PushBytes", reflect.TypeOf((*MockStream)(nil).PushBytes), arg0, arg1, arg2)
+}
+
+// Start mocks base method.
+func (m *MockStream) Start(arg0 context.Context, arg1 string) (StreamResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Start", arg0, arg1)
+ ret0, _ := ret[0].(StreamResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Start indicates an expected call of Start.
+func (mr *MockStreamMockRecorder) Start(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockStream)(nil).Start), arg0, arg1)
+}
+
+// Stop mocks base method.
+func (m *MockStream) Stop(arg0 context.Context, arg1 string) (StreamResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Stop", arg0, arg1)
+ ret0, _ := ret[0].(StreamResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Stop indicates an expected call of Stop.
+func (mr *MockStreamMockRecorder) Stop(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockStream)(nil).Stop), arg0, arg1)
+}
diff --git a/pkg/logs/client/mock_streamgetter.go b/pkg/logs/client/mock_streamgetter.go
new file mode 100644
index 00000000000..c319a0ebeb6
--- /dev/null
+++ b/pkg/logs/client/mock_streamgetter.go
@@ -0,0 +1,51 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/logs/client (interfaces: StreamGetter)
+
+// Package client is a generated GoMock package.
+package client
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ events "github.com/kubeshop/testkube/pkg/logs/events"
+)
+
+// MockStreamGetter is a mock of StreamGetter interface.
+type MockStreamGetter struct {
+ ctrl *gomock.Controller
+ recorder *MockStreamGetterMockRecorder
+}
+
+// MockStreamGetterMockRecorder is the mock recorder for MockStreamGetter.
+type MockStreamGetterMockRecorder struct {
+ mock *MockStreamGetter
+}
+
+// NewMockStreamGetter creates a new mock instance.
+func NewMockStreamGetter(ctrl *gomock.Controller) *MockStreamGetter {
+ mock := &MockStreamGetter{ctrl: ctrl}
+ mock.recorder = &MockStreamGetterMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockStreamGetter) EXPECT() *MockStreamGetterMockRecorder {
+ return m.recorder
+}
+
+// Get mocks base method.
+func (m *MockStreamGetter) Get(arg0 context.Context, arg1 string) (chan events.LogResponse, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Get", arg0, arg1)
+ ret0, _ := ret[0].(chan events.LogResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockStreamGetterMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStreamGetter)(nil).Get), arg0, arg1)
+}
diff --git a/pkg/logs/client/stream.go b/pkg/logs/client/stream.go
index 46cbd89ab74..dc0bb3c5516 100644
--- a/pkg/logs/client/stream.go
+++ b/pkg/logs/client/stream.go
@@ -15,32 +15,30 @@ import (
"github.com/kubeshop/testkube/pkg/utils"
)
-func NewNatsLogStream(nc *nats.Conn, id string) (Stream, error) {
+const ConsumerPrefix = "lc"
+
+func NewNatsLogStream(nc *nats.Conn) (s Stream, err error) {
js, err := jetstream.New(nc)
if err != nil {
- return &NatsLogStream{}, err
+ return s, err
}
return &NatsLogStream{
- nc: nc,
- js: js,
- log: log.DefaultLogger,
- id: id,
- streamName: StreamPrefix + id,
+ nc: nc,
+ js: js,
+ log: log.DefaultLogger,
}, nil
}
type NatsLogStream struct {
- nc *nats.Conn
- js jetstream.JetStream
- log *zap.SugaredLogger
- streamName string
- id string
+ nc *nats.Conn
+ js jetstream.JetStream
+ log *zap.SugaredLogger
}
-func (c NatsLogStream) Init(ctx context.Context) (StreamMetadata, error) {
+func (c NatsLogStream) Init(ctx context.Context, id string) (StreamMetadata, error) {
s, err := c.js.CreateOrUpdateStream(ctx, jetstream.StreamConfig{
- Name: c.streamName,
+ Name: c.Name(id),
Storage: jetstream.FileStorage, // durable stream
})
@@ -48,83 +46,113 @@ func (c NatsLogStream) Init(ctx context.Context) (StreamMetadata, error) {
c.log.Debugw("stream upserted", "info", s.CachedInfo())
}
- return StreamMetadata{Name: c.streamName}, err
+ return StreamMetadata{Name: c.Name(id)}, err
+
+}
+func (c NatsLogStream) Finish(ctx context.Context, id string) error {
+ return c.Push(ctx, id, events.NewFinishLog())
}
// Push log chunk to NATS stream
-func (c NatsLogStream) Push(ctx context.Context, chunk events.Log) error {
- b, err := json.Marshal(chunk)
+func (c NatsLogStream) Push(ctx context.Context, id string, log *events.Log) error {
+ b, err := json.Marshal(log)
if err != nil {
return err
}
- return c.PushBytes(ctx, b)
+ return c.PushBytes(ctx, id, b)
}
// Push log chunk to NATS stream
// TODO handle message repeat with backoff strategy on error
-func (c NatsLogStream) PushBytes(ctx context.Context, chunk []byte) error {
- _, err := c.js.Publish(ctx, c.streamName, chunk)
+func (c NatsLogStream) PushBytes(ctx context.Context, id string, bytes []byte) error {
+ _, err := c.js.Publish(ctx, c.Name(id), bytes)
return err
}
// Start emits start event to the stream - logs service will handle start and create new stream
-func (c NatsLogStream) Start(ctx context.Context) (resp StreamResponse, err error) {
- return c.syncCall(ctx, StartSubject)
+func (c NatsLogStream) Start(ctx context.Context, id string) (resp StreamResponse, err error) {
+ return c.syncCall(ctx, StartSubject, id)
}
// Stop emits stop event to the stream and waits for given stream to be stopped fully - logs service will handle stop and close stream and all subscribers
-func (c NatsLogStream) Stop(ctx context.Context) (resp StreamResponse, err error) {
- return c.syncCall(ctx, StopSubject)
+func (c NatsLogStream) Stop(ctx context.Context, id string) (resp StreamResponse, err error) {
+ return c.syncCall(ctx, StopSubject, id)
}
// Get returns channel with log stream chunks for given execution id connects through GRPC to log service
-func (c NatsLogStream) Get(ctx context.Context) (chan events.LogResponse, error) {
+func (c NatsLogStream) Get(ctx context.Context, id string) (chan events.LogResponse, error) {
ch := make(chan events.LogResponse)
- name := fmt.Sprintf("lc%s%s", c.id, utils.RandAlphanum(6))
- cons, err := c.js.CreateOrUpdateConsumer(ctx, c.streamName, jetstream.ConsumerConfig{
- Name: name,
- Durable: name,
- DeliverPolicy: jetstream.DeliverAllPolicy,
- })
+ name := fmt.Sprintf("%s%s%s", ConsumerPrefix, id, utils.RandAlphanum(6))
+ cons, err := c.js.CreateOrUpdateConsumer(
+ ctx,
+ c.Name(id),
+ jetstream.ConsumerConfig{
+ Name: name,
+ Durable: name,
+ DeliverPolicy: jetstream.DeliverAllPolicy,
+ },
+ )
if err != nil {
return ch, err
}
- log := c.log.With("id", c.id)
- cons.Consume(func(msg jetstream.Msg) {
- log.Debugw("got message", "data", string(msg.Data()))
+ log := c.log.With("id", id)
- // deliver to subscriber
- logChunk := events.Log{}
- err := json.Unmarshal(msg.Data(), &logChunk)
- if err != nil {
- if err := msg.Nak(); err != nil {
- log.Errorw("error nacking message", "error", err)
+ go func() {
+ defer close(ch)
+ for {
+ msg, err := cons.Next()
+ if err != nil {
ch <- events.LogResponse{Error: err}
return
}
- return
+
+ if finished := c.handleJetstreamMessage(log, ch, msg); finished {
+ return
+ }
}
+ }()
+
+ return ch, nil
+}
- if err := msg.Ack(); err != nil {
+func (c NatsLogStream) handleJetstreamMessage(log *zap.SugaredLogger, ch chan events.LogResponse, msg jetstream.Msg) (finish bool) {
+ // deliver to subscriber
+ logChunk := events.Log{}
+ err := json.Unmarshal(msg.Data(), &logChunk)
+ if err != nil {
+ if err := msg.Nak(); err != nil {
+ log.Errorw("error nacking message", "error", err)
ch <- events.LogResponse{Error: err}
- log.Errorw("error acking message", "error", err)
return
}
+ return
+ }
- ch <- events.LogResponse{Log: logChunk}
- })
+ if err := msg.Ack(); err != nil {
+ ch <- events.LogResponse{Error: err}
+ log.Errorw("error acking message", "error", err)
+ return
+ }
- return ch, nil
+ if events.IsFinished(&logChunk) {
+ return true
+ }
+
+ ch <- events.LogResponse{Log: logChunk}
+ return
}
// syncCall sends request to given subject and waits for response
-func (c NatsLogStream) syncCall(ctx context.Context, subject string) (resp StreamResponse, err error) {
- b, _ := json.Marshal(events.Trigger{Id: c.id})
+func (c NatsLogStream) syncCall(ctx context.Context, subject, id string) (resp StreamResponse, err error) {
+ b, err := json.Marshal(events.NewTrigger(id))
+ if err != nil {
+ return resp, err
+ }
m, err := c.nc.Request(subject, b, time.Minute)
if err != nil {
return resp, err
@@ -132,3 +160,11 @@ func (c NatsLogStream) syncCall(ctx context.Context, subject string) (resp Strea
return StreamResponse{Message: m.Data}, nil
}
+
+func (c NatsLogStream) Name(id ...string) string {
+ if len(id) > 0 {
+ return StreamPrefix + id[0]
+ }
+
+ return StreamPrefix + utils.RandAlphanum(10)
+}
diff --git a/pkg/logs/client/stream_test.go b/pkg/logs/client/stream_test.go
index e5390da6b58..75705f2ddf7 100644
--- a/pkg/logs/client/stream_test.go
+++ b/pkg/logs/client/stream_test.go
@@ -9,48 +9,126 @@ import (
"github.com/stretchr/testify/assert"
"github.com/kubeshop/testkube/pkg/event/bus"
+ "github.com/kubeshop/testkube/pkg/logs/events"
)
func TestStream_StartStop(t *testing.T) {
- ns, nc := bus.TestServerWithConnection()
- defer ns.Shutdown()
+ t.Run("start and stop events are triggered", func(t *testing.T) {
+ // given nats server with jetstream
+ ns, nc := bus.TestServerWithConnection()
+ defer ns.Shutdown()
- ctx := context.Background()
+ id := "111"
- client, err := NewNatsLogStream(nc, "111")
- assert.NoError(t, err)
+ ctx := context.Background()
- meta, err := client.Init(ctx)
- assert.NoError(t, err)
- assert.Equal(t, StreamPrefix+"111", meta.Name)
+ // and log stream
+ client, err := NewNatsLogStream(nc)
+ assert.NoError(t, err)
- err = client.PushBytes(ctx, []byte(`{"content":"hello 1"}`))
- assert.NoError(t, err)
+ // initialized
+ meta, err := client.Init(ctx, id)
+ assert.NoError(t, err)
+ assert.Equal(t, StreamPrefix+id, meta.Name)
+
+ // when data are passed
+ err = client.PushBytes(ctx, id, []byte(`{"resourceId":"hello 1"}`))
+ assert.NoError(t, err)
+
+ var startReceived, stopReceived bool
+
+ _, err = nc.Subscribe(StartSubject, func(m *nats.Msg) {
+ m.Respond([]byte("ok"))
+ startReceived = true
+ })
+ assert.NoError(t, err)
+ _, err = nc.Subscribe(StopSubject, func(m *nats.Msg) {
+ m.Respond([]byte("ok"))
+ stopReceived = true
+ })
+
+ assert.NoError(t, err)
- var startReceived, stopReceived bool
+ // and stream started
+ d, err := client.Start(ctx, id)
+ assert.NoError(t, err)
+ assert.Equal(t, "ok", string(d.Message))
- _, err = nc.Subscribe(StartSubject, func(m *nats.Msg) {
- fmt.Printf("%s\n", m.Data)
- m.Respond([]byte("ok"))
- startReceived = true
+ // and stream stopped
+ d, err = client.Stop(ctx, id)
+ assert.NoError(t, err)
+ assert.Equal(t, "ok", string(d.Message))
+
+ // then start/stop subjects should be notified
+ assert.True(t, startReceived)
+ assert.True(t, stopReceived)
})
- assert.NoError(t, err)
- _, err = nc.Subscribe(StopSubject, func(m *nats.Msg) {
- fmt.Printf("%s\n", m.Data)
- m.Respond([]byte("ok"))
- stopReceived = true
+
+ t.Run("channel is closed when log is finished", func(t *testing.T) {
+ // given nats server with jetstream
+ ns, nc := bus.TestServerWithConnection()
+ defer ns.Shutdown()
+
+ id := "222"
+
+ ctx := context.Background()
+
+ // and log stream
+ client, err := NewNatsLogStream(nc)
+ assert.NoError(t, err)
+
+ // initialized
+ meta, err := client.Init(ctx, id)
+ assert.NoError(t, err)
+ assert.Equal(t, StreamPrefix+id, meta.Name)
+
+ // when messages are sent
+ err = client.Push(ctx, id, events.NewLog("log line 1"))
+ assert.NoError(t, err)
+ err = client.Push(ctx, id, events.NewLog("log line 2"))
+ assert.NoError(t, err)
+ err = client.Push(ctx, id, events.NewLog("log line 3"))
+ assert.NoError(t, err)
+ // and stream is set as finished
+ err = client.Finish(ctx, id)
+ assert.NoError(t, err)
+
+ // and replay of messages is done
+ ch, err := client.Get(ctx, id)
+ assert.NoError(t, err)
+
+ messagesCount := 0
+
+ for l := range ch {
+ fmt.Printf("%+v\n", l)
+ messagesCount++
+ if events.IsFinished(&l.Log) {
+ break
+ }
+ }
+
+ // then
+ assert.Equal(t, 3, messagesCount)
})
+}
+func TestStream_Name(t *testing.T) {
+ client, err := NewNatsLogStream(nil)
assert.NoError(t, err)
- d, err := client.Start(ctx)
- assert.NoError(t, err)
- assert.Equal(t, "ok", string(d.Message))
+ t.Run("passed one string param", func(t *testing.T) {
+ name := client.Name("111")
+ assert.Equal(t, StreamPrefix+"111", name)
+ })
- d, err = client.Stop(ctx)
- assert.NoError(t, err)
- assert.Equal(t, "ok", string(d.Message))
+ t.Run("passed no string params generates random name", func(t *testing.T) {
+ name := client.Name()
+ assert.Len(t, name, len(StreamPrefix)+10)
+ })
+
+ t.Run("passed more string params ignore rest", func(t *testing.T) {
+ name := client.Name("111", "222", "333")
+ assert.Equal(t, StreamPrefix+"111", name)
+ })
- assert.True(t, startReceived)
- assert.True(t, stopReceived)
}
diff --git a/pkg/logs/config/logs_config.go b/pkg/logs/config/logs_config.go
index 3296297d520..fd03c823be6 100644
--- a/pkg/logs/config/logs_config.go
+++ b/pkg/logs/config/logs_config.go
@@ -1,16 +1,61 @@
package config
import (
+ "time"
+
"github.com/kelseyhightower/envconfig"
)
type Config struct {
- NatsURI string `envconfig:"NATS_URI" default:"nats://localhost:4222"`
+ Debug bool `envconfig:"DEBUG" default:"false"`
+
+ // Debug variables
+ AttachDebugAdapter bool `envconfig:"ATTACH_DEBUG_ADAPTER" default:"false"`
+ TraceMessages bool `envconfig:"TRACE_MESSAGES" default:"false"`
+
+ TestkubeProAPIKey string `envconfig:"TESTKUBE_PRO_API_KEY" default:""`
+ TestkubeProURL string `envconfig:"TESTKUBE_PRO_URL" default:""`
+ TestkubeProLogsPath string `envconfig:"TESTKUBE_PRO_LOGS_PATH" default:"/logs"`
+ TestkubeProTLSInsecure bool `envconfig:"TESTKUBE_PRO_TLS_INSECURE" default:"false"`
+ TestkubeProCertFile string `envconfig:"TESTKUBE_PRO_CERT_FILE" default:""`
+ TestkubeProKeyFile string `envconfig:"TESTKUBE_PRO_KEY_FILE" default:""`
+ TestkubeProCAFile string `envconfig:"TESTKUBE_PRO_CA_FILE" default:""`
+ TestkubeProWorkerCount int `envconfig:"TESTKUBE_PRO_WORKER_COUNT" default:"50"`
+ TestkubeProLogStreamWorkerCount int `envconfig:"TESTKUBE_PRO_LOG_STREAM_WORKER_COUNT" default:"25"`
+ TestkubeProSkipVerify bool `envconfig:"TESTKUBE_PRO_SKIP_VERIFY" default:"false"`
+
+ NatsURI string `envconfig:"NATS_URI" default:"nats://localhost:4222"`
+ NatsSecure bool `envconfig:"NATS_SECURE" default:"false"`
+ NatsSkipVerify bool `envconfig:"NATS_SKIP_VERIFY" default:"false"`
+ NatsCertFile string `envconfig:"NATS_CERT_FILE" default:""`
+ NatsKeyFile string `envconfig:"NATS_KEY_FILE" default:""`
+ NatsCAFile string `envconfig:"NATS_CA_FILE" default:""`
+ NatsConnectTimeout time.Duration `envconfig:"NATS_CONNECT_TIMEOUT" default:"5s"`
+
Namespace string `envconfig:"NAMESPACE" default:"testkube"`
ExecutionId string `envconfig:"ID" default:""`
HttpAddress string `envconfig:"HTTP_ADDRESS" default:":8080"`
GrpcAddress string `envconfig:"GRPC_ADDRESS" default:":9090"`
KVBucketName string `envconfig:"KV_BUCKET_NAME" default:"logsState"`
+
+ GrpcSecure bool `envconfig:"GRPC_SECURE" default:"false"`
+ GrpcClientAuth bool `envconfig:"GRPC_CLIENT_AUTH" default:"false"`
+ GrpcCertFile string `envconfig:"GRPC_CERT_FILE" default:""`
+ GrpcKeyFile string `envconfig:"GRPC_KEY_FILE" default:""`
+ GrpcClientCAFile string `envconfig:"GRPC_CLIENT_CA_FILE" 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:""`
}
func Get() (*Config, error) {
diff --git a/pkg/logs/events.go b/pkg/logs/events.go
index 18278bfa1ce..f79d7896e93 100644
--- a/pkg/logs/events.go
+++ b/pkg/logs/events.go
@@ -4,11 +4,14 @@ import (
"context"
"encoding/json"
"fmt"
+ "sync"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
+ "github.com/pkg/errors"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/logs/adapter"
"github.com/kubeshop/testkube/pkg/logs/events"
"github.com/kubeshop/testkube/pkg/logs/state"
@@ -20,14 +23,28 @@ const (
StreamPrefix = "log"
- StartSubject = "events.logs.start"
- StopSubject = "events.logs.stop"
-
StartQueue = "logsstart"
StopQueue = "logsstop"
+
+ LogStartSubject = "events.logs.start"
+ LogStopSubject = "events.logs.stop"
+)
+
+var (
+ StartSubjects = map[string]string{
+ "test": testkube.TestStartSubject,
+ "generic": LogStartSubject,
+ }
+
+ StopSubjects = map[string]string{
+ "test": testkube.TestStopSubject,
+ "generic": LogStopSubject,
+ }
)
type Consumer struct {
+ // Name of the consumer
+ Name string
// Context is a consumer context you can call Stop() method on it when no more messages are expected
Context jetstream.ConsumeContext
// Instance is a NATS consumer instance
@@ -36,29 +53,28 @@ type Consumer struct {
func (ls *LogsService) initConsumer(ctx context.Context, a adapter.Adapter, streamName, id string, i int) (jetstream.Consumer, error) {
name := fmt.Sprintf("lc%s%s%d", id, a.Name(), i)
+
+ err := a.Init(ctx, id)
+ if err != nil {
+ return nil, errors.Wrap(err, "can't init adapter")
+ }
+
return ls.js.CreateOrUpdateConsumer(ctx, streamName, jetstream.ConsumerConfig{
- Name: name,
- Durable: name,
+ Name: name,
+ // Durable: name,
// FilterSubject: streamName,
DeliverPolicy: jetstream.DeliverAllPolicy,
})
}
-func (ls *LogsService) createStream(ctx context.Context, event events.Trigger) (jetstream.Stream, error) {
- // create stream for incoming logs
- streamName := StreamPrefix + event.Id
- return ls.js.CreateOrUpdateStream(ctx, jetstream.StreamConfig{
- Name: streamName,
- Storage: jetstream.FileStorage, // durable stream as we can hit mem limit
- })
-}
-
// handleMessage will handle incoming message from logs stream and proxy it to given adapter
-func (ls *LogsService) handleMessage(a adapter.Adapter, event events.Trigger) func(msg jetstream.Msg) {
- log := ls.log.With("id", event.Id, "consumer", a.Name())
+func (ls *LogsService) handleMessage(ctx context.Context, a adapter.Adapter, id string) func(msg jetstream.Msg) {
+ log := ls.log.With("id", id, "adapter", a.Name())
return func(msg jetstream.Msg) {
- log.Debugw("got message", "data", string(msg.Data()))
+ if ls.traceMessages {
+ log.Debugw("got message", "data", string(msg.Data()))
+ }
// deliver to subscriber
logChunk := events.Log{}
@@ -71,7 +87,7 @@ func (ls *LogsService) handleMessage(a adapter.Adapter, event events.Trigger) fu
return
}
- err = a.Notify(event.Id, logChunk)
+ err = a.Notify(ctx, id, logChunk)
if err != nil {
if err := msg.Nak(); err != nil {
log.Errorw("error nacking message", "error", err)
@@ -87,7 +103,7 @@ func (ls *LogsService) handleMessage(a adapter.Adapter, event events.Trigger) fu
}
// handleStart will handle start event and create logs consumers, also manage state of given (execution) id
-func (ls *LogsService) handleStart(ctx context.Context) func(msg *nats.Msg) {
+func (ls *LogsService) handleStart(ctx context.Context, subject string) func(msg *nats.Msg) {
return func(msg *nats.Msg) {
event := events.Trigger{}
err := json.Unmarshal(msg.Data, &event)
@@ -95,23 +111,25 @@ func (ls *LogsService) handleStart(ctx context.Context) func(msg *nats.Msg) {
ls.log.Errorw("can't handle start event", "error", err)
return
}
- log := ls.log.With("id", event.Id, "event", "start")
+ id := event.ResourceId
+ log := ls.log.With("id", id, "event", "start")
- ls.state.Put(ctx, event.Id, state.LogStatePending)
- s, err := ls.createStream(ctx, event)
+ ls.state.Put(ctx, id, state.LogStatePending)
+
+ s, err := ls.logStream.Init(ctx, id)
if err != nil {
- ls.log.Errorw("error creating stream", "error", err, "id", event.Id)
+ ls.log.Errorw("error creating stream", "error", err, "id", id)
return
}
log.Infow("stream created", "stream", s)
- streamName := StreamPrefix + event.Id
+ streamName := StreamPrefix + id
// for each adapter create NATS consumer and consume stream from it e.g. cloud s3 or others
for i, adapter := range ls.adapters {
l := log.With("adapter", adapter.Name())
- c, err := ls.initConsumer(ctx, adapter, streamName, event.Id, i)
+ c, err := ls.initConsumer(ctx, adapter, streamName, id, i)
if err != nil {
log.Errorw("error creating consumer", "error", err)
return
@@ -119,106 +137,154 @@ func (ls *LogsService) handleStart(ctx context.Context) func(msg *nats.Msg) {
// handle message per each adapter
l.Infow("consumer created", "consumer", c.CachedInfo(), "stream", streamName)
- cons, err := c.Consume(ls.handleMessage(adapter, event))
+ cons, err := c.Consume(ls.handleMessage(ctx, adapter, id))
if err != nil {
log.Errorw("error creating consumer", "error", err, "consumer", c.CachedInfo())
continue
}
+ consumerName := id + "_" + adapter.Name() + "_" + subject
// store consumer instance so we can stop it later in StopSubject handler
- ls.consumerInstances.Store(event.Id+"_"+adapter.Name(), Consumer{
+ ls.consumerInstances.Store(consumerName, Consumer{
+ Name: consumerName,
Context: cons,
Instance: c,
})
- l.Infow("consumer started", "consumer", adapter.Name(), "id", event.Id, "stream", streamName)
+ l.Infow("consumer started", "adapter", adapter.Name(), "id", id, "stream", streamName)
}
- // reply to start event that everything was initialized correctly
- err = msg.Respond([]byte("ok"))
- if err != nil {
- log.Errorw("error responding to start event", "error", err)
- return
+ // confirm when reply is set
+ if msg.Reply != "" {
+ // reply to start event that everything was initialized correctly
+ err = msg.Respond([]byte("ok"))
+ if err != nil {
+ log.Errorw("error responding to start event", "error", err)
+ return
+ }
}
}
}
// handleStop will handle stop event and stop logs consumers, also clean consumers state
-func (ls *LogsService) handleStop(ctx context.Context) func(msg *nats.Msg) {
+func (ls *LogsService) handleStop(ctx context.Context, group string) func(msg *nats.Msg) {
return func(msg *nats.Msg) {
+ var (
+ wg sync.WaitGroup
+ stopped = 0
+ event = events.Trigger{}
+ )
+
+ ls.log.Debugw("got stop event", "data", string(msg.Data))
- event := events.Trigger{}
err := json.Unmarshal(msg.Data, &event)
if err != nil {
ls.log.Errorw("can't handle stop event", "error", err)
return
}
- l := ls.log.With("id", event.Id, "event", "stop")
+ id := event.ResourceId
+
+ l := ls.log.With("id", id, "event", "stop")
- maxTries := 10
- repeated := 0
+ if msg.Reply != "" {
+ err = msg.Respond([]byte("stop-queued"))
+ if err != nil {
+ l.Errorw("error responding to stop event", "error", err)
+ }
+ }
- toDelete := []string{}
for _, adapter := range ls.adapters {
- toDelete = append(toDelete, event.Id+"_"+adapter.Name())
- }
-
- consumerDeleteWaitInterval := 5 * time.Second
-
- for {
- loop:
- // Delete each consumer for given execution id
- for i, name := range toDelete {
- // load consumer and check if has pending messages
- c, found := ls.consumerInstances.Load(name)
- if !found {
- l.Warnw("consumer not found", "found", found, "name", name)
- toDelete = append(toDelete[:i], toDelete[i+1:]...)
- goto loop // rewrite toDelete and start again
- }
-
- consumer := c.(Consumer)
-
- info, err := consumer.Instance.Info(ctx)
- if err != nil {
- l.Errorw("error getting consumer info", "error", err, "id", event.Id)
- continue
- }
-
- // finally delete consumer
- if info.NumPending == 0 {
- consumer.Context.Stop()
- ls.consumerInstances.Delete(name)
- toDelete = append(toDelete[:i], toDelete[i+1:]...)
- l.Infow("stopping consumer", "id", name)
- goto loop // rewrite toDelete and start again
- }
+ consumerName := id + "_" + adapter.Name() + "_" + group
+
+ // locate consumer on this pod
+ c, found := ls.consumerInstances.Load(consumerName)
+ if !found {
+ l.Debugw("consumer not found on this pod", "found", found, "name", consumerName)
+ continue
}
+ l.Debugw("consumer instance found", "c", c, "found", found, "name", consumerName)
- if len(toDelete) == 0 {
- ls.state.Put(ctx, event.Id, state.LogStateFinished)
- l.Infow("all logs consumers stopped", "id", event.Id)
+ // stop consumer
+ wg.Add(1)
+ stopped++
+ consumer := c.(Consumer)
- err = msg.Respond([]byte("stopped"))
- if err != nil {
- l.Errorw("error responding to stop event", "error", err)
- return
- }
+ go ls.stopConsumer(ctx, &wg, consumer, adapter, id)
+ }
- return
- }
+ wg.Wait()
+ l.Debugw("wait completed")
- // handle max tries of cleaning executors
- repeated++
- if repeated >= maxTries {
- l.Errorw("error cleaning consumeres after max tries", "toDeleteLeft", toDelete, "tries", repeated)
- return
+ if stopped > 0 {
+ ls.state.Put(ctx, event.ResourceId, state.LogStateFinished)
+ l.Infow("execution logs consumers stopped", "id", event.ResourceId, "stopped", stopped)
+ } else {
+ l.Debugw("no consumers found on this pod to stop")
+ }
+ }
+}
+
+func (ls *LogsService) stopConsumer(ctx context.Context, wg *sync.WaitGroup, consumer Consumer, adapter adapter.Adapter, id string) {
+ defer wg.Done()
+
+ var (
+ info *jetstream.ConsumerInfo
+ err error
+ l = ls.log
+ retries = 0
+ maxRetries = 50
+ )
+
+ defer func() {
+ // send log finish message as consumer listening for logs needs to be closed
+ err = ls.logStream.Finish(ctx, id)
+ if err != nil {
+ ls.log.Errorw("log stream finish error")
+ }
+ }()
+
+ l.Debugw("stopping consumer", "name", consumer.Name)
+
+ for {
+ info, err = consumer.Instance.Info(ctx)
+ if err != nil {
+ l.Errorw("error getting consumer info", "error", err, "name", consumer.Name)
+ return
+ }
+
+ nothingToProcess := info.NumAckPending == 0 && info.NumPending == 0
+ messagesDelivered := info.Delivered.Consumer > 0 && info.Delivered.Stream > 0
+
+ l.Debugw("consumer info", "nothingToProcess", nothingToProcess, "messagesDelivered", messagesDelivered, "info", info)
+
+ // check if there was some messages processed
+ if nothingToProcess && messagesDelivered {
+ // stop nats consumer
+ consumer.Context.Stop()
+ // delete nats consumer instance from memory
+ ls.consumerInstances.Delete(consumer.Name)
+ l.Infow("stopping and removing consumer", "name", consumer.Name, "consumerSeq", info.Delivered.Consumer, "streamSeq", info.Delivered.Stream, "last", info.Delivered.Last)
+
+ // call adapter stop to handle given id
+ err := adapter.Stop(ctx, id)
+ if err != nil {
+ l.Errorw("stop error", "adapter", adapter.Name(), "error", err)
}
+ return
+ }
- time.Sleep(consumerDeleteWaitInterval)
+ // retry if there is no messages processed as there could be slower logs
+ retries++
+ if retries >= maxRetries {
+ l.Errorw("error stopping consumer", "error", err, "name", consumer.Name, "consumerSeq", info.Delivered.Consumer, "streamSeq", info.Delivered.Stream, "last", info.Delivered.Last)
+ return
}
+
+ // pause a little bit
+ l.Debugw("waiting for consumer to finish", "name", consumer.Name, "retries", retries, "consumerSeq", info.Delivered.Consumer, "streamSeq", info.Delivered.Stream, "last", info.Delivered.Last)
+ time.Sleep(ls.stopPauseInterval)
}
}
diff --git a/pkg/logs/events/events.go b/pkg/logs/events/events.go
index f2548ba369c..eae4dfc453f 100644
--- a/pkg/logs/events/events.go
+++ b/pkg/logs/events/events.go
@@ -2,6 +2,7 @@ package events
import (
"bytes"
+ "encoding/json"
"regexp"
"time"
@@ -9,13 +10,6 @@ import (
"github.com/kubeshop/testkube/pkg/executor/output"
)
-// Generic event like log-start log-end
-type Trigger struct {
- Id string `json:"id,omitempty"`
- Type string `json:"type,omitempty"`
- Metadata map[string]string `json:"metadata,omitempty"`
-}
-
type LogVersion string
const (
@@ -23,61 +17,124 @@ const (
LogVersionV1 LogVersion = "v1"
// v2 - raw binary format, timestamps are based on Kubernetes logs, line is raw log line
LogVersionV2 LogVersion = "v2"
+
+ SourceJobPod = "job-pod"
+ SourceScheduler = "test-scheduler"
+ SourceContainerExecutor = "container-executor"
+ SourceJobExecutor = "job-executor"
)
+// check if trigger implements model generic event type
+var _ testkube.Trigger = Trigger{}
+
+// NewTrigger returns Trigger instance
+func NewTrigger(id string) Trigger {
+ return Trigger{ResourceId: id}
+}
+
+// Generic event like log-start log-end with resource id
+type Trigger struct {
+ ResourceId string `json:"resourceId,omitempty"`
+}
+
+// GetResourceId implements testkube.Trigger interface
+func (t Trigger) GetResourceId() string {
+ return t.ResourceId
+}
+
type LogResponse struct {
Log Log
Error error
}
-type Log struct {
- Time time.Time `json:"ts,omitempty"`
- Content string `json:"content,omitempty"`
- Type string `json:"type,omitempty"`
- Source string `json:"source,omitempty"`
- Metadata map[string]string `json:"metadata,omitempty"`
- Error bool `json:"error,omitempty"`
- Version LogVersion `json:"version,omitempty"`
+type Log testkube.LogV2
- // Old output - for backwards compatibility - will be removed
- V1 *LogOutputV1 `json:"v1,omitempty"`
+func NewFinishLog() *Log {
+ return &Log{
+ Time: time.Now(),
+ Content: "processing logs finished",
+ Type_: "finish",
+ Source: "log-server",
+ }
}
-type LogOutputV1 struct {
- Result *testkube.ExecutionResult
+
+func IsFinished(log *Log) bool {
+ return log.Type_ == "finish"
}
-func NewLogResponse(ts time.Time, content []byte) Log {
- return Log{
- Time: ts,
- Content: string(content),
+func NewErrorLog(err error) *Log {
+ var msg string
+ if err != nil {
+ msg = err.Error()
+ }
+ return &Log{
+ Time: time.Now(),
+ Error_: true,
+ Content: msg,
+ }
+}
+
+func NewLog(content ...string) *Log {
+ log := &Log{
+ Time: time.Now(),
Metadata: map[string]string{},
}
+
+ if len(content) > 0 {
+ log.WithContent(content[0])
+ }
+
+ return log
+}
+
+func (l *Log) WithContent(s string) *Log {
+ l.Content = s
+ return l
+}
+
+func (l *Log) WithError(err error) *Log {
+ l.Error_ = true
+
+ if err != nil {
+ l.Content = err.Error()
+ }
+
+ return l
}
-// log line/chunk data
-func (c *Log) WithMetadataEntry(key, value string) *Log {
- if c.Metadata == nil {
- c.Metadata = map[string]string{}
+func (l *Log) WithMetadataEntry(key, value string) *Log {
+ if l.Metadata == nil {
+ l.Metadata = map[string]string{}
}
- c.Metadata[key] = value
- return c
+ l.Metadata[key] = value
+ return l
}
-func (c *Log) WithVersion(version LogVersion) *Log {
- c.Version = version
- return c
+func (l *Log) WithType(t string) *Log {
+ l.Type_ = t
+ return l
}
-func (c *Log) WithV1Result(result *testkube.ExecutionResult) *Log {
- c.V1.Result = result
- return c
+func (l *Log) WithSource(s string) *Log {
+ l.Source = s
+ return l
+}
+
+func (l *Log) WithVersion(version LogVersion) *Log {
+ l.Version = string(version)
+ return l
+}
+
+func (l *Log) WithV1Result(result *testkube.ExecutionResult) *Log {
+ l.V1.Result = result
+ return l
}
var timestampRegexp = regexp.MustCompile("^[0-9]{4}-[0-9]{2}-[0-9]{2}T.*")
-// NewLogResponseFromBytes creates new LogResponse from bytes it's aware of new and old log formats
+// NewLogFromBytes creates new LogResponse from bytes it's aware of new and old log formats
// default log format will be based on raw bytes with timestamp on the beginning
-func NewLogResponseFromBytes(b []byte) Log {
+func NewLogFromBytes(b []byte) *Log {
// detect timestamp - new logs have timestamp
var (
@@ -119,40 +176,96 @@ func NewLogResponseFromBytes(b []byte) Log {
if err != nil {
// try to read in case of some lines which we couldn't parse
// sometimes we're not able to control all stdout messages from libs
- return Log{
- Time: ts,
- Content: err.Error(),
- Type: o.Type_,
- Error: true,
- Version: LogVersionV1,
- }
+ return newErrorLog(err, content)
}
// pass parsed results for v1
// for new executor it'll be omitted in logs (as looks like we're not using it already)
if o.Type_ == output.TypeResult {
- return Log{
+ return &Log{
Time: ts,
Content: o.Content,
- Version: LogVersionV1,
- V1: &LogOutputV1{
+ Version: string(LogVersionV1),
+ V1: &testkube.LogV1{
Result: o.Result,
},
}
}
- return Log{
+ return &Log{
Time: ts,
Content: o.Content,
- Version: LogVersionV1,
+ Version: string(LogVersionV1),
}
}
// END DEPRECATED
// new non-JSON format (just raw lines will be logged)
- return Log{
+ return &Log{
Time: ts,
+ Content: string(content),
+ Version: string(LogVersionV2),
+ }
+}
+
+// ReadLogLine tries to read possible log lines from any source
+// - logv2 - JSON
+// - logv1 - old log format JSON - DEPRECATED
+// - possible errors or raw log lines
+func ReadLogLine(b []byte) *Log {
+ logsV1Prefix := []byte("{\"id\"")
+ logsV2Prefix := []byte("{")
+
+ switch true {
+ case bytes.HasPrefix(b, logsV1Prefix):
+ o, err := output.GetLogEntry(b)
+ if err != nil {
+ return newErrorLog(err, b)
+ }
+ return mapLogV1toV2(o)
+
+ case bytes.HasPrefix(b, logsV2Prefix):
+ var o Log
+ err := json.Unmarshal(b, &o)
+ if err != nil {
+ return newErrorLog(err, b)
+ }
+ return &o
+ }
+
+ return &Log{
Content: string(b),
- Version: LogVersionV2,
}
}
+
+func newErrorLog(err error, b []byte) *Log {
+ return &Log{
+ Content: string(b),
+ Error_: true,
+ Version: string(LogVersionV1),
+ Metadata: map[string]string{"error": err.Error()},
+ }
+
+}
+
+func mapLogV1toV2(o output.Output) *Log {
+ // pass parsed results for v1
+ // for new executor it'll be omitted in logs (as looks like we're not using it already)
+ if o.Type_ == output.TypeResult {
+ return &Log{
+ Time: o.Time,
+ Content: o.Content,
+ Version: string(LogVersionV1),
+ V1: &testkube.LogV1{
+ Result: o.Result,
+ },
+ }
+ }
+
+ return &Log{
+ Time: o.Time,
+ Content: o.Content,
+ Version: string(LogVersionV1),
+ }
+
+}
diff --git a/pkg/logs/events/events_test.go b/pkg/logs/events/events_test.go
new file mode 100644
index 00000000000..671f16f0b41
--- /dev/null
+++ b/pkg/logs/events/events_test.go
@@ -0,0 +1,37 @@
+package events
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestNewLogFromBytes(t *testing.T) {
+ assert := require.New(t)
+
+ t.Run("log line with timestamp passed from kube api", func(t *testing.T) {
+ b := []byte("2024-03-11T10:47:41.070097107Z Line")
+
+ l := NewLogFromBytes(b)
+
+ assert.Equal("2024-03-11 10:47:41.070097107 +0000 UTC", l.Time.String())
+ assert.Equal("Line", l.Content)
+ })
+
+ t.Run("log line without timestamp", func(t *testing.T) {
+ b := []byte("Line")
+
+ l := NewLogFromBytes(b)
+
+ assert.Equal("Line", l.Content)
+ })
+
+ t.Run("old log line without timestamp", func(t *testing.T) {
+ b := []byte(`{"content":"Line"}`)
+
+ l := NewLogFromBytes(b)
+
+ assert.Equal("Line", l.Content)
+ })
+
+}
diff --git a/pkg/logs/events_test.go b/pkg/logs/events_test.go
index 288aea12788..27cd80a3d45 100644
--- a/pkg/logs/events_test.go
+++ b/pkg/logs/events_test.go
@@ -7,9 +7,12 @@ import (
"testing"
"time"
+ "github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"github.com/stretchr/testify/assert"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/event"
"github.com/kubeshop/testkube/pkg/event/bus"
"github.com/kubeshop/testkube/pkg/logs/adapter"
"github.com/kubeshop/testkube/pkg/logs/client"
@@ -17,6 +20,8 @@ import (
"github.com/kubeshop/testkube/pkg/logs/state"
)
+var waitTime = time.Second
+
func TestLogs_EventsFlow(t *testing.T) {
t.Parallel()
@@ -29,6 +34,8 @@ func TestLogs_EventsFlow(t *testing.T) {
ns, nc := bus.TestServerWithConnection()
defer ns.Shutdown()
+ id := "stop-test"
+
// and jetstream configured
js, err := jetstream.New(nc)
assert.NoError(t, err)
@@ -41,8 +48,11 @@ func TestLogs_EventsFlow(t *testing.T) {
// and logs state manager
state := state.NewState(kv)
+ logsStream, err := client.NewNatsLogStream(nc)
+ assert.NoError(t, err)
+
// and initialized log service
- log := NewLogsService(nc, js, state).
+ log := NewLogsService(nc, js, state, logsStream).
WithRandomPort()
// given example adapters
@@ -62,30 +72,34 @@ func TestLogs_EventsFlow(t *testing.T) {
<-log.Ready
// and logs stream client
- stream, err := client.NewNatsLogStream(nc, "stop-test")
+ stream, err := client.NewNatsLogStream(nc)
assert.NoError(t, err)
// and initialized log stream for given ID
- meta, err := stream.Init(ctx)
+ meta, err := stream.Init(ctx, id)
assert.NotEmpty(t, meta.Name)
assert.NoError(t, err)
// when start event triggered
- _, err = stream.Start(ctx)
+ _, err = stream.Start(ctx, id)
assert.NoError(t, err)
// and when data pushed to the log stream
- stream.Push(ctx, events.NewLogResponse(time.Now(), []byte("hello 1")))
+ stream.Push(ctx, id, events.NewLog("hello 1"))
+ stream.Push(ctx, id, events.NewLog("hello 2"))
// and stop event triggered
- _, err = stream.Stop(ctx)
+ _, err = stream.Stop(ctx, id)
assert.NoError(t, err)
+ // cooldown stop time
+ time.Sleep(waitTime)
+
// then all adapters should be gracefully stopped
assert.Equal(t, 0, log.GetConsumersStats(ctx).Count)
})
- t.Run("should react on new message and pass data to adapter", func(t *testing.T) {
+ t.Run("should start and stop on test event", func(t *testing.T) {
// given context with 1s deadline
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Minute))
defer cancel()
@@ -94,20 +108,25 @@ func TestLogs_EventsFlow(t *testing.T) {
ns, nc := bus.TestServerWithConnection()
defer ns.Shutdown()
+ id := "id1"
+
// and jetstream configured
js, err := jetstream.New(nc)
assert.NoError(t, err)
// and KV store
- kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: "state-test"})
+ kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: "start-stop-on-test"})
assert.NoError(t, err)
assert.NotNil(t, kv)
// and logs state manager
state := state.NewState(kv)
+ logsStream, err := client.NewNatsLogStream(nc)
+ assert.NoError(t, err)
+
// and initialized log service
- log := NewLogsService(nc, js, state).
+ log := NewLogsService(nc, js, state, logsStream).
WithRandomPort()
// given example adapter
@@ -117,9 +136,91 @@ func TestLogs_EventsFlow(t *testing.T) {
// with 4 adapters (the same adapter is added 4 times so it'll receive 4 times more messages)
log.AddAdapter(a)
- log.AddAdapter(a)
- log.AddAdapter(a)
- log.AddAdapter(a)
+
+ // and log service running
+ go func() {
+ log.Run(ctx)
+ }()
+
+ // and test event emitter
+ ec, err := nats.NewEncodedConn(nc, nats.JSON_ENCODER)
+ assert.NoError(t, err)
+ eventBus := bus.NewNATSBus(ec)
+ emitter := event.NewEmitter(eventBus, "test-cluster", map[string]string{})
+
+ // and stream client
+ stream, err := client.NewNatsLogStream(nc)
+ assert.NoError(t, err)
+
+ // and initialized log stream for given ID
+ meta, err := stream.Init(ctx, id)
+ assert.NotEmpty(t, meta.Name)
+ assert.NoError(t, err)
+
+ // and ready to get messages
+ <-log.Ready
+
+ // when start event triggered
+ emitter.Notify(testkube.NewEventStartTest(&testkube.Execution{Id: "id1"}))
+
+ for i := 0; i < messagesCount; i++ {
+ // and when data pushed to the log stream
+ err = stream.Push(ctx, id, events.NewLog("hello"))
+ assert.NoError(t, err)
+ }
+
+ // and wait for message to be propagated
+ emitter.Notify(testkube.NewEventEndTestFailed(&testkube.Execution{Id: "id1"}))
+
+ time.Sleep(waitTime)
+
+ assertMessagesCount(t, a, messagesCount)
+
+ })
+
+ t.Run("should react on new message and pass data to adapter", func(t *testing.T) {
+ // given context with 1s deadline
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Minute))
+ defer cancel()
+
+ // and NATS test server with connection
+ ns, nc := bus.TestServerWithConnection()
+ defer ns.Shutdown()
+
+ id := "messages-test"
+
+ // and jetstream configured
+ js, err := jetstream.New(nc)
+ assert.NoError(t, err)
+
+ // and KV store
+ kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: "state-test"})
+ assert.NoError(t, err)
+ assert.NotNil(t, kv)
+
+ // and logs state manager
+ state := state.NewState(kv)
+
+ logsStream, err := client.NewNatsLogStream(nc)
+ assert.NoError(t, err)
+
+ // and initialized log service
+ log := NewLogsService(nc, js, state, logsStream).
+ WithRandomPort()
+
+ // given example adapter
+ a1 := NewMockAdapter()
+ a2 := NewMockAdapter()
+ a3 := NewMockAdapter()
+ a4 := NewMockAdapter()
+
+ messagesCount := 1000
+
+ // with 4 adapters (the same adapter is added 4 times so it'll receive 4 times more messages)
+ log.AddAdapter(a1)
+ log.AddAdapter(a2)
+ log.AddAdapter(a3)
+ log.AddAdapter(a4)
// and log service running
go func() {
@@ -130,29 +231,36 @@ func TestLogs_EventsFlow(t *testing.T) {
<-log.Ready
// and stream client
- stream, err := client.NewNatsLogStream(nc, "messages-test")
+ stream, err := client.NewNatsLogStream(nc)
assert.NoError(t, err)
// and initialized log stream for given ID
- meta, err := stream.Init(ctx)
+ meta, err := stream.Init(ctx, id)
assert.NotEmpty(t, meta.Name)
assert.NoError(t, err)
// when start event triggered
- _, err = stream.Start(ctx)
+ _, err = stream.Start(ctx, id)
assert.NoError(t, err)
for i := 0; i < messagesCount; i++ {
// and when data pushed to the log stream
- err = stream.Push(ctx, events.NewLogResponse(time.Now(), []byte("hello")))
+ err = stream.Push(ctx, id, events.NewLog("hello"))
assert.NoError(t, err)
}
// and wait for message to be propagated
- _, err = stream.Stop(ctx)
+ _, err = stream.Stop(ctx, id)
assert.NoError(t, err)
- assertMessagesCount(t, a, 4*messagesCount)
+ // cool down
+ time.Sleep(waitTime)
+
+ // then each adapter should receive messages
+ assertMessagesCount(t, a1, messagesCount)
+ assertMessagesCount(t, a2, messagesCount)
+ assertMessagesCount(t, a3, messagesCount)
+ assertMessagesCount(t, a4, messagesCount)
})
@@ -169,6 +277,8 @@ func TestLogs_EventsFlow(t *testing.T) {
js, err := jetstream.New(nc)
assert.NoError(t, err)
+ id := "executionid1"
+
// and KV store
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: "state-test"})
assert.NoError(t, err)
@@ -177,8 +287,11 @@ func TestLogs_EventsFlow(t *testing.T) {
// and logs state manager
state := state.NewState(kv)
+ logsStream, err := client.NewNatsLogStream(nc)
+ assert.NoError(t, err)
+
// and initialized log service
- log := NewLogsService(nc, js, state).
+ log := NewLogsService(nc, js, state, logsStream).
WithRandomPort()
// given example adapters
@@ -198,25 +311,34 @@ func TestLogs_EventsFlow(t *testing.T) {
<-log.Ready
// and logs stream client
- stream, err := client.NewNatsLogStream(nc, "stop-test")
+ stream, err := client.NewNatsLogStream(nc)
assert.NoError(t, err)
// and initialized log stream for given ID
- meta, err := stream.Init(ctx)
+ meta, err := stream.Init(ctx, id)
assert.NotEmpty(t, meta.Name)
assert.NoError(t, err)
// when start event triggered
- _, err = stream.Start(ctx)
+ _, err = stream.Start(ctx, id)
assert.NoError(t, err)
// then we should have 2 consumers
stats := log.GetConsumersStats(ctx)
assert.Equal(t, 2, stats.Count)
+ stream.Push(ctx, id, events.NewLog("hello 1"))
+ stream.Push(ctx, id, events.NewLog("hello 1"))
+ stream.Push(ctx, id, events.NewLog("hello 1"))
+
// when stop event triggered
- _, err = stream.Stop(ctx)
+ r, err := stream.Stop(ctx, id)
assert.NoError(t, err)
+ assert.False(t, r.Error)
+ assert.Equal(t, "stop-queued", string(r.Message))
+
+ // there will be wait for mess
+ time.Sleep(waitTime)
// then all adapters should be gracefully stopped
assert.Equal(t, 0, log.GetConsumersStats(ctx).Count)
@@ -246,7 +368,15 @@ type MockAdapter struct {
name string
}
-func (s *MockAdapter) Notify(id string, e events.Log) error {
+func (s *MockAdapter) Init(ctx context.Context, id string) error {
+ return nil
+}
+
+func (s *MockAdapter) Notify(ctx context.Context, id string, e events.Log) error {
+ // don't count finished logs
+ if events.IsFinished(&e) {
+ return nil
+ }
s.lock.Lock()
defer s.lock.Unlock()
@@ -255,7 +385,7 @@ func (s *MockAdapter) Notify(id string, e events.Log) error {
return nil
}
-func (s *MockAdapter) Stop(id string) error {
+func (s *MockAdapter) Stop(ctx context.Context, id string) error {
fmt.Printf("stopping %s \n", id)
return nil
}
diff --git a/pkg/logs/healthcheck_test.go b/pkg/logs/healthcheck_test.go
index 8c9191aef8a..6fbd2134a6f 100644
--- a/pkg/logs/healthcheck_test.go
+++ b/pkg/logs/healthcheck_test.go
@@ -20,7 +20,7 @@ func TestLogsService_RunHealthcheckHandler(t *testing.T) {
svc := LogsService{log: log.DefaultLogger}
svc.WithRandomPort()
go svc.RunHealthCheckHandler(ctx)
- go svc.RunGRPCServer(ctx)
+ go svc.RunGRPCServer(ctx, nil)
defer svc.Shutdown(ctx)
time.Sleep(100 * time.Millisecond)
diff --git a/pkg/logs/logsserver.go b/pkg/logs/logsserver.go
index c5d2c55eae7..c22321232c4 100644
--- a/pkg/logs/logsserver.go
+++ b/pkg/logs/logsserver.go
@@ -44,8 +44,12 @@ func (s LogsServer) Logs(req *pb.LogRequest, stream pb.LogsService_LogsServer) e
s.log.Debugw("starting sending stream", "repo", repo)
// stream logs from repository through GRPC channel
- for l := range repo.Get(ctx, req.ExecutionId) {
+ ch, err := repo.Get(ctx, req.ExecutionId)
+ if err != nil {
+ return err
+ }
+ for l := range ch {
s.log.Debug("sending log chunk", "log", l)
if err := stream.Send(pb.MapResponseToPB(l)); err != nil {
return err
@@ -55,5 +59,4 @@ func (s LogsServer) Logs(req *pb.LogRequest, stream pb.LogsService_LogsServer) e
s.log.Debugw("stream finished", "id", req.ExecutionId)
return nil
-
}
diff --git a/pkg/logs/logsserver_test.go b/pkg/logs/logsserver_test.go
index 31a0a254836..de18fcec532 100644
--- a/pkg/logs/logsserver_test.go
+++ b/pkg/logs/logsserver_test.go
@@ -23,19 +23,20 @@ func TestGRPC_Server(t *testing.T) {
state := &StateMock{state: state.LogStatePending}
- ls := NewLogsService(nil, nil, state).
+ ls := NewLogsService(nil, nil, state, nil).
WithLogsRepositoryFactory(LogsFactoryMock{}).
WithRandomPort()
- go ls.RunGRPCServer(ctx)
+ go ls.RunGRPCServer(ctx, nil)
// allow server to splin up
time.Sleep(time.Millisecond * 100)
expectedCount := 0
- stream := client.NewGrpcClient(ls.grpcAddress)
- ch := stream.Get(ctx, "id1")
+ stream := client.NewGrpcClient(ls.grpcAddress, nil)
+ ch, err := stream.Get(ctx, "id1")
+ assert.NoError(t, err)
t.Log("waiting for logs")
@@ -68,12 +69,12 @@ func (l LogsFactoryMock) GetRepository(state state.LogState) (repository.LogsRep
type LogsRepositoryMock struct{}
-func (l LogsRepositoryMock) Get(ctx context.Context, id string) chan events.LogResponse {
+func (l LogsRepositoryMock) Get(ctx context.Context, id string) (chan events.LogResponse, error) {
ch := make(chan events.LogResponse, 10)
defer close(ch)
for i := 0; i < count; i++ {
- ch <- events.LogResponse{Log: events.Log{Time: time.Now(), Content: fmt.Sprintf("test %d", i), Error: false, Type: "test", Source: "test", Metadata: map[string]string{"test": "test"}}}
+ ch <- events.LogResponse{Log: events.Log{Time: time.Now(), Content: fmt.Sprintf("test %d", i), Error_: false, Type_: "test", Source: "test", Metadata: map[string]string{"test": "test"}}}
}
- return ch
+ return ch, nil
}
diff --git a/pkg/logs/pb/logs.pb.go b/pkg/logs/pb/logs.pb.go
index 9e5d019812d..9a6c90ffc02 100644
--- a/pkg/logs/pb/logs.pb.go
+++ b/pkg/logs/pb/logs.pb.go
@@ -1,8 +1,8 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
-// protoc-gen-go v1.28.1
+// protoc-gen-go v1.32.0
// protoc v3.19.4
-// source: logs.proto
+// source: pkg/logs/pb/logs.proto
package pb
@@ -21,6 +21,52 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
+type StreamResponseStatus int32
+
+const (
+ StreamResponseStatus_Completed StreamResponseStatus = 0
+ StreamResponseStatus_Failed StreamResponseStatus = 1
+)
+
+// Enum value maps for StreamResponseStatus.
+var (
+ StreamResponseStatus_name = map[int32]string{
+ 0: "Completed",
+ 1: "Failed",
+ }
+ StreamResponseStatus_value = map[string]int32{
+ "Completed": 0,
+ "Failed": 1,
+ }
+)
+
+func (x StreamResponseStatus) Enum() *StreamResponseStatus {
+ p := new(StreamResponseStatus)
+ *p = x
+ return p
+}
+
+func (x StreamResponseStatus) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (StreamResponseStatus) Descriptor() protoreflect.EnumDescriptor {
+ return file_pkg_logs_pb_logs_proto_enumTypes[0].Descriptor()
+}
+
+func (StreamResponseStatus) Type() protoreflect.EnumType {
+ return &file_pkg_logs_pb_logs_proto_enumTypes[0]
+}
+
+func (x StreamResponseStatus) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use StreamResponseStatus.Descriptor instead.
+func (StreamResponseStatus) EnumDescriptor() ([]byte, []int) {
+ return file_pkg_logs_pb_logs_proto_rawDescGZIP(), []int{0}
+}
+
type LogRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -32,7 +78,7 @@ type LogRequest struct {
func (x *LogRequest) Reset() {
*x = LogRequest{}
if protoimpl.UnsafeEnabled {
- mi := &file_logs_proto_msgTypes[0]
+ mi := &file_pkg_logs_pb_logs_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -45,7 +91,7 @@ func (x *LogRequest) String() string {
func (*LogRequest) ProtoMessage() {}
func (x *LogRequest) ProtoReflect() protoreflect.Message {
- mi := &file_logs_proto_msgTypes[0]
+ mi := &file_pkg_logs_pb_logs_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -58,7 +104,7 @@ func (x *LogRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use LogRequest.ProtoReflect.Descriptor instead.
func (*LogRequest) Descriptor() ([]byte, []int) {
- return file_logs_proto_rawDescGZIP(), []int{0}
+ return file_pkg_logs_pb_logs_proto_rawDescGZIP(), []int{0}
}
func (x *LogRequest) GetExecutionId() string {
@@ -68,7 +114,7 @@ func (x *LogRequest) GetExecutionId() string {
return ""
}
-type LogResponse struct {
+type Log struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
@@ -82,23 +128,23 @@ type LogResponse struct {
Metadata map[string]string `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
-func (x *LogResponse) Reset() {
- *x = LogResponse{}
+func (x *Log) Reset() {
+ *x = Log{}
if protoimpl.UnsafeEnabled {
- mi := &file_logs_proto_msgTypes[1]
+ mi := &file_pkg_logs_pb_logs_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
-func (x *LogResponse) String() string {
+func (x *Log) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*LogResponse) ProtoMessage() {}
+func (*Log) ProtoMessage() {}
-func (x *LogResponse) ProtoReflect() protoreflect.Message {
- mi := &file_logs_proto_msgTypes[1]
+func (x *Log) ProtoReflect() protoreflect.Message {
+ mi := &file_pkg_logs_pb_logs_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -109,134 +155,284 @@ func (x *LogResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use LogResponse.ProtoReflect.Descriptor instead.
-func (*LogResponse) Descriptor() ([]byte, []int) {
- return file_logs_proto_rawDescGZIP(), []int{1}
+// Deprecated: Use Log.ProtoReflect.Descriptor instead.
+func (*Log) Descriptor() ([]byte, []int) {
+ return file_pkg_logs_pb_logs_proto_rawDescGZIP(), []int{1}
}
-func (x *LogResponse) GetTime() *timestamppb.Timestamp {
+func (x *Log) GetTime() *timestamppb.Timestamp {
if x != nil {
return x.Time
}
return nil
}
-func (x *LogResponse) GetContent() string {
+func (x *Log) GetContent() string {
if x != nil {
return x.Content
}
return ""
}
-func (x *LogResponse) GetError() bool {
+func (x *Log) GetError() bool {
if x != nil {
return x.Error
}
return false
}
-func (x *LogResponse) GetType() string {
+func (x *Log) GetType() string {
if x != nil {
return x.Type
}
return ""
}
-func (x *LogResponse) GetSource() string {
+func (x *Log) GetSource() string {
if x != nil {
return x.Source
}
return ""
}
-func (x *LogResponse) GetVersion() string {
+func (x *Log) GetVersion() string {
if x != nil {
return x.Version
}
return ""
}
-func (x *LogResponse) GetMetadata() map[string]string {
+func (x *Log) GetMetadata() map[string]string {
if x != nil {
return x.Metadata
}
return nil
}
-var File_logs_proto protoreflect.FileDescriptor
-
-var file_logs_proto_rawDesc = []byte{
- 0x0a, 0x0a, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x6c, 0x6f,
- 0x67, 0x73, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
- 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72,
- 0x6f, 0x74, 0x6f, 0x22, 0x2f, 0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
- 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69,
- 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69,
- 0x6f, 0x6e, 0x49, 0x64, 0x22, 0xad, 0x02, 0x0a, 0x0b, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
- 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04,
- 0x74, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18,
- 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x14,
- 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x65,
- 0x72, 0x72, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72,
- 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
- 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65,
- 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6c,
- 0x6f, 0x67, 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e,
- 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d,
- 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64,
- 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18,
- 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
- 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
- 0x3a, 0x02, 0x38, 0x01, 0x32, 0x3c, 0x0a, 0x0b, 0x4c, 0x6f, 0x67, 0x73, 0x53, 0x65, 0x72, 0x76,
- 0x69, 0x63, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x10, 0x2e, 0x6c, 0x6f,
- 0x67, 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e,
- 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
- 0x30, 0x01, 0x42, 0x0d, 0x5a, 0x0b, 0x70, 0x6b, 0x67, 0x2f, 0x6c, 0x6f, 0x67, 0x73, 0x2f, 0x70,
- 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+type CloudLogRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ EnvironmentId string `protobuf:"bytes,1,opt,name=environment_id,json=environmentId,proto3" json:"environment_id,omitempty"`
+ ExecutionId string `protobuf:"bytes,2,opt,name=execution_id,json=executionId,proto3" json:"execution_id,omitempty"`
+ TestName string `protobuf:"bytes,3,opt,name=test_name,json=testName,proto3" json:"test_name,omitempty"`
+}
+
+func (x *CloudLogRequest) Reset() {
+ *x = CloudLogRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_pkg_logs_pb_logs_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CloudLogRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CloudLogRequest) ProtoMessage() {}
+
+func (x *CloudLogRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_pkg_logs_pb_logs_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CloudLogRequest.ProtoReflect.Descriptor instead.
+func (*CloudLogRequest) Descriptor() ([]byte, []int) {
+ return file_pkg_logs_pb_logs_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *CloudLogRequest) GetEnvironmentId() string {
+ if x != nil {
+ return x.EnvironmentId
+ }
+ return ""
+}
+
+func (x *CloudLogRequest) GetExecutionId() string {
+ if x != nil {
+ return x.ExecutionId
+ }
+ return ""
+}
+
+func (x *CloudLogRequest) GetTestName() string {
+ if x != nil {
+ return x.TestName
+ }
+ return ""
+}
+
+type StreamResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
+ Status StreamResponseStatus `protobuf:"varint,2,opt,name=status,proto3,enum=logs.StreamResponseStatus" json:"status,omitempty"`
+}
+
+func (x *StreamResponse) Reset() {
+ *x = StreamResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_pkg_logs_pb_logs_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *StreamResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StreamResponse) ProtoMessage() {}
+
+func (x *StreamResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_pkg_logs_pb_logs_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use StreamResponse.ProtoReflect.Descriptor instead.
+func (*StreamResponse) Descriptor() ([]byte, []int) {
+ return file_pkg_logs_pb_logs_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *StreamResponse) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
+func (x *StreamResponse) GetStatus() StreamResponseStatus {
+ if x != nil {
+ return x.Status
+ }
+ return StreamResponseStatus_Completed
+}
+
+var File_pkg_logs_pb_logs_proto protoreflect.FileDescriptor
+
+var file_pkg_logs_pb_logs_proto_rawDesc = []byte{
+ 0x0a, 0x16, 0x70, 0x6b, 0x67, 0x2f, 0x6c, 0x6f, 0x67, 0x73, 0x2f, 0x70, 0x62, 0x2f, 0x6c, 0x6f,
+ 0x67, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x1a, 0x1f,
+ 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f,
+ 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
+ 0x2f, 0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a,
+ 0x0c, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64,
+ 0x22, 0x9d, 0x02, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
+ 0x6d, 0x70, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74,
+ 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65,
+ 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28,
+ 0x08, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65,
+ 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
+ 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x33,
+ 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b,
+ 0x32, 0x17, 0x2e, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x2e, 0x4d, 0x65, 0x74, 0x61,
+ 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,
+ 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45,
+ 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
+ 0x22, 0x78, 0x0a, 0x0f, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65,
+ 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x6e, 0x76,
+ 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x78,
+ 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x0b, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1b, 0x0a,
+ 0x09, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x08, 0x74, 0x65, 0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x5e, 0x0a, 0x0e, 0x53, 0x74,
+ 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07,
+ 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d,
+ 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x53, 0x74,
+ 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x74,
+ 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2a, 0x31, 0x0a, 0x14, 0x53, 0x74,
+ 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x74,
+ 0x75, 0x73, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x10,
+ 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x01, 0x32, 0x34, 0x0a,
+ 0x0b, 0x4c, 0x6f, 0x67, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x25, 0x0a, 0x04,
+ 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x10, 0x2e, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x4c, 0x6f,
+ 0x67, 0x30, 0x01, 0x32, 0x6b, 0x0a, 0x10, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x4c, 0x6f, 0x67, 0x73,
+ 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x53, 0x74, 0x72, 0x65, 0x61,
+ 0x6d, 0x12, 0x09, 0x2e, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x1a, 0x14, 0x2e, 0x6c,
+ 0x6f, 0x67, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+ 0x73, 0x65, 0x28, 0x01, 0x12, 0x2a, 0x0a, 0x04, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x15, 0x2e, 0x6c,
+ 0x6f, 0x67, 0x73, 0x2e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x30, 0x01,
+ 0x42, 0x0d, 0x5a, 0x0b, 0x70, 0x6b, 0x67, 0x2f, 0x6c, 0x6f, 0x67, 0x73, 0x2f, 0x70, 0x62, 0x62,
+ 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
- file_logs_proto_rawDescOnce sync.Once
- file_logs_proto_rawDescData = file_logs_proto_rawDesc
+ file_pkg_logs_pb_logs_proto_rawDescOnce sync.Once
+ file_pkg_logs_pb_logs_proto_rawDescData = file_pkg_logs_pb_logs_proto_rawDesc
)
-func file_logs_proto_rawDescGZIP() []byte {
- file_logs_proto_rawDescOnce.Do(func() {
- file_logs_proto_rawDescData = protoimpl.X.CompressGZIP(file_logs_proto_rawDescData)
+func file_pkg_logs_pb_logs_proto_rawDescGZIP() []byte {
+ file_pkg_logs_pb_logs_proto_rawDescOnce.Do(func() {
+ file_pkg_logs_pb_logs_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_logs_pb_logs_proto_rawDescData)
})
- return file_logs_proto_rawDescData
-}
-
-var file_logs_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
-var file_logs_proto_goTypes = []interface{}{
- (*LogRequest)(nil), // 0: logs.LogRequest
- (*LogResponse)(nil), // 1: logs.LogResponse
- nil, // 2: logs.LogResponse.MetadataEntry
- (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp
-}
-var file_logs_proto_depIdxs = []int32{
- 3, // 0: logs.LogResponse.time:type_name -> google.protobuf.Timestamp
- 2, // 1: logs.LogResponse.metadata:type_name -> logs.LogResponse.MetadataEntry
- 0, // 2: logs.LogsService.Logs:input_type -> logs.LogRequest
- 1, // 3: logs.LogsService.Logs:output_type -> logs.LogResponse
- 3, // [3:4] is the sub-list for method output_type
- 2, // [2:3] is the sub-list for method input_type
- 2, // [2:2] is the sub-list for extension type_name
- 2, // [2:2] is the sub-list for extension extendee
- 0, // [0:2] is the sub-list for field type_name
-}
-
-func init() { file_logs_proto_init() }
-func file_logs_proto_init() {
- if File_logs_proto != nil {
+ return file_pkg_logs_pb_logs_proto_rawDescData
+}
+
+var file_pkg_logs_pb_logs_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_pkg_logs_pb_logs_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
+var file_pkg_logs_pb_logs_proto_goTypes = []interface{}{
+ (StreamResponseStatus)(0), // 0: logs.StreamResponseStatus
+ (*LogRequest)(nil), // 1: logs.LogRequest
+ (*Log)(nil), // 2: logs.Log
+ (*CloudLogRequest)(nil), // 3: logs.CloudLogRequest
+ (*StreamResponse)(nil), // 4: logs.StreamResponse
+ nil, // 5: logs.Log.MetadataEntry
+ (*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp
+}
+var file_pkg_logs_pb_logs_proto_depIdxs = []int32{
+ 6, // 0: logs.Log.time:type_name -> google.protobuf.Timestamp
+ 5, // 1: logs.Log.metadata:type_name -> logs.Log.MetadataEntry
+ 0, // 2: logs.StreamResponse.status:type_name -> logs.StreamResponseStatus
+ 1, // 3: logs.LogsService.Logs:input_type -> logs.LogRequest
+ 2, // 4: logs.CloudLogsService.Stream:input_type -> logs.Log
+ 3, // 5: logs.CloudLogsService.Logs:input_type -> logs.CloudLogRequest
+ 2, // 6: logs.LogsService.Logs:output_type -> logs.Log
+ 4, // 7: logs.CloudLogsService.Stream:output_type -> logs.StreamResponse
+ 2, // 8: logs.CloudLogsService.Logs:output_type -> logs.Log
+ 6, // [6:9] is the sub-list for method output_type
+ 3, // [3:6] is the sub-list for method input_type
+ 3, // [3:3] is the sub-list for extension type_name
+ 3, // [3:3] is the sub-list for extension extendee
+ 0, // [0:3] is the sub-list for field type_name
+}
+
+func init() { file_pkg_logs_pb_logs_proto_init() }
+func file_pkg_logs_pb_logs_proto_init() {
+ if File_pkg_logs_pb_logs_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
- file_logs_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ file_pkg_logs_pb_logs_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*LogRequest); i {
case 0:
return &v.state
@@ -248,8 +444,32 @@ func file_logs_proto_init() {
return nil
}
}
- file_logs_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*LogResponse); i {
+ file_pkg_logs_pb_logs_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Log); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_pkg_logs_pb_logs_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CloudLogRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_pkg_logs_pb_logs_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*StreamResponse); i {
case 0:
return &v.state
case 1:
@@ -265,18 +485,19 @@ func file_logs_proto_init() {
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
- RawDescriptor: file_logs_proto_rawDesc,
- NumEnums: 0,
- NumMessages: 3,
+ RawDescriptor: file_pkg_logs_pb_logs_proto_rawDesc,
+ NumEnums: 1,
+ NumMessages: 5,
NumExtensions: 0,
- NumServices: 1,
+ NumServices: 2,
},
- GoTypes: file_logs_proto_goTypes,
- DependencyIndexes: file_logs_proto_depIdxs,
- MessageInfos: file_logs_proto_msgTypes,
+ GoTypes: file_pkg_logs_pb_logs_proto_goTypes,
+ DependencyIndexes: file_pkg_logs_pb_logs_proto_depIdxs,
+ EnumInfos: file_pkg_logs_pb_logs_proto_enumTypes,
+ MessageInfos: file_pkg_logs_pb_logs_proto_msgTypes,
}.Build()
- File_logs_proto = out.File
- file_logs_proto_rawDesc = nil
- file_logs_proto_goTypes = nil
- file_logs_proto_depIdxs = nil
+ File_pkg_logs_pb_logs_proto = out.File
+ file_pkg_logs_pb_logs_proto_rawDesc = nil
+ file_pkg_logs_pb_logs_proto_goTypes = nil
+ file_pkg_logs_pb_logs_proto_depIdxs = nil
}
diff --git a/pkg/logs/pb/logs.proto b/pkg/logs/pb/logs.proto
index caf7c5602c8..168e19ebb56 100644
--- a/pkg/logs/pb/logs.proto
+++ b/pkg/logs/pb/logs.proto
@@ -7,14 +7,16 @@ option go_package = "pkg/logs/pb";
import "google/protobuf/timestamp.proto";
service LogsService {
- rpc Logs(LogRequest) returns (stream LogResponse);
+ rpc Logs(LogRequest) returns (stream Log);
}
message LogRequest {
string execution_id = 2;
}
-message LogResponse{
+
+
+message Log{
google.protobuf.Timestamp time = 1;
string content = 2;
@@ -29,3 +31,26 @@ message LogResponse{
}
+// CloudLogsService client will be used in cloud adapter in logs server
+// CloudLogsService server will be implemented on cloud side
+service CloudLogsService {
+ rpc Stream(stream Log) returns (StreamResponse);
+ rpc Logs(CloudLogRequest) returns (stream Log);
+}
+
+message CloudLogRequest {
+ string environment_id = 1;
+ string execution_id = 2;
+ string test_name = 3;
+}
+
+
+message StreamResponse {
+ string message = 1;
+ StreamResponseStatus status = 2;
+}
+
+enum StreamResponseStatus {
+ Completed = 0;
+ Failed = 1;
+}
\ No newline at end of file
diff --git a/pkg/logs/pb/logs_grpc.pb.go b/pkg/logs/pb/logs_grpc.pb.go
index 13613736fe2..13dac477e57 100644
--- a/pkg/logs/pb/logs_grpc.pb.go
+++ b/pkg/logs/pb/logs_grpc.pb.go
@@ -1,8 +1,8 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
-// - protoc-gen-go-grpc v1.2.0
+// - protoc-gen-go-grpc v1.3.0
// - protoc v3.19.4
-// source: logs.proto
+// source: pkg/logs/pb/logs.proto
package pb
@@ -18,6 +18,10 @@ import (
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
+const (
+ LogsService_Logs_FullMethodName = "/logs.LogsService/Logs"
+)
+
// LogsServiceClient is the client API for LogsService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
@@ -34,7 +38,7 @@ func NewLogsServiceClient(cc grpc.ClientConnInterface) LogsServiceClient {
}
func (c *logsServiceClient) Logs(ctx context.Context, in *LogRequest, opts ...grpc.CallOption) (LogsService_LogsClient, error) {
- stream, err := c.cc.NewStream(ctx, &LogsService_ServiceDesc.Streams[0], "/logs.LogsService/Logs", opts...)
+ stream, err := c.cc.NewStream(ctx, &LogsService_ServiceDesc.Streams[0], LogsService_Logs_FullMethodName, opts...)
if err != nil {
return nil, err
}
@@ -49,7 +53,7 @@ func (c *logsServiceClient) Logs(ctx context.Context, in *LogRequest, opts ...gr
}
type LogsService_LogsClient interface {
- Recv() (*LogResponse, error)
+ Recv() (*Log, error)
grpc.ClientStream
}
@@ -57,8 +61,8 @@ type logsServiceLogsClient struct {
grpc.ClientStream
}
-func (x *logsServiceLogsClient) Recv() (*LogResponse, error) {
- m := new(LogResponse)
+func (x *logsServiceLogsClient) Recv() (*Log, error) {
+ m := new(Log)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
@@ -102,7 +106,7 @@ func _LogsService_Logs_Handler(srv interface{}, stream grpc.ServerStream) error
}
type LogsService_LogsServer interface {
- Send(*LogResponse) error
+ Send(*Log) error
grpc.ServerStream
}
@@ -110,7 +114,7 @@ type logsServiceLogsServer struct {
grpc.ServerStream
}
-func (x *logsServiceLogsServer) Send(m *LogResponse) error {
+func (x *logsServiceLogsServer) Send(m *Log) error {
return x.ServerStream.SendMsg(m)
}
@@ -128,5 +132,193 @@ var LogsService_ServiceDesc = grpc.ServiceDesc{
ServerStreams: true,
},
},
- Metadata: "logs.proto",
+ Metadata: "pkg/logs/pb/logs.proto",
+}
+
+const (
+ CloudLogsService_Stream_FullMethodName = "/logs.CloudLogsService/Stream"
+ CloudLogsService_Logs_FullMethodName = "/logs.CloudLogsService/Logs"
+)
+
+// CloudLogsServiceClient is the client API for CloudLogsService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type CloudLogsServiceClient interface {
+ Stream(ctx context.Context, opts ...grpc.CallOption) (CloudLogsService_StreamClient, error)
+ Logs(ctx context.Context, in *CloudLogRequest, opts ...grpc.CallOption) (CloudLogsService_LogsClient, error)
+}
+
+type cloudLogsServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewCloudLogsServiceClient(cc grpc.ClientConnInterface) CloudLogsServiceClient {
+ return &cloudLogsServiceClient{cc}
+}
+
+func (c *cloudLogsServiceClient) Stream(ctx context.Context, opts ...grpc.CallOption) (CloudLogsService_StreamClient, error) {
+ stream, err := c.cc.NewStream(ctx, &CloudLogsService_ServiceDesc.Streams[0], CloudLogsService_Stream_FullMethodName, opts...)
+ if err != nil {
+ return nil, err
+ }
+ x := &cloudLogsServiceStreamClient{stream}
+ return x, nil
+}
+
+type CloudLogsService_StreamClient interface {
+ Send(*Log) error
+ CloseAndRecv() (*StreamResponse, error)
+ grpc.ClientStream
+}
+
+type cloudLogsServiceStreamClient struct {
+ grpc.ClientStream
+}
+
+func (x *cloudLogsServiceStreamClient) Send(m *Log) error {
+ return x.ClientStream.SendMsg(m)
+}
+
+func (x *cloudLogsServiceStreamClient) CloseAndRecv() (*StreamResponse, error) {
+ if err := x.ClientStream.CloseSend(); err != nil {
+ return nil, err
+ }
+ m := new(StreamResponse)
+ if err := x.ClientStream.RecvMsg(m); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+func (c *cloudLogsServiceClient) Logs(ctx context.Context, in *CloudLogRequest, opts ...grpc.CallOption) (CloudLogsService_LogsClient, error) {
+ stream, err := c.cc.NewStream(ctx, &CloudLogsService_ServiceDesc.Streams[1], CloudLogsService_Logs_FullMethodName, opts...)
+ if err != nil {
+ return nil, err
+ }
+ x := &cloudLogsServiceLogsClient{stream}
+ if err := x.ClientStream.SendMsg(in); err != nil {
+ return nil, err
+ }
+ if err := x.ClientStream.CloseSend(); err != nil {
+ return nil, err
+ }
+ return x, nil
+}
+
+type CloudLogsService_LogsClient interface {
+ Recv() (*Log, error)
+ grpc.ClientStream
+}
+
+type cloudLogsServiceLogsClient struct {
+ grpc.ClientStream
+}
+
+func (x *cloudLogsServiceLogsClient) Recv() (*Log, error) {
+ m := new(Log)
+ if err := x.ClientStream.RecvMsg(m); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+// CloudLogsServiceServer is the server API for CloudLogsService service.
+// All implementations must embed UnimplementedCloudLogsServiceServer
+// for forward compatibility
+type CloudLogsServiceServer interface {
+ Stream(CloudLogsService_StreamServer) error
+ Logs(*CloudLogRequest, CloudLogsService_LogsServer) error
+ mustEmbedUnimplementedCloudLogsServiceServer()
+}
+
+// UnimplementedCloudLogsServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedCloudLogsServiceServer struct {
+}
+
+func (UnimplementedCloudLogsServiceServer) Stream(CloudLogsService_StreamServer) error {
+ return status.Errorf(codes.Unimplemented, "method Stream not implemented")
+}
+func (UnimplementedCloudLogsServiceServer) Logs(*CloudLogRequest, CloudLogsService_LogsServer) error {
+ return status.Errorf(codes.Unimplemented, "method Logs not implemented")
+}
+func (UnimplementedCloudLogsServiceServer) mustEmbedUnimplementedCloudLogsServiceServer() {}
+
+// UnsafeCloudLogsServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to CloudLogsServiceServer will
+// result in compilation errors.
+type UnsafeCloudLogsServiceServer interface {
+ mustEmbedUnimplementedCloudLogsServiceServer()
+}
+
+func RegisterCloudLogsServiceServer(s grpc.ServiceRegistrar, srv CloudLogsServiceServer) {
+ s.RegisterService(&CloudLogsService_ServiceDesc, srv)
+}
+
+func _CloudLogsService_Stream_Handler(srv interface{}, stream grpc.ServerStream) error {
+ return srv.(CloudLogsServiceServer).Stream(&cloudLogsServiceStreamServer{stream})
+}
+
+type CloudLogsService_StreamServer interface {
+ SendAndClose(*StreamResponse) error
+ Recv() (*Log, error)
+ grpc.ServerStream
+}
+
+type cloudLogsServiceStreamServer struct {
+ grpc.ServerStream
+}
+
+func (x *cloudLogsServiceStreamServer) SendAndClose(m *StreamResponse) error {
+ return x.ServerStream.SendMsg(m)
+}
+
+func (x *cloudLogsServiceStreamServer) Recv() (*Log, error) {
+ m := new(Log)
+ if err := x.ServerStream.RecvMsg(m); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+func _CloudLogsService_Logs_Handler(srv interface{}, stream grpc.ServerStream) error {
+ m := new(CloudLogRequest)
+ if err := stream.RecvMsg(m); err != nil {
+ return err
+ }
+ return srv.(CloudLogsServiceServer).Logs(m, &cloudLogsServiceLogsServer{stream})
+}
+
+type CloudLogsService_LogsServer interface {
+ Send(*Log) error
+ grpc.ServerStream
+}
+
+type cloudLogsServiceLogsServer struct {
+ grpc.ServerStream
+}
+
+func (x *cloudLogsServiceLogsServer) Send(m *Log) error {
+ return x.ServerStream.SendMsg(m)
+}
+
+// CloudLogsService_ServiceDesc is the grpc.ServiceDesc for CloudLogsService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var CloudLogsService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "logs.CloudLogsService",
+ HandlerType: (*CloudLogsServiceServer)(nil),
+ Methods: []grpc.MethodDesc{},
+ Streams: []grpc.StreamDesc{
+ {
+ StreamName: "Stream",
+ Handler: _CloudLogsService_Stream_Handler,
+ ClientStreams: true,
+ },
+ {
+ StreamName: "Logs",
+ Handler: _CloudLogsService_Logs_Handler,
+ ServerStreams: true,
+ },
+ },
+ Metadata: "pkg/logs/pb/logs.proto",
}
diff --git a/pkg/logs/pb/mapper.go b/pkg/logs/pb/mapper.go
index bf2e87748d6..dcee292da20 100644
--- a/pkg/logs/pb/mapper.go
+++ b/pkg/logs/pb/mapper.go
@@ -6,34 +6,34 @@ import (
"github.com/kubeshop/testkube/pkg/logs/events"
)
-// TODO figure out how to pass errors better
-func MapResponseToPB(r events.LogResponse) *LogResponse {
- chunk := r.Log
- content := chunk.Content
- isError := false
+func MapResponseToPB(r events.LogResponse) *Log {
+ log := r.Log
if r.Error != nil {
- content = r.Error.Error()
- isError = true
+ log.Content = r.Error.Error()
}
- return &LogResponse{
- Time: timestamppb.New(chunk.Time),
- Content: content,
- Error: isError,
- Type: chunk.Type,
- Source: chunk.Source,
- Metadata: chunk.Metadata,
- Version: string(chunk.Version),
+ return MapToPB(log)
+}
+
+func MapToPB(r events.Log) *Log {
+ return &Log{
+ Time: timestamppb.New(r.Time),
+ Content: r.Content,
+ Error: r.Error_,
+ Type: r.Type_,
+ Source: r.Source,
+ Metadata: r.Metadata,
+ Version: r.Version,
}
}
-func MapFromPB(chunk *LogResponse) events.Log {
+func MapFromPB(log *Log) events.Log {
return events.Log{
- Time: chunk.Time.AsTime(),
- Content: chunk.Content,
- Error: chunk.Error,
- Type: chunk.Type,
- Source: chunk.Source,
- Metadata: chunk.Metadata,
- Version: events.LogVersion(chunk.Version),
+ Time: log.Time.AsTime(),
+ Content: log.Content,
+ Error_: log.Error,
+ Type_: log.Type,
+ Source: log.Source,
+ Metadata: log.Metadata,
+ Version: log.Version,
}
}
diff --git a/pkg/logs/repository/factory.go b/pkg/logs/repository/factory.go
index b6b0f2d853f..ef98edc8222 100644
--- a/pkg/logs/repository/factory.go
+++ b/pkg/logs/repository/factory.go
@@ -5,7 +5,7 @@ import (
"github.com/kubeshop/testkube/pkg/logs/client"
"github.com/kubeshop/testkube/pkg/logs/state"
- "github.com/kubeshop/testkube/pkg/storage/minio"
+ "github.com/kubeshop/testkube/pkg/storage"
)
var ErrUnknownState = errors.New("unknown state")
@@ -14,18 +14,27 @@ type Factory interface {
GetRepository(state state.LogState) (LogsRepository, error)
}
+func NewJsMinioFactory(storageClient storage.ClientBucket, bucket string, logStream client.StreamGetter) Factory {
+ return JsMinioFactory{
+ storageClient: storageClient,
+ bucket: bucket,
+ logStream: logStream,
+ }
+}
+
type JsMinioFactory struct {
- minio *minio.Client
- js client.Client
+ storageClient storage.ClientBucket
+ bucket string
+ logStream client.StreamGetter
}
func (b JsMinioFactory) GetRepository(s state.LogState) (LogsRepository, error) {
switch s {
// pending get from buffer
case state.LogStatePending:
- return NewJetstreamRepository(b.js), nil
- case state.LogStateFinished:
- return NewMinioRepository(b.minio), nil
+ return NewJetstreamRepository(b.logStream), nil
+ case state.LogStateFinished, state.LogStateUnknown:
+ return NewMinioRepository(b.storageClient, b.bucket), nil
default:
return nil, ErrUnknownState
}
diff --git a/pkg/logs/repository/interface.go b/pkg/logs/repository/interface.go
index 376b6050097..845ad7feef3 100644
--- a/pkg/logs/repository/interface.go
+++ b/pkg/logs/repository/interface.go
@@ -15,5 +15,5 @@ type RepositoryBuilder interface {
// LogsRepository is the repository primitive to get logs from
type LogsRepository interface {
- Get(ctx context.Context, id string) chan events.LogResponse
+ Get(ctx context.Context, id string) (chan events.LogResponse, error)
}
diff --git a/pkg/logs/repository/jetstream.go b/pkg/logs/repository/jetstream.go
index a538fa57c04..b11305e93c4 100644
--- a/pkg/logs/repository/jetstream.go
+++ b/pkg/logs/repository/jetstream.go
@@ -9,15 +9,15 @@ import (
var _ LogsRepository = &JetstreamLogsRepository{}
-func NewJetstreamRepository(client client.Client) LogsRepository {
+func NewJetstreamRepository(client client.StreamGetter) LogsRepository {
return JetstreamLogsRepository{c: client}
}
// Jet
type JetstreamLogsRepository struct {
- c client.Client
+ c client.StreamGetter
}
-func (r JetstreamLogsRepository) Get(ctx context.Context, id string) chan events.LogResponse {
+func (r JetstreamLogsRepository) Get(ctx context.Context, id string) (chan events.LogResponse, error) {
return r.c.Get(ctx, id)
}
diff --git a/pkg/logs/repository/minio.go b/pkg/logs/repository/minio.go
index 7adfe5c55ff..e789df3cdd6 100644
--- a/pkg/logs/repository/minio.go
+++ b/pkg/logs/repository/minio.go
@@ -1,20 +1,142 @@
package repository
import (
+ "bufio"
+ "bytes"
"context"
+ "encoding/json"
+ "errors"
+ "io"
+ "strings"
+ "time"
+ "go.uber.org/zap"
+
+ "github.com/kubeshop/testkube/pkg/log"
"github.com/kubeshop/testkube/pkg/logs/events"
- "github.com/kubeshop/testkube/pkg/storage/minio"
+ "github.com/kubeshop/testkube/pkg/repository/result"
+ "github.com/kubeshop/testkube/pkg/storage"
+ "github.com/kubeshop/testkube/pkg/utils"
+)
+
+const (
+ defaultBufferSize = 100
+ logsV1Prefix = "{\"id\""
)
-func NewMinioRepository(minio *minio.Client) LogsRepository {
- return MinioLogsRepository{}
+func NewMinioRepository(storageClient storage.ClientBucket, bucket string) LogsRepository {
+ return MinioLogsRepository{
+ storageClient: storageClient,
+ log: log.DefaultLogger,
+ bucket: bucket,
+ }
}
type MinioLogsRepository struct {
+ storageClient storage.ClientBucket
+ log *zap.SugaredLogger
+ bucket string
+}
+
+func (r MinioLogsRepository) Get(ctx context.Context, id string) (chan events.LogResponse, error) {
+ file, info, err := r.storageClient.DownloadFileFromBucket(ctx, r.bucket, "", id)
+ if err != nil {
+ r.log.Errorw("error downloading log file from bucket", "error", err)
+ return nil, err
+ }
+
+ ch := make(chan events.LogResponse, defaultBufferSize)
+
+ go func() {
+ defer close(ch)
+
+ buffer, version, err := r.readLineLogsV2(file, ch)
+ if err != nil {
+ ch <- events.LogResponse{Error: err}
+ return
+ }
+
+ if version == events.LogVersionV1 {
+ if err = r.readLineLogsV1(ch, buffer, info.LastModified); err != nil {
+ ch <- events.LogResponse{Error: err}
+ return
+ }
+ }
+ }()
+
+ return ch, nil
+}
+
+func (r MinioLogsRepository) readLineLogsV2(file io.Reader, ch chan events.LogResponse) ([]byte, events.LogVersion, error) {
+ var buffer []byte
+ reader := bufio.NewReader(file)
+ firstLine := false
+ version := events.LogVersionV2
+ for {
+ b, err := utils.ReadLongLine(reader)
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+
+ r.log.Errorw("error getting log line", "error", err)
+ return nil, "", err
+ }
+
+ if !firstLine {
+ firstLine = true
+ if strings.HasPrefix(string(b), logsV1Prefix) {
+ version = events.LogVersionV1
+ }
+ }
+
+ if version == events.LogVersionV1 {
+ buffer = append(buffer, b...)
+ }
+
+ if version == events.LogVersionV2 {
+ var log events.Log
+ err = json.Unmarshal(b, &log)
+ if err != nil {
+ r.log.Errorw("error unmarshalling log line", "error", err)
+ ch <- events.LogResponse{Error: err}
+ continue
+ }
+
+ ch <- events.LogResponse{Log: log}
+ }
+ }
+
+ return buffer, version, nil
}
-func (r MinioLogsRepository) Get(ctx context.Context, id string) chan events.LogResponse {
- ch := make(chan events.LogResponse, 100)
- return ch
+func (r MinioLogsRepository) readLineLogsV1(ch chan events.LogResponse, buffer []byte, logTime time.Time) error {
+ var output result.ExecutionOutput
+ decoder := json.NewDecoder(bytes.NewBuffer(buffer))
+ err := decoder.Decode(&output)
+ if err != nil {
+ r.log.Errorw("error decoding logs", "error", err)
+ return err
+ }
+
+ reader := bufio.NewReader(bytes.NewBuffer([]byte(output.Output)))
+ for {
+ b, err := utils.ReadLongLine(reader)
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+
+ r.log.Errorw("error getting log line", "error", err)
+ return err
+ }
+
+ ch <- events.LogResponse{Log: events.Log{
+ Time: logTime,
+ Content: string(b),
+ Version: string(events.LogVersionV1),
+ }}
+ }
+
+ return nil
}
diff --git a/pkg/logs/repository/minio_test.go b/pkg/logs/repository/minio_test.go
new file mode 100644
index 00000000000..37a8b81ba1b
--- /dev/null
+++ b/pkg/logs/repository/minio_test.go
@@ -0,0 +1,142 @@
+package repository
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/golang/mock/gomock"
+ "github.com/minio/minio-go/v7"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/kubeshop/testkube/pkg/logs/events"
+ "github.com/kubeshop/testkube/pkg/repository/result"
+ "github.com/kubeshop/testkube/pkg/storage"
+)
+
+func TestRepository_MinioGetLogV2(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ storageClient := storage.NewMockClient(mockCtrl)
+ ctx := context.TODO()
+
+ var data []byte
+
+ eventLog1 := events.Log{
+ Content: "storage logs 1",
+ Source: events.SourceJobPod,
+ Version: string(events.LogVersionV2),
+ }
+
+ b, err := json.Marshal(eventLog1)
+ assert.NoError(t, err)
+
+ data = append(data, b...)
+ data = append(data, []byte("\n")...)
+
+ eventLog2 := events.Log{
+ Content: "storage logs 2",
+ Source: events.SourceJobPod,
+ Version: string(events.LogVersionV2),
+ }
+
+ b, err = json.Marshal(eventLog2)
+ assert.NoError(t, err)
+
+ data = append(data, b...)
+ data = append(data, []byte("\n")...)
+
+ storageClient.EXPECT().DownloadFileFromBucket(gomock.Any(), "bucket", "", "test-execution-1").
+ Return(bytes.NewReader(data), minio.ObjectInfo{}, nil)
+ r := NewMinioRepository(storageClient, "bucket")
+
+ tests := []struct {
+ name string
+ eventLogs []events.Log
+ }{
+ {
+ name: "Test getting logs from minio",
+ eventLogs: []events.Log{eventLog1, eventLog2},
+ },
+ }
+
+ var res []events.Log
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ logs, err := r.Get(ctx, "test-execution-1")
+ assert.NoError(t, err)
+
+ for out := range logs {
+ res = append(res, out.Log)
+ }
+
+ assert.Equal(t, tt.eventLogs, res)
+ })
+ }
+}
+
+func TestRepository_MinioGetLogsV1(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ storageClient := storage.NewMockClient(mockCtrl)
+ ctx := context.TODO()
+
+ var data []byte
+
+ contentLog1 := "storage logs 1"
+ contentLog2 := "storage logs 2"
+ output := result.ExecutionOutput{
+ Id: "id",
+ Name: "execution-name",
+ TestName: "test-name",
+ TestSuiteName: "testsuite-name",
+ Output: contentLog1 + "\n" + contentLog2,
+ }
+
+ data, err := json.Marshal(output)
+ assert.NoError(t, err)
+
+ current := time.Now()
+ storageClient.EXPECT().DownloadFileFromBucket(gomock.Any(), "bucket", "", "test-execution-1").
+ Return(bytes.NewReader(data), minio.ObjectInfo{LastModified: current}, nil)
+ r := NewMinioRepository(storageClient, "bucket")
+
+ tests := []struct {
+ name string
+ eventLogs []events.Log
+ }{
+ {
+ name: "Test getting logs from minio",
+ eventLogs: []events.Log{
+ {
+ Time: current,
+ Content: contentLog1,
+ Version: string(events.LogVersionV1),
+ },
+ {
+ Time: current,
+ Content: contentLog2,
+ Version: string(events.LogVersionV1),
+ },
+ },
+ },
+ }
+
+ var res []events.Log
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ logs, err := r.Get(ctx, "test-execution-1")
+ assert.NoError(t, err)
+
+ for out := range logs {
+ res = append(res, out.Log)
+ }
+
+ assert.Equal(t, tt.eventLogs, res)
+ })
+ }
+}
diff --git a/pkg/logs/service.go b/pkg/logs/service.go
index 98013158067..bca2730b11a 100644
--- a/pkg/logs/service.go
+++ b/pkg/logs/service.go
@@ -6,19 +6,25 @@ package logs
import (
"context"
+ "crypto/tls"
+ "crypto/x509"
"fmt"
"math/rand"
"net"
"net/http"
+ "os"
"sync"
+ "time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"go.uber.org/zap"
"google.golang.org/grpc"
+ "google.golang.org/grpc/credentials"
"github.com/kubeshop/testkube/pkg/log"
"github.com/kubeshop/testkube/pkg/logs/adapter"
+ "github.com/kubeshop/testkube/pkg/logs/client"
"github.com/kubeshop/testkube/pkg/logs/pb"
"github.com/kubeshop/testkube/pkg/logs/repository"
"github.com/kubeshop/testkube/pkg/logs/state"
@@ -27,9 +33,11 @@ import (
const (
DefaultHttpAddress = ":8080"
DefaultGrpcAddress = ":9090"
+
+ defaultStopPauseInterval = 200 * time.Millisecond
)
-func NewLogsService(nats *nats.Conn, js jetstream.JetStream, state state.Interface) *LogsService {
+func NewLogsService(nats *nats.Conn, js jetstream.JetStream, state state.Interface, stream client.Stream) *LogsService {
return &LogsService{
nats: nats,
adapters: []adapter.Adapter{},
@@ -40,6 +48,8 @@ func NewLogsService(nats *nats.Conn, js jetstream.JetStream, state state.Interfa
grpcAddress: DefaultGrpcAddress,
consumerInstances: sync.Map{},
state: state,
+ stopPauseInterval: defaultStopPauseInterval,
+ logStream: stream,
}
}
@@ -50,6 +60,9 @@ type LogsService struct {
js jetstream.JetStream
adapters []adapter.Adapter
+ // logStream to manage and send data to logs streams
+ logStream client.Stream
+
Ready chan struct{}
// grpcAddress is address for grpc server
@@ -70,6 +83,12 @@ type LogsService struct {
// will allow to distiguish from where load data from in OSS
// cloud will be loading always them locally
state state.Interface
+
+ // stop wait time for messages cool down
+ stopPauseInterval time.Duration
+
+ // trace incoming messages
+ traceMessages bool
}
// AddAdapter adds new adapter to logs service adapters will be configred based on given mode
@@ -88,11 +107,16 @@ func (ls *LogsService) Run(ctx context.Context) (err error) {
// For start event we must build stream for given execution id and start consuming it
// this one will must follow a queue group each pod will get it's own bunch of executions to handle
// Start event will be triggered by logs process controller (scheduler)
- ls.nats.QueueSubscribe(StartSubject, StartQueue, ls.handleStart(ctx))
+ // group is common name for both start and stop subjects
+ for group, subject := range StartSubjects {
+ ls.nats.QueueSubscribe(subject, StartQueue, ls.handleStart(ctx, group))
+ }
// listen on all pods as we don't control which one will have given consumer
// Stop event will be triggered by logs process controller (scheduler)
- ls.nats.Subscribe(StopSubject, ls.handleStop(ctx))
+ for group, subject := range StopSubjects {
+ ls.nats.Subscribe(subject, ls.handleStop(ctx, group))
+ }
// Send ready signal
ls.Ready <- struct{}{}
@@ -104,13 +128,19 @@ func (ls *LogsService) Run(ctx context.Context) (err error) {
}
// TODO handle TLS
-func (ls *LogsService) RunGRPCServer(ctx context.Context) error {
+func (ls *LogsService) RunGRPCServer(ctx context.Context, creds credentials.TransportCredentials) error {
lis, err := net.Listen("tcp", ls.grpcAddress)
if err != nil {
return err
}
- ls.grpcServer = grpc.NewServer()
+ var opts []grpc.ServerOption
+ if creds != nil {
+ opts = append(opts, grpc.Creds(creds))
+ }
+
+ ls.grpcServer = grpc.NewServer(opts...)
+
pb.RegisterLogsServiceServer(ls.grpcServer, NewLogsServer(ls.logsRepositoryFactory, ls.state))
ls.log.Infow("starting grpc server", "address", ls.grpcAddress)
@@ -137,11 +167,21 @@ func (ls *LogsService) WithHttpAddress(address string) *LogsService {
return ls
}
+func (ls *LogsService) WithMessageTracing(enabled bool) *LogsService {
+ ls.traceMessages = enabled
+ return ls
+}
+
func (ls *LogsService) WithGrpcAddress(address string) *LogsService {
ls.grpcAddress = address
return ls
}
+func (ls *LogsService) WithPauseInterval(duration time.Duration) *LogsService {
+ ls.stopPauseInterval = duration
+ return ls
+}
+
func (ls *LogsService) WithRandomPort() *LogsService {
port := rand.Intn(1000) + 17000
ls.httpAddress = fmt.Sprintf("127.0.0.1:%d", port)
@@ -154,3 +194,52 @@ func (ls *LogsService) WithLogsRepositoryFactory(f repository.Factory) *LogsServ
ls.logsRepositoryFactory = f
return ls
}
+
+// GrpcConnectionConfig contains GRPC connection parameters
+type GrpcConnectionConfig struct {
+ Secure bool
+ ClientAuth bool
+ CertFile string
+ KeyFile string
+ ClientCAFile string
+}
+
+// GetGrpcTransportCredentials returns transport credentials for GRPC connection config
+func GetGrpcTransportCredentials(cfg GrpcConnectionConfig) (credentials.TransportCredentials, error) {
+ var creds credentials.TransportCredentials
+
+ if cfg.Secure {
+ var tlsConfig tls.Config
+ tlsConfig.ClientAuth = tls.NoClientCert
+ if cfg.ClientAuth {
+ tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
+ }
+
+ if cfg.CertFile != "" && cfg.KeyFile != "" {
+ cert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile)
+ if err != nil {
+ return nil, err
+ }
+
+ tlsConfig.Certificates = []tls.Certificate{cert}
+ }
+
+ if cfg.ClientCAFile != "" {
+ caCertificate, err := os.ReadFile(cfg.ClientCAFile)
+ if err != nil {
+ return nil, err
+ }
+
+ certPool := x509.NewCertPool()
+ if !certPool.AppendCertsFromPEM(caCertificate) {
+ return nil, fmt.Errorf("failed to add client CA's certificate")
+ }
+
+ tlsConfig.ClientCAs = certPool
+ }
+
+ creds = credentials.NewTLS(&tlsConfig)
+ }
+
+ return creds, nil
+}
diff --git a/pkg/logs/service_test.go b/pkg/logs/service_test.go
index 8ff1e4f1090..2ba896c6cc9 100644
--- a/pkg/logs/service_test.go
+++ b/pkg/logs/service_test.go
@@ -13,10 +13,10 @@ func TestLogsService_AddAdapter(t *testing.T) {
t.Run("should add adapter", func(t *testing.T) {
svc := LogsService{}
- svc.AddAdapter(adapter.NewDummyAdapter())
- svc.AddAdapter(adapter.NewDummyAdapter())
- svc.AddAdapter(adapter.NewDummyAdapter())
- svc.AddAdapter(adapter.NewDummyAdapter())
+ svc.AddAdapter(adapter.NewDebugAdapter())
+ svc.AddAdapter(adapter.NewDebugAdapter())
+ svc.AddAdapter(adapter.NewDebugAdapter())
+ svc.AddAdapter(adapter.NewDebugAdapter())
assert.Equal(t, 4, len(svc.adapters))
})
diff --git a/pkg/logs/sidecar/proxy.go b/pkg/logs/sidecar/proxy.go
index ad99a2388c9..5360ecdc109 100644
--- a/pkg/logs/sidecar/proxy.go
+++ b/pkg/logs/sidecar/proxy.go
@@ -32,13 +32,13 @@ var (
const (
pollInterval = time.Second
- podStartTimeout = time.Second * 60
+ podStartTimeout = 30 * time.Minute
logsBuffer = 1000
)
func NewProxy(clientset kubernetes.Interface, podsClient tcorev1.PodInterface, logsStream client.Stream, js jetstream.JetStream, log *zap.SugaredLogger, namespace, executionId string) *Proxy {
return &Proxy{
- log: log.With("namespace", namespace, "executionId", executionId),
+ log: log.With("service", "logs-proxy", "namespace", namespace, "executionId", executionId),
js: js,
clientset: clientset,
namespace: namespace,
@@ -55,7 +55,7 @@ type Proxy struct {
namespace string
executionId string
podsClient tcorev1.PodInterface
- logsStream client.Stream
+ logsStream client.InitializedStreamPusher
}
func (p *Proxy) Run(ctx context.Context) error {
@@ -63,11 +63,10 @@ func (p *Proxy) Run(ctx context.Context) error {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
- logs := make(chan events.Log, logsBuffer)
+ logs := make(chan *events.Log, logsBuffer)
// create stream for incoming logs
-
- _, err := p.logsStream.Init(ctx)
+ _, err := p.logsStream.Init(ctx, p.executionId)
if err != nil {
return err
}
@@ -76,7 +75,7 @@ func (p *Proxy) Run(ctx context.Context) error {
p.log.Debugw("logs proxy stream started")
err := p.streamLogs(ctx, logs)
if err != nil {
- p.handleError(err, "proxy stream logs error")
+ p.handleError(err, "logs proxy stream error")
}
}()
@@ -84,12 +83,13 @@ func (p *Proxy) Run(ctx context.Context) error {
select {
case <-sigs:
p.log.Warn("logs proxy received signal, exiting", "signal", sigs)
+ p.handleError(ErrStopSignalReceived, "context cancelled stopping logs proxy")
return ErrStopSignalReceived
case <-ctx.Done():
p.log.Warn("logs proxy context cancelled, exiting")
return nil
default:
- err = p.logsStream.Push(ctx, l)
+ err = p.logsStream.Push(ctx, p.executionId, l)
if err != nil {
p.handleError(err, "error pushing logs to stream")
return err
@@ -101,7 +101,7 @@ func (p *Proxy) Run(ctx context.Context) error {
return nil
}
-func (p *Proxy) streamLogs(ctx context.Context, logs chan events.Log) (err error) {
+func (p *Proxy) streamLogs(ctx context.Context, logs chan *events.Log) (err error) {
pods, err := executor.GetJobPods(ctx, p.podsClient, p.executionId, 1, 10)
if err != nil {
p.handleError(err, "error getting job pods")
@@ -139,7 +139,7 @@ func (p *Proxy) streamLogs(ctx context.Context, logs chan events.Log) (err error
return
}
-func (p *Proxy) streamLogsFromPod(pod corev1.Pod, logs chan events.Log) (err error) {
+func (p *Proxy) streamLogsFromPod(pod corev1.Pod, logs chan *events.Log) (err error) {
defer close(logs)
var containers []string
@@ -183,7 +183,8 @@ func (p *Proxy) streamLogsFromPod(pod corev1.Pod, logs chan events.Log) (err err
}
// parse log line - also handle old (output.Output) and new format (just unstructured []byte)
- logs <- events.NewLogResponseFromBytes(b)
+ logs <- events.NewLogFromBytes(b).
+ WithSource(events.SourceJobPod)
}
if err != nil {
@@ -240,16 +241,9 @@ func (p *Proxy) getPodContainerStatuses(pod corev1.Pod) (status string) {
// handleError will handle errors and push it as log chunk to logs stream
func (p *Proxy) handleError(err error, title string) {
if err != nil {
- ch := events.Log{
- Error: true,
- Content: err.Error(),
- }
-
p.log.Errorw(title, "error", err)
-
- if err == nil {
- p.logsStream.Push(context.Background(), ch)
- } else {
+ err = p.logsStream.Push(context.Background(), p.executionId, events.NewErrorLog(err))
+ if err != nil {
p.log.Errorw("error pushing error to stream", "title", title, "error", err)
}
diff --git a/pkg/logs/state/state.go b/pkg/logs/state/state.go
index 577e2d86873..abc7cc5f516 100644
--- a/pkg/logs/state/state.go
+++ b/pkg/logs/state/state.go
@@ -7,6 +7,11 @@ import (
"github.com/nats-io/nats.go/jetstream"
)
+var (
+ // state not found error
+ ErrStateNotFound = errors.New("no state found")
+)
+
// NewState creates new state storage
func NewState(kv jetstream.KeyValue) Interface {
return &State{
@@ -23,15 +28,18 @@ type State struct {
func (s State) Get(ctx context.Context, key string) (LogState, error) {
state, err := s.kv.Get(ctx, key)
if err != nil {
+ if err == jetstream.ErrKeyNotFound {
+ return LogStateUnknown, nil
+ }
+
return LogStateUnknown, err
}
if len(state.Value()) == 0 {
- return LogStateUnknown, errors.New("no state found")
+ return LogStateUnknown, ErrStateNotFound
}
return LogState(state.Value()[0]), nil
-
}
// Put puts state for given key - executionId
diff --git a/pkg/logs/state/state_test.go b/pkg/logs/state/state_test.go
index 9d753f713fe..c19ba18d377 100644
--- a/pkg/logs/state/state_test.go
+++ b/pkg/logs/state/state_test.go
@@ -26,8 +26,9 @@ func TestState(t *testing.T) {
s := NewState(kv)
t.Run("get non existing state", func(t *testing.T) {
- _, err := s.Get(ctx, "1")
- assert.Error(t, err)
+ state1, err := s.Get(ctx, "1")
+ assert.NoError(t, err)
+ assert.Equal(t, LogStateUnknown, state1)
})
t.Run("store state data and get it", func(t *testing.T) {
diff --git a/pkg/mapper/testexecutions/mapper.go b/pkg/mapper/testexecutions/mapper.go
index f7fcc8d36ec..31beb807c9b 100644
--- a/pkg/mapper/testexecutions/mapper.go
+++ b/pkg/mapper/testexecutions/mapper.go
@@ -5,6 +5,7 @@ import (
testexecutionv1 "github.com/kubeshop/testkube-operator/api/testexecution/v1"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ mappertcl "github.com/kubeshop/testkube/pkg/tcl/mappertcl/testexecutions"
)
// MapCRDVariables maps variables between API and operator CRDs
@@ -208,6 +209,7 @@ func MapAPIToCRD(request *testkube.Execution, generation int64) testexecutionv1.
PreRunScript: request.PreRunScript,
PostRunScript: request.PostRunScript,
ExecutePostRunScriptBeforeScraping: request.ExecutePostRunScriptBeforeScraping,
+ SourceScripts: request.SourceScripts,
RunningContext: runningContext,
ContainerShell: request.ContainerShell,
SlavePodRequest: podRequest,
@@ -216,5 +218,7 @@ func MapAPIToCRD(request *testkube.Execution, generation int64) testexecutionv1.
result.LatestExecution.StartTime.Time = request.StartTime
result.LatestExecution.EndTime.Time = request.EndTime
- return result
+
+ // Pro edition only (tcl protected code)
+ return *mappertcl.MapAPIToCRD(request, &result)
}
diff --git a/pkg/mapper/tests/kube_openapi.go b/pkg/mapper/tests/kube_openapi.go
index fbe4baae632..84fe46c1d7c 100644
--- a/pkg/mapper/tests/kube_openapi.go
+++ b/pkg/mapper/tests/kube_openapi.go
@@ -6,6 +6,7 @@ import (
commonv1 "github.com/kubeshop/testkube-operator/api/common/v1"
testsv3 "github.com/kubeshop/testkube-operator/api/tests/v3"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ mappertcl "github.com/kubeshop/testkube/pkg/tcl/mappertcl/tests"
)
// MapTestListKubeToAPI maps CRD list data to OpenAPI spec tests list
@@ -153,7 +154,7 @@ func MapExecutionRequestFromSpec(specExecutionRequest *testsv3.ExecutionRequest)
podRequest.PodTemplateReference = specExecutionRequest.SlavePodRequest.PodTemplateReference
}
- return &testkube.ExecutionRequest{
+ result := &testkube.ExecutionRequest{
Name: specExecutionRequest.Name,
TestSuiteName: specExecutionRequest.TestSuiteName,
Number: specExecutionRequest.Number,
@@ -183,6 +184,7 @@ func MapExecutionRequestFromSpec(specExecutionRequest *testsv3.ExecutionRequest)
PreRunScript: specExecutionRequest.PreRunScript,
PostRunScript: specExecutionRequest.PostRunScript,
ExecutePostRunScriptBeforeScraping: specExecutionRequest.ExecutePostRunScriptBeforeScraping,
+ SourceScripts: specExecutionRequest.SourceScripts,
PvcTemplate: specExecutionRequest.PvcTemplate,
PvcTemplateReference: specExecutionRequest.PvcTemplateReference,
ScraperTemplate: specExecutionRequest.ScraperTemplate,
@@ -192,6 +194,9 @@ func MapExecutionRequestFromSpec(specExecutionRequest *testsv3.ExecutionRequest)
EnvSecrets: MapEnvReferences(specExecutionRequest.EnvSecrets),
SlavePodRequest: podRequest,
}
+
+ // Pro edition only (tcl protected code)
+ return mappertcl.MapExecutionRequestFromSpec(specExecutionRequest, result)
}
// MapImagePullSecrets maps Kubernetes spec to testkube model
@@ -518,6 +523,10 @@ func MapSpecExecutionRequestToExecutionUpdateRequest(
envSecrets := MapEnvReferences(request.EnvSecrets)
executionRequest.EnvSecrets = &envSecrets
executionRequest.ExecutePostRunScriptBeforeScraping = &request.ExecutePostRunScriptBeforeScraping
+ executionRequest.SourceScripts = &request.SourceScripts
+
+ // Pro edition only (tcl protected code)
+ mappertcl.MapSpecExecutionRequestToExecutionUpdateRequest(request, executionRequest)
if request.ArtifactRequest != nil {
artifactRequest := &testkube.ArtifactUpdateRequest{
diff --git a/pkg/mapper/tests/openapi_kube.go b/pkg/mapper/tests/openapi_kube.go
index 919583ced42..64aa258734b 100644
--- a/pkg/mapper/tests/openapi_kube.go
+++ b/pkg/mapper/tests/openapi_kube.go
@@ -7,6 +7,7 @@ import (
testsv3 "github.com/kubeshop/testkube-operator/api/tests/v3"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ mappertcl "github.com/kubeshop/testkube/pkg/tcl/mappertcl/tests"
)
// MapUpsertToSpec maps TestUpsertRequest to Test CRD spec
@@ -165,7 +166,7 @@ func MapExecutionRequestToSpecExecutionRequest(executionRequest *testkube.Execut
podRequest.PodTemplateReference = executionRequest.SlavePodRequest.PodTemplateReference
}
- return &testsv3.ExecutionRequest{
+ result := &testsv3.ExecutionRequest{
Name: executionRequest.Name,
TestSuiteName: executionRequest.TestSuiteName,
Number: executionRequest.Number,
@@ -195,6 +196,7 @@ func MapExecutionRequestToSpecExecutionRequest(executionRequest *testkube.Execut
PreRunScript: executionRequest.PreRunScript,
PostRunScript: executionRequest.PostRunScript,
ExecutePostRunScriptBeforeScraping: executionRequest.ExecutePostRunScriptBeforeScraping,
+ SourceScripts: executionRequest.SourceScripts,
PvcTemplate: executionRequest.PvcTemplate,
PvcTemplateReference: executionRequest.PvcTemplateReference,
ScraperTemplate: executionRequest.ScraperTemplate,
@@ -204,6 +206,9 @@ func MapExecutionRequestToSpecExecutionRequest(executionRequest *testkube.Execut
EnvSecrets: mapEnvReferences(executionRequest.EnvSecrets),
SlavePodRequest: podRequest,
}
+
+ // Pro edition only (tcl protected code)
+ return mappertcl.MapExecutionRequestToSpecExecutionRequest(executionRequest, result)
}
func mapImagePullSecrets(secrets []testkube.LocalObjectReference) (res []v1.LocalObjectReference) {
@@ -627,6 +632,15 @@ func MapExecutionUpdateRequestToSpecExecutionRequest(executionRequest *testkube.
emptyExecution = false
}
+ // Pro edition only (tcl protected code)
+ if !mappertcl.MapExecutionUpdateRequestToSpecExecutionRequest(executionRequest, request) {
+ emptyExecution = false
+ }
+
+ if executionRequest.SourceScripts != nil {
+ request.SourceScripts = *executionRequest.SourceScripts
+ }
+
if executionRequest.ArtifactRequest != nil {
emptyArtifact := true
if !(*executionRequest.ArtifactRequest == nil || (*executionRequest.ArtifactRequest).IsEmpty()) {
diff --git a/pkg/mapper/testsuiteexecutions/mapper.go b/pkg/mapper/testsuiteexecutions/mapper.go
index 80fd67f8f32..ca2fd4c462d 100644
--- a/pkg/mapper/testsuiteexecutions/mapper.go
+++ b/pkg/mapper/testsuiteexecutions/mapper.go
@@ -6,6 +6,7 @@ import (
testsuiteexecutionv1 "github.com/kubeshop/testkube-operator/api/testsuiteexecution/v1"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ mappertcl "github.com/kubeshop/testkube/pkg/tcl/mappertcl/testsuiteexecutions"
)
// MapCRDVariables maps variables between API and operator CRDs
@@ -211,6 +212,7 @@ func MapExecutionCRD(request *testkube.Execution) *testsuiteexecutionv1.Executio
PreRunScript: request.PreRunScript,
PostRunScript: request.PostRunScript,
ExecutePostRunScriptBeforeScraping: request.ExecutePostRunScriptBeforeScraping,
+ SourceScripts: request.SourceScripts,
RunningContext: runningContext,
ContainerShell: request.ContainerShell,
SlavePodRequest: podRequest,
@@ -218,7 +220,9 @@ func MapExecutionCRD(request *testkube.Execution) *testsuiteexecutionv1.Executio
result.StartTime.Time = request.StartTime
result.EndTime.Time = request.EndTime
- return result
+
+ // Pro edition only (tcl protected code)
+ return mappertcl.MapExecutionCRD(request, result)
}
func MapTestSuiteStepV2ToCRD(request *testkube.TestSuiteStepV2) *testsuiteexecutionv1.TestSuiteStepV2 {
diff --git a/pkg/mapper/testsuites/kube_openapi.go b/pkg/mapper/testsuites/kube_openapi.go
index c845608de7b..4f384847350 100644
--- a/pkg/mapper/testsuites/kube_openapi.go
+++ b/pkg/mapper/testsuites/kube_openapi.go
@@ -80,7 +80,8 @@ func mapCRStepToAPI(crstep testsuitesv3.TestSuiteStepSpec) (teststep testkube.Te
switch true {
case crstep.Test != "":
teststep = testkube.TestSuiteStep{
- Test: crstep.Test,
+ Test: crstep.Test,
+ ExecutionRequest: MapTestStepExecutionRequestCRDToAPI(crstep.ExecutionRequest),
}
case crstep.Delay.Duration != 0:
@@ -355,3 +356,53 @@ func MapSpecExecutionRequestToExecutionUpdateRequest(request *testsuitesv3.TestS
return executionRequest
}
+
+func MapTestStepExecutionRequestCRDToAPI(request *testsuitesv3.TestSuiteStepExecutionRequest) *testkube.TestSuiteStepExecutionRequest {
+ if request == nil {
+ return nil
+ }
+ variables := map[string]testkube.Variable{}
+ for k, v := range request.Variables {
+ varType := testkube.VariableType(v.Type_)
+ variables[k] = testkube.Variable{
+ Name: v.Name,
+ Value: v.Value,
+ Type_: &varType,
+ }
+ }
+
+ var runningContext *testkube.RunningContext
+
+ if request.RunningContext != nil {
+ runningContext = &testkube.RunningContext{
+ Type_: string(request.RunningContext.Type_),
+ Context: request.RunningContext.Context,
+ }
+ }
+
+ argsMode := ""
+ if request.ArgsMode != "" {
+ argsMode = string(request.ArgsMode)
+ }
+
+ return &testkube.TestSuiteStepExecutionRequest{
+ ExecutionLabels: request.ExecutionLabels,
+ Variables: variables,
+ Command: request.Command,
+ Args: request.Args,
+ ArgsMode: argsMode,
+ Sync: request.Sync,
+ HttpProxy: request.HttpProxy,
+ HttpsProxy: request.HttpsProxy,
+ NegativeTest: request.NegativeTest,
+ JobTemplate: request.JobTemplate,
+ JobTemplateReference: request.JobTemplateReference,
+ CronJobTemplate: request.CronJobTemplate,
+ CronJobTemplateReference: request.CronJobTemplateReference,
+ ScraperTemplate: request.ScraperTemplate,
+ ScraperTemplateReference: request.ScraperTemplateReference,
+ PvcTemplate: request.PvcTemplate,
+ PvcTemplateReference: request.PvcTemplateReference,
+ RunningContext: runningContext,
+ }
+}
diff --git a/pkg/mapper/testsuites/openapi_kube.go b/pkg/mapper/testsuites/openapi_kube.go
index f3ba5892363..4132768594b 100644
--- a/pkg/mapper/testsuites/openapi_kube.go
+++ b/pkg/mapper/testsuites/openapi_kube.go
@@ -5,6 +5,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ v1 "github.com/kubeshop/testkube-operator/api/common/v1"
testsuitesv3 "github.com/kubeshop/testkube-operator/api/testsuite/v3"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/types"
@@ -198,6 +199,7 @@ func mapTestStepToCRD(step testkube.TestSuiteStep) (stepSpec testsuitesv3.TestSu
}
case testkube.TestSuiteStepTypeExecuteTest:
stepSpec.Test = step.Test
+ stepSpec.ExecutionRequest = MapTestStepExecutionRequestCRD(step.ExecutionRequest)
}
return stepSpec, nil
@@ -437,3 +439,47 @@ func MapExecutionToTestSuiteStatus(execution *testkube.TestSuiteExecution) (spec
return specStatus
}
+
+func MapTestStepExecutionRequestCRD(request *testkube.TestSuiteStepExecutionRequest) *testsuitesv3.TestSuiteStepExecutionRequest {
+ if request == nil {
+ return nil
+ }
+
+ variables := map[string]testsuitesv3.Variable{}
+ for k, v := range request.Variables {
+ variables[k] = testsuitesv3.Variable{
+ Name: v.Name,
+ Value: v.Value,
+ Type_: string(*v.Type_),
+ }
+ }
+
+ var runningContext *v1.RunningContext
+ if request.RunningContext != nil {
+ runningContext = &v1.RunningContext{
+ Type_: v1.RunningContextType(request.RunningContext.Type_),
+ Context: request.RunningContext.Context,
+ }
+ }
+
+ return &testsuitesv3.TestSuiteStepExecutionRequest{
+ ExecutionLabels: request.ExecutionLabels,
+ Variables: variables,
+ Args: request.Args,
+ ArgsMode: testsuitesv3.ArgsModeType(request.ArgsMode),
+ Command: request.Command,
+ Sync: request.Sync,
+ HttpProxy: request.HttpProxy,
+ HttpsProxy: request.HttpsProxy,
+ NegativeTest: request.NegativeTest,
+ JobTemplate: request.JobTemplate,
+ JobTemplateReference: request.JobTemplateReference,
+ CronJobTemplate: request.CronJobTemplate,
+ CronJobTemplateReference: request.CronJobTemplateReference,
+ ScraperTemplate: request.ScraperTemplate,
+ ScraperTemplateReference: request.ScraperTemplateReference,
+ PvcTemplate: request.PvcTemplate,
+ PvcTemplateReference: request.PvcTemplateReference,
+ RunningContext: runningContext,
+ }
+}
diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go
index 6766014f789..2881e947950 100644
--- a/pkg/reconciler/reconciler.go
+++ b/pkg/reconciler/reconciler.go
@@ -34,18 +34,16 @@ type Client struct {
testResultRepository testresult.Repository
executorsClient *executorsclientv1.ExecutorsClient
logger *zap.SugaredLogger
- namespace string
}
func NewClient(k8sclient kubernetes.Interface, resultRepository result.Repository, testResultRepository testresult.Repository,
- executorsClient *executorsclientv1.ExecutorsClient, logger *zap.SugaredLogger, namespace string) *Client {
+ executorsClient *executorsclientv1.ExecutorsClient, logger *zap.SugaredLogger) *Client {
return &Client{
k8sclient: k8sclient,
resultRepository: resultRepository,
testResultRepository: testResultRepository,
executorsClient: executorsClient,
logger: logger,
- namespace: namespace,
}
}
@@ -95,7 +93,7 @@ OuterLoop:
errMessage := errTestAbnoramallyTerminated.Error()
id := execution.Id
- pods, err := executor.GetJobPods(ctx, client.k8sclient.CoreV1().Pods(client.namespace), id, 1, 10)
+ pods, err := executor.GetJobPods(ctx, client.k8sclient.CoreV1().Pods(execution.TestNamespace), id, 1, 10)
if err == nil {
ExecutorLoop:
for _, pod := range pods.Items {
@@ -120,7 +118,7 @@ OuterLoop:
if supportArtifacts && execution.ArtifactRequest != nil && execution.ArtifactRequest.StorageClassName != "" {
id = execution.Id + "-scraper"
- pods, err = executor.GetJobPods(ctx, client.k8sclient.CoreV1().Pods(client.namespace), id, 1, 10)
+ pods, err = executor.GetJobPods(ctx, client.k8sclient.CoreV1().Pods(execution.TestNamespace), id, 1, 10)
if err == nil {
ScraperLoop:
for _, pod := range pods.Items {
diff --git a/pkg/repository/config/mock_repository.go b/pkg/repository/config/mock_repository.go
index 55aa28b5d8f..3dbb1e61a7c 100644
--- a/pkg/repository/config/mock_repository.go
+++ b/pkg/repository/config/mock_repository.go
@@ -9,7 +9,6 @@ import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
-
testkube "github.com/kubeshop/testkube/pkg/api/v1/testkube"
)
diff --git a/pkg/repository/result/interface.go b/pkg/repository/result/interface.go
index 26cf04ac2cf..a10928e1917 100644
--- a/pkg/repository/result/interface.go
+++ b/pkg/repository/result/interface.go
@@ -35,6 +35,8 @@ type Repository interface {
Sequences
// Get gets execution result by id or name
Get(ctx context.Context, id string) (testkube.Execution, error)
+ // Get gets execution result without output
+ GetExecution(ctx context.Context, id string) (testkube.Execution, error)
// GetByNameAndTest gets execution result by name and test name
GetByNameAndTest(ctx context.Context, name, testName string) (testkube.Execution, error)
// GetLatestByTest gets latest execution result by test
@@ -69,8 +71,10 @@ type Repository interface {
DeleteByTestSuites(ctx context.Context, testSuiteNames []string) (err error)
// DeleteForAllTestSuites deletes execution results for all test suites
DeleteForAllTestSuites(ctx context.Context) (err error)
-
+ // GetTestMetrics returns metrics for test
GetTestMetrics(ctx context.Context, name string, limit, last int) (metrics testkube.ExecutionsMetrics, err error)
+ // Count returns executions count
+ Count(ctx context.Context, filter Filter) (int64, error)
}
type Sequences interface {
diff --git a/pkg/repository/result/minio_output.go b/pkg/repository/result/minio_output.go
index 3ae1ff6a86f..2ad4a73cda3 100644
--- a/pkg/repository/result/minio_output.go
+++ b/pkg/repository/result/minio_output.go
@@ -42,7 +42,7 @@ func (m *MinioRepository) GetOutput(ctx context.Context, id, testName, testSuite
}
func (m *MinioRepository) getOutput(ctx context.Context, id string) (ExecutionOutput, error) {
- file, err := m.storage.DownloadFileFromBucket(ctx, m.bucket, "", id)
+ file, _, err := m.storage.DownloadFileFromBucket(ctx, m.bucket, "", id)
if err != nil && err == minio.ErrArtifactsNotFound {
log.DefaultLogger.Infow("output not found in minio", "id", id)
return ExecutionOutput{}, nil
@@ -180,7 +180,7 @@ func (m *MinioRepository) DeleteAllOutput(ctx context.Context) error {
}
func (m *MinioRepository) StreamOutput(ctx context.Context, executionID, testName, testSuiteName string) (reader io.Reader, err error) {
- file, err := m.storage.DownloadFileFromBucket(ctx, m.bucket, "", executionID)
+ file, _, err := m.storage.DownloadFileFromBucket(ctx, m.bucket, "", executionID)
if err != nil {
return nil, err
}
diff --git a/pkg/repository/result/minio_output_test.go b/pkg/repository/result/minio_output_test.go
index 4191cd3c2c6..f7d72e2ef2e 100644
--- a/pkg/repository/result/minio_output_test.go
+++ b/pkg/repository/result/minio_output_test.go
@@ -6,6 +6,7 @@ import (
"testing"
gomock "github.com/golang/mock/gomock"
+ "github.com/minio/minio-go/v7"
"github.com/stretchr/testify/assert"
"github.com/kubeshop/testkube/pkg/storage"
@@ -16,7 +17,8 @@ func TestGetOutputSize(t *testing.T) {
storageMock := storage.NewMockClient(mockCtrl)
outputClient := NewMinioOutputRepository(storageMock, nil, "test-bucket")
streamContent := "test line"
- storageMock.EXPECT().DownloadFileFromBucket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(strings.NewReader(streamContent), nil)
+ storageMock.EXPECT().DownloadFileFromBucket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
+ Return(strings.NewReader(streamContent), minio.ObjectInfo{}, nil)
size, err := outputClient.GetOutputSize(context.Background(), "test-id", "test-name", "test-suite-name")
assert.Nil(t, err)
assert.Equal(t, len(streamContent), size)
diff --git a/pkg/repository/result/mock_repository.go b/pkg/repository/result/mock_repository.go
index 29f7faa3294..a03399c72db 100644
--- a/pkg/repository/result/mock_repository.go
+++ b/pkg/repository/result/mock_repository.go
@@ -10,7 +10,6 @@ import (
time "time"
gomock "github.com/golang/mock/gomock"
-
testkube "github.com/kubeshop/testkube/pkg/api/v1/testkube"
)
@@ -37,6 +36,21 @@ func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
return m.recorder
}
+// Count mocks base method.
+func (m *MockRepository) Count(arg0 context.Context, arg1 Filter) (int64, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Count", arg0, arg1)
+ ret0, _ := ret[0].(int64)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Count indicates an expected call of Count.
+func (mr *MockRepositoryMockRecorder) Count(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Count", reflect.TypeOf((*MockRepository)(nil).Count), arg0, arg1)
+}
+
// DeleteAll mocks base method.
func (m *MockRepository) DeleteAll(arg0 context.Context) error {
m.ctrl.T.Helper()
@@ -165,6 +179,21 @@ func (mr *MockRepositoryMockRecorder) GetByNameAndTest(arg0, arg1, arg2 interfac
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByNameAndTest", reflect.TypeOf((*MockRepository)(nil).GetByNameAndTest), arg0, arg1, arg2)
}
+// GetExecution mocks base method.
+func (m *MockRepository) GetExecution(arg0 context.Context, arg1 string) (testkube.Execution, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetExecution", arg0, arg1)
+ ret0, _ := ret[0].(testkube.Execution)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetExecution indicates an expected call of GetExecution.
+func (mr *MockRepositoryMockRecorder) GetExecution(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExecution", reflect.TypeOf((*MockRepository)(nil).GetExecution), arg0, arg1)
+}
+
// GetExecutionTotals mocks base method.
func (m *MockRepository) GetExecutionTotals(arg0 context.Context, arg1 bool, arg2 ...Filter) (testkube.ExecutionsTotals, error) {
m.ctrl.T.Helper()
diff --git a/pkg/repository/result/mongo.go b/pkg/repository/result/mongo.go
index d4606f31782..398b838844b 100644
--- a/pkg/repository/result/mongo.go
+++ b/pkg/repository/result/mongo.go
@@ -12,8 +12,12 @@ import (
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
+ "go.uber.org/zap"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/featureflags"
+ "github.com/kubeshop/testkube/pkg/log"
+ logsclient "github.com/kubeshop/testkube/pkg/logs/client"
"github.com/kubeshop/testkube/pkg/storage"
)
@@ -32,6 +36,7 @@ const (
StepMaxCount = 100
)
+// NewMongoRepository creates a new MongoRepository - used by other testkube components - use opts to extend the functionality
func NewMongoRepository(db *mongo.Database, allowDiskUse, isDocDb bool, opts ...MongoRepositoryOpt) *MongoRepository {
r := &MongoRepository{
db: db,
@@ -40,6 +45,7 @@ func NewMongoRepository(db *mongo.Database, allowDiskUse, isDocDb bool, opts ...
OutputRepository: NewMongoOutputRepository(db),
allowDiskUse: allowDiskUse,
isDocDb: isDocDb,
+ log: log.DefaultLogger,
}
for _, opt := range opts {
@@ -61,6 +67,7 @@ func NewMongoRepositoryWithOutputRepository(
SequencesColl: db.Collection(CollectionSequences),
OutputRepository: outputRepository,
allowDiskUse: allowDiskUse,
+ log: log.DefaultLogger,
}
for _, opt := range opts {
@@ -76,6 +83,7 @@ func NewMongoRepositoryWithMinioOutputStorage(db *mongo.Database, allowDiskUse b
ResultsColl: db.Collection(CollectionResults),
SequencesColl: db.Collection(CollectionSequences),
allowDiskUse: allowDiskUse,
+ log: log.DefaultLogger,
}
repo.OutputRepository = NewMinioOutputRepository(storageClient, repo.ResultsColl, bucket)
return &repo
@@ -86,12 +94,27 @@ type MongoRepository struct {
ResultsColl *mongo.Collection
SequencesColl *mongo.Collection
OutputRepository OutputRepository
+ logGrpcClient logsclient.StreamGetter
allowDiskUse bool
isDocDb bool
+ features featureflags.FeatureFlags
+ log *zap.SugaredLogger
}
type MongoRepositoryOpt func(*MongoRepository)
+func WithLogsClient(client logsclient.StreamGetter) MongoRepositoryOpt {
+ return func(r *MongoRepository) {
+ r.logGrpcClient = client
+ }
+}
+
+func WithFeatureFlags(features featureflags.FeatureFlags) MongoRepositoryOpt {
+ return func(r *MongoRepository) {
+ r.features = features
+ }
+}
+
func WithMongoRepositoryResultCollection(collection *mongo.Collection) MongoRepositoryOpt {
return func(r *MongoRepository) {
r.ResultsColl = collection
@@ -104,15 +127,54 @@ func WithMongoRepositorySequenceCollection(collection *mongo.Collection) MongoRe
}
}
+func (r *MongoRepository) getOutputFromLogServer(ctx context.Context, result *testkube.Execution) (string, error) {
+ if r.logGrpcClient == nil {
+ return "", nil
+ }
+
+ if result.ExecutionResult == nil || !result.ExecutionResult.IsCompleted() {
+ return "", nil
+ }
+
+ logs, err := r.logGrpcClient.Get(ctx, result.Id)
+ if err != nil {
+ return "", err
+ }
+
+ output := ""
+ for log := range logs {
+ if log.Error != nil {
+ r.log.Errorw("can't get log line", "error", log.Error)
+ continue
+ }
+
+ output += log.Log.Content + "\n"
+ }
+
+ return output, nil
+}
+
+func (r *MongoRepository) GetExecution(ctx context.Context, id string) (result testkube.Execution, err error) {
+ err = r.ResultsColl.FindOne(ctx, bson.M{"$or": bson.A{bson.M{"id": id}, bson.M{"name": id}}}).Decode(&result)
+ if err != nil {
+ return
+ }
+ return *result.UnscapeDots(), err
+}
+
func (r *MongoRepository) Get(ctx context.Context, id string) (result testkube.Execution, err error) {
err = r.ResultsColl.FindOne(ctx, bson.M{"$or": bson.A{bson.M{"id": id}, bson.M{"name": id}}}).Decode(&result)
if err != nil {
return
}
if len(result.ExecutionResult.Output) == 0 {
- result.ExecutionResult.Output, err = r.OutputRepository.GetOutput(ctx, result.Id, result.TestName, result.TestSuiteName)
- if err == mongo.ErrNoDocuments {
- err = nil
+ if r.features.LogsV2 {
+ result.ExecutionResult.Output, err = r.getOutputFromLogServer(ctx, &result)
+ } else {
+ result.ExecutionResult.Output, err = r.OutputRepository.GetOutput(ctx, result.Id, result.TestName, result.TestSuiteName)
+ if err == mongo.ErrNoDocuments {
+ err = nil
+ }
}
}
return *result.UnscapeDots(), err
@@ -124,9 +186,13 @@ func (r *MongoRepository) GetByNameAndTest(ctx context.Context, name, testName s
return
}
if len(result.ExecutionResult.Output) == 0 {
- result.ExecutionResult.Output, err = r.OutputRepository.GetOutput(ctx, result.Id, result.TestName, result.TestSuiteName)
- if err == mongo.ErrNoDocuments {
- err = nil
+ if r.features.LogsV2 {
+ result.ExecutionResult.Output, err = r.getOutputFromLogServer(ctx, &result)
+ } else {
+ result.ExecutionResult.Output, err = r.OutputRepository.GetOutput(ctx, result.Id, result.TestName, result.TestSuiteName)
+ if err == mongo.ErrNoDocuments {
+ err = nil
+ }
}
}
return *result.UnscapeDots(), err
@@ -177,9 +243,13 @@ func (r *MongoRepository) slowGetLatestByTest(ctx context.Context, testName stri
}
result := (&items[0]).UnscapeDots()
if len(result.ExecutionResult.Output) == 0 {
- result.ExecutionResult.Output, err = r.OutputRepository.GetOutput(ctx, result.Id, result.TestName, result.TestSuiteName)
- if err == mongo.ErrNoDocuments {
- err = nil
+ if r.features.LogsV2 {
+ result.ExecutionResult.Output, err = r.getOutputFromLogServer(ctx, result)
+ } else {
+ result.ExecutionResult.Output, err = r.OutputRepository.GetOutput(ctx, result.Id, result.TestName, result.TestSuiteName)
+ if err == mongo.ErrNoDocuments {
+ err = nil
+ }
}
}
return result, err
@@ -235,9 +305,13 @@ func (r *MongoRepository) GetLatestByTest(ctx context.Context, testName string)
}
result := (&items[0]).UnscapeDots()
if len(result.ExecutionResult.Output) == 0 {
- result.ExecutionResult.Output, err = r.OutputRepository.GetOutput(ctx, result.Id, result.TestName, result.TestSuiteName)
- if err == mongo.ErrNoDocuments {
- err = nil
+ if r.features.LogsV2 {
+ result.ExecutionResult.Output, err = r.getOutputFromLogServer(ctx, result)
+ } else {
+ result.ExecutionResult.Output, err = r.OutputRepository.GetOutput(ctx, result.Id, result.TestName, result.TestSuiteName)
+ if err == mongo.ErrNoDocuments {
+ err = nil
+ }
}
}
return result, err
@@ -392,6 +466,11 @@ func (r *MongoRepository) GetExecutions(ctx context.Context, filter Filter) (res
return
}
+func (r *MongoRepository) Count(ctx context.Context, filter Filter) (count int64, err error) {
+ query, _ := composeQueryAndOpts(filter)
+ return r.ResultsColl.CountDocuments(ctx, query)
+}
+
func (r *MongoRepository) GetExecutionTotals(ctx context.Context, paging bool, filter ...Filter) (totals testkube.ExecutionsTotals, err error) {
var result []struct {
Status string `bson:"_id"`
@@ -500,7 +579,10 @@ func (r *MongoRepository) Insert(ctx context.Context, result testkube.Execution)
if err != nil {
return
}
- err = r.OutputRepository.InsertOutput(ctx, result.Id, result.TestName, result.TestSuiteName, output)
+
+ if !r.features.LogsV2 {
+ err = r.OutputRepository.InsertOutput(ctx, result.Id, result.TestName, result.TestSuiteName, output)
+ }
return
}
@@ -512,7 +594,10 @@ func (r *MongoRepository) Update(ctx context.Context, result testkube.Execution)
if err != nil {
return
}
- err = r.OutputRepository.UpdateOutput(ctx, result.Id, result.TestName, result.TestSuiteName, output)
+
+ if !r.features.LogsV2 {
+ err = r.OutputRepository.UpdateOutput(ctx, result.Id, result.TestName, result.TestSuiteName, output)
+ }
return
}
@@ -543,7 +628,9 @@ func (r *MongoRepository) UpdateResult(ctx context.Context, id string, result te
return err
}
- err = r.OutputRepository.UpdateOutput(ctx, id, result.TestName, result.TestSuiteName, cleanOutput(output))
+ if !r.features.LogsV2 {
+ err = r.OutputRepository.UpdateOutput(ctx, id, result.TestName, result.TestSuiteName, cleanOutput(output))
+ }
return
}
diff --git a/pkg/repository/testresult/interface.go b/pkg/repository/testresult/interface.go
index ddd1c11d173..2fc76c2d49f 100644
--- a/pkg/repository/testresult/interface.go
+++ b/pkg/repository/testresult/interface.go
@@ -27,7 +27,7 @@ type Filter interface {
Selector() string
}
-//go:generate mockgen -destination=./mock_repository.go -package=testresult "github.com/kubeshop/testkube/internal/pkg/api/repository/testresult" Repository
+//go:generate mockgen -destination=./mock_repository.go -package=testresult "github.com/kubeshop/testkube/pkg/repository/testresult" Repository
type Repository interface {
// Get gets execution result by id or name
Get(ctx context.Context, id string) (testkube.TestSuiteExecution, error)
@@ -55,6 +55,8 @@ type Repository interface {
DeleteAll(ctx context.Context) error
// DeleteByTestSuites deletes execution results by test suites
DeleteByTestSuites(ctx context.Context, testSuiteNames []string) (err error)
-
+ // GetTestSuiteMetrics returns metrics for test suite
GetTestSuiteMetrics(ctx context.Context, name string, limit, last int) (metrics testkube.ExecutionsMetrics, err error)
+ // Count returns executions count
+ Count(ctx context.Context, filter Filter) (int64, error)
}
diff --git a/pkg/repository/testresult/mock_repository.go b/pkg/repository/testresult/mock_repository.go
index 9b0fd0025bb..d74f63f6cea 100644
--- a/pkg/repository/testresult/mock_repository.go
+++ b/pkg/repository/testresult/mock_repository.go
@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/kubeshop/testkube/internal/pkg/api/repository/testresult (interfaces: Repository)
+// Source: github.com/kubeshop/testkube/pkg/repository/testresult (interfaces: Repository)
// Package testresult is a generated GoMock package.
package testresult
@@ -10,7 +10,6 @@ import (
time "time"
gomock "github.com/golang/mock/gomock"
-
testkube "github.com/kubeshop/testkube/pkg/api/v1/testkube"
)
@@ -37,6 +36,21 @@ func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
return m.recorder
}
+// Count mocks base method.
+func (m *MockRepository) Count(arg0 context.Context, arg1 Filter) (int64, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Count", arg0, arg1)
+ ret0, _ := ret[0].(int64)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Count indicates an expected call of Count.
+func (mr *MockRepositoryMockRecorder) Count(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Count", reflect.TypeOf((*MockRepository)(nil).Count), arg0, arg1)
+}
+
// DeleteAll mocks base method.
func (m *MockRepository) DeleteAll(arg0 context.Context) error {
m.ctrl.T.Helper()
diff --git a/pkg/repository/testresult/mongo.go b/pkg/repository/testresult/mongo.go
index f5fc69f4ec8..b1f16a11f7e 100644
--- a/pkg/repository/testresult/mongo.go
+++ b/pkg/repository/testresult/mongo.go
@@ -288,6 +288,11 @@ func (r *MongoRepository) GetNewestExecutions(ctx context.Context, limit int) (r
return
}
+func (r *MongoRepository) Count(ctx context.Context, filter Filter) (count int64, err error) {
+ query, _ := composeQueryAndOpts(filter)
+ return r.Coll.CountDocuments(ctx, query)
+}
+
func (r *MongoRepository) GetExecutionsTotals(ctx context.Context, filter ...Filter) (totals testkube.ExecutionsTotals, err error) {
var result []struct {
Status string `bson:"_id"`
diff --git a/pkg/scheduler/service.go b/pkg/scheduler/service.go
index 6e1a6656ed6..2b50e3007ed 100644
--- a/pkg/scheduler/service.go
+++ b/pkg/scheduler/service.go
@@ -12,21 +12,23 @@ import (
testsuiteexecutionsclientv1 "github.com/kubeshop/testkube-operator/pkg/client/testsuiteexecutions/v1"
testsuitesv3 "github.com/kubeshop/testkube-operator/pkg/client/testsuites/v3"
v1 "github.com/kubeshop/testkube/internal/app/api/metrics"
- "github.com/kubeshop/testkube/internal/featureflags"
"github.com/kubeshop/testkube/pkg/configmap"
"github.com/kubeshop/testkube/pkg/event"
"github.com/kubeshop/testkube/pkg/executor/client"
+ "github.com/kubeshop/testkube/pkg/featureflags"
+ logsclient "github.com/kubeshop/testkube/pkg/logs/client"
"github.com/kubeshop/testkube/pkg/repository/result"
"github.com/kubeshop/testkube/pkg/repository/testresult"
"github.com/kubeshop/testkube/pkg/secret"
+ "github.com/kubeshop/testkube/pkg/tcl/checktcl"
)
type Scheduler struct {
metrics v1.Metrics
executor client.Executor
containerExecutor client.Executor
- executionResults result.Repository
- testExecutionResults testresult.Repository
+ testResults result.Repository
+ testsuiteResults testresult.Repository
executorsClient executorsv1.Interface
testsClient testsv3.Interface
testSuitesClient testsuitesv3.Interface
@@ -40,6 +42,10 @@ type Scheduler struct {
eventsBus bus.Bus
dashboardURI string
featureFlags featureflags.FeatureFlags
+ logsStream logsclient.Stream
+ subscriptionChecker checktcl.SubscriptionChecker
+ namespace string
+ agentAPITLSSecret string
}
func NewScheduler(
@@ -61,14 +67,17 @@ func NewScheduler(
eventsBus bus.Bus,
dashboardURI string,
featureFlags featureflags.FeatureFlags,
+ logsStream logsclient.Stream,
+ namespace string,
+ agentAPITLSSecret string,
) *Scheduler {
return &Scheduler{
metrics: metrics,
executor: executor,
containerExecutor: containerExecutor,
secretClient: secretClient,
- executionResults: executionResults,
- testExecutionResults: testExecutionResults,
+ testResults: executionResults,
+ testsuiteResults: testExecutionResults,
executorsClient: executorsClient,
testsClient: testsClient,
testSuitesClient: testSuitesClient,
@@ -81,5 +90,15 @@ func NewScheduler(
eventsBus: eventsBus,
dashboardURI: dashboardURI,
featureFlags: featureFlags,
+ logsStream: logsStream,
+ namespace: namespace,
+ agentAPITLSSecret: agentAPITLSSecret,
}
}
+
+// WithSubscriptionChecker sets subscription checker for the Scheduler
+// This is used to check if Pro/Enterprise subscription is valid
+func (s *Scheduler) WithSubscriptionChecker(subscriptionChecker checktcl.SubscriptionChecker) *Scheduler {
+ s.subscriptionChecker = subscriptionChecker
+ return s
+}
diff --git a/pkg/scheduler/test_scheduler.go b/pkg/scheduler/test_scheduler.go
index 4bfbcd97b92..657a13fca64 100644
--- a/pkg/scheduler/test_scheduler.go
+++ b/pkg/scheduler/test_scheduler.go
@@ -4,22 +4,28 @@ import (
"context"
"fmt"
"path/filepath"
+ "strings"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
testsv3 "github.com/kubeshop/testkube-operator/api/tests/v3"
testsourcev1 "github.com/kubeshop/testkube-operator/api/testsource/v1"
+ "github.com/kubeshop/testkube-operator/pkg/secret"
"github.com/kubeshop/testkube/internal/common"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/executor"
"github.com/kubeshop/testkube/pkg/executor/client"
+ "github.com/kubeshop/testkube/pkg/logs/events"
testsmapper "github.com/kubeshop/testkube/pkg/mapper/tests"
+ "github.com/kubeshop/testkube/pkg/tcl/checktcl"
+ "github.com/kubeshop/testkube/pkg/tcl/schedulertcl"
"github.com/kubeshop/testkube/pkg/workerpool"
)
const (
- containerType = "container"
+ containerType = "container"
+ gitCredentialPrefix = "git_credential_"
)
func (s *Scheduler) PrepareTestRequests(work []testsv3.Test, request testkube.ExecutionRequest) []workerpool.Request[
@@ -54,47 +60,52 @@ func (s *Scheduler) executeTest(ctx context.Context, test testkube.Test, request
}
// test name + test execution name should be unique
- execution, _ = s.executionResults.GetByNameAndTest(ctx, request.Name, test.Name)
+ execution, _ = s.testResults.GetByNameAndTest(ctx, request.Name, test.Name)
+
if execution.Name == request.Name {
- return execution.Err(errors.Errorf("test execution with name %s already exists", request.Name)), nil
+ err := errors.Errorf("test execution with name %s already exists", request.Name)
+ return s.handleExecutionError(ctx, execution, "duplicate execution: %w", err)
}
secretUUID, err := s.testsClient.GetCurrentSecretUUID(test.Name)
if err != nil {
- return execution.Errw(request.Id, "can't get current secret uuid: %w", err), nil
+ return s.handleExecutionError(ctx, execution, "can't get current secret uuid: %w", err)
}
request.TestSecretUUID = secretUUID
// merge available data into execution options test spec, executor spec, request, test id
options, err := s.getExecuteOptions(test.Namespace, test.Name, request)
if err != nil {
- return execution.Errw(request.Id, "can't create valid execution options: %w", err), nil
+ return s.handleExecutionError(ctx, execution, "can't get execute options: %w", err)
}
// store execution in storage, can be fetched from API now
- execution = newExecutionFromExecutionOptions(options)
+ execution, err = newExecutionFromExecutionOptions(s.subscriptionChecker, options)
+ if err != nil {
+ return s.handleExecutionError(ctx, execution, "can't get new execution: %w", err)
+ }
+
options.ID = execution.Id
- if err := s.createSecretsReferences(&execution); err != nil {
- return execution.Errw(execution.Id, "can't create secret variables `Secret` references: %w", err), nil
+ s.events.Notify(testkube.NewEventStartTest(&execution))
+
+ if err := s.createSecretsReferences(&execution, &options); err != nil {
+ return s.handleExecutionError(ctx, execution, "can't create secret variables `Secret` references: %w", err)
}
- err = s.executionResults.Insert(ctx, execution)
+ err = s.testResults.Insert(ctx, execution)
if err != nil {
- return execution.Errw(execution.Id, "can't create new test execution, can't insert into storage: %w", err), nil
+ return s.handleExecutionError(ctx, execution, "can't create new test execution, can't insert into storage: %w", err)
}
- s.logger.Infow("calling executor with options", "options", options.Request)
+ s.logger.Infow("calling executor with options", "executionId", execution.Id, "options", options.Request)
execution.Start()
- s.events.Notify(testkube.NewEventStartTest(&execution))
-
// update storage with current execution status
- err = s.executionResults.StartExecution(ctx, execution.Id, execution.StartTime)
+ err = s.testResults.StartExecution(ctx, execution.Id, execution.StartTime)
if err != nil {
- s.events.Notify(testkube.NewEventEndTestFailed(&execution))
- return execution.Errw(execution.Id, "can't execute test, can't insert into storage error: %w", err), nil
+ return s.handleExecutionError(ctx, execution, "can't execute test, can't insert into storage error: %w", err)
}
// sync/async test execution
@@ -104,21 +115,57 @@ func (s *Scheduler) executeTest(ctx context.Context, test testkube.Test, request
execution.ExecutionResult = result
// update storage with current execution status
- if uerr := s.executionResults.UpdateResult(ctx, execution.Id, execution); uerr != nil {
- s.events.Notify(testkube.NewEventEndTestFailed(&execution))
- return execution.Errw(execution.Id, "update execution error: %w", uerr), nil
+ if uerr := s.testResults.UpdateResult(ctx, execution.Id, execution); uerr != nil {
+ return s.handleExecutionError(ctx, execution, "update execution error: %w", err)
}
if err != nil {
- s.events.Notify(testkube.NewEventEndTestFailed(&execution))
- return execution.Errw(execution.Id, "test execution failed: %w", err), nil
+ return s.handleExecutionError(ctx, execution, "test execution failed: %w", err)
}
s.logger.Infow("test started", "executionId", execution.Id, "status", execution.ExecutionResult.Status)
+ s.handleExecutionStart(ctx, execution)
+
return execution, nil
}
+func (s *Scheduler) handleExecutionStart(ctx context.Context, execution testkube.Execution) {
+ // pass here all needed execution data to the log
+ if s.featureFlags.LogsV2 {
+
+ l := events.NewLog(fmt.Sprintf("starting execution %s (%s)", execution.Name, execution.Id)).
+ WithType("execution-config").
+ WithVersion(events.LogVersionV2).
+ WithSource("test-scheduler").
+ WithMetadataEntry("command", strings.Join(execution.Command, " ")).
+ WithMetadataEntry("argsmode", execution.ArgsMode).
+ WithMetadataEntry("args", strings.Join(execution.Args, " ")).
+ WithMetadataEntry("pre-run", execution.PreRunScript).
+ WithMetadataEntry("post-run", execution.PostRunScript)
+
+ s.logsStream.Push(ctx, execution.Id, l)
+ }
+}
+
+func (s *Scheduler) handleExecutionError(ctx context.Context, execution testkube.Execution, msgTpl string, err error) (testkube.Execution, error) {
+ // push error log to the log stream if logs v2 enabled
+ if s.featureFlags.LogsV2 {
+ l := events.NewLog(fmt.Sprintf(msgTpl, err)).
+ WithType("error").
+ WithVersion(events.LogVersionV2).
+ WithSource("test-scheduler")
+
+ s.logsStream.Push(ctx, execution.Id, l)
+
+ }
+
+ // notify events that execution failed
+ s.events.Notify(testkube.NewEventEndTestFailed(&execution))
+
+ return execution.Errw(execution.Id, msgTpl, err), nil
+}
+
func (s *Scheduler) startTestExecution(ctx context.Context, options client.ExecuteOptions, execution *testkube.Execution) (result *testkube.ExecutionResult, err error) {
executor := s.getExecutor(options.TestName)
return executor.Execute(ctx, execution, options)
@@ -146,7 +193,7 @@ func (s *Scheduler) getExecutor(testName string) client.Executor {
}
func (s *Scheduler) getNextExecutionNumber(testName string) int32 {
- number, err := s.executionResults.GetNextExecutionNumber(context.Background(), testName)
+ number, err := s.testResults.GetNextExecutionNumber(context.Background(), testName)
if err != nil {
s.logger.Errorw("retrieving latest execution", "error", err)
return number
@@ -156,7 +203,7 @@ func (s *Scheduler) getNextExecutionNumber(testName string) int32 {
}
// createSecretsReferences strips secrets from text and store it inside model as reference to secret
-func (s *Scheduler) createSecretsReferences(execution *testkube.Execution) (err error) {
+func (s *Scheduler) createSecretsReferences(execution *testkube.Execution, options *client.ExecuteOptions) (err error) {
secrets := map[string]string{}
secretName := execution.Id + "-vars"
@@ -184,6 +231,32 @@ func (s *Scheduler) createSecretsReferences(execution *testkube.Execution) (err
}
}
+ secretRefs := []*testkube.SecretRef{options.UsernameSecret, options.TokenSecret}
+ for _, secretRef := range secretRefs {
+ if secretRef == nil {
+ continue
+ }
+
+ if execution.TestNamespace == s.namespace || (secretRef.Name != secret.GetMetadataName(execution.TestName, client.SecretTest) &&
+ secretRef.Name != secret.GetMetadataName(execution.TestName, client.SecretSource)) {
+ continue
+ }
+
+ data, err := s.secretClient.Get(secretRef.Name)
+ if err != nil {
+ return err
+ }
+
+ value, ok := data[secretRef.Key]
+ if !ok {
+ return fmt.Errorf("secret key %s not found for secret %s", secretRef.Key, secretRef.Name)
+ }
+
+ secrets[gitCredentialPrefix+secretRef.Key] = value
+ secretRef.Name = secretName
+ secretRef.Key = gitCredentialPrefix + secretRef.Key
+ }
+
labels := map[string]string{"executionID": execution.Id, "testName": execution.TestName}
if len(secrets) > 0 {
@@ -191,13 +264,14 @@ func (s *Scheduler) createSecretsReferences(execution *testkube.Execution) (err
secretName,
labels,
secrets,
+ execution.TestNamespace,
)
}
return nil
}
-func newExecutionFromExecutionOptions(options client.ExecuteOptions) testkube.Execution {
+func newExecutionFromExecutionOptions(subscriptionChecker checktcl.SubscriptionChecker, options client.ExecuteOptions) (testkube.Execution, error) {
execution := testkube.NewExecution(
options.Request.Id,
options.Namespace,
@@ -225,13 +299,23 @@ func newExecutionFromExecutionOptions(options client.ExecuteOptions) testkube.Ex
execution.PreRunScript = options.Request.PreRunScript
execution.PostRunScript = options.Request.PostRunScript
execution.ExecutePostRunScriptBeforeScraping = options.Request.ExecutePostRunScriptBeforeScraping
+ execution.SourceScripts = options.Request.SourceScripts
execution.RunningContext = options.Request.RunningContext
execution.TestExecutionName = options.Request.TestExecutionName
execution.DownloadArtifactExecutionIDs = options.Request.DownloadArtifactExecutionIDs
execution.DownloadArtifactTestNames = options.Request.DownloadArtifactTestNames
execution.SlavePodRequest = options.Request.SlavePodRequest
- return execution
+ // Pro edition only (tcl protected code)
+ if schedulertcl.HasExecutionNamespace(&options.Request) {
+ if err := subscriptionChecker.IsActiveOrgPlanEnterpriseForFeature("execution namespace"); err != nil {
+ return execution, err
+ }
+
+ execution = schedulertcl.NewExecutionFromExecutionOptions(options.Request, execution)
+ }
+
+ return execution, nil
}
func (s *Scheduler) getExecuteOptions(namespace, id string, request testkube.ExecutionRequest) (options client.ExecuteOptions, err error) {
@@ -260,6 +344,7 @@ func (s *Scheduler) getExecuteOptions(namespace, id string, request testkube.Exe
test := testsmapper.MapTestCRToAPI(*testCR)
+ request.Namespace = namespace
if test.ExecutionRequest != nil {
// Test variables lowest priority, then test suite, then test suite execution / test execution
request.Variables = mergeVariables(test.ExecutionRequest.Variables, request.Variables)
@@ -347,6 +432,10 @@ func (s *Scheduler) getExecuteOptions(namespace, id string, request testkube.Exe
request.ExecutePostRunScriptBeforeScraping = test.ExecutionRequest.ExecutePostRunScriptBeforeScraping
}
+ if !request.SourceScripts && test.ExecutionRequest.SourceScripts {
+ request.SourceScripts = test.ExecutionRequest.SourceScripts
+ }
+
request.ArtifactRequest = mergeArtifacts(request.ArtifactRequest, test.ExecutionRequest.ArtifactRequest)
if request.ArtifactRequest != nil && request.ArtifactRequest.VolumeMountPath == "" {
request.ArtifactRequest.VolumeMountPath = filepath.Join(executor.VolumeDir, "artifacts")
@@ -358,6 +447,15 @@ func (s *Scheduler) getExecuteOptions(namespace, id string, request testkube.Exe
s.logger.Infow("setting negative test from test definition", "test", test.Name, "negativeTest", test.ExecutionRequest.NegativeTest)
request.NegativeTest = test.ExecutionRequest.NegativeTest
}
+
+ // Pro edition only (tcl protected code)
+ if schedulertcl.HasExecutionNamespace(test.ExecutionRequest) {
+ if err = s.subscriptionChecker.IsActiveOrgPlanEnterpriseForFeature("execution namespace"); err != nil {
+ return options, err
+ }
+
+ request = schedulertcl.GetExecuteOptions(test.ExecutionRequest, request)
+ }
}
// get executor from kubernetes CRs
@@ -395,7 +493,7 @@ func (s *Scheduler) getExecuteOptions(namespace, id string, request testkube.Exe
continue
}
- data, err := s.configMapClient.Get(context.Background(), configMap.Reference.Name)
+ data, err := s.configMapClient.Get(context.Background(), configMap.Reference.Name, request.Namespace)
if err != nil {
return options, errors.Errorf("can't get config map: %v", err)
}
@@ -415,7 +513,7 @@ func (s *Scheduler) getExecuteOptions(namespace, id string, request testkube.Exe
continue
}
- data, err := s.secretClient.Get(secret.Reference.Name)
+ data, err := s.secretClient.Get(secret.Reference.Name, request.Namespace)
if err != nil {
return options, errors.Errorf("can't get secret: %v", err)
}
@@ -449,7 +547,7 @@ func (s *Scheduler) getExecuteOptions(namespace, id string, request testkube.Exe
return client.ExecuteOptions{
TestName: id,
- Namespace: namespace,
+ Namespace: request.Namespace,
TestSpec: testCR.Spec,
ExecutorName: executorCR.ObjectMeta.Name,
ExecutorSpec: executorCR.Spec,
@@ -459,6 +557,7 @@ func (s *Scheduler) getExecuteOptions(namespace, id string, request testkube.Exe
UsernameSecret: usernameSecret,
TokenSecret: tokenSecret,
CertificateSecret: certificateSecret,
+ AgentAPITLSSecret: s.agentAPITLSSecret,
ImagePullSecretNames: imagePullSecrets,
Features: s.featureFlags,
}, nil
diff --git a/pkg/scheduler/test_scheduler_test.go b/pkg/scheduler/test_scheduler_test.go
index 85fc5dddc21..5146db27275 100644
--- a/pkg/scheduler/test_scheduler_test.go
+++ b/pkg/scheduler/test_scheduler_test.go
@@ -112,7 +112,7 @@ func TestGetExecuteOptions(t *testing.T) {
mockTestsClient.EXPECT().Get("id").Return(&mockTest, nil).Times(1)
mockExecutorsClient.EXPECT().GetByType(mockExecutorTypes).Return(&mockExecutor, nil)
- mockConfigMapClient.EXPECT().Get(gomock.Any(), "configmap").Times(1)
+ mockConfigMapClient.EXPECT().Get(gomock.Any(), "configmap", "namespace").Times(1)
req := testkube.ExecutionRequest{
Name: "id-1",
@@ -145,6 +145,7 @@ func TestGetExecuteOptions(t *testing.T) {
PreRunScript: "",
PostRunScript: "",
ExecutePostRunScriptBeforeScraping: true,
+ SourceScripts: true,
ScraperTemplate: "",
ScraperTemplateReference: "",
PvcTemplate: "",
diff --git a/pkg/scheduler/testsuite_scheduler.go b/pkg/scheduler/testsuite_scheduler.go
index ba564919051..57d9c6e9f8a 100644
--- a/pkg/scheduler/testsuite_scheduler.go
+++ b/pkg/scheduler/testsuite_scheduler.go
@@ -14,6 +14,7 @@ import (
"github.com/kubeshop/testkube/pkg/event/bus"
testsuiteexecutionsmapper "github.com/kubeshop/testkube/pkg/mapper/testsuiteexecutions"
testsuitesmapper "github.com/kubeshop/testkube/pkg/mapper/testsuites"
+
"github.com/kubeshop/testkube/pkg/telemetry"
"github.com/kubeshop/testkube/pkg/version"
"github.com/kubeshop/testkube/pkg/workerpool"
@@ -27,6 +28,7 @@ const (
type testTuple struct {
test testkube.Test
executionID string
+ stepRequest *testkube.TestSuiteStepExecutionRequest
}
func (s *Scheduler) PrepareTestSuiteRequests(work []testsuitesv3.TestSuite, request testkube.TestSuiteExecutionRequest) []workerpool.Request[
@@ -117,7 +119,7 @@ func (s *Scheduler) executeTestSuite(ctx context.Context, testSuite testkube.Tes
}
testsuiteExecution = testkube.NewStartedTestSuiteExecution(testSuite, request)
- err = s.testExecutionResults.Insert(ctx, testsuiteExecution)
+ err = s.testsuiteResults.Insert(ctx, testsuiteExecution)
if err != nil {
s.logger.Infow("Inserting test execution", "error", err)
}
@@ -208,7 +210,7 @@ func (s *Scheduler) runSteps(ctx context.Context, wg *sync.WaitGroup, testsuiteE
}
}
- err := s.testExecutionResults.Update(ctx, *testsuiteExecution)
+ err := s.testsuiteResults.Update(ctx, *testsuiteExecution)
if err != nil {
s.logger.Infow("Updating test execution", "error", err)
}
@@ -224,7 +226,7 @@ func (s *Scheduler) runSteps(ctx context.Context, wg *sync.WaitGroup, testsuiteE
s.logger.Debugw("Batch step execution result", "step", batchStepResult.Execute, "results", results)
- err = s.testExecutionResults.Update(ctx, *testsuiteExecution)
+ err = s.testsuiteResults.Update(ctx, *testsuiteExecution)
if err != nil {
s.logger.Errorw("saving test suite execution results error", "error", err)
@@ -262,7 +264,7 @@ func (s *Scheduler) runSteps(ctx context.Context, wg *sync.WaitGroup, testsuiteE
s.metrics.IncExecuteTestSuite(*testsuiteExecution, s.dashboardURI)
- err = s.testExecutionResults.Update(ctx, *testsuiteExecution)
+ err = s.testsuiteResults.Update(ctx, *testsuiteExecution)
if err != nil {
s.logger.Errorw("saving final test suite execution result error", "error", err)
}
@@ -272,7 +274,7 @@ func (s *Scheduler) runSteps(ctx context.Context, wg *sync.WaitGroup, testsuiteE
func (s *Scheduler) runAfterEachStep(ctx context.Context, execution *testkube.TestSuiteExecution, wg *sync.WaitGroup) {
execution.Stop()
- err := s.testExecutionResults.EndExecution(ctx, *execution)
+ err := s.testsuiteResults.EndExecution(ctx, *execution)
if err != nil {
s.logger.Errorw("error setting end time", "error", err.Error())
}
@@ -444,6 +446,7 @@ func (s *Scheduler) executeTestStep(ctx context.Context, testsuiteExecution test
testTuples = append(testTuples, testTuple{
test: testkube.Test{Name: executeTestStep, Namespace: testsuiteExecution.TestSuite.Namespace},
executionID: execution.Id,
+ stepRequest: step.ExecutionRequest,
})
case testkube.TestSuiteStepTypeDelay:
if step.Delay == "" {
@@ -506,6 +509,7 @@ func (s *Scheduler) executeTestStep(ctx context.Context, testsuiteExecution test
for i := range testTuples {
req.Name = fmt.Sprintf("%s-%s", testSuiteName, testTuples[i].test.Name)
req.Id = testTuples[i].executionID
+ req = MergeStepRequest(testTuples[i].stepRequest, req)
requests[i] = workerpool.Request[testkube.Test, testkube.ExecutionRequest, testkube.Execution]{
Object: testTuples[i].test,
Options: req,
@@ -518,7 +522,7 @@ func (s *Scheduler) executeTestStep(ctx context.Context, testsuiteExecution test
}
result.Start()
- if err := s.testExecutionResults.Update(ctx, testsuiteExecution); err != nil {
+ if err := s.testsuiteResults.Update(ctx, testsuiteExecution); err != nil {
s.logger.Errorw("saving test suite execution start time error", "error", err)
}
@@ -543,7 +547,7 @@ func (s *Scheduler) executeTestStep(ctx context.Context, testsuiteExecution test
if result.Execute[i].Execution.Id == r.Result.Id {
result.Execute[i].Execution = &value
- if err := s.testExecutionResults.Update(ctx, testsuiteExecution); err != nil {
+ if err := s.testsuiteResults.Update(ctx, testsuiteExecution); err != nil {
s.logger.Errorw("saving test suite execution results error", "error", err)
}
}
@@ -552,7 +556,7 @@ func (s *Scheduler) executeTestStep(ctx context.Context, testsuiteExecution test
}
result.Stop()
- if err := s.testExecutionResults.Update(ctx, testsuiteExecution); err != nil {
+ if err := s.testsuiteResults.Update(ctx, testsuiteExecution); err != nil {
s.logger.Errorw("saving test suite execution end time error", "error", err)
}
}
@@ -619,3 +623,57 @@ func (s *Scheduler) delayWithAbortionCheck(duration time.Duration, testSuiteId s
}
}
}
+
+// MergeStepRequest inherits step request fields with execution request
+func MergeStepRequest(stepRequest *testkube.TestSuiteStepExecutionRequest, executionRequest testkube.ExecutionRequest) testkube.ExecutionRequest {
+ if stepRequest == nil {
+ return executionRequest
+ }
+ if stepRequest.ExecutionLabels != nil {
+ executionRequest.ExecutionLabels = stepRequest.ExecutionLabels
+ }
+
+ if stepRequest.Variables != nil {
+ executionRequest.Variables = mergeVariables(executionRequest.Variables, stepRequest.Variables)
+ }
+
+ if len(stepRequest.Args) != 0 {
+ if stepRequest.ArgsMode == string(testkube.ArgsModeTypeAppend) || stepRequest.ArgsMode == "" {
+ executionRequest.Args = append(executionRequest.Args, stepRequest.Args...)
+ }
+
+ if stepRequest.ArgsMode == string(testkube.ArgsModeTypeOverride) || stepRequest.ArgsMode == string(testkube.ArgsModeTypeReplace) {
+ executionRequest.Args = stepRequest.Args
+ }
+ }
+
+ if stepRequest.Command != nil {
+ executionRequest.Command = stepRequest.Command
+ }
+ executionRequest.HttpProxy = setStringField(executionRequest.HttpProxy, stepRequest.HttpProxy)
+ executionRequest.HttpsProxy = setStringField(executionRequest.HttpsProxy, stepRequest.HttpsProxy)
+ executionRequest.CronJobTemplate = setStringField(executionRequest.CronJobTemplate, stepRequest.CronJobTemplate)
+ executionRequest.CronJobTemplateReference = setStringField(executionRequest.CronJobTemplateReference, stepRequest.CronJobTemplateReference)
+ executionRequest.JobTemplate = setStringField(executionRequest.JobTemplate, stepRequest.JobTemplate)
+ executionRequest.JobTemplateReference = setStringField(executionRequest.JobTemplateReference, stepRequest.JobTemplateReference)
+ executionRequest.ScraperTemplate = setStringField(executionRequest.ScraperTemplate, stepRequest.ScraperTemplate)
+ executionRequest.ScraperTemplateReference = setStringField(executionRequest.ScraperTemplateReference, stepRequest.ScraperTemplateReference)
+ executionRequest.PvcTemplate = setStringField(executionRequest.PvcTemplate, stepRequest.PvcTemplate)
+ executionRequest.PvcTemplateReference = setStringField(executionRequest.PvcTemplate, stepRequest.PvcTemplateReference)
+
+ if stepRequest.RunningContext != nil {
+ executionRequest.RunningContext = &testkube.RunningContext{
+ Type_: string(stepRequest.RunningContext.Type_),
+ Context: stepRequest.RunningContext.Context,
+ }
+ }
+
+ return executionRequest
+}
+
+func setStringField(oldValue string, newValue string) string {
+ if newValue != "" {
+ return newValue
+ }
+ return oldValue
+}
diff --git a/pkg/secret/client.go b/pkg/secret/client.go
index 43948c1c1ad..3f3b6ff9b6f 100644
--- a/pkg/secret/client.go
+++ b/pkg/secret/client.go
@@ -18,10 +18,10 @@ const testkubeTestSecretLabel = "tests-secrets"
//go:generate mockgen -destination=./mock_client.go -package=secret "github.com/kubeshop/testkube/pkg/secret" Interface
type Interface interface {
- Get(id string) (map[string]string, error)
+ Get(id string, namespace ...string) (map[string]string, error)
GetObject(id string) (*v1.Secret, error)
List(all bool) (map[string]map[string]string, error)
- Create(id string, labels, stringData map[string]string) error
+ Create(id string, labels, stringData map[string]string, namespace ...string) error
Apply(id string, labels, stringData map[string]string) error
Update(id string, labels, stringData map[string]string) error
Delete(id string) error
@@ -50,8 +50,13 @@ func NewClient(namespace string) (*Client, error) {
}
// Get is a method to retrieve an existing secret
-func (c *Client) Get(id string) (map[string]string, error) {
- secretsClient := c.ClientSet.CoreV1().Secrets(c.Namespace)
+func (c *Client) Get(id string, namespace ...string) (map[string]string, error) {
+ ns := c.Namespace
+ if len(namespace) != 0 {
+ ns = namespace[0]
+ }
+
+ secretsClient := c.ClientSet.CoreV1().Secrets(ns)
ctx := context.Background()
secretSpec, err := secretsClient.Get(ctx, id, metav1.GetOptions{})
@@ -110,11 +115,16 @@ func (c *Client) List(all bool) (map[string]map[string]string, error) {
}
// Create is a method to create new secret
-func (c *Client) Create(id string, labels, stringData map[string]string) error {
- secretsClient := c.ClientSet.CoreV1().Secrets(c.Namespace)
+func (c *Client) Create(id string, labels, stringData map[string]string, namespace ...string) error {
+ ns := c.Namespace
+ if len(namespace) != 0 {
+ ns = namespace[0]
+ }
+
+ secretsClient := c.ClientSet.CoreV1().Secrets(ns)
ctx := context.Background()
- secretSpec := NewSpec(id, c.Namespace, labels, stringData)
+ secretSpec := NewSpec(id, ns, labels, stringData)
if _, err := secretsClient.Create(ctx, secretSpec, metav1.CreateOptions{}); err != nil {
return err
}
diff --git a/pkg/secret/mock_client.go b/pkg/secret/mock_client.go
index 41559e44496..90bbe750c35 100644
--- a/pkg/secret/mock_client.go
+++ b/pkg/secret/mock_client.go
@@ -49,17 +49,22 @@ func (mr *MockInterfaceMockRecorder) Apply(arg0, arg1, arg2 interface{}) *gomock
}
// Create mocks base method.
-func (m *MockInterface) Create(arg0 string, arg1, arg2 map[string]string) error {
+func (m *MockInterface) Create(arg0 string, arg1, arg2 map[string]string, arg3 ...string) error {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Create", arg0, arg1, arg2)
+ varargs := []interface{}{arg0, arg1, arg2}
+ for _, a := range arg3 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Create", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Create indicates an expected call of Create.
-func (mr *MockInterfaceMockRecorder) Create(arg0, arg1, arg2 interface{}) *gomock.Call {
+func (mr *MockInterfaceMockRecorder) Create(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockInterface)(nil).Create), arg0, arg1, arg2)
+ varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockInterface)(nil).Create), varargs...)
}
// Delete mocks base method.
@@ -91,18 +96,23 @@ func (mr *MockInterfaceMockRecorder) DeleteAll(arg0 interface{}) *gomock.Call {
}
// Get mocks base method.
-func (m *MockInterface) Get(arg0 string) (map[string]string, error) {
+func (m *MockInterface) Get(arg0 string, arg1 ...string) (map[string]string, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Get", arg0)
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Get", varargs...)
ret0, _ := ret[0].(map[string]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get.
-func (mr *MockInterfaceMockRecorder) Get(arg0 interface{}) *gomock.Call {
+func (mr *MockInterfaceMockRecorder) Get(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), arg0)
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), varargs...)
}
// GetObject mocks base method.
diff --git a/pkg/skopeo/client.go b/pkg/skopeo/client.go
index 7bb9a31c052..6d027ed1857 100644
--- a/pkg/skopeo/client.go
+++ b/pkg/skopeo/client.go
@@ -38,8 +38,10 @@ type DockerAuthConfig struct {
// DockerImage contains definition of docker image
type DockerImage struct {
Config struct {
+ User string `json:"User"`
Entrypoint []string `json:"Entrypoint"`
Cmd []string `json:"Cmd"`
+ WorkingDir string `json:"WorkingDir"`
} `json:"config"`
History []struct {
Created time.Time `json:"created"`
@@ -83,7 +85,7 @@ func (c *client) Inspect(image string) (*DockerImage, error) {
}
if len(c.dockerAuthConfigs) != 0 {
- i := 1 + rand.Intn(len(c.dockerAuthConfigs))
+ i := rand.Intn(len(c.dockerAuthConfigs))
args = append(args, "--creds", c.dockerAuthConfigs[i].Username+":"+c.dockerAuthConfigs[i].Password)
}
diff --git a/pkg/slack/slack.go b/pkg/slack/slack.go
index b703ceaae0b..c2bdc74e5e6 100644
--- a/pkg/slack/slack.go
+++ b/pkg/slack/slack.go
@@ -47,7 +47,7 @@ func NewNotifier(template, clusterName, dashboardURI string, config []Notificati
notifier := Notifier{messageTemplate: template, clusterName: clusterName, dashboardURI: dashboardURI,
config: NewConfig(config), envs: envs}
notifier.timestamps = make(map[string]string)
- if token, ok := os.LookupEnv("SLACK_TOKEN"); ok {
+ if token, ok := os.LookupEnv("SLACK_TOKEN"); ok && token != "" {
log.DefaultLogger.Infow("initializing slack client", "SLACK_TOKEN", text.Obfuscate(token))
notifier.client = slack.New(token, slack.OptionDebug(true))
notifier.Ready = true
@@ -221,17 +221,25 @@ func (s *Notifier) composeTestMessage(execution *testkube.Execution, eventType t
Labels: testkube.MapToString(execution.Labels),
TestName: execution.TestName,
TestType: execution.TestType,
- Status: string(*execution.ExecutionResult.Status),
+ Status: string(testkube.QUEUED_ExecutionStatus),
StartTime: execution.StartTime.String(),
EndTime: execution.EndTime.String(),
Duration: execution.Duration,
- TotalSteps: len(execution.ExecutionResult.Steps),
- FailedSteps: execution.ExecutionResult.FailedStepsCount(),
+ TotalSteps: 0,
+ FailedSteps: 0,
ClusterName: s.clusterName,
DashboardURI: s.dashboardURI,
Envs: s.envs,
}
+ if execution.ExecutionResult != nil {
+ if execution.ExecutionResult.Status != nil {
+ args.Status = string(*execution.ExecutionResult.Status)
+ }
+ args.TotalSteps = len(execution.ExecutionResult.Steps)
+ args.FailedSteps = execution.ExecutionResult.FailedStepsCount()
+ }
+
log.DefaultLogger.Infow("Execution changed", "status", execution.ExecutionResult.Status)
var message bytes.Buffer
diff --git a/pkg/storage/artifacts.go b/pkg/storage/artifacts.go
index 3e2fe176c6b..9ce0815dc43 100644
--- a/pkg/storage/artifacts.go
+++ b/pkg/storage/artifacts.go
@@ -10,9 +10,9 @@ import (
//go:generate mockgen -destination=./artifacts_mock.go -package=storage "github.com/kubeshop/testkube/pkg/storage" ArtifactsStorage
type ArtifactsStorage interface {
// ListFiles lists available files in the configured bucket
- ListFiles(ctx context.Context, executionId, testName, testSuiteName string) ([]testkube.Artifact, error)
+ ListFiles(ctx context.Context, executionId, testName, testSuiteName, testWorkflowName string) ([]testkube.Artifact, error)
// DownloadFile downloads file from configured
- DownloadFile(ctx context.Context, file, executionId, testName, testSuiteName string) (io.Reader, error)
+ DownloadFile(ctx context.Context, file, executionId, testName, testSuiteName, testWorkflowName string) (io.Reader, error)
// DownloadArchive downloads archive from configured
DownloadArchive(ctx context.Context, executionId string, masks []string) (io.Reader, error)
// UploadFile uploads file to configured bucket
diff --git a/pkg/storage/artifacts_mock.go b/pkg/storage/artifacts_mock.go
index 8a62c76869e..19db9eef45f 100644
--- a/pkg/storage/artifacts_mock.go
+++ b/pkg/storage/artifacts_mock.go
@@ -10,7 +10,6 @@ import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
-
testkube "github.com/kubeshop/testkube/pkg/api/v1/testkube"
)
@@ -53,18 +52,18 @@ func (mr *MockArtifactsStorageMockRecorder) DownloadArchive(arg0, arg1, arg2 int
}
// DownloadFile mocks base method.
-func (m *MockArtifactsStorage) DownloadFile(arg0 context.Context, arg1, arg2, arg3, arg4 string) (io.Reader, error) {
+func (m *MockArtifactsStorage) DownloadFile(arg0 context.Context, arg1, arg2, arg3, arg4, arg5 string) (io.Reader, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DownloadFile", arg0, arg1, arg2, arg3, arg4)
+ ret := m.ctrl.Call(m, "DownloadFile", arg0, arg1, arg2, arg3, arg4, arg5)
ret0, _ := ret[0].(io.Reader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DownloadFile indicates an expected call of DownloadFile.
-func (mr *MockArtifactsStorageMockRecorder) DownloadFile(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+func (mr *MockArtifactsStorageMockRecorder) DownloadFile(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadFile", reflect.TypeOf((*MockArtifactsStorage)(nil).DownloadFile), arg0, arg1, arg2, arg3, arg4)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadFile", reflect.TypeOf((*MockArtifactsStorage)(nil).DownloadFile), arg0, arg1, arg2, arg3, arg4, arg5)
}
// GetValidBucketName mocks base method.
@@ -82,18 +81,18 @@ func (mr *MockArtifactsStorageMockRecorder) GetValidBucketName(arg0, arg1 interf
}
// ListFiles mocks base method.
-func (m *MockArtifactsStorage) ListFiles(arg0 context.Context, arg1, arg2, arg3 string) ([]testkube.Artifact, error) {
+func (m *MockArtifactsStorage) ListFiles(arg0 context.Context, arg1, arg2, arg3, arg4 string) ([]testkube.Artifact, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListFiles", arg0, arg1, arg2, arg3)
+ ret := m.ctrl.Call(m, "ListFiles", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].([]testkube.Artifact)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListFiles indicates an expected call of ListFiles.
-func (mr *MockArtifactsStorageMockRecorder) ListFiles(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+func (mr *MockArtifactsStorageMockRecorder) ListFiles(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFiles", reflect.TypeOf((*MockArtifactsStorage)(nil).ListFiles), arg0, arg1, arg2, arg3)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFiles", reflect.TypeOf((*MockArtifactsStorage)(nil).ListFiles), arg0, arg1, arg2, arg3, arg4)
}
// PlaceFiles mocks base method.
diff --git a/pkg/storage/minio/artifacts_storage.go b/pkg/storage/minio/artifacts_storage.go
index cd96ec4822b..36b3ec99cff 100644
--- a/pkg/storage/minio/artifacts_storage.go
+++ b/pkg/storage/minio/artifacts_storage.go
@@ -18,12 +18,12 @@ func NewMinIOArtifactClient(client storage.Client) *ArtifactClient {
}
// ListFiles lists available files in the bucket from the config
-func (c *ArtifactClient) ListFiles(ctx context.Context, executionId, testName, testSuiteName string) ([]testkube.Artifact, error) {
+func (c *ArtifactClient) ListFiles(ctx context.Context, executionId, testName, testSuiteName, testWorkflowName string) ([]testkube.Artifact, error) {
return c.client.ListFiles(ctx, executionId)
}
// DownloadFile downloads file from bucket from the config
-func (c *ArtifactClient) DownloadFile(ctx context.Context, file, executionId, testName, testSuiteName string) (io.Reader, error) {
+func (c *ArtifactClient) DownloadFile(ctx context.Context, file, executionId, testName, testSuiteName, testWorkflowName string) (io.Reader, error) {
return c.client.DownloadFile(ctx, executionId, file)
}
diff --git a/pkg/storage/minio/artifacts_storage_integration_test.go b/pkg/storage/minio/artifacts_storage_integration_test.go
index f14f880fda1..85712de8b64 100644
--- a/pkg/storage/minio/artifacts_storage_integration_test.go
+++ b/pkg/storage/minio/artifacts_storage_integration_test.go
@@ -46,7 +46,7 @@ func TestArtifactClient(t *testing.T) {
t.Fatalf("unable to upload file: %v", err)
}
// Call ListFiles
- files, err := artifactClient.ListFiles(ctx, "test-execution-id-1", "", "")
+ files, err := artifactClient.ListFiles(ctx, "test-execution-id-1", "", "", "")
assert.NoError(t, err)
assert.Lenf(t, files, 1, "expected 1 file to be returned")
@@ -63,7 +63,7 @@ func TestArtifactClient(t *testing.T) {
t.Fatalf("unable to upload file: %v", err)
}
- reader, err := artifactClient.DownloadFile(ctx, "test-file", "test-execution-id-2", "", "")
+ reader, err := artifactClient.DownloadFile(ctx, "test-file", "test-execution-id-2", "", "", "")
if err != nil {
t.Fatalf("unable to download file: %v", err)
}
diff --git a/pkg/storage/minio/minio.go b/pkg/storage/minio/minio.go
index 8078d90857e..ffc48e85882 100644
--- a/pkg/storage/minio/minio.go
+++ b/pkg/storage/minio/minio.go
@@ -3,8 +3,6 @@ package minio
import (
"bytes"
"context"
- "crypto/tls"
- "crypto/x509"
"fmt"
"hash/fnv"
"io"
@@ -12,11 +10,11 @@ import (
"path/filepath"
"regexp"
"strings"
+ "time"
"github.com/pkg/errors"
"github.com/minio/minio-go/v7"
- "github.com/minio/minio-go/v7/pkg/credentials"
"github.com/minio/minio-go/v7/pkg/lifecycle"
"go.uber.org/zap"
@@ -35,89 +33,20 @@ var ErrArtifactsNotFound = errors.New("Execution doesn't have any artifacts asso
// Client for managing MinIO storage server
type Client struct {
- Endpoint string
- accessKeyID string
- secretAccessKey string
- ssl bool
- region string
- token string
- bucket string
- opts []Option
- minioclient *minio.Client
- tlsConfig *tls.Config
- Log *zap.SugaredLogger
-}
-
-type Option func(*Client) error
-
-// Insecure is an Option to enable TLS secure connections that skip server verification.
-func Insecure() Option {
- return func(o *Client) error {
- if o.tlsConfig == nil {
- o.tlsConfig = &tls.Config{MinVersion: tls.VersionTLS12}
- }
- o.tlsConfig.InsecureSkipVerify = true
- o.ssl = true
- return nil
- }
-}
-
-// RootCAs is a helper option to provide the RootCAs pool from a list of filenames.
-// If Secure is not already set this will set it as well.
-func RootCAs(file ...string) Option {
- return func(o *Client) error {
- pool := x509.NewCertPool()
- for _, f := range file {
- rootPEM, err := os.ReadFile(f)
- if err != nil || rootPEM == nil {
- return fmt.Errorf("nats: error loading or parsing rootCA file: %v", err)
- }
- ok := pool.AppendCertsFromPEM(rootPEM)
- if !ok {
- return fmt.Errorf("nats: failed to parse root certificate from %q", f)
- }
- }
- if o.tlsConfig == nil {
- o.tlsConfig = &tls.Config{MinVersion: tls.VersionTLS12}
- }
- o.tlsConfig.RootCAs = pool
- o.ssl = true
- return nil
- }
-}
-
-// ClientCert is a helper option to provide the client certificate from a file.
-// If Secure is not already set this will set it as well.
-func ClientCert(certFile, keyFile string) Option {
- return func(o *Client) error {
- cert, err := tls.LoadX509KeyPair(certFile, keyFile)
- if err != nil {
- return fmt.Errorf("nats: error loading client certificate: %v", err)
- }
- cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
- if err != nil {
- return fmt.Errorf("nats: error parsing client certificate: %v", err)
- }
- if o.tlsConfig == nil {
- o.tlsConfig = &tls.Config{MinVersion: tls.VersionTLS12}
- }
- o.tlsConfig.Certificates = []tls.Certificate{cert}
- o.ssl = true
- return nil
- }
+ region string
+ bucket string
+ minioClient *minio.Client
+ Log *zap.SugaredLogger
+ minioConnecter *Connecter
}
// NewClient returns new MinIO client
func NewClient(endpoint, accessKeyID, secretAccessKey, region, token, bucket string, opts ...Option) *Client {
c := &Client{
- region: region,
- accessKeyID: accessKeyID,
- secretAccessKey: secretAccessKey,
- token: token,
- bucket: bucket,
- Endpoint: endpoint,
- opts: opts,
- Log: log.DefaultLogger,
+ minioConnecter: NewConnecter(endpoint, accessKeyID, secretAccessKey, region, token, bucket, log.DefaultLogger, opts...),
+ region: region,
+ bucket: bucket,
+ Log: log.DefaultLogger,
}
return c
@@ -125,47 +54,13 @@ func NewClient(endpoint, accessKeyID, secretAccessKey, region, token, bucket str
// Connect connects to MinIO server
func (c *Client) Connect() error {
- for _, opt := range c.opts {
- if err := opt(c); err != nil {
- return errors.Wrapf(err, "error connecting to server")
- }
- }
- creds := credentials.NewIAM("")
- c.Log.Debugw("connecting to server",
- "endpoint", c.Endpoint,
- "accessKeyID", c.accessKeyID,
- "region", c.region,
- "token", c.token,
- "bucket", c.bucket,
- "ssl", c.ssl)
- if c.accessKeyID != "" && c.secretAccessKey != "" {
- creds = credentials.NewStaticV4(c.accessKeyID, c.secretAccessKey, c.token)
- }
- transport, err := minio.DefaultTransport(c.ssl)
- if err != nil {
- c.Log.Errorw("error creating minio transport", "error", err)
- return err
- }
- transport.TLSClientConfig = c.tlsConfig
- opts := &minio.Options{
- Creds: creds,
- Secure: c.ssl,
- Transport: transport,
- }
- if c.region != "" {
- opts.Region = c.region
- }
- mclient, err := minio.New(c.Endpoint, opts)
- if err != nil {
- c.Log.Errorw("error connecting to minio", "error", err)
- return err
- }
- c.minioclient = mclient
+ var err error
+ c.minioClient, err = c.minioConnecter.GetClient()
return err
}
func (c *Client) SetExpirationPolicy(expirationDays int) error {
- if expirationDays != 0 && c.minioclient != nil {
+ if expirationDays != 0 && c.minioClient != nil {
lifecycleConfig := &lifecycle.Configuration{
Rules: []lifecycle.Rule{
{
@@ -177,7 +72,7 @@ func (c *Client) SetExpirationPolicy(expirationDays int) error {
},
},
}
- return c.minioclient.SetBucketLifecycle(context.TODO(), c.bucket, lifecycleConfig)
+ return c.minioClient.SetBucketLifecycle(context.TODO(), c.bucket, lifecycleConfig)
}
return nil
}
@@ -187,11 +82,11 @@ func (c *Client) CreateBucket(ctx context.Context, bucket string) error {
if err := c.Connect(); err != nil {
return err
}
- err := c.minioclient.MakeBucket(ctx, bucket, minio.MakeBucketOptions{Region: c.region})
+ err := c.minioClient.MakeBucket(ctx, bucket, minio.MakeBucketOptions{Region: c.region})
if err != nil {
c.Log.Errorw("error creating bucket", "error", err)
// Check to see if we already own this bucket (which happens if you run this twice)
- exists, errBucketExists := c.minioclient.BucketExists(ctx, bucket)
+ exists, errBucketExists := c.minioClient.BucketExists(ctx, bucket)
if errBucketExists == nil && exists {
return fmt.Errorf("bucket %q already exists", bucket)
} else {
@@ -206,7 +101,7 @@ func (c *Client) DeleteBucket(ctx context.Context, bucket string, force bool) er
if err := c.Connect(); err != nil {
return err
}
- return c.minioclient.RemoveBucketWithOptions(ctx, bucket, minio.RemoveBucketOptions{ForceDelete: force})
+ return c.minioClient.RemoveBucketWithOptions(ctx, bucket, minio.RemoveBucketOptions{ForceDelete: force})
}
// ListBuckets lists available buckets
@@ -215,7 +110,7 @@ func (c *Client) ListBuckets(ctx context.Context) ([]string, error) {
return nil, err
}
var toReturn []string
- if buckets, err := c.minioclient.ListBuckets(ctx); err != nil {
+ if buckets, err := c.minioClient.ListBuckets(ctx); err != nil {
return nil, err
} else {
for _, bucket := range buckets {
@@ -232,7 +127,7 @@ func (c *Client) listFiles(ctx context.Context, bucket, bucketFolder string) ([]
}
var toReturn []testkube.Artifact
- exists, err := c.minioclient.BucketExists(ctx, bucket)
+ exists, err := c.minioClient.BucketExists(ctx, bucket)
if err != nil {
return nil, err
}
@@ -246,7 +141,7 @@ func (c *Client) listFiles(ctx context.Context, bucket, bucketFolder string) ([]
listOptions.Prefix = bucketFolder
}
- for obj := range c.minioclient.ListObjects(ctx, bucket, listOptions) {
+ for obj := range c.minioClient.ListObjects(ctx, bucket, listOptions) {
if obj.Err != nil {
return nil, obj.Err
}
@@ -264,7 +159,7 @@ func (c *Client) ListFiles(ctx context.Context, bucketFolder string) ([]testkube
c.Log.Infow("listing files", "bucket", c.bucket, "bucketFolder", bucketFolder)
// TODO: this is for back compatibility, remove it sometime in the future
if bucketFolder != "" {
- if exist, err := c.minioclient.BucketExists(ctx, bucketFolder); err == nil && exist {
+ if exist, err := c.minioClient.BucketExists(ctx, bucketFolder); err == nil && exist {
formerResult, err := c.listFiles(ctx, bucketFolder, "")
if err == nil && len(formerResult) > 0 {
return formerResult, nil
@@ -291,7 +186,7 @@ func (c *Client) saveFile(ctx context.Context, bucket, bucketFolder, filePath st
return fmt.Errorf("minio object stat (file:%s) error: %w", filePath, err)
}
- exists, err := c.minioclient.BucketExists(ctx, bucket)
+ exists, err := c.minioClient.BucketExists(ctx, bucket)
if err != nil || !exists {
err := c.CreateBucket(ctx, bucket)
if err != nil {
@@ -305,7 +200,7 @@ func (c *Client) saveFile(ctx context.Context, bucket, bucketFolder, filePath st
}
c.Log.Debugw("saving object in minio", "filePath", filePath, "fileName", fileName, "bucket", bucket, "size", objectStat.Size())
- _, err = c.minioclient.PutObject(ctx, bucket, fileName, object, objectStat.Size(), minio.PutObjectOptions{ContentType: "application/octet-stream"})
+ _, err = c.minioClient.PutObject(ctx, bucket, fileName, object, objectStat.Size(), minio.PutObjectOptions{ContentType: "application/octet-stream"})
if err != nil {
return fmt.Errorf("minio saving file (%s) put object error: %w", fileName, err)
}
@@ -314,7 +209,7 @@ func (c *Client) saveFile(ctx context.Context, bucket, bucketFolder, filePath st
}
func (c *Client) SaveFileDirect(ctx context.Context, folder, file string, data io.Reader, size int64, opts minio.PutObjectOptions) error {
- exists, err := c.minioclient.BucketExists(ctx, c.bucket)
+ exists, err := c.minioClient.BucketExists(ctx, c.bucket)
if err != nil {
return errors.Wrapf(err, "error checking does bucket %s exists", c.bucket)
}
@@ -333,7 +228,7 @@ func (c *Client) SaveFileDirect(ctx context.Context, folder, file string, data i
opts.ContentType = "application/octet-stream"
}
c.Log.Debugw("saving object in minio", "filename", filename, "bucket", c.bucket, "size", size)
- _, err = c.minioclient.PutObject(ctx, c.bucket, filename, data, size, opts)
+ _, err = c.minioClient.PutObject(ctx, c.bucket, filename, data, size, opts)
if err != nil {
return errors.Wrapf(err, "minio saving file (%s) put object error", filename)
}
@@ -354,7 +249,7 @@ func (c *Client) downloadFile(ctx context.Context, bucket, bucketFolder, file st
return nil, fmt.Errorf("minio DownloadFile .Connect error: %w", err)
}
- exists, err := c.minioclient.BucketExists(ctx, bucket)
+ exists, err := c.minioClient.BucketExists(ctx, bucket)
if err != nil {
return nil, err
}
@@ -368,7 +263,7 @@ func (c *Client) downloadFile(ctx context.Context, bucket, bucketFolder, file st
file = strings.Trim(bucketFolder, "/") + "/" + file
}
- reader, err := c.minioclient.GetObject(ctx, bucket, file, minio.GetObjectOptions{})
+ reader, err := c.minioClient.GetObject(ctx, bucket, file, minio.GetObjectOptions{})
if err != nil {
return nil, fmt.Errorf("minio DownloadFile GetObject error: %w", err)
}
@@ -388,7 +283,7 @@ func (c *Client) DownloadFile(ctx context.Context, bucketFolder, file string) (*
var objFirst *minio.Object
var errFirst error
if bucketFolder != "" {
- exists, err := c.minioclient.BucketExists(ctx, bucketFolder)
+ exists, err := c.minioClient.BucketExists(ctx, bucketFolder)
c.Log.Debugw("Checking if bucket exists", exists, err)
if err == nil && exists {
c.Log.Infow("Bucket exists, trying to get files from former bucket per execution", exists, err)
@@ -412,7 +307,7 @@ func (c *Client) downloadArchive(ctx context.Context, bucket, bucketFolder strin
return nil, fmt.Errorf("minio DownloadArchive .Connect error: %w", err)
}
- exists, err := c.minioclient.BucketExists(ctx, bucket)
+ exists, err := c.minioClient.BucketExists(ctx, bucket)
if err != nil {
return nil, err
}
@@ -441,7 +336,7 @@ func (c *Client) downloadArchive(ctx context.Context, bucket, bucketFolder strin
}
var files []*archive.File
- for obj := range c.minioclient.ListObjects(ctx, bucket, listOptions) {
+ for obj := range c.minioClient.ListObjects(ctx, bucket, listOptions) {
if obj.Err != nil {
return nil, fmt.Errorf("minio DownloadArchive ListObjects error: %w", obj.Err)
}
@@ -466,7 +361,7 @@ func (c *Client) downloadArchive(ctx context.Context, bucket, bucketFolder strin
}
for i := range files {
- reader, err := c.minioclient.GetObject(ctx, bucket, files[i].Name, minio.GetObjectOptions{})
+ reader, err := c.minioClient.GetObject(ctx, bucket, files[i].Name, minio.GetObjectOptions{})
if err != nil {
return nil, fmt.Errorf("minio DownloadArchive GetObject error: %w", err)
}
@@ -497,7 +392,7 @@ func (c *Client) DownloadArchive(ctx context.Context, bucketFolder string, masks
var objFirst io.Reader
var errFirst error
if bucketFolder != "" {
- exists, err := c.minioclient.BucketExists(ctx, bucketFolder)
+ exists, err := c.minioClient.BucketExists(ctx, bucketFolder)
c.Log.Debugw("Checking if bucket exists", exists, err)
if err == nil && exists {
c.Log.Infow("Bucket exists, trying to get archive from former bucket per execution", exists, err)
@@ -515,9 +410,19 @@ func (c *Client) DownloadArchive(ctx context.Context, bucketFolder string, masks
}
// DownloadFileFromBucket downloads file from given bucket
-func (c *Client) DownloadFileFromBucket(ctx context.Context, bucket, bucketFolder, file string) (io.Reader, error) {
+func (c *Client) DownloadFileFromBucket(ctx context.Context, bucket, bucketFolder, file string) (io.Reader, minio.ObjectInfo, error) {
c.Log.Debugw("Downloading file", "bucket", bucket, "bucketFolder", bucketFolder, "file", file)
- return c.downloadFile(ctx, bucket, bucketFolder, file)
+ object, err := c.downloadFile(ctx, bucket, bucketFolder, file)
+ if err != nil {
+ return nil, minio.ObjectInfo{}, err
+ }
+
+ info, err := object.Stat()
+ if err != nil {
+ return nil, minio.ObjectInfo{}, err
+ }
+
+ return object, info, nil
}
// DownloadArrchiveFromBucket downloads archive from given bucket
@@ -568,7 +473,7 @@ func (c *Client) uploadFile(ctx context.Context, bucket, bucketFolder, filePath
return fmt.Errorf("minio UploadFile connection error: %w", err)
}
- exists, err := c.minioclient.BucketExists(ctx, bucket)
+ exists, err := c.minioClient.BucketExists(ctx, bucket)
if err != nil {
return fmt.Errorf("could not check if bucket already exists for copy files: %w", err)
}
@@ -586,7 +491,7 @@ func (c *Client) uploadFile(ctx context.Context, bucket, bucketFolder, filePath
}
c.Log.Debugw("saving object in minio", "file", filePath, "bucket", bucket)
- _, err = c.minioclient.PutObject(ctx, bucket, filePath, reader, objectSize, minio.PutObjectOptions{ContentType: "application/octet-stream"})
+ _, err = c.minioClient.PutObject(ctx, bucket, filePath, reader, objectSize, minio.PutObjectOptions{ContentType: "application/octet-stream"})
if err != nil {
return fmt.Errorf("minio saving file (%s) put object error: %w", filePath, err)
}
@@ -611,7 +516,7 @@ func (c *Client) PlaceFiles(ctx context.Context, bucketFolders []string, prefix
output.PrintLog(fmt.Sprintf("%s Minio PlaceFiles connection error: %s", ui.IconWarning, err.Error()))
return fmt.Errorf("minio PlaceFiles connection error: %w", err)
}
- exists, err := c.minioclient.BucketExists(ctx, c.bucket)
+ exists, err := c.minioClient.BucketExists(ctx, c.bucket)
if err != nil {
output.PrintLog(fmt.Sprintf("%s Could not check if bucket already exists for files %s", ui.IconWarning, err.Error()))
return fmt.Errorf("could not check if bucket already exists for files: %w", err)
@@ -644,7 +549,7 @@ func (c *Client) PlaceFiles(ctx context.Context, bucketFolders []string, prefix
}
path := filepath.Join(prefix, f.Name)
- err = c.minioclient.FGetObject(ctx, c.bucket, objectName, path, minio.GetObjectOptions{})
+ err = c.minioClient.FGetObject(ctx, c.bucket, objectName, path, minio.GetObjectOptions{})
if err != nil {
output.PrintEvent(fmt.Sprintf("%s Could not download file %s", ui.IconCross, f.Name))
return fmt.Errorf("could not persist file %s from bucket %s, folder %s: %w", f.Name, c.bucket, folder, err)
@@ -673,7 +578,7 @@ func (c *Client) deleteFile(ctx context.Context, bucket, bucketFolder, file stri
return fmt.Errorf("minio DeleteFile connection error: %w", err)
}
- exists, err := c.minioclient.BucketExists(ctx, bucket)
+ exists, err := c.minioClient.BucketExists(ctx, bucket)
if err != nil {
return fmt.Errorf("could not check if bucket already exists for delete file: %w", err)
}
@@ -687,7 +592,7 @@ func (c *Client) deleteFile(ctx context.Context, bucket, bucketFolder, file stri
file = strings.Trim(bucketFolder, "/") + "/" + file
}
- err = c.minioclient.RemoveObject(ctx, bucket, file, minio.RemoveObjectOptions{ForceDelete: true})
+ err = c.minioClient.RemoveObject(ctx, bucket, file, minio.RemoveObjectOptions{ForceDelete: true})
if err != nil {
return fmt.Errorf("minio DeleteFile RemoveObject error: %w", err)
}
@@ -700,7 +605,7 @@ func (c *Client) DeleteFile(ctx context.Context, bucketFolder, file string) erro
// TODO: this is for back compatibility, remove it sometime in the future
var errFirst error
if bucketFolder != "" {
- if exist, err := c.minioclient.BucketExists(ctx, bucketFolder); err != nil || !exist {
+ if exist, err := c.minioClient.BucketExists(ctx, bucketFolder); err != nil || !exist {
errFirst = c.DeleteFileFromBucket(ctx, bucketFolder, "", file)
if err == nil {
return nil
@@ -728,3 +633,33 @@ func (c *Client) IsConnectionPossible(ctx context.Context) (bool, error) {
return true, nil
}
+
+func (c *Client) PresignDownloadFileFromBucket(ctx context.Context, bucket, bucketFolder, file string, expires time.Duration) (string, error) {
+ if err := c.Connect(); err != nil {
+ return "", err
+ }
+ if bucketFolder != "" {
+ file = strings.Trim(bucketFolder, "/") + "/" + file
+ }
+ c.Log.Debugw("presigning get object from minio", "file", file, "bucket", bucket)
+ url, err := c.minioClient.PresignedPutObject(ctx, bucket, file, expires)
+ if err != nil {
+ return "", err
+ }
+ return url.String(), nil
+}
+
+func (c *Client) PresignUploadFileToBucket(ctx context.Context, bucket, bucketFolder, filePath string, expires time.Duration) (string, error) {
+ if err := c.Connect(); err != nil {
+ return "", err
+ }
+ if bucketFolder != "" {
+ filePath = strings.Trim(bucketFolder, "/") + "/" + filePath
+ }
+ c.Log.Debugw("presigning put object in minio", "file", filePath, "bucket", bucket)
+ url, err := c.minioClient.PresignedPutObject(ctx, bucket, filePath, expires)
+ if err != nil {
+ return "", err
+ }
+ return url.String(), nil
+}
diff --git a/pkg/storage/minio/minio_connecter.go b/pkg/storage/minio/minio_connecter.go
new file mode 100644
index 00000000000..1a2df483e12
--- /dev/null
+++ b/pkg/storage/minio/minio_connecter.go
@@ -0,0 +1,147 @@
+package minio
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "fmt"
+ "os"
+
+ "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/credentials"
+ "github.com/pkg/errors"
+ "go.uber.org/zap"
+)
+
+type Option func(*Connecter) error
+
+// Insecure is an Option to enable TLS secure connections that skip server verification.
+func Insecure() Option {
+ return func(o *Connecter) error {
+ if o.TlsConfig == nil {
+ o.TlsConfig = &tls.Config{MinVersion: tls.VersionTLS12}
+ }
+ o.TlsConfig.InsecureSkipVerify = true
+ o.Ssl = true
+ return nil
+ }
+}
+
+// RootCAs is a helper option to provide the RootCAs pool from a list of filenames.
+// If Secure is not already set this will set it as well.
+func RootCAs(file ...string) Option {
+ return func(o *Connecter) error {
+ pool := x509.NewCertPool()
+ for _, f := range file {
+ rootPEM, err := os.ReadFile(f)
+ if err != nil || rootPEM == nil {
+ return fmt.Errorf("minio: error loading or parsing rootCA file: %v", err)
+ }
+ ok := pool.AppendCertsFromPEM(rootPEM)
+ if !ok {
+ return fmt.Errorf("minio: failed to parse root certificate from %q", f)
+ }
+ }
+ if o.TlsConfig == nil {
+ o.TlsConfig = &tls.Config{MinVersion: tls.VersionTLS12}
+ }
+ o.TlsConfig.RootCAs = pool
+ o.Ssl = true
+ return nil
+ }
+}
+
+// ClientCert is a helper option to provide the client certificate from a file.
+// If Secure is not already set this will set it as well.
+func ClientCert(certFile, keyFile string) Option {
+ return func(o *Connecter) error {
+ cert, err := tls.LoadX509KeyPair(certFile, keyFile)
+ if err != nil {
+ return fmt.Errorf("minio: error loading client certificate: %v", err)
+ }
+ cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
+ if err != nil {
+ return fmt.Errorf("minio: error parsing client certificate: %v", err)
+ }
+ if o.TlsConfig == nil {
+ o.TlsConfig = &tls.Config{MinVersion: tls.VersionTLS12}
+ }
+ o.TlsConfig.Certificates = []tls.Certificate{cert}
+ o.Ssl = true
+ return nil
+ }
+}
+
+type Connecter struct {
+ Endpoint string
+ AccessKeyID string
+ SecretAccessKey string
+ Region string
+ Token string
+ Bucket string
+ Ssl bool
+ TlsConfig *tls.Config
+ Opts []Option
+ Log *zap.SugaredLogger
+ client *minio.Client
+}
+
+// NewConnecter creates a new Connecter
+func NewConnecter(endpoint, accessKeyID, secretAccessKey, region, token, bucket string, log *zap.SugaredLogger, opts ...Option) *Connecter {
+ c := &Connecter{
+ Endpoint: endpoint,
+ AccessKeyID: accessKeyID,
+ SecretAccessKey: secretAccessKey,
+ Region: region,
+ Token: token,
+ Bucket: bucket,
+ Opts: opts,
+ Log: log,
+ }
+ return c
+}
+
+// GetClient() connects to MinIO
+func (c *Connecter) GetClient() (*minio.Client, error) {
+ if c.client != nil {
+ return c.client, nil
+ }
+
+ for _, opt := range c.Opts {
+ if err := opt(c); err != nil {
+ return nil, errors.Wrapf(err, "error connecting to server")
+ }
+ }
+ creds := credentials.NewIAM("")
+ c.Log.Debugw("connecting to server",
+ "endpoint", c.Endpoint,
+ "accessKeyID", c.AccessKeyID,
+ "region", c.Region,
+ "token", c.Token,
+ "bucket", c.Bucket,
+ "ssl", c.Ssl)
+ if c.AccessKeyID != "" && c.SecretAccessKey != "" {
+ creds = credentials.NewStaticV4(c.AccessKeyID, c.SecretAccessKey, c.Token)
+ }
+ transport, err := minio.DefaultTransport(c.Ssl)
+ if err != nil {
+ c.Log.Errorw("error creating minio transport", "error", err)
+ return nil, err
+ }
+ transport.TLSClientConfig = c.TlsConfig
+ opts := &minio.Options{
+ Creds: creds,
+ Secure: c.Ssl,
+ Transport: transport,
+ }
+ if c.Region != "" {
+ opts.Region = c.Region
+ }
+ mclient, err := minio.New(c.Endpoint, opts)
+ if err != nil {
+ c.Log.Errorw("error connecting to minio", "error", err)
+ return nil, err
+ }
+
+ c.client = mclient
+ return mclient, nil
+}
diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go
index d96c9a6e3eb..40e2695a396 100644
--- a/pkg/storage/storage.go
+++ b/pkg/storage/storage.go
@@ -3,6 +3,7 @@ package storage
import (
"context"
"io"
+ "time"
"github.com/minio/minio-go/v7"
@@ -10,14 +11,14 @@ import (
)
// Client is storage client abstraction
+//
+//go:generate mockgen -destination=./storage_mock.go -package=storage "github.com/kubeshop/testkube/pkg/storage" Client
type Client interface {
ClientBucket
ClientImplicitBucket
}
// ClientImplicitBucket is storage client abstraction where bucket name is provided from config
-//
-//go:generate mockgen -destination=./storage_mock.go -package=storage "github.com/kubeshop/testkube/pkg/storage" ClientImplicitBucket
type ClientImplicitBucket interface {
IsConnectionPossible(ctx context.Context) (bool, error)
ListFiles(ctx context.Context, bucketFolder string) ([]testkube.Artifact, error)
@@ -34,9 +35,11 @@ type ClientBucket interface {
CreateBucket(ctx context.Context, bucket string) error
DeleteBucket(ctx context.Context, bucket string, force bool) error
ListBuckets(ctx context.Context) ([]string, error)
- DownloadFileFromBucket(ctx context.Context, bucket, bucketFolder, file string) (io.Reader, error)
+ DownloadFileFromBucket(ctx context.Context, bucket, bucketFolder, file string) (io.Reader, minio.ObjectInfo, error)
DownloadArchiveFromBucket(ctx context.Context, bucket, bucketFolder string, masks []string) (io.Reader, error)
UploadFileToBucket(ctx context.Context, bucket, bucketFolder, filePath string, reader io.Reader, objectSize int64) error
GetValidBucketName(parentType string, parentName string) string
DeleteFileFromBucket(ctx context.Context, bucket, bucketFolder, file string) error
+ PresignDownloadFileFromBucket(ctx context.Context, bucket, bucketFolder, file string, expires time.Duration) (string, error)
+ PresignUploadFileToBucket(ctx context.Context, bucket, bucketFolder, filePath string, expires time.Duration) (string, error)
}
diff --git a/pkg/storage/storage_mock.go b/pkg/storage/storage_mock.go
index addaa2b5d7a..03583bc5548 100644
--- a/pkg/storage/storage_mock.go
+++ b/pkg/storage/storage_mock.go
@@ -8,6 +8,7 @@ import (
context "context"
io "io"
reflect "reflect"
+ time "time"
gomock "github.com/golang/mock/gomock"
testkube "github.com/kubeshop/testkube/pkg/api/v1/testkube"
@@ -139,12 +140,13 @@ func (mr *MockClientMockRecorder) DownloadFile(arg0, arg1, arg2 interface{}) *go
}
// DownloadFileFromBucket mocks base method.
-func (m *MockClient) DownloadFileFromBucket(arg0 context.Context, arg1, arg2, arg3 string) (io.Reader, error) {
+func (m *MockClient) DownloadFileFromBucket(arg0 context.Context, arg1, arg2, arg3 string) (io.Reader, minio.ObjectInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DownloadFileFromBucket", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(io.Reader)
- ret1, _ := ret[1].(error)
- return ret0, ret1
+ ret1, _ := ret[1].(minio.ObjectInfo)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
}
// DownloadFileFromBucket indicates an expected call of DownloadFileFromBucket.
@@ -226,6 +228,36 @@ func (mr *MockClientMockRecorder) PlaceFiles(arg0, arg1, arg2 interface{}) *gomo
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PlaceFiles", reflect.TypeOf((*MockClient)(nil).PlaceFiles), arg0, arg1, arg2)
}
+// PresignDownloadFileFromBucket mocks base method.
+func (m *MockClient) PresignDownloadFileFromBucket(arg0 context.Context, arg1, arg2, arg3 string, arg4 time.Duration) (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PresignDownloadFileFromBucket", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// PresignDownloadFileFromBucket indicates an expected call of PresignDownloadFileFromBucket.
+func (mr *MockClientMockRecorder) PresignDownloadFileFromBucket(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PresignDownloadFileFromBucket", reflect.TypeOf((*MockClient)(nil).PresignDownloadFileFromBucket), arg0, arg1, arg2, arg3, arg4)
+}
+
+// PresignUploadFileToBucket mocks base method.
+func (m *MockClient) PresignUploadFileToBucket(arg0 context.Context, arg1, arg2, arg3 string, arg4 time.Duration) (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PresignUploadFileToBucket", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// PresignUploadFileToBucket indicates an expected call of PresignUploadFileToBucket.
+func (mr *MockClientMockRecorder) PresignUploadFileToBucket(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PresignUploadFileToBucket", reflect.TypeOf((*MockClient)(nil).PresignUploadFileToBucket), arg0, arg1, arg2, arg3, arg4)
+}
+
// SaveFile mocks base method.
func (m *MockClient) SaveFile(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
diff --git a/pkg/tcl/README.md b/pkg/tcl/README.md
new file mode 100644
index 00000000000..25ca004f001
--- /dev/null
+++ b/pkg/tcl/README.md
@@ -0,0 +1,7 @@
+# Testkube - TCL Package
+
+This folder contains special code with the Testkube Community license.
+
+## License
+
+The code in this folder is licensed under the Testkube Community License. Please see the [LICENSE](../../licenses/TCL.txt) file for more information.
diff --git a/pkg/tcl/apitcl/v1/pro.go b/pkg/tcl/apitcl/v1/pro.go
new file mode 100644
index 00000000000..6be330962ae
--- /dev/null
+++ b/pkg/tcl/apitcl/v1/pro.go
@@ -0,0 +1,45 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package v1
+
+import (
+ "net/http"
+
+ "github.com/gofiber/fiber/v2"
+ "github.com/pkg/errors"
+)
+
+func (s *apiTCL) isPro() bool {
+ return s.ProContext != nil
+}
+
+//nolint:unused
+func (s *apiTCL) isProPaid() bool {
+ // TODO: Replace with proper implementation
+ return s.isPro()
+}
+
+func (s *apiTCL) pro(h fiber.Handler) fiber.Handler {
+ return func(ctx *fiber.Ctx) error {
+ if s.isPro() {
+ return h(ctx)
+ }
+ return s.Error(ctx, http.StatusPaymentRequired, errors.New("this functionality is only for the Pro/Enterprise subscription"))
+ }
+}
+
+//nolint:unused
+func (s *apiTCL) proPaid(h fiber.Handler) fiber.Handler {
+ return func(ctx *fiber.Ctx) error {
+ if s.isProPaid() {
+ return h(ctx)
+ }
+ return s.Error(ctx, http.StatusPaymentRequired, errors.New("this functionality is only for paid plans of Pro/Enterprise subscription"))
+ }
+}
diff --git a/pkg/tcl/apitcl/v1/server.go b/pkg/tcl/apitcl/v1/server.go
new file mode 100644
index 00000000000..d92de952c73
--- /dev/null
+++ b/pkg/tcl/apitcl/v1/server.go
@@ -0,0 +1,142 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package v1
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/gofiber/fiber/v2"
+ kubeclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/pkg/client/testworkflows/v1"
+ apiv1 "github.com/kubeshop/testkube/internal/app/api/v1"
+ "github.com/kubeshop/testkube/internal/config"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/imageinspector"
+ "github.com/kubeshop/testkube/pkg/tcl/repositorytcl/testworkflow"
+ "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowexecutor"
+)
+
+type apiTCL struct {
+ apiv1.TestkubeAPI
+ ProContext *config.ProContext
+ ImageInspector imageinspector.Inspector
+ TestWorkflowResults testworkflow.Repository
+ TestWorkflowOutput testworkflow.OutputRepository
+ TestWorkflowsClient testworkflowsv1.Interface
+ TestWorkflowTemplatesClient testworkflowsv1.TestWorkflowTemplatesInterface
+ TestWorkflowExecutor testworkflowexecutor.TestWorkflowExecutor
+ ApiUrl string
+}
+
+type ApiTCL interface {
+ AppendRoutes()
+ GetTestWorkflowNotificationsStream(ctx context.Context, executionID string) (chan testkube.TestWorkflowExecutionNotification, error)
+}
+
+func NewApiTCL(
+ testkubeAPI apiv1.TestkubeAPI,
+ proContext *config.ProContext,
+ kubeClient kubeclient.Client,
+ imageInspector imageinspector.Inspector,
+ testWorkflowResults testworkflow.Repository,
+ testWorkflowOutput testworkflow.OutputRepository,
+ apiUrl string,
+) ApiTCL {
+ executor := testworkflowexecutor.New(testkubeAPI.Events, testkubeAPI.Clientset, testWorkflowResults, testWorkflowOutput, testkubeAPI.Namespace)
+ go executor.Recover(context.Background())
+ return &apiTCL{
+ TestkubeAPI: testkubeAPI,
+ ProContext: proContext,
+ ImageInspector: imageInspector,
+ TestWorkflowResults: testWorkflowResults,
+ TestWorkflowOutput: testWorkflowOutput,
+ TestWorkflowsClient: testworkflowsv1.NewClient(kubeClient, testkubeAPI.Namespace),
+ TestWorkflowTemplatesClient: testworkflowsv1.NewTestWorkflowTemplatesClient(kubeClient, testkubeAPI.Namespace),
+ TestWorkflowExecutor: executor,
+ ApiUrl: apiUrl,
+ }
+}
+
+func (s *apiTCL) NotImplemented(c *fiber.Ctx) error {
+ return s.Error(c, http.StatusNotImplemented, errors.New("not implemented yet"))
+}
+
+func (s *apiTCL) BadGateway(c *fiber.Ctx, prefix, description string, err error) error {
+ return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: %s: %w", prefix, description, err))
+}
+
+func (s *apiTCL) InternalError(c *fiber.Ctx, prefix, description string, err error) error {
+ return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: %s: %w", prefix, description, err))
+}
+
+func (s *apiTCL) BadRequest(c *fiber.Ctx, prefix, description string, err error) error {
+ return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: %s: %w", prefix, description, err))
+}
+
+func (s *apiTCL) NotFound(c *fiber.Ctx, prefix, description string, err error) error {
+ return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: %s: %w", prefix, description, err))
+}
+
+func (s *apiTCL) ClientError(c *fiber.Ctx, prefix string, err error) error {
+ if IsNotFound(err) {
+ return s.NotFound(c, prefix, "client not found", err)
+ }
+ return s.BadGateway(c, prefix, "client problem", err)
+}
+
+func (s *apiTCL) AppendRoutes() {
+ root := s.Routes
+
+ // Register TestWorkflows as additional source for labels
+ s.WithLabelSources(s.TestWorkflowsClient, s.TestWorkflowTemplatesClient)
+
+ testWorkflows := root.Group("/test-workflows")
+ testWorkflows.Get("/", s.pro(s.ListTestWorkflowsHandler()))
+ testWorkflows.Post("/", s.pro(s.CreateTestWorkflowHandler()))
+ testWorkflows.Delete("/", s.pro(s.DeleteTestWorkflowsHandler()))
+ testWorkflows.Get("/:id", s.pro(s.GetTestWorkflowHandler()))
+ testWorkflows.Put("/:id", s.pro(s.UpdateTestWorkflowHandler()))
+ testWorkflows.Delete("/:id", s.pro(s.DeleteTestWorkflowHandler()))
+ testWorkflows.Get("/:id/executions", s.pro(s.ListTestWorkflowExecutionsHandler()))
+ testWorkflows.Post("/:id/executions", s.pro(s.ExecuteTestWorkflowHandler()))
+ testWorkflows.Get("/:id/metrics", s.pro(s.GetTestWorkflowMetricsHandler()))
+ testWorkflows.Get("/:id/executions/:executionID", s.pro(s.GetTestWorkflowExecutionHandler()))
+ testWorkflows.Post("/:id/abort", s.pro(s.AbortAllTestWorkflowExecutionsHandler()))
+ testWorkflows.Post("/:id/executions/:executionID/abort", s.pro(s.AbortTestWorkflowExecutionHandler()))
+ testWorkflows.Get("/:id/executions/:executionID/logs", s.pro(s.GetTestWorkflowExecutionLogsHandler()))
+
+ testWorkflowExecutions := root.Group("/test-workflow-executions")
+ testWorkflowExecutions.Get("/", s.pro(s.ListTestWorkflowExecutionsHandler()))
+ testWorkflowExecutions.Get("/:executionID", s.pro(s.GetTestWorkflowExecutionHandler()))
+ testWorkflowExecutions.Get("/:executionID/notifications", s.pro(s.StreamTestWorkflowExecutionNotificationsHandler()))
+ testWorkflowExecutions.Get("/:executionID/notifications/stream", s.pro(s.StreamTestWorkflowExecutionNotificationsWebSocketHandler()))
+ testWorkflowExecutions.Post("/:executionID/abort", s.pro(s.AbortTestWorkflowExecutionHandler()))
+ testWorkflowExecutions.Get("/:executionID/logs", s.pro(s.GetTestWorkflowExecutionLogsHandler()))
+ testWorkflowExecutions.Get("/:executionID/artifacts", s.pro(s.ListTestWorkflowExecutionArtifactsHandler()))
+ testWorkflowExecutions.Get("/:executionID/artifacts/:filename", s.pro(s.GetTestWorkflowArtifactHandler()))
+ testWorkflowExecutions.Get("/:executionID/artifact-archive", s.pro(s.GetTestWorkflowArtifactArchiveHandler()))
+
+ testWorkflowWithExecutions := root.Group("/test-workflow-with-executions")
+ testWorkflowWithExecutions.Get("/", s.pro(s.ListTestWorkflowWithExecutionsHandler()))
+ testWorkflowWithExecutions.Get("/:id", s.pro(s.GetTestWorkflowWithExecutionHandler()))
+
+ root.Post("/preview-test-workflow", s.pro(s.PreviewTestWorkflowHandler()))
+
+ testWorkflowTemplates := root.Group("/test-workflow-templates")
+ testWorkflowTemplates.Get("/", s.pro(s.ListTestWorkflowTemplatesHandler()))
+ testWorkflowTemplates.Post("/", s.pro(s.CreateTestWorkflowTemplateHandler()))
+ testWorkflowTemplates.Delete("/", s.pro(s.DeleteTestWorkflowTemplatesHandler()))
+ testWorkflowTemplates.Get("/:id", s.pro(s.GetTestWorkflowTemplateHandler()))
+ testWorkflowTemplates.Put("/:id", s.pro(s.UpdateTestWorkflowTemplateHandler()))
+ testWorkflowTemplates.Delete("/:id", s.pro(s.DeleteTestWorkflowTemplateHandler()))
+}
diff --git a/pkg/tcl/apitcl/v1/testworkflowexecutions.go b/pkg/tcl/apitcl/v1/testworkflowexecutions.go
new file mode 100644
index 00000000000..b8847c0d856
--- /dev/null
+++ b/pkg/tcl/apitcl/v1/testworkflowexecutions.go
@@ -0,0 +1,439 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package v1
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "math"
+ "net/http"
+ "net/url"
+ "strconv"
+
+ "github.com/gofiber/fiber/v2"
+ "github.com/gofiber/websocket/v2"
+ "github.com/pkg/errors"
+
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/datefilter"
+ "github.com/kubeshop/testkube/pkg/tcl/repositorytcl/testworkflow"
+ "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowcontroller"
+)
+
+func (s *apiTCL) StreamTestWorkflowExecutionNotificationsHandler() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ ctx := c.Context()
+ id := c.Params("executionID")
+ errPrefix := fmt.Sprintf("failed to stream test workflow execution notifications '%s'", id)
+
+ // Fetch execution from database
+ execution, err := s.TestWorkflowResults.Get(ctx, id)
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+
+ // Check for the logs
+ ctrl, err := testworkflowcontroller.New(ctx, s.Clientset, s.Namespace, execution.Id, execution.ScheduledAt)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "fetching job", err)
+ }
+
+ // Initiate processing event stream
+ ctx.SetContentType("text/event-stream")
+ ctx.Response.Header.Set("Cache-Control", "no-cache")
+ ctx.Response.Header.Set("Connection", "keep-alive")
+ ctx.Response.Header.Set("Transfer-Encoding", "chunked")
+
+ // Stream the notifications
+ ctx.SetBodyStreamWriter(func(w *bufio.Writer) {
+ _ = w.Flush()
+ enc := json.NewEncoder(w)
+
+ for n := range ctrl.Watch(ctx).Stream(ctx).Channel() {
+ if n.Error == nil {
+ _ = enc.Encode(n.Value)
+ _, _ = fmt.Fprintf(w, "\n")
+ _ = w.Flush()
+ }
+ }
+ })
+
+ return nil
+ }
+}
+
+func (s *apiTCL) StreamTestWorkflowExecutionNotificationsWebSocketHandler() fiber.Handler {
+ return websocket.New(func(c *websocket.Conn) {
+ ctx, ctxCancel := context.WithCancel(context.Background())
+ id := c.Params("executionID")
+
+ // Stop reading when the WebSocket connection is already closed
+ originalClose := c.CloseHandler()
+ c.SetCloseHandler(func(code int, text string) error {
+ ctxCancel()
+ return originalClose(code, text)
+ })
+ defer c.Conn.Close()
+
+ // Fetch execution from database
+ execution, err := s.TestWorkflowResults.Get(ctx, id)
+ if err != nil {
+ return
+ }
+
+ // Check for the logs TODO: Load from the database if possible
+ ctrl, err := testworkflowcontroller.New(ctx, s.Clientset, s.Namespace, execution.Id, execution.ScheduledAt)
+ if err != nil {
+ return
+ }
+
+ for n := range ctrl.Watch(ctx).Stream(ctx).Channel() {
+ if n.Error == nil {
+ _ = c.WriteJSON(n.Value)
+ }
+ }
+ })
+}
+
+func (s *apiTCL) ListTestWorkflowExecutionsHandler() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ errPrefix := "failed to list test workflow executions"
+
+ filter := getWorkflowExecutionsFilterFromRequest(c)
+
+ executions, err := s.TestWorkflowResults.GetExecutionsSummary(c.Context(), filter)
+ if err != nil {
+ return s.ClientError(c, errPrefix+": get execution results", err)
+ }
+
+ executionTotals, err := s.TestWorkflowResults.GetExecutionsTotals(c.Context(), testworkflow.NewExecutionsFilter().WithName(filter.Name()))
+ if err != nil {
+ return s.ClientError(c, errPrefix+": get totals", err)
+ }
+
+ filterTotals := *filter.(*testworkflow.FilterImpl)
+ filterTotals.WithPage(0).WithPageSize(math.MaxInt32)
+ filteredTotals, err := s.TestWorkflowResults.GetExecutionsTotals(c.Context(), filterTotals)
+ if err != nil {
+ return s.ClientError(c, errPrefix+": get filtered totals", err)
+ }
+
+ results := testkube.TestWorkflowExecutionsResult{
+ Totals: &executionTotals,
+ Filtered: &filteredTotals,
+ Results: executions,
+ }
+ return c.JSON(results)
+ }
+}
+
+func (s *apiTCL) GetTestWorkflowMetricsHandler() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ workflowName := c.Params("id")
+
+ const DefaultLimit = 0
+ limit, err := strconv.Atoi(c.Query("limit", strconv.Itoa(DefaultLimit)))
+ if err != nil {
+ limit = DefaultLimit
+ }
+
+ const DefaultLastDays = 7
+ last, err := strconv.Atoi(c.Query("last", strconv.Itoa(DefaultLastDays)))
+ if err != nil {
+ last = DefaultLastDays
+ }
+
+ metrics, err := s.TestWorkflowResults.GetTestWorkflowMetrics(c.Context(), workflowName, limit, last)
+ if err != nil {
+ return s.ClientError(c, "get metrics for workflow", err)
+ }
+
+ return c.JSON(metrics)
+ }
+}
+
+func (s *apiTCL) GetTestWorkflowExecutionHandler() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ ctx := c.Context()
+ id := c.Params("id", "")
+ executionID := c.Params("executionID")
+
+ var execution testkube.TestWorkflowExecution
+ var err error
+ if id == "" {
+ execution, err = s.TestWorkflowResults.Get(ctx, executionID)
+ } else {
+ execution, err = s.TestWorkflowResults.GetByNameAndTestWorkflow(ctx, executionID, id)
+ }
+ if err != nil {
+ return s.ClientError(c, "get execution", err)
+ }
+
+ return c.JSON(execution)
+ }
+}
+
+func (s *apiTCL) GetTestWorkflowExecutionLogsHandler() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ ctx := c.Context()
+ id := c.Params("id", "")
+ executionID := c.Params("executionID")
+
+ var execution testkube.TestWorkflowExecution
+ var err error
+ if id == "" {
+ execution, err = s.TestWorkflowResults.Get(ctx, executionID)
+ } else {
+ execution, err = s.TestWorkflowResults.GetByNameAndTestWorkflow(ctx, executionID, id)
+ }
+ if err != nil {
+ return s.ClientError(c, "get execution", err)
+ }
+
+ reader, err := s.TestWorkflowOutput.ReadLog(ctx, executionID, execution.Workflow.Name)
+ if err != nil {
+ return s.InternalError(c, "can't get log", executionID, err)
+ }
+
+ c.Context().SetContentType(mediaTypePlainText)
+ _, err = io.Copy(c.Response().BodyWriter(), reader)
+ return err
+ }
+}
+
+func (s *apiTCL) AbortTestWorkflowExecutionHandler() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ ctx := c.Context()
+ name := c.Params("id")
+ executionID := c.Params("executionID")
+ errPrefix := fmt.Sprintf("failed to abort test workflow execution '%s'", executionID)
+
+ var execution testkube.TestWorkflowExecution
+ var err error
+ if name == "" {
+ execution, err = s.TestWorkflowResults.Get(ctx, executionID)
+ } else {
+ execution, err = s.TestWorkflowResults.GetByNameAndTestWorkflow(ctx, executionID, name)
+ }
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+
+ if execution.Result != nil && execution.Result.IsFinished() {
+ return s.BadRequest(c, errPrefix, "checking execution", errors.New("execution already finished"))
+ }
+
+ // Obtain the controller
+ ctrl, err := testworkflowcontroller.New(ctx, s.Clientset, s.Namespace, execution.Id, execution.ScheduledAt)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "fetching job", err)
+ }
+
+ // Abort the execution
+ err = ctrl.Abort(context.Background())
+ if err != nil {
+ return s.ClientError(c, "aborting test workflow execution", err)
+ }
+
+ c.Status(http.StatusNoContent)
+
+ return nil
+ }
+}
+
+func (s *apiTCL) AbortAllTestWorkflowExecutionsHandler() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ ctx := c.Context()
+ name := c.Params("id")
+ errPrefix := fmt.Sprintf("failed to abort test workflow executions '%s'", name)
+
+ // Fetch executions
+ filter := testworkflow.NewExecutionsFilter().WithName(name).WithStatus(string(testkube.RUNNING_TestWorkflowStatus))
+ executions, err := s.TestWorkflowResults.GetExecutions(ctx, filter)
+ if err != nil {
+ if IsNotFound(err) {
+ c.Status(http.StatusNoContent)
+ return nil
+ }
+ return s.ClientError(c, errPrefix, err)
+ }
+
+ for _, execution := range executions {
+ // Obtain the controller
+ ctrl, err := testworkflowcontroller.New(ctx, s.Clientset, s.Namespace, execution.Id, execution.ScheduledAt)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "fetching job", err)
+ }
+
+ // Abort the execution
+ err = ctrl.Abort(context.Background())
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+ }
+
+ c.Status(http.StatusNoContent)
+
+ return nil
+ }
+}
+
+func (s *apiTCL) ListTestWorkflowExecutionArtifactsHandler() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ executionID := c.Params("executionID")
+ errPrefix := fmt.Sprintf("failed to list artifacts for test workflow execution %s", executionID)
+
+ execution, err := s.TestWorkflowResults.Get(c.Context(), executionID)
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+
+ files, err := s.ArtifactsStorage.ListFiles(c.Context(), execution.Id, "", "", execution.Workflow.Name)
+ if err != nil {
+ return s.InternalError(c, errPrefix, "storage client could not list test workflow files", err)
+ }
+
+ return c.JSON(files)
+ }
+}
+
+func (s *apiTCL) GetTestWorkflowArtifactHandler() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ executionID := c.Params("executionID")
+ fileName := c.Params("filename")
+ errPrefix := fmt.Sprintf("failed to get artifact %s for workflow execution %s", fileName, executionID)
+
+ // TODO fix this someday :) we don't know 15 mins before release why it's working this way
+ // remember about CLI client and Dashboard client too!
+ unescaped, err := url.QueryUnescape(fileName)
+ if err == nil {
+ fileName = unescaped
+ }
+ unescaped, err = url.QueryUnescape(fileName)
+ if err == nil {
+ fileName = unescaped
+ }
+ //// quickfix end
+
+ execution, err := s.TestWorkflowResults.Get(c.Context(), executionID)
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+
+ file, err := s.ArtifactsStorage.DownloadFile(c.Context(), fileName, execution.Id, "", "", execution.Workflow.Name)
+ if err != nil {
+ return s.InternalError(c, errPrefix, "could not download file", err)
+ }
+
+ return c.SendStream(file)
+ }
+}
+
+func (s *apiTCL) GetTestWorkflowArtifactArchiveHandler() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ executionID := c.Params("executionID")
+ query := c.Request().URI().QueryString()
+ errPrefix := fmt.Sprintf("failed to get artifact archive for test workflow execution %s", executionID)
+
+ values, err := url.ParseQuery(string(query))
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "could not parse query string", err)
+ }
+
+ execution, err := s.TestWorkflowResults.Get(c.Context(), executionID)
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+
+ archive, err := s.ArtifactsStorage.DownloadArchive(c.Context(), execution.Id, values["mask"])
+ if err != nil {
+ return s.InternalError(c, errPrefix, "could not download workflow artifact archive", err)
+ }
+
+ return c.SendStream(archive)
+ }
+}
+
+func (s *apiTCL) GetTestWorkflowNotificationsStream(ctx context.Context, executionID string) (chan testkube.TestWorkflowExecutionNotification, error) {
+ // Load the execution
+ execution, err := s.TestWorkflowResults.Get(ctx, executionID)
+ if err != nil {
+ return nil, err
+ }
+
+ // Check for the logs
+ ctrl, err := testworkflowcontroller.New(ctx, s.Clientset, s.Namespace, execution.Id, execution.ScheduledAt)
+ if err != nil {
+ return nil, err
+ }
+
+ // Stream the notifications
+ ch := make(chan testkube.TestWorkflowExecutionNotification)
+ go func() {
+ for n := range ctrl.Watch(ctx).Stream(ctx).Channel() {
+ if n.Error == nil {
+ ch <- n.Value.ToInternal()
+ }
+ }
+ close(ch)
+ }()
+ return ch, nil
+}
+
+func getWorkflowExecutionsFilterFromRequest(c *fiber.Ctx) testworkflow.Filter {
+ filter := testworkflow.NewExecutionsFilter()
+ name := c.Params("id", "")
+ if name != "" {
+ filter = filter.WithName(name)
+ }
+
+ textSearch := c.Query("textSearch", "")
+ if textSearch != "" {
+ filter = filter.WithTextSearch(textSearch)
+ }
+
+ page, err := strconv.Atoi(c.Query("page", ""))
+ if err == nil {
+ filter = filter.WithPage(page)
+ }
+
+ pageSize, err := strconv.Atoi(c.Query("pageSize", ""))
+ if err == nil && pageSize != 0 {
+ filter = filter.WithPageSize(pageSize)
+ }
+
+ status := c.Query("status", "")
+ if status != "" {
+ filter = filter.WithStatus(status)
+ }
+
+ last, err := strconv.Atoi(c.Query("last", "0"))
+ if err == nil && last != 0 {
+ filter = filter.WithLastNDays(last)
+ }
+
+ dFilter := datefilter.NewDateFilter(c.Query("startDate", ""), c.Query("endDate", ""))
+ if dFilter.IsStartValid {
+ filter = filter.WithStartDate(dFilter.Start)
+ }
+
+ if dFilter.IsEndValid {
+ filter = filter.WithEndDate(dFilter.End)
+ }
+
+ selector := c.Query("selector")
+ if selector != "" {
+ filter = filter.WithSelector(selector)
+ }
+
+ return filter
+}
diff --git a/pkg/tcl/apitcl/v1/testworkflows.go b/pkg/tcl/apitcl/v1/testworkflows.go
new file mode 100644
index 00000000000..94098a52076
--- /dev/null
+++ b/pkg/tcl/apitcl/v1/testworkflows.go
@@ -0,0 +1,421 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package v1
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gofiber/fiber/v2"
+ "github.com/pkg/errors"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+ testworkflowmappers "github.com/kubeshop/testkube/pkg/tcl/mapperstcl/testworkflows"
+ "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor"
+ "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowresolver"
+)
+
+func (s *apiTCL) ListTestWorkflowsHandler() fiber.Handler {
+ errPrefix := "failed to list test workflows"
+ return func(c *fiber.Ctx) (err error) {
+ workflows, err := s.getFilteredTestWorkflowList(c)
+ if err != nil {
+ return s.BadGateway(c, errPrefix, "client problem", err)
+ }
+ err = SendResourceList(c, "TestWorkflow", testworkflowsv1.GroupVersion, testworkflowmappers.MapTestWorkflowKubeToAPI, workflows.Items...)
+ if err != nil {
+ return s.InternalError(c, errPrefix, "serialization problem", err)
+ }
+ return
+ }
+}
+
+func (s *apiTCL) GetTestWorkflowHandler() fiber.Handler {
+ return func(c *fiber.Ctx) (err error) {
+ name := c.Params("id")
+ errPrefix := fmt.Sprintf("failed to get test workflow '%s'", name)
+ workflow, err := s.TestWorkflowsClient.Get(name)
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+ err = SendResource(c, "TestWorkflow", testworkflowsv1.GroupVersion, testworkflowmappers.MapKubeToAPI, workflow)
+ if err != nil {
+ return s.InternalError(c, errPrefix, "serialization problem", err)
+ }
+ return
+ }
+}
+
+func (s *apiTCL) DeleteTestWorkflowHandler() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ name := c.Params("id")
+ errPrefix := fmt.Sprintf("failed to delete test workflow '%s'", name)
+ err := s.TestWorkflowsClient.Delete(name)
+ s.Metrics.IncDeleteTestWorkflow(err)
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+ skipExecutions := c.Query("skipDeleteExecutions", "")
+ if skipExecutions != "true" {
+ err = s.TestWorkflowResults.DeleteByTestWorkflow(context.Background(), name)
+ if err != nil {
+ return s.ClientError(c, "deleting executions", err)
+ }
+ }
+ return c.SendStatus(http.StatusNoContent)
+ }
+}
+
+func (s *apiTCL) DeleteTestWorkflowsHandler() fiber.Handler {
+ errPrefix := "failed to delete test workflows"
+ return func(c *fiber.Ctx) error {
+ selector := c.Query("selector")
+ workflows, err := s.TestWorkflowsClient.List(selector)
+ if err != nil {
+ return s.BadGateway(c, errPrefix, "client problem", err)
+ }
+
+ // Delete
+ err = s.TestWorkflowsClient.DeleteByLabels(selector)
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+
+ // Mark as deleted
+ for range workflows.Items {
+ s.Metrics.IncDeleteTestWorkflow(err)
+ }
+
+ // Delete the executions
+ skipExecutions := c.Query("skipDeleteExecutions", "")
+ if skipExecutions != "true" {
+ names := common.MapSlice(workflows.Items, func(t testworkflowsv1.TestWorkflow) string {
+ return t.Name
+ })
+ err = s.TestWorkflowResults.DeleteByTestWorkflows(context.Background(), names)
+ if err != nil {
+ return s.ClientError(c, "deleting executions", err)
+ }
+ }
+
+ return c.SendStatus(http.StatusNoContent)
+ }
+}
+
+func (s *apiTCL) CreateTestWorkflowHandler() fiber.Handler {
+ errPrefix := "failed to create test workflow"
+ return func(c *fiber.Ctx) (err error) {
+ // Deserialize resource
+ obj := new(testworkflowsv1.TestWorkflow)
+ if HasYAML(c) {
+ err = common.DeserializeCRD(obj, c.Body())
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "invalid body", err)
+ }
+ } else {
+ var v *testkube.TestWorkflow
+ err = c.BodyParser(&v)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "invalid body", err)
+ }
+ obj = testworkflowmappers.MapAPIToKube(v)
+ }
+
+ // Validate resource
+ if obj == nil || obj.Name == "" {
+ return s.BadRequest(c, errPrefix, "invalid body", errors.New("name is required"))
+ }
+ obj.Namespace = s.Namespace
+
+ // Create the resource
+ obj, err = s.TestWorkflowsClient.Create(obj)
+ s.Metrics.IncCreateTestWorkflow(err)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "client error", err)
+ }
+
+ err = SendResource(c, "TestWorkflow", testworkflowsv1.GroupVersion, testworkflowmappers.MapKubeToAPI, obj)
+ if err != nil {
+ return s.InternalError(c, errPrefix, "serialization problem", err)
+ }
+ return
+ }
+}
+
+func (s *apiTCL) UpdateTestWorkflowHandler() fiber.Handler {
+ errPrefix := "failed to update test workflow"
+ return func(c *fiber.Ctx) (err error) {
+ name := c.Params("id")
+
+ // Deserialize resource
+ obj := new(testworkflowsv1.TestWorkflow)
+ if HasYAML(c) {
+ err = common.DeserializeCRD(obj, c.Body())
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "invalid body", err)
+ }
+ } else {
+ var v *testkube.TestWorkflow
+ err = c.BodyParser(&v)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "invalid body", err)
+ }
+ obj = testworkflowmappers.MapAPIToKube(v)
+ }
+
+ // Read existing resource
+ workflow, err := s.TestWorkflowsClient.Get(name)
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+
+ // Validate resource
+ if obj == nil {
+ return s.BadRequest(c, errPrefix, "invalid body", errors.New("body is required"))
+ }
+ obj.Namespace = workflow.Namespace
+ obj.Name = workflow.Name
+ obj.ResourceVersion = workflow.ResourceVersion
+
+ // Update the resource
+ obj, err = s.TestWorkflowsClient.Update(obj)
+ s.Metrics.IncUpdateTestWorkflow(err)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "client error", err)
+ }
+
+ err = SendResource(c, "TestWorkflow", testworkflowsv1.GroupVersion, testworkflowmappers.MapKubeToAPI, obj)
+ if err != nil {
+ return s.InternalError(c, errPrefix, "serialization problem", err)
+ }
+ return
+ }
+}
+
+func (s *apiTCL) PreviewTestWorkflowHandler() fiber.Handler {
+ errPrefix := "failed to resolve test workflow"
+ return func(c *fiber.Ctx) (err error) {
+ // Check if it should inline templates
+ inline, _ := strconv.ParseBool(c.Query("inline"))
+
+ // Deserialize resource
+ obj := new(testworkflowsv1.TestWorkflow)
+ if HasYAML(c) {
+ err = common.DeserializeCRD(obj, c.Body())
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "invalid body", err)
+ }
+ } else {
+ var v *testkube.TestWorkflow
+ err = c.BodyParser(&v)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "invalid body", err)
+ }
+ obj = testworkflowmappers.MapAPIToKube(v)
+ }
+
+ // Validate resource
+ if obj == nil {
+ return s.BadRequest(c, errPrefix, "invalid body", errors.New("name is required"))
+ }
+ obj.Namespace = s.Namespace
+
+ if inline {
+ // Fetch the templates
+ tpls := testworkflowresolver.ListTemplates(obj)
+ tplsMap := make(map[string]testworkflowsv1.TestWorkflowTemplate, len(tpls))
+ for name := range tpls {
+ tpl, err := s.TestWorkflowTemplatesClient.Get(name)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "fetching error", err)
+ }
+ tplsMap[name] = *tpl
+ }
+
+ // Resolve the TestWorkflow
+ err = testworkflowresolver.ApplyTemplates(obj, tplsMap)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "resolving error", err)
+ }
+ }
+
+ err = SendResource(c, "TestWorkflow", testworkflowsv1.GroupVersion, testworkflowmappers.MapKubeToAPI, obj)
+ if err != nil {
+ return s.InternalError(c, errPrefix, "serialization problem", err)
+ }
+ return
+ }
+}
+
+// TODO: Add metrics
+func (s *apiTCL) ExecuteTestWorkflowHandler() fiber.Handler {
+ return func(c *fiber.Ctx) (err error) {
+ ctx := c.Context()
+ name := c.Params("id")
+ errPrefix := fmt.Sprintf("failed to execute test workflow '%s'", name)
+ workflow, err := s.TestWorkflowsClient.Get(name)
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+
+ // Delete unnecessary data
+ delete(workflow.Annotations, "kubectl.kubernetes.io/last-applied-configuration")
+
+ // Preserve initial workflow
+ initialWorkflow := workflow.DeepCopy()
+
+ // Load the execution request
+ var request testkube.TestWorkflowExecutionRequest
+ err = c.BodyParser(&request)
+ if err != nil && !errors.Is(err, fiber.ErrUnprocessableEntity) {
+ return s.BadRequest(c, errPrefix, "invalid body", err)
+ }
+
+ // Fetch the templates
+ tpls := testworkflowresolver.ListTemplates(workflow)
+ tplsMap := make(map[string]testworkflowsv1.TestWorkflowTemplate, len(tpls))
+ for tplName := range tpls {
+ tpl, err := s.TestWorkflowTemplatesClient.Get(tplName)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "fetching error", err)
+ }
+ tplsMap[tplName] = *tpl
+ }
+
+ // Apply the configuration
+ _, err = testworkflowresolver.ApplyWorkflowConfig(workflow, testworkflowmappers.MapConfigValueAPIToKube(request.Config))
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "configuration", err)
+ }
+
+ // Resolve the TestWorkflow
+ err = testworkflowresolver.ApplyTemplates(workflow, tplsMap)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "resolving error", err)
+ }
+
+ // Build the basic Execution data
+ id := primitive.NewObjectID().Hex()
+ now := time.Now()
+ machine := expressionstcl.NewMachine().
+ RegisterStringMap("internal", map[string]string{
+ "storage.url": os.Getenv("STORAGE_ENDPOINT"),
+ "storage.accessKey": os.Getenv("STORAGE_ACCESSKEYID"),
+ "storage.secretKey": os.Getenv("STORAGE_SECRETACCESSKEY"),
+ "storage.region": os.Getenv("STORAGE_REGION"),
+ "storage.bucket": os.Getenv("STORAGE_BUCKET"),
+ "storage.token": os.Getenv("STORAGE_TOKEN"),
+ "storage.ssl": common.GetOr(os.Getenv("STORAGE_SSL"), "false"),
+ "storage.skipVerify": common.GetOr(os.Getenv("STORAGE_SKIP_VERIFY"), "false"),
+ "storage.certFile": os.Getenv("STORAGE_CERT_FILE"),
+ "storage.keyFile": os.Getenv("STORAGE_KEY_FILE"),
+ "storage.caFile": os.Getenv("STORAGE_CA_FILE"),
+
+ "cloud.enabled": strconv.FormatBool(os.Getenv("TESTKUBE_PRO_API_KEY") != "" || os.Getenv("TESTKUBE_CLOUD_API_KEY") != ""),
+ "cloud.api.key": common.GetOr(os.Getenv("TESTKUBE_PRO_API_KEY"), os.Getenv("TESTKUBE_CLOUD_API_KEY")),
+ "cloud.api.tlsInsecure": common.GetOr(os.Getenv("TESTKUBE_PRO_TLS_INSECURE"), os.Getenv("TESTKUBE_CLOUD_TLS_INSECURE"), "false"),
+ "cloud.api.skipVerify": common.GetOr(os.Getenv("TESTKUBE_PRO_SKIP_VERIFY"), os.Getenv("TESTKUBE_CLOUD_SKIP_VERIFY"), "false"),
+ "cloud.api.url": common.GetOr(os.Getenv("TESTKUBE_PRO_URL"), os.Getenv("TESTKUBE_CLOUD_URL")),
+
+ "dashboard.url": os.Getenv("TESTKUBE_DASHBOARD_URI"),
+ "api.url": s.ApiUrl,
+ "namespace": s.Namespace,
+ }).
+ RegisterStringMap("workflow", map[string]string{
+ "name": workflow.Name,
+ }).
+ RegisterStringMap("execution", map[string]string{
+ "id": id,
+ })
+
+ // Preserve resolved TestWorkflow
+ resolvedWorkflow := workflow.DeepCopy()
+
+ // Process the TestWorkflow
+ bundle, err := testworkflowprocessor.NewFullFeatured(s.ImageInspector).
+ Bundle(c.Context(), workflow, machine)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "processing error", err)
+ }
+
+ // Load execution identifier data
+ // TODO: Consider if that should not be shared (as now it is between Tests and Test Suites)
+ number, _ := s.ExecutionResults.GetNextExecutionNumber(context.Background(), workflow.Name)
+ executionName := request.Name
+ if executionName == "" {
+ executionName = fmt.Sprintf("%s-%d", workflow.Name, number)
+ }
+
+ // Ensure it is unique name
+ // TODO: Consider if we shouldn't make name unique across all TestWorkflows
+ next, _ := s.TestWorkflowResults.GetByNameAndTestWorkflow(ctx, executionName, workflow.Name)
+ if next.Name == executionName {
+ return s.BadRequest(c, errPrefix, "execution name already exists", errors.New(executionName))
+ }
+
+ // Build Execution entity
+ // TODO: Consider storing "config" as well
+ execution := testkube.TestWorkflowExecution{
+ Id: id,
+ Name: executionName,
+ Number: number,
+ ScheduledAt: now,
+ StatusAt: now,
+ Signature: testworkflowprocessor.MapSignatureListToInternal(bundle.Signature),
+ Result: &testkube.TestWorkflowResult{
+ Status: common.Ptr(testkube.QUEUED_TestWorkflowStatus),
+ PredictedStatus: common.Ptr(testkube.PASSED_TestWorkflowStatus),
+ Initialization: &testkube.TestWorkflowStepResult{
+ Status: common.Ptr(testkube.QUEUED_TestWorkflowStepStatus),
+ },
+ Steps: testworkflowprocessor.MapSignatureListToStepResults(bundle.Signature),
+ },
+ Output: []testkube.TestWorkflowOutput{},
+ Workflow: testworkflowmappers.MapKubeToAPI(initialWorkflow),
+ ResolvedWorkflow: testworkflowmappers.MapKubeToAPI(resolvedWorkflow),
+ }
+ err = s.TestWorkflowResults.Insert(ctx, execution)
+ if err != nil {
+ return s.InternalError(c, errPrefix, "inserting execution to storage", err)
+ }
+
+ // Schedule the execution
+ s.TestWorkflowExecutor.Schedule(bundle, execution)
+
+ return c.JSON(execution)
+ }
+}
+
+func (s *apiTCL) getFilteredTestWorkflowList(c *fiber.Ctx) (*testworkflowsv1.TestWorkflowList, error) {
+ crWorkflows, err := s.TestWorkflowsClient.List(c.Query("selector"))
+ if err != nil {
+ return nil, err
+ }
+
+ search := c.Query("textSearch")
+ if search != "" {
+ // filter items array
+ for i := len(crWorkflows.Items) - 1; i >= 0; i-- {
+ if !strings.Contains(crWorkflows.Items[i].Name, search) {
+ crWorkflows.Items = append(crWorkflows.Items[:i], crWorkflows.Items[i+1:]...)
+ }
+ }
+ }
+
+ return crWorkflows, nil
+}
diff --git a/pkg/tcl/apitcl/v1/testworkflowtemplates.go b/pkg/tcl/apitcl/v1/testworkflowtemplates.go
new file mode 100644
index 00000000000..7fd846a19fb
--- /dev/null
+++ b/pkg/tcl/apitcl/v1/testworkflowtemplates.go
@@ -0,0 +1,188 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package v1
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/gofiber/fiber/v2"
+ "github.com/pkg/errors"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ mappers2 "github.com/kubeshop/testkube/pkg/tcl/mapperstcl/testworkflows"
+)
+
+func (s *apiTCL) ListTestWorkflowTemplatesHandler() fiber.Handler {
+ errPrefix := "failed to list test workflow templates"
+ return func(c *fiber.Ctx) (err error) {
+ templates, err := s.getFilteredTestWorkflowTemplateList(c)
+ if err != nil {
+ return s.BadGateway(c, errPrefix, "client problem", err)
+ }
+ err = SendResourceList(c, "TestWorkflowTemplate", testworkflowsv1.GroupVersion, mappers2.MapTestWorkflowTemplateKubeToAPI, templates.Items...)
+ if err != nil {
+ return s.InternalError(c, errPrefix, "serialization problem", err)
+ }
+ return
+ }
+}
+
+func (s *apiTCL) GetTestWorkflowTemplateHandler() fiber.Handler {
+ return func(c *fiber.Ctx) (err error) {
+ name := c.Params("id")
+ errPrefix := fmt.Sprintf("failed to get test workflow template '%s'", name)
+ template, err := s.TestWorkflowTemplatesClient.Get(name)
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+ err = SendResource(c, "TestWorkflowTemplate", testworkflowsv1.GroupVersion, mappers2.MapTemplateKubeToAPI, template)
+ if err != nil {
+ return s.InternalError(c, errPrefix, "serialization problem", err)
+ }
+ return
+ }
+}
+
+func (s *apiTCL) DeleteTestWorkflowTemplateHandler() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ name := c.Params("id")
+ errPrefix := fmt.Sprintf("failed to delete test workflow template '%s'", name)
+ err := s.TestWorkflowTemplatesClient.Delete(name)
+ s.Metrics.IncDeleteTestWorkflowTemplate(err)
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+ return c.SendStatus(http.StatusNoContent)
+ }
+}
+
+func (s *apiTCL) DeleteTestWorkflowTemplatesHandler() fiber.Handler {
+ errPrefix := "failed to delete test workflow templates"
+ return func(c *fiber.Ctx) error {
+ selector := c.Query("selector")
+ err := s.TestWorkflowTemplatesClient.DeleteByLabels(selector)
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+ return c.SendStatus(http.StatusNoContent)
+ }
+}
+
+func (s *apiTCL) CreateTestWorkflowTemplateHandler() fiber.Handler {
+ errPrefix := "failed to create test workflow template"
+ return func(c *fiber.Ctx) (err error) {
+ // Deserialize resource
+ obj := new(testworkflowsv1.TestWorkflowTemplate)
+ if HasYAML(c) {
+ err = common.DeserializeCRD(obj, c.Body())
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "invalid body", err)
+ }
+ } else {
+ var v *testkube.TestWorkflowTemplate
+ err = c.BodyParser(&v)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "invalid body", err)
+ }
+ obj = mappers2.MapTemplateAPIToKube(v)
+ }
+
+ // Validate resource
+ if obj == nil || obj.Name == "" {
+ return s.BadRequest(c, errPrefix, "invalid body", errors.New("name is required"))
+ }
+ obj.Namespace = s.Namespace
+
+ // Create the resource
+ obj, err = s.TestWorkflowTemplatesClient.Create(obj)
+ s.Metrics.IncCreateTestWorkflowTemplate(err)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "client error", err)
+ }
+
+ err = SendResource(c, "TestWorkflowTemplate", testworkflowsv1.GroupVersion, mappers2.MapTemplateKubeToAPI, obj)
+ if err != nil {
+ return s.InternalError(c, errPrefix, "serialization problem", err)
+ }
+ return
+ }
+}
+
+func (s *apiTCL) UpdateTestWorkflowTemplateHandler() fiber.Handler {
+ errPrefix := "failed to update test workflow template"
+ return func(c *fiber.Ctx) (err error) {
+ name := c.Params("id")
+
+ // Deserialize resource
+ obj := new(testworkflowsv1.TestWorkflowTemplate)
+ if HasYAML(c) {
+ err = common.DeserializeCRD(obj, c.Body())
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "invalid body", err)
+ }
+ } else {
+ var v *testkube.TestWorkflowTemplate
+ err = c.BodyParser(&v)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "invalid body", err)
+ }
+ obj = mappers2.MapTemplateAPIToKube(v)
+ }
+
+ // Read existing resource
+ template, err := s.TestWorkflowTemplatesClient.Get(name)
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+
+ // Validate resource
+ if obj == nil {
+ return s.BadRequest(c, errPrefix, "invalid body", errors.New("body is required"))
+ }
+ obj.Namespace = template.Namespace
+ obj.Name = template.Name
+ obj.ResourceVersion = template.ResourceVersion
+
+ // Update the resource
+ obj, err = s.TestWorkflowTemplatesClient.Update(obj)
+ s.Metrics.IncUpdateTestWorkflowTemplate(err)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "client error", err)
+ }
+
+ err = SendResource(c, "TestWorkflowTemplate", testworkflowsv1.GroupVersion, mappers2.MapTemplateKubeToAPI, obj)
+ if err != nil {
+ return s.InternalError(c, errPrefix, "serialization problem", err)
+ }
+ return
+ }
+}
+
+func (s *apiTCL) getFilteredTestWorkflowTemplateList(c *fiber.Ctx) (*testworkflowsv1.TestWorkflowTemplateList, error) {
+ crTemplates, err := s.TestWorkflowTemplatesClient.List(c.Query("selector"))
+ if err != nil {
+ return nil, err
+ }
+
+ search := c.Query("textSearch")
+ if search != "" {
+ search = strings.ReplaceAll(search, "/", "--")
+ for i := len(crTemplates.Items) - 1; i >= 0; i-- {
+ if !strings.Contains(crTemplates.Items[i].Name, search) {
+ crTemplates.Items = append(crTemplates.Items[:i], crTemplates.Items[i+1:]...)
+ }
+ }
+ }
+
+ return crTemplates, nil
+}
diff --git a/pkg/tcl/apitcl/v1/testworkflowwithexecutions.go b/pkg/tcl/apitcl/v1/testworkflowwithexecutions.go
new file mode 100644
index 00000000000..b4e34d5c348
--- /dev/null
+++ b/pkg/tcl/apitcl/v1/testworkflowwithexecutions.go
@@ -0,0 +1,154 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package v1
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "sort"
+ "strconv"
+
+ "github.com/gofiber/fiber/v2"
+
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/repository/result"
+ testworkflowmappers "github.com/kubeshop/testkube/pkg/tcl/mapperstcl/testworkflows"
+)
+
+func (s *apiTCL) GetTestWorkflowWithExecutionHandler() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ name := c.Params("id")
+ errPrefix := fmt.Sprintf("failed to get test workflow '%s' with execution", name)
+ if name == "" {
+ return s.Error(c, http.StatusBadRequest, errors.New(errPrefix+": id cannot be empty"))
+ }
+ crWorkflow, err := s.TestWorkflowsClient.Get(name)
+ if err != nil {
+ return s.ClientError(c, errPrefix, err)
+ }
+
+ workflow := testworkflowmappers.MapKubeToAPI(crWorkflow)
+
+ ctx := c.Context()
+ execution, err := s.TestWorkflowResults.GetLatestByTestWorkflow(ctx, name)
+ if err != nil && !IsNotFound(err) {
+ return s.ClientError(c, errPrefix, err)
+ }
+
+ return c.JSON(testkube.TestWorkflowWithExecution{
+ Workflow: workflow,
+ LatestExecution: execution,
+ })
+ }
+}
+
+func (s *apiTCL) ListTestWorkflowWithExecutionsHandler() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ errPrefix := "failed to list test workflows with executions"
+ crWorkflows, err := s.getFilteredTestWorkflowList(c)
+ if err != nil {
+ return s.ClientError(c, errPrefix+": get filtered workflows", err)
+ }
+
+ workflows := testworkflowmappers.MapListKubeToAPI(crWorkflows)
+ ctx := c.Context()
+ results := make([]testkube.TestWorkflowWithExecutionSummary, 0, len(workflows))
+ workflowNames := make([]string, len(workflows))
+ for i := range workflows {
+ workflowNames[i] = workflows[i].Name
+ }
+
+ executions, err := s.TestWorkflowResults.GetLatestByTestWorkflows(ctx, workflowNames)
+ if err != nil {
+ return s.ClientError(c, errPrefix+": getting latest executions", err)
+ }
+ executionMap := make(map[string]testkube.TestWorkflowExecutionSummary, len(executions))
+ for i := range executions {
+ executionMap[executions[i].Workflow.Name] = executions[i]
+ }
+
+ for i := range workflows {
+ if execution, ok := executionMap[workflows[i].Name]; ok {
+ results = append(results, testkube.TestWorkflowWithExecutionSummary{
+ Workflow: &workflows[i],
+ LatestExecution: &execution,
+ })
+ } else {
+ results = append(results, testkube.TestWorkflowWithExecutionSummary{
+ Workflow: &workflows[i],
+ })
+ }
+ }
+
+ sort.Slice(results, func(i, j int) bool {
+ iTime := results[i].Workflow.Created
+ if results[i].LatestExecution != nil {
+ iTime = results[i].LatestExecution.StatusAt
+ }
+ jTime := results[j].Workflow.Created
+ if results[j].LatestExecution != nil {
+ jTime = results[j].LatestExecution.StatusAt
+ }
+ return iTime.After(jTime)
+ })
+
+ status := c.Query("status")
+ if status != "" {
+ statusList, err := testkube.ParseTestWorkflowStatusList(status, ",")
+ if err != nil {
+ return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: execution status filter invalid: %w", errPrefix, err))
+ }
+
+ statusMap := statusList.ToMap()
+ // filter items array
+ for i := len(results) - 1; i >= 0; i-- {
+ if results[i].LatestExecution != nil && results[i].LatestExecution.Result.Status != nil {
+ if _, ok := statusMap[*results[i].LatestExecution.Result.Status]; ok {
+ continue
+ }
+ }
+
+ results = append(results[:i], results[i+1:]...)
+ }
+ }
+
+ var page, pageSize int
+ pageParam := c.Query("page", "")
+ if pageParam != "" {
+ pageSize = result.PageDefaultLimit
+ page, err = strconv.Atoi(pageParam)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "workflow page filter invalid", err)
+ }
+ }
+
+ pageSizeParam := c.Query("pageSize", "")
+ if pageSizeParam != "" {
+ pageSize, err = strconv.Atoi(pageSizeParam)
+ if err != nil {
+ return s.BadRequest(c, errPrefix, "workflow page size filter invalid", err)
+ }
+ }
+
+ if pageParam != "" || pageSizeParam != "" {
+ startPos := page * pageSize
+ endPos := (page + 1) * pageSize
+ if startPos < len(results) {
+ if endPos > len(results) {
+ endPos = len(results)
+ }
+
+ results = results[startPos:endPos]
+ }
+ }
+
+ return c.JSON(results)
+ }
+}
diff --git a/pkg/tcl/apitcl/v1/utils.go b/pkg/tcl/apitcl/v1/utils.go
new file mode 100644
index 00000000000..1e00b44ef65
--- /dev/null
+++ b/pkg/tcl/apitcl/v1/utils.go
@@ -0,0 +1,80 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package v1
+
+import (
+ "github.com/gofiber/fiber/v2"
+ "github.com/pkg/errors"
+ "go.mongodb.org/mongo-driver/mongo"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+ k8serrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+
+ "github.com/kubeshop/testkube/internal/common"
+)
+
+const (
+ mediaTypeJSON = "application/json"
+ mediaTypeYAML = "text/yaml"
+ mediaTypePlainText = "text/plain"
+)
+
+func ExpectsYAML(c *fiber.Ctx) bool {
+ return c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML || c.Query("_yaml") == "true"
+}
+
+func HasYAML(c *fiber.Ctx) bool {
+ return string(c.Request().Header.ContentType()) == mediaTypeYAML
+}
+
+func SendResourceList[T interface{}, U interface{}](c *fiber.Ctx, kind string, groupVersion schema.GroupVersion, jsonMapper func(T) U, data ...T) error {
+ if ExpectsYAML(c) {
+ return SendCRDs(c, kind, groupVersion, data...)
+ }
+ result := make([]U, len(data))
+ for i, item := range data {
+ result[i] = jsonMapper(item)
+ }
+ return c.JSON(result)
+}
+
+func SendResource[T interface{}, U interface{}](c *fiber.Ctx, kind string, groupVersion schema.GroupVersion, jsonMapper func(T) U, data T) error {
+ if ExpectsYAML(c) {
+ return SendCRDs(c, kind, groupVersion, data)
+ }
+ return c.JSON(jsonMapper(data))
+}
+
+func SendCRDs[T interface{}](c *fiber.Ctx, kind string, groupVersion schema.GroupVersion, crds ...T) error {
+ b, err := common.SerializeCRDs(crds, common.SerializeOptions{
+ OmitCreationTimestamp: true,
+ CleanMeta: true,
+ Kind: kind,
+ GroupVersion: &groupVersion,
+ })
+ if err != nil {
+ return err
+ }
+ c.Context().SetContentType(mediaTypeYAML)
+ return c.Send(b)
+}
+
+func IsNotFound(err error) bool {
+ if err == nil {
+ return false
+ }
+ if errors.Is(err, mongo.ErrNoDocuments) || k8serrors.IsNotFound(err) {
+ return true
+ }
+ if e, ok := status.FromError(err); ok {
+ return e.Code() == codes.NotFound
+ }
+ return false
+}
diff --git a/pkg/tcl/checktcl/organization_plan.go b/pkg/tcl/checktcl/organization_plan.go
new file mode 100644
index 00000000000..f43c65524e1
--- /dev/null
+++ b/pkg/tcl/checktcl/organization_plan.go
@@ -0,0 +1,66 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package checktcl
+
+// Enterprise / Pro mode.
+type OrganizationPlanTestkubeMode string
+
+const (
+ OrganizationPlanTestkubeModeEnterprise OrganizationPlanTestkubeMode = "enterprise"
+ // TODO: Use "pro" in the future when refactoring TK Pro API server to use "pro" instead of "cloud"
+ OrganizationPlanTestkubeModePro OrganizationPlanTestkubeMode = "cloud"
+)
+
+// Ref: #/components/schemas/PlanStatus
+type PlanStatus string
+
+const (
+ PlanStatusActive PlanStatus = "Active"
+ PlanStatusCanceled PlanStatus = "Canceled"
+ PlanStatusIncomplete PlanStatus = "Incomplete"
+ PlanStatusIncompleteExpired PlanStatus = "IncompleteExpired"
+ PlanStatusPastDue PlanStatus = "PastDue"
+ PlanStatusTrailing PlanStatus = "Trailing"
+ PlanStatusUnpaid PlanStatus = "Unpaid"
+ PlanStatusDeleted PlanStatus = "Deleted"
+ PlanStatusLocked PlanStatus = "Locked"
+ PlanStatusBlocked PlanStatus = "Blocked"
+)
+
+// Ref: #/components/schemas/OrganizationPlan
+type OrganizationPlan struct {
+ // Enterprise / Pro mode.
+ TestkubeMode OrganizationPlanTestkubeMode `json:"testkubeMode"`
+ // Is current plan trial.
+ IsTrial bool `json:"isTrial"`
+ PlanStatus PlanStatus `json:"planStatus"`
+}
+
+func (p OrganizationPlan) IsEnterprise() bool {
+ return p.TestkubeMode == OrganizationPlanTestkubeModeEnterprise
+}
+
+func (p OrganizationPlan) IsPro() bool {
+ return p.TestkubeMode == OrganizationPlanTestkubeModePro
+}
+
+func (p OrganizationPlan) IsActive() bool {
+ return p.PlanStatus == PlanStatusActive
+}
+
+func (p OrganizationPlan) IsEmpty() bool {
+ return p.PlanStatus == "" && p.TestkubeMode == "" && !p.IsTrial
+}
+
+type GetOrganizationPlanRequest struct{}
+type GetOrganizationPlanResponse struct {
+ TestkubeMode string
+ IsTrial bool
+ PlanStatus string
+}
diff --git a/pkg/tcl/checktcl/subscription.go b/pkg/tcl/checktcl/subscription.go
new file mode 100644
index 00000000000..cb426e96b80
--- /dev/null
+++ b/pkg/tcl/checktcl/subscription.go
@@ -0,0 +1,102 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package checktcl
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/pkg/errors"
+ "google.golang.org/grpc"
+
+ "github.com/kubeshop/testkube/internal/config"
+ "github.com/kubeshop/testkube/pkg/cloud"
+ cloudconfig "github.com/kubeshop/testkube/pkg/cloud/data/config"
+ "github.com/kubeshop/testkube/pkg/cloud/data/executor"
+)
+
+type SubscriptionChecker struct {
+ proContext config.ProContext
+ orgPlan OrganizationPlan
+}
+
+// NewSubscriptionChecker creates a new subscription checker using the agent token
+func NewSubscriptionChecker(ctx context.Context, proContext config.ProContext, cloudClient cloud.TestKubeCloudAPIClient, grpcConn *grpc.ClientConn) (SubscriptionChecker, error) {
+ executor := executor.NewCloudGRPCExecutor(cloudClient, grpcConn, proContext.APIKey)
+
+ req := GetOrganizationPlanRequest{}
+ response, err := executor.Execute(ctx, cloudconfig.CmdConfigGetOrganizationPlan, req)
+ if err != nil {
+ return SubscriptionChecker{}, err
+ }
+
+ var commandResponse GetOrganizationPlanResponse
+ if err := json.Unmarshal(response, &commandResponse); err != nil {
+ return SubscriptionChecker{}, err
+ }
+
+ subscription := OrganizationPlan{
+ TestkubeMode: OrganizationPlanTestkubeMode(commandResponse.TestkubeMode),
+ IsTrial: commandResponse.IsTrial,
+ PlanStatus: PlanStatus(commandResponse.PlanStatus),
+ }
+
+ return SubscriptionChecker{proContext: proContext, orgPlan: subscription}, nil
+}
+
+// GetCurrentOrganizationPlan returns current organization plan
+func (c *SubscriptionChecker) GetCurrentOrganizationPlan() (OrganizationPlan, error) {
+ if c.orgPlan.IsEmpty() {
+ return OrganizationPlan{}, errors.New("organization plan is not set")
+ }
+ return c.orgPlan, nil
+}
+
+// IsOrgPlanEnterprise checks if organization plan is enterprise
+func (c *SubscriptionChecker) IsOrgPlanEnterprise() (bool, error) {
+ if c.orgPlan.IsEmpty() {
+ return false, errors.New("organization plan is not set")
+ }
+ return c.orgPlan.IsEnterprise(), nil
+}
+
+// IsOrgPlanCloud checks if organization plan is cloud
+func (c *SubscriptionChecker) IsOrgPlanPro() (bool, error) {
+ if c.orgPlan.IsEmpty() {
+ return false, errors.New("organization plan is not set")
+ }
+ return c.orgPlan.IsPro(), nil
+}
+
+// IsOrgPlanActive checks if organization plan is active
+func (c *SubscriptionChecker) IsOrgPlanActive() (bool, error) {
+ if c.orgPlan.IsEmpty() {
+ return false, errors.New("organization plan is not set")
+ }
+ return c.orgPlan.IsActive(), nil
+}
+
+// IsActiveOrgPlanEnterpriseForFeature checks if organization plan is active and enterprise for feature
+func (c *SubscriptionChecker) IsActiveOrgPlanEnterpriseForFeature(featureName string) error {
+ plan, err := c.GetCurrentOrganizationPlan()
+ if err != nil {
+ return errors.Wrap(err, fmt.Sprintf("%s is a commercial feature", featureName))
+ }
+
+ if !plan.IsActive() {
+ return errors.New(fmt.Sprintf("%s is not available: inactive subscription plan", featureName))
+ }
+
+ if !plan.IsEnterprise() {
+ return errors.New(fmt.Sprintf("%s is not allowed: wrong subscription plan", featureName))
+ }
+
+ return nil
+}
diff --git a/pkg/tcl/checktcl/subscription_test.go b/pkg/tcl/checktcl/subscription_test.go
new file mode 100644
index 00000000000..80182a7080a
--- /dev/null
+++ b/pkg/tcl/checktcl/subscription_test.go
@@ -0,0 +1,251 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package checktcl
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSubscriptionChecker_GetCurrentOrganizationPlan(t *testing.T) {
+ tests := []struct {
+ name string
+ orgPlan OrganizationPlan
+ want OrganizationPlan
+ wantErr bool
+ }{
+ {
+ name: "Org plan does not exist",
+ wantErr: true,
+ },
+ {
+ name: "Org plan exists",
+ orgPlan: OrganizationPlan{
+ TestkubeMode: OrganizationPlanTestkubeModeEnterprise,
+ IsTrial: false,
+ PlanStatus: PlanStatusActive,
+ },
+ want: OrganizationPlan{
+ TestkubeMode: OrganizationPlanTestkubeModeEnterprise,
+ IsTrial: false,
+ PlanStatus: PlanStatusActive,
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &SubscriptionChecker{
+ orgPlan: tt.orgPlan,
+ }
+ got, err := c.GetCurrentOrganizationPlan()
+ if (err != nil) != tt.wantErr {
+ t.Errorf("SubscriptionChecker.GetCurrentOrganizationPlan() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("SubscriptionChecker.GetCurrentOrganizationPlan() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestSubscriptionChecker_IsOrgPlanEnterprise(t *testing.T) {
+ tests := []struct {
+ name string
+ orgPlan OrganizationPlan
+ want bool
+ wantErr bool
+ }{
+ {
+ name: "no org plan",
+ wantErr: true,
+ },
+ {
+ name: "enterprise org plan",
+ orgPlan: OrganizationPlan{
+ TestkubeMode: OrganizationPlanTestkubeModeEnterprise,
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "pro org plan",
+ orgPlan: OrganizationPlan{
+ TestkubeMode: OrganizationPlanTestkubeModePro,
+ },
+ want: false,
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &SubscriptionChecker{
+ orgPlan: tt.orgPlan,
+ }
+ got, err := c.IsOrgPlanEnterprise()
+ if (err != nil) != tt.wantErr {
+ t.Errorf("SubscriptionChecker.IsOrgPlanEnterprise() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("SubscriptionChecker.IsOrgPlanEnterprise() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestSubscriptionChecker_IsOrgPlanPro(t *testing.T) {
+ tests := []struct {
+ name string
+ orgPlan OrganizationPlan
+ want bool
+ wantErr bool
+ }{
+ {
+ name: "no org plan",
+ wantErr: true,
+ },
+ {
+ name: "enterprise org plan",
+ orgPlan: OrganizationPlan{
+ TestkubeMode: OrganizationPlanTestkubeModeEnterprise,
+ },
+ want: false,
+ wantErr: false,
+ },
+ {
+ name: "pro org plan",
+ orgPlan: OrganizationPlan{
+ TestkubeMode: OrganizationPlanTestkubeModePro,
+ },
+ want: true,
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &SubscriptionChecker{
+ orgPlan: tt.orgPlan,
+ }
+ got, err := c.IsOrgPlanPro()
+ if (err != nil) != tt.wantErr {
+ t.Errorf("SubscriptionChecker.IsOrgPlanPro() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("SubscriptionChecker.IsOrgPlanPro() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestSubscriptionChecker_IsOrgPlanActive(t *testing.T) {
+ tests := []struct {
+ name string
+ orgPlan OrganizationPlan
+ want bool
+ wantErr bool
+ }{
+ {
+ name: "no org plan",
+ wantErr: true,
+ },
+ {
+ name: "active org plan",
+ orgPlan: OrganizationPlan{
+ PlanStatus: PlanStatusActive,
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "inactive org plan",
+ orgPlan: OrganizationPlan{
+ PlanStatus: PlanStatusUnpaid,
+ },
+ want: false,
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &SubscriptionChecker{
+ orgPlan: tt.orgPlan,
+ }
+ got, err := c.IsOrgPlanActive()
+ if (err != nil) != tt.wantErr {
+ t.Errorf("SubscriptionChecker.IsOrgPlanActive() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("SubscriptionChecker.IsOrgPlanActive() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestSubscriptionChecker_IsActiveOrgPlanEnterpriseForFeature(t *testing.T) {
+ featureName := "feature"
+ tests := []struct {
+ name string
+ orgPlan OrganizationPlan
+ err error
+ }{
+ {
+ name: "enterprise active org plan",
+ orgPlan: OrganizationPlan{
+ TestkubeMode: OrganizationPlanTestkubeModeEnterprise,
+ IsTrial: false,
+ PlanStatus: PlanStatusActive,
+ },
+ err: nil,
+ },
+ {
+ name: "no org plan",
+ err: fmt.Errorf("%s is a commercial feature: organization plan is not set", featureName),
+ },
+ {
+ name: "enterprise inactive org plan",
+ orgPlan: OrganizationPlan{
+ TestkubeMode: OrganizationPlanTestkubeModeEnterprise,
+ IsTrial: false,
+ PlanStatus: PlanStatusUnpaid,
+ },
+ err: fmt.Errorf("%s is not available: inactive subscription plan", featureName),
+ },
+ {
+ name: "non enterprise actibe org plan",
+ orgPlan: OrganizationPlan{
+ TestkubeMode: OrganizationPlanTestkubeModePro,
+ IsTrial: false,
+ PlanStatus: PlanStatusActive,
+ },
+ err: fmt.Errorf("%s is not allowed: wrong subscription plan", featureName),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &SubscriptionChecker{
+ orgPlan: tt.orgPlan,
+ }
+
+ err := c.IsActiveOrgPlanEnterpriseForFeature(featureName)
+ if tt.err != nil {
+ assert.EqualError(t, err, tt.err.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
diff --git a/pkg/tcl/cloudtcl/data/testworkflow/commands.go b/pkg/tcl/cloudtcl/data/testworkflow/commands.go
new file mode 100644
index 00000000000..548c5d866cb
--- /dev/null
+++ b/pkg/tcl/cloudtcl/data/testworkflow/commands.go
@@ -0,0 +1,79 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflow
+
+import "github.com/kubeshop/testkube/pkg/cloud/data/executor"
+
+const (
+ CmdTestWorkflowExecutionGet executor.Command = "workflow_execution_get"
+ CmdTestWorkflowExecutionGetByNameAndWorkflow executor.Command = "workflow_execution_get_by_name_and_workflow"
+ CmdTestWorkflowExecutionGetLatestByWorkflow executor.Command = "workflow_execution_get_latest_by_workflow"
+ CmdTestWorkflowExecutionGetRunning executor.Command = "workflow_execution_get_running"
+ CmdTestWorkflowExecutionGetLatestByWorkflows executor.Command = "workflow_execution_get_latest_by_workflows"
+ CmdTestWorkflowExecutionGetExecutionTotals executor.Command = "workflow_execution_get_execution_totals"
+ CmdTestWorkflowExecutionGetExecutions executor.Command = "workflow_execution_get_executions"
+ CmdTestWorkflowExecutionGetExecutionsSummary executor.Command = "workflow_execution_get_executions_summary"
+ CmdTestWorkflowExecutionInsert executor.Command = "workflow_execution_insert"
+ CmdTestWorkflowExecutionUpdate executor.Command = "workflow_execution_update"
+ CmdTestWorkflowExecutionUpdateResult executor.Command = "workflow_execution_update_result"
+ CmdTestWorkflowExecutionUpdateOutput executor.Command = "workflow_execution_update_output"
+ CmdTestWorkflowExecutionDeleteByWorkflow executor.Command = "workflow_execution_delete_by_workflow"
+ CmdTestWorkflowExecutionDeleteAll executor.Command = "workflow_execution_delete_all"
+ CmdTestWorkflowExecutionDeleteByWorkflows executor.Command = "workflow_execution_delete_by_workflows"
+ CmdTestWorkflowExecutionGetWorkflowMetrics executor.Command = "workflow_execution_get_workflow_metrics"
+
+ CmdTestWorkflowOutputPresignSaveLog executor.Command = "workflow_output_presign_save_log"
+ CmdTestWorkflowOutputPresignReadLog executor.Command = "workflow_output_presign_read_log"
+ CmdTestWorkflowOutputHasLog executor.Command = "workflow_output_has_log"
+)
+
+func command(v interface{}) executor.Command {
+ switch v.(type) {
+ case ExecutionGetRequest:
+ return CmdTestWorkflowExecutionGet
+ case ExecutionGetByNameAndWorkflowRequest:
+ return CmdTestWorkflowExecutionGetByNameAndWorkflow
+ case ExecutionGetLatestByWorkflowRequest:
+ return CmdTestWorkflowExecutionGetLatestByWorkflow
+ case ExecutionGetRunningRequest:
+ return CmdTestWorkflowExecutionGetRunning
+ case ExecutionGetLatestByWorkflowsRequest:
+ return CmdTestWorkflowExecutionGetLatestByWorkflows
+ case ExecutionGetExecutionTotalsRequest:
+ return CmdTestWorkflowExecutionGetExecutionTotals
+ case ExecutionGetExecutionsRequest:
+ return CmdTestWorkflowExecutionGetExecutions
+ case ExecutionGetExecutionsSummaryRequest:
+ return CmdTestWorkflowExecutionGetExecutionsSummary
+ case ExecutionInsertRequest:
+ return CmdTestWorkflowExecutionInsert
+ case ExecutionUpdateRequest:
+ return CmdTestWorkflowExecutionUpdate
+ case ExecutionUpdateResultRequest:
+ return CmdTestWorkflowExecutionUpdateResult
+ case ExecutionUpdateOutputRequest:
+ return CmdTestWorkflowExecutionUpdateOutput
+ case ExecutionDeleteByWorkflowRequest:
+ return CmdTestWorkflowExecutionDeleteByWorkflow
+ case ExecutionDeleteAllRequest:
+ return CmdTestWorkflowExecutionDeleteAll
+ case ExecutionDeleteByWorkflowsRequest:
+ return CmdTestWorkflowExecutionDeleteByWorkflows
+ case ExecutionGetWorkflowMetricsRequest:
+ return CmdTestWorkflowExecutionGetWorkflowMetrics
+
+ case OutputPresignSaveLogRequest:
+ return CmdTestWorkflowOutputPresignSaveLog
+ case OutputPresignReadLogRequest:
+ return CmdTestWorkflowOutputPresignReadLog
+ case OutputHasLogRequest:
+ return CmdTestWorkflowOutputHasLog
+ }
+ panic("unknown test workflows Cloud request")
+}
diff --git a/pkg/tcl/cloudtcl/data/testworkflow/execution.go b/pkg/tcl/cloudtcl/data/testworkflow/execution.go
new file mode 100644
index 00000000000..a960a7d18f3
--- /dev/null
+++ b/pkg/tcl/cloudtcl/data/testworkflow/execution.go
@@ -0,0 +1,142 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflow
+
+import (
+ "context"
+
+ "github.com/kubeshop/testkube/pkg/tcl/repositorytcl/testworkflow"
+
+ "google.golang.org/grpc"
+
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/cloud"
+ "github.com/kubeshop/testkube/pkg/cloud/data/executor"
+)
+
+var _ testworkflow.Repository = (*CloudRepository)(nil)
+
+type CloudRepository struct {
+ executor executor.Executor
+}
+
+func NewCloudRepository(client cloud.TestKubeCloudAPIClient, grpcConn *grpc.ClientConn, apiKey string) *CloudRepository {
+ return &CloudRepository{executor: executor.NewCloudGRPCExecutor(client, grpcConn, apiKey)}
+}
+
+func (r *CloudRepository) Get(ctx context.Context, id string) (testkube.TestWorkflowExecution, error) {
+ req := ExecutionGetRequest{ID: id}
+ process := func(v ExecutionGetResponse) testkube.TestWorkflowExecution {
+ return v.WorkflowExecution
+ }
+ return pass(r.executor, ctx, req, process)
+}
+
+func (r *CloudRepository) GetByNameAndTestWorkflow(ctx context.Context, name, workflowName string) (result testkube.TestWorkflowExecution, err error) {
+ req := ExecutionGetByNameAndWorkflowRequest{Name: name, WorkflowName: workflowName}
+ process := func(v ExecutionGetResponse) testkube.TestWorkflowExecution {
+ return v.WorkflowExecution
+ }
+ return pass(r.executor, ctx, req, process)
+}
+
+func (r *CloudRepository) GetLatestByTestWorkflow(ctx context.Context, workflowName string) (*testkube.TestWorkflowExecution, error) {
+ req := ExecutionGetLatestByWorkflowRequest{WorkflowName: workflowName}
+ process := func(v ExecutionGetLatestByWorkflowResponse) *testkube.TestWorkflowExecution {
+ return v.WorkflowExecution
+ }
+ return pass(r.executor, ctx, req, process)
+}
+
+func (r *CloudRepository) GetLatestByTestWorkflows(ctx context.Context, workflowNames []string) (executions []testkube.TestWorkflowExecutionSummary, err error) {
+ req := ExecutionGetLatestByWorkflowsRequest{WorkflowNames: workflowNames}
+ process := func(v ExecutionGetLatestByWorkflowsResponse) []testkube.TestWorkflowExecutionSummary {
+ return v.WorkflowExecutions
+ }
+ return pass(r.executor, ctx, req, process)
+}
+
+func (r *CloudRepository) GetRunning(ctx context.Context) (result []testkube.TestWorkflowExecution, err error) {
+ req := ExecutionGetRunningRequest{}
+ process := func(v ExecutionGetRunningResponse) []testkube.TestWorkflowExecution {
+ return v.WorkflowExecutions
+ }
+ return pass(r.executor, ctx, req, process)
+}
+
+func (r *CloudRepository) GetExecutionsTotals(ctx context.Context, filter ...testworkflow.Filter) (totals testkube.ExecutionsTotals, err error) {
+ req := ExecutionGetExecutionTotalsRequest{Filter: mapFilters(filter)}
+ process := func(v ExecutionGetExecutionTotalsResponse) testkube.ExecutionsTotals {
+ return v.Totals
+ }
+ return pass(r.executor, ctx, req, process)
+}
+
+func (r *CloudRepository) GetExecutions(ctx context.Context, filter testworkflow.Filter) (result []testkube.TestWorkflowExecution, err error) {
+ req := ExecutionGetExecutionsRequest{Filter: filter.(*testworkflow.FilterImpl)}
+ process := func(v ExecutionGetExecutionsResponse) []testkube.TestWorkflowExecution {
+ return v.WorkflowExecutions
+ }
+ return pass(r.executor, ctx, req, process)
+}
+
+func (r *CloudRepository) GetExecutionsSummary(ctx context.Context, filter testworkflow.Filter) (result []testkube.TestWorkflowExecutionSummary, err error) {
+ req := ExecutionGetExecutionsSummaryRequest{Filter: filter.(*testworkflow.FilterImpl)}
+ process := func(v ExecutionGetExecutionsSummaryResponse) []testkube.TestWorkflowExecutionSummary {
+ return v.WorkflowExecutions
+ }
+ return pass(r.executor, ctx, req, process)
+}
+
+func (r *CloudRepository) Insert(ctx context.Context, result testkube.TestWorkflowExecution) (err error) {
+ req := ExecutionInsertRequest{WorkflowExecution: result}
+ return passNoContent(r.executor, ctx, req)
+}
+
+func (r *CloudRepository) Update(ctx context.Context, result testkube.TestWorkflowExecution) (err error) {
+ req := ExecutionUpdateRequest{WorkflowExecution: result}
+ return passNoContent(r.executor, ctx, req)
+}
+
+func (r *CloudRepository) UpdateResult(ctx context.Context, id string, result *testkube.TestWorkflowResult) (err error) {
+ req := ExecutionUpdateResultRequest{ID: id, Result: result}
+ return passNoContent(r.executor, ctx, req)
+}
+
+func (r *CloudRepository) UpdateOutput(ctx context.Context, id string, output []testkube.TestWorkflowOutput) (err error) {
+ req := ExecutionUpdateOutputRequest{ID: id, Output: output}
+ return passNoContent(r.executor, ctx, req)
+}
+
+// DeleteByTestWorkflow deletes execution results by workflow
+func (r *CloudRepository) DeleteByTestWorkflow(ctx context.Context, workflowName string) (err error) {
+ req := ExecutionDeleteByWorkflowRequest{WorkflowName: workflowName}
+ return passNoContent(r.executor, ctx, req)
+}
+
+// DeleteAll deletes all execution results
+func (r *CloudRepository) DeleteAll(ctx context.Context) (err error) {
+ req := ExecutionDeleteAllRequest{}
+ return passNoContent(r.executor, ctx, req)
+}
+
+// DeleteByTestWorkflows deletes execution results by workflows
+func (r *CloudRepository) DeleteByTestWorkflows(ctx context.Context, workflowNames []string) (err error) {
+ req := ExecutionDeleteByWorkflowsRequest{WorkflowNames: workflowNames}
+ return passNoContent(r.executor, ctx, req)
+}
+
+// GetTestWorkflowMetrics returns test executions metrics
+func (r *CloudRepository) GetTestWorkflowMetrics(ctx context.Context, name string, limit, last int) (metrics testkube.ExecutionsMetrics, err error) {
+ req := ExecutionGetWorkflowMetricsRequest{Name: name, Limit: limit, Last: last}
+ process := func(v ExecutionGetWorkflowMetricsResponse) testkube.ExecutionsMetrics {
+ return v.Metrics
+ }
+ return pass(r.executor, ctx, req, process)
+}
diff --git a/pkg/tcl/cloudtcl/data/testworkflow/execution_models.go b/pkg/tcl/cloudtcl/data/testworkflow/execution_models.go
new file mode 100644
index 00000000000..cbf41943f67
--- /dev/null
+++ b/pkg/tcl/cloudtcl/data/testworkflow/execution_models.go
@@ -0,0 +1,138 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflow
+
+import (
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/tcl/repositorytcl/testworkflow"
+)
+
+type ExecutionGetRequest struct {
+ ID string `json:"id"`
+}
+
+type ExecutionGetResponse struct {
+ WorkflowExecution testkube.TestWorkflowExecution `json:"workflowExecution"`
+}
+
+type ExecutionGetByNameAndWorkflowRequest struct {
+ Name string `json:"name"`
+ WorkflowName string `json:"workflowName"`
+}
+
+type ExecutionGetByNameAndWorkflowResponse struct {
+ WorkflowExecution testkube.TestWorkflowExecution `json:"workflowExecution"`
+}
+
+type ExecutionGetLatestByWorkflowRequest struct {
+ WorkflowName string `json:"workflowName"`
+}
+
+type ExecutionGetLatestByWorkflowResponse struct {
+ WorkflowExecution *testkube.TestWorkflowExecution `json:"workflowExecution"`
+}
+
+type ExecutionGetRunningRequest struct {
+}
+
+type ExecutionGetRunningResponse struct {
+ WorkflowExecutions []testkube.TestWorkflowExecution `json:"workflowExecutions"`
+}
+
+type ExecutionGetLatestByWorkflowsRequest struct {
+ WorkflowNames []string `json:"workflowNames"`
+}
+
+type ExecutionGetLatestByWorkflowsResponse struct {
+ WorkflowExecutions []testkube.TestWorkflowExecutionSummary `json:"workflowExecutions"`
+}
+
+type ExecutionGetExecutionTotalsRequest struct {
+ Filter []*testworkflow.FilterImpl `json:"filter"`
+}
+
+type ExecutionGetExecutionTotalsResponse struct {
+ Totals testkube.ExecutionsTotals `json:"totals"`
+}
+
+type ExecutionGetExecutionsRequest struct {
+ Filter *testworkflow.FilterImpl `json:"filter"`
+}
+
+type ExecutionGetExecutionsResponse struct {
+ WorkflowExecutions []testkube.TestWorkflowExecution `json:"workflowExecutions"`
+}
+
+type ExecutionGetExecutionsSummaryRequest struct {
+ Filter *testworkflow.FilterImpl `json:"filter"`
+}
+
+type ExecutionGetExecutionsSummaryResponse struct {
+ WorkflowExecutions []testkube.TestWorkflowExecutionSummary `json:"workflowExecutions"`
+}
+
+type ExecutionInsertRequest struct {
+ WorkflowExecution testkube.TestWorkflowExecution `json:"workflowExecution"`
+}
+
+type ExecutionInsertResponse struct {
+}
+
+type ExecutionUpdateRequest struct {
+ WorkflowExecution testkube.TestWorkflowExecution `json:"workflowExecution"`
+}
+
+type ExecutionUpdateResponse struct {
+}
+
+type ExecutionUpdateResultRequest struct {
+ ID string `json:"id"`
+ Result *testkube.TestWorkflowResult `json:"result"`
+}
+
+type ExecutionUpdateResultResponse struct {
+}
+
+type ExecutionUpdateOutputRequest struct {
+ ID string `json:"id"`
+ Output []testkube.TestWorkflowOutput `json:"output"`
+}
+
+type ExecutionUpdateOutputResponse struct {
+}
+
+type ExecutionDeleteByWorkflowRequest struct {
+ WorkflowName string `json:"workflowName"`
+}
+
+type ExecutionDeleteByWorkflowResponse struct {
+}
+
+type ExecutionDeleteAllRequest struct {
+}
+
+type ExecutionDeleteAllResponse struct {
+}
+
+type ExecutionDeleteByWorkflowsRequest struct {
+ WorkflowNames []string `json:"workflowNames"`
+}
+
+type ExecutionDeleteByWorkflowsResponse struct {
+}
+
+type ExecutionGetWorkflowMetricsRequest struct {
+ Name string `json:"name"`
+ Limit int `json:"limit"`
+ Last int `json:"last"`
+}
+
+type ExecutionGetWorkflowMetricsResponse struct {
+ Metrics testkube.ExecutionsMetrics `json:"metrics"`
+}
diff --git a/pkg/tcl/cloudtcl/data/testworkflow/output.go b/pkg/tcl/cloudtcl/data/testworkflow/output.go
new file mode 100644
index 00000000000..94ffdceff91
--- /dev/null
+++ b/pkg/tcl/cloudtcl/data/testworkflow/output.go
@@ -0,0 +1,107 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflow
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "net/http"
+
+ "github.com/kubeshop/testkube/pkg/tcl/repositorytcl/testworkflow"
+
+ "github.com/pkg/errors"
+ "google.golang.org/grpc"
+
+ "github.com/kubeshop/testkube/pkg/cloud"
+ "github.com/kubeshop/testkube/pkg/cloud/data/executor"
+)
+
+var _ testworkflow.OutputRepository = (*CloudOutputRepository)(nil)
+
+type CloudOutputRepository struct {
+ executor executor.Executor
+}
+
+func NewCloudOutputRepository(client cloud.TestKubeCloudAPIClient, grpcConn *grpc.ClientConn, apiKey string) *CloudOutputRepository {
+ return &CloudOutputRepository{executor: executor.NewCloudGRPCExecutor(client, grpcConn, apiKey)}
+}
+
+// PresignSaveLog builds presigned storage URL to save the output in Cloud
+func (r *CloudOutputRepository) PresignSaveLog(ctx context.Context, id, workflowName string) (string, error) {
+ req := OutputPresignSaveLogRequest{ID: id, WorkflowName: workflowName}
+ process := func(v OutputPresignSaveLogResponse) string {
+ return v.URL
+ }
+ return pass(r.executor, ctx, req, process)
+}
+
+// PresignReadLog builds presigned storage URL to read the output from Cloud
+func (r *CloudOutputRepository) PresignReadLog(ctx context.Context, id, workflowName string) (string, error) {
+ req := OutputPresignReadLogRequest{ID: id, WorkflowName: workflowName}
+ process := func(v OutputPresignReadLogResponse) string {
+ return v.URL
+ }
+ return pass(r.executor, ctx, req, process)
+}
+
+// SaveLog streams the output from the workflow to Cloud
+func (r *CloudOutputRepository) SaveLog(ctx context.Context, id, workflowName string, reader io.Reader) error {
+ url, err := r.PresignSaveLog(ctx, id, workflowName)
+ if err != nil {
+ return err
+ }
+ // FIXME: It should stream instead
+ data, err := io.ReadAll(reader)
+ if err != nil {
+ return err
+ }
+ req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bytes.NewBuffer(data))
+ req.Header.Add("Content-Type", "application/octet-stream")
+ if err != nil {
+ return err
+ }
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return errors.Wrap(err, "failed to save file in cloud storage")
+ }
+ if res.StatusCode != http.StatusOK {
+ return errors.Errorf("error saving file with presigned url: expected 200 OK response code, got %d", res.StatusCode)
+ }
+ return nil
+}
+
+// ReadLog streams the output from Cloud
+func (r *CloudOutputRepository) ReadLog(ctx context.Context, id, workflowName string) (io.Reader, error) {
+ url, err := r.PresignReadLog(ctx, id, workflowName)
+ if err != nil {
+ return nil, err
+ }
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
+ if err != nil {
+ return nil, err
+ }
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to get file from cloud storage")
+ }
+ if res.StatusCode != http.StatusOK {
+ return nil, errors.Errorf("error getting file from presigned url: expected 200 OK response code, got %d", res.StatusCode)
+ }
+ return res.Body, nil
+}
+
+// HasLog checks if there is an output in Cloud
+func (r *CloudOutputRepository) HasLog(ctx context.Context, id, workflowName string) (bool, error) {
+ req := OutputHasLogRequest{ID: id, WorkflowName: workflowName}
+ process := func(v OutputHasLogResponse) bool {
+ return v.Has
+ }
+ return pass(r.executor, ctx, req, process)
+}
diff --git a/pkg/tcl/cloudtcl/data/testworkflow/output_models.go b/pkg/tcl/cloudtcl/data/testworkflow/output_models.go
new file mode 100644
index 00000000000..df941c9fe6b
--- /dev/null
+++ b/pkg/tcl/cloudtcl/data/testworkflow/output_models.go
@@ -0,0 +1,36 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflow
+
+type OutputPresignSaveLogRequest struct {
+ ID string `json:"id"`
+ WorkflowName string `json:"workflowName"`
+}
+
+type OutputPresignSaveLogResponse struct {
+ URL string `json:"url"`
+}
+
+type OutputPresignReadLogRequest struct {
+ ID string `json:"id"`
+ WorkflowName string `json:"workflowName"`
+}
+
+type OutputPresignReadLogResponse struct {
+ URL string `json:"url"`
+}
+
+type OutputHasLogRequest struct {
+ ID string `json:"id"`
+ WorkflowName string `json:"workflowName"`
+}
+
+type OutputHasLogResponse struct {
+ Has bool `json:"has"`
+}
diff --git a/pkg/tcl/cloudtcl/data/testworkflow/utils.go b/pkg/tcl/cloudtcl/data/testworkflow/utils.go
new file mode 100644
index 00000000000..ba9f3a80ce4
--- /dev/null
+++ b/pkg/tcl/cloudtcl/data/testworkflow/utils.go
@@ -0,0 +1,60 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflow
+
+import (
+ "context"
+ "encoding/json"
+
+ "github.com/kubeshop/testkube/pkg/cloud/data/executor"
+ "github.com/kubeshop/testkube/pkg/tcl/repositorytcl/testworkflow"
+)
+
+func passWithErr[T any, U any](e executor.Executor, ctx context.Context, req interface{}, fn func(u T) (U, error)) (v U, err error) {
+ response, err := e.Execute(ctx, command(req), req)
+ if err != nil {
+ return v, err
+ }
+ var commandResponse T
+ if err = json.Unmarshal(response, &commandResponse); err != nil {
+ return v, err
+ }
+ return fn(commandResponse)
+}
+
+func pass[T any, U any](e executor.Executor, ctx context.Context, req interface{}, fn func(u T) U) (v U, err error) {
+ return passWithErr(e, ctx, req, func(u T) (U, error) {
+ return fn(u), nil
+ })
+}
+
+func passNoContentProcess[T any](e executor.Executor, ctx context.Context, req interface{}, fn func(u T) error) (err error) {
+ _, err = passWithErr(e, ctx, req, func(u T) (interface{}, error) {
+ return nil, fn(u)
+ })
+ return err
+}
+
+func passNoContent(e executor.Executor, ctx context.Context, req interface{}) (err error) {
+ return passNoContentProcess(e, ctx, req, func(u interface{}) error {
+ return nil
+ })
+}
+
+func mapFilters(s []testworkflow.Filter) []*testworkflow.FilterImpl {
+ v := make([]*testworkflow.FilterImpl, len(s))
+ for i := range s {
+ if vv, ok := s[i].(testworkflow.FilterImpl); ok {
+ v[i] = &vv
+ } else {
+ v[i] = s[i].(*testworkflow.FilterImpl)
+ }
+ }
+ return v
+}
diff --git a/pkg/tcl/expressionstcl/accessor.go b/pkg/tcl/expressionstcl/accessor.go
new file mode 100644
index 00000000000..1e1cadf0818
--- /dev/null
+++ b/pkg/tcl/expressionstcl/accessor.go
@@ -0,0 +1,70 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "fmt"
+)
+
+type accessor struct {
+ name string
+}
+
+func newAccessor(name string) Expression {
+ return &accessor{name: name}
+}
+
+func (s *accessor) Type() Type {
+ return TypeUnknown
+}
+
+func (s *accessor) String() string {
+ return s.name
+}
+
+func (s *accessor) SafeString() string {
+ return s.String()
+}
+
+func (s *accessor) Template() string {
+ return "{{" + s.String() + "}}"
+}
+
+func (s *accessor) SafeResolve(m ...Machine) (v Expression, changed bool, err error) {
+ if m == nil {
+ return s, false, nil
+ }
+
+ for i := range m {
+ result, ok, err := m[i].Get(s.name)
+ if err != nil {
+ return nil, false, fmt.Errorf("error while accessing %s: %s", s.String(), err.Error())
+ }
+ if ok {
+ return result, true, nil
+ }
+ }
+ return s, false, nil
+}
+
+func (s *accessor) Resolve(m ...Machine) (v Expression, err error) {
+ return deepResolve(s, m...)
+}
+
+func (s *accessor) Static() StaticValue {
+ return nil
+}
+
+func (s *accessor) Accessors() map[string]struct{} {
+ return map[string]struct{}{s.name: {}}
+}
+
+func (s *accessor) Functions() map[string]struct{} {
+ return nil
+}
diff --git a/pkg/tcl/expressionstcl/call.go b/pkg/tcl/expressionstcl/call.go
new file mode 100644
index 00000000000..5f2492fc936
--- /dev/null
+++ b/pkg/tcl/expressionstcl/call.go
@@ -0,0 +1,132 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "fmt"
+ "maps"
+ "strings"
+)
+
+type call struct {
+ name string
+ args []Expression
+}
+
+func newCall(name string, args []Expression) Expression {
+ for i := range args {
+ if args[i] == nil {
+ args[i] = None
+ }
+ }
+ return &call{name: name, args: args}
+}
+
+func (s *call) Type() Type {
+ if IsStdFunction(s.name) {
+ return GetStdFunctionReturnType(s.name)
+ }
+ return TypeUnknown
+}
+
+func (s *call) String() string {
+ args := make([]string, len(s.args))
+ for i, arg := range s.args {
+ args[i] = arg.String()
+ }
+ return fmt.Sprintf("%s(%s)", s.name, strings.Join(args, ","))
+}
+
+func (s *call) SafeString() string {
+ return s.String()
+}
+
+func (s *call) Template() string {
+ if s.name == stringCastStdFn {
+ args := make([]string, len(s.args))
+ for i, a := range s.args {
+ args[i] = a.Template()
+ }
+ return strings.Join(args, "")
+ }
+ return "{{" + s.String() + "}}"
+}
+
+func (s *call) isResolved() bool {
+ for i := range s.args {
+ if s.args[i].Static() == nil {
+ return false
+ }
+ }
+ return true
+}
+
+func (s *call) resolvedArgs() []StaticValue {
+ v := make([]StaticValue, len(s.args))
+ for i, vv := range s.args {
+ v[i] = vv.Static()
+ }
+ return v
+}
+
+func (s *call) SafeResolve(m ...Machine) (v Expression, changed bool, err error) {
+ var ch bool
+ for i := range s.args {
+ s.args[i], ch, err = s.args[i].SafeResolve(m...)
+ changed = changed || ch
+ if err != nil {
+ return nil, changed, err
+ }
+ }
+ if s.isResolved() {
+ args := s.resolvedArgs()
+ result, ok, err := StdLibMachine.Call(s.name, args...)
+ if ok {
+ if err != nil {
+ return nil, true, fmt.Errorf("error while calling %s: %s", s.String(), err.Error())
+ }
+ return result, true, nil
+ }
+ for i := range m {
+ result, ok, err = m[i].Call(s.name, args...)
+ if err != nil {
+ return nil, true, fmt.Errorf("error while calling %s: %s", s.String(), err.Error())
+ }
+ if ok {
+ return result, true, nil
+ }
+ }
+ }
+ return s, changed, nil
+}
+
+func (s *call) Resolve(m ...Machine) (v Expression, err error) {
+ return deepResolve(s, m...)
+}
+
+func (s *call) Static() StaticValue {
+ return nil
+}
+
+func (s *call) Accessors() map[string]struct{} {
+ result := make(map[string]struct{})
+ for i := range s.args {
+ maps.Copy(result, s.args[i].Accessors())
+ }
+ return result
+}
+
+func (s *call) Functions() map[string]struct{} {
+ result := make(map[string]struct{})
+ for i := range s.args {
+ maps.Copy(result, s.args[i].Functions())
+ }
+ result[s.name] = struct{}{}
+ return result
+}
diff --git a/pkg/tcl/expressionstcl/conditional.go b/pkg/tcl/expressionstcl/conditional.go
new file mode 100644
index 00000000000..69b6ff94b3d
--- /dev/null
+++ b/pkg/tcl/expressionstcl/conditional.go
@@ -0,0 +1,109 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "fmt"
+ "maps"
+)
+
+type conditional struct {
+ condition Expression
+ truthy Expression
+ falsy Expression
+}
+
+func newConditional(condition, truthy, falsy Expression) Expression {
+ if condition == nil {
+ condition = None
+ }
+ if truthy == nil {
+ truthy = None
+ }
+ if falsy == nil {
+ falsy = None
+ }
+ return &conditional{condition: condition, truthy: truthy, falsy: falsy}
+}
+
+func (s *conditional) Type() Type {
+ r1 := s.truthy.Type()
+ r2 := s.falsy.Type()
+ if r1 == r2 {
+ return r1
+ }
+ return TypeUnknown
+}
+
+func (s *conditional) String() string {
+ return fmt.Sprintf("%s ? %s : %s", s.condition.String(), s.truthy.String(), s.falsy.String())
+}
+
+func (s *conditional) SafeString() string {
+ return "(" + s.String() + ")"
+}
+
+func (s *conditional) Template() string {
+ return "{{" + s.String() + "}}"
+}
+
+func (s *conditional) SafeResolve(m ...Machine) (v Expression, changed bool, err error) {
+ var ch bool
+ s.condition, ch, err = s.condition.SafeResolve(m...)
+ changed = changed || ch
+ if err != nil {
+ return nil, changed, err
+ }
+ if s.condition.Static() != nil {
+ var b bool
+ b, err = s.condition.Static().BoolValue()
+ if err != nil {
+ return nil, true, err
+ }
+ if b {
+ return s.truthy, true, err
+ }
+ return s.falsy, true, err
+ }
+ s.truthy, ch, err = s.truthy.SafeResolve(m...)
+ changed = changed || ch
+ if err != nil {
+ return nil, changed, err
+ }
+ s.falsy, ch, err = s.falsy.SafeResolve(m...)
+ changed = changed || ch
+ if err != nil {
+ return nil, changed, err
+ }
+ return s, changed, nil
+}
+
+func (s *conditional) Resolve(m ...Machine) (v Expression, err error) {
+ return deepResolve(s, m...)
+}
+
+func (s *conditional) Static() StaticValue {
+ return nil
+}
+
+func (s *conditional) Accessors() map[string]struct{} {
+ result := make(map[string]struct{})
+ maps.Copy(result, s.condition.Accessors())
+ maps.Copy(result, s.truthy.Accessors())
+ maps.Copy(result, s.falsy.Accessors())
+ return result
+}
+
+func (s *conditional) Functions() map[string]struct{} {
+ result := make(map[string]struct{})
+ maps.Copy(result, s.condition.Functions())
+ maps.Copy(result, s.truthy.Functions())
+ maps.Copy(result, s.falsy.Functions())
+ return result
+}
diff --git a/pkg/tcl/expressionstcl/convert.go b/pkg/tcl/expressionstcl/convert.go
new file mode 100644
index 00000000000..8b4d522c980
--- /dev/null
+++ b/pkg/tcl/expressionstcl/convert.go
@@ -0,0 +1,164 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strconv"
+ "strings"
+)
+
+func toString(s interface{}) (string, error) {
+ // Fast track
+ v, ok := s.(string)
+ if ok {
+ return v, nil
+ }
+ if isNone(s) {
+ return "", nil
+ }
+ // Convert
+ if isNumber(s) {
+ return fmt.Sprintf("%v", s), nil
+ }
+ if isSlice(s) {
+ var err error
+ value := reflect.ValueOf(s)
+ results := make([]string, value.Len())
+ for i := 0; i < value.Len(); i++ {
+ results[i], err = toString(value.Index(i).Interface())
+ if err != nil {
+ err = fmt.Errorf("error while converting '%v' slice item: %v", value.Index(i), err)
+ return "", err
+ }
+ }
+ return strings.Join(results, ","), nil
+ }
+ b, err := json.Marshal(s)
+ if err != nil {
+ return "", fmt.Errorf("error while converting '%v' map to JSON: %v", s, err)
+ }
+ r := string(b)
+ if isMap(s) && r == "null" {
+ return "{}", nil
+ }
+ return r, nil
+}
+
+func toFloat(s interface{}) (float64, error) {
+ // Fast track
+ if v, ok := s.(float64); ok {
+ return v, nil
+ }
+ if isNone(s) {
+ return 0, nil
+ }
+ // Convert
+ str, err := toString(s)
+ if err != nil {
+ return 0, err
+ }
+ v, err := strconv.ParseFloat(str, 64)
+ if err != nil {
+ return 0, fmt.Errorf("error while converting value to number: %v: %v", s, err)
+ }
+ return v, nil
+}
+
+func toInt(s interface{}) (int64, error) {
+ // Fast track
+ if v, ok := s.(int64); ok {
+ return v, nil
+ }
+ if v, ok := s.(int); ok {
+ return int64(v), nil
+ }
+ if isNone(s) {
+ return 0, nil
+ }
+ // Convert
+ v, err := toFloat(s)
+ return int64(v), err
+}
+
+func toBool(s interface{}) (bool, error) {
+ // Fast track
+ if v, ok := s.(bool); ok {
+ return v, nil
+ }
+ if isNone(s) {
+ return false, nil
+ }
+ if isMap(s) || isSlice(s) {
+ return reflect.ValueOf(s).Len() > 0, nil
+ }
+ // Convert
+ value, err := toString(s)
+ if err != nil {
+ return false, fmt.Errorf("error while converting value to bool: %v: %v", value, err)
+ }
+ return !(value == "" || value == "false" || value == "0" || value == "off"), nil
+}
+
+func toMap(s interface{}) (map[string]interface{}, error) {
+ // Fast track
+ if v, ok := s.(map[string]interface{}); ok {
+ return v, nil
+ }
+ if isNone(s) {
+ return nil, nil
+ }
+ // Convert
+ if isMap(s) {
+ value := reflect.ValueOf(s)
+ res := make(map[string]interface{}, value.Len())
+ for _, k := range value.MapKeys() {
+ kk, err := toString(k.Interface())
+ if err != nil {
+ return nil, fmt.Errorf("error while converting map key to string: %v: %v", k, err)
+ }
+ res[kk] = value.MapIndex(k).Interface()
+ }
+ return res, nil
+ }
+ if isSlice(s) {
+ value := reflect.ValueOf(s)
+ res := make(map[string]interface{}, value.Len())
+ for i := 0; i < value.Len(); i++ {
+ res[strconv.Itoa(i)] = value.Index(i).Interface()
+ }
+ return res, nil
+ }
+ return nil, fmt.Errorf("error while converting value to map: %v", s)
+}
+
+func toSlice(s interface{}) ([]interface{}, error) {
+ // Fast track
+ if v, ok := s.([]interface{}); ok {
+ return v, nil
+ }
+ if isNone(s) {
+ return nil, nil
+ }
+ // Convert
+ if isSlice(s) {
+ value := reflect.ValueOf(s)
+ res := make([]interface{}, value.Len())
+ for i := 0; i < value.Len(); i++ {
+ res[i] = value.Index(i).Interface()
+ }
+ return res, nil
+ }
+ if isMap(s) {
+ return nil, fmt.Errorf("error while converting map to slice: %v", s)
+ }
+ return nil, fmt.Errorf("error while converting value to slice: %v", s)
+}
diff --git a/pkg/tcl/expressionstcl/expression.go b/pkg/tcl/expressionstcl/expression.go
new file mode 100644
index 00000000000..516d5c235f1
--- /dev/null
+++ b/pkg/tcl/expressionstcl/expression.go
@@ -0,0 +1,51 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+//go:generate mockgen -destination=./mock_expression.go -package=expressionstcl "github.com/kubeshop/testkube/pkg/tcl/expressionstcl" Expression
+type Expression interface {
+ String() string
+ SafeString() string
+ Template() string
+ Type() Type
+ SafeResolve(...Machine) (Expression, bool, error)
+ Resolve(...Machine) (Expression, error)
+ Static() StaticValue
+ Accessors() map[string]struct{}
+ Functions() map[string]struct{}
+}
+
+type Type string
+
+const (
+ TypeUnknown Type = ""
+ TypeBool Type = "bool"
+ TypeString Type = "string"
+ TypeFloat64 Type = "float64"
+ TypeInt64 Type = "int64"
+)
+
+//go:generate mockgen -destination=./mock_staticvalue.go -package=expressionstcl "github.com/kubeshop/testkube/pkg/tcl/expressionstcl" StaticValue
+type StaticValue interface {
+ Expression
+ IsNone() bool
+ IsString() bool
+ IsBool() bool
+ IsInt() bool
+ IsNumber() bool
+ IsMap() bool
+ IsSlice() bool
+ Value() interface{}
+ BoolValue() (bool, error)
+ IntValue() (int64, error)
+ FloatValue() (float64, error)
+ StringValue() (string, error)
+ MapValue() (map[string]interface{}, error)
+ SliceValue() ([]interface{}, error)
+}
diff --git a/pkg/tcl/expressionstcl/finalizer.go b/pkg/tcl/expressionstcl/finalizer.go
new file mode 100644
index 00000000000..b96b5929882
--- /dev/null
+++ b/pkg/tcl/expressionstcl/finalizer.go
@@ -0,0 +1,80 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "errors"
+)
+
+type finalizer struct {
+ handler FinalizerFn
+}
+
+type finalizerItem struct {
+ function bool
+ name string
+}
+
+type FinalizerItem interface {
+ Name() string
+ IsFunction() bool
+}
+
+type FinalizerResult int8
+
+const (
+ FinalizerResultFail FinalizerResult = -1
+ FinalizerResultNone FinalizerResult = 0
+ FinalizerResultPreserve FinalizerResult = 1
+)
+
+type FinalizerFn = func(item FinalizerItem) FinalizerResult
+
+func NewFinalizer(fn FinalizerFn) Machine {
+ return &finalizer{handler: fn}
+}
+
+func (f *finalizer) Get(name string) (Expression, bool, error) {
+ result := f.handler(finalizerItem{name: name})
+ if result == FinalizerResultFail {
+ return nil, true, errors.New("unknown variable")
+ } else if result == FinalizerResultNone {
+ return None, true, nil
+ }
+ return nil, false, nil
+}
+
+func (f *finalizer) Call(name string, _ ...StaticValue) (Expression, bool, error) {
+ result := f.handler(finalizerItem{function: true, name: name})
+ if result == FinalizerResultFail {
+ return nil, true, errors.New("unknown function")
+ } else if result == FinalizerResultNone {
+ return None, true, nil
+ }
+ return nil, false, nil
+}
+
+func (f finalizerItem) IsFunction() bool {
+ return f.function
+}
+
+func (f finalizerItem) Name() string {
+ return f.name
+}
+
+func FinalizerFailFn(_ FinalizerItem) FinalizerResult {
+ return FinalizerResultFail
+}
+
+func FinalizerNoneFn(_ FinalizerItem) FinalizerResult {
+ return FinalizerResultNone
+}
+
+var FinalizerFail = NewFinalizer(FinalizerFailFn)
+var FinalizerNone = NewFinalizer(FinalizerNoneFn)
diff --git a/pkg/tcl/expressionstcl/generic.go b/pkg/tcl/expressionstcl/generic.go
new file mode 100644
index 00000000000..5c62fb1c97c
--- /dev/null
+++ b/pkg/tcl/expressionstcl/generic.go
@@ -0,0 +1,262 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "fmt"
+ "reflect"
+ "strings"
+
+ "github.com/pkg/errors"
+ "k8s.io/apimachinery/pkg/util/intstr"
+)
+
+type tagData struct {
+ key string
+ value string
+}
+
+func parseTag(tag string) tagData {
+ s := strings.Split(tag, ",")
+ if len(s) > 1 {
+ return tagData{key: s[0], value: s[1]}
+ }
+ return tagData{value: s[0]}
+}
+
+func hasUnexportedFields(v reflect.Value) bool {
+ if v.Kind() != reflect.Struct {
+ return false
+ }
+ t := v.Type()
+ for i := 0; i < t.NumField(); i++ {
+ if !t.Field(i).IsExported() {
+ return true
+ }
+ }
+ return false
+}
+
+func clone(v reflect.Value) reflect.Value {
+ if v.Kind() == reflect.String {
+ s := v.String()
+ return reflect.ValueOf(&s).Elem()
+ } else if v.Kind() == reflect.Struct {
+ r := reflect.New(v.Type()).Elem()
+ t := v.Type()
+ for i := 0; i < r.NumField(); i++ {
+ if t.Field(i).IsExported() {
+ r.Field(i).Set(v.Field(i))
+ }
+ }
+ return r
+ }
+ return v
+}
+
+func resolve(v reflect.Value, t tagData, m []Machine, force bool, finalize bool) (changed bool, err error) {
+ if t.value == "force" {
+ force = true
+ }
+ if t.key == "" && t.value == "" && !force {
+ return
+ }
+
+ ptr := v
+ for v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface {
+ if v.IsNil() {
+ return
+ }
+ ptr = v
+ v = v.Elem()
+ }
+
+ if v.IsZero() || !v.IsValid() || (v.Kind() == reflect.Slice || v.Kind() == reflect.Map) && v.IsNil() {
+ return
+ }
+
+ switch v.Kind() {
+ case reflect.Struct:
+ // TODO: Cache the tags for structs for better performance
+ vv, ok := v.Interface().(intstr.IntOrString)
+ if ok {
+ if vv.Type == intstr.String {
+ return resolve(v.FieldByName("StrVal"), t, m, force, finalize)
+ }
+ } else if t.value == "include" || force {
+ tt := v.Type()
+ for i := 0; i < tt.NumField(); i++ {
+ f := tt.Field(i)
+ tagStr := f.Tag.Get("expr")
+ tag := parseTag(tagStr)
+ if !f.IsExported() {
+ if tagStr != "" && tagStr != "-" {
+ return changed, errors.New(f.Name + ": private property marked with `expr` clause")
+ }
+ continue
+ }
+ value := v.FieldByName(f.Name)
+ var ch bool
+ ch, err = resolve(value, tag, m, force, finalize)
+ if ch {
+ changed = true
+ }
+ if err != nil {
+ return changed, errors.Wrap(err, f.Name)
+ }
+ }
+ }
+ return
+ case reflect.Slice:
+ if t.value == "" && !force {
+ return changed, nil
+ }
+ for i := 0; i < v.Len(); i++ {
+ ch, err := resolve(v.Index(i), t, m, force, finalize)
+ if ch {
+ changed = true
+ }
+ if err != nil {
+ return changed, errors.Wrap(err, fmt.Sprintf("%d", i))
+ }
+ }
+ return
+ case reflect.Map:
+ if t.value == "" && t.key == "" && !force {
+ return changed, nil
+ }
+ for _, k := range v.MapKeys() {
+ if (t.value != "" || force) && !hasUnexportedFields(v.MapIndex(k)) {
+ // It's not possible to get a pointer to map element,
+ // so we need to copy it and reassign
+ item := clone(v.MapIndex(k))
+ var ch bool
+ ch, err = resolve(item, t, m, force, finalize)
+ if ch {
+ changed = true
+ }
+ if err != nil {
+ return changed, errors.Wrap(err, k.String())
+ }
+ v.SetMapIndex(k, item)
+ }
+ if (t.key != "" || force) && !hasUnexportedFields(k) && !hasUnexportedFields(v.MapIndex(k)) {
+ key := clone(k)
+ var ch bool
+ ch, err = resolve(key, tagData{value: t.key}, m, force, finalize)
+ if ch {
+ changed = true
+ }
+ if err != nil {
+ return changed, errors.Wrap(err, "key("+k.String()+")")
+ }
+ if !key.Equal(k) {
+ item := clone(v.MapIndex(k))
+ v.SetMapIndex(k, reflect.Value{})
+ v.SetMapIndex(key.Convert(k.Type()), item)
+ }
+ }
+ }
+ return
+ case reflect.String:
+ if t.value == "expression" {
+ var expr Expression
+ str := v.String()
+ expr, err = CompileAndResolve(str, m...)
+ if err != nil {
+ return changed, err
+ }
+ var vv string
+ if finalize {
+ expr2, err := expr.Resolve(FinalizerFail)
+ if err != nil {
+ return changed, errors.Wrap(err, "resolving the value")
+ }
+ vv, _ = expr2.Static().StringValue()
+ } else {
+ vv = expr.String()
+ }
+ changed = vv != str
+ if ptr.Kind() == reflect.String {
+ v.SetString(vv)
+ } else {
+ ptr.Set(reflect.ValueOf(&vv))
+ }
+ } else if (t.value == "template" && !IsTemplateStringWithoutExpressions(v.String())) || force {
+ var expr Expression
+ str := v.String()
+ expr, err = CompileAndResolveTemplate(str, m...)
+ if err != nil {
+ return changed, err
+ }
+ var vv string
+ if finalize {
+ expr2, err := expr.Resolve(FinalizerFail)
+ if err != nil {
+ return changed, errors.Wrap(err, "resolving the value")
+ }
+ vv, _ = expr2.Static().StringValue()
+ } else {
+ vv = expr.Template()
+ }
+ changed = vv != str
+ if ptr.Kind() == reflect.String {
+ v.SetString(vv)
+ } else {
+ ptr.Set(reflect.ValueOf(&vv))
+ }
+ }
+ return
+ }
+
+ // Ignore unrecognized values
+ return
+}
+
+func simplify(t interface{}, tag tagData, m ...Machine) error {
+ v := reflect.ValueOf(t)
+ if v.Kind() != reflect.Pointer {
+ return errors.New("pointer needs to be passed to Simplify function")
+ }
+ changed, err := resolve(v, tag, m, false, false)
+ i := 1
+ for changed && err == nil {
+ if i > maxCallStack {
+ return fmt.Errorf("maximum call stack exceeded while simplifying struct")
+ }
+ changed, err = resolve(v, tag, m, false, false)
+ i++
+ }
+ return err
+}
+
+func finalize(t interface{}, tag tagData, m ...Machine) error {
+ v := reflect.ValueOf(t)
+ if v.Kind() != reflect.Pointer {
+ return errors.New("pointer needs to be passed to Finalize function")
+ }
+ _, err := resolve(v, tag, m, false, true)
+ return err
+}
+
+func Simplify(t interface{}, m ...Machine) error {
+ return simplify(t, tagData{value: "include"}, m...)
+}
+
+func SimplifyForce(t interface{}, m ...Machine) error {
+ return simplify(t, tagData{value: "force"}, m...)
+}
+
+func Finalize(t interface{}, m ...Machine) error {
+ return finalize(t, tagData{value: "include"}, m...)
+}
+
+func FinalizeForce(t interface{}, m ...Machine) error {
+ return finalize(t, tagData{value: "force"}, m...)
+}
diff --git a/pkg/tcl/expressionstcl/generic_test.go b/pkg/tcl/expressionstcl/generic_test.go
new file mode 100644
index 00000000000..1e23300c087
--- /dev/null
+++ b/pkg/tcl/expressionstcl/generic_test.go
@@ -0,0 +1,224 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/util/intstr"
+
+ "github.com/kubeshop/testkube/internal/common"
+)
+
+type testObj2 struct {
+ Expr string `expr:"expression"`
+ Dummy string
+}
+
+type testObj struct {
+ Expr string `expr:"expression"`
+ Tmpl string `expr:"template"`
+ ExprPtr *string `expr:"expression"`
+ TmplPtr *string `expr:"template"`
+ IntExpr intstr.IntOrString `expr:"expression"`
+ IntTmpl intstr.IntOrString `expr:"template"`
+ IntExprPtr *intstr.IntOrString `expr:"expression"`
+ IntTmplPtr *intstr.IntOrString `expr:"template"`
+ Obj testObj2 `expr:"include"`
+ ObjPtr *testObj2 `expr:"include"`
+ SliceExprStr []string `expr:"expression"`
+ SliceExprStrPtr *[]string `expr:"expression"`
+ SliceExprObj []testObj2 `expr:"include"`
+ MapKeyVal map[string]string `expr:"template,template"`
+ MapValIntTmpl map[string]intstr.IntOrString `expr:"template"`
+ MapKeyTmpl map[string]string `expr:"template,"`
+ MapValTmpl map[string]string `expr:"template"`
+ MapTmplExpr map[string]string `expr:"template,expression"`
+ Dummy string
+ DummyPtr *string
+ DummyObj testObj2
+ DummyObjPtr *testObj2
+}
+
+type testObjNested struct {
+ Value corev1.Volume `expr:"force"`
+ Dummy corev1.Volume
+}
+
+var testMachine = NewMachine().
+ Register("dummy", "test").
+ Register("ten", 10)
+
+func TestGenericString(t *testing.T) {
+ obj := testObj{
+ Expr: "5 + 3 + ten",
+ Tmpl: "{{ 10 + 3 }}{{ ten }}",
+ ExprPtr: common.Ptr("1 + 2 + ten"),
+ TmplPtr: common.Ptr("{{ 4 + 3 }}{{ ten }}"),
+ Dummy: "5 + 3 + ten",
+ DummyPtr: common.Ptr("5 + 3 + ten"),
+ }
+ err := Simplify(&obj, testMachine)
+ assert.NoError(t, err)
+ assert.Equal(t, "18", obj.Expr)
+ assert.Equal(t, "1310", obj.Tmpl)
+ assert.Equal(t, common.Ptr("13"), obj.ExprPtr)
+ assert.Equal(t, common.Ptr("710"), obj.TmplPtr)
+ assert.Equal(t, "5 + 3 + ten", obj.Dummy)
+ assert.Equal(t, common.Ptr("5 + 3 + ten"), obj.DummyPtr)
+}
+
+func TestGenericIntOrString(t *testing.T) {
+ obj := testObj{
+ IntExpr: intstr.IntOrString{Type: intstr.String, StrVal: "5 + 3 + ten"},
+ IntTmpl: intstr.IntOrString{Type: intstr.String, StrVal: "{{ 10 + 3 }}{{ ten }}"},
+ IntExprPtr: &intstr.IntOrString{Type: intstr.String, StrVal: "1 + 2 + ten"},
+ IntTmplPtr: &intstr.IntOrString{Type: intstr.String, StrVal: "{{ 4 + 3 }}{{ ten }}"},
+ }
+ err := Simplify(&obj, testMachine)
+ assert.NoError(t, err)
+ assert.Equal(t, "18", obj.IntExpr.String())
+ assert.Equal(t, "1310", obj.IntTmpl.String())
+ assert.Equal(t, "13", obj.IntExprPtr.String())
+ assert.Equal(t, "710", obj.IntTmplPtr.String())
+}
+
+func TestGenericSlice(t *testing.T) {
+ obj := testObj{
+ SliceExprStr: []string{"200 + 100", "100 + 200", "ten", "abc"},
+ SliceExprStrPtr: &[]string{"200 + 100", "100 + 200", "ten", "abc"},
+ SliceExprObj: []testObj2{{Expr: "10 + 5", Dummy: "3 + 2"}},
+ }
+ err := Simplify(&obj, testMachine)
+ assert.NoError(t, err)
+ assert.Equal(t, []string{"300", "300", "10", "abc"}, obj.SliceExprStr)
+ assert.Equal(t, &[]string{"300", "300", "10", "abc"}, obj.SliceExprStrPtr)
+ assert.Equal(t, []testObj2{{Expr: "15", Dummy: "3 + 2"}}, obj.SliceExprObj)
+}
+
+func TestGenericMap(t *testing.T) {
+ obj := testObj{
+ MapKeyVal: map[string]string{"{{ 10 + 3 }}2": "{{ 3 + 5 }}"},
+ MapKeyTmpl: map[string]string{"{{ 10 + 3 }}2": "{{ 3 + 5 }}"},
+ MapValTmpl: map[string]string{"{{ 10 + 3 }}2": "{{ 3 + 5 }}"},
+ MapValIntTmpl: map[string]intstr.IntOrString{"{{ 10 + 3 }}2": {Type: intstr.String, StrVal: "{{ 3 + 5 }}"}},
+ MapTmplExpr: map[string]string{"{{ 10 + 3 }}2": "3 + 5"},
+ }
+ err := Simplify(&obj, testMachine)
+ assert.NoError(t, err)
+ assert.Equal(t, map[string]string{"132": "8"}, obj.MapKeyVal)
+ assert.Equal(t, map[string]string{"132": "{{ 3 + 5 }}"}, obj.MapKeyTmpl)
+ assert.Equal(t, map[string]string{"{{ 10 + 3 }}2": "8"}, obj.MapValTmpl)
+ assert.Equal(t, map[string]intstr.IntOrString{"{{ 10 + 3 }}2": {Type: intstr.String, StrVal: "8"}}, obj.MapValIntTmpl)
+ assert.Equal(t, map[string]string{"132": "8"}, obj.MapTmplExpr)
+}
+
+func TestNestedObject(t *testing.T) {
+ obj := testObj{
+ Obj: testObj2{Expr: "10 + 5", Dummy: "3 + 2"},
+ ObjPtr: &testObj2{Expr: "10 + 8", Dummy: "33 + 2"},
+ DummyObj: testObj2{Expr: "10 + 8", Dummy: "333 + 2"},
+ DummyObjPtr: &testObj2{Expr: "10 + 8", Dummy: "3333 + 2"},
+ }
+ err := Simplify(&obj, testMachine)
+ assert.NoError(t, err)
+ assert.Equal(t, testObj2{Expr: "15", Dummy: "3 + 2"}, obj.Obj)
+ assert.Equal(t, &testObj2{Expr: "18", Dummy: "33 + 2"}, obj.ObjPtr)
+ assert.Equal(t, testObj2{Expr: "10 + 8", Dummy: "333 + 2"}, obj.DummyObj)
+ assert.Equal(t, &testObj2{Expr: "10 + 8", Dummy: "3333 + 2"}, obj.DummyObjPtr)
+}
+
+func TestGenericNotMutateStringPointer(t *testing.T) {
+ ptr := common.Ptr("200 + 10")
+ obj := testObj{
+ ExprPtr: ptr,
+ }
+ _ = Simplify(&obj, testMachine)
+ assert.Equal(t, common.Ptr("200 + 10"), ptr)
+}
+
+func TestGenericCompileError(t *testing.T) {
+ got := testObj{
+ Tmpl: "{{ 1 + 2 }}{{ 3",
+ }
+ err := Simplify(&got)
+
+ assert.Contains(t, fmt.Sprintf("%v", err), "Tmpl: template error")
+}
+
+func TestGenericForceSimplify(t *testing.T) {
+ got := corev1.Volume{
+ Name: "{{ 3 + 2 }}{{ 5 }}",
+ VolumeSource: corev1.VolumeSource{
+ ConfigMap: &corev1.ConfigMapVolumeSource{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "{{ 4433 }}"},
+ },
+ },
+ }
+ err := SimplifyForce(&got)
+
+ want := corev1.Volume{
+ Name: "55",
+ VolumeSource: corev1.VolumeSource{
+ ConfigMap: &corev1.ConfigMapVolumeSource{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "4433"},
+ },
+ },
+ }
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, got)
+}
+
+func TestGenericForceSimplifyNested(t *testing.T) {
+ got := testObjNested{
+ Value: corev1.Volume{
+ Name: "{{ 3 + 2 }}{{ 5 }}",
+ VolumeSource: corev1.VolumeSource{
+ ConfigMap: &corev1.ConfigMapVolumeSource{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "{{ 4433 }}"},
+ },
+ },
+ },
+ Dummy: corev1.Volume{
+ Name: "{{ 3 + 2 }}{{ 5 }}",
+ VolumeSource: corev1.VolumeSource{
+ ConfigMap: &corev1.ConfigMapVolumeSource{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "{{ 4433 }}"},
+ },
+ },
+ },
+ }
+ err := Simplify(&got)
+
+ want := testObjNested{
+ Value: corev1.Volume{
+ Name: "55",
+ VolumeSource: corev1.VolumeSource{
+ ConfigMap: &corev1.ConfigMapVolumeSource{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "4433"},
+ },
+ },
+ },
+ Dummy: corev1.Volume{
+ Name: "{{ 3 + 2 }}{{ 5 }}",
+ VolumeSource: corev1.VolumeSource{
+ ConfigMap: &corev1.ConfigMapVolumeSource{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "{{ 4433 }}"},
+ },
+ },
+ },
+ }
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, got)
+}
diff --git a/pkg/tcl/expressionstcl/machine.go b/pkg/tcl/expressionstcl/machine.go
new file mode 100644
index 00000000000..95251ecb4af
--- /dev/null
+++ b/pkg/tcl/expressionstcl/machine.go
@@ -0,0 +1,103 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import "strings"
+
+//go:generate mockgen -destination=./mock_machine.go -package=expressionstcl "github.com/kubeshop/testkube/pkg/tcl/expressionstcl" Machine
+type Machine interface {
+ Get(name string) (Expression, bool, error)
+ Call(name string, args ...StaticValue) (Expression, bool, error)
+}
+
+type MachineAccessorExt = func(name string) (interface{}, bool, error)
+type MachineAccessor = func(name string) (interface{}, bool)
+type MachineFn = func(values ...StaticValue) (interface{}, bool, error)
+
+type machine struct {
+ accessors []MachineAccessorExt
+ functions map[string]MachineFn
+}
+
+func NewMachine() *machine {
+ return &machine{
+ accessors: make([]MachineAccessorExt, 0),
+ functions: make(map[string]MachineFn),
+ }
+}
+
+func (m *machine) Register(name string, value interface{}) *machine {
+ return m.RegisterAccessor(func(n string) (interface{}, bool) {
+ if n == name {
+ return value, true
+ }
+ return nil, false
+ })
+}
+
+func (m *machine) RegisterStringMap(prefix string, value map[string]string) *machine {
+ if len(prefix) > 0 {
+ prefix += "."
+ }
+ return m.RegisterAccessor(func(n string) (interface{}, bool) {
+ if !strings.HasPrefix(n, prefix) {
+ return nil, false
+ }
+ v, ok := value[n[len(prefix):]]
+ return v, ok
+ })
+}
+
+func (m *machine) RegisterAccessorExt(fn MachineAccessorExt) *machine {
+ m.accessors = append(m.accessors, fn)
+ return m
+}
+
+func (m *machine) RegisterAccessor(fn MachineAccessor) *machine {
+ return m.RegisterAccessorExt(func(name string) (interface{}, bool, error) {
+ v, ok := fn(name)
+ return v, ok, nil
+ })
+}
+
+func (m *machine) RegisterFunction(name string, fn MachineFn) *machine {
+ m.functions[name] = fn
+ return m
+}
+
+func (m *machine) Get(name string) (Expression, bool, error) {
+ for i := range m.accessors {
+ r, ok, err := m.accessors[i](name)
+ if err != nil {
+ return nil, true, err
+ }
+ if ok {
+ if v, ok := r.(Expression); ok {
+ return v, true, nil
+ }
+ return NewValue(r), true, nil
+ }
+ }
+ return nil, false, nil
+}
+
+func (m *machine) Call(name string, args ...StaticValue) (Expression, bool, error) {
+ fn, ok := m.functions[name]
+ if !ok {
+ return nil, false, nil
+ }
+ r, ok, err := fn(args...)
+ if !ok || err != nil {
+ return nil, ok, err
+ }
+ if v, ok := r.(Expression); ok {
+ return v, true, nil
+ }
+ return NewValue(r), true, nil
+}
diff --git a/pkg/tcl/expressionstcl/machineutils.go b/pkg/tcl/expressionstcl/machineutils.go
new file mode 100644
index 00000000000..d0b9a21d59d
--- /dev/null
+++ b/pkg/tcl/expressionstcl/machineutils.go
@@ -0,0 +1,74 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import "strings"
+
+type limitedMachine struct {
+ prefix string
+ machine Machine
+}
+
+func PrefixMachine(prefix string, machine Machine) Machine {
+ return &limitedMachine{
+ prefix: prefix,
+ machine: machine,
+ }
+}
+
+func (m *limitedMachine) Get(name string) (Expression, bool, error) {
+ if strings.HasPrefix(name, m.prefix) {
+ return m.machine.Get(name)
+ }
+ return nil, false, nil
+}
+
+func (m *limitedMachine) Call(name string, args ...StaticValue) (Expression, bool, error) {
+ if strings.HasPrefix(name, m.prefix) {
+ return m.machine.Call(name, args...)
+ }
+ return nil, false, nil
+}
+
+type combinedMachine struct {
+ machines []Machine
+}
+
+func CombinedMachines(machines ...Machine) Machine {
+ return &combinedMachine{machines: machines}
+}
+
+func (m *combinedMachine) Get(name string) (Expression, bool, error) {
+ for i := range m.machines {
+ v, ok, err := m.machines[i].Get(name)
+ if err != nil || ok {
+ return v, ok, err
+ }
+ }
+ return nil, false, nil
+}
+
+func (m *combinedMachine) Call(name string, args ...StaticValue) (Expression, bool, error) {
+ for i := range m.machines {
+ v, ok, err := m.machines[i].Call(name, args...)
+ if err != nil || ok {
+ return v, ok, err
+ }
+ }
+ return nil, false, nil
+}
+
+func ReplacePrefixMachine(from string, to string) Machine {
+ return NewMachine().RegisterAccessor(func(name string) (interface{}, bool) {
+ if strings.HasPrefix(name, from) {
+ return newAccessor(to + name[len(from):]), true
+ }
+ return nil, false
+ })
+}
diff --git a/pkg/tcl/expressionstcl/math.go b/pkg/tcl/expressionstcl/math.go
new file mode 100644
index 00000000000..5a638d6ceef
--- /dev/null
+++ b/pkg/tcl/expressionstcl/math.go
@@ -0,0 +1,319 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "errors"
+ "fmt"
+ "maps"
+ math2 "math"
+)
+
+type operator string
+
+const (
+ operatorEquals operator = "="
+ operatorEqualsAlias operator = "=="
+ operatorNotEquals operator = "!="
+ operatorNotEqualsAlias operator = "<>"
+ operatorGt operator = ">"
+ operatorGte operator = ">="
+ operatorLt operator = "<"
+ operatorLte operator = "<="
+ operatorAnd operator = "&&"
+ operatorOr operator = "||"
+ operatorAdd operator = "+"
+ operatorSubtract operator = "-"
+ operatorModulo operator = "%"
+ operatorDivide operator = "/"
+ operatorMultiply operator = "*"
+ operatorPower operator = "**"
+)
+
+func getOperatorPriority(op operator) int {
+ switch op {
+ case operatorAnd, operatorOr:
+ return 0
+ case operatorEquals, operatorEqualsAlias, operatorNotEquals, operatorNotEqualsAlias,
+ operatorGt, operatorGte, operatorLt, operatorLte:
+ return 1
+ case operatorAdd, operatorSubtract:
+ return 2
+ case operatorMultiply, operatorDivide, operatorModulo:
+ return 3
+ case operatorPower:
+ return 4
+ }
+ panic("unknown operator: " + op)
+}
+
+type math struct {
+ operator operator
+ left Expression
+ right Expression
+}
+
+func newMath(operator operator, left Expression, right Expression) Expression {
+ if left == nil {
+ left = None
+ }
+ if right == nil {
+ right = None
+ }
+ return &math{operator: operator, left: left, right: right}
+}
+
+func runOp[T interface{}, U interface{}](v1 StaticValue, v2 StaticValue, mapper func(value StaticValue) (T, error), op func(s1, s2 T) U) (StaticValue, error) {
+ s1, err1 := mapper(v1)
+ if err1 != nil {
+ return nil, err1
+ }
+ s2, err2 := mapper(v2)
+ if err2 != nil {
+ return nil, err2
+ }
+ return NewValue(op(s1, s2)), nil
+}
+
+func staticString(v StaticValue) (string, error) {
+ return v.StringValue()
+}
+
+func staticFloat(v StaticValue) (float64, error) {
+ return v.FloatValue()
+}
+
+func staticBool(v StaticValue) (bool, error) {
+ return v.BoolValue()
+}
+
+func (s *math) performMath(v1 StaticValue, v2 StaticValue) (StaticValue, error) {
+ switch s.operator {
+ case operatorEquals, operatorEqualsAlias:
+ return runOp(v1, v2, staticString, func(s1, s2 string) bool {
+ return s1 == s2
+ })
+ case operatorNotEquals, operatorNotEqualsAlias:
+ return runOp(v1, v2, staticString, func(s1, s2 string) bool {
+ return s1 != s2
+ })
+ case operatorGt:
+ return runOp(v1, v2, staticFloat, func(s1, s2 float64) bool {
+ return s1 > s2
+ })
+ case operatorLt:
+ return runOp(v1, v2, staticFloat, func(s1, s2 float64) bool {
+ return s1 < s2
+ })
+ case operatorGte:
+ return runOp(v1, v2, staticFloat, func(s1, s2 float64) bool {
+ return s1 >= s2
+ })
+ case operatorLte:
+ return runOp(v1, v2, staticFloat, func(s1, s2 float64) bool {
+ return s1 <= s2
+ })
+ case operatorAnd:
+ return runOp(v1, v2, staticBool, func(s1, s2 bool) interface{} {
+ if s1 {
+ return v2.Value()
+ }
+ return v1.Value()
+ })
+ case operatorOr:
+ return runOp(v1, v2, staticBool, func(s1, s2 bool) interface{} {
+ if s1 {
+ return v1.Value()
+ }
+ return v2.Value()
+ })
+ case operatorAdd:
+ if v1.IsString() || v2.IsString() {
+ return runOp(v1, v2, staticString, func(s1, s2 string) string {
+ return s1 + s2
+ })
+ }
+ return runOp(v1, v2, staticFloat, func(s1, s2 float64) float64 {
+ return s1 + s2
+ })
+ case operatorSubtract:
+ return runOp(v1, v2, staticFloat, func(s1, s2 float64) float64 {
+ return s1 - s2
+ })
+ case operatorModulo:
+ divideByZero := false
+ res, err := runOp(v1, v2, staticFloat, func(s1, s2 float64) float64 {
+ if s2 == 0 {
+ divideByZero = true
+ return 0
+ }
+ return math2.Mod(s1, s2)
+ })
+ if divideByZero {
+ return nil, errors.New("cannot modulo by zero")
+ }
+ return res, err
+ case operatorDivide:
+ divideByZero := false
+ res, err := runOp(v1, v2, staticFloat, func(s1, s2 float64) float64 {
+ if s2 == 0 {
+ divideByZero = true
+ return 0
+ }
+ return s1 / s2
+ })
+ if divideByZero {
+ return nil, errors.New("cannot divide by zero")
+ }
+ return res, err
+ case operatorMultiply:
+ return runOp(v1, v2, staticFloat, func(s1, s2 float64) float64 {
+ return s1 * s2
+ })
+ case operatorPower:
+ return runOp(v1, v2, staticFloat, func(s1, s2 float64) float64 {
+ return math2.Pow(s1, s2)
+ })
+ default:
+ }
+ return nil, fmt.Errorf("unknown math operator: %s", s.operator)
+}
+
+func (s *math) Type() Type {
+ l := s.left.Type()
+ r := s.right.Type()
+ switch s.operator {
+ case operatorAnd, operatorOr:
+ if l == r {
+ return l
+ }
+ return TypeUnknown
+ case operatorPower, operatorModulo, operatorSubtract, operatorMultiply, operatorDivide:
+ return TypeFloat64
+ case operatorAdd:
+ if l == TypeString || r == TypeString {
+ return TypeString
+ }
+ return TypeFloat64
+ case operatorEquals, operatorNotEquals, operatorEqualsAlias, operatorNotEqualsAlias, operatorGt, operatorLt, operatorGte, operatorLte:
+ return TypeBool
+ default:
+ return TypeUnknown
+ }
+}
+
+func (s *math) itemString(v Expression) string {
+ if vv, ok := v.(*math); ok {
+ if getOperatorPriority(vv.operator) >= getOperatorPriority(s.operator) {
+ return v.String()
+ }
+ }
+ return v.SafeString()
+}
+
+func (s *math) String() string {
+ return s.itemString(s.left) + string(s.operator) + s.itemString(s.right)
+}
+
+func (s *math) SafeString() string {
+ return "(" + s.String() + ")"
+}
+
+func (s *math) Template() string {
+ // Simplify the template when it is possible
+ if s.operator == operatorAdd && s.Type() == TypeString {
+ return s.left.Template() + s.right.Template()
+ }
+ return "{{" + s.String() + "}}"
+}
+
+func (s *math) SafeResolve(m ...Machine) (v Expression, changed bool, err error) {
+ var ch bool
+ s.left, ch, err = s.left.SafeResolve(m...)
+ changed = changed || ch
+ if err != nil {
+ return
+ }
+
+ // Fast track for cutting dead paths
+ if s.left.Static() != nil {
+ if s.operator == operatorAnd {
+ b, err := s.left.Static().BoolValue()
+ if err == nil && !b {
+ return s.left, true, nil
+ } else if err == nil {
+ return s.right, true, nil
+ }
+ } else if s.operator == operatorOr {
+ b, err := s.left.Static().BoolValue()
+ if err == nil && b {
+ return s.left, true, nil
+ } else if err == nil {
+ return s.right, true, nil
+ }
+ }
+ }
+
+ s.right, ch, err = s.right.SafeResolve(m...)
+ changed = changed || ch
+ if err != nil {
+ return
+ }
+
+ // Fast track for cutting dead paths
+ t := s.left.Type()
+ if s.left.Static() == nil && s.right.Static() != nil && t != TypeUnknown && t == s.right.Type() && t == TypeBool {
+ if s.operator == operatorAnd {
+ b, err := s.right.Static().BoolValue()
+ if err == nil && !b {
+ return s.right, true, nil
+ } else if err == nil {
+ return s.left, true, nil
+ }
+ } else if s.operator == operatorOr {
+ b, err := s.right.Static().BoolValue()
+ if err == nil && b {
+ return s.right, true, nil
+ } else if err == nil {
+ return s.left, true, nil
+ }
+ }
+ }
+
+ if s.left.Static() != nil && s.right.Static() != nil {
+ res, err := s.performMath(s.left.Static(), s.right.Static())
+ if err != nil {
+ return nil, changed, fmt.Errorf("error while performing math: %s: %s", s.String(), err)
+ }
+ return res, true, nil
+ }
+ return s, changed, nil
+}
+
+func (s *math) Resolve(m ...Machine) (v Expression, err error) {
+ return deepResolve(s, m...)
+}
+
+func (s *math) Static() StaticValue {
+ return nil
+}
+
+func (s *math) Accessors() map[string]struct{} {
+ result := make(map[string]struct{})
+ maps.Copy(result, s.left.Accessors())
+ maps.Copy(result, s.right.Accessors())
+ return result
+}
+
+func (s *math) Functions() map[string]struct{} {
+ result := make(map[string]struct{})
+ maps.Copy(result, s.left.Functions())
+ maps.Copy(result, s.right.Functions())
+ return result
+}
diff --git a/pkg/tcl/expressionstcl/mock_expression.go b/pkg/tcl/expressionstcl/mock_expression.go
new file mode 100644
index 00000000000..c8689efa401
--- /dev/null
+++ b/pkg/tcl/expressionstcl/mock_expression.go
@@ -0,0 +1,171 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/tcl/expressionstcl (interfaces: Expression)
+
+// Package expressionstcl is a generated GoMock package.
+package expressionstcl
+
+import (
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+)
+
+// MockExpression is a mock of Expression interface.
+type MockExpression struct {
+ ctrl *gomock.Controller
+ recorder *MockExpressionMockRecorder
+}
+
+// MockExpressionMockRecorder is the mock recorder for MockExpression.
+type MockExpressionMockRecorder struct {
+ mock *MockExpression
+}
+
+// NewMockExpression creates a new mock instance.
+func NewMockExpression(ctrl *gomock.Controller) *MockExpression {
+ mock := &MockExpression{ctrl: ctrl}
+ mock.recorder = &MockExpressionMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockExpression) EXPECT() *MockExpressionMockRecorder {
+ return m.recorder
+}
+
+// Accessors mocks base method.
+func (m *MockExpression) Accessors() map[string]struct{} {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Accessors")
+ ret0, _ := ret[0].(map[string]struct{})
+ return ret0
+}
+
+// Accessors indicates an expected call of Accessors.
+func (mr *MockExpressionMockRecorder) Accessors() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Accessors", reflect.TypeOf((*MockExpression)(nil).Accessors))
+}
+
+// Functions mocks base method.
+func (m *MockExpression) Functions() map[string]struct{} {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Functions")
+ ret0, _ := ret[0].(map[string]struct{})
+ return ret0
+}
+
+// Functions indicates an expected call of Functions.
+func (mr *MockExpressionMockRecorder) Functions() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Functions", reflect.TypeOf((*MockExpression)(nil).Functions))
+}
+
+// Resolve mocks base method.
+func (m *MockExpression) Resolve(arg0 ...Machine) (Expression, error) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Resolve", varargs...)
+ ret0, _ := ret[0].(Expression)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Resolve indicates an expected call of Resolve.
+func (mr *MockExpressionMockRecorder) Resolve(arg0 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*MockExpression)(nil).Resolve), arg0...)
+}
+
+// SafeResolve mocks base method.
+func (m *MockExpression) SafeResolve(arg0 ...Machine) (Expression, bool, error) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SafeResolve", varargs...)
+ ret0, _ := ret[0].(Expression)
+ ret1, _ := ret[1].(bool)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// SafeResolve indicates an expected call of SafeResolve.
+func (mr *MockExpressionMockRecorder) SafeResolve(arg0 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SafeResolve", reflect.TypeOf((*MockExpression)(nil).SafeResolve), arg0...)
+}
+
+// SafeString mocks base method.
+func (m *MockExpression) SafeString() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SafeString")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// SafeString indicates an expected call of SafeString.
+func (mr *MockExpressionMockRecorder) SafeString() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SafeString", reflect.TypeOf((*MockExpression)(nil).SafeString))
+}
+
+// Static mocks base method.
+func (m *MockExpression) Static() StaticValue {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Static")
+ ret0, _ := ret[0].(StaticValue)
+ return ret0
+}
+
+// Static indicates an expected call of Static.
+func (mr *MockExpressionMockRecorder) Static() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Static", reflect.TypeOf((*MockExpression)(nil).Static))
+}
+
+// String mocks base method.
+func (m *MockExpression) String() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "String")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// String indicates an expected call of String.
+func (mr *MockExpressionMockRecorder) String() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockExpression)(nil).String))
+}
+
+// Template mocks base method.
+func (m *MockExpression) Template() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Template")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// Template indicates an expected call of Template.
+func (mr *MockExpressionMockRecorder) Template() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Template", reflect.TypeOf((*MockExpression)(nil).Template))
+}
+
+// Type mocks base method.
+func (m *MockExpression) Type() Type {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Type")
+ ret0, _ := ret[0].(Type)
+ return ret0
+}
+
+// Type indicates an expected call of Type.
+func (mr *MockExpressionMockRecorder) Type() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*MockExpression)(nil).Type))
+}
diff --git a/pkg/tcl/expressionstcl/mock_machine.go b/pkg/tcl/expressionstcl/mock_machine.go
new file mode 100644
index 00000000000..407256ba5ab
--- /dev/null
+++ b/pkg/tcl/expressionstcl/mock_machine.go
@@ -0,0 +1,71 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/tcl/expressionstcl (interfaces: Machine)
+
+// Package expressionstcl is a generated GoMock package.
+package expressionstcl
+
+import (
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+)
+
+// MockMachine is a mock of Machine interface.
+type MockMachine struct {
+ ctrl *gomock.Controller
+ recorder *MockMachineMockRecorder
+}
+
+// MockMachineMockRecorder is the mock recorder for MockMachine.
+type MockMachineMockRecorder struct {
+ mock *MockMachine
+}
+
+// NewMockMachine creates a new mock instance.
+func NewMockMachine(ctrl *gomock.Controller) *MockMachine {
+ mock := &MockMachine{ctrl: ctrl}
+ mock.recorder = &MockMachineMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockMachine) EXPECT() *MockMachineMockRecorder {
+ return m.recorder
+}
+
+// Call mocks base method.
+func (m *MockMachine) Call(arg0 string, arg1 ...StaticValue) (Expression, bool, error) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Call", varargs...)
+ ret0, _ := ret[0].(Expression)
+ ret1, _ := ret[1].(bool)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// Call indicates an expected call of Call.
+func (mr *MockMachineMockRecorder) Call(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockMachine)(nil).Call), varargs...)
+}
+
+// Get mocks base method.
+func (m *MockMachine) Get(arg0 string) (Expression, bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Get", arg0)
+ ret0, _ := ret[0].(Expression)
+ ret1, _ := ret[1].(bool)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockMachineMockRecorder) Get(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockMachine)(nil).Get), arg0)
+}
diff --git a/pkg/tcl/expressionstcl/mock_staticvalue.go b/pkg/tcl/expressionstcl/mock_staticvalue.go
new file mode 100644
index 00000000000..eed6881bfc7
--- /dev/null
+++ b/pkg/tcl/expressionstcl/mock_staticvalue.go
@@ -0,0 +1,373 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/tcl/expressionstcl (interfaces: StaticValue)
+
+// Package expressionstcl is a generated GoMock package.
+package expressionstcl
+
+import (
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+)
+
+// MockStaticValue is a mock of StaticValue interface.
+type MockStaticValue struct {
+ ctrl *gomock.Controller
+ recorder *MockStaticValueMockRecorder
+}
+
+// MockStaticValueMockRecorder is the mock recorder for MockStaticValue.
+type MockStaticValueMockRecorder struct {
+ mock *MockStaticValue
+}
+
+// NewMockStaticValue creates a new mock instance.
+func NewMockStaticValue(ctrl *gomock.Controller) *MockStaticValue {
+ mock := &MockStaticValue{ctrl: ctrl}
+ mock.recorder = &MockStaticValueMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockStaticValue) EXPECT() *MockStaticValueMockRecorder {
+ return m.recorder
+}
+
+// Accessors mocks base method.
+func (m *MockStaticValue) Accessors() map[string]struct{} {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Accessors")
+ ret0, _ := ret[0].(map[string]struct{})
+ return ret0
+}
+
+// Accessors indicates an expected call of Accessors.
+func (mr *MockStaticValueMockRecorder) Accessors() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Accessors", reflect.TypeOf((*MockStaticValue)(nil).Accessors))
+}
+
+// BoolValue mocks base method.
+func (m *MockStaticValue) BoolValue() (bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "BoolValue")
+ ret0, _ := ret[0].(bool)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// BoolValue indicates an expected call of BoolValue.
+func (mr *MockStaticValueMockRecorder) BoolValue() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BoolValue", reflect.TypeOf((*MockStaticValue)(nil).BoolValue))
+}
+
+// FloatValue mocks base method.
+func (m *MockStaticValue) FloatValue() (float64, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "FloatValue")
+ ret0, _ := ret[0].(float64)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// FloatValue indicates an expected call of FloatValue.
+func (mr *MockStaticValueMockRecorder) FloatValue() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FloatValue", reflect.TypeOf((*MockStaticValue)(nil).FloatValue))
+}
+
+// Functions mocks base method.
+func (m *MockStaticValue) Functions() map[string]struct{} {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Functions")
+ ret0, _ := ret[0].(map[string]struct{})
+ return ret0
+}
+
+// Functions indicates an expected call of Functions.
+func (mr *MockStaticValueMockRecorder) Functions() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Functions", reflect.TypeOf((*MockStaticValue)(nil).Functions))
+}
+
+// IntValue mocks base method.
+func (m *MockStaticValue) IntValue() (int64, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "IntValue")
+ ret0, _ := ret[0].(int64)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// IntValue indicates an expected call of IntValue.
+func (mr *MockStaticValueMockRecorder) IntValue() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntValue", reflect.TypeOf((*MockStaticValue)(nil).IntValue))
+}
+
+// IsBool mocks base method.
+func (m *MockStaticValue) IsBool() bool {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "IsBool")
+ ret0, _ := ret[0].(bool)
+ return ret0
+}
+
+// IsBool indicates an expected call of IsBool.
+func (mr *MockStaticValueMockRecorder) IsBool() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsBool", reflect.TypeOf((*MockStaticValue)(nil).IsBool))
+}
+
+// IsInt mocks base method.
+func (m *MockStaticValue) IsInt() bool {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "IsInt")
+ ret0, _ := ret[0].(bool)
+ return ret0
+}
+
+// IsInt indicates an expected call of IsInt.
+func (mr *MockStaticValueMockRecorder) IsInt() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsInt", reflect.TypeOf((*MockStaticValue)(nil).IsInt))
+}
+
+// IsMap mocks base method.
+func (m *MockStaticValue) IsMap() bool {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "IsMap")
+ ret0, _ := ret[0].(bool)
+ return ret0
+}
+
+// IsMap indicates an expected call of IsMap.
+func (mr *MockStaticValueMockRecorder) IsMap() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsMap", reflect.TypeOf((*MockStaticValue)(nil).IsMap))
+}
+
+// IsNone mocks base method.
+func (m *MockStaticValue) IsNone() bool {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "IsNone")
+ ret0, _ := ret[0].(bool)
+ return ret0
+}
+
+// IsNone indicates an expected call of IsNone.
+func (mr *MockStaticValueMockRecorder) IsNone() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsNone", reflect.TypeOf((*MockStaticValue)(nil).IsNone))
+}
+
+// IsNumber mocks base method.
+func (m *MockStaticValue) IsNumber() bool {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "IsNumber")
+ ret0, _ := ret[0].(bool)
+ return ret0
+}
+
+// IsNumber indicates an expected call of IsNumber.
+func (mr *MockStaticValueMockRecorder) IsNumber() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsNumber", reflect.TypeOf((*MockStaticValue)(nil).IsNumber))
+}
+
+// IsSlice mocks base method.
+func (m *MockStaticValue) IsSlice() bool {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "IsSlice")
+ ret0, _ := ret[0].(bool)
+ return ret0
+}
+
+// IsSlice indicates an expected call of IsSlice.
+func (mr *MockStaticValueMockRecorder) IsSlice() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSlice", reflect.TypeOf((*MockStaticValue)(nil).IsSlice))
+}
+
+// IsString mocks base method.
+func (m *MockStaticValue) IsString() bool {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "IsString")
+ ret0, _ := ret[0].(bool)
+ return ret0
+}
+
+// IsString indicates an expected call of IsString.
+func (mr *MockStaticValueMockRecorder) IsString() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsString", reflect.TypeOf((*MockStaticValue)(nil).IsString))
+}
+
+// MapValue mocks base method.
+func (m *MockStaticValue) MapValue() (map[string]interface{}, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "MapValue")
+ ret0, _ := ret[0].(map[string]interface{})
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// MapValue indicates an expected call of MapValue.
+func (mr *MockStaticValueMockRecorder) MapValue() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MapValue", reflect.TypeOf((*MockStaticValue)(nil).MapValue))
+}
+
+// Resolve mocks base method.
+func (m *MockStaticValue) Resolve(arg0 ...Machine) (Expression, error) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Resolve", varargs...)
+ ret0, _ := ret[0].(Expression)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Resolve indicates an expected call of Resolve.
+func (mr *MockStaticValueMockRecorder) Resolve(arg0 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*MockStaticValue)(nil).Resolve), arg0...)
+}
+
+// SafeResolve mocks base method.
+func (m *MockStaticValue) SafeResolve(arg0 ...Machine) (Expression, bool, error) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SafeResolve", varargs...)
+ ret0, _ := ret[0].(Expression)
+ ret1, _ := ret[1].(bool)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// SafeResolve indicates an expected call of SafeResolve.
+func (mr *MockStaticValueMockRecorder) SafeResolve(arg0 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SafeResolve", reflect.TypeOf((*MockStaticValue)(nil).SafeResolve), arg0...)
+}
+
+// SafeString mocks base method.
+func (m *MockStaticValue) SafeString() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SafeString")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// SafeString indicates an expected call of SafeString.
+func (mr *MockStaticValueMockRecorder) SafeString() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SafeString", reflect.TypeOf((*MockStaticValue)(nil).SafeString))
+}
+
+// SliceValue mocks base method.
+func (m *MockStaticValue) SliceValue() ([]interface{}, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SliceValue")
+ ret0, _ := ret[0].([]interface{})
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// SliceValue indicates an expected call of SliceValue.
+func (mr *MockStaticValueMockRecorder) SliceValue() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SliceValue", reflect.TypeOf((*MockStaticValue)(nil).SliceValue))
+}
+
+// Static mocks base method.
+func (m *MockStaticValue) Static() StaticValue {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Static")
+ ret0, _ := ret[0].(StaticValue)
+ return ret0
+}
+
+// Static indicates an expected call of Static.
+func (mr *MockStaticValueMockRecorder) Static() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Static", reflect.TypeOf((*MockStaticValue)(nil).Static))
+}
+
+// String mocks base method.
+func (m *MockStaticValue) String() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "String")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// String indicates an expected call of String.
+func (mr *MockStaticValueMockRecorder) String() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockStaticValue)(nil).String))
+}
+
+// StringValue mocks base method.
+func (m *MockStaticValue) StringValue() (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StringValue")
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// StringValue indicates an expected call of StringValue.
+func (mr *MockStaticValueMockRecorder) StringValue() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StringValue", reflect.TypeOf((*MockStaticValue)(nil).StringValue))
+}
+
+// Template mocks base method.
+func (m *MockStaticValue) Template() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Template")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// Template indicates an expected call of Template.
+func (mr *MockStaticValueMockRecorder) Template() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Template", reflect.TypeOf((*MockStaticValue)(nil).Template))
+}
+
+// Type mocks base method.
+func (m *MockStaticValue) Type() Type {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Type")
+ ret0, _ := ret[0].(Type)
+ return ret0
+}
+
+// Type indicates an expected call of Type.
+func (mr *MockStaticValueMockRecorder) Type() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*MockStaticValue)(nil).Type))
+}
+
+// Value mocks base method.
+func (m *MockStaticValue) Value() interface{} {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Value")
+ ret0, _ := ret[0].(interface{})
+ return ret0
+}
+
+// Value indicates an expected call of Value.
+func (mr *MockStaticValueMockRecorder) Value() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Value", reflect.TypeOf((*MockStaticValue)(nil).Value))
+}
diff --git a/pkg/tcl/expressionstcl/negative.go b/pkg/tcl/expressionstcl/negative.go
new file mode 100644
index 00000000000..da67b4a4a9d
--- /dev/null
+++ b/pkg/tcl/expressionstcl/negative.go
@@ -0,0 +1,72 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import "fmt"
+
+type negative struct {
+ expr Expression
+}
+
+func newNegative(expr Expression) Expression {
+ if expr == nil {
+ expr = None
+ }
+ return &negative{expr: expr}
+}
+
+func (s *negative) Type() Type {
+ return TypeBool
+}
+
+func (s *negative) String() string {
+ return fmt.Sprintf("!%s", s.expr.SafeString())
+}
+
+func (s *negative) SafeString() string {
+ return s.String()
+}
+
+func (s *negative) Template() string {
+ return "{{" + s.String() + "}}"
+}
+
+func (s *negative) SafeResolve(m ...Machine) (v Expression, changed bool, err error) {
+ s.expr, changed, err = s.expr.SafeResolve(m...)
+ if err != nil {
+ return nil, changed, err
+ }
+ st := s.expr.Static()
+ if st == nil {
+ return s, changed, nil
+ }
+
+ vv, err := st.BoolValue()
+ if err != nil {
+ return nil, changed, err
+ }
+ return NewValue(!vv), changed, nil
+}
+
+func (s *negative) Resolve(m ...Machine) (v Expression, err error) {
+ return deepResolve(s, m...)
+}
+
+func (s *negative) Static() StaticValue {
+ // FIXME: it should get environment to call
+ return nil
+}
+
+func (s *negative) Accessors() map[string]struct{} {
+ return s.expr.Accessors()
+}
+
+func (s *negative) Functions() map[string]struct{} {
+ return s.expr.Functions()
+}
diff --git a/pkg/tcl/expressionstcl/parse.go b/pkg/tcl/expressionstcl/parse.go
new file mode 100644
index 00000000000..224c12975b7
--- /dev/null
+++ b/pkg/tcl/expressionstcl/parse.go
@@ -0,0 +1,254 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "errors"
+ "fmt"
+ math2 "math"
+ "regexp"
+ "strings"
+)
+
+func parseNextExpression(t []token, priority int) (e Expression, i int, err error) {
+ e, i, err = getNextSegment(t)
+ if err != nil {
+ return
+ }
+
+ for {
+ // End of the expression
+ if len(t) == i {
+ return e, i, nil
+ }
+
+ switch t[i].Type {
+ case tokenTypeTernary:
+ i += 1
+ te, ti, terr := parseNextExpression(t[i:], 0)
+ i += ti
+ if terr != nil {
+ return nil, i, terr
+ }
+ if len(t) == i {
+ return nil, i, fmt.Errorf("premature end of expression: expected ternary separator")
+ }
+ if t[i].Type != tokenTypeTernarySeparator {
+ return nil, i, fmt.Errorf("expression syntax error: expected ternary separator: found %v", t[i])
+ }
+ i++
+ fe, fi, ferr := parseNextExpression(t[i:], 0)
+ i += fi
+ if ferr != nil {
+ return nil, i, ferr
+ }
+ e = newConditional(e, te, fe)
+ case tokenTypeMath:
+ op := operator(t[i].Value.(string))
+ nextPriority := getOperatorPriority(op)
+ if priority >= nextPriority {
+ return e, i, nil
+ }
+ i += 1
+ ne, ni, nerr := parseNextExpression(t[i:], nextPriority)
+ i += ni
+ if nerr != nil {
+ return nil, i, nerr
+ }
+ e = newMath(op, e, ne)
+ default:
+ return e, i, err
+ }
+ }
+}
+
+func getNextSegment(t []token) (e Expression, i int, err error) {
+ if len(t) == 0 {
+ return nil, 0, errors.New("premature end of expression")
+ }
+
+ // Parentheses - (a(b) + c)
+ if t[0].Type == tokenTypeOpen {
+ e, i, err = parseNextExpression(t[1:], -1)
+ i++
+ if err != nil {
+ return nil, i, err
+ }
+ if len(t) <= i || t[i].Type != tokenTypeClose {
+ return nil, i, fmt.Errorf("syntax error: expected parentheses close")
+ }
+ return e, i + 1, err
+ }
+
+ // Static value - "abc", 444, {"a": 10}, true, [45, 3]
+ if t[0].Type == tokenTypeJson {
+ return NewValue(t[0].Value), 1, nil
+ }
+
+ // Negation - !expr
+ if t[0].Type == tokenTypeNot {
+ e, i, err = parseNextExpression(t[1:], math2.MaxInt)
+ if err != nil {
+ return nil, 0, err
+ }
+ return newNegative(e), i + 1, nil
+ }
+
+ // Negative numbers - -5
+ if t[0].Type == tokenTypeMath && operator(t[0].Value.(string)) == operatorSubtract {
+ e, i, err = parseNextExpression(t[1:], -1)
+ if err != nil {
+ return nil, 0, err
+ }
+ return newMath(operatorSubtract, NewValue(0), e), i + 1, nil
+ }
+
+ // Call - abc(a, b, c)
+ if t[0].Type == tokenTypeAccessor && len(t) > 1 && t[1].Type == tokenTypeOpen {
+ args := make([]Expression, 0)
+ index := 2
+ for {
+ // Ensure there is another token (for call close or next argument)
+ if len(t) <= index {
+ return nil, 2, errors.New("premature end of expression: missing call close")
+ }
+
+ // Close the call
+ if t[index].Type == tokenTypeClose {
+ break
+ }
+
+ // Ensure comma between arguments
+ if len(args) != 0 {
+ if t[index].Type != tokenTypeComma {
+ return nil, 2, errors.New("expression syntax error: expected comma or call close")
+ }
+ index++
+ }
+ next, l, err := parseNextExpression(t[index:], -1)
+ index += l
+ if err != nil {
+ return nil, index, err
+ }
+ args = append(args, next)
+ }
+ return newCall(t[0].Value.(string), args), index + 1, nil
+ }
+
+ // Accessor - abc
+ if t[0].Type == tokenTypeAccessor {
+ return newAccessor(t[0].Value.(string)), 1, nil
+ }
+
+ return nil, 0, fmt.Errorf("unexpected token in expression: %v", t)
+}
+
+func parse(t []token) (e Expression, err error) {
+ if len(t) == 0 {
+ return None, nil
+ }
+ e, l, err := parseNextExpression(t, -1)
+ if err != nil {
+ return nil, err
+ }
+ if l < len(t) {
+ return nil, fmt.Errorf("unexpected token after end of expression: %v", t[l])
+ }
+ return e, nil
+}
+
+func Compile(exp string) (Expression, error) {
+ t, _, e := tokenize(exp, 0)
+ if e != nil {
+ return nil, fmt.Errorf("tokenizer error: %v", e)
+ }
+ v, e := parse(t)
+ if e != nil {
+ return nil, fmt.Errorf("parser error: %v", e)
+ }
+ return v.Resolve()
+}
+
+func MustCompile(exp string) Expression {
+ v, err := Compile(exp)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+var endExprRe = regexp.MustCompile(`^\s*}}`)
+
+func CompileTemplate(tpl string) (Expression, error) {
+ var e Expression
+
+ offset := 0
+ for index := strings.Index(tpl[offset:], "{{"); index != -1; index = strings.Index(tpl[offset:], "{{") {
+ if index != 0 {
+ e = newMath(operatorAdd, e, NewStringValue(tpl[offset:offset+index]))
+ }
+ offset += index + 2
+ tokens, i, err := tokenize(tpl, offset)
+ offset = i
+ if err == nil {
+ return nil, errors.New("template error: expression not closed")
+ }
+ if !endExprRe.MatchString(tpl[offset:]) || !strings.Contains(err.Error(), "unknown character") {
+ return nil, fmt.Errorf("tokenizer error: %v", err)
+ }
+ offset += len(endExprRe.FindString(tpl[offset:]))
+ if len(tokens) == 0 {
+ continue
+ }
+ v, err := parse(tokens)
+ if err != nil {
+ return nil, fmt.Errorf("parser error: %v", e)
+ }
+ v, err = v.Resolve()
+ if err != nil {
+ return nil, fmt.Errorf("expression error: %v", e)
+ }
+ e = newMath(operatorAdd, e, CastToString(v))
+ }
+ if offset < len(tpl) {
+ e = newMath(operatorAdd, e, NewStringValue(tpl[offset:]))
+ }
+ if e == nil {
+ return NewStringValue(""), nil
+ }
+ return e.Resolve()
+}
+
+func MustCompileTemplate(tpl string) Expression {
+ v, err := CompileTemplate(tpl)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+func CompileAndResolve(exp string, m ...Machine) (Expression, error) {
+ e, err := Compile(exp)
+ if err != nil {
+ return e, err
+ }
+ return e.Resolve(m...)
+}
+
+func CompileAndResolveTemplate(tpl string, m ...Machine) (Expression, error) {
+ e, err := CompileTemplate(tpl)
+ if err != nil {
+ return e, err
+ }
+ return e.Resolve(m...)
+}
+
+func IsTemplateStringWithoutExpressions(tpl string) bool {
+ return !strings.Contains(tpl, "{{")
+}
diff --git a/pkg/tcl/expressionstcl/parse_test.go b/pkg/tcl/expressionstcl/parse_test.go
new file mode 100644
index 00000000000..fd16bab9c2e
--- /dev/null
+++ b/pkg/tcl/expressionstcl/parse_test.go
@@ -0,0 +1,261 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCompileBasic(t *testing.T) {
+ assert.Equal(t, "value", must(MustCompile(`"value"`).Static().StringValue()))
+}
+
+func TestCompileTernary(t *testing.T) {
+ assert.Equal(t, "value", must(MustCompile(`true ? "value" : "another"`).Static().StringValue()))
+ assert.Equal(t, "another", must(MustCompile(`false ? "value" : "another"`).Static().StringValue()))
+ assert.Equal(t, "xyz", must(MustCompile(`false ? "value" : true ? "xyz" :"another"`).Static().StringValue()))
+ assert.Equal(t, "xyz", must(MustCompile(`false ? "value" : (true ? "xyz" :"another")`).Static().StringValue()))
+ assert.Equal(t, 5.78, must(MustCompile(`false ? 3 : (true ? 5.78 : 2)`).Static().FloatValue()))
+}
+
+func TestCompileMath(t *testing.T) {
+ assert.Equal(t, 5.0, must(MustCompile(`2 + 3`).Static().FloatValue()))
+ assert.Equal(t, 0.6, must(MustCompile(`3 / 5`).Static().FloatValue()))
+ assert.Equal(t, true, must(MustCompile(`3 <> 5`).Static().BoolValue()))
+ assert.Equal(t, true, must(MustCompile(`3 != 5`).Static().BoolValue()))
+ assert.Equal(t, false, must(MustCompile(`3 == 5`).Static().BoolValue()))
+ assert.Equal(t, false, must(MustCompile(`3 = 5`).Static().BoolValue()))
+}
+
+func TestCompileLogical(t *testing.T) {
+ assert.Equal(t, "true", MustCompile(`!(false && r1)`).String())
+ assert.Equal(t, "false", MustCompile(`!true && r1`).String())
+ assert.Equal(t, "r1", MustCompile(`true && r1`).String())
+ assert.Equal(t, "r1", MustCompile(`!true || r1`).String())
+ assert.Equal(t, "true", MustCompile(`true || r1`).String())
+ assert.Equal(t, "11", MustCompile(`5 - -3 * 2`).String())
+ assert.Equal(t, "r1&&false", MustCompile(`r1 && false`).String())
+ assert.Equal(t, "bool(r1)", MustCompile(`bool(r1) && true`).String())
+ assert.Equal(t, "false", MustCompile(`bool(r1) && false`).String())
+ assert.Equal(t, "r1||false", MustCompile(`r1 || false`).String())
+ assert.Equal(t, "bool(r1)", MustCompile(`bool(r1) || false`).String())
+ assert.Equal(t, "r1||true", MustCompile(`r1 || true`).String())
+ assert.Equal(t, "true", MustCompile(`bool(r1) || true`).String())
+}
+
+func TestCompileMathOperationsPrecedence(t *testing.T) {
+ assert.Equal(t, 7.0, must(MustCompile(`1 + 2 * 3`).Static().FloatValue()))
+ assert.Equal(t, 11.0, must(MustCompile(`1 + (2 * 3) + 4`).Static().FloatValue()))
+ assert.Equal(t, 11.0, must(MustCompile(`1 + 2 * 3 + 4`).Static().FloatValue()))
+ assert.Equal(t, 30.0, must(MustCompile(`1 + 2 * 3 * 4 + 5`).Static().FloatValue()))
+ assert.Equal(t, true, must(MustCompile(`1 + 2 * 3 * 4 + 5 <> 3`).Static().BoolValue()))
+
+ assert.Equal(t, false, must(MustCompile(`1 + 2 * 3 * 4 + 5 == 3`).Static().BoolValue()))
+ assert.Equal(t, true, must(MustCompile(`1 + 2 * 3 * 4 + 5 = 30`).Static().BoolValue()))
+ assert.Equal(t, false, must(MustCompile(`1 + 2 * 3 * 4 + 5 <> 30`).Static().BoolValue()))
+ assert.Equal(t, false, must(MustCompile(`1 + 2 * 3 * 4 + 5 <> 20 + 10`).Static().BoolValue()))
+ assert.Equal(t, true, must(MustCompile(`1 + 2 * 3 * 4 + 5 = 20 + 10`).Static().BoolValue()))
+ assert.Equal(t, false, must(MustCompile(`1 + 2 * 3 * 4 + 5 <> 20 + 10`).Static().BoolValue()))
+ assert.Equal(t, true, must(MustCompile(`1 + 2 * 3 * 4 + 5 = 2 + 3 * 6 + 10`).Static().BoolValue()))
+ assert.Equal(t, false, must(MustCompile(`1 + 2 * 3 * 4 + 5 <> 2 + 3 * 6 + 10`).Static().BoolValue()))
+ assert.Equal(t, 8.0, must(MustCompile(`5 + 3 / 3 * 3`).Static().FloatValue()))
+ assert.Equal(t, true, must(MustCompile(`5 + 3 / 3 * 3 = 8`).Static().BoolValue()))
+ assert.Equal(t, 8.0, must(MustCompile(`5 + 3 * 3 / 3`).Static().FloatValue()))
+ assert.Equal(t, true, must(MustCompile(`5 + 3 * 3 / 3 = 8`).Static().BoolValue()))
+ assert.Equal(t, true, must(MustCompile(`5 + 3 * 3 / 3 = 2 + 3 * 2`).Static().BoolValue()))
+ assert.Equal(t, false, must(MustCompile(`5 + 3 * 3 / 3 = 3 + 3 * 2`).Static().BoolValue()))
+
+ assert.Equal(t, false, must(MustCompile(`true && false || false && true`).Static().BoolValue()))
+ assert.Equal(t, true, must(MustCompile(`true && false || true`).Static().BoolValue()))
+ assert.Equal(t, int64(0), must(MustCompile(`1 && 0 && 2`).Static().IntValue()))
+ assert.Equal(t, int64(2), must(MustCompile(`1 && 0 || 2`).Static().IntValue()))
+ assert.Equal(t, int64(1), must(MustCompile(`1 || 0 || 2`).Static().IntValue()))
+
+ assert.Equal(t, true, must(MustCompile(`10 > 2 && 5 <= 5`).Static().BoolValue()))
+ assert.Equal(t, false, must(MustCompile(`10 > 2 && 5 < 5`).Static().BoolValue()))
+ assert.Error(t, errOnly(Compile(`10 > 2 > 3`)))
+
+ assert.Equal(t, 817.0, must(MustCompile(`1 + 2 * 3 ** 4 * 5 + 6`).Static().FloatValue()))
+ assert.Equal(t, 4.5, must(MustCompile(`72 / 2 ** 4`).Static().FloatValue()))
+ assert.InDelta(t, 3.6, must(MustCompile(`3 * 5.2 % 4`).Static().FloatValue()), 0.00001)
+
+ assert.Equal(t, true, must(MustCompile(`!0 && 500`).Static().BoolValue()))
+ assert.Equal(t, false, must(MustCompile(`!5 && 500`).Static().BoolValue()))
+
+ assert.Equal(t, "A+B*(C+D)/E*F+G<>H**I*J**K", MustCompile(`A + B * (C + D) / E * F + G <> H ** I * J ** K`).String())
+}
+
+func TestBuildTemplate(t *testing.T) {
+ assert.Equal(t, "abc", MustCompile(`"abc"`).Template())
+ assert.Equal(t, "abcdef", MustCompile(`"abc" + "def"`).Template())
+ assert.Equal(t, "abc9", MustCompile(`"abc" + 9`).Template())
+ assert.Equal(t, "abc{{env.xyz}}", MustCompile(`"abc" + env.xyz`).Template())
+ assert.Equal(t, "{{env.xyz}}abc", MustCompile(`env.xyz + "abc"`).Template())
+ assert.Equal(t, "{{env.xyz+env.abc}}abc", MustCompile(`env.xyz + env.abc + "abc"`).Template())
+ assert.Equal(t, "{{env.xyz+env.abc}}abc", MustCompile(`env.xyz + env.abc + "abc"`).Template())
+ assert.Equal(t, "{{3+env.xyz+env.abc}}", MustCompile(`3 + env.xyz + env.abc`).Template())
+ assert.Equal(t, "3{{env.xyz}}{{env.abc}}", MustCompile(`string(3) + env.xyz + env.abc`).Template())
+ assert.Equal(t, "3{{env.xyz+env.abc}}", MustCompile(`string(3) + (env.xyz + env.abc)`).Template())
+ assert.Equal(t, "3{{env.xyz}}{{env.abc}}", MustCompile(`"3" + env.xyz + env.abc`).Template())
+ assert.Equal(t, "3{{env.xyz+env.abc}}", MustCompile(`"3" + (env.xyz + env.abc)`).Template())
+}
+
+func TestCompileTemplate(t *testing.T) {
+ assert.Equal(t, `""`, MustCompileTemplate(``).String())
+ assert.Equal(t, `"abc"`, MustCompileTemplate(`abc`).String())
+ assert.Equal(t, `"abcxyz5"`, MustCompileTemplate(`abc{{ "xyz" }}{{ 5 }}`).String())
+ assert.Equal(t, `"abc50"`, MustCompileTemplate(`abc{{ 5 + 45 }}`).String())
+ assert.Equal(t, `"abc50def"`, MustCompileTemplate(`abc{{ 5 + 45 }}def`).String())
+ assert.Equal(t, `"abc50def"+string(env.abc*5)+"20"`, MustCompileTemplate(`abc{{ 5 + 45 }}def{{env.abc * 5}}20`).String())
+
+ assert.Equal(t, `abc50def`, must(MustCompileTemplate(`abc{{ 5 + 45 }}def`).Static().StringValue()))
+}
+
+func TestCompilePartialResolution(t *testing.T) {
+ vm := NewMachine().
+ Register("someint", 555).
+ Register("somestring", "foo").
+ RegisterAccessor(func(name string) (interface{}, bool) {
+ if strings.HasPrefix(name, "env.") {
+ return "[placeholder:" + name[4:] + "]", true
+ }
+ return nil, false
+ }).
+ RegisterAccessor(func(name string) (interface{}, bool) {
+ if strings.HasPrefix(name, "secrets.") {
+ return MustCompile("secret(" + name[8:] + ")"), true
+ }
+ return nil, false
+ }).
+ RegisterFunction("mainEndpoint", func(values ...StaticValue) (interface{}, bool, error) {
+ if len(values) != 0 {
+ return nil, true, errors.New("the mainEndpoint should have no parameters")
+ }
+ return MustCompile(`env.apiUrl`), true, nil
+ })
+
+ assert.Equal(t, `555`, must(MustCompile(`someint`).Resolve(vm)).String())
+ assert.Equal(t, `"[placeholder:name]"`, must(MustCompile(`env.name`).Resolve(vm)).String())
+ assert.Equal(t, `secret(name)`, must(MustCompile(`secrets.name`).Resolve(vm)).String())
+ assert.Equal(t, `"[placeholder:apiUrl]"`, must(MustCompile(`mainEndpoint()`).Resolve(vm)).String())
+}
+
+func TestCompileResolution(t *testing.T) {
+ vm := NewMachine().
+ Register("someint", 555).
+ Register("somestring", "foo").
+ RegisterAccessor(func(name string) (interface{}, bool) {
+ if strings.HasPrefix(name, "env.") {
+ return "[placeholder:" + name[4:] + "]", true
+ }
+ return nil, false
+ }).
+ RegisterAccessor(func(name string) (interface{}, bool) {
+ if strings.HasPrefix(name, "secrets.") {
+ return MustCompile("secret(" + name[8:] + ")"), true
+ }
+ return nil, false
+ }).
+ RegisterFunction("mainEndpoint", func(values ...StaticValue) (interface{}, bool, error) {
+ if len(values) != 0 {
+ return nil, true, errors.New("the mainEndpoint should have no parameters")
+ }
+ return MustCompile(`env.apiUrl`), true, nil
+ })
+
+ assert.Equal(t, `555`, must(MustCompile(`someint`).Resolve(vm, FinalizerFail)).String())
+ assert.Equal(t, `"[placeholder:name]"`, must(MustCompile(`env.name`).Resolve(vm, FinalizerFail)).String())
+ assert.Error(t, errOnly(MustCompile(`secrets.name`).Resolve(vm, FinalizerFail)))
+ assert.Equal(t, `"[placeholder:apiUrl]"`, must(MustCompile(`mainEndpoint()`).Resolve(vm, FinalizerFail)).String())
+}
+
+func TestCircularResolution(t *testing.T) {
+ vm := NewMachine().
+ RegisterFunction("one", func(values ...StaticValue) (interface{}, bool, error) {
+ return MustCompile("two()"), true, nil
+ }).
+ RegisterFunction("two", func(values ...StaticValue) (interface{}, bool, error) {
+ return MustCompile("one()"), true, nil
+ }).
+ RegisterFunction("self", func(values ...StaticValue) (interface{}, bool, error) {
+ return MustCompile("self()"), true, nil
+ })
+
+ assert.Contains(t, fmt.Sprintf("%v", errOnly(MustCompile(`one()`).Resolve(vm, FinalizerFail))), "call stack exceeded")
+ assert.Contains(t, fmt.Sprintf("%v", errOnly(MustCompile(`self()`).Resolve(vm, FinalizerFail))), "call stack exceeded")
+}
+
+func TestMinusNumber(t *testing.T) {
+ assert.Equal(t, -4.0, must(MustCompile("-4").Static().FloatValue()))
+}
+
+func TestCompileMultilineString(t *testing.T) {
+ assert.Equal(t, `"\nabc\ndef\n"`, MustCompile(`"
+abc
+def
+"`).String())
+}
+
+func TestCompileEscapeTemplate(t *testing.T) {
+ assert.Equal(t, `foo{{"{{"}}barbaz{{"{{"}}`, MustCompileTemplate(`foo{{"{{bar"}}baz{{"{{"}}`).Template())
+}
+
+func TestCompileStandardLib(t *testing.T) {
+ assert.Equal(t, `false`, MustCompile(`bool(0)`).String())
+ assert.Equal(t, `true`, MustCompile(`bool(500)`).String())
+ assert.Equal(t, `"500"`, MustCompile(`string(500)`).String())
+ assert.Equal(t, `500`, MustCompile(`int(500)`).String())
+ assert.Equal(t, `500`, MustCompile(`int(500.888)`).String())
+ assert.Equal(t, `500`, MustCompile(`int("500")`).String())
+ assert.Equal(t, `500.44`, MustCompile(`float("500.44")`).String())
+ assert.Equal(t, `500`, MustCompile(`json("500")`).String())
+ assert.Equal(t, `{"a":500}`, MustCompile(`json("{\"a\": 500}")`).String())
+ assert.Equal(t, `"{\"a\":500}"`, MustCompile(`tojson({"a": 500})`).String())
+ assert.Equal(t, `"500.8"`, MustCompile(`tojson(500.8)`).String())
+ assert.Equal(t, `"\"500.8\""`, MustCompile(`tojson("500.8")`).String())
+ assert.Equal(t, `"abc"`, MustCompile(`shellquote("abc")`).String())
+ assert.Equal(t, `"'a b c'"`, MustCompile(`shellquote("a b c")`).String())
+ assert.Equal(t, `"'a b c' 'd e f'"`, MustCompile(`shellquote("a b c", "d e f")`).String())
+ assert.Equal(t, `"''"`, MustCompile(`shellquote(null)`).String())
+ assert.Equal(t, `"abc d"`, MustCompile(`trim(" abc d \n ")`).String())
+ assert.Equal(t, `"abc"`, MustCompile(`yaml("\"abc\"")`).String())
+ assert.Equal(t, `{"foo":{"bar":"baz"}}`, MustCompile(`yaml("foo:\n bar: 'baz'")`).String())
+ assert.Equal(t, `"foo:\n bar: baz\n"`, MustCompile(`toyaml({"foo":{"bar":"baz"}})`).String())
+ assert.Equal(t, `{"a":["b","v"]}`, MustCompile(`yaml("
+a:
+- b
+- v
+")`).String())
+ assert.Equal(t, `["a",10,["a",4]]`, MustCompile(`list("a", 10, ["a", 4])`).String())
+ assert.Equal(t, `"a,10,a,4"`, MustCompile(`join(["a",10,["a",4]])`).String())
+ assert.Equal(t, `"a---10---a,4"`, MustCompile(`join(["a",10,["a",4]], "---")`).String())
+ assert.Equal(t, `[""]`, MustCompile(`split(null)`).String())
+ assert.Equal(t, `["a","b","c"]`, MustCompile(`split("a,b,c")`).String())
+ assert.Equal(t, `["a","b","c"]`, MustCompile(`split("a---b---c", "---")`).String())
+}
+
+func TestCompileDetectAccessors(t *testing.T) {
+ assert.Equal(t, map[string]struct{}{"something": {}}, MustCompile(`something`).Accessors())
+ assert.Equal(t, map[string]struct{}{"something": {}, "other": {}, "another": {}}, MustCompile(`calling(something, 5 * (other + 3), !another)`).Accessors())
+}
+
+func TestCompileDetectFunctions(t *testing.T) {
+ assert.Equal(t, map[string]struct{}(nil), MustCompile(`something`).Functions())
+ assert.Equal(t, map[string]struct{}{"calling": {}, "something": {}, "string": {}, "a": {}}, MustCompile(`calling(something(), 45 + 2 + 10 + string(abc * a(c)))`).Functions())
+}
+
+func TestCompileImmutableNone(t *testing.T) {
+ assert.Same(t, None, NewValue(noneValue))
+ assert.Same(t, NewValue(noneValue), NewValue(noneValue))
+}
diff --git a/pkg/tcl/expressionstcl/static.go b/pkg/tcl/expressionstcl/static.go
new file mode 100644
index 00000000000..e8ecc3ffdcf
--- /dev/null
+++ b/pkg/tcl/expressionstcl/static.go
@@ -0,0 +1,160 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "encoding/json"
+ "strings"
+)
+
+type static struct {
+ value interface{}
+}
+
+var none *static
+var None StaticValue = none
+
+func NewValue(value interface{}) StaticValue {
+ if value == noneValue {
+ return None
+ }
+ return &static{value: value}
+}
+
+func NewStringValue(value interface{}) StaticValue {
+ v, _ := toString(value)
+ return NewValue(v)
+}
+
+func (s *static) Type() Type {
+ if s == nil {
+ return TypeUnknown
+ }
+ switch s.value.(type) {
+ case int64:
+ return TypeInt64
+ case float64:
+ return TypeFloat64
+ case string:
+ return TypeString
+ case bool:
+ return TypeBool
+ default:
+ return TypeUnknown
+ }
+}
+
+func (s *static) String() string {
+ if s.IsNone() {
+ return "null"
+ }
+ b, _ := json.Marshal(s.value)
+ if len(b) == 0 {
+ return "null"
+ }
+ r := string(b)
+ if s.IsMap() && r == "null" {
+ return "{}"
+ }
+ if s.IsSlice() && r == "null" {
+ return "[]"
+ }
+ return r
+}
+
+func (s *static) SafeString() string {
+ return s.String()
+}
+
+func (s *static) Template() string {
+ if s.IsNone() {
+ return ""
+ }
+ v, _ := s.StringValue()
+ return strings.ReplaceAll(v, "{{", "{{\"{{\"}}")
+}
+
+func (s *static) SafeResolve(_ ...Machine) (Expression, bool, error) {
+ return s, false, nil
+}
+
+func (s *static) Resolve(_ ...Machine) (Expression, error) {
+ return s, nil
+}
+
+func (s *static) Static() StaticValue {
+ return s
+}
+
+func (s *static) IsNone() bool {
+ return s == nil
+}
+
+func (s *static) IsString() bool {
+ return !s.IsNone() && isString(s.value)
+}
+
+func (s *static) IsBool() bool {
+ return !s.IsNone() && isBool(s.value)
+}
+
+func (s *static) IsInt() bool {
+ return !s.IsNone() && isInt(s.value)
+}
+
+func (s *static) IsNumber() bool {
+ return !s.IsNone() && isNumber(s.value)
+}
+
+func (s *static) IsMap() bool {
+ return !s.IsNone() && isMap(s.value)
+}
+
+func (s *static) IsSlice() bool {
+ return !s.IsNone() && isSlice(s.value)
+}
+
+func (s *static) Value() interface{} {
+ if s.IsNone() {
+ return noneValue
+ }
+ return s.value
+}
+
+func (s *static) StringValue() (string, error) {
+ return toString(s.Value())
+}
+
+func (s *static) BoolValue() (bool, error) {
+ return toBool(s.Value())
+}
+
+func (s *static) IntValue() (int64, error) {
+ return toInt(s.Value())
+}
+
+func (s *static) FloatValue() (float64, error) {
+ return toFloat(s.Value())
+}
+
+func (s *static) MapValue() (map[string]interface{}, error) {
+ return toMap(s.Value())
+}
+
+func (s *static) SliceValue() ([]interface{}, error) {
+ return toSlice(s.Value())
+}
+
+func (s *static) Accessors() map[string]struct{} {
+ return nil
+}
+
+func (s *static) Functions() map[string]struct{} {
+ return nil
+}
diff --git a/pkg/tcl/expressionstcl/static_test.go b/pkg/tcl/expressionstcl/static_test.go
new file mode 100644
index 00000000000..40daf184185
--- /dev/null
+++ b/pkg/tcl/expressionstcl/static_test.go
@@ -0,0 +1,256 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func must[T interface{}](v T, e error) T {
+ if e != nil {
+ panic(e)
+ }
+ return v
+}
+
+func errOnly(_ interface{}, e error) error {
+ return e
+}
+
+func TestStaticBool(t *testing.T) {
+ // Types
+ assert.Equal(t, "false", NewValue(false).String())
+ assert.Equal(t, "true", NewValue(true).String())
+ assert.Equal(t, true, NewValue(false).IsBool())
+ assert.Equal(t, true, NewValue(true).IsBool())
+ assert.Equal(t, false, NewValue(false).IsNone())
+ assert.Equal(t, false, NewValue(true).IsNone())
+ assert.Equal(t, false, NewValue(true).IsInt())
+ assert.Equal(t, false, NewValue(true).IsNumber())
+ assert.Equal(t, false, NewValue(true).IsString())
+ assert.Equal(t, false, NewValue(true).IsMap())
+ assert.Equal(t, false, NewValue(true).IsSlice())
+
+ // Conversion
+ assert.Equal(t, false, must(NewValue(false).BoolValue()))
+ assert.Equal(t, true, must(NewValue(true).BoolValue()))
+ assert.Error(t, errOnly(NewValue(true).IntValue()))
+ assert.Error(t, errOnly(NewValue(true).FloatValue()))
+ assert.Equal(t, "true", must(NewValue(true).StringValue()))
+ assert.Equal(t, "false", must(NewValue(false).StringValue()))
+ assert.Error(t, errOnly(NewValue(true).MapValue()))
+ assert.Error(t, errOnly(NewValue(true).SliceValue()))
+}
+
+func TestStaticInt(t *testing.T) {
+ // Types
+ assert.Equal(t, "0", NewValue(0).String())
+ assert.Equal(t, "1", NewValue(1).String())
+ assert.Equal(t, false, NewValue(0).IsBool())
+ assert.Equal(t, false, NewValue(1).IsBool())
+ assert.Equal(t, false, NewValue(0).IsNone())
+ assert.Equal(t, false, NewValue(1).IsNone())
+ assert.Equal(t, true, NewValue(1).IsInt())
+ assert.Equal(t, true, NewValue(1).IsNumber())
+ assert.Equal(t, false, NewValue(1).IsString())
+ assert.Equal(t, false, NewValue(1).IsMap())
+ assert.Equal(t, false, NewValue(1).IsSlice())
+
+ // Conversion
+ assert.Equal(t, false, must(NewValue(0).BoolValue()))
+ assert.Equal(t, true, must(NewValue(1).BoolValue()))
+ assert.Equal(t, int64(1), must(NewValue(1).IntValue()))
+ assert.Equal(t, 1.0, must(NewValue(1).FloatValue()))
+ assert.Equal(t, "1", must(NewValue(1).StringValue()))
+ assert.Error(t, errOnly(NewValue(1).MapValue()))
+ assert.Error(t, errOnly(NewValue(1).SliceValue()))
+}
+
+func TestStaticFloat(t *testing.T) {
+ // Types
+ assert.Equal(t, "0", NewValue(0.0).String())
+ assert.Equal(t, "1.5", NewValue(1.5).String())
+ assert.Equal(t, false, NewValue(0.0).IsBool())
+ assert.Equal(t, false, NewValue(1.0).IsBool())
+ assert.Equal(t, false, NewValue(1.5).IsBool())
+ assert.Equal(t, false, NewValue(0.0).IsNone())
+ assert.Equal(t, false, NewValue(1.0).IsNone())
+ assert.Equal(t, false, NewValue(1.5).IsNone())
+ assert.Equal(t, true, NewValue(1.0).IsInt())
+ assert.Equal(t, false, NewValue(1.8).IsInt())
+ assert.Equal(t, true, NewValue(1.5).IsNumber())
+ assert.Equal(t, false, NewValue(1.7).IsString())
+ assert.Equal(t, false, NewValue(1.7).IsMap())
+ assert.Equal(t, false, NewValue(1.3).IsSlice())
+
+ // Conversion
+ assert.Equal(t, false, must(NewValue(0.0).BoolValue()))
+ assert.Equal(t, true, must(NewValue(0.5).BoolValue()))
+ assert.Equal(t, true, must(NewValue(1.0).BoolValue()))
+ assert.Equal(t, true, must(NewValue(1.5).BoolValue()))
+ assert.Equal(t, int64(1), must(NewValue(1.8).IntValue()))
+ assert.Equal(t, 1.8, must(NewValue(1.8).FloatValue()))
+ assert.Equal(t, "1.877778", must(NewValue(1.877778).StringValue()))
+ assert.Equal(t, "1.88", must(NewValue(1.88).StringValue()))
+ assert.Error(t, errOnly(NewValue(1.8).MapValue()))
+ assert.Error(t, errOnly(NewValue(1.8).SliceValue()))
+}
+
+func TestStaticString(t *testing.T) {
+ // Types
+ assert.Equal(t, `""`, NewValue("").String())
+ assert.Equal(t, `"value"`, NewValue("value").String())
+ assert.Equal(t, `"v\"alue"`, NewValue("v\"alue").String())
+ assert.Equal(t, false, NewValue("").IsBool())
+ assert.Equal(t, false, NewValue("value").IsBool())
+ assert.Equal(t, false, NewValue("").IsNone())
+ assert.Equal(t, false, NewValue("value").IsNone())
+ assert.Equal(t, false, NewValue("5").IsInt())
+ assert.Equal(t, false, NewValue("value").IsInt())
+ assert.Equal(t, false, NewValue("5").IsNumber())
+ assert.Equal(t, false, NewValue("value").IsNumber())
+ assert.Equal(t, true, NewValue("").IsString())
+ assert.Equal(t, true, NewValue("value").IsString())
+ assert.Equal(t, false, NewValue("value").IsMap())
+ assert.Equal(t, false, NewValue("value").IsSlice())
+
+ // Conversion
+ assert.Equal(t, false, must(NewValue("").BoolValue()))
+ assert.Equal(t, false, must(NewValue("0").BoolValue()))
+ assert.Equal(t, false, must(NewValue("off").BoolValue()))
+ assert.Equal(t, false, must(NewValue("false").BoolValue()))
+ assert.Equal(t, true, must(NewValue("False").BoolValue()))
+ assert.Equal(t, true, must(NewValue("true").BoolValue()))
+ assert.Equal(t, true, must(NewValue("on").BoolValue()))
+ assert.Equal(t, true, must(NewValue("1").BoolValue()))
+ assert.Equal(t, true, must(NewValue("something").BoolValue()))
+ assert.Equal(t, int64(1), must(NewValue("1").IntValue()))
+ assert.Equal(t, int64(1), must(NewValue("1.5").IntValue()))
+ assert.Error(t, errOnly(NewValue("").IntValue()))
+ assert.Error(t, errOnly(NewValue("5 apples").IntValue()))
+ assert.Equal(t, 1.0, must(NewValue("1").FloatValue()))
+ assert.Equal(t, 1.5, must(NewValue("1.5").FloatValue()))
+ assert.Error(t, errOnly(NewValue("").FloatValue()))
+ assert.Error(t, errOnly(NewValue("5 apples").FloatValue()))
+ assert.Equal(t, "", must(NewValue("").StringValue()))
+ assert.Equal(t, "value", must(NewValue("value").StringValue()))
+ assert.Equal(t, `v"alu\e`, must(NewValue(`v"alu\e`).StringValue()))
+ assert.Error(t, errOnly(NewValue("").MapValue()))
+ assert.Error(t, errOnly(NewValue("v").MapValue()))
+ assert.Error(t, errOnly(NewValue("").SliceValue()))
+ assert.Error(t, errOnly(NewValue("v").SliceValue()))
+}
+
+func TestStaticMap(t *testing.T) {
+ // Types
+ assert.Equal(t, "{}", NewValue(map[string]interface{}(nil)).String())
+ assert.Equal(t, "{}", NewValue(map[string]string(nil)).String())
+ assert.Equal(t, `{"a":"b"}`, NewValue(map[string]string{"a": "b"}).String())
+ assert.Equal(t, `{"3":"b"}`, NewValue(map[int]string{3: "b"}).String())
+ assert.Equal(t, false, NewValue(map[string]interface{}(nil)).IsBool())
+ assert.Equal(t, false, NewValue(map[string]interface{}{}).IsBool())
+ assert.Equal(t, false, NewValue(map[string]interface{}{"a": "b"}).IsBool())
+ assert.Equal(t, false, NewValue(map[string]interface{}(nil)).IsNone())
+ assert.Equal(t, false, NewValue(map[string]interface{}{}).IsNone())
+ assert.Equal(t, false, NewValue(map[string]interface{}{"a": "b"}).IsNone())
+ assert.Equal(t, false, NewValue(map[int]interface{}{3: "3"}).IsInt())
+ assert.Equal(t, false, NewValue(map[int]interface{}{3: "3"}).IsNumber())
+ assert.Equal(t, false, NewValue(map[int]interface{}{3: "3"}).IsString())
+ assert.Equal(t, true, NewValue(map[string]interface{}(nil)).IsMap())
+ assert.Equal(t, true, NewValue(map[string]interface{}{}).IsMap())
+ assert.Equal(t, true, NewValue(map[string]interface{}{"a": "b"}).IsMap())
+ assert.Equal(t, false, NewValue(map[string]interface{}(nil)).IsSlice())
+ assert.Equal(t, false, NewValue(map[string]interface{}{}).IsSlice())
+ assert.Equal(t, false, NewValue(map[string]interface{}{"a": "b"}).IsSlice())
+
+ // Conversion
+ assert.Equal(t, false, must(NewValue(map[string]string{}).BoolValue()))
+ assert.Equal(t, false, must(NewValue(map[string]string(nil)).BoolValue()))
+ assert.Equal(t, true, must(NewValue(map[string]string{"a": "b"}).BoolValue()))
+ assert.Error(t, errOnly(NewValue(map[string]string(nil)).IntValue()))
+ assert.Error(t, errOnly(NewValue(map[string]string{}).IntValue()))
+ assert.Error(t, errOnly(NewValue(map[string]string{"a": "b"}).IntValue()))
+ assert.Error(t, errOnly(NewValue(map[string]string(nil)).FloatValue()))
+ assert.Error(t, errOnly(NewValue(map[string]string{}).FloatValue()))
+ assert.Error(t, errOnly(NewValue(map[string]string{"a": "b"}).FloatValue()))
+ assert.Equal(t, "{}", must(NewValue(map[string]string(nil)).StringValue()))
+ assert.Equal(t, "{}", must(NewValue(map[string]string{}).StringValue()))
+ assert.Equal(t, `{"a":"b"}`, must(NewValue(map[string]string{"a": "b"}).StringValue()))
+ assert.Equal(t, map[string]interface{}{}, must(NewValue(map[string]string(nil)).MapValue()))
+ assert.Equal(t, map[string]interface{}{}, must(NewValue(map[string]string{}).MapValue()))
+ assert.Equal(t, map[string]interface{}{"a": "b"}, must(NewValue(map[string]string{"a": "b"}).MapValue()))
+ assert.Error(t, errOnly(NewValue(map[string]string(nil)).SliceValue()))
+ assert.Error(t, errOnly(NewValue(map[int]string{}).SliceValue()))
+ assert.Error(t, errOnly(NewValue(map[int]string{3: "a"}).SliceValue()))
+}
+
+func TestStaticSlice(t *testing.T) {
+ // Types
+ assert.Equal(t, "[]", NewValue([]interface{}(nil)).String())
+ assert.Equal(t, "[]", NewValue([]string(nil)).String())
+ assert.Equal(t, `["a","b"]`, NewValue([]string{"a", "b"}).String())
+ assert.Equal(t, `[3]`, NewValue([]int{3}).String())
+ assert.Equal(t, false, NewValue([]interface{}(nil)).IsBool())
+ assert.Equal(t, false, NewValue([]interface{}{}).IsBool())
+ assert.Equal(t, false, NewValue([]interface{}{"a", "b"}).IsBool())
+ assert.Equal(t, false, NewValue([]interface{}(nil)).IsNone())
+ assert.Equal(t, false, NewValue([]interface{}{}).IsNone())
+ assert.Equal(t, false, NewValue([]interface{}{"a", "b"}).IsNone())
+ assert.Equal(t, false, NewValue([]interface{}{3: "3"}).IsInt())
+ assert.Equal(t, false, NewValue([]interface{}{3: "3"}).IsNumber())
+ assert.Equal(t, false, NewValue([]interface{}{3: "3"}).IsString())
+ assert.Equal(t, false, NewValue([]interface{}(nil)).IsMap())
+ assert.Equal(t, false, NewValue([]interface{}{}).IsMap())
+ assert.Equal(t, false, NewValue([]interface{}{"a", "b"}).IsMap())
+ assert.Equal(t, true, NewValue([]interface{}(nil)).IsSlice())
+ assert.Equal(t, true, NewValue([]interface{}{}).IsSlice())
+ assert.Equal(t, true, NewValue([]interface{}{"a", "b"}).IsSlice())
+
+ // Conversion
+ assert.Equal(t, false, must(NewValue([]string{}).BoolValue()))
+ assert.Equal(t, false, must(NewValue([]string(nil)).BoolValue()))
+ assert.Equal(t, true, must(NewValue([]string{"a", "b"}).BoolValue()))
+ assert.Error(t, errOnly(NewValue([]string(nil)).IntValue()))
+ assert.Error(t, errOnly(NewValue([]string{}).IntValue()))
+ assert.Error(t, errOnly(NewValue([]string{"a", "b"}).IntValue()))
+ assert.Error(t, errOnly(NewValue([]string(nil)).FloatValue()))
+ assert.Error(t, errOnly(NewValue([]string{}).FloatValue()))
+ assert.Error(t, errOnly(NewValue([]string{"a", "b"}).FloatValue()))
+ assert.Equal(t, "", must(NewValue([]string(nil)).StringValue()))
+ assert.Equal(t, "", must(NewValue([]string{}).StringValue()))
+ assert.Equal(t, `a,b`, must(NewValue([]string{"a", "b"}).StringValue()))
+ assert.Equal(t, map[string]interface{}{}, must(NewValue([]string(nil)).MapValue()))
+ assert.Equal(t, map[string]interface{}{}, must(NewValue([]string{}).MapValue()))
+ assert.Equal(t, map[string]interface{}{"0": "a", "1": "b"}, must(NewValue([]string{"a", "b"}).MapValue()))
+ assert.Equal(t, []interface{}{}, must(NewValue([]string(nil)).SliceValue()))
+ assert.Equal(t, []interface{}{}, must(NewValue([]string{}).SliceValue()))
+ assert.Equal(t, slice("a"), must(NewValue([]string{"a"}).SliceValue()))
+}
+
+func TestStaticNone(t *testing.T) {
+ // Types
+ assert.Equal(t, "null", None.String())
+ assert.Equal(t, false, None.IsBool())
+ assert.Equal(t, true, None.IsNone())
+ assert.Equal(t, false, None.IsInt())
+ assert.Equal(t, false, None.IsNumber())
+ assert.Equal(t, false, None.IsString())
+ assert.Equal(t, false, None.IsMap())
+ assert.Equal(t, false, None.IsSlice())
+
+ // Conversion
+ assert.Equal(t, false, must(None.BoolValue()))
+ assert.Equal(t, int64(0), must(None.IntValue()))
+ assert.Equal(t, 0.0, must(None.FloatValue()))
+ assert.Equal(t, "", must(None.StringValue()))
+ assert.Equal(t, map[string]interface{}(nil), must(None.MapValue()))
+ assert.Equal(t, []interface{}(nil), must(None.SliceValue()))
+}
diff --git a/pkg/tcl/expressionstcl/stdlib.go b/pkg/tcl/expressionstcl/stdlib.go
new file mode 100644
index 00000000000..fb6b6cb74df
--- /dev/null
+++ b/pkg/tcl/expressionstcl/stdlib.go
@@ -0,0 +1,269 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "github.com/kballard/go-shellquote"
+ "gopkg.in/yaml.v3"
+)
+
+type StdFunction struct {
+ ReturnType Type
+ Handler func(...StaticValue) (Expression, error)
+}
+
+type stdMachine struct{}
+
+var StdLibMachine = &stdMachine{}
+
+var stdFunctions = map[string]StdFunction{
+ "string": {
+ ReturnType: TypeString,
+ Handler: func(value ...StaticValue) (Expression, error) {
+ str := ""
+ for i := range value {
+ next, _ := value[i].StringValue()
+ str += next
+ }
+ return NewValue(str), nil
+ },
+ },
+ "list": {
+ Handler: func(value ...StaticValue) (Expression, error) {
+ v := make([]interface{}, len(value))
+ for i := range value {
+ v[i] = value[i].Value()
+ }
+ return NewValue(v), nil
+ },
+ },
+ "join": {
+ ReturnType: TypeString,
+ Handler: func(value ...StaticValue) (Expression, error) {
+ if len(value) == 0 || len(value) > 2 {
+ return nil, fmt.Errorf(`"join" function expects 1-2 arguments, %d provided`, len(value))
+ }
+ if value[0].IsNone() {
+ return value[0], nil
+ }
+ if !value[0].IsSlice() {
+ return nil, fmt.Errorf(`"join" function expects a slice as 1st argument: %v provided`, value[0].Value())
+ }
+ slice, err := value[0].SliceValue()
+ if err != nil {
+ return nil, fmt.Errorf(`"join" function error: reading slice: %s`, err.Error())
+ }
+ v := make([]string, len(slice))
+ for i := range slice {
+ v[i], _ = toString(slice[i])
+ }
+ separator := ","
+ if len(value) == 2 {
+ separator, _ = value[1].StringValue()
+ }
+ return NewValue(strings.Join(v, separator)), nil
+ },
+ },
+ "split": {
+ Handler: func(value ...StaticValue) (Expression, error) {
+ if len(value) == 0 || len(value) > 2 {
+ return nil, fmt.Errorf(`"split" function expects 1-2 arguments, %d provided`, len(value))
+ }
+ str, _ := value[0].StringValue()
+ separator := ","
+ if len(value) == 2 {
+ separator, _ = value[1].StringValue()
+ }
+ return NewValue(strings.Split(str, separator)), nil
+ },
+ },
+ "int": {
+ ReturnType: TypeInt64,
+ Handler: func(value ...StaticValue) (Expression, error) {
+ if len(value) != 1 {
+ return nil, fmt.Errorf(`"int" function expects 1 argument, %d provided`, len(value))
+ }
+ v, err := value[0].IntValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(v), nil
+ },
+ },
+ "bool": {
+ ReturnType: TypeBool,
+ Handler: func(value ...StaticValue) (Expression, error) {
+ if len(value) != 1 {
+ return nil, fmt.Errorf(`"bool" function expects 1 argument, %d provided`, len(value))
+ }
+ v, err := value[0].BoolValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(v), nil
+ },
+ },
+ "float": {
+ ReturnType: TypeFloat64,
+ Handler: func(value ...StaticValue) (Expression, error) {
+ if len(value) != 1 {
+ return nil, fmt.Errorf(`"float" function expects 1 argument, %d provided`, len(value))
+ }
+ v, err := value[0].FloatValue()
+ if err != nil {
+ return nil, err
+ }
+ return NewValue(v), nil
+ },
+ },
+ "tojson": {
+ ReturnType: TypeString,
+ Handler: func(value ...StaticValue) (Expression, error) {
+ if len(value) != 1 {
+ return nil, fmt.Errorf(`"tojson" function expects 1 argument, %d provided`, len(value))
+ }
+ b, err := json.Marshal(value[0].Value())
+ if err != nil {
+ return nil, fmt.Errorf(`"tojson" function had problem marshalling: %s`, err.Error())
+ }
+ return NewValue(string(b)), nil
+ },
+ },
+ "json": {
+ Handler: func(value ...StaticValue) (Expression, error) {
+ if len(value) != 1 {
+ return nil, fmt.Errorf(`"json" function expects 1 argument, %d provided`, len(value))
+ }
+ if !value[0].IsString() {
+ return nil, fmt.Errorf(`"json" function argument should be a string`)
+ }
+ var v interface{}
+ err := json.Unmarshal([]byte(value[0].Value().(string)), &v)
+ if err != nil {
+ return nil, fmt.Errorf(`"json" function had problem unmarshalling: %s`, err.Error())
+ }
+ return NewValue(v), nil
+ },
+ },
+ "toyaml": {
+ ReturnType: TypeString,
+ Handler: func(value ...StaticValue) (Expression, error) {
+ if len(value) != 1 {
+ return nil, fmt.Errorf(`"toyaml" function expects 1 argument, %d provided`, len(value))
+ }
+ b, err := yaml.Marshal(value[0].Value())
+ if err != nil {
+ return nil, fmt.Errorf(`"toyaml" function had problem marshalling: %s`, err.Error())
+ }
+ return NewValue(string(b)), nil
+ },
+ },
+ "yaml": {
+ Handler: func(value ...StaticValue) (Expression, error) {
+ if len(value) != 1 {
+ return nil, fmt.Errorf(`"yaml" function expects 1 argument, %d provided`, len(value))
+ }
+ if !value[0].IsString() {
+ return nil, fmt.Errorf(`"yaml" function argument should be a string`)
+ }
+ var v interface{}
+ err := yaml.Unmarshal([]byte(value[0].Value().(string)), &v)
+ if err != nil {
+ return nil, fmt.Errorf(`"yaml" function had problem unmarshalling: %s`, err.Error())
+ }
+ return NewValue(v), nil
+ },
+ },
+ "shellquote": {
+ ReturnType: TypeString,
+ Handler: func(value ...StaticValue) (Expression, error) {
+ args := make([]string, len(value))
+ for i := range value {
+ args[i], _ = value[i].StringValue()
+ }
+ return NewValue(shellquote.Join(args...)), nil
+ },
+ },
+ "trim": {
+ ReturnType: TypeString,
+ Handler: func(value ...StaticValue) (Expression, error) {
+ if len(value) != 1 {
+ return nil, fmt.Errorf(`"trim" function expects 1 argument, %d provided`, len(value))
+ }
+ if !value[0].IsString() {
+ return nil, fmt.Errorf(`"trim" function argument should be a string`)
+ }
+ str, _ := value[0].StringValue()
+ return NewValue(strings.TrimSpace(str)), nil
+ },
+ },
+}
+
+const (
+ stringCastStdFn = "string"
+ boolCastStdFn = "bool"
+ intCastStdFn = "int"
+ floatCastStdFn = "float"
+)
+
+func CastToString(v Expression) Expression {
+ if v.Static() != nil {
+ return NewStringValue(v.Static().Value())
+ } else if v.Type() == TypeString {
+ return v
+ }
+ return newCall(stringCastStdFn, []Expression{v})
+}
+
+func CastToBool(v Expression) Expression {
+ if v.Type() == TypeBool {
+ return v
+ }
+ return newCall(boolCastStdFn, []Expression{v})
+}
+
+func CastToInt(v Expression) Expression {
+ if v.Type() == TypeInt64 {
+ return v
+ }
+ return newCall(intCastStdFn, []Expression{v})
+}
+
+func CastToFloat(v Expression) Expression {
+ if v.Type() == TypeFloat64 {
+ return v
+ }
+ return newCall(intCastStdFn, []Expression{v})
+}
+
+func IsStdFunction(name string) bool {
+ _, ok := stdFunctions[name]
+ return ok
+}
+
+func GetStdFunctionReturnType(name string) Type {
+ return stdFunctions[name].ReturnType
+}
+
+func (*stdMachine) Get(name string) (Expression, bool, error) {
+ return nil, false, nil
+}
+
+func (*stdMachine) Call(name string, args ...StaticValue) (Expression, bool, error) {
+ fn, ok := stdFunctions[name]
+ if ok {
+ exp, err := fn.Handler(args...)
+ return exp, true, err
+ }
+ return nil, false, nil
+}
diff --git a/pkg/tcl/expressionstcl/tokenize.go b/pkg/tcl/expressionstcl/tokenize.go
new file mode 100644
index 00000000000..ae2e2b2c4f1
--- /dev/null
+++ b/pkg/tcl/expressionstcl/tokenize.go
@@ -0,0 +1,107 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "regexp"
+)
+
+var mathOperatorRe = regexp.MustCompile(`^(?:!=|<>|==|>=|<=|&&|\*\*|\|\||[+\-*/><=%])`)
+var noneRe = regexp.MustCompile(`^null(?:[^a-zA-Z\d_.]|$)`)
+var jsonValueRe = regexp.MustCompile(`^(?:["{\[\d]|((?:true|false)(?:[^a-zA-Z\d_.]|$)))`)
+var accessorRe = regexp.MustCompile(`^[a-zA-Z\d_](?:[a-zA-Z\d_.]*[a-zA-Z\d_])?`)
+var spaceRe = regexp.MustCompile(`^\s+`)
+
+func tokenizeNext(exp string, i int) (token, int, error) {
+ for i < len(exp) {
+ switch true {
+ case exp[i] == ',':
+ return tokenComma, i + 1, nil
+ case exp[i] == '(':
+ return tokenOpen, i + 1, nil
+ case exp[i] == ')':
+ return tokenClose, i + 1, nil
+ case exp[i] == ':':
+ return tokenTernarySeparator, i + 1, nil
+ case mathOperatorRe.MatchString(exp[i:]):
+ op := mathOperatorRe.FindString(exp[i:])
+ return tokenMath(op), i + len(op), nil
+ case exp[i] == '?':
+ return tokenTernary, i + 1, nil
+ case exp[i] == '!':
+ return tokenNot, i + 1, nil
+ case spaceRe.MatchString(exp[i:]):
+ space := spaceRe.FindString(exp[i:])
+ i += len(space)
+ case noneRe.MatchString(exp[i:]):
+ return tokenJson(noneValue), i + 4, nil
+ case jsonValueRe.MatchString(exp[i:]):
+ // Allow multi-line string with literal \n
+ // TODO: Optimize, and allow deeper in the tree
+ appended := 0
+ if exp[i] == '"' {
+ inside := true
+ for index := i + 1; inside && index < len(exp); index++ {
+ if exp[index] == '\\' {
+ index++
+ } else if exp[index] == '"' {
+ inside = false
+ } else if exp[index] == '\n' {
+ exp = exp[0:index] + "\\n" + exp[index+1:]
+ appended++
+ } else if exp[index] == '\t' {
+ exp = exp[0:index] + "\\t" + exp[index+1:]
+ appended++
+ }
+ }
+ }
+ decoder := json.NewDecoder(bytes.NewBuffer([]byte(exp[i:])))
+ var val interface{}
+ err := decoder.Decode(&val)
+ if err != nil {
+ return token{}, i, fmt.Errorf("error while decoding JSON from index %d in expression: %s: %s", i, exp, err.Error())
+ }
+ return tokenJson(val), i + int(decoder.InputOffset()) - appended, nil
+ case accessorRe.MatchString(exp[i:]):
+ acc := accessorRe.FindString(exp[i:])
+ return tokenAccessor(acc), i + len(acc), nil
+ default:
+ return token{}, i, fmt.Errorf("unknown character at index %d in expression: %s", i, exp)
+ }
+ }
+ return token{}, 0, io.EOF
+}
+
+func tokenize(exp string, index int) (tokens []token, i int, err error) {
+ tokens = make([]token, 0)
+ var t token
+ for i = index; i < len(exp); {
+ t, i, err = tokenizeNext(exp, i)
+ if err != nil {
+ if err == io.EOF {
+ return tokens, i, nil
+ }
+ return tokens, i, err
+ }
+ tokens = append(tokens, t)
+ }
+ return
+}
+
+func mustTokenize(exp string) []token {
+ tokens, _, err := tokenize(exp, 0)
+ if err != nil {
+ panic(err)
+ }
+ return tokens
+}
diff --git a/pkg/tcl/expressionstcl/tokenize_test.go b/pkg/tcl/expressionstcl/tokenize_test.go
new file mode 100644
index 00000000000..638c8c64784
--- /dev/null
+++ b/pkg/tcl/expressionstcl/tokenize_test.go
@@ -0,0 +1,74 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func slice(v ...interface{}) []interface{} {
+ return v
+}
+
+func TestTokenizeSimple(t *testing.T) {
+ operators := []string{"&&", "||", "!=", "<>", "==", "=", "+", "-", "*", ">", "<", "<=", ">=", "%", "**"}
+ for _, op := range operators {
+ assert.Equal(t, []token{tokenAccessor("a"), tokenMath(op), tokenAccessor("b")}, mustTokenize("a"+op+"b"))
+ }
+ assert.Equal(t, []token{tokenNot, tokenAccessor("abc")}, mustTokenize(`!abc`))
+ assert.Equal(t, []token{tokenAccessor("a"), tokenTernary, tokenAccessor("b"), tokenTernarySeparator, tokenAccessor("c")}, mustTokenize(`a ? b : c`))
+ assert.Equal(t, []token{tokenOpen, tokenAccessor("a"), tokenClose}, mustTokenize(`(a)`))
+ assert.Equal(t, []token{tokenAccessor("a"), tokenOpen, tokenAccessor("b"), tokenComma, tokenJson(true), tokenClose}, mustTokenize(`a(b, true)`))
+ assert.Equal(t, []token{tokenJson(noneValue)}, mustTokenize("null"))
+ assert.Equal(t, []token{tokenJson(noneValue), tokenMath("+"), tokenJson(4.0)}, mustTokenize("null + 4"))
+}
+
+func TestTokenizeJson(t *testing.T) {
+ assert.Equal(t, []token{tokenJson(1.0), tokenMath("+"), tokenJson(255.0)}, mustTokenize(`1 + 255`))
+ assert.Equal(t, []token{tokenJson(1.6), tokenMath("+"), tokenJson(255.0)}, mustTokenize(`1.6 + 255`))
+ assert.Equal(t, []token{tokenJson("abc"), tokenMath("+"), tokenJson("d")}, mustTokenize(`"abc" + "d"`))
+ assert.Equal(t, []token{tokenJson(map[string]interface{}{"key1": "value1", "key2": "value2"})}, mustTokenize(`{"key1": "value1", "key2": "value2"}`))
+ assert.Equal(t, []token{tokenJson(slice("a", "b"))}, mustTokenize(`["a", "b"]`))
+ assert.Equal(t, []token{tokenJson(true)}, mustTokenize(`true`))
+ assert.Equal(t, []token{tokenJson(false)}, mustTokenize(`false`))
+}
+
+func TestTokenizeComplex(t *testing.T) {
+ want := []token{
+ tokenAccessor("env.value"), tokenMath("&&"), tokenOpen, tokenAccessor("env.alternative"), tokenMath("+"), tokenJson("cd"), tokenClose, tokenMath("=="), tokenJson("abc"),
+ tokenTernary, tokenJson(10.5),
+ tokenTernarySeparator, tokenNot, tokenAccessor("ignored"), tokenTernary, tokenOpen, tokenJson(14.0), tokenMath("+"), tokenJson(3.1), tokenMath("*"), tokenJson(5.0), tokenClose,
+ tokenTernarySeparator, tokenAccessor("transform"), tokenOpen, tokenJson("a"), tokenComma,
+ tokenJson(map[string]interface{}{"x": "y"}), tokenComma, tokenJson(slice("z")), tokenClose,
+ }
+ assert.Equal(t, want, mustTokenize(`
+ env.value && (env.alternative + "cd") == "abc"
+ ? 10.5
+ : !ignored ? (14 + 3.1 * 5)
+ : transform("a",
+ {"x": "y"}, ["z"])
+ `))
+}
+
+func TestTokenizeInvalidAccessor(t *testing.T) {
+ tokens, _, err := tokenize(`abc.`, 0)
+ assert.Error(t, err)
+ assert.Equal(t, []token{tokenAccessor("abc")}, tokens)
+}
+
+func TestTokenizeInvalidJson(t *testing.T) {
+ tokens, _, err := tokenize(`{"abc": "d"`, 0)
+ tokens2, _, err2 := tokenize(`{"abc": d}`, 0)
+ assert.Error(t, err)
+ assert.Equal(t, []token{}, tokens)
+ assert.Error(t, err2)
+ assert.Equal(t, []token{}, tokens2)
+}
diff --git a/pkg/tcl/expressionstcl/tokens.go b/pkg/tcl/expressionstcl/tokens.go
new file mode 100644
index 00000000000..82b670574b9
--- /dev/null
+++ b/pkg/tcl/expressionstcl/tokens.go
@@ -0,0 +1,56 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+type tokenType uint8
+
+const (
+ // Primitives
+ tokenTypeAccessor tokenType = iota
+ tokenTypeJson
+
+ // Math
+ tokenTypeNot
+ tokenTypeMath
+ tokenTypeOpen
+ tokenTypeClose
+
+ // Logical
+ tokenTypeTernary
+ tokenTypeTernarySeparator
+
+ // Functions
+ tokenTypeComma
+)
+
+type token struct {
+ Type tokenType
+ Value interface{}
+}
+
+var (
+ tokenNot = token{Type: tokenTypeNot}
+ tokenOpen = token{Type: tokenTypeOpen}
+ tokenClose = token{Type: tokenTypeClose}
+ tokenTernary = token{Type: tokenTypeTernary}
+ tokenTernarySeparator = token{Type: tokenTypeTernarySeparator}
+ tokenComma = token{Type: tokenTypeComma}
+)
+
+func tokenMath(op string) token {
+ return token{Type: tokenTypeMath, Value: op}
+}
+
+func tokenJson(value interface{}) token {
+ return token{Type: tokenTypeJson, Value: value}
+}
+
+func tokenAccessor(value interface{}) token {
+ return token{Type: tokenTypeAccessor, Value: value}
+}
diff --git a/pkg/tcl/expressionstcl/typechecking.go b/pkg/tcl/expressionstcl/typechecking.go
new file mode 100644
index 00000000000..9e05b329454
--- /dev/null
+++ b/pkg/tcl/expressionstcl/typechecking.go
@@ -0,0 +1,58 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import "reflect"
+
+type noneType struct{}
+
+var noneValue noneType
+
+func isInt(s interface{}) bool {
+ switch s.(type) {
+ case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
+ return true
+ case float32:
+ return s.(float32) == float32(int32(s.(float32)))
+ case float64:
+ return s.(float64) == float64(int64(s.(float64)))
+ }
+ return false
+}
+
+func isString(s interface{}) bool {
+ _, ok := s.(string)
+ return ok
+}
+
+func isBool(s interface{}) bool {
+ _, ok := s.(bool)
+ return ok
+}
+
+func isNone(s interface{}) bool {
+ _, ok := s.(noneType)
+ return ok
+}
+
+func isNumber(s interface{}) bool {
+ switch s.(type) {
+ case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
+ return true
+ }
+ return false
+}
+
+func isMap(s interface{}) bool {
+ return reflect.ValueOf(s).Kind() == reflect.Map
+}
+
+func isSlice(s interface{}) bool {
+ return reflect.ValueOf(s).Kind() == reflect.Slice
+}
diff --git a/pkg/tcl/expressionstcl/utils.go b/pkg/tcl/expressionstcl/utils.go
new file mode 100644
index 00000000000..2b02878f565
--- /dev/null
+++ b/pkg/tcl/expressionstcl/utils.go
@@ -0,0 +1,64 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package expressionstcl
+
+import (
+ "fmt"
+
+ "github.com/pkg/errors"
+)
+
+const maxCallStack = 10_000
+
+func deepResolve(expr Expression, machines ...Machine) (Expression, error) {
+ i := 1
+ expr, changed, err := expr.SafeResolve(machines...)
+ for changed && err == nil && expr.Static() == nil {
+ if i > maxCallStack {
+ return expr, fmt.Errorf("maximum call stack exceeded while resolving expression: %s", expr.String())
+ }
+ expr, changed, err = expr.SafeResolve(machines...)
+ i++
+ }
+ return expr, err
+}
+
+func EvalTemplate(tpl string, machines ...Machine) (string, error) {
+ expr, err := CompileTemplate(tpl)
+ if err != nil {
+ return "", errors.Wrap(err, "compiling")
+ }
+ expr, err = expr.Resolve(machines...)
+ if err != nil {
+ return "", errors.Wrap(err, "resolving")
+ }
+ if expr.Static() == nil {
+ return "", fmt.Errorf("template should be static: %s", expr.Template())
+ }
+ return expr.Static().StringValue()
+}
+
+func EvalExpression(str string, machines ...Machine) (StaticValue, error) {
+ expr, err := Compile(str)
+ if err != nil {
+ return nil, errors.Wrap(err, "compiling")
+ }
+ expr, err = expr.Resolve(machines...)
+ if err != nil {
+ return nil, errors.Wrap(err, "resolving")
+ }
+ if expr.Static() == nil {
+ return nil, fmt.Errorf("expression should be static: %s", expr.String())
+ }
+ return expr.Static(), nil
+}
+
+func Escape(str string) string {
+ return NewStringValue(str).Template()
+}
diff --git a/pkg/tcl/mapperstcl/testworkflows/kube_openapi.go b/pkg/tcl/mapperstcl/testworkflows/kube_openapi.go
new file mode 100644
index 00000000000..04b66b7e302
--- /dev/null
+++ b/pkg/tcl/mapperstcl/testworkflows/kube_openapi.go
@@ -0,0 +1,626 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflows
+
+import (
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/resource"
+ "k8s.io/apimachinery/pkg/util/intstr"
+
+ testsv3 "github.com/kubeshop/testkube-operator/api/tests/v3"
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+)
+
+func MapIntOrStringToString(i intstr.IntOrString) string {
+ return i.String()
+}
+
+func MapIntOrStringPtrToStringPtr(i *intstr.IntOrString) *string {
+ if i == nil {
+ return nil
+ }
+ return common.Ptr(MapIntOrStringToString(*i))
+}
+
+func MapStringToBoxedString(v *string) *testkube.BoxedString {
+ if v == nil {
+ return nil
+ }
+ return &testkube.BoxedString{Value: *v}
+}
+
+func MapStringTypeToBoxedString[T ~string](v *T) *testkube.BoxedString {
+ if v == nil {
+ return nil
+ }
+ return &testkube.BoxedString{Value: string(*v)}
+}
+
+func MapBoolToBoxedBoolean(v *bool) *testkube.BoxedBoolean {
+ if v == nil {
+ return nil
+ }
+ return &testkube.BoxedBoolean{Value: *v}
+}
+
+func MapStringSliceToBoxedStringList(v *[]string) *testkube.BoxedStringList {
+ if v == nil {
+ return nil
+ }
+ return &testkube.BoxedStringList{Value: *v}
+}
+
+func MapInt64ToBoxedInteger(v *int64) *testkube.BoxedInteger {
+ if v == nil {
+ return MapInt32ToBoxedInteger(nil)
+ }
+ return MapInt32ToBoxedInteger(common.Ptr(int32(*v)))
+}
+
+func MapInt32ToBoxedInteger(v *int32) *testkube.BoxedInteger {
+ if v == nil {
+ return nil
+ }
+ return &testkube.BoxedInteger{Value: *v}
+}
+
+func MapQuantityToBoxedString(v *resource.Quantity) *testkube.BoxedString {
+ if v == nil {
+ return nil
+ }
+ return &testkube.BoxedString{Value: v.String()}
+}
+
+func MapHostPathVolumeSourceKubeToAPI(v corev1.HostPathVolumeSource) testkube.HostPathVolumeSource {
+ return testkube.HostPathVolumeSource{
+ Path: v.Path,
+ Type_: MapStringTypeToBoxedString[corev1.HostPathType](v.Type),
+ }
+}
+
+func MapEmptyDirVolumeSourceKubeToAPI(v corev1.EmptyDirVolumeSource) testkube.EmptyDirVolumeSource {
+ return testkube.EmptyDirVolumeSource{
+ Medium: string(v.Medium),
+ SizeLimit: MapQuantityToBoxedString(v.SizeLimit),
+ }
+}
+
+func MapGCEPersistentDiskVolumeSourceKubeToAPI(v corev1.GCEPersistentDiskVolumeSource) testkube.GcePersistentDiskVolumeSource {
+ return testkube.GcePersistentDiskVolumeSource{
+ PdName: v.PDName,
+ FsType: v.FSType,
+ Partition: v.Partition,
+ ReadOnly: v.ReadOnly,
+ }
+}
+
+func MapAWSElasticBlockStoreVolumeSourceKubeToAPI(v corev1.AWSElasticBlockStoreVolumeSource) testkube.AwsElasticBlockStoreVolumeSource {
+ return testkube.AwsElasticBlockStoreVolumeSource{
+ VolumeID: v.VolumeID,
+ FsType: v.FSType,
+ Partition: v.Partition,
+ ReadOnly: v.ReadOnly,
+ }
+}
+
+func MapKeyToPathKubeToAPI(v corev1.KeyToPath) testkube.SecretVolumeSourceItems {
+ return testkube.SecretVolumeSourceItems{
+ Key: v.Key,
+ Path: v.Path,
+ Mode: MapInt32ToBoxedInteger(v.Mode),
+ }
+}
+
+func MapSecretVolumeSourceKubeToAPI(v corev1.SecretVolumeSource) testkube.SecretVolumeSource {
+ return testkube.SecretVolumeSource{
+ SecretName: v.SecretName,
+ Items: common.MapSlice(v.Items, MapKeyToPathKubeToAPI),
+ DefaultMode: MapInt32ToBoxedInteger(v.DefaultMode),
+ Optional: common.ResolvePtr(v.Optional, false),
+ }
+}
+
+func MapNFSVolumeSourceKubeToAPI(v corev1.NFSVolumeSource) testkube.NfsVolumeSource {
+ return testkube.NfsVolumeSource{
+ Server: v.Server,
+ Path: v.Path,
+ ReadOnly: v.ReadOnly,
+ }
+}
+
+func MapPersistentVolumeClaimVolumeSourceKubeToAPI(v corev1.PersistentVolumeClaimVolumeSource) testkube.PersistentVolumeClaimVolumeSource {
+ return testkube.PersistentVolumeClaimVolumeSource{
+ ClaimName: v.ClaimName,
+ ReadOnly: v.ReadOnly,
+ }
+}
+
+func MapCephFSVolumeSourceKubeToAPI(v corev1.CephFSVolumeSource) testkube.CephFsVolumeSource {
+ return testkube.CephFsVolumeSource{
+ Monitors: v.Monitors,
+ Path: v.Path,
+ User: v.User,
+ SecretFile: v.SecretFile,
+ SecretRef: common.MapPtr(v.SecretRef, MapLocalObjectReferenceKubeToAPI),
+ ReadOnly: v.ReadOnly,
+ }
+}
+
+func MapAzureFileVolumeSourceKubeToAPI(v corev1.AzureFileVolumeSource) testkube.AzureFileVolumeSource {
+ return testkube.AzureFileVolumeSource{
+ SecretName: v.SecretName,
+ ShareName: v.ShareName,
+ ReadOnly: v.ReadOnly,
+ }
+}
+
+func MapConfigMapVolumeSourceKubeToAPI(v corev1.ConfigMapVolumeSource) testkube.ConfigMapVolumeSource {
+ return testkube.ConfigMapVolumeSource{
+ Name: v.Name,
+ Items: common.MapSlice(v.Items, MapKeyToPathKubeToAPI),
+ DefaultMode: MapInt32ToBoxedInteger(v.DefaultMode),
+ Optional: common.ResolvePtr(v.Optional, false),
+ }
+}
+
+func MapAzureDiskVolumeSourceKubeToAPI(v corev1.AzureDiskVolumeSource) testkube.AzureDiskVolumeSource {
+ return testkube.AzureDiskVolumeSource{
+ DiskName: v.DiskName,
+ DiskURI: v.DataDiskURI,
+ CachingMode: MapStringTypeToBoxedString[corev1.AzureDataDiskCachingMode](v.CachingMode),
+ FsType: MapStringToBoxedString(v.FSType),
+ ReadOnly: common.ResolvePtr(v.ReadOnly, false),
+ Kind: MapStringTypeToBoxedString[corev1.AzureDataDiskKind](v.Kind),
+ }
+}
+
+func MapVolumeKubeToAPI(v corev1.Volume) testkube.Volume {
+ // TODO: Add rest of VolumeSource types in future,
+ // so they will be recognized by JSON API and persisted with Execution.
+ return testkube.Volume{
+ Name: v.Name,
+ HostPath: common.MapPtr(v.HostPath, MapHostPathVolumeSourceKubeToAPI),
+ EmptyDir: common.MapPtr(v.EmptyDir, MapEmptyDirVolumeSourceKubeToAPI),
+ GcePersistentDisk: common.MapPtr(v.GCEPersistentDisk, MapGCEPersistentDiskVolumeSourceKubeToAPI),
+ AwsElasticBlockStore: common.MapPtr(v.AWSElasticBlockStore, MapAWSElasticBlockStoreVolumeSourceKubeToAPI),
+ Secret: common.MapPtr(v.Secret, MapSecretVolumeSourceKubeToAPI),
+ Nfs: common.MapPtr(v.NFS, MapNFSVolumeSourceKubeToAPI),
+ PersistentVolumeClaim: common.MapPtr(v.PersistentVolumeClaim, MapPersistentVolumeClaimVolumeSourceKubeToAPI),
+ Cephfs: common.MapPtr(v.CephFS, MapCephFSVolumeSourceKubeToAPI),
+ AzureFile: common.MapPtr(v.AzureFile, MapAzureFileVolumeSourceKubeToAPI),
+ ConfigMap: common.MapPtr(v.ConfigMap, MapConfigMapVolumeSourceKubeToAPI),
+ AzureDisk: common.MapPtr(v.AzureDisk, MapAzureDiskVolumeSourceKubeToAPI),
+ }
+}
+
+func MapEnvVarKubeToAPI(v corev1.EnvVar) testkube.EnvVar {
+ return testkube.EnvVar{
+ Name: v.Name,
+ Value: v.Value,
+ ValueFrom: common.MapPtr(v.ValueFrom, MapEnvVarSourceKubeToAPI),
+ }
+}
+
+func MapConfigMapKeyRefKubeToAPI(v *corev1.ConfigMapKeySelector) *testkube.EnvVarSourceConfigMapKeyRef {
+ if v == nil {
+ return nil
+ }
+ return &testkube.EnvVarSourceConfigMapKeyRef{
+ Key: v.Key,
+ Name: v.Name,
+ Optional: common.ResolvePtr(v.Optional, false),
+ }
+}
+
+func MapFieldRefKubeToAPI(v *corev1.ObjectFieldSelector) *testkube.EnvVarSourceFieldRef {
+ if v == nil {
+ return nil
+ }
+ return &testkube.EnvVarSourceFieldRef{
+ ApiVersion: v.APIVersion,
+ FieldPath: v.FieldPath,
+ }
+}
+
+func MapResourceFieldRefKubeToAPI(v *corev1.ResourceFieldSelector) *testkube.EnvVarSourceResourceFieldRef {
+ if v == nil {
+ return nil
+ }
+ divisor := ""
+ if !v.Divisor.IsZero() {
+ divisor = v.Divisor.String()
+ }
+ return &testkube.EnvVarSourceResourceFieldRef{
+ ContainerName: v.ContainerName,
+ Divisor: divisor,
+ Resource: v.Resource,
+ }
+}
+
+func MapSecretKeyRefKubeToAPI(v *corev1.SecretKeySelector) *testkube.EnvVarSourceSecretKeyRef {
+ if v == nil {
+ return nil
+ }
+ return &testkube.EnvVarSourceSecretKeyRef{
+ Key: v.Key,
+ Name: v.Name,
+ Optional: common.ResolvePtr(v.Optional, false),
+ }
+}
+
+func MapEnvVarSourceKubeToAPI(v corev1.EnvVarSource) testkube.EnvVarSource {
+ return testkube.EnvVarSource{
+ ConfigMapKeyRef: MapConfigMapKeyRefKubeToAPI(v.ConfigMapKeyRef),
+ FieldRef: MapFieldRefKubeToAPI(v.FieldRef),
+ ResourceFieldRef: MapResourceFieldRefKubeToAPI(v.ResourceFieldRef),
+ SecretKeyRef: MapSecretKeyRefKubeToAPI(v.SecretKeyRef),
+ }
+}
+
+func MapConfigMapEnvSourceKubeToAPI(v *corev1.ConfigMapEnvSource) *testkube.ConfigMapEnvSource {
+ if v == nil {
+ return nil
+ }
+ return &testkube.ConfigMapEnvSource{
+ Name: v.Name,
+ Optional: common.ResolvePtr(v.Optional, false),
+ }
+}
+
+func MapSecretEnvSourceKubeToAPI(v *corev1.SecretEnvSource) *testkube.SecretEnvSource {
+ if v == nil {
+ return nil
+ }
+ return &testkube.SecretEnvSource{
+ Name: v.Name,
+ Optional: common.ResolvePtr(v.Optional, false),
+ }
+}
+
+func MapEnvFromSourceKubeToAPI(v corev1.EnvFromSource) testkube.EnvFromSource {
+ return testkube.EnvFromSource{
+ Prefix: v.Prefix,
+ ConfigMapRef: MapConfigMapEnvSourceKubeToAPI(v.ConfigMapRef),
+ SecretRef: MapSecretEnvSourceKubeToAPI(v.SecretRef),
+ }
+}
+
+func MapSecurityContextKubeToAPI(v *corev1.SecurityContext) *testkube.SecurityContext {
+ if v == nil {
+ return nil
+ }
+ return &testkube.SecurityContext{
+ Privileged: MapBoolToBoxedBoolean(v.Privileged),
+ RunAsUser: MapInt64ToBoxedInteger(v.RunAsUser),
+ RunAsGroup: MapInt64ToBoxedInteger(v.RunAsGroup),
+ RunAsNonRoot: MapBoolToBoxedBoolean(v.RunAsNonRoot),
+ ReadOnlyRootFilesystem: MapBoolToBoxedBoolean(v.ReadOnlyRootFilesystem),
+ AllowPrivilegeEscalation: MapBoolToBoxedBoolean(v.AllowPrivilegeEscalation),
+ }
+}
+
+func MapLocalObjectReferenceKubeToAPI(v corev1.LocalObjectReference) testkube.LocalObjectReference {
+ return testkube.LocalObjectReference{Name: v.Name}
+}
+
+func MapConfigValueKubeToAPI(v map[string]intstr.IntOrString) map[string]string {
+ return common.MapMap(v, MapIntOrStringToString)
+}
+
+func MapParameterTypeKubeToAPI(v testworkflowsv1.ParameterType) *testkube.TestWorkflowParameterType {
+ if v == "" {
+ return nil
+ }
+ return common.Ptr(testkube.TestWorkflowParameterType(v))
+}
+
+func MapGitAuthTypeKubeToAPI(v testsv3.GitAuthType) *testkube.ContentGitAuthType {
+ if v == "" {
+ return nil
+ }
+ return common.Ptr(testkube.ContentGitAuthType(v))
+}
+
+func MapImagePullPolicyKubeToAPI(v corev1.PullPolicy) *testkube.ImagePullPolicy {
+ if v == "" {
+ return nil
+ }
+ return common.Ptr(testkube.ImagePullPolicy(v))
+}
+
+func MapParameterSchemaKubeToAPI(v testworkflowsv1.ParameterSchema) testkube.TestWorkflowParameterSchema {
+ return testkube.TestWorkflowParameterSchema{
+ Description: v.Description,
+ Type_: MapParameterTypeKubeToAPI(v.Type),
+ Enum: v.Enum,
+ Example: common.ResolvePtr(common.MapPtr(v.Example, MapIntOrStringToString), ""),
+ Default_: MapStringToBoxedString(MapIntOrStringPtrToStringPtr(v.Default)),
+ Format: v.Format,
+ Pattern: v.Pattern,
+ MinLength: MapInt64ToBoxedInteger(v.MinLength),
+ MaxLength: MapInt64ToBoxedInteger(v.MaxLength),
+ Minimum: MapInt64ToBoxedInteger(v.Minimum),
+ Maximum: MapInt64ToBoxedInteger(v.Maximum),
+ ExclusiveMinimum: MapInt64ToBoxedInteger(v.ExclusiveMinimum),
+ ExclusiveMaximum: MapInt64ToBoxedInteger(v.ExclusiveMaximum),
+ MultipleOf: MapInt64ToBoxedInteger(v.MultipleOf),
+ }
+}
+
+func MapTemplateRefKubeToAPI(v testworkflowsv1.TemplateRef) testkube.TestWorkflowTemplateRef {
+ return testkube.TestWorkflowTemplateRef{
+ Name: v.Name,
+ Config: MapConfigValueKubeToAPI(v.Config),
+ }
+}
+
+func MapContentGitKubeToAPI(v testworkflowsv1.ContentGit) testkube.TestWorkflowContentGit {
+ return testkube.TestWorkflowContentGit{
+ Uri: v.Uri,
+ Revision: v.Revision,
+ Username: v.Username,
+ UsernameFrom: common.MapPtr(v.UsernameFrom, MapEnvVarSourceKubeToAPI),
+ Token: v.Token,
+ TokenFrom: common.MapPtr(v.TokenFrom, MapEnvVarSourceKubeToAPI),
+ AuthType: MapGitAuthTypeKubeToAPI(v.AuthType),
+ MountPath: v.MountPath,
+ Paths: v.Paths,
+ }
+}
+
+func MapContentKubeToAPI(v testworkflowsv1.Content) testkube.TestWorkflowContent {
+ return testkube.TestWorkflowContent{
+ Git: common.MapPtr(v.Git, MapContentGitKubeToAPI),
+ Files: common.MapSlice(v.Files, MapContentFileKubeToAPI),
+ }
+}
+
+func MapContentFileKubeToAPI(v testworkflowsv1.ContentFile) testkube.TestWorkflowContentFile {
+ return testkube.TestWorkflowContentFile{
+ Path: v.Path,
+ Content: v.Content,
+ ContentFrom: common.MapPtr(v.ContentFrom, MapEnvVarSourceKubeToAPI),
+ Mode: MapInt32ToBoxedInteger(v.Mode),
+ }
+}
+
+func MapResourcesListKubeToAPI(v map[corev1.ResourceName]intstr.IntOrString) *testkube.TestWorkflowResourcesList {
+ if len(v) == 0 {
+ return nil
+ }
+ empty := intstr.IntOrString{Type: intstr.String, StrVal: ""}
+ return &testkube.TestWorkflowResourcesList{
+ Cpu: MapIntOrStringToString(common.GetMapValue(v, corev1.ResourceCPU, empty)),
+ Memory: MapIntOrStringToString(common.GetMapValue(v, corev1.ResourceMemory, empty)),
+ Storage: MapIntOrStringToString(common.GetMapValue(v, corev1.ResourceStorage, empty)),
+ EphemeralStorage: MapIntOrStringToString(common.GetMapValue(v, corev1.ResourceEphemeralStorage, empty)),
+ }
+}
+
+func MapResourcesKubeToAPI(v testworkflowsv1.Resources) testkube.TestWorkflowResources {
+ requests := MapResourcesListKubeToAPI(v.Requests)
+ limits := MapResourcesListKubeToAPI(v.Limits)
+ return testkube.TestWorkflowResources{
+ Limits: limits,
+ Requests: requests,
+ }
+}
+
+func MapJobConfigKubeToAPI(v testworkflowsv1.JobConfig) testkube.TestWorkflowJobConfig {
+ return testkube.TestWorkflowJobConfig{
+ Labels: v.Labels,
+ Annotations: v.Annotations,
+ }
+}
+
+func MapPodConfigKubeToAPI(v testworkflowsv1.PodConfig) testkube.TestWorkflowPodConfig {
+ return testkube.TestWorkflowPodConfig{
+ ServiceAccountName: v.ServiceAccountName,
+ ImagePullSecrets: common.MapSlice(v.ImagePullSecrets, MapLocalObjectReferenceKubeToAPI),
+ NodeSelector: v.NodeSelector,
+ Labels: v.Labels,
+ Annotations: v.Annotations,
+ Volumes: common.MapSlice(v.Volumes, MapVolumeKubeToAPI),
+ }
+}
+
+func MapVolumeMountKubeToAPI(v corev1.VolumeMount) testkube.VolumeMount {
+ return testkube.VolumeMount{
+ Name: v.Name,
+ ReadOnly: v.ReadOnly,
+ MountPath: v.MountPath,
+ SubPath: v.SubPath,
+ MountPropagation: MapStringTypeToBoxedString[corev1.MountPropagationMode](v.MountPropagation),
+ SubPathExpr: v.SubPathExpr,
+ }
+}
+
+func MapContainerConfigKubeToAPI(v testworkflowsv1.ContainerConfig) testkube.TestWorkflowContainerConfig {
+ return testkube.TestWorkflowContainerConfig{
+ WorkingDir: MapStringToBoxedString(v.WorkingDir),
+ Image: v.Image,
+ ImagePullPolicy: MapImagePullPolicyKubeToAPI(v.ImagePullPolicy),
+ Env: common.MapSlice(v.Env, MapEnvVarKubeToAPI),
+ EnvFrom: common.MapSlice(v.EnvFrom, MapEnvFromSourceKubeToAPI),
+ Command: MapStringSliceToBoxedStringList(v.Command),
+ Args: MapStringSliceToBoxedStringList(v.Args),
+ Resources: common.MapPtr(v.Resources, MapResourcesKubeToAPI),
+ SecurityContext: MapSecurityContextKubeToAPI(v.SecurityContext),
+ VolumeMounts: common.MapSlice(v.VolumeMounts, MapVolumeMountKubeToAPI),
+ }
+}
+
+func MapStepRunKubeToAPI(v testworkflowsv1.StepRun) testkube.TestWorkflowContainerConfig {
+ return MapContainerConfigKubeToAPI(v.ContainerConfig)
+}
+
+func MapStepExecuteTestKubeToAPI(v testworkflowsv1.StepExecuteTest) testkube.TestWorkflowStepExecuteTestRef {
+ return testkube.TestWorkflowStepExecuteTestRef{
+ Name: v.Name,
+ }
+}
+
+func MapTestWorkflowRefKubeToAPI(v testworkflowsv1.StepExecuteWorkflow) testkube.TestWorkflowRef {
+ return testkube.TestWorkflowRef{
+ Name: v.Name,
+ Config: MapConfigValueKubeToAPI(v.Config),
+ }
+}
+
+func MapStepExecuteKubeToAPI(v testworkflowsv1.StepExecute) testkube.TestWorkflowStepExecute {
+ return testkube.TestWorkflowStepExecute{
+ Parallelism: v.Parallelism,
+ Async: v.Async,
+ Tests: common.MapSlice(v.Tests, MapStepExecuteTestKubeToAPI),
+ Workflows: common.MapSlice(v.Workflows, MapTestWorkflowRefKubeToAPI),
+ }
+}
+
+func MapStepArtifactsCompressionKubeToAPI(v testworkflowsv1.ArtifactCompression) testkube.TestWorkflowStepArtifactsCompression {
+ return testkube.TestWorkflowStepArtifactsCompression{
+ Name: v.Name,
+ }
+}
+
+func MapStepArtifactsKubeToAPI(v testworkflowsv1.StepArtifacts) testkube.TestWorkflowStepArtifacts {
+ return testkube.TestWorkflowStepArtifacts{
+ WorkingDir: MapStringToBoxedString(v.WorkingDir),
+ Compress: common.MapPtr(v.Compress, MapStepArtifactsCompressionKubeToAPI),
+ Paths: v.Paths,
+ }
+}
+
+func MapRetryPolicyKubeToAPI(v testworkflowsv1.RetryPolicy) testkube.TestWorkflowRetryPolicy {
+ return testkube.TestWorkflowRetryPolicy{
+ Count: v.Count,
+ Until: v.Until,
+ }
+}
+
+func MapStepKubeToAPI(v testworkflowsv1.Step) testkube.TestWorkflowStep {
+ return testkube.TestWorkflowStep{
+ Name: v.Name,
+ Condition: v.Condition,
+ Negative: v.Negative,
+ Optional: v.Optional,
+ Use: common.MapSlice(v.Use, MapTemplateRefKubeToAPI),
+ Template: common.MapPtr(v.Template, MapTemplateRefKubeToAPI),
+ Retry: common.MapPtr(v.Retry, MapRetryPolicyKubeToAPI),
+ Timeout: v.Timeout,
+ Delay: v.Delay,
+ Content: common.MapPtr(v.Content, MapContentKubeToAPI),
+ Shell: v.Shell,
+ Run: common.MapPtr(v.Run, MapStepRunKubeToAPI),
+ WorkingDir: MapStringToBoxedString(v.WorkingDir),
+ Container: common.MapPtr(v.Container, MapContainerConfigKubeToAPI),
+ Execute: common.MapPtr(v.Execute, MapStepExecuteKubeToAPI),
+ Artifacts: common.MapPtr(v.Artifacts, MapStepArtifactsKubeToAPI),
+ Setup: common.MapSlice(v.Setup, MapStepKubeToAPI),
+ Steps: common.MapSlice(v.Steps, MapStepKubeToAPI),
+ }
+}
+
+func MapIndependentStepKubeToAPI(v testworkflowsv1.IndependentStep) testkube.TestWorkflowIndependentStep {
+ return testkube.TestWorkflowIndependentStep{
+ Name: v.Name,
+ Condition: v.Condition,
+ Negative: v.Negative,
+ Optional: v.Optional,
+ Retry: common.MapPtr(v.Retry, MapRetryPolicyKubeToAPI),
+ Timeout: v.Timeout,
+ Delay: v.Delay,
+ Content: common.MapPtr(v.Content, MapContentKubeToAPI),
+ Shell: v.Shell,
+ Run: common.MapPtr(v.Run, MapStepRunKubeToAPI),
+ WorkingDir: MapStringToBoxedString(v.WorkingDir),
+ Container: common.MapPtr(v.Container, MapContainerConfigKubeToAPI),
+ Execute: common.MapPtr(v.Execute, MapStepExecuteKubeToAPI),
+ Artifacts: common.MapPtr(v.Artifacts, MapStepArtifactsKubeToAPI),
+ Setup: common.MapSlice(v.Setup, MapIndependentStepKubeToAPI),
+ Steps: common.MapSlice(v.Steps, MapIndependentStepKubeToAPI),
+ }
+}
+
+func MapSpecKubeToAPI(v testworkflowsv1.TestWorkflowSpec) testkube.TestWorkflowSpec {
+ return testkube.TestWorkflowSpec{
+ Use: common.MapSlice(v.Use, MapTemplateRefKubeToAPI),
+ Config: common.MapMap(v.Config, MapParameterSchemaKubeToAPI),
+ Content: common.MapPtr(v.Content, MapContentKubeToAPI),
+ Container: common.MapPtr(v.Container, MapContainerConfigKubeToAPI),
+ Job: common.MapPtr(v.Job, MapJobConfigKubeToAPI),
+ Pod: common.MapPtr(v.Pod, MapPodConfigKubeToAPI),
+ Setup: common.MapSlice(v.Setup, MapStepKubeToAPI),
+ Steps: common.MapSlice(v.Steps, MapStepKubeToAPI),
+ After: common.MapSlice(v.After, MapStepKubeToAPI),
+ }
+}
+
+func MapTemplateSpecKubeToAPI(v testworkflowsv1.TestWorkflowTemplateSpec) testkube.TestWorkflowTemplateSpec {
+ return testkube.TestWorkflowTemplateSpec{
+ Config: common.MapMap(v.Config, MapParameterSchemaKubeToAPI),
+ Content: common.MapPtr(v.Content, MapContentKubeToAPI),
+ Container: common.MapPtr(v.Container, MapContainerConfigKubeToAPI),
+ Job: common.MapPtr(v.Job, MapJobConfigKubeToAPI),
+ Pod: common.MapPtr(v.Pod, MapPodConfigKubeToAPI),
+ Setup: common.MapSlice(v.Setup, MapIndependentStepKubeToAPI),
+ Steps: common.MapSlice(v.Steps, MapIndependentStepKubeToAPI),
+ After: common.MapSlice(v.After, MapIndependentStepKubeToAPI),
+ }
+}
+
+func MapTestWorkflowKubeToAPI(w testworkflowsv1.TestWorkflow) testkube.TestWorkflow {
+ return testkube.TestWorkflow{
+ Name: w.Name,
+ Namespace: w.Namespace,
+ Labels: w.Labels,
+ Annotations: w.Annotations,
+ Created: w.CreationTimestamp.Time,
+ Description: w.Description,
+ Spec: common.Ptr(MapSpecKubeToAPI(w.Spec)),
+ }
+}
+
+func MapTestWorkflowTemplateKubeToAPI(w testworkflowsv1.TestWorkflowTemplate) testkube.TestWorkflowTemplate {
+ return testkube.TestWorkflowTemplate{
+ Name: w.Name,
+ Namespace: w.Namespace,
+ Labels: w.Labels,
+ Annotations: w.Annotations,
+ Created: w.CreationTimestamp.Time,
+ Description: w.Description,
+ Spec: common.Ptr(MapTemplateSpecKubeToAPI(w.Spec)),
+ }
+}
+
+func MapTemplateKubeToAPI(w *testworkflowsv1.TestWorkflowTemplate) *testkube.TestWorkflowTemplate {
+ return common.MapPtr(w, MapTestWorkflowTemplateKubeToAPI)
+}
+
+func MapKubeToAPI(w *testworkflowsv1.TestWorkflow) *testkube.TestWorkflow {
+ return common.MapPtr(w, MapTestWorkflowKubeToAPI)
+}
+
+func MapListKubeToAPI(v *testworkflowsv1.TestWorkflowList) []testkube.TestWorkflow {
+ workflows := make([]testkube.TestWorkflow, len(v.Items))
+ for i, item := range v.Items {
+ workflows[i] = MapTestWorkflowKubeToAPI(item)
+ }
+ return workflows
+}
+
+func MapTemplateListKubeToAPI(v *testworkflowsv1.TestWorkflowTemplateList) []testkube.TestWorkflowTemplate {
+ workflows := make([]testkube.TestWorkflowTemplate, len(v.Items))
+ for i, item := range v.Items {
+ workflows[i] = MapTestWorkflowTemplateKubeToAPI(item)
+ }
+ return workflows
+}
diff --git a/pkg/tcl/mapperstcl/testworkflows/mappers_test.go b/pkg/tcl/mapperstcl/testworkflows/mappers_test.go
new file mode 100644
index 00000000000..f0df2a6d363
--- /dev/null
+++ b/pkg/tcl/mapperstcl/testworkflows/mappers_test.go
@@ -0,0 +1,403 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflows
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/resource"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/util/intstr"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/internal/common"
+)
+
+var (
+ container = testworkflowsv1.ContainerConfig{
+ WorkingDir: common.Ptr("/wd"),
+ Image: "some-image",
+ ImagePullPolicy: "IfNotPresent",
+ Env: []corev1.EnvVar{
+ {Name: "some-naaame", Value: "some-value"},
+ {Name: "some-naaame", ValueFrom: &corev1.EnvVarSource{
+ FieldRef: &corev1.ObjectFieldSelector{
+ APIVersion: "api.value.1",
+ FieldPath: "the.field.pa",
+ },
+ ResourceFieldRef: &corev1.ResourceFieldSelector{
+ ContainerName: "con-name",
+ Resource: "anc",
+ },
+ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "cfg-name"},
+ Key: "cfg-key",
+ },
+ SecretKeyRef: &corev1.SecretKeySelector{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "some-sec"},
+ Key: "sec-key",
+ },
+ }},
+ },
+ EnvFrom: []corev1.EnvFromSource{
+ {
+ Prefix: "some-prefix",
+ ConfigMapRef: &corev1.ConfigMapEnvSource{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "some-name",
+ },
+ },
+ SecretRef: &corev1.SecretEnvSource{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "some-sec",
+ },
+ Optional: common.Ptr(true),
+ },
+ },
+ },
+ Command: common.Ptr([]string{"c", "d"}),
+ Args: common.Ptr([]string{"ar", "gs"}),
+ Resources: &testworkflowsv1.Resources{
+ Limits: map[corev1.ResourceName]intstr.IntOrString{
+ corev1.ResourceCPU: {Type: intstr.String, StrVal: "300m"},
+ corev1.ResourceMemory: {Type: intstr.Int, IntVal: 1024},
+ },
+ Requests: map[corev1.ResourceName]intstr.IntOrString{
+ corev1.ResourceCPU: {Type: intstr.String, StrVal: "3800m"},
+ corev1.ResourceMemory: {Type: intstr.Int, IntVal: 10204},
+ },
+ },
+ SecurityContext: &corev1.SecurityContext{
+ RunAsUser: common.Ptr(int64(334)),
+ RunAsGroup: common.Ptr(int64(11)),
+ RunAsNonRoot: common.Ptr(true),
+ ReadOnlyRootFilesystem: common.Ptr(false),
+ AllowPrivilegeEscalation: nil,
+ },
+ }
+ content = testworkflowsv1.Content{
+ Git: &testworkflowsv1.ContentGit{
+ Uri: "some-uri",
+ Revision: "some-revision",
+ Username: "some-username",
+ UsernameFrom: &corev1.EnvVarSource{
+ FieldRef: &corev1.ObjectFieldSelector{
+ APIVersion: "testworkflows.dummy.io/v1",
+ FieldPath: "the.field.path",
+ },
+ ResourceFieldRef: &corev1.ResourceFieldSelector{
+ ContainerName: "container.name",
+ Resource: "the.resource",
+ Divisor: resource.MustParse("300"),
+ },
+ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "the-name-config"},
+ Key: "the-key",
+ Optional: common.Ptr(true),
+ },
+ SecretKeyRef: &corev1.SecretKeySelector{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "the-name-secret"},
+ Key: "the-key-secret",
+ Optional: nil,
+ },
+ },
+ Token: "the-token",
+ TokenFrom: &corev1.EnvVarSource{
+ FieldRef: &corev1.ObjectFieldSelector{
+ APIVersion: "some.dummy.api/v1",
+ FieldPath: "some.field",
+ },
+ ResourceFieldRef: &corev1.ResourceFieldSelector{
+ ContainerName: "some-container-name",
+ Resource: "some-resource",
+ Divisor: resource.MustParse("200"),
+ },
+ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "the-name"},
+ Key: "the-abc",
+ Optional: nil,
+ },
+ SecretKeyRef: &corev1.SecretKeySelector{
+ LocalObjectReference: corev1.LocalObjectReference{Name: "xyz"},
+ Key: "222",
+ Optional: nil,
+ },
+ },
+ AuthType: "basic",
+ MountPath: "/some/output/path",
+ Paths: []string{"a", "b", "c"},
+ },
+ Files: []testworkflowsv1.ContentFile{
+ {
+ Path: "some-path",
+ Content: "some-content",
+ ContentFrom: &corev1.EnvVarSource{
+ FieldRef: &corev1.ObjectFieldSelector{
+ APIVersion: "api.version.abc",
+ FieldPath: "field.path",
+ },
+ },
+ Mode: common.Ptr(int32(0777)),
+ },
+ },
+ }
+ stepBase = testworkflowsv1.StepBase{
+ Name: "some-name",
+ Condition: "some-condition",
+ Negative: true,
+ Optional: false,
+ Retry: &testworkflowsv1.RetryPolicy{
+ Count: 444,
+ Until: "abc",
+ },
+ Timeout: "3h15m",
+ Delay: "2m40s",
+ Content: &testworkflowsv1.Content{
+ Git: &testworkflowsv1.ContentGit{
+ Uri: "some-url",
+ Revision: "another-rev",
+ Username: "some-username",
+ UsernameFrom: &corev1.EnvVarSource{
+ FieldRef: &corev1.ObjectFieldSelector{
+ APIVersion: "dummy.api",
+ FieldPath: "field.path.there",
+ },
+ ResourceFieldRef: &corev1.ResourceFieldSelector{
+ ContainerName: "con-name",
+ Resource: "abc1",
+ },
+ },
+ Token: "",
+ TokenFrom: &corev1.EnvVarSource{
+ FieldRef: &corev1.ObjectFieldSelector{
+ APIVersion: "test.v1",
+ FieldPath: "abc.there",
+ },
+ },
+ AuthType: "basic",
+ MountPath: "/a/b/c",
+ Paths: []string{"p", "a", "th"},
+ },
+ Files: []testworkflowsv1.ContentFile{
+ {Path: "abc", Content: "some-content"},
+ },
+ },
+ Shell: "shell-to-run",
+ Run: &testworkflowsv1.StepRun{
+ ContainerConfig: testworkflowsv1.ContainerConfig{
+ WorkingDir: common.Ptr("/abc"),
+ Image: "im-g",
+ ImagePullPolicy: "IfNotPresent",
+ Env: []corev1.EnvVar{
+ {Name: "abc", Value: "230"},
+ },
+ EnvFrom: []corev1.EnvFromSource{
+ {Prefix: "abc"},
+ },
+ Command: common.Ptr([]string{"c", "m", "d"}),
+ Args: common.Ptr([]string{"arg", "s", "d"}),
+ Resources: &testworkflowsv1.Resources{
+ Limits: map[corev1.ResourceName]intstr.IntOrString{
+ corev1.ResourceCPU: {Type: intstr.Int, IntVal: 444},
+ },
+ },
+ SecurityContext: &corev1.SecurityContext{
+ RunAsUser: common.Ptr(int64(444)),
+ RunAsGroup: nil,
+ RunAsNonRoot: common.Ptr(true),
+ ReadOnlyRootFilesystem: nil,
+ AllowPrivilegeEscalation: nil,
+ },
+ },
+ },
+ WorkingDir: common.Ptr("/ssss"),
+ Container: &testworkflowsv1.ContainerConfig{
+ WorkingDir: common.Ptr("/aaaa"),
+ Image: "ssss",
+ ImagePullPolicy: "Never",
+ Env: []corev1.EnvVar{{Name: "xyz", Value: "bar"}},
+ Command: common.Ptr([]string{"ab"}),
+ Args: common.Ptr([]string{"abrgs"}),
+ Resources: &testworkflowsv1.Resources{
+ Requests: map[corev1.ResourceName]intstr.IntOrString{
+ corev1.ResourceMemory: {Type: intstr.String, StrVal: "300m"},
+ },
+ },
+ SecurityContext: &corev1.SecurityContext{
+ Privileged: common.Ptr(true),
+ RunAsUser: common.Ptr(int64(33)),
+ },
+ },
+ Execute: &testworkflowsv1.StepExecute{
+ Parallelism: 880,
+ Async: false,
+ Tests: []testworkflowsv1.StepExecuteTest{{Name: "some-name-test"}},
+ Workflows: []testworkflowsv1.StepExecuteWorkflow{{Name: "some-workflow", Config: map[string]intstr.IntOrString{
+ "id": {Type: intstr.String, StrVal: "xyzz"},
+ }}},
+ },
+ Artifacts: &testworkflowsv1.StepArtifacts{
+ Compress: &testworkflowsv1.ArtifactCompression{
+ Name: "some-artifact.tar.gz",
+ },
+ Paths: []string{"/get", "/from/there"},
+ },
+ }
+ step = testworkflowsv1.Step{
+ StepBase: stepBase,
+ Use: []testworkflowsv1.TemplateRef{
+ {Name: "/abc", Config: map[string]intstr.IntOrString{
+ "xxx": {Type: intstr.Int, IntVal: 322},
+ }},
+ },
+ Template: &testworkflowsv1.TemplateRef{
+ Name: "other-one",
+ Config: map[string]intstr.IntOrString{
+ "foo": {Type: intstr.String, StrVal: "bar"},
+ },
+ },
+ Steps: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{Name: "xyz"}},
+ },
+ }
+ independentStep = testworkflowsv1.IndependentStep{
+ StepBase: stepBase,
+ Steps: []testworkflowsv1.IndependentStep{
+ {StepBase: testworkflowsv1.StepBase{Name: "xyz"}},
+ },
+ }
+ workflowSpecBase = testworkflowsv1.TestWorkflowSpecBase{
+ Config: map[string]testworkflowsv1.ParameterSchema{
+ "some-key": {
+ Description: "some-description",
+ Type: "integer",
+ Enum: []string{"en", "um"},
+ Example: &intstr.IntOrString{
+ Type: intstr.String,
+ StrVal: "some-vale",
+ },
+ Default: &intstr.IntOrString{
+ Type: intstr.Int,
+ IntVal: 233,
+ },
+ ParameterStringSchema: testworkflowsv1.ParameterStringSchema{
+ Format: "url",
+ Pattern: "^abc$",
+ MinLength: common.Ptr(int64(1)),
+ MaxLength: common.Ptr(int64(2)),
+ },
+ ParameterNumberSchema: testworkflowsv1.ParameterNumberSchema{
+ Minimum: common.Ptr(int64(3)),
+ Maximum: common.Ptr(int64(4)),
+ ExclusiveMinimum: common.Ptr(int64(5)),
+ ExclusiveMaximum: common.Ptr(int64(7)),
+ MultipleOf: common.Ptr(int64(8)),
+ },
+ },
+ },
+ Content: &content,
+ Container: &container,
+ Job: &testworkflowsv1.JobConfig{
+ Labels: map[string]string{"some-key": "some-value"},
+ Annotations: map[string]string{"some-key=2": "some-value-2"},
+ },
+ Pod: &testworkflowsv1.PodConfig{
+ ServiceAccountName: "some-name",
+ ImagePullSecrets: []corev1.LocalObjectReference{{Name: "v1"}, {Name: "v2"}},
+ NodeSelector: map[string]string{"some-key-3": "some-value"},
+ Labels: map[string]string{"some-key-4": "some-value"},
+ Annotations: map[string]string{"some-key=5": "some-value-2"},
+ },
+ }
+)
+
+func TestMapTestWorkflowBackAndForth(t *testing.T) {
+ want := testworkflowsv1.TestWorkflow{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "TestWorkflow",
+ APIVersion: "testworkflows.testkube.io/v1",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dummy",
+ Namespace: "dummy-namespace",
+ },
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ Use: []testworkflowsv1.TemplateRef{
+ {
+ Name: "some-name",
+ Config: map[string]intstr.IntOrString{
+ "some-key": {Type: intstr.String, StrVal: "some-value"},
+ "some-key-2": {Type: intstr.Int, IntVal: 444},
+ },
+ },
+ },
+ TestWorkflowSpecBase: workflowSpecBase,
+ Setup: []testworkflowsv1.Step{step},
+ Steps: []testworkflowsv1.Step{step, step},
+ After: []testworkflowsv1.Step{step, step, step, step},
+ },
+ }
+ got := MapTestWorkflowAPIToKube(MapTestWorkflowKubeToAPI(*want.DeepCopy()))
+ assert.Equal(t, want, got)
+}
+
+func TestMapEmptyTestWorkflowBackAndForth(t *testing.T) {
+ want := testworkflowsv1.TestWorkflow{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "TestWorkflow",
+ APIVersion: "testworkflows.testkube.io/v1",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dummy",
+ Namespace: "dummy-namespace",
+ },
+ Spec: testworkflowsv1.TestWorkflowSpec{},
+ }
+ got := MapTestWorkflowAPIToKube(MapTestWorkflowKubeToAPI(*want.DeepCopy()))
+ assert.Equal(t, want, got)
+}
+
+func TestMapTestWorkflowTemplateBackAndForth(t *testing.T) {
+ want := testworkflowsv1.TestWorkflowTemplate{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "TestWorkflowTemplate",
+ APIVersion: "testworkflows.testkube.io/v1",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dummy",
+ Namespace: "dummy-namespace",
+ },
+ Spec: testworkflowsv1.TestWorkflowTemplateSpec{
+ TestWorkflowSpecBase: workflowSpecBase,
+ Setup: []testworkflowsv1.IndependentStep{independentStep},
+ Steps: []testworkflowsv1.IndependentStep{independentStep, independentStep},
+ After: []testworkflowsv1.IndependentStep{independentStep, independentStep, independentStep, independentStep},
+ },
+ }
+ got := MapTestWorkflowTemplateAPIToKube(MapTestWorkflowTemplateKubeToAPI(*want.DeepCopy()))
+ assert.Equal(t, want, got)
+}
+
+func TestMapEmptyTestWorkflowTemplateBackAndForth(t *testing.T) {
+ want := testworkflowsv1.TestWorkflowTemplate{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "TestWorkflowTemplate",
+ APIVersion: "testworkflows.testkube.io/v1",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dummy",
+ Namespace: "dummy-namespace",
+ },
+ Spec: testworkflowsv1.TestWorkflowTemplateSpec{},
+ }
+ got := MapTestWorkflowTemplateAPIToKube(MapTestWorkflowTemplateKubeToAPI(*want.DeepCopy()))
+ assert.Equal(t, want, got)
+}
diff --git a/pkg/tcl/mapperstcl/testworkflows/openapi_kube.go b/pkg/tcl/mapperstcl/testworkflows/openapi_kube.go
new file mode 100644
index 00000000000..ed9cdf07607
--- /dev/null
+++ b/pkg/tcl/mapperstcl/testworkflows/openapi_kube.go
@@ -0,0 +1,675 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflows
+
+import (
+ "strconv"
+
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/resource"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/util/intstr"
+
+ testsv3 "github.com/kubeshop/testkube-operator/api/tests/v3"
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+)
+
+func MapStringToIntOrString(i string) intstr.IntOrString {
+ if v, err := strconv.ParseInt(i, 10, 32); err == nil {
+ return intstr.IntOrString{Type: intstr.Int, IntVal: int32(v)}
+ }
+ return intstr.IntOrString{Type: intstr.String, StrVal: i}
+}
+
+func MapStringPtrToIntOrStringPtr(i *string) *intstr.IntOrString {
+ if i == nil {
+ return nil
+ }
+ return common.Ptr(MapStringToIntOrString(*i))
+}
+
+func MapBoxedStringToString(v *testkube.BoxedString) *string {
+ if v == nil {
+ return nil
+ }
+ return &v.Value
+}
+
+func MapBoxedStringToType[T ~string](v *testkube.BoxedString) *T {
+ if v == nil {
+ return nil
+ }
+ return common.Ptr(T(v.Value))
+}
+
+func MapBoxedStringToQuantity(v testkube.BoxedString) resource.Quantity {
+ q, _ := resource.ParseQuantity(v.Value)
+ return q
+}
+
+func MapBoxedBooleanToBool(v *testkube.BoxedBoolean) *bool {
+ if v == nil {
+ return nil
+ }
+ return &v.Value
+}
+
+func MapBoxedStringListToStringSlice(v *testkube.BoxedStringList) *[]string {
+ if v == nil {
+ return nil
+ }
+ return &v.Value
+}
+
+func MapBoxedIntegerToInt64(v *testkube.BoxedInteger) *int64 {
+ if v == nil {
+ return nil
+ }
+ return common.Ptr(int64(v.Value))
+}
+
+func MapBoxedIntegerToInt32(v *testkube.BoxedInteger) *int32 {
+ if v == nil {
+ return nil
+ }
+ return &v.Value
+}
+
+func MapEnvVarAPIToKube(v testkube.EnvVar) corev1.EnvVar {
+ return corev1.EnvVar{
+ Name: v.Name,
+ Value: v.Value,
+ ValueFrom: common.MapPtr(v.ValueFrom, MapEnvVarSourceAPIToKube),
+ }
+}
+
+func MapConfigMapKeyRefAPIToKube(v *testkube.EnvVarSourceConfigMapKeyRef) *corev1.ConfigMapKeySelector {
+ if v == nil {
+ return nil
+ }
+ return &corev1.ConfigMapKeySelector{
+ Key: v.Key,
+ LocalObjectReference: corev1.LocalObjectReference{Name: v.Name},
+ Optional: common.PtrOrNil(v.Optional),
+ }
+}
+
+func MapFieldRefAPIToKube(v *testkube.EnvVarSourceFieldRef) *corev1.ObjectFieldSelector {
+ if v == nil {
+ return nil
+ }
+ return &corev1.ObjectFieldSelector{
+ APIVersion: v.ApiVersion,
+ FieldPath: v.FieldPath,
+ }
+}
+
+func MapResourceFieldRefAPIToKube(v *testkube.EnvVarSourceResourceFieldRef) *corev1.ResourceFieldSelector {
+ if v == nil {
+ return nil
+ }
+ divisor, _ := resource.ParseQuantity(v.Divisor)
+ return &corev1.ResourceFieldSelector{
+ ContainerName: v.ContainerName,
+ Divisor: divisor,
+ Resource: v.Resource,
+ }
+}
+
+func MapSecretKeyRefAPIToKube(v *testkube.EnvVarSourceSecretKeyRef) *corev1.SecretKeySelector {
+ if v == nil {
+ return nil
+ }
+ return &corev1.SecretKeySelector{
+ Key: v.Key,
+ LocalObjectReference: corev1.LocalObjectReference{Name: v.Name},
+ Optional: common.PtrOrNil(v.Optional),
+ }
+}
+
+func MapEnvVarSourceAPIToKube(v testkube.EnvVarSource) corev1.EnvVarSource {
+ return corev1.EnvVarSource{
+ ConfigMapKeyRef: MapConfigMapKeyRefAPIToKube(v.ConfigMapKeyRef),
+ FieldRef: MapFieldRefAPIToKube(v.FieldRef),
+ ResourceFieldRef: MapResourceFieldRefAPIToKube(v.ResourceFieldRef),
+ SecretKeyRef: MapSecretKeyRefAPIToKube(v.SecretKeyRef),
+ }
+}
+
+func MapConfigMapEnvSourceAPIToKube(v *testkube.ConfigMapEnvSource) *corev1.ConfigMapEnvSource {
+ if v == nil {
+ return nil
+ }
+ return &corev1.ConfigMapEnvSource{
+ LocalObjectReference: corev1.LocalObjectReference{Name: v.Name},
+ Optional: common.PtrOrNil(v.Optional),
+ }
+}
+
+func MapSecretEnvSourceAPIToKube(v *testkube.SecretEnvSource) *corev1.SecretEnvSource {
+ if v == nil {
+ return nil
+ }
+ return &corev1.SecretEnvSource{
+ LocalObjectReference: corev1.LocalObjectReference{Name: v.Name},
+ Optional: common.PtrOrNil(v.Optional),
+ }
+}
+
+func MapEnvFromSourceAPIToKube(v testkube.EnvFromSource) corev1.EnvFromSource {
+ return corev1.EnvFromSource{
+ Prefix: v.Prefix,
+ ConfigMapRef: MapConfigMapEnvSourceAPIToKube(v.ConfigMapRef),
+ SecretRef: MapSecretEnvSourceAPIToKube(v.SecretRef),
+ }
+}
+
+func MapSecurityContextAPIToKube(v *testkube.SecurityContext) *corev1.SecurityContext {
+ if v == nil {
+ return nil
+ }
+ return &corev1.SecurityContext{
+ Privileged: MapBoxedBooleanToBool(v.Privileged),
+ RunAsUser: MapBoxedIntegerToInt64(v.RunAsUser),
+ RunAsGroup: MapBoxedIntegerToInt64(v.RunAsGroup),
+ RunAsNonRoot: MapBoxedBooleanToBool(v.RunAsNonRoot),
+ ReadOnlyRootFilesystem: MapBoxedBooleanToBool(v.ReadOnlyRootFilesystem),
+ AllowPrivilegeEscalation: MapBoxedBooleanToBool(v.AllowPrivilegeEscalation),
+ }
+}
+
+func MapLocalObjectReferenceAPIToKube(v testkube.LocalObjectReference) corev1.LocalObjectReference {
+ return corev1.LocalObjectReference{Name: v.Name}
+}
+
+func MapConfigValueAPIToKube(v map[string]string) map[string]intstr.IntOrString {
+ return common.MapMap(v, MapStringToIntOrString)
+}
+
+func MapParameterTypeAPIToKube(v *testkube.TestWorkflowParameterType) testworkflowsv1.ParameterType {
+ if v == nil {
+ return ""
+ }
+ return testworkflowsv1.ParameterType(*v)
+}
+
+func MapGitAuthTypeAPIToKube(v *testkube.ContentGitAuthType) testsv3.GitAuthType {
+ if v == nil {
+ return ""
+ }
+ return testsv3.GitAuthType(*v)
+}
+
+func MapImagePullPolicyAPIToKube(v *testkube.ImagePullPolicy) corev1.PullPolicy {
+ if v == nil {
+ return ""
+ }
+ return corev1.PullPolicy(*v)
+}
+
+func MapParameterSchemaAPIToKube(v testkube.TestWorkflowParameterSchema) testworkflowsv1.ParameterSchema {
+ var example *intstr.IntOrString
+ if v.Example != "" {
+ example = common.Ptr(MapStringToIntOrString(v.Example))
+ }
+ return testworkflowsv1.ParameterSchema{
+ Description: v.Description,
+ Type: MapParameterTypeAPIToKube(v.Type_),
+ Enum: v.Enum,
+ Example: example,
+ Default: MapStringPtrToIntOrStringPtr(MapBoxedStringToString(v.Default_)),
+ ParameterStringSchema: testworkflowsv1.ParameterStringSchema{
+ Format: v.Format,
+ Pattern: v.Pattern,
+ MinLength: MapBoxedIntegerToInt64(v.MinLength),
+ MaxLength: MapBoxedIntegerToInt64(v.MaxLength),
+ },
+ ParameterNumberSchema: testworkflowsv1.ParameterNumberSchema{
+ Minimum: MapBoxedIntegerToInt64(v.Minimum),
+ Maximum: MapBoxedIntegerToInt64(v.Maximum),
+ ExclusiveMinimum: MapBoxedIntegerToInt64(v.ExclusiveMinimum),
+ ExclusiveMaximum: MapBoxedIntegerToInt64(v.ExclusiveMaximum),
+ MultipleOf: MapBoxedIntegerToInt64(v.MultipleOf),
+ },
+ }
+}
+
+func MapTemplateRefAPIToKube(v testkube.TestWorkflowTemplateRef) testworkflowsv1.TemplateRef {
+ return testworkflowsv1.TemplateRef{
+ Name: v.Name,
+ Config: MapConfigValueAPIToKube(v.Config),
+ }
+}
+
+func MapContentGitAPIToKube(v testkube.TestWorkflowContentGit) testworkflowsv1.ContentGit {
+ return testworkflowsv1.ContentGit{
+ Uri: v.Uri,
+ Revision: v.Revision,
+ Username: v.Username,
+ UsernameFrom: common.MapPtr(v.UsernameFrom, MapEnvVarSourceAPIToKube),
+ Token: v.Token,
+ TokenFrom: common.MapPtr(v.TokenFrom, MapEnvVarSourceAPIToKube),
+ AuthType: MapGitAuthTypeAPIToKube(v.AuthType),
+ MountPath: v.MountPath,
+ Paths: v.Paths,
+ }
+}
+
+func MapContentAPIToKube(v testkube.TestWorkflowContent) testworkflowsv1.Content {
+ return testworkflowsv1.Content{
+ Git: common.MapPtr(v.Git, MapContentGitAPIToKube),
+ Files: common.MapSlice(v.Files, MapContentFileAPIToKube),
+ }
+}
+
+func MapContentFileAPIToKube(v testkube.TestWorkflowContentFile) testworkflowsv1.ContentFile {
+ return testworkflowsv1.ContentFile{
+ Path: v.Path,
+ Content: v.Content,
+ ContentFrom: common.MapPtr(v.ContentFrom, MapEnvVarSourceAPIToKube),
+ Mode: MapBoxedIntegerToInt32(v.Mode),
+ }
+}
+
+func MapResourcesListAPIToKube(v *testkube.TestWorkflowResourcesList) map[corev1.ResourceName]intstr.IntOrString {
+ if v == nil {
+ return nil
+ }
+ res := make(map[corev1.ResourceName]intstr.IntOrString)
+ if v.Cpu != "" {
+ res[corev1.ResourceCPU] = MapStringToIntOrString(v.Cpu)
+ }
+ if v.Memory != "" {
+ res[corev1.ResourceMemory] = MapStringToIntOrString(v.Memory)
+ }
+ if v.Storage != "" {
+ res[corev1.ResourceStorage] = MapStringToIntOrString(v.Storage)
+ }
+ if v.EphemeralStorage != "" {
+ res[corev1.ResourceEphemeralStorage] = MapStringToIntOrString(v.EphemeralStorage)
+ }
+ return res
+}
+
+func MapResourcesAPIToKube(v testkube.TestWorkflowResources) testworkflowsv1.Resources {
+ return testworkflowsv1.Resources{
+ Limits: MapResourcesListAPIToKube(v.Limits),
+ Requests: MapResourcesListAPIToKube(v.Requests),
+ }
+}
+
+func MapJobConfigAPIToKube(v testkube.TestWorkflowJobConfig) testworkflowsv1.JobConfig {
+ return testworkflowsv1.JobConfig{
+ Labels: v.Labels,
+ Annotations: v.Annotations,
+ }
+}
+
+func MapHostPathVolumeSourceAPIToKube(v testkube.HostPathVolumeSource) corev1.HostPathVolumeSource {
+ return corev1.HostPathVolumeSource{
+ Path: v.Path,
+ Type: MapBoxedStringToType[corev1.HostPathType](v.Type_),
+ }
+}
+
+func MapEmptyDirVolumeSourceAPIToKube(v testkube.EmptyDirVolumeSource) corev1.EmptyDirVolumeSource {
+ return corev1.EmptyDirVolumeSource{
+ Medium: corev1.StorageMedium(v.Medium),
+ SizeLimit: common.MapPtr(v.SizeLimit, MapBoxedStringToQuantity),
+ }
+}
+
+func MapGCEPersistentDiskVolumeSourceAPIToKube(v testkube.GcePersistentDiskVolumeSource) corev1.GCEPersistentDiskVolumeSource {
+ return corev1.GCEPersistentDiskVolumeSource{
+ PDName: v.PdName,
+ FSType: v.FsType,
+ Partition: v.Partition,
+ ReadOnly: v.ReadOnly,
+ }
+}
+
+func MapAWSElasticBlockStoreVolumeSourceAPIToKube(v testkube.AwsElasticBlockStoreVolumeSource) corev1.AWSElasticBlockStoreVolumeSource {
+ return corev1.AWSElasticBlockStoreVolumeSource{
+ VolumeID: v.VolumeID,
+ FSType: v.FsType,
+ Partition: v.Partition,
+ ReadOnly: v.ReadOnly,
+ }
+}
+
+func MapKeyToPathAPIToKube(v testkube.SecretVolumeSourceItems) corev1.KeyToPath {
+ return corev1.KeyToPath{
+ Key: v.Key,
+ Path: v.Path,
+ Mode: MapBoxedIntegerToInt32(v.Mode),
+ }
+}
+
+func MapSecretVolumeSourceAPIToKube(v testkube.SecretVolumeSource) corev1.SecretVolumeSource {
+ return corev1.SecretVolumeSource{
+ SecretName: v.SecretName,
+ Items: common.MapSlice(v.Items, MapKeyToPathAPIToKube),
+ DefaultMode: MapBoxedIntegerToInt32(v.DefaultMode),
+ Optional: common.PtrOrNil(v.Optional),
+ }
+}
+
+func MapNFSVolumeSourceAPIToKube(v testkube.NfsVolumeSource) corev1.NFSVolumeSource {
+ return corev1.NFSVolumeSource{
+ Server: v.Server,
+ Path: v.Path,
+ ReadOnly: v.ReadOnly,
+ }
+}
+
+func MapPersistentVolumeClaimVolumeSourceAPIToKube(v testkube.PersistentVolumeClaimVolumeSource) corev1.PersistentVolumeClaimVolumeSource {
+ return corev1.PersistentVolumeClaimVolumeSource{
+ ClaimName: v.ClaimName,
+ ReadOnly: v.ReadOnly,
+ }
+}
+
+func MapCephFSVolumeSourceAPIToKube(v testkube.CephFsVolumeSource) corev1.CephFSVolumeSource {
+ return corev1.CephFSVolumeSource{
+ Monitors: v.Monitors,
+ Path: v.Path,
+ User: v.User,
+ SecretFile: v.SecretFile,
+ SecretRef: common.MapPtr(v.SecretRef, MapLocalObjectReferenceAPIToKube),
+ ReadOnly: v.ReadOnly,
+ }
+}
+
+func MapAzureFileVolumeSourceAPIToKube(v testkube.AzureFileVolumeSource) corev1.AzureFileVolumeSource {
+ return corev1.AzureFileVolumeSource{
+ SecretName: v.SecretName,
+ ShareName: v.ShareName,
+ ReadOnly: v.ReadOnly,
+ }
+}
+
+func MapConfigMapVolumeSourceAPIToKube(v testkube.ConfigMapVolumeSource) corev1.ConfigMapVolumeSource {
+ return corev1.ConfigMapVolumeSource{
+ LocalObjectReference: corev1.LocalObjectReference{Name: v.Name},
+ Items: common.MapSlice(v.Items, MapKeyToPathAPIToKube),
+ DefaultMode: MapBoxedIntegerToInt32(v.DefaultMode),
+ Optional: common.PtrOrNil(v.Optional),
+ }
+}
+
+func MapAzureDiskVolumeSourceAPIToKube(v testkube.AzureDiskVolumeSource) corev1.AzureDiskVolumeSource {
+ return corev1.AzureDiskVolumeSource{
+ DiskName: v.DiskName,
+ DataDiskURI: v.DiskURI,
+ CachingMode: MapBoxedStringToType[corev1.AzureDataDiskCachingMode](v.CachingMode),
+ FSType: MapBoxedStringToString(v.FsType),
+ ReadOnly: common.PtrOrNil(v.ReadOnly),
+ Kind: MapBoxedStringToType[corev1.AzureDataDiskKind](v.Kind),
+ }
+}
+
+func MapVolumeAPIToKube(v testkube.Volume) corev1.Volume {
+ // TODO: Add rest of VolumeSource types in future,
+ // so they will be recognized by JSON API and persisted with Execution.
+ return corev1.Volume{
+ Name: v.Name,
+ VolumeSource: corev1.VolumeSource{
+ HostPath: common.MapPtr(v.HostPath, MapHostPathVolumeSourceAPIToKube),
+ EmptyDir: common.MapPtr(v.EmptyDir, MapEmptyDirVolumeSourceAPIToKube),
+ GCEPersistentDisk: common.MapPtr(v.GcePersistentDisk, MapGCEPersistentDiskVolumeSourceAPIToKube),
+ AWSElasticBlockStore: common.MapPtr(v.AwsElasticBlockStore, MapAWSElasticBlockStoreVolumeSourceAPIToKube),
+ Secret: common.MapPtr(v.Secret, MapSecretVolumeSourceAPIToKube),
+ NFS: common.MapPtr(v.Nfs, MapNFSVolumeSourceAPIToKube),
+ PersistentVolumeClaim: common.MapPtr(v.PersistentVolumeClaim, MapPersistentVolumeClaimVolumeSourceAPIToKube),
+ CephFS: common.MapPtr(v.Cephfs, MapCephFSVolumeSourceAPIToKube),
+ AzureFile: common.MapPtr(v.AzureFile, MapAzureFileVolumeSourceAPIToKube),
+ ConfigMap: common.MapPtr(v.ConfigMap, MapConfigMapVolumeSourceAPIToKube),
+ AzureDisk: common.MapPtr(v.AzureDisk, MapAzureDiskVolumeSourceAPIToKube),
+ },
+ }
+}
+
+func MapPodConfigAPIToKube(v testkube.TestWorkflowPodConfig) testworkflowsv1.PodConfig {
+ return testworkflowsv1.PodConfig{
+ ServiceAccountName: v.ServiceAccountName,
+ ImagePullSecrets: common.MapSlice(v.ImagePullSecrets, MapLocalObjectReferenceAPIToKube),
+ NodeSelector: v.NodeSelector,
+ Labels: v.Labels,
+ Annotations: v.Annotations,
+ Volumes: common.MapSlice(v.Volumes, MapVolumeAPIToKube),
+ }
+}
+
+func MapVolumeMountAPIToKube(v testkube.VolumeMount) corev1.VolumeMount {
+ return corev1.VolumeMount{
+ Name: v.Name,
+ ReadOnly: v.ReadOnly,
+ MountPath: v.MountPath,
+ SubPath: v.SubPath,
+ MountPropagation: MapBoxedStringToType[corev1.MountPropagationMode](v.MountPropagation),
+ SubPathExpr: v.SubPathExpr,
+ }
+}
+
+func MapContainerConfigAPIToKube(v testkube.TestWorkflowContainerConfig) testworkflowsv1.ContainerConfig {
+ return testworkflowsv1.ContainerConfig{
+ WorkingDir: MapBoxedStringToString(v.WorkingDir),
+ Image: v.Image,
+ ImagePullPolicy: MapImagePullPolicyAPIToKube(v.ImagePullPolicy),
+ Env: common.MapSlice(v.Env, MapEnvVarAPIToKube),
+ EnvFrom: common.MapSlice(v.EnvFrom, MapEnvFromSourceAPIToKube),
+ Command: MapBoxedStringListToStringSlice(v.Command),
+ Args: MapBoxedStringListToStringSlice(v.Args),
+ Resources: common.MapPtr(v.Resources, MapResourcesAPIToKube),
+ SecurityContext: MapSecurityContextAPIToKube(v.SecurityContext),
+ VolumeMounts: common.MapSlice(v.VolumeMounts, MapVolumeMountAPIToKube),
+ }
+}
+
+func MapStepRunAPIToKube(v testkube.TestWorkflowContainerConfig) testworkflowsv1.StepRun {
+ return testworkflowsv1.StepRun{
+ ContainerConfig: MapContainerConfigAPIToKube(v),
+ }
+}
+
+func MapStepExecuteTestAPIToKube(v testkube.TestWorkflowStepExecuteTestRef) testworkflowsv1.StepExecuteTest {
+ return testworkflowsv1.StepExecuteTest{
+ Name: v.Name,
+ }
+}
+
+func MapTestWorkflowRefAPIToKube(v testkube.TestWorkflowRef) testworkflowsv1.StepExecuteWorkflow {
+ return testworkflowsv1.StepExecuteWorkflow{
+ Name: v.Name,
+ Config: MapConfigValueAPIToKube(v.Config),
+ }
+}
+
+func MapStepExecuteAPIToKube(v testkube.TestWorkflowStepExecute) testworkflowsv1.StepExecute {
+ return testworkflowsv1.StepExecute{
+ Parallelism: v.Parallelism,
+ Async: v.Async,
+ Tests: common.MapSlice(v.Tests, MapStepExecuteTestAPIToKube),
+ Workflows: common.MapSlice(v.Workflows, MapTestWorkflowRefAPIToKube),
+ }
+}
+
+func MapStepArtifactsCompressionAPIToKube(v testkube.TestWorkflowStepArtifactsCompression) testworkflowsv1.ArtifactCompression {
+ return testworkflowsv1.ArtifactCompression{
+ Name: v.Name,
+ }
+}
+
+func MapStepArtifactsAPIToKube(v testkube.TestWorkflowStepArtifacts) testworkflowsv1.StepArtifacts {
+ return testworkflowsv1.StepArtifacts{
+ WorkingDir: MapBoxedStringToString(v.WorkingDir),
+ Compress: common.MapPtr(v.Compress, MapStepArtifactsCompressionAPIToKube),
+ Paths: v.Paths,
+ }
+}
+
+func MapRetryPolicyAPIToKube(v testkube.TestWorkflowRetryPolicy) testworkflowsv1.RetryPolicy {
+ return testworkflowsv1.RetryPolicy{
+ Count: v.Count,
+ Until: v.Until,
+ }
+}
+
+func MapStepAPIToKube(v testkube.TestWorkflowStep) testworkflowsv1.Step {
+ return testworkflowsv1.Step{
+ StepBase: testworkflowsv1.StepBase{
+ Name: v.Name,
+ Condition: v.Condition,
+ Negative: v.Negative,
+ Optional: v.Optional,
+ Retry: common.MapPtr(v.Retry, MapRetryPolicyAPIToKube),
+ Timeout: v.Timeout,
+ Delay: v.Delay,
+ Content: common.MapPtr(v.Content, MapContentAPIToKube),
+ Shell: v.Shell,
+ Run: common.MapPtr(v.Run, MapStepRunAPIToKube),
+ WorkingDir: MapBoxedStringToString(v.WorkingDir),
+ Container: common.MapPtr(v.Container, MapContainerConfigAPIToKube),
+ Execute: common.MapPtr(v.Execute, MapStepExecuteAPIToKube),
+ Artifacts: common.MapPtr(v.Artifacts, MapStepArtifactsAPIToKube),
+ },
+ Use: common.MapSlice(v.Use, MapTemplateRefAPIToKube),
+ Template: common.MapPtr(v.Template, MapTemplateRefAPIToKube),
+ Setup: common.MapSlice(v.Setup, MapStepAPIToKube),
+ Steps: common.MapSlice(v.Steps, MapStepAPIToKube),
+ }
+}
+
+func MapIndependentStepAPIToKube(v testkube.TestWorkflowIndependentStep) testworkflowsv1.IndependentStep {
+ return testworkflowsv1.IndependentStep{
+ StepBase: testworkflowsv1.StepBase{
+ Name: v.Name,
+ Condition: v.Condition,
+ Negative: v.Negative,
+ Optional: v.Optional,
+ Retry: common.MapPtr(v.Retry, MapRetryPolicyAPIToKube),
+ Timeout: v.Timeout,
+ Delay: v.Delay,
+ Content: common.MapPtr(v.Content, MapContentAPIToKube),
+ Shell: v.Shell,
+ Run: common.MapPtr(v.Run, MapStepRunAPIToKube),
+ WorkingDir: MapBoxedStringToString(v.WorkingDir),
+ Container: common.MapPtr(v.Container, MapContainerConfigAPIToKube),
+ Execute: common.MapPtr(v.Execute, MapStepExecuteAPIToKube),
+ Artifacts: common.MapPtr(v.Artifacts, MapStepArtifactsAPIToKube),
+ },
+ Setup: common.MapSlice(v.Setup, MapIndependentStepAPIToKube),
+ Steps: common.MapSlice(v.Steps, MapIndependentStepAPIToKube),
+ }
+}
+
+func MapSpecAPIToKube(v testkube.TestWorkflowSpec) testworkflowsv1.TestWorkflowSpec {
+ return testworkflowsv1.TestWorkflowSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Config: common.MapMap(v.Config, MapParameterSchemaAPIToKube),
+ Content: common.MapPtr(v.Content, MapContentAPIToKube),
+ Container: common.MapPtr(v.Container, MapContainerConfigAPIToKube),
+ Job: common.MapPtr(v.Job, MapJobConfigAPIToKube),
+ Pod: common.MapPtr(v.Pod, MapPodConfigAPIToKube),
+ },
+ Use: common.MapSlice(v.Use, MapTemplateRefAPIToKube),
+ Setup: common.MapSlice(v.Setup, MapStepAPIToKube),
+ Steps: common.MapSlice(v.Steps, MapStepAPIToKube),
+ After: common.MapSlice(v.After, MapStepAPIToKube),
+ }
+}
+
+func MapTemplateSpecAPIToKube(v testkube.TestWorkflowTemplateSpec) testworkflowsv1.TestWorkflowTemplateSpec {
+ return testworkflowsv1.TestWorkflowTemplateSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Config: common.MapMap(v.Config, MapParameterSchemaAPIToKube),
+ Content: common.MapPtr(v.Content, MapContentAPIToKube),
+ Container: common.MapPtr(v.Container, MapContainerConfigAPIToKube),
+ Job: common.MapPtr(v.Job, MapJobConfigAPIToKube),
+ Pod: common.MapPtr(v.Pod, MapPodConfigAPIToKube),
+ },
+ Setup: common.MapSlice(v.Setup, MapIndependentStepAPIToKube),
+ Steps: common.MapSlice(v.Steps, MapIndependentStepAPIToKube),
+ After: common.MapSlice(v.After, MapIndependentStepAPIToKube),
+ }
+}
+
+func MapTestWorkflowAPIToKube(w testkube.TestWorkflow) testworkflowsv1.TestWorkflow {
+ return testworkflowsv1.TestWorkflow{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "TestWorkflow",
+ APIVersion: testworkflowsv1.GroupVersion.Group + "/" + testworkflowsv1.GroupVersion.Version,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: w.Name,
+ Namespace: w.Namespace,
+ Labels: w.Labels,
+ Annotations: w.Annotations,
+ CreationTimestamp: metav1.Time{Time: w.Created},
+ },
+ Description: w.Description,
+ Spec: common.ResolvePtr(common.MapPtr(w.Spec, MapSpecAPIToKube), testworkflowsv1.TestWorkflowSpec{}),
+ }
+}
+
+func MapTestWorkflowTemplateAPIToKube(w testkube.TestWorkflowTemplate) testworkflowsv1.TestWorkflowTemplate {
+ return testworkflowsv1.TestWorkflowTemplate{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "TestWorkflowTemplate",
+ APIVersion: testworkflowsv1.GroupVersion.Group + "/" + testworkflowsv1.GroupVersion.Version,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: w.Name,
+ Namespace: w.Namespace,
+ Labels: w.Labels,
+ Annotations: w.Annotations,
+ CreationTimestamp: metav1.Time{Time: w.Created},
+ },
+ Description: w.Description,
+ Spec: common.ResolvePtr(common.MapPtr(w.Spec, MapTemplateSpecAPIToKube), testworkflowsv1.TestWorkflowTemplateSpec{}),
+ }
+}
+
+func MapTemplateAPIToKube(w *testkube.TestWorkflowTemplate) *testworkflowsv1.TestWorkflowTemplate {
+ return common.MapPtr(w, MapTestWorkflowTemplateAPIToKube)
+}
+
+func MapAPIToKube(w *testkube.TestWorkflow) *testworkflowsv1.TestWorkflow {
+ return common.MapPtr(w, MapTestWorkflowAPIToKube)
+}
+
+func MapListAPIToKube(v []testkube.TestWorkflow) testworkflowsv1.TestWorkflowList {
+ items := make([]testworkflowsv1.TestWorkflow, len(v))
+ for i, item := range v {
+ items[i] = MapTestWorkflowAPIToKube(item)
+ }
+ return testworkflowsv1.TestWorkflowList{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "TestWorkflowList",
+ APIVersion: testworkflowsv1.GroupVersion.String(),
+ },
+ Items: items,
+ }
+}
+
+func MapTemplateListAPIToKube(v []testkube.TestWorkflowTemplate) testworkflowsv1.TestWorkflowTemplateList {
+ items := make([]testworkflowsv1.TestWorkflowTemplate, len(v))
+ for i, item := range v {
+ items[i] = MapTestWorkflowTemplateAPIToKube(item)
+ }
+ return testworkflowsv1.TestWorkflowTemplateList{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "TestWorkflowTemplateList",
+ APIVersion: testworkflowsv1.GroupVersion.String(),
+ },
+ Items: items,
+ }
+}
diff --git a/pkg/tcl/mappertcl/testexecutions/mapper.go b/pkg/tcl/mappertcl/testexecutions/mapper.go
new file mode 100644
index 00000000000..49208cd8dca
--- /dev/null
+++ b/pkg/tcl/mappertcl/testexecutions/mapper.go
@@ -0,0 +1,28 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testexecutions
+
+import (
+ testexecutionv1 "github.com/kubeshop/testkube-operator/api/testexecution/v1"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+)
+
+// MapAPIToCRD maps OpenAPI spec Execution to CRD TestExecutionStatus
+func MapAPIToCRD(sourceRequest *testkube.Execution,
+ destinationRequest *testexecutionv1.TestExecutionStatus) *testexecutionv1.TestExecutionStatus {
+ if sourceRequest == nil || destinationRequest == nil {
+ return destinationRequest
+ }
+
+ if destinationRequest.LatestExecution != nil {
+ destinationRequest.LatestExecution.ExecutionNamespace = sourceRequest.ExecutionNamespace
+ }
+
+ return destinationRequest
+}
diff --git a/pkg/tcl/mappertcl/tests/kube_openapi.go b/pkg/tcl/mappertcl/tests/kube_openapi.go
new file mode 100644
index 00000000000..360df1f93a5
--- /dev/null
+++ b/pkg/tcl/mappertcl/tests/kube_openapi.go
@@ -0,0 +1,35 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package tests
+
+import (
+ testsv3 "github.com/kubeshop/testkube-operator/api/tests/v3"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+)
+
+// MapExecutionRequestFromSpec maps CRD to OpenAPI spec ExecutionREquest
+func MapExecutionRequestFromSpec(sourceRequest *testsv3.ExecutionRequest,
+ destinationRequest *testkube.ExecutionRequest) *testkube.ExecutionRequest {
+ if sourceRequest == nil || destinationRequest == nil {
+ return destinationRequest
+ }
+
+ destinationRequest.ExecutionNamespace = sourceRequest.ExecutionNamespace
+ return destinationRequest
+}
+
+// MapSpecExecutionRequestToExecutionUpdateRequest maps ExecutionRequest CRD spec to ExecutionUpdateRequest OpenAPI spec to
+func MapSpecExecutionRequestToExecutionUpdateRequest(
+ sourceRequest *testsv3.ExecutionRequest, destinationRequest *testkube.ExecutionUpdateRequest) {
+ if sourceRequest == nil || destinationRequest == nil {
+ return
+ }
+
+ destinationRequest.ExecutionNamespace = &sourceRequest.ExecutionNamespace
+}
diff --git a/pkg/tcl/mappertcl/tests/openapi_kube.go b/pkg/tcl/mappertcl/tests/openapi_kube.go
new file mode 100644
index 00000000000..bb531e99cad
--- /dev/null
+++ b/pkg/tcl/mappertcl/tests/openapi_kube.go
@@ -0,0 +1,40 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package tests
+
+import (
+ testsv3 "github.com/kubeshop/testkube-operator/api/tests/v3"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+)
+
+// MapExecutionRequestToSpecExecutionRequest maps ExecutionRequest OpenAPI spec to ExecutionRequest CRD spec
+func MapExecutionRequestToSpecExecutionRequest(sourceRequest *testkube.ExecutionRequest,
+ destinationRequest *testsv3.ExecutionRequest) *testsv3.ExecutionRequest {
+ if sourceRequest == nil || destinationRequest == nil {
+ return destinationRequest
+ }
+
+ destinationRequest.ExecutionNamespace = sourceRequest.ExecutionNamespace
+ return destinationRequest
+}
+
+// MapExecutionUpdateRequestToSpecExecutionRequest maps ExecutionUpdateRequest OpenAPI spec to ExecutionRequest CRD spec
+func MapExecutionUpdateRequestToSpecExecutionRequest(sourceRequest *testkube.ExecutionUpdateRequest,
+ destinationRequest *testsv3.ExecutionRequest) bool {
+ if sourceRequest == nil || destinationRequest == nil {
+ return true
+ }
+
+ if sourceRequest.ExecutionNamespace != nil {
+ destinationRequest.ExecutionNamespace = *sourceRequest.ExecutionNamespace
+ return false
+ }
+
+ return true
+}
diff --git a/pkg/tcl/mappertcl/testsuiteexecutions/mapper.go b/pkg/tcl/mappertcl/testsuiteexecutions/mapper.go
new file mode 100644
index 00000000000..d06e31b8d47
--- /dev/null
+++ b/pkg/tcl/mappertcl/testsuiteexecutions/mapper.go
@@ -0,0 +1,25 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testsuiteexecutions
+
+import (
+ testsuiteexecutionv1 "github.com/kubeshop/testkube-operator/api/testsuiteexecution/v1"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+)
+
+// MapExecutionCRD maps OpenAPI spec Execution to CRD
+func MapExecutionCRD(sourceRequest *testkube.Execution,
+ destinationRequest *testsuiteexecutionv1.Execution) *testsuiteexecutionv1.Execution {
+ if sourceRequest == nil || destinationRequest == nil {
+ return destinationRequest
+ }
+
+ destinationRequest.ExecutionNamespace = sourceRequest.ExecutionNamespace
+ return destinationRequest
+}
diff --git a/pkg/tcl/repositorytcl/testworkflow/filter.go b/pkg/tcl/repositorytcl/testworkflow/filter.go
new file mode 100644
index 00000000000..91c90f7bd96
--- /dev/null
+++ b/pkg/tcl/repositorytcl/testworkflow/filter.go
@@ -0,0 +1,140 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflow
+
+import (
+ "time"
+
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+)
+
+type FilterImpl struct {
+ FName string
+ FLastNDays int
+ FStartDate *time.Time
+ FEndDate *time.Time
+ FStatuses []testkube.TestWorkflowStatus
+ FPage int
+ FPageSize int
+ FTextSearch string
+ FSelector string
+}
+
+func NewExecutionsFilter() *FilterImpl {
+ result := FilterImpl{FPage: 0, FPageSize: PageDefaultLimit}
+ return &result
+}
+
+func (f *FilterImpl) WithName(name string) *FilterImpl {
+ f.FName = name
+ return f
+}
+
+func (f *FilterImpl) WithLastNDays(days int) *FilterImpl {
+ f.FLastNDays = days
+ return f
+}
+
+func (f *FilterImpl) WithStartDate(date time.Time) *FilterImpl {
+ f.FStartDate = &date
+ return f
+}
+
+func (f *FilterImpl) WithEndDate(date time.Time) *FilterImpl {
+ f.FEndDate = &date
+ return f
+}
+
+func (f *FilterImpl) WithStatus(status string) *FilterImpl {
+ statuses, err := testkube.ParseTestWorkflowStatusList(status, ",")
+ if err == nil {
+ f.FStatuses = statuses
+ }
+ return f
+}
+
+func (f *FilterImpl) WithPage(page int) *FilterImpl {
+ f.FPage = page
+ return f
+}
+
+func (f *FilterImpl) WithPageSize(pageSize int) *FilterImpl {
+ f.FPageSize = pageSize
+ return f
+}
+
+func (f *FilterImpl) WithTextSearch(textSearch string) *FilterImpl {
+ f.FTextSearch = textSearch
+ return f
+}
+
+func (f *FilterImpl) WithSelector(selector string) *FilterImpl {
+ f.FSelector = selector
+ return f
+}
+
+func (f FilterImpl) Name() string {
+ return f.FName
+}
+
+func (f FilterImpl) NameDefined() bool {
+ return f.FName != ""
+}
+
+func (f FilterImpl) LastNDaysDefined() bool {
+ return f.FLastNDays > 0
+}
+
+func (f FilterImpl) LastNDays() int {
+ return f.FLastNDays
+}
+
+func (f FilterImpl) StartDateDefined() bool {
+ return f.FStartDate != nil
+}
+
+func (f FilterImpl) StartDate() time.Time {
+ return *f.FStartDate
+}
+
+func (f FilterImpl) EndDateDefined() bool {
+ return f.FEndDate != nil
+}
+
+func (f FilterImpl) EndDate() time.Time {
+ return *f.FEndDate
+}
+
+func (f FilterImpl) StatusesDefined() bool {
+ return len(f.FStatuses) != 0
+}
+
+func (f FilterImpl) Statuses() []testkube.TestWorkflowStatus {
+ return f.FStatuses
+}
+
+func (f FilterImpl) Page() int {
+ return f.FPage
+}
+
+func (f FilterImpl) PageSize() int {
+ return f.FPageSize
+}
+
+func (f FilterImpl) TextSearchDefined() bool {
+ return f.FTextSearch != ""
+}
+
+func (f FilterImpl) TextSearch() string {
+ return f.FTextSearch
+}
+
+func (f FilterImpl) Selector() string {
+ return f.FSelector
+}
diff --git a/pkg/tcl/repositorytcl/testworkflow/interface.go b/pkg/tcl/repositorytcl/testworkflow/interface.go
new file mode 100644
index 00000000000..431c49cbf1d
--- /dev/null
+++ b/pkg/tcl/repositorytcl/testworkflow/interface.go
@@ -0,0 +1,87 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflow
+
+import (
+ "context"
+ "io"
+ "time"
+
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+)
+
+const PageDefaultLimit int = 100
+
+type Filter interface {
+ Name() string
+ NameDefined() bool
+ LastNDays() int
+ LastNDaysDefined() bool
+ StartDate() time.Time
+ StartDateDefined() bool
+ EndDate() time.Time
+ EndDateDefined() bool
+ Statuses() []testkube.TestWorkflowStatus
+ StatusesDefined() bool
+ Page() int
+ PageSize() int
+ TextSearchDefined() bool
+ TextSearch() string
+ Selector() string
+}
+
+//go:generate mockgen -destination=./mock_repository.go -package=testworkflow "github.com/kubeshop/testkube/pkg/tcl/repositorytcl/testworkflow" Repository
+type Repository interface {
+ // Get gets execution result by id or name
+ Get(ctx context.Context, id string) (testkube.TestWorkflowExecution, error)
+ // GetByNameAndTestWorkflow gets execution result by name
+ GetByNameAndTestWorkflow(ctx context.Context, name, workflowName string) (testkube.TestWorkflowExecution, error)
+ // GetLatestByTestWorkflow gets latest execution result by workflow
+ GetLatestByTestWorkflow(ctx context.Context, workflowName string) (*testkube.TestWorkflowExecution, error)
+ // GetRunning get list of executions that are still running
+ GetRunning(ctx context.Context) ([]testkube.TestWorkflowExecution, error)
+ // GetLatestByTestWorkflows gets latest execution results by workflow names
+ GetLatestByTestWorkflows(ctx context.Context, workflowNames []string) (executions []testkube.TestWorkflowExecutionSummary, err error)
+ // GetExecutionsTotals gets executions total stats using a filter, use filter with no data for all
+ GetExecutionsTotals(ctx context.Context, filter ...Filter) (totals testkube.ExecutionsTotals, err error)
+ // GetExecutions gets executions using a filter, use filter with no data for all
+ GetExecutions(ctx context.Context, filter Filter) ([]testkube.TestWorkflowExecution, error)
+ // GetExecutions gets executions using a filter, use filter with no data for all
+ GetExecutionsSummary(ctx context.Context, filter Filter) ([]testkube.TestWorkflowExecutionSummary, error)
+ // Insert inserts new execution result
+ Insert(ctx context.Context, result testkube.TestWorkflowExecution) error
+ // Update updates execution
+ Update(ctx context.Context, result testkube.TestWorkflowExecution) error
+ // UpdateResult updates execution result
+ UpdateResult(ctx context.Context, id string, result *testkube.TestWorkflowResult) (err error)
+ // UpdateOutput updates list of output references in the execution result
+ UpdateOutput(ctx context.Context, id string, output []testkube.TestWorkflowOutput) (err error)
+ // DeleteByTestWorkflow deletes execution results by workflow
+ DeleteByTestWorkflow(ctx context.Context, workflowName string) error
+ // DeleteAll deletes all execution results
+ DeleteAll(ctx context.Context) error
+ // DeleteByTestWorkflows deletes execution results by workflows
+ DeleteByTestWorkflows(ctx context.Context, workflowNames []string) (err error)
+ // GetTestWorkflowMetrics get metrics based on the TestWorkflow results
+ GetTestWorkflowMetrics(ctx context.Context, name string, limit, last int) (metrics testkube.ExecutionsMetrics, err error)
+}
+
+//go:generate mockgen -destination=./mock_output_repository.go -package=testworkflow "github.com/kubeshop/testkube/pkg/tcl/repositorytcl/testworkflow" OutputRepository
+type OutputRepository interface {
+ // PresignSaveLog builds presigned storage URL to save the output in Minio
+ PresignSaveLog(ctx context.Context, id, workflowName string) (string, error)
+ // PresignReadLog builds presigned storage URL to read the output from Minio
+ PresignReadLog(ctx context.Context, id, workflowName string) (string, error)
+ // SaveLog streams the output from the workflow to Minio
+ SaveLog(ctx context.Context, id, workflowName string, reader io.Reader) error
+ // ReadLog streams the output from Minio
+ ReadLog(ctx context.Context, id, workflowName string) (io.Reader, error)
+ // HasLog checks if there is an output in Minio
+ HasLog(ctx context.Context, id, workflowName string) (bool, error)
+}
diff --git a/pkg/tcl/repositorytcl/testworkflow/minio_output_repository.go b/pkg/tcl/repositorytcl/testworkflow/minio_output_repository.go
new file mode 100644
index 00000000000..5802dc4b0e1
--- /dev/null
+++ b/pkg/tcl/repositorytcl/testworkflow/minio_output_repository.go
@@ -0,0 +1,68 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflow
+
+import (
+ "context"
+ "io"
+ "time"
+
+ "github.com/kubeshop/testkube/pkg/log"
+ "github.com/kubeshop/testkube/pkg/storage"
+)
+
+var _ OutputRepository = (*MinioRepository)(nil)
+
+const bucketFolder = "testworkflows"
+
+type MinioRepository struct {
+ storage storage.Client
+ bucket string
+}
+
+func NewMinioOutputRepository(storageClient storage.Client, bucket string) *MinioRepository {
+ log.DefaultLogger.Debugw("creating minio workflow output repository", "bucket", bucket)
+ return &MinioRepository{
+ storage: storageClient,
+ bucket: bucket,
+ }
+}
+
+// PresignSaveLog builds presigned storage URL to save the output in Cloud
+func (m *MinioRepository) PresignSaveLog(ctx context.Context, id, workflowName string) (string, error) {
+ return m.storage.PresignUploadFileToBucket(ctx, m.bucket, bucketFolder, id, 24*time.Hour)
+}
+
+// PresignReadLog builds presigned storage URL to read the output from Cloud
+func (m *MinioRepository) PresignReadLog(ctx context.Context, id, workflowName string) (string, error) {
+ return m.storage.PresignDownloadFileFromBucket(ctx, m.bucket, bucketFolder, id, 15*time.Minute)
+}
+
+func (m *MinioRepository) SaveLog(ctx context.Context, id, workflowName string, reader io.Reader) error {
+ log.DefaultLogger.Debugw("inserting output", "id", id, "workflowName", workflowName)
+ return m.storage.UploadFileToBucket(ctx, m.bucket, bucketFolder, id, reader, -1)
+}
+
+func (m *MinioRepository) ReadLog(ctx context.Context, id, workflowName string) (io.Reader, error) {
+ file, _, err := m.storage.DownloadFileFromBucket(ctx, m.bucket, bucketFolder, id)
+ if err != nil {
+ return nil, err
+ }
+ return file, nil
+}
+
+func (m *MinioRepository) HasLog(ctx context.Context, id, workflowName string) (bool, error) {
+ subCtx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ _, _, err := m.storage.DownloadFileFromBucket(subCtx, m.bucket, bucketFolder, id)
+ if err != nil {
+ return false, err
+ }
+ return true, nil
+}
diff --git a/pkg/tcl/repositorytcl/testworkflow/mock_output_repository.go b/pkg/tcl/repositorytcl/testworkflow/mock_output_repository.go
new file mode 100644
index 00000000000..bc01a3f0fbb
--- /dev/null
+++ b/pkg/tcl/repositorytcl/testworkflow/mock_output_repository.go
@@ -0,0 +1,110 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/tcl/repositorytcl/testworkflow (interfaces: OutputRepository)
+
+// Package testworkflow is a generated GoMock package.
+package testworkflow
+
+import (
+ context "context"
+ io "io"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+)
+
+// MockOutputRepository is a mock of OutputRepository interface.
+type MockOutputRepository struct {
+ ctrl *gomock.Controller
+ recorder *MockOutputRepositoryMockRecorder
+}
+
+// MockOutputRepositoryMockRecorder is the mock recorder for MockOutputRepository.
+type MockOutputRepositoryMockRecorder struct {
+ mock *MockOutputRepository
+}
+
+// NewMockOutputRepository creates a new mock instance.
+func NewMockOutputRepository(ctrl *gomock.Controller) *MockOutputRepository {
+ mock := &MockOutputRepository{ctrl: ctrl}
+ mock.recorder = &MockOutputRepositoryMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockOutputRepository) EXPECT() *MockOutputRepositoryMockRecorder {
+ return m.recorder
+}
+
+// HasLog mocks base method.
+func (m *MockOutputRepository) HasLog(arg0 context.Context, arg1, arg2 string) (bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "HasLog", arg0, arg1, arg2)
+ ret0, _ := ret[0].(bool)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// HasLog indicates an expected call of HasLog.
+func (mr *MockOutputRepositoryMockRecorder) HasLog(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasLog", reflect.TypeOf((*MockOutputRepository)(nil).HasLog), arg0, arg1, arg2)
+}
+
+// PresignReadLog mocks base method.
+func (m *MockOutputRepository) PresignReadLog(arg0 context.Context, arg1, arg2 string) (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PresignReadLog", arg0, arg1, arg2)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// PresignReadLog indicates an expected call of PresignReadLog.
+func (mr *MockOutputRepositoryMockRecorder) PresignReadLog(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PresignReadLog", reflect.TypeOf((*MockOutputRepository)(nil).PresignReadLog), arg0, arg1, arg2)
+}
+
+// PresignSaveLog mocks base method.
+func (m *MockOutputRepository) PresignSaveLog(arg0 context.Context, arg1, arg2 string) (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PresignSaveLog", arg0, arg1, arg2)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// PresignSaveLog indicates an expected call of PresignSaveLog.
+func (mr *MockOutputRepositoryMockRecorder) PresignSaveLog(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PresignSaveLog", reflect.TypeOf((*MockOutputRepository)(nil).PresignSaveLog), arg0, arg1, arg2)
+}
+
+// ReadLog mocks base method.
+func (m *MockOutputRepository) ReadLog(arg0 context.Context, arg1, arg2 string) (io.Reader, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ReadLog", arg0, arg1, arg2)
+ ret0, _ := ret[0].(io.Reader)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ReadLog indicates an expected call of ReadLog.
+func (mr *MockOutputRepositoryMockRecorder) ReadLog(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadLog", reflect.TypeOf((*MockOutputRepository)(nil).ReadLog), arg0, arg1, arg2)
+}
+
+// SaveLog mocks base method.
+func (m *MockOutputRepository) SaveLog(arg0 context.Context, arg1, arg2 string, arg3 io.Reader) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SaveLog", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// SaveLog indicates an expected call of SaveLog.
+func (mr *MockOutputRepositoryMockRecorder) SaveLog(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveLog", reflect.TypeOf((*MockOutputRepository)(nil).SaveLog), arg0, arg1, arg2, arg3)
+}
diff --git a/pkg/tcl/repositorytcl/testworkflow/mock_repository.go b/pkg/tcl/repositorytcl/testworkflow/mock_repository.go
new file mode 100644
index 00000000000..d0a58a348a2
--- /dev/null
+++ b/pkg/tcl/repositorytcl/testworkflow/mock_repository.go
@@ -0,0 +1,274 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/tcl/repositorytcl/testworkflow (interfaces: Repository)
+
+// Package testworkflow is a generated GoMock package.
+package testworkflow
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ testkube "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+)
+
+// MockRepository is a mock of Repository interface.
+type MockRepository struct {
+ ctrl *gomock.Controller
+ recorder *MockRepositoryMockRecorder
+}
+
+// MockRepositoryMockRecorder is the mock recorder for MockRepository.
+type MockRepositoryMockRecorder struct {
+ mock *MockRepository
+}
+
+// NewMockRepository creates a new mock instance.
+func NewMockRepository(ctrl *gomock.Controller) *MockRepository {
+ mock := &MockRepository{ctrl: ctrl}
+ mock.recorder = &MockRepositoryMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
+ return m.recorder
+}
+
+// DeleteAll mocks base method.
+func (m *MockRepository) DeleteAll(arg0 context.Context) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeleteAll", arg0)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeleteAll indicates an expected call of DeleteAll.
+func (mr *MockRepositoryMockRecorder) DeleteAll(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAll", reflect.TypeOf((*MockRepository)(nil).DeleteAll), arg0)
+}
+
+// DeleteByTestWorkflow mocks base method.
+func (m *MockRepository) DeleteByTestWorkflow(arg0 context.Context, arg1 string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeleteByTestWorkflow", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeleteByTestWorkflow indicates an expected call of DeleteByTestWorkflow.
+func (mr *MockRepositoryMockRecorder) DeleteByTestWorkflow(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteByTestWorkflow", reflect.TypeOf((*MockRepository)(nil).DeleteByTestWorkflow), arg0, arg1)
+}
+
+// DeleteByTestWorkflows mocks base method.
+func (m *MockRepository) DeleteByTestWorkflows(arg0 context.Context, arg1 []string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeleteByTestWorkflows", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeleteByTestWorkflows indicates an expected call of DeleteByTestWorkflows.
+func (mr *MockRepositoryMockRecorder) DeleteByTestWorkflows(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteByTestWorkflows", reflect.TypeOf((*MockRepository)(nil).DeleteByTestWorkflows), arg0, arg1)
+}
+
+// Get mocks base method.
+func (m *MockRepository) Get(arg0 context.Context, arg1 string) (testkube.TestWorkflowExecution, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Get", arg0, arg1)
+ ret0, _ := ret[0].(testkube.TestWorkflowExecution)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockRepositoryMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRepository)(nil).Get), arg0, arg1)
+}
+
+// GetByNameAndTestWorkflow mocks base method.
+func (m *MockRepository) GetByNameAndTestWorkflow(arg0 context.Context, arg1, arg2 string) (testkube.TestWorkflowExecution, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetByNameAndTestWorkflow", arg0, arg1, arg2)
+ ret0, _ := ret[0].(testkube.TestWorkflowExecution)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetByNameAndTestWorkflow indicates an expected call of GetByNameAndTestWorkflow.
+func (mr *MockRepositoryMockRecorder) GetByNameAndTestWorkflow(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByNameAndTestWorkflow", reflect.TypeOf((*MockRepository)(nil).GetByNameAndTestWorkflow), arg0, arg1, arg2)
+}
+
+// GetExecutions mocks base method.
+func (m *MockRepository) GetExecutions(arg0 context.Context, arg1 Filter) ([]testkube.TestWorkflowExecution, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetExecutions", arg0, arg1)
+ ret0, _ := ret[0].([]testkube.TestWorkflowExecution)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetExecutions indicates an expected call of GetExecutions.
+func (mr *MockRepositoryMockRecorder) GetExecutions(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExecutions", reflect.TypeOf((*MockRepository)(nil).GetExecutions), arg0, arg1)
+}
+
+// GetExecutionsSummary mocks base method.
+func (m *MockRepository) GetExecutionsSummary(arg0 context.Context, arg1 Filter) ([]testkube.TestWorkflowExecutionSummary, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetExecutionsSummary", arg0, arg1)
+ ret0, _ := ret[0].([]testkube.TestWorkflowExecutionSummary)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetExecutionsSummary indicates an expected call of GetExecutionsSummary.
+func (mr *MockRepositoryMockRecorder) GetExecutionsSummary(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExecutionsSummary", reflect.TypeOf((*MockRepository)(nil).GetExecutionsSummary), arg0, arg1)
+}
+
+// GetExecutionsTotals mocks base method.
+func (m *MockRepository) GetExecutionsTotals(arg0 context.Context, arg1 ...Filter) (testkube.ExecutionsTotals, error) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0}
+ for _, a := range arg1 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "GetExecutionsTotals", varargs...)
+ ret0, _ := ret[0].(testkube.ExecutionsTotals)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetExecutionsTotals indicates an expected call of GetExecutionsTotals.
+func (mr *MockRepositoryMockRecorder) GetExecutionsTotals(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0}, arg1...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExecutionsTotals", reflect.TypeOf((*MockRepository)(nil).GetExecutionsTotals), varargs...)
+}
+
+// GetLatestByTestWorkflow mocks base method.
+func (m *MockRepository) GetLatestByTestWorkflow(arg0 context.Context, arg1 string) (*testkube.TestWorkflowExecution, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetLatestByTestWorkflow", arg0, arg1)
+ ret0, _ := ret[0].(*testkube.TestWorkflowExecution)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetLatestByTestWorkflow indicates an expected call of GetLatestByTestWorkflow.
+func (mr *MockRepositoryMockRecorder) GetLatestByTestWorkflow(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestByTestWorkflow", reflect.TypeOf((*MockRepository)(nil).GetLatestByTestWorkflow), arg0, arg1)
+}
+
+// GetLatestByTestWorkflows mocks base method.
+func (m *MockRepository) GetLatestByTestWorkflows(arg0 context.Context, arg1 []string) ([]testkube.TestWorkflowExecutionSummary, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetLatestByTestWorkflows", arg0, arg1)
+ ret0, _ := ret[0].([]testkube.TestWorkflowExecutionSummary)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetLatestByTestWorkflows indicates an expected call of GetLatestByTestWorkflows.
+func (mr *MockRepositoryMockRecorder) GetLatestByTestWorkflows(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestByTestWorkflows", reflect.TypeOf((*MockRepository)(nil).GetLatestByTestWorkflows), arg0, arg1)
+}
+
+// GetRunning mocks base method.
+func (m *MockRepository) GetRunning(arg0 context.Context) ([]testkube.TestWorkflowExecution, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetRunning", arg0)
+ ret0, _ := ret[0].([]testkube.TestWorkflowExecution)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetRunning indicates an expected call of GetRunning.
+func (mr *MockRepositoryMockRecorder) GetRunning(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRunning", reflect.TypeOf((*MockRepository)(nil).GetRunning), arg0)
+}
+
+// GetTestWorkflowMetrics mocks base method.
+func (m *MockRepository) GetTestWorkflowMetrics(arg0 context.Context, arg1 string, arg2, arg3 int) (testkube.ExecutionsMetrics, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetTestWorkflowMetrics", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(testkube.ExecutionsMetrics)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetTestWorkflowMetrics indicates an expected call of GetTestWorkflowMetrics.
+func (mr *MockRepositoryMockRecorder) GetTestWorkflowMetrics(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTestWorkflowMetrics", reflect.TypeOf((*MockRepository)(nil).GetTestWorkflowMetrics), arg0, arg1, arg2, arg3)
+}
+
+// Insert mocks base method.
+func (m *MockRepository) Insert(arg0 context.Context, arg1 testkube.TestWorkflowExecution) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Insert", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Insert indicates an expected call of Insert.
+func (mr *MockRepositoryMockRecorder) Insert(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockRepository)(nil).Insert), arg0, arg1)
+}
+
+// Update mocks base method.
+func (m *MockRepository) Update(arg0 context.Context, arg1 testkube.TestWorkflowExecution) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Update", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Update indicates an expected call of Update.
+func (mr *MockRepositoryMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), arg0, arg1)
+}
+
+// UpdateOutput mocks base method.
+func (m *MockRepository) UpdateOutput(arg0 context.Context, arg1 string, arg2 []testkube.TestWorkflowOutput) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UpdateOutput", arg0, arg1, arg2)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// UpdateOutput indicates an expected call of UpdateOutput.
+func (mr *MockRepositoryMockRecorder) UpdateOutput(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOutput", reflect.TypeOf((*MockRepository)(nil).UpdateOutput), arg0, arg1, arg2)
+}
+
+// UpdateResult mocks base method.
+func (m *MockRepository) UpdateResult(arg0 context.Context, arg1 string, arg2 *testkube.TestWorkflowResult) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UpdateResult", arg0, arg1, arg2)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// UpdateResult indicates an expected call of UpdateResult.
+func (mr *MockRepositoryMockRecorder) UpdateResult(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateResult", reflect.TypeOf((*MockRepository)(nil).UpdateResult), arg0, arg1, arg2)
+}
diff --git a/pkg/tcl/repositorytcl/testworkflow/mongo.go b/pkg/tcl/repositorytcl/testworkflow/mongo.go
new file mode 100644
index 00000000000..9cfd8529ff3
--- /dev/null
+++ b/pkg/tcl/repositorytcl/testworkflow/mongo.go
@@ -0,0 +1,428 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflow
+
+import (
+ "context"
+ "strings"
+ "time"
+
+ "github.com/kubeshop/testkube/pkg/repository/common"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+)
+
+var _ Repository = (*MongoRepository)(nil)
+
+const CollectionName = "testworkflowresults"
+
+func NewMongoRepository(db *mongo.Database, allowDiskUse bool, opts ...MongoRepositoryOpt) *MongoRepository {
+ r := &MongoRepository{
+ db: db,
+ Coll: db.Collection(CollectionName),
+ allowDiskUse: allowDiskUse,
+ }
+
+ for _, opt := range opts {
+ opt(r)
+ }
+
+ return r
+}
+
+type MongoRepository struct {
+ db *mongo.Database
+ Coll *mongo.Collection
+ allowDiskUse bool
+}
+
+func WithMongoRepositoryCollection(collection *mongo.Collection) MongoRepositoryOpt {
+ return func(r *MongoRepository) {
+ r.Coll = collection
+ }
+}
+
+type MongoRepositoryOpt func(*MongoRepository)
+
+func (r *MongoRepository) Get(ctx context.Context, id string) (result testkube.TestWorkflowExecution, err error) {
+ err = r.Coll.FindOne(ctx, bson.M{"$or": bson.A{bson.M{"id": id}, bson.M{"name": id}}}).Decode(&result)
+ return *result.UnscapeDots(), err
+}
+
+func (r *MongoRepository) GetByNameAndTestWorkflow(ctx context.Context, name, workflowName string) (result testkube.TestWorkflowExecution, err error) {
+ err = r.Coll.FindOne(ctx, bson.M{"$or": bson.A{bson.M{"id": name}, bson.M{"name": name}}, "workflow.name": workflowName}).Decode(&result)
+ return *result.UnscapeDots(), err
+}
+
+func (r *MongoRepository) GetLatestByTestWorkflow(ctx context.Context, workflowName string) (*testkube.TestWorkflowExecution, error) {
+ opts := options.Aggregate()
+ pipeline := []bson.M{
+ {"$sort": bson.M{"statusat": -1}},
+ {"$match": bson.M{"workflow.name": workflowName}},
+ {"$limit": 1},
+ }
+ cursor, err := r.Coll.Aggregate(ctx, pipeline, opts)
+ if err != nil {
+ return nil, err
+ }
+ var items []testkube.TestWorkflowExecution
+ err = cursor.All(ctx, &items)
+ if err != nil {
+ return nil, err
+ }
+ if len(items) == 0 {
+ return nil, mongo.ErrNoDocuments
+ }
+ return items[0].UnscapeDots(), err
+}
+
+func (r *MongoRepository) GetLatestByTestWorkflows(ctx context.Context, workflowNames []string) (executions []testkube.TestWorkflowExecutionSummary, err error) {
+ if len(workflowNames) == 0 {
+ return executions, nil
+ }
+
+ documents := bson.A{}
+ for _, workflowName := range workflowNames {
+ documents = append(documents, bson.M{"workflow.name": workflowName})
+ }
+
+ pipeline := []bson.M{
+ {"$sort": bson.M{"statusat": -1}},
+ {"$project": bson.M{
+ "_id": 0,
+ "output": 0,
+ "signature": 0,
+ "result.steps": 0,
+ "result.initialization": 0,
+ "workflow.spec": 0,
+ "resolvedWorkflow": 0,
+ }},
+ {"$match": bson.M{"$or": documents}},
+ {"$group": bson.M{"_id": "$workflow.name", "execution": bson.M{"$first": "$$ROOT"}}},
+ {"$replaceRoot": bson.M{"newRoot": "$execution"}},
+ }
+
+ opts := options.Aggregate()
+ if r.allowDiskUse {
+ opts.SetAllowDiskUse(r.allowDiskUse)
+ }
+
+ cursor, err := r.Coll.Aggregate(ctx, pipeline, opts)
+ if err != nil {
+ return nil, err
+ }
+ err = cursor.All(ctx, &executions)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(executions) == 0 {
+ return executions, nil
+ }
+
+ for i := range executions {
+ executions[i].UnscapeDots()
+ }
+ return executions, nil
+}
+
+// TODO: Add limit?
+func (r *MongoRepository) GetRunning(ctx context.Context) (result []testkube.TestWorkflowExecution, err error) {
+ result = make([]testkube.TestWorkflowExecution, 0)
+ opts := &options.FindOptions{}
+ opts.SetSort(bson.D{{Key: "_id", Value: -1}})
+ if r.allowDiskUse {
+ opts.SetAllowDiskUse(r.allowDiskUse)
+ }
+
+ cursor, err := r.Coll.Find(ctx, bson.M{
+ "$or": bson.A{
+ bson.M{"result.status": testkube.RUNNING_TestWorkflowStatus},
+ bson.M{"result.status": testkube.QUEUED_TestWorkflowStatus},
+ },
+ }, opts)
+ if err != nil {
+ return result, err
+ }
+ err = cursor.All(ctx, &result)
+
+ for i := range result {
+ result[i].UnscapeDots()
+ }
+ return
+}
+
+func (r *MongoRepository) GetExecutionsTotals(ctx context.Context, filter ...Filter) (totals testkube.ExecutionsTotals, err error) {
+ var result []struct {
+ Status string `bson:"_id"`
+ Count int `bson:"count"`
+ }
+
+ query := bson.M{}
+ if len(filter) > 0 {
+ query, _ = composeQueryAndOpts(filter[0])
+ }
+
+ pipeline := []bson.D{{{Key: "$match", Value: query}}}
+ if len(filter) > 0 {
+ pipeline = append(pipeline, bson.D{{Key: "$sort", Value: bson.D{{Key: "statusat", Value: -1}}}})
+ pipeline = append(pipeline, bson.D{{Key: "$skip", Value: int64(filter[0].Page() * filter[0].PageSize())}})
+ pipeline = append(pipeline, bson.D{{Key: "$limit", Value: int64(filter[0].PageSize())}})
+ }
+
+ pipeline = append(pipeline, bson.D{{Key: "$group", Value: bson.D{{Key: "_id", Value: "$result.status"},
+ {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}}}})
+
+ opts := options.Aggregate()
+ if r.allowDiskUse {
+ opts.SetAllowDiskUse(r.allowDiskUse)
+ }
+
+ cursor, err := r.Coll.Aggregate(ctx, pipeline, opts)
+ if err != nil {
+ return totals, err
+ }
+ err = cursor.All(ctx, &result)
+ if err != nil {
+ return totals, err
+ }
+
+ var sum int32
+
+ for _, o := range result {
+ sum += int32(o.Count)
+ switch testkube.TestWorkflowStatus(o.Status) {
+ case testkube.QUEUED_TestWorkflowStatus:
+ totals.Queued = int32(o.Count)
+ case testkube.RUNNING_TestWorkflowStatus:
+ totals.Running = int32(o.Count)
+ case testkube.PASSED_TestWorkflowStatus:
+ totals.Passed = int32(o.Count)
+ case testkube.FAILED_TestWorkflowStatus, testkube.ABORTED_TestWorkflowStatus:
+ totals.Failed = int32(o.Count)
+ }
+ }
+ totals.Results = sum
+
+ return
+}
+
+func (r *MongoRepository) GetExecutions(ctx context.Context, filter Filter) (result []testkube.TestWorkflowExecution, err error) {
+ result = make([]testkube.TestWorkflowExecution, 0)
+ query, opts := composeQueryAndOpts(filter)
+ if r.allowDiskUse {
+ opts.SetAllowDiskUse(r.allowDiskUse)
+ }
+
+ cursor, err := r.Coll.Find(ctx, query, opts)
+ if err != nil {
+ return
+ }
+ err = cursor.All(ctx, &result)
+
+ for i := range result {
+ result[i].UnscapeDots()
+ }
+ return
+}
+
+func (r *MongoRepository) GetExecutionsSummary(ctx context.Context, filter Filter) (result []testkube.TestWorkflowExecutionSummary, err error) {
+ result = make([]testkube.TestWorkflowExecutionSummary, 0)
+ query, opts := composeQueryAndOpts(filter)
+ if r.allowDiskUse {
+ opts.SetAllowDiskUse(r.allowDiskUse)
+ }
+
+ opts = opts.SetProjection(bson.M{
+ "_id": 0,
+ "output": 0,
+ "signature": 0,
+ "result.steps": 0,
+ "result.initialization": 0,
+ "workflow.spec": 0,
+ "resolvedWorkflow": 0,
+ })
+ cursor, err := r.Coll.Find(ctx, query, opts)
+ if err != nil {
+ return
+ }
+ err = cursor.All(ctx, &result)
+
+ for i := range result {
+ result[i].UnscapeDots()
+ }
+ return
+}
+
+func (r *MongoRepository) Insert(ctx context.Context, result testkube.TestWorkflowExecution) (err error) {
+ result.EscapeDots()
+ _, err = r.Coll.InsertOne(ctx, result)
+ return
+}
+
+func (r *MongoRepository) Update(ctx context.Context, result testkube.TestWorkflowExecution) (err error) {
+ result.EscapeDots()
+ _, err = r.Coll.ReplaceOne(ctx, bson.M{"id": result.Id}, result)
+ return
+}
+
+func (r *MongoRepository) UpdateResult(ctx context.Context, id string, result *testkube.TestWorkflowResult) (err error) {
+ data := bson.M{"result": result}
+ if !result.FinishedAt.IsZero() {
+ data["statusat"] = result.FinishedAt
+ }
+ _, err = r.Coll.UpdateOne(ctx, bson.M{"id": id}, bson.M{"$set": data})
+ return
+}
+
+func (r *MongoRepository) UpdateOutput(ctx context.Context, id string, refs []testkube.TestWorkflowOutput) (err error) {
+ _, err = r.Coll.UpdateOne(ctx, bson.M{"id": id}, bson.M{"$set": bson.M{"output": refs}})
+ return
+}
+
+func composeQueryAndOpts(filter Filter) (bson.M, *options.FindOptions) {
+ query := bson.M{}
+ opts := options.Find()
+ startTimeQuery := bson.M{}
+
+ if filter.NameDefined() {
+ query["workflow.name"] = filter.Name()
+ }
+
+ if filter.TextSearchDefined() {
+ query["name"] = bson.M{"$regex": primitive.Regex{Pattern: filter.TextSearch(), Options: "i"}}
+ }
+
+ if filter.LastNDaysDefined() {
+ startTimeQuery["$gte"] = time.Now().Add(-time.Duration(filter.LastNDays()) * 24 * time.Hour)
+ }
+
+ if filter.StartDateDefined() {
+ startTimeQuery["$gte"] = filter.StartDate()
+ }
+
+ if filter.EndDateDefined() {
+ startTimeQuery["$lte"] = filter.EndDate()
+ }
+
+ if len(startTimeQuery) > 0 {
+ query["scheduledat"] = startTimeQuery
+ }
+
+ if filter.StatusesDefined() {
+ statuses := filter.Statuses()
+ if len(statuses) == 1 {
+ query["result.status"] = statuses[0]
+ } else {
+ var conditions bson.A
+ for _, status := range statuses {
+ conditions = append(conditions, bson.M{"result.status": status})
+ }
+
+ query["$or"] = conditions
+ }
+ }
+
+ if filter.Selector() != "" {
+ items := strings.Split(filter.Selector(), ",")
+ for _, item := range items {
+ elements := strings.Split(item, "=")
+ if len(elements) == 2 {
+ query["workflow.labels."+elements[0]] = elements[1]
+ } else if len(elements) == 1 {
+ query["workflow.labels."+elements[0]] = bson.M{"$exists": true}
+ }
+ }
+ }
+
+ opts.SetSkip(int64(filter.Page() * filter.PageSize()))
+ opts.SetLimit(int64(filter.PageSize()))
+ opts.SetSort(bson.D{{Key: "scheduledat", Value: -1}})
+
+ return query, opts
+}
+
+// DeleteByTestWorkflow deletes execution results by workflow
+func (r *MongoRepository) DeleteByTestWorkflow(ctx context.Context, workflowName string) (err error) {
+ _, err = r.Coll.DeleteMany(ctx, bson.M{"workflow.name": workflowName})
+ return
+}
+
+// DeleteAll deletes all execution results
+func (r *MongoRepository) DeleteAll(ctx context.Context) (err error) {
+ _, err = r.Coll.DeleteMany(ctx, bson.M{})
+ return
+}
+
+// DeleteByTestWorkflows deletes execution results by workflows
+func (r *MongoRepository) DeleteByTestWorkflows(ctx context.Context, workflowNames []string) (err error) {
+ if len(workflowNames) == 0 {
+ return nil
+ }
+
+ conditions := bson.A{}
+ for _, workflowName := range workflowNames {
+ conditions = append(conditions, bson.M{"workflow.name": workflowName})
+ }
+
+ filter := bson.M{"$or": conditions}
+
+ _, err = r.Coll.DeleteMany(ctx, filter)
+ return
+}
+
+// TODO: Avoid calculating for all executions in memory (same for tests/test suites)
+// GetTestWorkflowMetrics returns test executions metrics
+func (r *MongoRepository) GetTestWorkflowMetrics(ctx context.Context, name string, limit, last int) (metrics testkube.ExecutionsMetrics, err error) {
+ query := bson.M{"workflow.name": name}
+
+ if last > 0 {
+ query["scheduledat"] = bson.M{"$gte": time.Now().Add(-time.Duration(last) * 24 * time.Hour)}
+ }
+
+ pipeline := []bson.M{
+ {"$sort": bson.M{"scheduledat": -1}},
+ {"$match": query},
+ {"$project": bson.M{
+ "status": "$result.status",
+ "duration": "$result.duration",
+ "starttime": "$scheduledat",
+ "name": 1,
+ }},
+ }
+
+ opts := options.Aggregate()
+ if r.allowDiskUse {
+ opts.SetAllowDiskUse(r.allowDiskUse)
+ }
+
+ cursor, err := r.Coll.Aggregate(ctx, pipeline, opts)
+ if err != nil {
+ return metrics, err
+ }
+
+ var executions []testkube.ExecutionsMetricsExecutions
+ err = cursor.All(ctx, &executions)
+
+ if err != nil {
+ return metrics, err
+ }
+
+ metrics = common.CalculateMetrics(executions)
+ if limit > 0 && limit < len(metrics.Executions) {
+ metrics.Executions = metrics.Executions[:limit]
+ }
+
+ return metrics, nil
+}
diff --git a/pkg/tcl/schedulertcl/test_scheduler.go b/pkg/tcl/schedulertcl/test_scheduler.go
new file mode 100644
index 00000000000..4d019a9b67f
--- /dev/null
+++ b/pkg/tcl/schedulertcl/test_scheduler.go
@@ -0,0 +1,64 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package schedulertcl
+
+import (
+ "strings"
+
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+)
+
+// NewExecutionFromExecutionOptions creates new execution from execution options
+func NewExecutionFromExecutionOptions(request testkube.ExecutionRequest, execution testkube.Execution) testkube.Execution {
+ execution.ExecutionNamespace = request.ExecutionNamespace
+
+ return execution
+}
+
+// GetExecuteOptions returns execute options
+func GetExecuteOptions(sourceRequest *testkube.ExecutionRequest,
+ destinationRequest testkube.ExecutionRequest) testkube.ExecutionRequest {
+ if sourceRequest == nil {
+ return destinationRequest
+ }
+
+ if destinationRequest.ExecutionNamespace == "" && sourceRequest.ExecutionNamespace != "" {
+ destinationRequest.ExecutionNamespace = sourceRequest.ExecutionNamespace
+ }
+
+ if destinationRequest.ExecutionNamespace != "" {
+ destinationRequest.Namespace = destinationRequest.ExecutionNamespace
+ }
+
+ return destinationRequest
+}
+
+// HasExecutionNamespace checks whether execution has execution namespace
+func HasExecutionNamespace(request *testkube.ExecutionRequest) bool {
+ return request.ExecutionNamespace != ""
+}
+
+// GetServiceAccountNamesFromConfig returns service account names from config
+func GetServiceAccountNamesFromConfig(serviceAccountNames map[string]string, config string) map[string]string {
+ if serviceAccountNames == nil {
+ serviceAccountNames = make(map[string]string)
+ }
+
+ items := strings.Split(config, ",")
+ for _, item := range items {
+ elements := strings.Split(item, "=")
+ if len(elements) != 2 {
+ continue
+ }
+
+ serviceAccountNames[elements[0]] = elements[1]
+ }
+
+ return serviceAccountNames
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowcontroller/cleanup.go b/pkg/tcl/testworkflowstcl/testworkflowcontroller/cleanup.go
new file mode 100644
index 00000000000..57d41ebe085
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowcontroller/cleanup.go
@@ -0,0 +1,64 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowcontroller
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor"
+)
+
+func cleanupConfigMaps(ctx context.Context, clientSet kubernetes.Interface, namespace, id string) error {
+ return clientSet.CoreV1().ConfigMaps(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{
+ LabelSelector: fmt.Sprintf("%s=%s", testworkflowprocessor.ExecutionIdLabelName, id),
+ })
+}
+
+func cleanupSecrets(ctx context.Context, clientSet kubernetes.Interface, namespace, id string) error {
+ return clientSet.CoreV1().Secrets(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{
+ LabelSelector: fmt.Sprintf("%s=%s", testworkflowprocessor.ExecutionIdLabelName, id),
+ })
+}
+
+func cleanupPods(ctx context.Context, clientSet kubernetes.Interface, namespace, id string) error {
+ return clientSet.CoreV1().Pods(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{
+ LabelSelector: fmt.Sprintf("%s=%s", testworkflowprocessor.ExecutionIdLabelName, id),
+ })
+}
+
+func cleanupJobs(ctx context.Context, clientSet kubernetes.Interface, namespace, id string) error {
+ return clientSet.BatchV1().Jobs(namespace).DeleteCollection(ctx, metav1.DeleteOptions{
+ PropagationPolicy: common.Ptr(metav1.DeletePropagationBackground),
+ }, metav1.ListOptions{
+ LabelSelector: fmt.Sprintf("%s=%s", testworkflowprocessor.ExecutionIdLabelName, id),
+ })
+}
+
+func Cleanup(ctx context.Context, clientSet kubernetes.Interface, namespace, id string) error {
+ var errs []error
+ ops := []func(context.Context, kubernetes.Interface, string, string) error{
+ cleanupJobs,
+ cleanupPods,
+ cleanupConfigMaps,
+ cleanupSecrets,
+ }
+ for _, op := range ops {
+ err := op(ctx, clientSet, namespace, id)
+ if err != nil {
+ errs = append(errs, err)
+ }
+ }
+ return errors.Join(errs...)
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowcontroller/controller.go b/pkg/tcl/testworkflowstcl/testworkflowcontroller/controller.go
new file mode 100644
index 00000000000..84e0b3ed92d
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowcontroller/controller.go
@@ -0,0 +1,410 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowcontroller
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/pkg/errors"
+ batchv1 "k8s.io/api/batch/v1"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/client-go/kubernetes"
+
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor"
+)
+
+const (
+ JobRetrievalTimeout = 1 * time.Second
+ JobEventRetrievalTimeout = 1 * time.Second
+)
+
+var (
+ ErrJobAborted = errors.New("job was aborted")
+ ErrJobTimeout = errors.New("timeout retrieving job")
+)
+
+type Controller interface {
+ Abort(ctx context.Context) error
+ Cleanup(ctx context.Context) error
+ Watch(ctx context.Context) Watcher[Notification]
+}
+
+func New(parentCtx context.Context, clientSet kubernetes.Interface, namespace, id string, scheduledAt time.Time) (Controller, error) {
+ // Create local context for stopping all the processes
+ ctx, ctxCancel := context.WithCancel(parentCtx)
+
+ // Optimistically, start watching all the resources
+ job := WatchJob(ctx, clientSet, namespace, id, 1)
+ jobEvents := WatchJobEvents(ctx, clientSet, namespace, id, -1)
+ pod := WatchMainPod(ctx, clientSet, namespace, id, 1)
+ podEvents := WatchPodEventsByPodWatcher(ctx, clientSet, namespace, pod, -1)
+
+ // Ensure the main Job exists in the cluster,
+ // and obtain the signature
+ var sig []testworkflowprocessor.Signature
+ var err error
+ select {
+ case j := <-job.Any(ctx):
+ if j.Error != nil {
+ ctxCancel()
+ return nil, j.Error
+ }
+ sig, err = testworkflowprocessor.GetSignatureFromJSON([]byte(j.Value.Annotations[testworkflowprocessor.SignatureAnnotationName]))
+ if err != nil {
+ ctxCancel()
+ return nil, errors.Wrap(err, "invalid job signature")
+ }
+ case <-time.After(JobRetrievalTimeout):
+ select {
+ case ev := <-jobEvents.Any(context.Background()):
+ if ev.Value != nil {
+ // Job was there, so it was aborted
+ err = ErrJobAborted
+ }
+ case <-time.After(JobEventRetrievalTimeout):
+ // The job is actually not found
+ err = ErrJobTimeout
+ }
+ ctxCancel()
+ return nil, err
+ }
+
+ // Build accessible controller
+ return &controller{
+ id: id,
+ namespace: namespace,
+ scheduledAt: scheduledAt,
+ signature: sig,
+ clientSet: clientSet,
+ ctx: ctx,
+ ctxCancel: ctxCancel,
+ job: job,
+ jobEvents: jobEvents,
+ pod: pod,
+ podEvents: podEvents,
+ }, nil
+}
+
+type controller struct {
+ id string
+ namespace string
+ scheduledAt time.Time
+ signature []testworkflowprocessor.Signature
+ clientSet kubernetes.Interface
+ ctx context.Context
+ ctxCancel context.CancelFunc
+ job Watcher[*batchv1.Job]
+ jobEvents Watcher[*corev1.Event]
+ pod Watcher[*corev1.Pod]
+ podEvents Watcher[*corev1.Event]
+}
+
+func (c *controller) Abort(ctx context.Context) error {
+ return c.Cleanup(ctx)
+}
+
+func (c *controller) Cleanup(ctx context.Context) error {
+ return Cleanup(ctx, c.clientSet, c.namespace, c.id)
+}
+
+func (c *controller) Watch(parentCtx context.Context) Watcher[Notification] {
+ ctx, ctxCancel := context.WithCancel(parentCtx)
+ w := newWatcher[Notification](ctx, 0)
+
+ go func() {
+ defer w.Close()
+ defer ctxCancel()
+
+ sig := make([]testkube.TestWorkflowSignature, len(c.signature))
+ for i, s := range c.signature {
+ sig[i] = s.ToInternal()
+ }
+
+ // Build initial result
+ result := testkube.TestWorkflowResult{
+ Status: common.Ptr(testkube.QUEUED_TestWorkflowStatus),
+ PredictedStatus: common.Ptr(testkube.PASSED_TestWorkflowStatus),
+ Initialization: &testkube.TestWorkflowStepResult{
+ Status: common.Ptr(testkube.QUEUED_TestWorkflowStepStatus),
+ },
+ Steps: testworkflowprocessor.MapSignatureListToStepResults(c.signature),
+ }
+
+ // Emit initial empty result
+ w.SendValue(Notification{Result: result.Clone()})
+
+ // Wait for the pod creation
+ for v := range WatchJobPreEvents(ctx, c.jobEvents, 0).Stream(ctx).Channel() {
+ if v.Error != nil {
+ w.SendError(v.Error)
+ continue
+ }
+ if v.Value.Reason == "SuccessfulCreate" {
+ result.QueuedAt = v.Value.CreationTimestamp.Time
+ }
+ if v.Value.Type == "Normal" {
+ continue
+ }
+ w.SendValue(Notification{
+ Timestamp: v.Value.CreationTimestamp.Time,
+ Log: fmt.Sprintf("%s (%s) %s\n", v.Value.CreationTimestamp.Time.Format(KubernetesLogTimeFormat), v.Value.Reason, v.Value.Message),
+ })
+ }
+
+ // Emit the result with queue time
+ if result.QueuedAt.IsZero() {
+ w.SendError(errors.New("job is in unknown state"))
+ return
+ }
+ w.SendValue(Notification{Result: result.Clone()})
+
+ // Wait for the pod initialization
+ for v := range WatchPodPreEvents(ctx, c.podEvents, 0).Stream(ctx).Channel() {
+ if v.Error != nil {
+ w.SendError(v.Error)
+ continue
+ }
+ if v.Value.Reason == "Scheduled" {
+ result.StartedAt = v.Value.CreationTimestamp.Time
+ result.Status = common.Ptr(testkube.RUNNING_TestWorkflowStatus)
+ }
+ if v.Value.Type == "Normal" {
+ continue
+ }
+ w.SendValue(Notification{
+ Timestamp: v.Value.CreationTimestamp.Time,
+ Log: fmt.Sprintf("%s (%s) %s\n", v.Value.CreationTimestamp.Time.Format(KubernetesLogTimeFormat), v.Value.Reason, v.Value.Message),
+ })
+ }
+
+ // Emit the result with start time
+ if result.StartedAt.IsZero() {
+ w.SendError(errors.New("pod is in unknown state"))
+ return
+ }
+ w.SendValue(Notification{Result: result.Clone()})
+
+ // Wait for the initialization container
+ for v := range WatchContainerPreEvents(ctx, c.podEvents, "tktw-init", 0, true).Stream(ctx).Channel() {
+ if v.Error != nil {
+ w.SendError(v.Error)
+ continue
+ }
+ if v.Value.Reason == "Created" {
+ result.Initialization.QueuedAt = v.Value.CreationTimestamp.Time
+ } else if v.Value.Reason == "Started" {
+ result.Initialization.StartedAt = v.Value.CreationTimestamp.Time
+ result.Initialization.Status = common.Ptr(testkube.RUNNING_TestWorkflowStepStatus)
+ }
+ if v.Value.Type == "Normal" {
+ continue
+ }
+ w.SendValue(Notification{
+ Timestamp: v.Value.CreationTimestamp.Time,
+ Log: fmt.Sprintf("%s (%s) %s\n", v.Value.CreationTimestamp.Time.Format(KubernetesLogTimeFormat), v.Value.Reason, v.Value.Message),
+ })
+ }
+
+ // Emit the result with start time
+ if result.Initialization.StartedAt.IsZero() {
+ w.SendError(errors.New("init container is in unknown state"))
+ return
+ }
+ w.SendValue(Notification{Result: result.Clone()})
+
+ // Watch the initialization container logs
+ lastTs := result.Initialization.StartedAt
+ pod := (<-c.pod.Any(ctx)).Value
+ for v := range WatchContainerLogs(ctx, c.clientSet, c.podEvents, c.namespace, pod.Name, "tktw-init").Stream(ctx).Channel() {
+ if v.Error != nil {
+ w.SendError(v.Error)
+ continue
+ }
+ if v.Value.Time.After(lastTs) {
+ lastTs = v.Value.Time
+ }
+ // TODO: Calibrate clock with v.Value.Hint or just first/last timestamp here
+ w.SendValue(Notification{
+ Timestamp: v.Value.Time,
+ Log: fmt.Sprintf("%s %s\n", v.Value.Time.Format(KubernetesLogTimeFormat), string(v.Value.Log)),
+ })
+ }
+
+ // Update the initialization container status
+ status, err := GetFinalContainerResult(ctx, c.pod, "tktw-init")
+ if err != nil {
+ w.SendError(err)
+ return
+ }
+ result.Initialization.FinishedAt = status.FinishedAt
+ if lastTs.After(result.Initialization.FinishedAt) {
+ result.Initialization.FinishedAt = lastTs
+ }
+ result.Initialization.Status = common.Ptr(status.Status)
+ if status.Status != testkube.PASSED_TestWorkflowStepStatus {
+ result.Status = common.Ptr(testkube.FAILED_TestWorkflowStatus)
+ result.PredictedStatus = result.Status
+ }
+ w.SendValue(Notification{Result: result.Clone()})
+
+ // Cancel when the initialization has failed
+ if status.Status != testkube.PASSED_TestWorkflowStepStatus {
+ return
+ }
+
+ // Watch each of the containers
+ lastTs = result.Initialization.FinishedAt
+ for _, container := range append(pod.Spec.InitContainers, pod.Spec.Containers...) {
+ // Ignore not-standard TestWorkflow containers
+ if _, ok := result.Steps[container.Name]; !ok {
+ continue
+ }
+
+ // Send the step queued time
+ stepResult := result.Steps[container.Name]
+ stepResult = result.UpdateStepResult(sig, container.Name, testkube.TestWorkflowStepResult{
+ QueuedAt: lastTs.UTC(),
+ }, c.scheduledAt)
+ w.SendValue(Notification{Result: result.Clone()})
+
+ // Watch for the container events
+ lastEvTs := time.Time{}
+ for v := range WatchContainerPreEvents(ctx, c.podEvents, container.Name, 0, false).Stream(ctx).Channel() {
+ if v.Error != nil {
+ w.SendError(v.Error)
+ continue
+ }
+ if lastEvTs.Before(v.Value.CreationTimestamp.Time) {
+ lastEvTs = v.Value.CreationTimestamp.Time
+ }
+ if v.Value.Reason == "Created" {
+ stepResult = result.UpdateStepResult(sig, container.Name, testkube.TestWorkflowStepResult{
+ QueuedAt: v.Value.CreationTimestamp.Time.UTC(),
+ }, c.scheduledAt)
+ } else if v.Value.Reason == "Started" {
+ stepResult = result.UpdateStepResult(sig, container.Name, testkube.TestWorkflowStepResult{
+ StartedAt: v.Value.CreationTimestamp.Time.UTC(),
+ Status: common.Ptr(testkube.RUNNING_TestWorkflowStepStatus),
+ }, c.scheduledAt)
+ }
+ if v.Value.Type == "Normal" {
+ continue
+ }
+ w.SendValue(Notification{
+ Timestamp: v.Value.CreationTimestamp.Time,
+ Ref: container.Name,
+ Log: fmt.Sprintf("%s (%s) %s\n", v.Value.CreationTimestamp.Time.Format(KubernetesLogTimeFormat), v.Value.Reason, v.Value.Message),
+ })
+ }
+
+ // Emit the next result
+ if stepResult.StartedAt.IsZero() {
+ w.SendError(errors.New("step container is in unknown state"))
+ break
+ }
+ w.SendValue(Notification{Result: result.Clone()})
+
+ // Watch for the container logs, outputs and statuses
+ for v := range WatchContainerLogs(ctx, c.clientSet, c.podEvents, c.namespace, pod.Name, container.Name).Stream(ctx).Channel() {
+ if v.Error != nil {
+ w.SendError(v.Error)
+ continue
+ }
+ if v.Value.Hint != nil {
+ if v.Value.Hint.Name == "start" && v.Value.Hint.Ref == container.Name {
+ if v.Value.Time.After(stepResult.StartedAt) {
+ stepResult = result.UpdateStepResult(sig, container.Name, testkube.TestWorkflowStepResult{
+ StartedAt: v.Value.Time.UTC(),
+ }, c.scheduledAt)
+ }
+ } else if v.Value.Hint.Name == "status" {
+ status := testkube.TestWorkflowStepStatus(v.Value.Hint.Value.(string))
+ if status == "" {
+ status = testkube.PASSED_TestWorkflowStepStatus
+ }
+ if _, ok := result.Steps[v.Value.Hint.Ref]; ok {
+ stepResult = result.UpdateStepResult(sig, v.Value.Hint.Ref, testkube.TestWorkflowStepResult{
+ Status: &status,
+ }, c.scheduledAt)
+ }
+ }
+ continue
+ }
+ if v.Value.Output != nil {
+ if _, ok := result.Steps[v.Value.Output.Ref]; ok {
+ w.SendValue(Notification{
+ Timestamp: v.Value.Time,
+ Ref: v.Value.Output.Ref,
+ Output: v.Value.Output,
+ })
+ }
+ continue
+ }
+ w.SendValue(Notification{Timestamp: v.Value.Time, Ref: container.Name, Log: string(v.Value.Log)})
+ }
+
+ // Watch container status
+ status, err := GetFinalContainerResult(ctx, c.pod, container.Name)
+ if err != nil {
+ w.SendError(err)
+ break
+ }
+ finishedAt := status.FinishedAt.UTC()
+ if !finishedAt.IsZero() && lastTs.After(finishedAt) {
+ finishedAt = lastTs.UTC()
+ }
+ stepResult = result.UpdateStepResult(sig, container.Name, testkube.TestWorkflowStepResult{
+ FinishedAt: finishedAt,
+ ExitCode: float64(status.ExitCode),
+ Status: common.Ptr(status.Status),
+ }, c.scheduledAt)
+ w.SendValue(Notification{Result: result.Clone()})
+
+ // Update the last timestamp
+ lastTs = finishedAt
+
+ // Break the function if the step has been aborted.
+ // Breaking only to the loop is not enough,
+ // because due to GKE bug, the Job is still pending,
+ // so it will get stuck there.
+ if status.Status == testkube.ABORTED_TestWorkflowStepStatus {
+ result.Recompute(sig, c.scheduledAt)
+ abortTs := result.Steps[container.Name].FinishedAt
+ if status.Details == "" {
+ status.Details = "Manual"
+ }
+
+ w.SendValue(Notification{
+ Timestamp: abortTs,
+ Ref: container.Name,
+ Log: fmt.Sprintf("\n%s Aborted (%s)", abortTs.Format(KubernetesLogTimeFormat), status.Details),
+ })
+ w.SendValue(Notification{Result: result.Clone()})
+ return
+ }
+ }
+
+ // Read the pod finish time
+ for v := range c.job.Stream(ctx).Channel() {
+ if v.Value != nil && v.Value.Status.CompletionTime != nil {
+ result.FinishedAt = v.Value.Status.CompletionTime.Time
+ }
+ }
+
+ // Compute the TestWorkflow status and dates
+ result.Recompute(sig, c.scheduledAt)
+ w.SendValue(Notification{Result: result.Clone()})
+ }()
+
+ return w
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowcontroller/logs.go b/pkg/tcl/testworkflowstcl/testworkflowcontroller/logs.go
new file mode 100644
index 00000000000..839441d80d5
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowcontroller/logs.go
@@ -0,0 +1,331 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowcontroller
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ errors2 "github.com/pkg/errors"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/client-go/kubernetes"
+
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-init/data"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/log"
+ "github.com/kubeshop/testkube/pkg/utils"
+)
+
+type Instruction struct {
+ Ref string
+ Name string
+ Value interface{}
+}
+
+func (i *Instruction) ToInternal() *testkube.TestWorkflowOutput {
+ if i == nil {
+ return nil
+ }
+ value := map[string]interface{}(nil)
+ if i.Value != nil {
+ v, _ := json.Marshal(i.Value)
+ e := json.Unmarshal(v, &value)
+ if e != nil {
+ log.DefaultLogger.Warnf("invalid output passed from TestWorfklow - %v", i.Value)
+ }
+ }
+ if v, ok := i.Value.(map[string]interface{}); ok {
+ value = v
+ }
+ return &testkube.TestWorkflowOutput{
+ Ref: i.Ref,
+ Name: i.Name,
+ Value: value,
+ }
+}
+
+type Comment struct {
+ Time time.Time
+ Hint *Instruction
+ Output *Instruction
+}
+
+type ContainerLog struct {
+ Time time.Time
+ Log []byte
+ Hint *Instruction
+ Output *Instruction
+}
+
+type ContainerResult struct {
+ Status testkube.TestWorkflowStepStatus
+ Details string
+ ExitCode int
+ FinishedAt time.Time
+}
+
+var UnknownContainerResult = ContainerResult{
+ Status: testkube.ABORTED_TestWorkflowStepStatus,
+ ExitCode: -1,
+}
+
+func GetContainerResult(c corev1.ContainerStatus) ContainerResult {
+ if c.State.Waiting != nil {
+ return ContainerResult{Status: testkube.QUEUED_TestWorkflowStepStatus, ExitCode: -1}
+ }
+ if c.State.Running != nil {
+ return ContainerResult{Status: testkube.RUNNING_TestWorkflowStepStatus, ExitCode: -1}
+ }
+ re := regexp.MustCompile(`^([^,]*),(0|[1-9]\d*)$`)
+
+ // Workaround - GKE sends SIGKILL after the container is already terminated,
+ // and the pod gets stuck then.
+ if c.State.Terminated.Reason != "Completed" {
+ return ContainerResult{Status: testkube.ABORTED_TestWorkflowStepStatus, Details: c.State.Terminated.Reason, ExitCode: -1, FinishedAt: c.State.Terminated.FinishedAt.Time}
+ }
+
+ msg := c.State.Terminated.Message
+ match := re.FindStringSubmatch(msg)
+ if match == nil {
+ return ContainerResult{Status: testkube.ABORTED_TestWorkflowStepStatus, ExitCode: -1, FinishedAt: c.State.Terminated.FinishedAt.Time}
+ }
+ status := testkube.TestWorkflowStepStatus(match[1])
+ exitCode, _ := strconv.Atoi(match[2])
+ if status == "" {
+ status = testkube.PASSED_TestWorkflowStepStatus
+ }
+ return ContainerResult{Status: status, ExitCode: exitCode, FinishedAt: c.State.Terminated.FinishedAt.Time}
+}
+
+func GetFinalContainerResult(ctx context.Context, pod Watcher[*corev1.Pod], containerName string) (ContainerResult, error) {
+ w := WatchContainerStatus(ctx, pod, containerName, 0)
+ stream := w.Stream(ctx)
+ defer w.Close()
+
+ for c := range stream.Channel() {
+ if c.Error != nil {
+ return UnknownContainerResult, c.Error
+ }
+ if c.Value.State.Terminated == nil {
+ continue
+ }
+ return GetContainerResult(c.Value), nil
+ }
+ return UnknownContainerResult, nil
+}
+
+var ErrNoStartedEvent = errors.New("started event not received")
+
+func WatchContainerPreEvents(ctx context.Context, podEvents Watcher[*corev1.Event], containerName string, cacheSize int, includePodWarnings bool) Watcher[*corev1.Event] {
+ w := newWatcher[*corev1.Event](ctx, cacheSize)
+ go func() {
+ events := WatchContainerEvents(ctx, podEvents, containerName, 0, includePodWarnings)
+ defer events.Close()
+ defer w.Close()
+
+ for ev := range events.Stream(ctx).Channel() {
+ if ev.Error != nil {
+ w.SendError(ev.Error)
+ } else {
+ w.SendValue(ev.Value)
+ if ev.Value.Reason == "Started" {
+ return
+ }
+ }
+ }
+ }()
+ return w
+}
+
+func WatchPodPreEvents(ctx context.Context, podEvents Watcher[*corev1.Event], cacheSize int) Watcher[*corev1.Event] {
+ w := newWatcher[*corev1.Event](ctx, cacheSize)
+ go func() {
+ defer w.Close()
+
+ for ev := range podEvents.Stream(w.ctx).Channel() {
+ if ev.Error != nil {
+ w.SendError(ev.Error)
+ } else {
+ w.SendValue(ev.Value)
+ if ev.Value.Reason == "Scheduled" {
+ return
+ }
+ }
+ }
+ }()
+ return w
+}
+
+func WaitUntilContainerIsStarted(ctx context.Context, podEvents Watcher[*corev1.Event], containerName string) error {
+ events := WatchContainerPreEvents(ctx, podEvents, containerName, 0, false)
+ defer events.Close()
+
+ for ev := range events.Stream(ctx).Channel() {
+ if ev.Error != nil {
+ return ev.Error
+ } else if ev.Value.Reason == "Started" {
+ return nil
+ }
+ }
+ return ErrNoStartedEvent
+}
+
+func WatchContainerLogs(ctx context.Context, clientSet kubernetes.Interface, podEvents Watcher[*corev1.Event], namespace, podName, containerName string) Watcher[ContainerLog] {
+ w := newWatcher[ContainerLog](ctx, 0)
+
+ go func() {
+ defer w.Close()
+
+ // Wait until "Started" event, to avoid calling logs on the
+ err := WaitUntilContainerIsStarted(ctx, podEvents, containerName)
+ if err != nil {
+ w.SendError(err)
+ return
+ }
+
+ // Create logs stream request
+ req := clientSet.CoreV1().Pods(namespace).GetLogs(podName, &corev1.PodLogOptions{
+ Follow: true,
+ Timestamps: true,
+ Container: containerName,
+ })
+ var stream io.ReadCloser
+ for {
+ stream, err = req.Stream(ctx)
+ if err != nil {
+ // The container is not necessarily already started when Started event is received
+ if !strings.Contains(err.Error(), "is waiting to start") {
+ w.SendError(err)
+ return
+ }
+ continue
+ }
+ break
+ }
+
+ go func() {
+ <-w.Done()
+ _ = stream.Close()
+ }()
+
+ // Parse and return the logs
+ reader := bufio.NewReader(stream)
+ var tsPrefix, tmpTsPrefix []byte
+ isNewLine := false
+ isStarted := false
+ var ts, tmpTs time.Time
+ for {
+ var prepend []byte
+
+ // Read next timestamp
+ tmpTs, tmpTsPrefix, err = ReadTimestamp(reader)
+ if err == nil {
+ ts = tmpTs
+ tsPrefix = tmpTsPrefix
+ } else if err == io.EOF {
+ return
+ } else {
+ // Edge case: Kubernetes may send critical errors without timestamp (like ionotify)
+ if len(tmpTsPrefix) > 0 {
+ prepend = tmpTsPrefix
+ }
+ w.SendError(err)
+ }
+
+ // Check for the next part
+ line, err := utils.ReadLongLine(reader)
+ if len(prepend) > 0 {
+ line = append(prepend, line...)
+ }
+ commentRe := regexp.MustCompile(fmt.Sprintf(`^%s(%s)?([^%s]+)%s([a-zA-Z0-9-_.]+)(?:%s([^\n]+))?%s$`,
+ data.InstructionPrefix, data.HintPrefix, data.InstructionSeparator, data.InstructionSeparator, data.InstructionValueSeparator, data.InstructionSeparator))
+
+ // Process the received line
+ if len(line) > 0 {
+ hadComment := false
+ // Fast check to avoid regexes
+ if len(line) >= 4 && string(line[:len(data.InstructionPrefix)]) == data.InstructionPrefix {
+ v := commentRe.FindSubmatch(line)
+ if v != nil {
+ isHint := string(v[1]) == data.HintPrefix
+ ref := string(v[2])
+ name := string(v[3])
+ result := Instruction{Ref: ref, Name: name}
+ log := ContainerLog{Time: ts}
+ if isHint {
+ log.Hint = &result
+ } else {
+ log.Output = &result
+ }
+ if len(v) > 4 && v[4] != nil {
+ err := json.Unmarshal(v[4], &result.Value)
+ if err == nil {
+ isNewLine = false
+ hadComment = true
+ w.SendValue(log)
+ }
+ } else {
+ isNewLine = false
+ hadComment = true
+ w.SendValue(log)
+ }
+ }
+ }
+
+ // Append as regular log if expected
+ if !hadComment {
+ if isNewLine {
+ line = append(append([]byte("\n"), tsPrefix...), line...)
+ } else if !isStarted {
+ line = append(tsPrefix, line...)
+ isStarted = true
+ }
+ w.SendValue(ContainerLog{Time: ts, Log: line})
+ isNewLine = true
+ }
+ } else if isStarted {
+ w.SendValue(ContainerLog{Time: ts, Log: append([]byte("\n"), tsPrefix...)})
+ }
+
+ // Handle the error
+ if err != nil {
+ if err != io.EOF {
+ w.SendError(err)
+ }
+ return
+ }
+ }
+ }()
+
+ return w
+}
+
+func ReadTimestamp(reader *bufio.Reader) (time.Time, []byte, error) {
+ tsPrefix := make([]byte, 31) // 30 bytes for timestamp + 1 byte for space
+ count, err := io.ReadFull(reader, tsPrefix)
+ if err != nil {
+ return time.Time{}, nil, err
+ }
+ if count < 31 {
+ return time.Time{}, nil, io.EOF
+ }
+ ts, err := time.Parse(KubernetesLogTimeFormat, string(tsPrefix[0:30]))
+ if err != nil {
+ return time.Time{}, tsPrefix, errors2.Wrap(err, "parsing timestamp")
+ }
+ return ts, tsPrefix, nil
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowcontroller/notification.go b/pkg/tcl/testworkflowstcl/testworkflowcontroller/notification.go
new file mode 100644
index 00000000000..22ae7ad2d03
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowcontroller/notification.go
@@ -0,0 +1,33 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowcontroller
+
+import (
+ "time"
+
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+)
+
+type Notification struct {
+ Timestamp time.Time `json:"ts"`
+ Result *testkube.TestWorkflowResult `json:"result,omitempty"`
+ Ref string `json:"ref,omitempty"`
+ Log string `json:"log,omitempty"`
+ Output *Instruction `json:"output,omitempty"`
+}
+
+func (n *Notification) ToInternal() testkube.TestWorkflowExecutionNotification {
+ return testkube.TestWorkflowExecutionNotification{
+ Ts: n.Timestamp,
+ Result: n.Result,
+ Ref: n.Ref,
+ Log: n.Log,
+ Output: n.Output.ToInternal(),
+ }
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowcontroller/utils.go b/pkg/tcl/testworkflowstcl/testworkflowcontroller/utils.go
new file mode 100644
index 00000000000..80ea622d231
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowcontroller/utils.go
@@ -0,0 +1,437 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowcontroller
+
+import (
+ "context"
+ "fmt"
+ "reflect"
+ "regexp"
+ "time"
+
+ batchv1 "k8s.io/api/batch/v1"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/watch"
+ "k8s.io/client-go/kubernetes"
+
+ "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor"
+)
+
+const (
+ KubernetesLogTimeFormat = "2006-01-02T15:04:05.000000000Z"
+)
+
+func IsPodDone(pod *corev1.Pod) bool {
+ return pod.Status.Phase != corev1.PodPending && pod.Status.Phase != corev1.PodRunning
+}
+
+func IsJobDone(job *batchv1.Job) bool {
+ return job.Status.Active == 0 && (job.Status.Succeeded > 0 || job.Status.Failed > 0)
+}
+
+func WatchJob(ctx context.Context, clientSet kubernetes.Interface, namespace, name string, cacheSize int) Watcher[*batchv1.Job] {
+ w := newWatcher[*batchv1.Job](ctx, cacheSize)
+
+ go func() {
+ defer w.Close()
+ selector := "metadata.name=" + name
+
+ // Get initial pods
+ list, err := clientSet.BatchV1().Jobs(namespace).List(w.ctx, metav1.ListOptions{
+ FieldSelector: selector,
+ })
+
+ // Expose the initial value
+ if err != nil {
+ w.SendError(err)
+ return
+ }
+ if len(list.Items) == 1 {
+ job := list.Items[0]
+ w.SendValue(&job)
+ if IsJobDone(&job) {
+ return
+ }
+ }
+
+ // Start watching for changes
+ jobs, err := clientSet.BatchV1().Jobs(namespace).Watch(w.ctx, metav1.ListOptions{
+ ResourceVersion: list.ResourceVersion,
+ FieldSelector: selector,
+ })
+ if err != nil {
+ w.SendError(err)
+ return
+ }
+ defer jobs.Stop()
+ for {
+ // Prioritize checking for done
+ select {
+ case <-w.Done():
+ return
+ default:
+ }
+ // Wait for results
+ select {
+ case <-w.Done():
+ return
+ case event, ok := <-jobs.ResultChan():
+ if !ok {
+ return
+ }
+ switch event.Type {
+ case watch.Added, watch.Modified:
+ job := event.Object.(*batchv1.Job)
+ w.SendValue(job)
+ if IsJobDone(job) {
+ return
+ }
+ case watch.Deleted:
+ return
+ }
+ }
+ }
+ }()
+
+ return w
+}
+
+func WatchMainPod(ctx context.Context, clientSet kubernetes.Interface, namespace, name string, cacheSize int) Watcher[*corev1.Pod] {
+ return watchPod(ctx, clientSet, namespace, ListOptions{
+ LabelSelector: testworkflowprocessor.ExecutionIdMainPodLabelName + "=" + name,
+ CacheSize: cacheSize,
+ })
+}
+
+func WatchPodByName(ctx context.Context, clientSet kubernetes.Interface, namespace, name string, cacheSize int) Watcher[*corev1.Pod] {
+ return watchPod(ctx, clientSet, namespace, ListOptions{
+ FieldSelector: "metadata.name=" + name,
+ CacheSize: cacheSize,
+ })
+}
+
+func watchPod(ctx context.Context, clientSet kubernetes.Interface, namespace string, options ListOptions) Watcher[*corev1.Pod] {
+ w := newWatcher[*corev1.Pod](ctx, options.CacheSize)
+
+ go func() {
+ defer w.Close()
+
+ // Get initial pods
+ list, err := clientSet.CoreV1().Pods(namespace).List(w.ctx, metav1.ListOptions{
+ FieldSelector: options.FieldSelector,
+ LabelSelector: options.LabelSelector,
+ })
+
+ // Expose the initial value
+ if err != nil {
+ w.SendError(err)
+ return
+ }
+ if len(list.Items) == 1 {
+ pod := list.Items[0]
+ w.SendValue(&pod)
+ if IsPodDone(&pod) {
+ return
+ }
+ }
+
+ // Start watching for changes
+ pods, err := clientSet.CoreV1().Pods(namespace).Watch(w.ctx, metav1.ListOptions{
+ ResourceVersion: list.ResourceVersion,
+ FieldSelector: options.FieldSelector,
+ LabelSelector: options.LabelSelector,
+ })
+ if err != nil {
+ w.SendError(err)
+ return
+ }
+ defer pods.Stop()
+ for {
+ // Prioritize checking for done
+ select {
+ case <-w.Done():
+ return
+ default:
+ }
+ // Wait for results
+ select {
+ case <-w.Done():
+ return
+ case event, ok := <-pods.ResultChan():
+ if !ok {
+ return
+ }
+ switch event.Type {
+ case watch.Added, watch.Modified:
+ pod := event.Object.(*corev1.Pod)
+ w.SendValue(pod)
+ if IsPodDone(pod) {
+ return
+ }
+ case watch.Deleted:
+ return
+ }
+ }
+ }
+ }()
+
+ return w
+}
+
+type ListOptions struct {
+ FieldSelector string
+ LabelSelector string
+ TypeMeta metav1.TypeMeta
+ CacheSize int
+}
+
+func GetEventContainerName(event *corev1.Event) string {
+ regex := regexp.MustCompile(`^spec\.(?:initContainers|containers)\{([^]]+)}`)
+ path := event.InvolvedObject.FieldPath
+ if regex.Match([]byte(path)) {
+ name := regex.ReplaceAllString(event.InvolvedObject.FieldPath, "$1")
+ return name
+ }
+ return ""
+}
+
+func WatchContainerEvents(ctx context.Context, podEvents Watcher[*corev1.Event], name string, cacheSize int, includePodWarnings bool) Watcher[*corev1.Event] {
+ w := newWatcher[*corev1.Event](ctx, cacheSize)
+ go func() {
+ stream := podEvents.Stream(ctx)
+ defer stream.Stop()
+ defer w.Close()
+ for {
+ select {
+ case <-w.Done():
+ return
+ case v, ok := <-stream.Channel():
+ if ok {
+ if v.Error != nil {
+ w.SendError(v.Error)
+ } else if GetEventContainerName(v.Value) == name {
+ w.SendValue(v.Value)
+ } else if includePodWarnings && v.Value.Type == "Warning" {
+ w.SendValue(v.Value)
+ }
+ } else {
+ return
+ }
+ }
+ }
+ }()
+ return w
+}
+
+func WatchContainerStatus(ctx context.Context, pod Watcher[*corev1.Pod], containerName string, cacheSize int) Watcher[corev1.ContainerStatus] {
+ w := newWatcher[corev1.ContainerStatus](ctx, cacheSize)
+
+ go func() {
+ stream := pod.Stream(ctx)
+ defer stream.Stop()
+ defer w.Close()
+ var prev corev1.ContainerStatus
+ for {
+ select {
+ case <-w.Done():
+ return
+ case p := <-stream.Channel():
+ if p.Error != nil {
+ w.SendError(p.Error)
+ continue
+ }
+ if p.Value == nil {
+ continue
+ }
+ for _, s := range append(p.Value.Status.InitContainerStatuses, p.Value.Status.ContainerStatuses...) {
+ if s.Name == containerName {
+ if !reflect.DeepEqual(s, prev) {
+ prev = s
+ w.SendValue(s)
+ }
+ break
+ }
+ }
+ if IsPodDone(p.Value) {
+ return
+ }
+ }
+ }
+ }()
+
+ return w
+}
+
+func WatchPodEventsByName(ctx context.Context, clientSet kubernetes.Interface, namespace, name string, cacheSize int) Watcher[*corev1.Event] {
+ return WatchEvents(ctx, clientSet, namespace, ListOptions{
+ FieldSelector: "involvedObject.name=" + name,
+ TypeMeta: metav1.TypeMeta{Kind: "Pod"},
+ CacheSize: cacheSize,
+ })
+}
+
+func WatchPodEventsByPodWatcher(ctx context.Context, clientSet kubernetes.Interface, namespace string, pod Watcher[*corev1.Pod], cacheSize int) Watcher[*corev1.Event] {
+ w := newWatcher[*corev1.Event](ctx, cacheSize)
+
+ go func() {
+ defer w.Close()
+
+ v, ok := <-pod.Any(ctx)
+ if v.Error != nil {
+ w.SendError(v.Error)
+ return
+ }
+ if !ok || v.Value == nil {
+ return
+ }
+ _, wch := watchEvents(clientSet, namespace, ListOptions{
+ FieldSelector: "involvedObject.name=" + v.Value.Name,
+ TypeMeta: metav1.TypeMeta{Kind: "Pod"},
+ }, w)
+
+ // Wait for all immediate events
+ <-wch
+
+ // Adds missing "Started" events.
+ // It may have duplicated "Started", but better than no events.
+ // @see {@link https://github.com/kubernetes/kubernetes/issues/122904#issuecomment-1944387021}
+ started := map[string]bool{}
+ for p := range pod.Stream(ctx).Channel() {
+ for i, s := range append(p.Value.Status.InitContainerStatuses, p.Value.Status.ContainerStatuses...) {
+ if !started[s.Name] && (s.State.Running != nil || s.State.Terminated != nil) {
+ ts := metav1.Time{Time: time.Now()}
+ if s.State.Running != nil {
+ ts = s.State.Running.StartedAt
+ } else if s.State.Terminated != nil {
+ ts = s.State.Terminated.StartedAt
+ }
+ started[s.Name] = true
+ fieldPath := fmt.Sprintf("spec.containers{%s}", s.Name)
+ if i >= len(p.Value.Status.InitContainerStatuses) {
+ fieldPath = fmt.Sprintf("spec.initContainers{%s}", s.Name)
+ }
+ w.SendValue(&corev1.Event{
+ ObjectMeta: metav1.ObjectMeta{CreationTimestamp: ts},
+ FirstTimestamp: ts,
+ Type: "Normal",
+ Reason: "Started",
+ Message: fmt.Sprintf("Started container %s", s.Name),
+ InvolvedObject: corev1.ObjectReference{FieldPath: fieldPath},
+ })
+ }
+ }
+ }
+ }()
+
+ return w
+}
+
+func WatchJobEvents(ctx context.Context, clientSet kubernetes.Interface, namespace, name string, cacheSize int) Watcher[*corev1.Event] {
+ return WatchEvents(ctx, clientSet, namespace, ListOptions{
+ FieldSelector: "involvedObject.name=" + name,
+ TypeMeta: metav1.TypeMeta{Kind: "Job"},
+ CacheSize: cacheSize,
+ })
+}
+
+func WatchJobPreEvents(ctx context.Context, jobEvents Watcher[*corev1.Event], cacheSize int) Watcher[*corev1.Event] {
+ w := newWatcher[*corev1.Event](ctx, cacheSize)
+ go func() {
+ defer w.Close()
+ stream := jobEvents.Stream(ctx)
+ defer stream.Stop()
+
+ for {
+ select {
+ case <-w.Done():
+ return
+ case v := <-stream.Channel():
+ if v.Error != nil {
+ w.SendError(v.Error)
+ } else {
+ w.SendValue(v.Value)
+ if v.Value.Reason == "SuccessfulCreate" {
+ return
+ }
+ }
+ }
+ }
+ }()
+ return w
+}
+
+func WatchEvents(ctx context.Context, clientSet kubernetes.Interface, namespace string, options ListOptions) Watcher[*corev1.Event] {
+ w, _ := watchEvents(clientSet, namespace, options, newWatcher[*corev1.Event](ctx, options.CacheSize))
+ return w
+}
+
+func watchEvents(clientSet kubernetes.Interface, namespace string, options ListOptions, w *watcher[*corev1.Event]) (Watcher[*corev1.Event], chan struct{}) {
+ initCh := make(chan struct{})
+ go func() {
+ defer w.Close()
+
+ // Get initial events
+ list, err := clientSet.CoreV1().Events(namespace).List(w.ctx, metav1.ListOptions{
+ FieldSelector: options.FieldSelector,
+ LabelSelector: options.LabelSelector,
+ TypeMeta: options.TypeMeta,
+ })
+
+ // Expose the initial value
+ if err != nil {
+ w.SendError(err)
+ close(initCh)
+ return
+ }
+ for _, event := range list.Items {
+ w.SendValue(event.DeepCopy())
+ }
+ close(initCh)
+
+ // Start watching for changes
+ events, err := clientSet.CoreV1().Events(namespace).Watch(w.ctx, metav1.ListOptions{
+ ResourceVersion: list.ResourceVersion,
+ FieldSelector: options.FieldSelector,
+ LabelSelector: options.LabelSelector,
+ TypeMeta: options.TypeMeta,
+ })
+ if err != nil {
+ w.SendError(err)
+ return
+ }
+ defer events.Stop()
+ for {
+ // Prioritize checking for done
+ select {
+ case <-w.Done():
+ return
+ default:
+ }
+ // Wait for results
+ select {
+ case <-w.Done():
+ return
+ case event, ok := <-events.ResultChan():
+ if !ok {
+ return
+ }
+ if event.Object == nil {
+ continue
+ }
+ switch event.Type {
+ case watch.Added, watch.Modified:
+ w.SendValue(event.Object.(*corev1.Event))
+ }
+ }
+ }
+ }()
+
+ return w, initCh
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowcontroller/watcher.go b/pkg/tcl/testworkflowstcl/testworkflowcontroller/watcher.go
new file mode 100644
index 00000000000..7bdac0fb40b
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowcontroller/watcher.go
@@ -0,0 +1,410 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowcontroller
+
+import (
+ "context"
+ "slices"
+ "sync"
+)
+
+type WatcherValue[T interface{}] struct {
+ Value T
+ Error error
+}
+
+type Watcher[T interface{}] interface {
+ Next(ctx context.Context) <-chan WatcherValue[T]
+ Any(ctx context.Context) <-chan WatcherValue[T]
+ Done() <-chan struct{}
+ Listen(fn func(WatcherValue[T], bool)) func()
+ Stream(ctx context.Context) WatcherChannel[T]
+ Close()
+}
+
+type watcher[T interface{}] struct {
+ ctx context.Context
+ ctxCancel context.CancelFunc
+ mu sync.Mutex
+ hasCh chan struct{}
+ ch chan WatcherValue[T]
+ listeners []*func(WatcherValue[T], bool)
+ paused bool
+ closed bool
+
+ cacheSize int
+ cacheOffset int
+ cache []WatcherValue[T]
+
+ readerCh chan<- struct{}
+}
+
+func newWatcher[T interface{}](ctx context.Context, cacheSize int) *watcher[T] {
+ finalCtx, ctxCancel := context.WithCancel(ctx)
+ return &watcher[T]{
+ ctx: finalCtx,
+ ctxCancel: ctxCancel,
+ hasCh: make(chan struct{}),
+ ch: make(chan WatcherValue[T]),
+ cacheSize: cacheSize,
+ }
+}
+
+func (w *watcher[T]) Pause() {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ w.paused = true
+ if w.readerCh != nil {
+ close(w.readerCh)
+ w.readerCh = nil
+ }
+}
+
+func (w *watcher[T]) Resume() {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ w.paused = false
+ w.recomputeReader()
+}
+
+func (w *watcher[T]) Next(ctx context.Context) <-chan WatcherValue[T] {
+ ch := make(chan WatcherValue[T])
+ var cancelListener func()
+ finalCtx, cancel := context.WithCancel(ctx)
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func() {
+ wg.Wait()
+ <-finalCtx.Done()
+ cancelListener()
+ }()
+ cancelListener = w.Listen(func(w WatcherValue[T], ok bool) {
+ wg.Wait() // on finished channel, the listener may be called before the lock goes down
+ cancelListener()
+ cancel()
+ if ok {
+ ch <- w
+ }
+ close(ch)
+ })
+ wg.Done()
+ return ch
+}
+
+func (w *watcher[T]) Any(ctx context.Context) <-chan WatcherValue[T] {
+ ch := make(chan WatcherValue[T])
+ go func() {
+ w.mu.Lock()
+ if len(w.cache) > 0 {
+ v := w.cache[len(w.cache)-1]
+ w.mu.Unlock()
+ ch <- v
+ close(ch)
+ return
+ }
+ w.mu.Unlock()
+ v, ok := <-w.Next(ctx)
+ if ok {
+ ch <- v
+ }
+ close(ch)
+ }()
+ return ch
+}
+
+func (w *watcher[T]) _send(v WatcherValue[T]) {
+ w.mu.Lock()
+
+ // Handle closed stream
+ if w.closed {
+ w.mu.Unlock()
+ return
+ }
+
+ // Save in cache
+ if w.cacheSize == 0 {
+ // Ignore cache
+ } else if w.cacheSize < 0 || w.cacheSize > len(w.cache) {
+ // Unlimited cache or still cache size
+ w.cache = append(w.cache, v)
+ } else {
+ // Emptying oldest entries in the cache
+ for i := 1; i < len(w.cache); i++ {
+ w.cache[i-1] = w.cache[i]
+ }
+ w.cache[len(w.cache)-1] = v
+ w.cacheOffset++
+ }
+ w.mu.Unlock()
+
+ // Ignore the panic due to the channel closed externally
+ defer func() {
+ recover()
+ }()
+
+ // Emit the data to the live stream
+ w.hasCh <- struct{}{}
+ w.ch <- v
+}
+
+func (w *watcher[T]) SendValue(value T) {
+ w._send(WatcherValue[T]{Value: value})
+
+}
+
+func (w *watcher[T]) SendError(err error) {
+ w._send(WatcherValue[T]{Error: err})
+}
+
+func (w *watcher[T]) Close() {
+ w.mu.Lock()
+ if !w.closed {
+ w.ctxCancel()
+ ch := w.ch
+ w.closed = true
+ close(ch)
+ close(w.hasCh)
+ w.mu.Unlock()
+ } else {
+ w.mu.Unlock()
+ }
+}
+
+func (w *watcher[T]) recomputeReader() {
+ if w.paused {
+ return
+ }
+ shouldRead := !w.closed && len(w.listeners) > 0
+ if shouldRead && w.readerCh == nil {
+ // Start the reader
+ ch := make(chan struct{})
+ w.readerCh = ch
+ go func() {
+ // Prioritize cancel channels
+ for {
+ select {
+ case <-ch:
+ return
+ default:
+ }
+ // Then wait for the results
+ select {
+ case <-ch:
+ return
+ case _, ok := <-w.hasCh:
+ listeners := slices.Clone(w.listeners)
+ if ok {
+ select {
+ case <-ch:
+ go func() {
+ defer func() {
+ recover()
+ }()
+ w.hasCh <- struct{}{} // replay hasCh in case it is needed in next iteration
+ }()
+ return
+ default:
+ }
+ }
+ value, ok := <-w.ch
+ var wg sync.WaitGroup
+ for _, l := range listeners {
+ wg.Add(1)
+ go func(fn func(WatcherValue[T], bool)) {
+ defer func() {
+ recover()
+ wg.Done()
+ }()
+ fn(value, ok)
+ }(*l)
+ }
+ wg.Wait()
+ }
+ }
+ }()
+ } else if !shouldRead && w.readerCh != nil {
+ // Stop the reader
+ close(w.readerCh)
+ w.readerCh = nil
+ }
+}
+
+func (w *watcher[T]) stop(ptr *func(WatcherValue[T], bool)) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ index := slices.Index(w.listeners, ptr)
+ if index == -1 {
+ return
+ }
+ // Delete the listener and stop a base channel reader if needed
+ *w.listeners[index] = func(value WatcherValue[T], ok bool) {}
+ w.listeners = append(w.listeners[0:index], w.listeners[index+1:]...)
+ w.recomputeReader()
+}
+
+func (w *watcher[T]) listenUnsafe(fn func(WatcherValue[T], bool)) func() {
+ // Fail immediately if the watcher is already closed
+ if w.closed {
+ go func() {
+ fn(WatcherValue[T]{}, false)
+ }()
+ return func() {}
+ }
+
+ // Append new listener and start a base channel reader if needed
+ ptr := &fn
+ w.listeners = append(w.listeners, ptr)
+ w.recomputeReader()
+ return func() {
+ w.stop(ptr)
+ }
+}
+
+func (w *watcher[T]) Listen(fn func(WatcherValue[T], bool)) func() {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ return w.listenUnsafe(fn)
+}
+
+func (w *watcher[T]) Done() <-chan struct{} {
+ return w.ctx.Done()
+}
+
+func (w *watcher[T]) getAndLock(index int) (WatcherValue[T], int, bool) {
+ w.mu.Lock()
+ index -= w.cacheOffset
+ if index < 0 {
+ index = 0
+ }
+ next := index + w.cacheOffset + 1
+
+ // Load value from cache
+ if index < len(w.cache) {
+ return w.cache[index], next, true
+ }
+
+ // Fetch next result
+ return WatcherValue[T]{}, next, false
+}
+
+func (w *watcher[T]) Stream(ctx context.Context) WatcherChannel[T] {
+ // Create the channel
+ wCh := &watcherChannel[T]{
+ ch: make(chan WatcherValue[T]),
+ }
+
+ // Handle context
+ finalCtx, cancel := context.WithCancel(ctx)
+ go func() {
+ <-finalCtx.Done()
+ wCh.Stop()
+ }()
+
+ // Fast-track when there are no cached messages
+ w.mu.Lock()
+ if len(w.cache) == 0 {
+ wCh.cancel = w.listenUnsafe(func(v WatcherValue[T], ok bool) {
+ defer func() {
+ // Ignore writing to already closed channel
+ recover()
+ }()
+ if ok {
+ wCh.ch <- v
+ } else if wCh.ch != nil {
+ wCh.Stop()
+ cancel()
+ }
+ })
+ w.mu.Unlock()
+ return wCh
+ }
+ w.mu.Unlock()
+
+ // Pick cache data
+ go func() {
+ defer func() {
+ // Ignore writing to already closed channel
+ recover()
+ }()
+
+ if wCh.ch == nil {
+ cancel()
+ return
+ }
+
+ // Send cache data
+ wCh.cancel = func() { cancel() }
+ var value WatcherValue[T]
+ var ok bool
+ index := 0
+ for value, index, ok = w.getAndLock(index); ok; value, index, ok = w.getAndLock(index) {
+ if wCh.ch == nil {
+ w.mu.Unlock()
+ cancel()
+ return
+ }
+ w.mu.Unlock()
+ wCh.ch <- value
+ }
+
+ if wCh.ch == nil {
+ w.mu.Unlock()
+ cancel()
+ return
+ }
+
+ // Start actually listening
+ wCh.cancel = w.listenUnsafe(func(v WatcherValue[T], ok bool) {
+ defer func() {
+ // Ignore writing to already closed channel
+ recover()
+ }()
+ if ok {
+ wCh.ch <- v
+ } else if wCh.ch != nil {
+ wCh.Stop()
+ cancel()
+ }
+ })
+ w.mu.Unlock()
+ }()
+
+ return wCh
+}
+
+type WatcherChannel[T interface{}] interface {
+ Channel() <-chan WatcherValue[T]
+ Stop()
+}
+
+type watcherChannel[T interface{}] struct {
+ cancel func()
+ ch chan WatcherValue[T]
+}
+
+func (w *watcherChannel[T]) Channel() <-chan WatcherValue[T] {
+ if w.ch == nil {
+ ch := make(chan WatcherValue[T])
+ close(ch)
+ return ch
+ }
+ return w.ch
+}
+
+func (w *watcherChannel[T]) Stop() {
+ if w.cancel != nil {
+ w.cancel()
+ w.cancel = nil
+ if w.ch != nil {
+ ch := w.ch
+ w.ch = nil
+ close(ch)
+ }
+ }
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowcontroller/watcher_test.go b/pkg/tcl/testworkflowstcl/testworkflowcontroller/watcher_test.go
new file mode 100644
index 00000000000..1aea5e13c92
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowcontroller/watcher_test.go
@@ -0,0 +1,155 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowcontroller
+
+import (
+ "context"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+type test struct {
+ value string
+}
+
+func queue(fn func()) {
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func() {
+ wg.Done()
+ fn()
+ }()
+ wg.Wait()
+}
+
+func TestWatcherSync(t *testing.T) {
+ w := newWatcher[test](context.Background(), 0)
+ defer w.Close()
+
+ go func() {
+ w.SendValue(test{value: "A"})
+ w.SendValue(test{value: "B"})
+ w.Close()
+ }()
+ a := <-w.Next(context.Background())
+ b := <-w.Next(context.Background())
+ c := <-w.Next(context.Background())
+ _, ok := <-w.Next(context.Background())
+
+ assert.Equal(t, WatcherValue[test]{Value: test{value: "A"}}, a)
+ assert.Equal(t, WatcherValue[test]{Value: test{value: "B"}}, b)
+ assert.Equal(t, WatcherValue[test]{}, c)
+ assert.Equal(t, false, ok)
+}
+
+func TestWatcherDistributed(t *testing.T) {
+ w := newWatcher[test](context.Background(), 0)
+ defer w.Close()
+
+ queue(func() {
+ w.SendValue(test{value: "A"})
+ w.SendValue(test{value: "B"})
+ w.Close()
+ })
+
+ w.Pause()
+ aCh, bCh := w.Next(context.Background()), w.Next(context.Background())
+ w.Resume()
+ a, b := <-aCh, <-bCh
+
+ c := <-w.Next(context.Background())
+ d := <-w.Next(context.Background())
+
+ assert.Equal(t, WatcherValue[test]{Value: test{value: "A"}}, a)
+ assert.Equal(t, WatcherValue[test]{Value: test{value: "A"}}, b)
+ assert.Equal(t, WatcherValue[test]{Value: test{value: "B"}}, c)
+ assert.Equal(t, WatcherValue[test]{}, d)
+}
+
+func TestWatcherSyncAdvanced(t *testing.T) {
+ w := newWatcher[test](context.Background(), 0)
+ defer w.Close()
+
+ go func() {
+ time.Sleep(500 * time.Microsecond)
+ w.SendValue(test{value: "A"})
+ w.SendValue(test{value: "B"})
+ w.Close()
+ }()
+
+ aCh := w.Next(context.Background())
+ w.SendValue(test{value: "A"})
+ go w.SendValue(test{value: "B"})
+ bCh := w.Next(context.Background())
+ a, b := <-aCh, <-bCh
+
+ assert.Equal(t, WatcherValue[test]{Value: test{value: "A"}}, a)
+ assert.Equal(t, WatcherValue[test]{Value: test{value: "B"}}, b)
+}
+
+func TestWatcherPause(t *testing.T) {
+ w := newWatcher[test](context.Background(), 0)
+ defer w.Close()
+
+ w.Pause()
+ aCh := w.Next(context.Background())
+ queue(func() {
+ w.SendValue(test{value: "A"})
+ })
+ bCh := w.Next(context.Background())
+ time.Sleep(500 * time.Microsecond)
+ var a, b WatcherValue[test]
+ select {
+ case a = <-aCh:
+ default:
+ }
+ select {
+ case b = <-bCh:
+ default:
+ }
+
+ assert.Equal(t, WatcherValue[test]{}, a)
+ assert.Equal(t, WatcherValue[test]{}, b)
+}
+
+func TestWatcherCache(t *testing.T) {
+ w := newWatcher[test](context.Background(), 2)
+ defer w.Close()
+
+ a := w.Stream(context.Background())
+ queue(func() {
+ w.SendValue(test{value: "A"})
+ w.SendValue(test{value: "B"})
+ w.SendValue(test{value: "C"})
+ time.Sleep(500 * time.Microsecond)
+ w.SendValue(test{value: "D"})
+ w.Close()
+ })
+ av1 := <-a.Channel()
+ av2 := <-a.Channel()
+ av3 := <-a.Channel()
+ a.Stop()
+
+ b := w.Stream(context.Background())
+ bv1 := <-b.Channel()
+ bv2 := <-b.Channel()
+ bv3 := <-b.Channel()
+ _, ok := <-b.Channel()
+
+ assert.Equal(t, WatcherValue[test]{Value: test{value: "A"}}, av1)
+ assert.Equal(t, WatcherValue[test]{Value: test{value: "B"}}, av2)
+ assert.Equal(t, WatcherValue[test]{Value: test{value: "C"}}, av3)
+ assert.Equal(t, WatcherValue[test]{Value: test{value: "B"}}, bv1)
+ assert.Equal(t, WatcherValue[test]{Value: test{value: "C"}}, bv2)
+ assert.Equal(t, WatcherValue[test]{Value: test{value: "D"}}, bv3)
+ assert.Equal(t, false, ok)
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowexecutor/executor.go b/pkg/tcl/testworkflowstcl/testworkflowexecutor/executor.go
new file mode 100644
index 00000000000..121d0b5d125
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowexecutor/executor.go
@@ -0,0 +1,234 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowexecutor
+
+import (
+ "bufio"
+ "context"
+ "io"
+ "sync"
+ "time"
+
+ "github.com/pkg/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-init/data"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ "github.com/kubeshop/testkube/pkg/event"
+ "github.com/kubeshop/testkube/pkg/log"
+ "github.com/kubeshop/testkube/pkg/tcl/repositorytcl/testworkflow"
+ "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowcontroller"
+ "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor"
+)
+
+//go:generate mockgen -destination=./mock_executor.go -package=testworkflowexecutor "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowexecutor" TestWorkflowExecutor
+type TestWorkflowExecutor interface {
+ Schedule(bundle *testworkflowprocessor.Bundle, execution testkube.TestWorkflowExecution)
+ Control(ctx context.Context, execution testkube.TestWorkflowExecution)
+ Recover(ctx context.Context)
+}
+
+type executor struct {
+ emitter *event.Emitter
+ clientSet kubernetes.Interface
+ repository testworkflow.Repository
+ output testworkflow.OutputRepository
+ namespace string
+}
+
+func New(emitter *event.Emitter, clientSet kubernetes.Interface, repository testworkflow.Repository, output testworkflow.OutputRepository, namespace string) TestWorkflowExecutor {
+ return &executor{
+ emitter: emitter,
+ clientSet: clientSet,
+ repository: repository,
+ output: output,
+ namespace: namespace,
+ }
+}
+
+func (e *executor) Schedule(bundle *testworkflowprocessor.Bundle, execution testkube.TestWorkflowExecution) {
+ // Inform about execution start
+ e.emitter.Notify(testkube.NewEventQueueTestWorkflow(&execution))
+
+ // Deploy required resources
+ err := e.Deploy(context.Background(), bundle)
+ if err != nil {
+ e.handleFatalError(execution, err, time.Time{})
+ return
+ }
+
+ // Start to control the results
+ go e.Control(context.Background(), execution)
+}
+
+func (e *executor) Deploy(ctx context.Context, bundle *testworkflowprocessor.Bundle) (err error) {
+ for _, item := range bundle.Secrets {
+ _, err = e.clientSet.CoreV1().Secrets(e.namespace).Create(ctx, &item, metav1.CreateOptions{})
+ if err != nil {
+ return
+ }
+ }
+ for _, item := range bundle.ConfigMaps {
+ _, err = e.clientSet.CoreV1().ConfigMaps(e.namespace).Create(ctx, &item, metav1.CreateOptions{})
+ if err != nil {
+ return
+ }
+ }
+ _, err = e.clientSet.BatchV1().Jobs(e.namespace).Create(ctx, &bundle.Job, metav1.CreateOptions{})
+ return
+}
+
+func (e *executor) handleFatalError(execution testkube.TestWorkflowExecution, err error, ts time.Time) {
+ // Detect error type
+ isAborted := errors.Is(err, testworkflowcontroller.ErrJobAborted)
+ isTimeout := errors.Is(err, testworkflowcontroller.ErrJobTimeout)
+
+ // Build error timestamp, adjusting it for aborting job
+ if ts.IsZero() {
+ ts = time.Now()
+ if isAborted || isTimeout {
+ ts = ts.Truncate(testworkflowcontroller.JobRetrievalTimeout)
+ }
+ }
+
+ // Apply the expected result
+ execution.Result.Fatal(err, isAborted, ts)
+ err = e.repository.UpdateResult(context.Background(), execution.Id, execution.Result)
+ if err != nil {
+ log.DefaultLogger.Errorf("failed to save fatal error for execution %s: %v", execution.Id, err)
+ }
+ e.emitter.Notify(testkube.NewEventEndTestWorkflowFailed(&execution))
+ go testworkflowcontroller.Cleanup(context.Background(), e.clientSet, e.namespace, execution.Id)
+}
+
+func (e *executor) Recover(ctx context.Context) {
+ list, err := e.repository.GetRunning(ctx)
+ if err != nil {
+ return
+ }
+ for _, execution := range list {
+ e.Control(context.Background(), execution)
+ }
+}
+
+func (e *executor) Control(ctx context.Context, execution testkube.TestWorkflowExecution) {
+ ctrl, err := testworkflowcontroller.New(ctx, e.clientSet, e.namespace, execution.Id, execution.ScheduledAt)
+ if err != nil {
+ e.handleFatalError(execution, err, time.Time{})
+ return
+ }
+
+ // Prepare stream for writing log
+ r, writer := io.Pipe()
+ reader := bufio.NewReader(r)
+ ref := ""
+
+ wg := sync.WaitGroup{}
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+
+ for v := range ctrl.Watch(ctx).Stream(ctx).Channel() {
+ if v.Error != nil {
+ continue
+ }
+ if v.Value.Output != nil {
+ execution.Output = append(execution.Output, *v.Value.Output.ToInternal())
+ } else if v.Value.Result != nil {
+ execution.Result = v.Value.Result
+ if execution.Result.IsFinished() {
+ execution.StatusAt = execution.Result.FinishedAt
+ }
+ err := e.repository.UpdateResult(ctx, execution.Id, execution.Result)
+ if err != nil {
+ log.DefaultLogger.Error(errors.Wrap(err, "error saving test workflow execution result"))
+ }
+ } else {
+ if ref != v.Value.Ref {
+ ref = v.Value.Ref
+ _, err := writer.Write([]byte(data.SprintHint(ref, "start")))
+ if err != nil {
+ log.DefaultLogger.Error(errors.Wrap(err, "saving log output signature"))
+ }
+ }
+ _, err := writer.Write([]byte(v.Value.Log))
+ if err != nil {
+ log.DefaultLogger.Error(errors.Wrap(err, "saving log output content"))
+ }
+ }
+ }
+
+ // Try to gracefully handle abort
+ if execution.Result.FinishedAt.IsZero() {
+ // Handle container failure
+ abortedAt := time.Time{}
+ for _, v := range execution.Result.Steps {
+ if v.Status != nil && *v.Status == testkube.ABORTED_TestWorkflowStepStatus {
+ abortedAt = v.FinishedAt
+ break
+ }
+ }
+ if !abortedAt.IsZero() {
+ e.handleFatalError(execution, testworkflowcontroller.ErrJobAborted, abortedAt)
+ } else {
+ // Handle unknown state
+ ctrl, err = testworkflowcontroller.New(ctx, e.clientSet, e.namespace, execution.Id, execution.ScheduledAt)
+ if err == nil {
+ for v := range ctrl.Watch(ctx).Stream(ctx).Channel() {
+ if v.Error != nil || v.Value.Output == nil {
+ continue
+ }
+
+ execution.Result = v.Value.Result
+ if execution.Result.IsFinished() {
+ execution.StatusAt = execution.Result.FinishedAt
+ }
+ err := e.repository.UpdateResult(ctx, execution.Id, execution.Result)
+ if err != nil {
+ log.DefaultLogger.Error(errors.Wrap(err, "error saving test workflow execution result"))
+ }
+ }
+ } else {
+ e.handleFatalError(execution, err, time.Time{})
+ }
+ }
+ }
+
+ err := writer.Close()
+ if err != nil {
+ log.DefaultLogger.Errorw("failed to close TestWorkflow log output stream", "id", execution.Id, "error", err)
+ }
+
+ // TODO: Consider AppendOutput ($push) instead
+ _ = e.repository.UpdateOutput(ctx, execution.Id, execution.Output)
+ if execution.Result.IsFinished() {
+ if execution.Result.IsPassed() {
+ e.emitter.Notify(testkube.NewEventEndTestWorkflowSuccess(&execution))
+ } else if execution.Result.IsAborted() {
+ e.emitter.Notify(testkube.NewEventEndTestWorkflowAborted(&execution))
+ } else {
+ e.emitter.Notify(testkube.NewEventEndTestWorkflowFailed(&execution))
+ }
+ }
+ }()
+
+ // Stream the log into Minio
+ err = e.output.SaveLog(context.Background(), execution.Id, execution.Workflow.Name, reader)
+ if err != nil {
+ log.DefaultLogger.Errorw("failed to save TestWorkflow log output", "id", execution.Id, "error", err)
+ }
+
+ wg.Wait()
+
+ err = testworkflowcontroller.Cleanup(ctx, e.clientSet, e.namespace, execution.Id)
+ if err != nil {
+ log.DefaultLogger.Errorw("failed to cleanup TestWorkflow resources", "id", execution.Id, "error", err)
+ }
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowexecutor/mock_executor.go b/pkg/tcl/testworkflowstcl/testworkflowexecutor/mock_executor.go
new file mode 100644
index 00000000000..8d8c669bb03
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowexecutor/mock_executor.go
@@ -0,0 +1,73 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowexecutor (interfaces: TestWorkflowExecutor)
+
+// Package testworkflowexecutor is a generated GoMock package.
+package testworkflowexecutor
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ testkube "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+ testworkflowprocessor "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor"
+)
+
+// MockTestWorkflowExecutor is a mock of TestWorkflowExecutor interface.
+type MockTestWorkflowExecutor struct {
+ ctrl *gomock.Controller
+ recorder *MockTestWorkflowExecutorMockRecorder
+}
+
+// MockTestWorkflowExecutorMockRecorder is the mock recorder for MockTestWorkflowExecutor.
+type MockTestWorkflowExecutorMockRecorder struct {
+ mock *MockTestWorkflowExecutor
+}
+
+// NewMockTestWorkflowExecutor creates a new mock instance.
+func NewMockTestWorkflowExecutor(ctrl *gomock.Controller) *MockTestWorkflowExecutor {
+ mock := &MockTestWorkflowExecutor{ctrl: ctrl}
+ mock.recorder = &MockTestWorkflowExecutorMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockTestWorkflowExecutor) EXPECT() *MockTestWorkflowExecutorMockRecorder {
+ return m.recorder
+}
+
+// Control mocks base method.
+func (m *MockTestWorkflowExecutor) Control(arg0 context.Context, arg1 testkube.TestWorkflowExecution) {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "Control", arg0, arg1)
+}
+
+// Control indicates an expected call of Control.
+func (mr *MockTestWorkflowExecutorMockRecorder) Control(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Control", reflect.TypeOf((*MockTestWorkflowExecutor)(nil).Control), arg0, arg1)
+}
+
+// Recover mocks base method.
+func (m *MockTestWorkflowExecutor) Recover(arg0 context.Context) {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "Recover", arg0)
+}
+
+// Recover indicates an expected call of Recover.
+func (mr *MockTestWorkflowExecutorMockRecorder) Recover(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Recover", reflect.TypeOf((*MockTestWorkflowExecutor)(nil).Recover), arg0)
+}
+
+// Schedule mocks base method.
+func (m *MockTestWorkflowExecutor) Schedule(arg0 *testworkflowprocessor.Bundle, arg1 testkube.TestWorkflowExecution) {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "Schedule", arg0, arg1)
+}
+
+// Schedule indicates an expected call of Schedule.
+func (mr *MockTestWorkflowExecutorMockRecorder) Schedule(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Schedule", reflect.TypeOf((*MockTestWorkflowExecutor)(nil).Schedule), arg0, arg1)
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/bundle.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/bundle.go
new file mode 100644
index 00000000000..5ec144df96f
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/bundle.go
@@ -0,0 +1,21 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+import (
+ batchv1 "k8s.io/api/batch/v1"
+ corev1 "k8s.io/api/core/v1"
+)
+
+type Bundle struct {
+ Secrets []corev1.Secret
+ ConfigMaps []corev1.ConfigMap
+ Job batchv1.Job
+ Signature []Signature
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/constants.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/constants.go
new file mode 100644
index 00000000000..d3cddeb8226
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/constants.go
@@ -0,0 +1,71 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ corev1 "k8s.io/api/core/v1"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/pkg/version"
+)
+
+const (
+ defaultImage = "busybox:1.36.1"
+ defaultShell = "/bin/sh"
+ defaultInternalPath = "/.tktw"
+ defaultDataPath = "/data"
+ defaultFsGroup = int64(1001)
+ ExecutionIdLabelName = "testworkflowid"
+ ExecutionIdMainPodLabelName = "testworkflowid-main"
+ SignatureAnnotationName = "testworkflows.testkube.io/signature"
+)
+
+var (
+ defaultInitPath = filepath.Join(defaultInternalPath, "init")
+ defaultStatePath = filepath.Join(defaultInternalPath, "state")
+)
+
+var (
+ defaultInitImage = getInitImage()
+ defaultToolkitImage = getToolkitImage()
+ defaultContainerConfig = testworkflowsv1.ContainerConfig{
+ Image: defaultImage,
+ Env: []corev1.EnvVar{
+ {Name: "CI", Value: "1"},
+ },
+ }
+)
+
+func getInitImage() string {
+ img := os.Getenv("TESTKUBE_TW_INIT_IMAGE")
+ if img == "" {
+ ver := version.Version
+ if ver == "" || ver == "dev" {
+ ver = "latest"
+ }
+ img = fmt.Sprintf("kubeshop/testkube-tw-init:%s", ver)
+ }
+ return img
+}
+
+func getToolkitImage() string {
+ img := os.Getenv("TESTKUBE_TW_TOOLKIT_IMAGE")
+ if img == "" {
+ ver := version.Version
+ if ver == "" || ver == "dev" {
+ ver = "latest"
+ }
+ img = fmt.Sprintf("kubeshop/testkube-tw-toolkit:%s", ver)
+ }
+ return img
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/container.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/container.go
new file mode 100644
index 00000000000..5f216915a50
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/container.go
@@ -0,0 +1,439 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+import (
+ "maps"
+ "path/filepath"
+ "slices"
+ "strings"
+
+ "github.com/pkg/errors"
+ corev1 "k8s.io/api/core/v1"
+ quantity "k8s.io/apimachinery/pkg/api/resource"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/imageinspector"
+ "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+ "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowresolver"
+)
+
+type container struct {
+ parent *container
+ Cr testworkflowsv1.ContainerConfig `expr:"include"`
+}
+
+type ContainerComposition interface {
+ Root() Container
+ Parent() Container
+ CreateChild() Container
+
+ Resolve(m ...expressionstcl.Machine) error
+}
+
+type ContainerAccessors interface {
+ Env() []corev1.EnvVar
+ EnvFrom() []corev1.EnvFromSource
+ VolumeMounts() []corev1.VolumeMount
+
+ ImagePullPolicy() corev1.PullPolicy
+ Image() string
+ Command() []string
+ Args() []string
+ WorkingDir() string
+
+ Detach() Container
+ ToKubernetesTemplate() (corev1.Container, error)
+
+ Resources() testworkflowsv1.Resources
+ SecurityContext() *corev1.SecurityContext
+}
+
+type ContainerMutations[T any] interface {
+ AppendEnv(env ...corev1.EnvVar) T
+ AppendEnvMap(env map[string]string) T
+ AppendEnvFrom(envFrom ...corev1.EnvFromSource) T
+ AppendVolumeMounts(volumeMounts ...corev1.VolumeMount) T
+ SetImagePullPolicy(policy corev1.PullPolicy) T
+ SetImage(image string) T
+ SetCommand(command ...string) T
+ SetArgs(args ...string) T
+ SetWorkingDir(workingDir string) T // "" = default to the image
+ SetResources(resources testworkflowsv1.Resources) T
+ SetSecurityContext(sc *corev1.SecurityContext) T
+
+ ApplyCR(cr *testworkflowsv1.ContainerConfig) T
+ ApplyImageData(image *imageinspector.Info) error
+ EnableToolkit(ref string) T
+}
+
+//go:generate mockgen -destination=./mock_container.go -package=testworkflowprocessor "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor" Container
+type Container interface {
+ ContainerComposition
+ ContainerAccessors
+ ContainerMutations[Container]
+}
+
+func NewContainer() Container {
+ return &container{}
+}
+
+func sum[T any](s1 []T, s2 []T) []T {
+ if len(s1) == 0 {
+ return s2
+ }
+ if len(s2) == 0 {
+ return s1
+ }
+ return append(append(make([]T, 0, len(s1)+len(s2)), s1...), s2...)
+}
+
+// Composition
+
+func (c *container) Root() Container {
+ if c.parent == nil {
+ return c
+ }
+ return c.parent.Parent()
+}
+
+func (c *container) Parent() Container {
+ return c.parent
+}
+
+func (c *container) CreateChild() Container {
+ return &container{parent: c}
+}
+
+// Getters
+
+func (c *container) Env() []corev1.EnvVar {
+ if c.parent == nil {
+ return c.Cr.Env
+ }
+ return sum(c.parent.Env(), c.Cr.Env)
+}
+
+func (c *container) EnvFrom() []corev1.EnvFromSource {
+ if c.parent == nil {
+ return c.Cr.EnvFrom
+ }
+ return sum(c.parent.EnvFrom(), c.Cr.EnvFrom)
+}
+
+func (c *container) VolumeMounts() []corev1.VolumeMount {
+ if c.parent == nil {
+ return c.Cr.VolumeMounts
+ }
+ return sum(c.parent.VolumeMounts(), c.Cr.VolumeMounts)
+}
+
+func (c *container) ImagePullPolicy() corev1.PullPolicy {
+ if c.parent == nil || c.Cr.ImagePullPolicy != "" {
+ return c.Cr.ImagePullPolicy
+ }
+ return c.parent.ImagePullPolicy()
+}
+
+func (c *container) Image() string {
+ if c.parent == nil || c.Cr.Image != "" {
+ return c.Cr.Image
+ }
+ return c.parent.Image()
+}
+
+func (c *container) Command() []string {
+ // Do not inherit command, if the Image was replaced on this depth
+ if c.parent == nil || c.Cr.Command != nil || (c.Cr.Image != "" && c.Cr.Image != c.Image()) {
+ if c.Cr.Command == nil {
+ return nil
+ }
+ return *c.Cr.Command
+ }
+ return c.parent.Command()
+}
+
+func (c *container) Args() []string {
+ // Do not inherit args, if the Image or Command was replaced on this depth
+ if c.parent == nil || (c.Cr.Args != nil && len(*c.Cr.Args) > 0) || c.Cr.Command != nil || (c.Cr.Image != "" && c.Cr.Image != c.Image()) {
+ if c.Cr.Args == nil {
+ return nil
+ }
+ return *c.Cr.Args
+ }
+ return c.parent.Args()
+}
+
+func (c *container) WorkingDir() string {
+ path := ""
+ if c.Cr.WorkingDir != nil {
+ path = *c.Cr.WorkingDir
+ }
+ if c.parent == nil {
+ return path
+ }
+ if filepath.IsAbs(path) {
+ return path
+ }
+ parentPath := c.parent.WorkingDir()
+ if parentPath == "" {
+ return path
+ }
+ return filepath.Join(parentPath, path)
+}
+
+func (c *container) Resources() (r testworkflowsv1.Resources) {
+ if c.parent != nil {
+ r = *common.Ptr(c.parent.Resources()).DeepCopy()
+ }
+ if c.Cr.Resources == nil {
+ return
+ }
+ if len(c.Cr.Resources.Requests) > 0 {
+ r.Requests = c.Cr.Resources.Requests
+ }
+ if len(c.Cr.Resources.Limits) > 0 {
+ r.Limits = c.Cr.Resources.Limits
+ }
+ return
+}
+
+func (c *container) SecurityContext() *corev1.SecurityContext {
+ if c.Cr.SecurityContext != nil {
+ return c.Cr.SecurityContext
+ }
+ if c.parent == nil {
+ return nil
+ }
+ return c.parent.SecurityContext()
+}
+
+// Mutations
+
+func (c *container) AppendEnv(env ...corev1.EnvVar) Container {
+ c.Cr.Env = append(c.Cr.Env, env...)
+ return c
+}
+
+func (c *container) AppendEnvMap(env map[string]string) Container {
+ for k, v := range env {
+ c.Cr.Env = append(c.Cr.Env, corev1.EnvVar{Name: k, Value: v})
+ }
+ return c
+}
+
+func (c *container) AppendEnvFrom(envFrom ...corev1.EnvFromSource) Container {
+ c.Cr.EnvFrom = append(c.Cr.EnvFrom, envFrom...)
+ return c
+}
+
+func (c *container) AppendVolumeMounts(volumeMounts ...corev1.VolumeMount) Container {
+ c.Cr.VolumeMounts = append(c.Cr.VolumeMounts, volumeMounts...)
+ return c
+}
+
+func (c *container) SetImagePullPolicy(policy corev1.PullPolicy) Container {
+ c.Cr.ImagePullPolicy = policy
+ return c
+}
+
+func (c *container) SetImage(image string) Container {
+ c.Cr.Image = image
+ return c
+}
+
+func (c *container) SetCommand(command ...string) Container {
+ c.Cr.Command = &command
+ return c
+}
+
+func (c *container) SetArgs(args ...string) Container {
+ c.Cr.Args = &args
+ return c
+}
+
+func (c *container) SetWorkingDir(workingDir string) Container {
+ c.Cr.WorkingDir = &workingDir
+ return c
+}
+
+func (c *container) SetResources(resources testworkflowsv1.Resources) Container {
+ c.Cr.Resources = &resources
+ return c
+}
+
+func (c *container) SetSecurityContext(sc *corev1.SecurityContext) Container {
+ c.Cr.SecurityContext = sc
+ return c
+}
+
+func (c *container) ApplyCR(config *testworkflowsv1.ContainerConfig) Container {
+ c.Cr = *testworkflowresolver.MergeContainerConfig(&c.Cr, config)
+ return c
+}
+
+func (c *container) ToContainerConfig() testworkflowsv1.ContainerConfig {
+ env := slices.Clone(c.Env())
+ for i := range env {
+ env[i] = *env[i].DeepCopy()
+ }
+ envFrom := slices.Clone(c.EnvFrom())
+ for i := range envFrom {
+ envFrom[i] = *envFrom[i].DeepCopy()
+ }
+ volumeMounts := slices.Clone(c.VolumeMounts())
+ for i := range volumeMounts {
+ volumeMounts[i] = *volumeMounts[i].DeepCopy()
+ }
+ return testworkflowsv1.ContainerConfig{
+ WorkingDir: common.Ptr(c.WorkingDir()),
+ Image: c.Image(),
+ ImagePullPolicy: c.ImagePullPolicy(),
+ Env: env,
+ EnvFrom: envFrom,
+ Command: common.Ptr(slices.Clone(c.Command())),
+ Args: common.Ptr(slices.Clone(c.Args())),
+ Resources: &testworkflowsv1.Resources{
+ Requests: maps.Clone(c.Resources().Requests),
+ Limits: maps.Clone(c.Resources().Limits),
+ },
+ SecurityContext: c.SecurityContext().DeepCopy(),
+ VolumeMounts: volumeMounts,
+ }
+}
+
+func (c *container) Detach() Container {
+ c.Cr = c.ToContainerConfig()
+ c.parent = nil
+ return c
+}
+
+func (c *container) ToKubernetesTemplate() (corev1.Container, error) {
+ cr := c.ToContainerConfig()
+ var command []string
+ if cr.Command != nil {
+ command = *cr.Command
+ }
+ var args []string
+ if cr.Args != nil {
+ args = *cr.Args
+ }
+ workingDir := ""
+ if cr.WorkingDir != nil {
+ workingDir = *cr.WorkingDir
+ }
+ resources := corev1.ResourceRequirements{}
+ if cr.Resources != nil {
+ if len(cr.Resources.Requests) > 0 {
+ resources.Requests = make(corev1.ResourceList)
+ }
+ if len(cr.Resources.Limits) > 0 {
+ resources.Limits = make(corev1.ResourceList)
+ }
+ for k, v := range cr.Resources.Requests {
+ var err error
+ resources.Requests[k], err = quantity.ParseQuantity(v.String())
+ if err != nil {
+ return corev1.Container{}, errors.Wrap(err, "parsing resources")
+ }
+ }
+ for k, v := range cr.Resources.Limits {
+ var err error
+ resources.Limits[k], err = quantity.ParseQuantity(v.String())
+ if err != nil {
+ return corev1.Container{}, errors.Wrap(err, "parsing resources")
+ }
+ }
+ }
+ return corev1.Container{
+ Image: cr.Image,
+ ImagePullPolicy: cr.ImagePullPolicy,
+ Command: command,
+ Args: args,
+ Env: cr.Env,
+ EnvFrom: cr.EnvFrom,
+ VolumeMounts: cr.VolumeMounts,
+ Resources: resources,
+ WorkingDir: workingDir,
+ SecurityContext: cr.SecurityContext,
+ }, nil
+}
+
+func (c *container) ApplyImageData(image *imageinspector.Info) error {
+ if image == nil {
+ return nil
+ }
+ err := c.Resolve(expressionstcl.NewMachine().
+ Register("image.command", image.Entrypoint).
+ Register("image.args", image.Cmd).
+ Register("image.workingDir", image.WorkingDir))
+ if err != nil {
+ return err
+ }
+ if len(c.Command()) == 0 {
+ args := c.Args()
+ c.SetCommand(image.Entrypoint...)
+ if len(args) == 0 {
+ c.SetArgs(image.Cmd...)
+ } else {
+ c.SetArgs(args...)
+ }
+ }
+ if image.WorkingDir != "" && c.WorkingDir() == "" {
+ c.SetWorkingDir(image.WorkingDir)
+ }
+ return nil
+}
+
+func (c *container) EnableToolkit(ref string) Container {
+ return c.AppendEnvMap(map[string]string{
+ "TK_REF": ref,
+ "TK_NS": "{{internal.namespace}}",
+ "TK_WF": "{{workflow.name}}",
+ "TK_EX": "{{execution.id}}",
+ "TK_C_URL": "{{internal.cloud.api.url}}",
+ "TK_C_KEY": "{{internal.cloud.api.key}}",
+ "TK_C_TLS_INSECURE": "{{internal.cloud.api.tlsInsecure}}",
+ "TK_C_SKIP_VERIFY": "{{internal.cloud.api.skipVerify}}",
+ "TK_OS_ENDPOINT": "{{internal.storage.url}}",
+ "TK_OS_ACCESSKEY": "{{internal.storage.accessKey}}",
+ "TK_OS_SECRETKEY": "{{internal.storage.secretKey}}",
+ "TK_OS_REGION": "{{internal.storage.region}}",
+ "TK_OS_TOKEN": "{{internal.storage.token}}",
+ "TK_OS_BUCKET": "{{internal.storage.bucket}}",
+ "TK_OS_SSL": "{{internal.storage.ssl}}",
+ "TK_OS_SSL_SKIP_VERIFY": "{{internal.storage.skipVerify}}",
+ "TK_OS_CERT_FILE": "{{internal.storage.certFile}}",
+ "TK_OS_KEY_FILE": "{{internal.storage.keyFile}}",
+ "TK_OS_CA_FILE": "{{internal.storage.caFile}}",
+ })
+}
+
+func (c *container) Resolve(m ...expressionstcl.Machine) error {
+ base := expressionstcl.NewMachine().
+ RegisterAccessor(func(name string) (interface{}, bool) {
+ if !strings.HasPrefix(name, "env.") {
+ return nil, false
+ }
+ env := c.Env()
+ name = name[4:]
+ for i := range env {
+ if env[i].Name == name {
+ value, err := expressionstcl.EvalTemplate(env[i].Value)
+ if err == nil {
+ return value, true
+ }
+ break
+ }
+ }
+ return nil, false
+ })
+ return expressionstcl.Simplify(c, append([]expressionstcl.Machine{base}, m...)...)
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/containerstage.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/containerstage.go
new file mode 100644
index 00000000000..d836b84949e
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/containerstage.go
@@ -0,0 +1,77 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+import (
+ "github.com/pkg/errors"
+
+ "github.com/kubeshop/testkube/pkg/imageinspector"
+ "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+)
+
+type containerStage struct {
+ stageMetadata
+ stageLifecycle
+ container Container
+}
+
+type ContainerStage interface {
+ Stage
+ Container() Container
+}
+
+func NewContainerStage(ref string, container Container) ContainerStage {
+ return &containerStage{
+ stageMetadata: stageMetadata{ref: ref},
+ container: container.CreateChild(),
+ }
+}
+
+func (s *containerStage) Len() int {
+ return 1
+}
+
+func (s *containerStage) Signature() Signature {
+ return &signature{
+ RefValue: s.ref,
+ NameValue: s.name,
+ CategoryValue: s.category,
+ OptionalValue: s.optional,
+ NegativeValue: s.negative,
+ ChildrenValue: nil,
+ }
+}
+
+func (s *containerStage) ContainerStages() []ContainerStage {
+ return []ContainerStage{s}
+}
+
+func (s *containerStage) GetImages() map[string]struct{} {
+ return map[string]struct{}{s.container.Image(): {}}
+}
+
+func (s *containerStage) Flatten() []Stage {
+ return []Stage{s}
+}
+
+func (s *containerStage) ApplyImages(images map[string]*imageinspector.Info) error {
+ return s.container.ApplyImageData(images[s.container.Image()])
+}
+
+func (s *containerStage) Resolve(m ...expressionstcl.Machine) error {
+ err := s.container.Resolve(m...)
+ if err != nil {
+ return errors.Wrap(err, "stage container")
+ }
+ return expressionstcl.Simplify(s, m...)
+}
+
+func (s *containerStage) Container() Container {
+ return s.container
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/groupstage.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/groupstage.go
new file mode 100644
index 00000000000..72b04173993
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/groupstage.go
@@ -0,0 +1,171 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+import (
+ "maps"
+
+ "github.com/pkg/errors"
+
+ "github.com/kubeshop/testkube/pkg/imageinspector"
+ "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+)
+
+type groupStage struct {
+ stageMetadata
+ stageLifecycle
+ children []Stage
+ virtual bool
+}
+
+type GroupStage interface {
+ Stage
+ Children() []Stage
+ RecursiveChildren() []Stage
+ Add(stages ...Stage) GroupStage
+}
+
+func NewGroupStage(ref string, virtual bool) GroupStage {
+ return &groupStage{
+ stageMetadata: stageMetadata{ref: ref},
+ virtual: virtual,
+ }
+}
+
+func (s *groupStage) Len() int {
+ count := 0
+ for _, ch := range s.children {
+ count += ch.Len()
+ }
+ return count
+}
+
+func (s *groupStage) Signature() Signature {
+ sig := []Signature(nil)
+ for _, ch := range s.children {
+ si := ch.Signature()
+ _, ok := ch.(GroupStage)
+ // Include children directly, if the stage is virtual
+ if ok && si.Name() == "" && !si.Optional() && !si.Negative() {
+ sig = append(sig, si.Children()...)
+ } else {
+ sig = append(sig, si)
+ }
+ }
+
+ return &signature{
+ RefValue: s.ref,
+ NameValue: s.name,
+ CategoryValue: s.category,
+ OptionalValue: s.optional,
+ NegativeValue: s.negative,
+ ChildrenValue: sig,
+ }
+}
+
+func (s *groupStage) ContainerStages() []ContainerStage {
+ c := []ContainerStage(nil)
+ for _, ch := range s.children {
+ c = append(c, ch.ContainerStages()...)
+ }
+ return c
+}
+
+func (s *groupStage) Children() []Stage {
+ return s.children
+}
+
+func (s *groupStage) RecursiveChildren() []Stage {
+ res := make([]Stage, 0)
+ for _, ch := range s.children {
+ if v, ok := ch.(GroupStage); ok {
+ res = append(res, v.RecursiveChildren()...)
+ } else {
+ res = append(res, ch)
+ }
+ }
+ return res
+}
+
+func (s *groupStage) GetImages() map[string]struct{} {
+ v := make(map[string]struct{})
+ for _, ch := range s.children {
+ maps.Copy(v, ch.GetImages())
+ }
+ return v
+}
+
+func (s *groupStage) Flatten() []Stage {
+ // Flatten children
+ next := []Stage(nil)
+ for _, ch := range s.children {
+ next = append(next, ch.Flatten()...)
+ }
+ s.children = next
+
+ // Delete empty stage
+ if len(s.children) == 0 {
+ return nil
+ }
+
+ // Flatten when it is completely virtual stage
+ if s.virtual {
+ return s.children
+ }
+
+ // Merge stage into single one below if possible
+ first := s.children[0]
+ if len(s.children) == 1 && (s.name == "" || first.Name() == "") && (s.timeout == "" || first.Timeout() == "") {
+ if first.Name() == "" {
+ first.SetName(s.name)
+ }
+ first.AppendConditions(s.condition)
+ if first.Timeout() == "" {
+ first.SetTimeout(s.timeout)
+ }
+ if s.negative {
+ first.SetNegative(!first.Negative())
+ }
+ if s.optional {
+ first.SetOptional(true)
+ }
+ return []Stage{first}
+ }
+
+ return []Stage{s}
+}
+
+func (s *groupStage) Add(stages ...Stage) GroupStage {
+ for _, ch := range stages {
+ if ch != nil {
+ s.children = append(s.children, ch.Flatten()...)
+ }
+ }
+ return s
+}
+
+func (s *groupStage) ApplyImages(images map[string]*imageinspector.Info) error {
+ for i := range s.children {
+ err := s.children[i].ApplyImages(images)
+ if err != nil {
+ return errors.Wrap(err, "applying image data")
+ }
+ }
+ return nil
+}
+
+func (s *groupStage) Resolve(m ...expressionstcl.Machine) error {
+ for i := range s.children {
+ err := s.children[i].Resolve(m...)
+ if err != nil {
+ return errors.Wrap(err, "group stage container")
+ }
+ }
+ return expressionstcl.Simplify(&s.stageMetadata, m...)
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/initprocess.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/initprocess.go
new file mode 100644
index 00000000000..274edb0bb91
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/initprocess.go
@@ -0,0 +1,208 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+import (
+ "errors"
+ "fmt"
+ "maps"
+ "strconv"
+ "strings"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/cmd/tcl/testworkflow-init/constants"
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+)
+
+type initProcess struct {
+ ref string
+ init []string
+ params []string
+ retry map[string]testworkflowsv1.RetryPolicy
+ command []string
+ args []string
+ envs []string
+ results []string
+ conditions map[string][]string
+ negative bool
+ errors []error
+}
+
+func NewInitProcess() *initProcess {
+ return &initProcess{
+ conditions: map[string][]string{},
+ retry: map[string]testworkflowsv1.RetryPolicy{},
+ }
+}
+
+func (p *initProcess) Error() error {
+ if len(p.errors) == 0 {
+ return nil
+ }
+ return errors.Join(p.errors...)
+}
+
+func (p *initProcess) SetRef(ref string) *initProcess {
+ p.ref = ref
+ return p
+}
+
+func (p *initProcess) Command() []string {
+ args := p.params
+
+ // TODO: Support nested retries
+ policy, ok := p.retry[p.ref]
+ if ok {
+ args = append(args, constants.ArgRetryCount, strconv.Itoa(int(policy.Count)), constants.ArgRetryUntil, expressionstcl.Escape(policy.Until))
+ }
+ if p.negative {
+ args = append(args, constants.ArgNegative, "true")
+ }
+ if len(p.init) > 0 {
+ args = append(args, constants.ArgInit, strings.Join(p.init, "&&"))
+ }
+ if len(p.envs) > 0 {
+ args = append(args, constants.ArgComputeEnv, strings.Join(p.envs, ","))
+ }
+ if len(p.conditions) > 0 {
+ for k, v := range p.conditions {
+ args = append(args, constants.ArgCondition, fmt.Sprintf("%s=%s", strings.Join(common.UniqueSlice(v), ","), k))
+ }
+ }
+ for _, r := range p.results {
+ args = append(args, constants.ArgResult, r)
+ }
+ return append([]string{defaultInitPath, p.ref}, append(args, constants.ArgSeparator)...)
+}
+
+func (p *initProcess) Args() []string {
+ args := make([]string, 0)
+ if len(p.command) > 0 {
+ args = p.command
+ }
+ if len(p.command) > 0 || len(p.args) > 0 {
+ args = append(args, p.args...)
+ }
+ return args
+}
+
+func (p *initProcess) param(args ...string) *initProcess {
+ p.params = append(p.params, args...)
+ return p
+}
+
+func (p *initProcess) compile(expr ...string) []string {
+ for i, e := range expr {
+ res, err := expressionstcl.Compile(e)
+ if err == nil {
+ expr[i] = res.String()
+ } else {
+ p.errors = append(p.errors, fmt.Errorf("resolving expression: %s: %s", expr[i], err.Error()))
+ }
+ }
+ return expr
+}
+
+func (p *initProcess) SetCommand(command ...string) *initProcess {
+ p.command = command
+ return p
+}
+
+func (p *initProcess) SetArgs(args ...string) *initProcess {
+ p.args = args
+ return p
+}
+
+func (p *initProcess) AddTimeout(duration string, refs ...string) *initProcess {
+ return p.param(constants.ArgTimeout, fmt.Sprintf("%s=%s", strings.Join(refs, ","), duration))
+}
+
+func (p *initProcess) SetInitialStatus(expr ...string) *initProcess {
+ p.init = nil
+ for _, v := range p.compile(expr...) {
+ p.init = append(p.init, v)
+ }
+ return p
+}
+
+func (p *initProcess) PrependInitialStatus(expr ...string) *initProcess {
+ init := []string(nil)
+ for _, v := range p.compile(expr...) {
+ init = append(init, v)
+ }
+ p.init = append(init, p.init...)
+ return p
+}
+
+func (p *initProcess) AddComputedEnvs(names ...string) *initProcess {
+ p.envs = append(p.envs, names...)
+ return p
+}
+
+func (p *initProcess) SetNegative(negative bool) *initProcess {
+ p.negative = negative
+ return p
+}
+
+func (p *initProcess) AddResult(condition string, refs ...string) *initProcess {
+ if len(refs) == 0 || condition == "" {
+ return p
+ }
+ p.results = append(p.results, fmt.Sprintf("%s=%s", strings.Join(refs, ","), p.compile(condition)[0]))
+ return p
+}
+
+func (p *initProcess) ResetResults() *initProcess {
+ p.results = nil
+ return p
+}
+
+func (p *initProcess) AddCondition(condition string, refs ...string) *initProcess {
+ if len(refs) == 0 || condition == "" {
+ return p
+ }
+ expr := p.compile(condition)[0]
+ p.conditions[expr] = append(p.conditions[expr], refs...)
+ return p
+}
+
+func (p *initProcess) ResetCondition() *initProcess {
+ p.conditions = make(map[string][]string)
+ return p
+}
+
+func (p *initProcess) AddRetryPolicy(policy testworkflowsv1.RetryPolicy, ref string) *initProcess {
+ if policy.Count <= 0 {
+ delete(p.retry, ref)
+ return p
+ }
+ until := policy.Until
+ if until == "" {
+ until = "passed"
+ }
+ p.retry[ref] = testworkflowsv1.RetryPolicy{Count: policy.Count, Until: until}
+ return p
+}
+
+func (p *initProcess) Children(ref string) *initProcess {
+ return &initProcess{
+ ref: ref,
+ params: p.params,
+ retry: maps.Clone(p.retry),
+ command: p.command,
+ args: p.args,
+ init: p.init,
+ envs: p.envs,
+ results: p.results,
+ conditions: maps.Clone(p.conditions),
+ negative: p.negative,
+ errors: p.errors,
+ }
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/intermediate.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/intermediate.go
new file mode 100644
index 00000000000..a51e097a906
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/intermediate.go
@@ -0,0 +1,189 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+import (
+ "errors"
+
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowresolver"
+)
+
+const maxConfigMapFileSize = 950 * 1024
+
+//go:generate mockgen -destination=./mock_intermediate.go -package=testworkflowprocessor "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor" Intermediate
+type Intermediate interface {
+ RefCounter
+
+ ContainerDefaults() Container
+ PodConfig() testworkflowsv1.PodConfig
+ JobConfig() testworkflowsv1.JobConfig
+
+ ConfigMaps() []corev1.ConfigMap
+ Secrets() []corev1.Secret
+ Volumes() []corev1.Volume
+
+ AppendJobConfig(cfg *testworkflowsv1.JobConfig) Intermediate
+ AppendPodConfig(cfg *testworkflowsv1.PodConfig) Intermediate
+
+ AddConfigMap(configMap corev1.ConfigMap) Intermediate
+ AddSecret(secret corev1.Secret) Intermediate
+ AddVolume(volume corev1.Volume) Intermediate
+
+ AddEmptyDirVolume(source *corev1.EmptyDirVolumeSource, mountPath string) corev1.VolumeMount
+
+ AddTextFile(file string) (corev1.VolumeMount, error)
+ AddBinaryFile(file []byte) (corev1.VolumeMount, error)
+}
+
+type intermediate struct {
+ refCounter
+
+ // Routine
+ Root GroupStage `expr:"include"`
+ Container Container `expr:"include"`
+
+ // Job & Pod resources & data
+ Pod testworkflowsv1.PodConfig `expr:"include"`
+ Job testworkflowsv1.JobConfig `expr:"include"`
+
+ // Actual Kubernetes resources to use
+ Secs []corev1.Secret `expr:"force"`
+ Cfgs []corev1.ConfigMap `expr:"force"`
+
+ // Storing files
+ currentConfigMapStorage *corev1.ConfigMap
+ estimatedConfigMapStorage int
+}
+
+func NewIntermediate() Intermediate {
+ return &intermediate{
+ Root: NewGroupStage("", true),
+ Container: NewContainer(),
+ }
+}
+
+func (s *intermediate) ContainerDefaults() Container {
+ return s.Container
+}
+
+func (s *intermediate) JobConfig() testworkflowsv1.JobConfig {
+ return s.Job
+}
+
+func (s *intermediate) PodConfig() testworkflowsv1.PodConfig {
+ return s.Pod
+}
+
+func (s *intermediate) ConfigMaps() []corev1.ConfigMap {
+ return s.Cfgs
+}
+
+func (s *intermediate) Secrets() []corev1.Secret {
+ return s.Secs
+}
+
+func (s *intermediate) Volumes() []corev1.Volume {
+ return s.Pod.Volumes
+}
+
+func (s *intermediate) AppendJobConfig(cfg *testworkflowsv1.JobConfig) Intermediate {
+ s.Job = *testworkflowresolver.MergeJobConfig(&s.Job, cfg)
+ return s
+}
+
+func (s *intermediate) AppendPodConfig(cfg *testworkflowsv1.PodConfig) Intermediate {
+ s.Pod = *testworkflowresolver.MergePodConfig(&s.Pod, cfg)
+ return s
+}
+
+func (s *intermediate) AddVolume(volume corev1.Volume) Intermediate {
+ s.Pod.Volumes = append(s.Pod.Volumes, volume)
+ return s
+}
+
+func (s *intermediate) AddConfigMap(configMap corev1.ConfigMap) Intermediate {
+ s.Cfgs = append(s.Cfgs, configMap)
+ return s
+}
+
+func (s *intermediate) AddSecret(secret corev1.Secret) Intermediate {
+ s.Secs = append(s.Secs, secret)
+ return s
+}
+
+func (s *intermediate) AddEmptyDirVolume(source *corev1.EmptyDirVolumeSource, mountPath string) corev1.VolumeMount {
+ if source == nil {
+ source = &corev1.EmptyDirVolumeSource{}
+ }
+ ref := s.NextRef()
+ s.AddVolume(corev1.Volume{Name: ref, VolumeSource: corev1.VolumeSource{EmptyDir: source}})
+ return corev1.VolumeMount{Name: ref, MountPath: mountPath}
+}
+
+// Handling files
+
+func (s *intermediate) getInternalConfigMapStorage(size int) *corev1.ConfigMap {
+ if size > maxConfigMapFileSize {
+ return nil
+ }
+ if size+s.estimatedConfigMapStorage > maxConfigMapFileSize || s.currentConfigMapStorage == nil {
+ ref := s.NextRef()
+ s.Cfgs = append(s.Cfgs, corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{Name: "{{execution.id}}-" + ref},
+ Immutable: common.Ptr(true),
+ Data: map[string]string{},
+ BinaryData: map[string][]byte{},
+ })
+ s.currentConfigMapStorage = &s.Cfgs[len(s.Cfgs)-1]
+ s.Pod.Volumes = append(s.Pod.Volumes, corev1.Volume{
+ Name: s.currentConfigMapStorage.Name + "-vol",
+ VolumeSource: corev1.VolumeSource{
+ ConfigMap: &corev1.ConfigMapVolumeSource{
+ LocalObjectReference: corev1.LocalObjectReference{Name: s.currentConfigMapStorage.Name},
+ },
+ },
+ })
+ }
+ return s.currentConfigMapStorage
+}
+
+func (s *intermediate) AddTextFile(file string) (corev1.VolumeMount, error) {
+ cfg := s.getInternalConfigMapStorage(len(file))
+ if cfg == nil {
+ return corev1.VolumeMount{}, errors.New("the maximum file size is 950KiB")
+ }
+ s.estimatedConfigMapStorage += len(file)
+ ref := s.NextRef()
+ cfg.Data[ref] = file
+ return corev1.VolumeMount{
+ Name: cfg.Name + "-vol",
+ ReadOnly: true,
+ SubPath: ref,
+ }, nil
+}
+
+func (s *intermediate) AddBinaryFile(file []byte) (corev1.VolumeMount, error) {
+ cfg := s.getInternalConfigMapStorage(len(file))
+ if cfg == nil {
+ return corev1.VolumeMount{}, errors.New("the maximum file size is 950KiB")
+ }
+ s.estimatedConfigMapStorage += len(file)
+ ref := s.NextRef()
+ cfg.BinaryData[ref] = file
+ return corev1.VolumeMount{
+ Name: cfg.Name + "-vol",
+ ReadOnly: true,
+ SubPath: ref,
+ }, nil
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/mock_container.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/mock_container.go
new file mode 100644
index 00000000000..71ea23db0a6
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/mock_container.go
@@ -0,0 +1,469 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor (interfaces: Container)
+
+// Package testworkflowprocessor is a generated GoMock package.
+package testworkflowprocessor
+
+import (
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ v1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ imageinspector "github.com/kubeshop/testkube/pkg/imageinspector"
+ expressionstcl "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+ v10 "k8s.io/api/core/v1"
+)
+
+// MockContainer is a mock of Container interface.
+type MockContainer struct {
+ ctrl *gomock.Controller
+ recorder *MockContainerMockRecorder
+}
+
+// MockContainerMockRecorder is the mock recorder for MockContainer.
+type MockContainerMockRecorder struct {
+ mock *MockContainer
+}
+
+// NewMockContainer creates a new mock instance.
+func NewMockContainer(ctrl *gomock.Controller) *MockContainer {
+ mock := &MockContainer{ctrl: ctrl}
+ mock.recorder = &MockContainerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockContainer) EXPECT() *MockContainerMockRecorder {
+ return m.recorder
+}
+
+// AppendEnv mocks base method.
+func (m *MockContainer) AppendEnv(arg0 ...v10.EnvVar) Container {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "AppendEnv", varargs...)
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// AppendEnv indicates an expected call of AppendEnv.
+func (mr *MockContainerMockRecorder) AppendEnv(arg0 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendEnv", reflect.TypeOf((*MockContainer)(nil).AppendEnv), arg0...)
+}
+
+// AppendEnvFrom mocks base method.
+func (m *MockContainer) AppendEnvFrom(arg0 ...v10.EnvFromSource) Container {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "AppendEnvFrom", varargs...)
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// AppendEnvFrom indicates an expected call of AppendEnvFrom.
+func (mr *MockContainerMockRecorder) AppendEnvFrom(arg0 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendEnvFrom", reflect.TypeOf((*MockContainer)(nil).AppendEnvFrom), arg0...)
+}
+
+// AppendEnvMap mocks base method.
+func (m *MockContainer) AppendEnvMap(arg0 map[string]string) Container {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AppendEnvMap", arg0)
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// AppendEnvMap indicates an expected call of AppendEnvMap.
+func (mr *MockContainerMockRecorder) AppendEnvMap(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendEnvMap", reflect.TypeOf((*MockContainer)(nil).AppendEnvMap), arg0)
+}
+
+// AppendVolumeMounts mocks base method.
+func (m *MockContainer) AppendVolumeMounts(arg0 ...v10.VolumeMount) Container {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "AppendVolumeMounts", varargs...)
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// AppendVolumeMounts indicates an expected call of AppendVolumeMounts.
+func (mr *MockContainerMockRecorder) AppendVolumeMounts(arg0 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendVolumeMounts", reflect.TypeOf((*MockContainer)(nil).AppendVolumeMounts), arg0...)
+}
+
+// ApplyCR mocks base method.
+func (m *MockContainer) ApplyCR(arg0 *v1.ContainerConfig) Container {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ApplyCR", arg0)
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// ApplyCR indicates an expected call of ApplyCR.
+func (mr *MockContainerMockRecorder) ApplyCR(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyCR", reflect.TypeOf((*MockContainer)(nil).ApplyCR), arg0)
+}
+
+// ApplyImageData mocks base method.
+func (m *MockContainer) ApplyImageData(arg0 *imageinspector.Info) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ApplyImageData", arg0)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ApplyImageData indicates an expected call of ApplyImageData.
+func (mr *MockContainerMockRecorder) ApplyImageData(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyImageData", reflect.TypeOf((*MockContainer)(nil).ApplyImageData), arg0)
+}
+
+// Args mocks base method.
+func (m *MockContainer) Args() []string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Args")
+ ret0, _ := ret[0].([]string)
+ return ret0
+}
+
+// Args indicates an expected call of Args.
+func (mr *MockContainerMockRecorder) Args() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Args", reflect.TypeOf((*MockContainer)(nil).Args))
+}
+
+// Command mocks base method.
+func (m *MockContainer) Command() []string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Command")
+ ret0, _ := ret[0].([]string)
+ return ret0
+}
+
+// Command indicates an expected call of Command.
+func (mr *MockContainerMockRecorder) Command() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Command", reflect.TypeOf((*MockContainer)(nil).Command))
+}
+
+// CreateChild mocks base method.
+func (m *MockContainer) CreateChild() Container {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreateChild")
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// CreateChild indicates an expected call of CreateChild.
+func (mr *MockContainerMockRecorder) CreateChild() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateChild", reflect.TypeOf((*MockContainer)(nil).CreateChild))
+}
+
+// Detach mocks base method.
+func (m *MockContainer) Detach() Container {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Detach")
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// Detach indicates an expected call of Detach.
+func (mr *MockContainerMockRecorder) Detach() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Detach", reflect.TypeOf((*MockContainer)(nil).Detach))
+}
+
+// Env mocks base method.
+func (m *MockContainer) Env() []v10.EnvVar {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Env")
+ ret0, _ := ret[0].([]v10.EnvVar)
+ return ret0
+}
+
+// Env indicates an expected call of Env.
+func (mr *MockContainerMockRecorder) Env() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Env", reflect.TypeOf((*MockContainer)(nil).Env))
+}
+
+// EnvFrom mocks base method.
+func (m *MockContainer) EnvFrom() []v10.EnvFromSource {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "EnvFrom")
+ ret0, _ := ret[0].([]v10.EnvFromSource)
+ return ret0
+}
+
+// EnvFrom indicates an expected call of EnvFrom.
+func (mr *MockContainerMockRecorder) EnvFrom() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnvFrom", reflect.TypeOf((*MockContainer)(nil).EnvFrom))
+}
+
+// Image mocks base method.
+func (m *MockContainer) Image() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Image")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// Image indicates an expected call of Image.
+func (mr *MockContainerMockRecorder) Image() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Image", reflect.TypeOf((*MockContainer)(nil).Image))
+}
+
+// ImagePullPolicy mocks base method.
+func (m *MockContainer) ImagePullPolicy() v10.PullPolicy {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ImagePullPolicy")
+ ret0, _ := ret[0].(v10.PullPolicy)
+ return ret0
+}
+
+// ImagePullPolicy indicates an expected call of ImagePullPolicy.
+func (mr *MockContainerMockRecorder) ImagePullPolicy() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImagePullPolicy", reflect.TypeOf((*MockContainer)(nil).ImagePullPolicy))
+}
+
+// Parent mocks base method.
+func (m *MockContainer) Parent() Container {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Parent")
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// Parent indicates an expected call of Parent.
+func (mr *MockContainerMockRecorder) Parent() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Parent", reflect.TypeOf((*MockContainer)(nil).Parent))
+}
+
+// Resolve mocks base method.
+func (m *MockContainer) Resolve(arg0 ...expressionstcl.Machine) error {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Resolve", varargs...)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Resolve indicates an expected call of Resolve.
+func (mr *MockContainerMockRecorder) Resolve(arg0 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*MockContainer)(nil).Resolve), arg0...)
+}
+
+// Resources mocks base method.
+func (m *MockContainer) Resources() v1.Resources {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Resources")
+ ret0, _ := ret[0].(v1.Resources)
+ return ret0
+}
+
+// Resources indicates an expected call of Resources.
+func (mr *MockContainerMockRecorder) Resources() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resources", reflect.TypeOf((*MockContainer)(nil).Resources))
+}
+
+// Root mocks base method.
+func (m *MockContainer) Root() Container {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Root")
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// Root indicates an expected call of Root.
+func (mr *MockContainerMockRecorder) Root() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Root", reflect.TypeOf((*MockContainer)(nil).Root))
+}
+
+// SecurityContext mocks base method.
+func (m *MockContainer) SecurityContext() *v10.SecurityContext {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SecurityContext")
+ ret0, _ := ret[0].(*v10.SecurityContext)
+ return ret0
+}
+
+// SecurityContext indicates an expected call of SecurityContext.
+func (mr *MockContainerMockRecorder) SecurityContext() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecurityContext", reflect.TypeOf((*MockContainer)(nil).SecurityContext))
+}
+
+// SetArgs mocks base method.
+func (m *MockContainer) SetArgs(arg0 ...string) Container {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SetArgs", varargs...)
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// SetArgs indicates an expected call of SetArgs.
+func (mr *MockContainerMockRecorder) SetArgs(arg0 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetArgs", reflect.TypeOf((*MockContainer)(nil).SetArgs), arg0...)
+}
+
+// SetCommand mocks base method.
+func (m *MockContainer) SetCommand(arg0 ...string) Container {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "SetCommand", varargs...)
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// SetCommand indicates an expected call of SetCommand.
+func (mr *MockContainerMockRecorder) SetCommand(arg0 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCommand", reflect.TypeOf((*MockContainer)(nil).SetCommand), arg0...)
+}
+
+// SetImage mocks base method.
+func (m *MockContainer) SetImage(arg0 string) Container {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetImage", arg0)
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// SetImage indicates an expected call of SetImage.
+func (mr *MockContainerMockRecorder) SetImage(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetImage", reflect.TypeOf((*MockContainer)(nil).SetImage), arg0)
+}
+
+// SetImagePullPolicy mocks base method.
+func (m *MockContainer) SetImagePullPolicy(arg0 v10.PullPolicy) Container {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetImagePullPolicy", arg0)
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// SetImagePullPolicy indicates an expected call of SetImagePullPolicy.
+func (mr *MockContainerMockRecorder) SetImagePullPolicy(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetImagePullPolicy", reflect.TypeOf((*MockContainer)(nil).SetImagePullPolicy), arg0)
+}
+
+// SetResources mocks base method.
+func (m *MockContainer) SetResources(arg0 v1.Resources) Container {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetResources", arg0)
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// SetResources indicates an expected call of SetResources.
+func (mr *MockContainerMockRecorder) SetResources(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetResources", reflect.TypeOf((*MockContainer)(nil).SetResources), arg0)
+}
+
+// SetSecurityContext mocks base method.
+func (m *MockContainer) SetSecurityContext(arg0 *v10.SecurityContext) Container {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetSecurityContext", arg0)
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// SetSecurityContext indicates an expected call of SetSecurityContext.
+func (mr *MockContainerMockRecorder) SetSecurityContext(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSecurityContext", reflect.TypeOf((*MockContainer)(nil).SetSecurityContext), arg0)
+}
+
+// SetWorkingDir mocks base method.
+func (m *MockContainer) SetWorkingDir(arg0 string) Container {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetWorkingDir", arg0)
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// SetWorkingDir indicates an expected call of SetWorkingDir.
+func (mr *MockContainerMockRecorder) SetWorkingDir(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetWorkingDir", reflect.TypeOf((*MockContainer)(nil).SetWorkingDir), arg0)
+}
+
+// ToKubernetesTemplate mocks base method.
+func (m *MockContainer) ToKubernetesTemplate() (v10.Container, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ToKubernetesTemplate")
+ ret0, _ := ret[0].(v10.Container)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ToKubernetesTemplate indicates an expected call of ToKubernetesTemplate.
+func (mr *MockContainerMockRecorder) ToKubernetesTemplate() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToKubernetesTemplate", reflect.TypeOf((*MockContainer)(nil).ToKubernetesTemplate))
+}
+
+// VolumeMounts mocks base method.
+func (m *MockContainer) VolumeMounts() []v10.VolumeMount {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "VolumeMounts")
+ ret0, _ := ret[0].([]v10.VolumeMount)
+ return ret0
+}
+
+// VolumeMounts indicates an expected call of VolumeMounts.
+func (mr *MockContainerMockRecorder) VolumeMounts() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeMounts", reflect.TypeOf((*MockContainer)(nil).VolumeMounts))
+}
+
+// WorkingDir mocks base method.
+func (m *MockContainer) WorkingDir() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "WorkingDir")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// WorkingDir indicates an expected call of WorkingDir.
+func (mr *MockContainerMockRecorder) WorkingDir() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WorkingDir", reflect.TypeOf((*MockContainer)(nil).WorkingDir))
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/mock_intermediate.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/mock_intermediate.go
new file mode 100644
index 00000000000..fef24c00cd0
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/mock_intermediate.go
@@ -0,0 +1,248 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor (interfaces: Intermediate)
+
+// Package testworkflowprocessor is a generated GoMock package.
+package testworkflowprocessor
+
+import (
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ v1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ v10 "k8s.io/api/core/v1"
+)
+
+// MockIntermediate is a mock of Intermediate interface.
+type MockIntermediate struct {
+ ctrl *gomock.Controller
+ recorder *MockIntermediateMockRecorder
+}
+
+// MockIntermediateMockRecorder is the mock recorder for MockIntermediate.
+type MockIntermediateMockRecorder struct {
+ mock *MockIntermediate
+}
+
+// NewMockIntermediate creates a new mock instance.
+func NewMockIntermediate(ctrl *gomock.Controller) *MockIntermediate {
+ mock := &MockIntermediate{ctrl: ctrl}
+ mock.recorder = &MockIntermediateMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockIntermediate) EXPECT() *MockIntermediateMockRecorder {
+ return m.recorder
+}
+
+// AddBinaryFile mocks base method.
+func (m *MockIntermediate) AddBinaryFile(arg0 []byte) (v10.VolumeMount, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AddBinaryFile", arg0)
+ ret0, _ := ret[0].(v10.VolumeMount)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// AddBinaryFile indicates an expected call of AddBinaryFile.
+func (mr *MockIntermediateMockRecorder) AddBinaryFile(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddBinaryFile", reflect.TypeOf((*MockIntermediate)(nil).AddBinaryFile), arg0)
+}
+
+// AddConfigMap mocks base method.
+func (m *MockIntermediate) AddConfigMap(arg0 v10.ConfigMap) Intermediate {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AddConfigMap", arg0)
+ ret0, _ := ret[0].(Intermediate)
+ return ret0
+}
+
+// AddConfigMap indicates an expected call of AddConfigMap.
+func (mr *MockIntermediateMockRecorder) AddConfigMap(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddConfigMap", reflect.TypeOf((*MockIntermediate)(nil).AddConfigMap), arg0)
+}
+
+// AddEmptyDirVolume mocks base method.
+func (m *MockIntermediate) AddEmptyDirVolume(arg0 *v10.EmptyDirVolumeSource, arg1 string) v10.VolumeMount {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AddEmptyDirVolume", arg0, arg1)
+ ret0, _ := ret[0].(v10.VolumeMount)
+ return ret0
+}
+
+// AddEmptyDirVolume indicates an expected call of AddEmptyDirVolume.
+func (mr *MockIntermediateMockRecorder) AddEmptyDirVolume(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEmptyDirVolume", reflect.TypeOf((*MockIntermediate)(nil).AddEmptyDirVolume), arg0, arg1)
+}
+
+// AddSecret mocks base method.
+func (m *MockIntermediate) AddSecret(arg0 v10.Secret) Intermediate {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AddSecret", arg0)
+ ret0, _ := ret[0].(Intermediate)
+ return ret0
+}
+
+// AddSecret indicates an expected call of AddSecret.
+func (mr *MockIntermediateMockRecorder) AddSecret(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddSecret", reflect.TypeOf((*MockIntermediate)(nil).AddSecret), arg0)
+}
+
+// AddTextFile mocks base method.
+func (m *MockIntermediate) AddTextFile(arg0 string) (v10.VolumeMount, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AddTextFile", arg0)
+ ret0, _ := ret[0].(v10.VolumeMount)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// AddTextFile indicates an expected call of AddTextFile.
+func (mr *MockIntermediateMockRecorder) AddTextFile(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddTextFile", reflect.TypeOf((*MockIntermediate)(nil).AddTextFile), arg0)
+}
+
+// AddVolume mocks base method.
+func (m *MockIntermediate) AddVolume(arg0 v10.Volume) Intermediate {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AddVolume", arg0)
+ ret0, _ := ret[0].(Intermediate)
+ return ret0
+}
+
+// AddVolume indicates an expected call of AddVolume.
+func (mr *MockIntermediateMockRecorder) AddVolume(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddVolume", reflect.TypeOf((*MockIntermediate)(nil).AddVolume), arg0)
+}
+
+// AppendJobConfig mocks base method.
+func (m *MockIntermediate) AppendJobConfig(arg0 *v1.JobConfig) Intermediate {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AppendJobConfig", arg0)
+ ret0, _ := ret[0].(Intermediate)
+ return ret0
+}
+
+// AppendJobConfig indicates an expected call of AppendJobConfig.
+func (mr *MockIntermediateMockRecorder) AppendJobConfig(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendJobConfig", reflect.TypeOf((*MockIntermediate)(nil).AppendJobConfig), arg0)
+}
+
+// AppendPodConfig mocks base method.
+func (m *MockIntermediate) AppendPodConfig(arg0 *v1.PodConfig) Intermediate {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AppendPodConfig", arg0)
+ ret0, _ := ret[0].(Intermediate)
+ return ret0
+}
+
+// AppendPodConfig indicates an expected call of AppendPodConfig.
+func (mr *MockIntermediateMockRecorder) AppendPodConfig(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendPodConfig", reflect.TypeOf((*MockIntermediate)(nil).AppendPodConfig), arg0)
+}
+
+// ConfigMaps mocks base method.
+func (m *MockIntermediate) ConfigMaps() []v10.ConfigMap {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ConfigMaps")
+ ret0, _ := ret[0].([]v10.ConfigMap)
+ return ret0
+}
+
+// ConfigMaps indicates an expected call of ConfigMaps.
+func (mr *MockIntermediateMockRecorder) ConfigMaps() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigMaps", reflect.TypeOf((*MockIntermediate)(nil).ConfigMaps))
+}
+
+// ContainerDefaults mocks base method.
+func (m *MockIntermediate) ContainerDefaults() Container {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ContainerDefaults")
+ ret0, _ := ret[0].(Container)
+ return ret0
+}
+
+// ContainerDefaults indicates an expected call of ContainerDefaults.
+func (mr *MockIntermediateMockRecorder) ContainerDefaults() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerDefaults", reflect.TypeOf((*MockIntermediate)(nil).ContainerDefaults))
+}
+
+// JobConfig mocks base method.
+func (m *MockIntermediate) JobConfig() v1.JobConfig {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "JobConfig")
+ ret0, _ := ret[0].(v1.JobConfig)
+ return ret0
+}
+
+// JobConfig indicates an expected call of JobConfig.
+func (mr *MockIntermediateMockRecorder) JobConfig() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JobConfig", reflect.TypeOf((*MockIntermediate)(nil).JobConfig))
+}
+
+// NextRef mocks base method.
+func (m *MockIntermediate) NextRef() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "NextRef")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// NextRef indicates an expected call of NextRef.
+func (mr *MockIntermediateMockRecorder) NextRef() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextRef", reflect.TypeOf((*MockIntermediate)(nil).NextRef))
+}
+
+// PodConfig mocks base method.
+func (m *MockIntermediate) PodConfig() v1.PodConfig {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PodConfig")
+ ret0, _ := ret[0].(v1.PodConfig)
+ return ret0
+}
+
+// PodConfig indicates an expected call of PodConfig.
+func (mr *MockIntermediateMockRecorder) PodConfig() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PodConfig", reflect.TypeOf((*MockIntermediate)(nil).PodConfig))
+}
+
+// Secrets mocks base method.
+func (m *MockIntermediate) Secrets() []v10.Secret {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Secrets")
+ ret0, _ := ret[0].([]v10.Secret)
+ return ret0
+}
+
+// Secrets indicates an expected call of Secrets.
+func (mr *MockIntermediateMockRecorder) Secrets() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Secrets", reflect.TypeOf((*MockIntermediate)(nil).Secrets))
+}
+
+// Volumes mocks base method.
+func (m *MockIntermediate) Volumes() []v10.Volume {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Volumes")
+ ret0, _ := ret[0].([]v10.Volume)
+ return ret0
+}
+
+// Volumes indicates an expected call of Volumes.
+func (mr *MockIntermediateMockRecorder) Volumes() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Volumes", reflect.TypeOf((*MockIntermediate)(nil).Volumes))
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/mock_internalprocessor.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/mock_internalprocessor.go
new file mode 100644
index 00000000000..4cbc3ae7ab7
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/mock_internalprocessor.go
@@ -0,0 +1,50 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor (interfaces: InternalProcessor)
+
+// Package testworkflowprocessor is a generated GoMock package.
+package testworkflowprocessor
+
+import (
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ v1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+)
+
+// MockInternalProcessor is a mock of InternalProcessor interface.
+type MockInternalProcessor struct {
+ ctrl *gomock.Controller
+ recorder *MockInternalProcessorMockRecorder
+}
+
+// MockInternalProcessorMockRecorder is the mock recorder for MockInternalProcessor.
+type MockInternalProcessorMockRecorder struct {
+ mock *MockInternalProcessor
+}
+
+// NewMockInternalProcessor creates a new mock instance.
+func NewMockInternalProcessor(ctrl *gomock.Controller) *MockInternalProcessor {
+ mock := &MockInternalProcessor{ctrl: ctrl}
+ mock.recorder = &MockInternalProcessorMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockInternalProcessor) EXPECT() *MockInternalProcessorMockRecorder {
+ return m.recorder
+}
+
+// Process mocks base method.
+func (m *MockInternalProcessor) Process(arg0 Intermediate, arg1 Container, arg2 v1.Step) (Stage, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Process", arg0, arg1, arg2)
+ ret0, _ := ret[0].(Stage)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Process indicates an expected call of Process.
+func (mr *MockInternalProcessorMockRecorder) Process(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Process", reflect.TypeOf((*MockInternalProcessor)(nil).Process), arg0, arg1, arg2)
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/mock_processor.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/mock_processor.go
new file mode 100644
index 00000000000..50759cc8c93
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/mock_processor.go
@@ -0,0 +1,71 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor (interfaces: Processor)
+
+// Package testworkflowprocessor is a generated GoMock package.
+package testworkflowprocessor
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ v1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ expressionstcl "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+)
+
+// MockProcessor is a mock of Processor interface.
+type MockProcessor struct {
+ ctrl *gomock.Controller
+ recorder *MockProcessorMockRecorder
+}
+
+// MockProcessorMockRecorder is the mock recorder for MockProcessor.
+type MockProcessorMockRecorder struct {
+ mock *MockProcessor
+}
+
+// NewMockProcessor creates a new mock instance.
+func NewMockProcessor(ctrl *gomock.Controller) *MockProcessor {
+ mock := &MockProcessor{ctrl: ctrl}
+ mock.recorder = &MockProcessorMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockProcessor) EXPECT() *MockProcessorMockRecorder {
+ return m.recorder
+}
+
+// Bundle mocks base method.
+func (m *MockProcessor) Bundle(arg0 context.Context, arg1 *v1.TestWorkflow, arg2 ...expressionstcl.Machine) (*Bundle, error) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Bundle", varargs...)
+ ret0, _ := ret[0].(*Bundle)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Bundle indicates an expected call of Bundle.
+func (mr *MockProcessorMockRecorder) Bundle(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bundle", reflect.TypeOf((*MockProcessor)(nil).Bundle), varargs...)
+}
+
+// Register mocks base method.
+func (m *MockProcessor) Register(arg0 func(InternalProcessor, Intermediate, Container, v1.Step) (Stage, error)) Processor {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Register", arg0)
+ ret0, _ := ret[0].(Processor)
+ return ret0
+}
+
+// Register indicates an expected call of Register.
+func (mr *MockProcessorMockRecorder) Register(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockProcessor)(nil).Register), arg0)
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/mock_stage.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/mock_stage.go
new file mode 100644
index 00000000000..5b23ca23f04
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/mock_stage.go
@@ -0,0 +1,367 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor (interfaces: Stage)
+
+// Package testworkflowprocessor is a generated GoMock package.
+package testworkflowprocessor
+
+import (
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ v1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ imageinspector "github.com/kubeshop/testkube/pkg/imageinspector"
+ expressionstcl "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+)
+
+// MockStage is a mock of Stage interface.
+type MockStage struct {
+ ctrl *gomock.Controller
+ recorder *MockStageMockRecorder
+}
+
+// MockStageMockRecorder is the mock recorder for MockStage.
+type MockStageMockRecorder struct {
+ mock *MockStage
+}
+
+// NewMockStage creates a new mock instance.
+func NewMockStage(ctrl *gomock.Controller) *MockStage {
+ mock := &MockStage{ctrl: ctrl}
+ mock.recorder = &MockStageMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockStage) EXPECT() *MockStageMockRecorder {
+ return m.recorder
+}
+
+// AppendConditions mocks base method.
+func (m *MockStage) AppendConditions(arg0 ...string) StageLifecycle {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "AppendConditions", varargs...)
+ ret0, _ := ret[0].(StageLifecycle)
+ return ret0
+}
+
+// AppendConditions indicates an expected call of AppendConditions.
+func (mr *MockStageMockRecorder) AppendConditions(arg0 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendConditions", reflect.TypeOf((*MockStage)(nil).AppendConditions), arg0...)
+}
+
+// ApplyImages mocks base method.
+func (m *MockStage) ApplyImages(arg0 map[string]*imageinspector.Info) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ApplyImages", arg0)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ApplyImages indicates an expected call of ApplyImages.
+func (mr *MockStageMockRecorder) ApplyImages(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyImages", reflect.TypeOf((*MockStage)(nil).ApplyImages), arg0)
+}
+
+// Category mocks base method.
+func (m *MockStage) Category() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Category")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// Category indicates an expected call of Category.
+func (mr *MockStageMockRecorder) Category() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Category", reflect.TypeOf((*MockStage)(nil).Category))
+}
+
+// Condition mocks base method.
+func (m *MockStage) Condition() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Condition")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// Condition indicates an expected call of Condition.
+func (mr *MockStageMockRecorder) Condition() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Condition", reflect.TypeOf((*MockStage)(nil).Condition))
+}
+
+// ContainerStages mocks base method.
+func (m *MockStage) ContainerStages() []ContainerStage {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ContainerStages")
+ ret0, _ := ret[0].([]ContainerStage)
+ return ret0
+}
+
+// ContainerStages indicates an expected call of ContainerStages.
+func (mr *MockStageMockRecorder) ContainerStages() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStages", reflect.TypeOf((*MockStage)(nil).ContainerStages))
+}
+
+// Flatten mocks base method.
+func (m *MockStage) Flatten() []Stage {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Flatten")
+ ret0, _ := ret[0].([]Stage)
+ return ret0
+}
+
+// Flatten indicates an expected call of Flatten.
+func (mr *MockStageMockRecorder) Flatten() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Flatten", reflect.TypeOf((*MockStage)(nil).Flatten))
+}
+
+// GetImages mocks base method.
+func (m *MockStage) GetImages() map[string]struct{} {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetImages")
+ ret0, _ := ret[0].(map[string]struct{})
+ return ret0
+}
+
+// GetImages indicates an expected call of GetImages.
+func (mr *MockStageMockRecorder) GetImages() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetImages", reflect.TypeOf((*MockStage)(nil).GetImages))
+}
+
+// Len mocks base method.
+func (m *MockStage) Len() int {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Len")
+ ret0, _ := ret[0].(int)
+ return ret0
+}
+
+// Len indicates an expected call of Len.
+func (mr *MockStageMockRecorder) Len() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Len", reflect.TypeOf((*MockStage)(nil).Len))
+}
+
+// Name mocks base method.
+func (m *MockStage) Name() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Name")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// Name indicates an expected call of Name.
+func (mr *MockStageMockRecorder) Name() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockStage)(nil).Name))
+}
+
+// Negative mocks base method.
+func (m *MockStage) Negative() bool {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Negative")
+ ret0, _ := ret[0].(bool)
+ return ret0
+}
+
+// Negative indicates an expected call of Negative.
+func (mr *MockStageMockRecorder) Negative() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Negative", reflect.TypeOf((*MockStage)(nil).Negative))
+}
+
+// Optional mocks base method.
+func (m *MockStage) Optional() bool {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Optional")
+ ret0, _ := ret[0].(bool)
+ return ret0
+}
+
+// Optional indicates an expected call of Optional.
+func (mr *MockStageMockRecorder) Optional() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Optional", reflect.TypeOf((*MockStage)(nil).Optional))
+}
+
+// Ref mocks base method.
+func (m *MockStage) Ref() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Ref")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// Ref indicates an expected call of Ref.
+func (mr *MockStageMockRecorder) Ref() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ref", reflect.TypeOf((*MockStage)(nil).Ref))
+}
+
+// Resolve mocks base method.
+func (m *MockStage) Resolve(arg0 ...expressionstcl.Machine) error {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Resolve", varargs...)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Resolve indicates an expected call of Resolve.
+func (mr *MockStageMockRecorder) Resolve(arg0 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*MockStage)(nil).Resolve), arg0...)
+}
+
+// RetryPolicy mocks base method.
+func (m *MockStage) RetryPolicy() v1.RetryPolicy {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "RetryPolicy")
+ ret0, _ := ret[0].(v1.RetryPolicy)
+ return ret0
+}
+
+// RetryPolicy indicates an expected call of RetryPolicy.
+func (mr *MockStageMockRecorder) RetryPolicy() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetryPolicy", reflect.TypeOf((*MockStage)(nil).RetryPolicy))
+}
+
+// SetCategory mocks base method.
+func (m *MockStage) SetCategory(arg0 string) StageMetadata {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetCategory", arg0)
+ ret0, _ := ret[0].(StageMetadata)
+ return ret0
+}
+
+// SetCategory indicates an expected call of SetCategory.
+func (mr *MockStageMockRecorder) SetCategory(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCategory", reflect.TypeOf((*MockStage)(nil).SetCategory), arg0)
+}
+
+// SetCondition mocks base method.
+func (m *MockStage) SetCondition(arg0 string) StageLifecycle {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetCondition", arg0)
+ ret0, _ := ret[0].(StageLifecycle)
+ return ret0
+}
+
+// SetCondition indicates an expected call of SetCondition.
+func (mr *MockStageMockRecorder) SetCondition(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCondition", reflect.TypeOf((*MockStage)(nil).SetCondition), arg0)
+}
+
+// SetName mocks base method.
+func (m *MockStage) SetName(arg0 string) StageMetadata {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetName", arg0)
+ ret0, _ := ret[0].(StageMetadata)
+ return ret0
+}
+
+// SetName indicates an expected call of SetName.
+func (mr *MockStageMockRecorder) SetName(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetName", reflect.TypeOf((*MockStage)(nil).SetName), arg0)
+}
+
+// SetNegative mocks base method.
+func (m *MockStage) SetNegative(arg0 bool) StageLifecycle {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetNegative", arg0)
+ ret0, _ := ret[0].(StageLifecycle)
+ return ret0
+}
+
+// SetNegative indicates an expected call of SetNegative.
+func (mr *MockStageMockRecorder) SetNegative(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNegative", reflect.TypeOf((*MockStage)(nil).SetNegative), arg0)
+}
+
+// SetOptional mocks base method.
+func (m *MockStage) SetOptional(arg0 bool) StageLifecycle {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetOptional", arg0)
+ ret0, _ := ret[0].(StageLifecycle)
+ return ret0
+}
+
+// SetOptional indicates an expected call of SetOptional.
+func (mr *MockStageMockRecorder) SetOptional(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetOptional", reflect.TypeOf((*MockStage)(nil).SetOptional), arg0)
+}
+
+// SetRetryPolicy mocks base method.
+func (m *MockStage) SetRetryPolicy(arg0 v1.RetryPolicy) StageLifecycle {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetRetryPolicy", arg0)
+ ret0, _ := ret[0].(StageLifecycle)
+ return ret0
+}
+
+// SetRetryPolicy indicates an expected call of SetRetryPolicy.
+func (mr *MockStageMockRecorder) SetRetryPolicy(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRetryPolicy", reflect.TypeOf((*MockStage)(nil).SetRetryPolicy), arg0)
+}
+
+// SetTimeout mocks base method.
+func (m *MockStage) SetTimeout(arg0 string) StageLifecycle {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetTimeout", arg0)
+ ret0, _ := ret[0].(StageLifecycle)
+ return ret0
+}
+
+// SetTimeout indicates an expected call of SetTimeout.
+func (mr *MockStageMockRecorder) SetTimeout(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTimeout", reflect.TypeOf((*MockStage)(nil).SetTimeout), arg0)
+}
+
+// Signature mocks base method.
+func (m *MockStage) Signature() Signature {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Signature")
+ ret0, _ := ret[0].(Signature)
+ return ret0
+}
+
+// Signature indicates an expected call of Signature.
+func (mr *MockStageMockRecorder) Signature() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Signature", reflect.TypeOf((*MockStage)(nil).Signature))
+}
+
+// Timeout mocks base method.
+func (m *MockStage) Timeout() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Timeout")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// Timeout indicates an expected call of Timeout.
+func (mr *MockStageMockRecorder) Timeout() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Timeout", reflect.TypeOf((*MockStage)(nil).Timeout))
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/operations.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/operations.go
new file mode 100644
index 00000000000..4724eb79959
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/operations.go
@@ -0,0 +1,303 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+import (
+ "encoding/json"
+ "fmt"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/pkg/errors"
+ corev1 "k8s.io/api/core/v1"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/pkg/tcl/mapperstcl/testworkflows"
+)
+
+func ProcessDelay(_ InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) {
+ if step.Delay == "" {
+ return nil, nil
+ }
+ t, err := time.ParseDuration(step.Delay)
+ if err != nil {
+ return nil, errors.Wrap(err, fmt.Sprintf("invalid duration: %s", step.Delay))
+ }
+ shell := container.CreateChild().
+ SetCommand("sleep").
+ SetArgs(fmt.Sprintf("%g", t.Seconds()))
+ stage := NewContainerStage(layer.NextRef(), shell)
+ stage.SetCategory(fmt.Sprintf("Delay: %s", step.Delay))
+ return stage, nil
+}
+
+func ProcessShellCommand(_ InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) {
+ if step.Shell == "" {
+ return nil, nil
+ }
+ shell := container.CreateChild().SetCommand(defaultShell).SetArgs("-c", step.Shell)
+ stage := NewContainerStage(layer.NextRef(), shell)
+ stage.SetCategory("Run shell command")
+ return stage, nil
+}
+
+func ProcessRunCommand(_ InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) {
+ if step.Run == nil {
+ return nil, nil
+ }
+ container = container.CreateChild().ApplyCR(&step.Run.ContainerConfig)
+ stage := NewContainerStage(layer.NextRef(), container)
+ stage.SetCategory("Run")
+ return stage, nil
+}
+
+func ProcessNestedSetupSteps(p InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) {
+ group := NewGroupStage(layer.NextRef(), true)
+ for _, n := range step.Setup {
+ stage, err := p.Process(layer, container.CreateChild(), n)
+ if err != nil {
+ return nil, err
+ }
+ group.Add(stage)
+ }
+ return group, nil
+}
+
+func ProcessNestedSteps(p InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) {
+ group := NewGroupStage(layer.NextRef(), true)
+ for _, n := range step.Steps {
+ stage, err := p.Process(layer, container.CreateChild(), n)
+ if err != nil {
+ return nil, err
+ }
+ group.Add(stage)
+ }
+ return group, nil
+}
+
+func ProcessExecute(_ InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) {
+ if step.Execute == nil {
+ return nil, nil
+ }
+ container = container.CreateChild()
+ stage := NewContainerStage(layer.NextRef(), container)
+ hasWorkflows := len(step.Execute.Workflows) > 0
+ hasTests := len(step.Execute.Tests) > 0
+
+ // Fail if there is nothing to run
+ if !hasTests && !hasWorkflows {
+ return nil, errors.New("no test workflows and tests provided to the 'execute' step")
+ }
+
+ container.
+ SetImage(defaultToolkitImage).
+ SetImagePullPolicy(corev1.PullIfNotPresent).
+ SetCommand("/toolkit", "execute").
+ EnableToolkit(stage.Ref())
+ args := make([]string, 0)
+ for _, t := range step.Execute.Tests {
+ args = append(args, "-t", t.Name)
+ }
+ for _, w := range step.Execute.Workflows {
+ if len(w.Config) == 0 {
+ args = append(args, "-w", w.Name)
+ } else {
+ v, _ := json.Marshal(testworkflows.MapConfigValueKubeToAPI(w.Config))
+ args = append(args, "-w", fmt.Sprintf(`%s={"config":%s}`, w.Name, v))
+ }
+ }
+ if step.Execute.Async {
+ args = append(args, "--async")
+ }
+ if step.Execute.Parallelism > 0 {
+ args = append(args, "-p", strconv.Itoa(int(step.Execute.Parallelism)))
+ }
+ container.SetArgs(args...)
+
+ // Add default label
+ types := make([]string, 0)
+ if hasWorkflows {
+ types = append(types, "test workflows")
+ }
+ if hasTests {
+ types = append(types, "tests")
+ }
+ stage.SetCategory("Execute " + strings.Join(types, " & "))
+
+ return stage, nil
+}
+
+func ProcessContentFiles(_ InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) {
+ if step.Content == nil {
+ return nil, nil
+ }
+ for _, f := range step.Content.Files {
+ if f.ContentFrom == nil {
+ vm, err := layer.AddTextFile(f.Content)
+ if err != nil {
+ return nil, fmt.Errorf("file %s: could not append: %s", f.Path, err.Error())
+ }
+ vm.MountPath = f.Path
+ container.AppendVolumeMounts(vm)
+ continue
+ }
+
+ volRef := "{{execution.id}}-" + layer.NextRef()
+
+ if f.ContentFrom.ConfigMapKeyRef != nil {
+ layer.AddVolume(corev1.Volume{
+ Name: volRef,
+ VolumeSource: corev1.VolumeSource{
+ ConfigMap: &corev1.ConfigMapVolumeSource{
+ LocalObjectReference: f.ContentFrom.ConfigMapKeyRef.LocalObjectReference,
+ Items: []corev1.KeyToPath{{Key: f.ContentFrom.ConfigMapKeyRef.Key, Path: "file"}},
+ DefaultMode: f.Mode,
+ Optional: f.ContentFrom.ConfigMapKeyRef.Optional,
+ },
+ },
+ })
+ container.AppendVolumeMounts(corev1.VolumeMount{Name: volRef, MountPath: f.Path, SubPath: "file"})
+ } else if f.ContentFrom.SecretKeyRef != nil {
+ layer.AddVolume(corev1.Volume{
+ Name: volRef,
+ VolumeSource: corev1.VolumeSource{
+ Secret: &corev1.SecretVolumeSource{
+ SecretName: f.ContentFrom.SecretKeyRef.Name,
+ Items: []corev1.KeyToPath{{Key: f.ContentFrom.SecretKeyRef.Key, Path: "file"}},
+ DefaultMode: f.Mode,
+ Optional: f.ContentFrom.SecretKeyRef.Optional,
+ },
+ },
+ })
+ container.AppendVolumeMounts(corev1.VolumeMount{Name: volRef, MountPath: f.Path, SubPath: "file"})
+ } else if f.ContentFrom.FieldRef != nil || f.ContentFrom.ResourceFieldRef != nil {
+ layer.AddVolume(corev1.Volume{
+ Name: volRef,
+ VolumeSource: corev1.VolumeSource{
+ Projected: &corev1.ProjectedVolumeSource{
+ Sources: []corev1.VolumeProjection{{
+ DownwardAPI: &corev1.DownwardAPIProjection{
+ Items: []corev1.DownwardAPIVolumeFile{{
+ Path: "file",
+ FieldRef: f.ContentFrom.FieldRef,
+ ResourceFieldRef: f.ContentFrom.ResourceFieldRef,
+ Mode: f.Mode,
+ }},
+ },
+ }},
+ DefaultMode: f.Mode,
+ },
+ },
+ })
+ container.AppendVolumeMounts(corev1.VolumeMount{Name: volRef, MountPath: f.Path, SubPath: "file"})
+ } else {
+ return nil, fmt.Errorf("file %s: unrecognized ContentFrom provided for file", f.Path)
+ }
+ }
+ return nil, nil
+}
+
+func ProcessContentGit(_ InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) {
+ if step.Content == nil || step.Content.Git == nil {
+ return nil, nil
+ }
+
+ selfContainer := container.CreateChild()
+ stage := NewContainerStage(layer.NextRef(), selfContainer)
+ stage.SetCategory("Clone Git repository")
+
+ // Compute mount path
+ mountPath := step.Content.Git.MountPath
+ if mountPath == "" {
+ mountPath = filepath.Join(defaultDataPath, "repo")
+ }
+
+ // Build volume pair and share with all siblings
+ volumeMount := layer.AddEmptyDirVolume(nil, mountPath)
+ container.AppendVolumeMounts(volumeMount)
+
+ selfContainer.
+ SetWorkingDir("/").
+ SetImage(defaultToolkitImage).
+ SetImagePullPolicy(corev1.PullIfNotPresent).
+ SetCommand("/toolkit", "clone", step.Content.Git.Uri).
+ EnableToolkit(stage.Ref())
+
+ args := []string{mountPath}
+
+ // Provide Git username
+ if step.Content.Git.UsernameFrom != nil {
+ container.AppendEnv(corev1.EnvVar{Name: "TK_GIT_USERNAME", ValueFrom: step.Content.Git.UsernameFrom})
+ args = append(args, "-u", "{{env.TK_GIT_USERNAME}}")
+ } else if step.Content.Git.Username != "" {
+ args = append(args, "-u", step.Content.Git.Username)
+ }
+
+ // Provide Git token
+ if step.Content.Git.TokenFrom != nil {
+ container.AppendEnv(corev1.EnvVar{Name: "TK_GIT_TOKEN", ValueFrom: step.Content.Git.TokenFrom})
+ args = append(args, "-t", "{{env.TK_GIT_TOKEN}}")
+ } else if step.Content.Git.Token != "" {
+ args = append(args, "-t", step.Content.Git.Token)
+ }
+
+ // Provide auth type
+ if step.Content.Git.AuthType != "" {
+ args = append(args, "-a", string(step.Content.Git.AuthType))
+ }
+
+ // Provide revision
+ if step.Content.Git.Revision != "" {
+ args = append(args, "-r", step.Content.Git.Revision)
+ }
+
+ // Provide sparse paths
+ if len(step.Content.Git.Paths) > 0 {
+ for _, pattern := range step.Content.Git.Paths {
+ args = append(args, "-p", pattern)
+ }
+ }
+
+ selfContainer.SetArgs(args...)
+
+ return stage, nil
+}
+
+func ProcessArtifacts(_ InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) {
+ if step.Artifacts == nil {
+ return nil, nil
+ }
+
+ if len(step.Artifacts.Paths) == 0 {
+ return nil, errors.New("there needs to be at least one path to scrap for artifacts")
+ }
+
+ selfContainer := container.CreateChild().
+ ApplyCR(&testworkflowsv1.ContainerConfig{WorkingDir: step.Artifacts.WorkingDir})
+ stage := NewContainerStage(layer.NextRef(), selfContainer)
+ stage.SetCondition("always")
+ stage.SetCategory("Upload artifacts")
+
+ selfContainer.
+ SetImage(defaultToolkitImage).
+ SetImagePullPolicy(corev1.PullIfNotPresent).
+ SetCommand("/toolkit", "artifacts", "-m", defaultDataPath).
+ EnableToolkit(stage.Ref())
+
+ args := make([]string, 0)
+ if step.Artifacts.Compress != nil {
+ args = append(args, "--compress", step.Artifacts.Compress.Name)
+ }
+ args = append(args, step.Artifacts.Paths...)
+ selfContainer.SetArgs(args...)
+
+ return stage, nil
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/processor.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/processor.go
new file mode 100644
index 00000000000..442dffa9017
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/processor.go
@@ -0,0 +1,311 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "maps"
+ "path/filepath"
+
+ "github.com/pkg/errors"
+ batchv1 "k8s.io/api/batch/v1"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/imageinspector"
+ "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+)
+
+//go:generate mockgen -destination=./mock_processor.go -package=testworkflowprocessor "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor" Processor
+type Processor interface {
+ Register(operation Operation) Processor
+ Bundle(ctx context.Context, workflow *testworkflowsv1.TestWorkflow, machines ...expressionstcl.Machine) (*Bundle, error)
+}
+
+//go:generate mockgen -destination=./mock_internalprocessor.go -package=testworkflowprocessor "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor" InternalProcessor
+type InternalProcessor interface {
+ Process(layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error)
+}
+
+type Operation = func(processor InternalProcessor, layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error)
+
+type processor struct {
+ inspector imageinspector.Inspector
+ operations []Operation
+}
+
+func New(inspector imageinspector.Inspector) Processor {
+ return &processor{inspector: inspector}
+}
+
+func NewFullFeatured(inspector imageinspector.Inspector) Processor {
+ return New(inspector).
+ Register(ProcessDelay).
+ Register(ProcessContentFiles).
+ Register(ProcessContentGit).
+ Register(ProcessNestedSetupSteps).
+ Register(ProcessRunCommand).
+ Register(ProcessShellCommand).
+ Register(ProcessExecute).
+ Register(ProcessNestedSteps).
+ Register(ProcessArtifacts)
+}
+
+func (p *processor) Register(operation Operation) Processor {
+ p.operations = append(p.operations, operation)
+ return p
+}
+
+func (p *processor) process(layer Intermediate, container Container, step testworkflowsv1.Step, ref string) (Stage, error) {
+ // Configure defaults
+ if step.WorkingDir != nil {
+ container.SetWorkingDir(*step.WorkingDir)
+ }
+ container.ApplyCR(step.Container)
+
+ // Build an initial group for the inner items
+ self := NewGroupStage(ref, false)
+ self.SetName(step.Name)
+ self.SetOptional(step.Optional).SetNegative(step.Negative).SetTimeout(step.Timeout)
+ if step.Condition != "" {
+ self.SetCondition(step.Condition)
+ } else {
+ self.SetCondition("passed")
+ }
+
+ // Run operations
+ for _, op := range p.operations {
+ stage, err := op(p, layer, container, step)
+ if err != nil {
+ return nil, err
+ }
+ self.Add(stage)
+ }
+
+ return self, nil
+}
+
+func (p *processor) Process(layer Intermediate, container Container, step testworkflowsv1.Step) (Stage, error) {
+ return p.process(layer, container, step, layer.NextRef())
+}
+
+func (p *processor) Bundle(ctx context.Context, workflow *testworkflowsv1.TestWorkflow, machines ...expressionstcl.Machine) (bundle *Bundle, err error) {
+ // Initialize intermediate layer
+ layer := NewIntermediate().
+ AppendPodConfig(workflow.Spec.Pod).
+ AppendJobConfig(workflow.Spec.Job)
+ layer.ContainerDefaults().
+ ApplyCR(defaultContainerConfig.DeepCopy()).
+ AppendVolumeMounts(layer.AddEmptyDirVolume(nil, defaultInternalPath)).
+ AppendVolumeMounts(layer.AddEmptyDirVolume(nil, defaultDataPath))
+
+ // Process steps
+ rootStep := testworkflowsv1.Step{
+ StepBase: testworkflowsv1.StepBase{
+ Content: workflow.Spec.Content,
+ Container: workflow.Spec.Container,
+ },
+ Steps: append(workflow.Spec.Setup, append(workflow.Spec.Steps, workflow.Spec.After...)...),
+ }
+ root, err := p.process(layer, layer.ContainerDefaults(), rootStep, "")
+ if err != nil {
+ return nil, errors.Wrap(err, "processing error")
+ }
+
+ // Validate if there is anything to run
+ if root.Len() == 0 {
+ return nil, errors.New("test workflow has nothing to run")
+ }
+
+ // Finalize ConfigMaps
+ configMaps := layer.ConfigMaps()
+ for i := range configMaps {
+ AnnotateControlledBy(&configMaps[i], "{{execution.id}}")
+ err = expressionstcl.FinalizeForce(&configMaps[i], machines...)
+ if err != nil {
+ return nil, errors.Wrap(err, "finalizing ConfigMap")
+ }
+ }
+
+ // Finalize Secrets
+ secrets := layer.Secrets()
+ for i := range secrets {
+ AnnotateControlledBy(&secrets[i], "{{execution.id}}")
+ err = expressionstcl.FinalizeForce(&secrets[i], machines...)
+ if err != nil {
+ return nil, errors.Wrap(err, "finalizing Secret")
+ }
+ }
+
+ // Finalize Volumes
+ volumes := layer.Volumes()
+ for i := range volumes {
+ err = expressionstcl.FinalizeForce(&volumes[i], machines...)
+ if err != nil {
+ return nil, errors.Wrap(err, "finalizing Volume")
+ }
+ }
+
+ // Append main label for the pod
+ layer.AppendPodConfig(&testworkflowsv1.PodConfig{
+ Labels: map[string]string{
+ ExecutionIdMainPodLabelName: "{{execution.id}}",
+ },
+ })
+
+ // Resolve job & pod config
+ jobConfig, podConfig := layer.JobConfig(), layer.PodConfig()
+ err = expressionstcl.FinalizeForce(&jobConfig, machines...)
+ if err != nil {
+ return nil, errors.Wrap(err, "finalizing job config")
+ }
+ err = expressionstcl.FinalizeForce(&podConfig, machines...)
+ if err != nil {
+ return nil, errors.Wrap(err, "finalizing pod config")
+ }
+
+ // Build signature
+ sig := root.Signature().Children()
+
+ // Load the image pull secrets
+ pullSecretNames := make([]string, len(podConfig.ImagePullSecrets))
+ for i, v := range podConfig.ImagePullSecrets {
+ pullSecretNames[i] = v.Name
+ }
+
+ // Load the image details
+ imageNames := root.GetImages()
+ images := make(map[string]*imageinspector.Info)
+ for image := range imageNames {
+ info, err := p.inspector.Inspect(ctx, "", image, corev1.PullIfNotPresent, pullSecretNames)
+ if err != nil {
+ return nil, fmt.Errorf("resolving image error: %s: %s", image, err.Error())
+ }
+ images[image] = info
+ }
+ err = root.ApplyImages(images)
+ if err != nil {
+ return nil, errors.Wrap(err, "applying image data")
+ }
+
+ // Build list of the containers
+ containers, err := buildKubernetesContainers(root, NewInitProcess().SetRef(root.Ref()), machines...)
+ if err != nil {
+ return nil, errors.Wrap(err, "building Kubernetes containers")
+ }
+ for i := range containers {
+ err = expressionstcl.FinalizeForce(&containers[i].EnvFrom, machines...)
+ if err != nil {
+ return nil, errors.Wrap(err, "finalizing container's envFrom")
+ }
+ err = expressionstcl.FinalizeForce(&containers[i].VolumeMounts, machines...)
+ if err != nil {
+ return nil, errors.Wrap(err, "finalizing container's volumeMounts")
+ }
+ err = expressionstcl.FinalizeForce(&containers[i].Resources, machines...)
+ if err != nil {
+ return nil, errors.Wrap(err, "finalizing container's resources")
+ }
+
+ // Resolve relative paths in the volumeMounts relatively to the working dir
+ workingDir := defaultDataPath
+ if containers[i].WorkingDir != "" {
+ workingDir = containers[i].WorkingDir
+ }
+ for j := range containers[i].VolumeMounts {
+ if !filepath.IsAbs(containers[i].VolumeMounts[j].MountPath) {
+ containers[i].VolumeMounts[j].MountPath = filepath.Clean(filepath.Join(workingDir, containers[i].VolumeMounts[j].MountPath))
+ }
+ }
+ }
+
+ // Build pod template
+ podSpec := corev1.PodTemplateSpec{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: podConfig.Annotations,
+ Labels: podConfig.Labels,
+ },
+ Spec: corev1.PodSpec{
+ RestartPolicy: corev1.RestartPolicyNever,
+ Volumes: volumes,
+ ImagePullSecrets: podConfig.ImagePullSecrets,
+ ServiceAccountName: podConfig.ServiceAccountName,
+ NodeSelector: podConfig.NodeSelector,
+ SecurityContext: &corev1.PodSecurityContext{
+ FSGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ }
+ AnnotateControlledBy(&podSpec, "{{execution.id}}")
+ err = expressionstcl.FinalizeForce(&podSpec, machines...)
+ if err != nil {
+ return nil, errors.Wrap(err, "finalizing pod template spec")
+ }
+ initContainer := corev1.Container{
+ Name: "tktw-init",
+ Image: defaultInitImage,
+ ImagePullPolicy: corev1.PullIfNotPresent,
+ Command: []string{"/bin/sh", "-c"},
+ Args: []string{fmt.Sprintf("cp /init %s && touch %s && chmod 777 %s && (echo -n ',0' > %s && echo 'Done' && exit 0) || (echo -n 'failed,1' > %s && exit 1)", defaultInitPath, defaultStatePath, defaultStatePath, "/dev/termination-log", "/dev/termination-log")},
+ VolumeMounts: layer.ContainerDefaults().VolumeMounts(),
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ }
+ err = expressionstcl.FinalizeForce(&initContainer, machines...)
+ if err != nil {
+ return nil, errors.Wrap(err, "finalizing container's resources")
+ }
+ podSpec.Spec.InitContainers = append([]corev1.Container{initContainer}, containers[:len(containers)-1]...)
+ podSpec.Spec.Containers = containers[len(containers)-1:]
+
+ // Build job spec
+ jobSpec := batchv1.Job{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "Job",
+ APIVersion: batchv1.SchemeGroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "{{execution.id}}",
+ Annotations: jobConfig.Annotations,
+ Labels: jobConfig.Labels,
+ },
+ Spec: batchv1.JobSpec{
+ BackoffLimit: common.Ptr(int32(0)),
+ },
+ }
+ AnnotateControlledBy(&jobSpec, "{{execution.id}}")
+ err = expressionstcl.FinalizeForce(&jobSpec, machines...)
+ if err != nil {
+ return nil, errors.Wrap(err, "finalizing job spec")
+ }
+ jobSpec.Spec.Template = podSpec
+
+ // Build signature
+ sigSerialized, _ := json.Marshal(sig)
+ jobAnnotations := make(map[string]string)
+ maps.Copy(jobAnnotations, jobSpec.Annotations)
+ maps.Copy(jobAnnotations, map[string]string{
+ SignatureAnnotationName: string(sigSerialized),
+ })
+ jobSpec.Annotations = jobAnnotations
+
+ // Build bundle
+ bundle = &Bundle{
+ ConfigMaps: configMaps,
+ Secrets: secrets,
+ Job: jobSpec,
+ Signature: sig,
+ }
+ return bundle, nil
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/processor_test.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/processor_test.go
new file mode 100644
index 00000000000..0458da876ba
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/processor_test.go
@@ -0,0 +1,1097 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ batchv1 "k8s.io/api/batch/v1"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/imageinspector"
+ "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+)
+
+type dummyInspector struct{}
+
+func (*dummyInspector) Inspect(ctx context.Context, registry, image string, pullPolicy corev1.PullPolicy, pullSecretNames []string) (*imageinspector.Info, error) {
+ return &imageinspector.Info{}, nil
+}
+
+var (
+ ins = &dummyInspector{}
+ proc = NewFullFeatured(ins)
+ execMachine = expressionstcl.NewMachine().
+ Register("execution.id", "dummy-id")
+)
+
+func TestProcessEmpty(t *testing.T) {
+ wf := &testworkflowsv1.TestWorkflow{}
+
+ _, err := proc.Bundle(context.Background(), wf, execMachine)
+
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "has nothing to run")
+}
+
+func TestProcessBasic(t *testing.T) {
+ wf := &testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ Steps: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{Shell: "shell-test"}},
+ },
+ },
+ }
+
+ res, err := proc.Bundle(context.Background(), wf, execMachine)
+ assert.NoError(t, err)
+
+ sig := res.Signature
+ sigSerialized, _ := json.Marshal(sig)
+
+ volumes := res.Job.Spec.Template.Spec.Volumes
+ volumeMounts := res.Job.Spec.Template.Spec.InitContainers[0].VolumeMounts
+
+ want := batchv1.Job{
+ TypeMeta: metav1.TypeMeta{Kind: "Job", APIVersion: "batch/v1"},
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dummy-id",
+ Labels: map[string]string{ExecutionIdLabelName: "dummy-id"},
+ Annotations: map[string]string{
+ SignatureAnnotationName: string(sigSerialized),
+ },
+ },
+ Spec: batchv1.JobSpec{
+ BackoffLimit: common.Ptr(int32(0)),
+ Template: corev1.PodTemplateSpec{
+ ObjectMeta: metav1.ObjectMeta{
+ Labels: map[string]string{
+ ExecutionIdLabelName: "dummy-id",
+ ExecutionIdMainPodLabelName: "dummy-id",
+ },
+ Annotations: map[string]string(nil),
+ },
+ Spec: corev1.PodSpec{
+ RestartPolicy: corev1.RestartPolicyNever,
+ Volumes: volumes,
+ InitContainers: []corev1.Container{
+ {
+ Name: "tktw-init",
+ Image: defaultInitImage,
+ ImagePullPolicy: corev1.PullIfNotPresent,
+ Command: []string{"/bin/sh", "-c"},
+ Args: []string{"cp /init /.tktw/init && touch /.tktw/state && chmod 777 /.tktw/state && (echo -n ',0' > /dev/termination-log && echo 'Done' && exit 0) || (echo -n 'failed,1' > /dev/termination-log && exit 1)"},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ Containers: []corev1.Container{
+ {
+ Name: sig[0].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[0].Ref(),
+ "-c", fmt.Sprintf("%s=passed", sig[0].Ref()),
+ "-r", fmt.Sprintf("=%s", sig[0].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ SecurityContext: &corev1.PodSecurityContext{
+ FSGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ },
+ }
+
+ assert.Equal(t, want, res.Job)
+
+ assert.Equal(t, 2, len(volumeMounts))
+ assert.Equal(t, 2, len(volumes))
+ assert.Equal(t, defaultInternalPath, volumeMounts[0].MountPath)
+ assert.Equal(t, defaultDataPath, volumeMounts[1].MountPath)
+ assert.True(t, volumeMounts[0].Name == volumes[0].Name)
+ assert.True(t, volumeMounts[1].Name == volumes[1].Name)
+}
+
+func TestProcessBasicEnvReference(t *testing.T) {
+ wf := &testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ Steps: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{
+ Container: &testworkflowsv1.ContainerConfig{
+ Env: []corev1.EnvVar{
+ {Name: "ZERO", Value: "foo"},
+ {Name: "UNDETERMINED", Value: "{{call(abc)}}xxx"},
+ {Name: "INPUT", Value: "{{env.ZERO}}bar"},
+ {Name: "NEXT", Value: "foo{{env.UNDETERMINED}}{{env.LAST}}"},
+ {Name: "LAST", Value: "foo{{env.INPUT}}bar"},
+ },
+ },
+ Shell: "shell-test",
+ }},
+ },
+ },
+ }
+
+ res, err := proc.Bundle(context.Background(), wf, execMachine)
+ sig := res.Signature
+
+ volumes := res.Job.Spec.Template.Spec.Volumes
+ volumeMounts := res.Job.Spec.Template.Spec.InitContainers[0].VolumeMounts
+
+ want := corev1.PodSpec{
+ RestartPolicy: corev1.RestartPolicyNever,
+ Volumes: volumes,
+ InitContainers: []corev1.Container{
+ {
+ Name: "tktw-init",
+ Image: defaultInitImage,
+ ImagePullPolicy: corev1.PullIfNotPresent,
+ Command: []string{"/bin/sh", "-c"},
+ Args: []string{"cp /init /.tktw/init && touch /.tktw/state && chmod 777 /.tktw/state && (echo -n ',0' > /dev/termination-log && echo 'Done' && exit 0) || (echo -n 'failed,1' > /dev/termination-log && exit 1)"},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ Containers: []corev1.Container{
+ {
+ Name: sig[0].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[0].Ref(),
+ "-e", "UNDETERMINED,NEXT",
+ "-c", fmt.Sprintf("%s=passed", sig[0].Ref()),
+ "-r", fmt.Sprintf("=%s", sig[0].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{
+ {Name: "CI", Value: "1"},
+ {Name: "ZERO", Value: "foo"},
+ {Name: "UNDETERMINED", Value: "{{call(abc)}}xxx"},
+ {Name: "INPUT", Value: "foobar"},
+ {Name: "NEXT", Value: "foo{{env.UNDETERMINED}}foofoobarbar"},
+ {Name: "LAST", Value: "foofoobarbar"},
+ },
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ SecurityContext: &corev1.PodSecurityContext{
+ FSGroup: common.Ptr(defaultFsGroup),
+ },
+ }
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, res.Job.Spec.Template.Spec)
+}
+
+func TestProcessMultipleSteps(t *testing.T) {
+ wf := &testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ Steps: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{Shell: "shell-test"}},
+ {StepBase: testworkflowsv1.StepBase{Shell: "shell-test-2"}},
+ },
+ },
+ }
+
+ res, err := proc.Bundle(context.Background(), wf, execMachine)
+ sig := res.Signature
+
+ volumes := res.Job.Spec.Template.Spec.Volumes
+ volumeMounts := res.Job.Spec.Template.Spec.InitContainers[0].VolumeMounts
+
+ want := corev1.PodSpec{
+ RestartPolicy: corev1.RestartPolicyNever,
+ Volumes: volumes,
+ InitContainers: []corev1.Container{
+ {
+ Name: "tktw-init",
+ Image: defaultInitImage,
+ ImagePullPolicy: corev1.PullIfNotPresent,
+ Command: []string{"/bin/sh", "-c"},
+ Args: []string{"cp /init /.tktw/init && touch /.tktw/state && chmod 777 /.tktw/state && (echo -n ',0' > /dev/termination-log && echo 'Done' && exit 0) || (echo -n 'failed,1' > /dev/termination-log && exit 1)"},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ {
+ Name: sig[0].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[0].Ref(),
+ "-c", fmt.Sprintf("%s,%s=passed", sig[0].Ref(), sig[1].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s", sig[0].Ref(), sig[1].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ Containers: []corev1.Container{
+ {
+ Name: sig[1].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[1].Ref(),
+ "-c", fmt.Sprintf("%s=passed", sig[1].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s", sig[0].Ref(), sig[1].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test-2"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ SecurityContext: &corev1.PodSecurityContext{
+ FSGroup: common.Ptr(defaultFsGroup),
+ },
+ }
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, res.Job.Spec.Template.Spec)
+}
+
+func TestProcessNestedSteps(t *testing.T) {
+ wf := &testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ Steps: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{Name: "A", Shell: "shell-test"}},
+ {
+ StepBase: testworkflowsv1.StepBase{Name: "B"},
+ Steps: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{Name: "C", Shell: "shell-test-2"}},
+ {StepBase: testworkflowsv1.StepBase{Name: "D", Shell: "shell-test-3"}},
+ },
+ },
+ {StepBase: testworkflowsv1.StepBase{Name: "E", Shell: "shell-test-4"}},
+ },
+ },
+ }
+
+ res, err := proc.Bundle(context.Background(), wf, execMachine)
+ sig := res.Signature
+
+ volumes := res.Job.Spec.Template.Spec.Volumes
+ volumeMounts := res.Job.Spec.Template.Spec.InitContainers[0].VolumeMounts
+
+ want := corev1.PodSpec{
+ RestartPolicy: corev1.RestartPolicyNever,
+ Volumes: volumes,
+ InitContainers: []corev1.Container{
+ {
+ Name: "tktw-init",
+ Image: defaultInitImage,
+ ImagePullPolicy: corev1.PullIfNotPresent,
+ Command: []string{"/bin/sh", "-c"},
+ Args: []string{"cp /init /.tktw/init && touch /.tktw/state && chmod 777 /.tktw/state && (echo -n ',0' > /dev/termination-log && echo 'Done' && exit 0) || (echo -n 'failed,1' > /dev/termination-log && exit 1)"},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ {
+ Name: sig[0].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[0].Ref(),
+ "-c", fmt.Sprintf("%s,%s,%s,%s=passed", sig[0].Ref(), sig[1].Children()[0].Ref(), sig[1].Children()[1].Ref(), sig[2].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s&&%s", sig[0].Ref(), sig[1].Ref(), sig[2].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ {
+ Name: sig[1].Children()[0].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[1].Children()[0].Ref(),
+ "-i", fmt.Sprintf("%s", sig[1].Ref()),
+ "-c", fmt.Sprintf("%s,%s,%s=passed", sig[1].Ref(), sig[1].Children()[0].Ref(), sig[1].Children()[1].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s&&%s", sig[0].Ref(), sig[1].Ref(), sig[2].Ref()),
+ "-r", fmt.Sprintf("%s=%s&&%s", sig[1].Ref(), sig[1].Children()[0].Ref(), sig[1].Children()[1].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test-2"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ {
+ Name: sig[1].Children()[1].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[1].Children()[1].Ref(),
+ "-i", fmt.Sprintf("%s", sig[1].Ref()),
+ "-c", fmt.Sprintf("%s=passed", sig[1].Children()[1].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s&&%s", sig[0].Ref(), sig[1].Ref(), sig[2].Ref()),
+ "-r", fmt.Sprintf("%s=%s&&%s", sig[1].Ref(), sig[1].Children()[0].Ref(), sig[1].Children()[1].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test-3"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ Containers: []corev1.Container{
+ {
+ Name: sig[2].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[2].Ref(),
+ "-c", fmt.Sprintf("%s=passed", sig[2].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s&&%s", sig[0].Ref(), sig[1].Ref(), sig[2].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test-4"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ SecurityContext: &corev1.PodSecurityContext{
+ FSGroup: common.Ptr(defaultFsGroup),
+ },
+ }
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, res.Job.Spec.Template.Spec)
+}
+
+func TestProcessOptionalSteps(t *testing.T) {
+ wf := &testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ Steps: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{Name: "A", Shell: "shell-test"}},
+ {
+ StepBase: testworkflowsv1.StepBase{Name: "B", Optional: true},
+ Steps: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{Name: "C", Shell: "shell-test-2"}},
+ {StepBase: testworkflowsv1.StepBase{Name: "D", Shell: "shell-test-3"}},
+ },
+ },
+ {StepBase: testworkflowsv1.StepBase{Name: "E", Shell: "shell-test-4"}},
+ },
+ },
+ }
+
+ res, err := proc.Bundle(context.Background(), wf, execMachine)
+ sig := res.Signature
+
+ volumes := res.Job.Spec.Template.Spec.Volumes
+ volumeMounts := res.Job.Spec.Template.Spec.InitContainers[0].VolumeMounts
+
+ want := corev1.PodSpec{
+ RestartPolicy: corev1.RestartPolicyNever,
+ Volumes: volumes,
+ InitContainers: []corev1.Container{
+ {
+ Name: "tktw-init",
+ Image: defaultInitImage,
+ ImagePullPolicy: corev1.PullIfNotPresent,
+ Command: []string{"/bin/sh", "-c"},
+ Args: []string{"cp /init /.tktw/init && touch /.tktw/state && chmod 777 /.tktw/state && (echo -n ',0' > /dev/termination-log && echo 'Done' && exit 0) || (echo -n 'failed,1' > /dev/termination-log && exit 1)"},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ {
+ Name: sig[0].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[0].Ref(),
+ "-c", fmt.Sprintf("%s,%s,%s,%s=passed", sig[0].Ref(), sig[1].Children()[0].Ref(), sig[1].Children()[1].Ref(), sig[2].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s", sig[0].Ref(), sig[2].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ {
+ Name: sig[1].Children()[0].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[1].Children()[0].Ref(),
+ "-i", fmt.Sprintf("%s", sig[1].Ref()),
+ "-c", fmt.Sprintf("%s,%s,%s=passed", sig[1].Ref(), sig[1].Children()[0].Ref(), sig[1].Children()[1].Ref()),
+ "-r", fmt.Sprintf("%s=%s&&%s", sig[1].Ref(), sig[1].Children()[0].Ref(), sig[1].Children()[1].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test-2"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ {
+ Name: sig[1].Children()[1].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[1].Children()[1].Ref(),
+ "-i", fmt.Sprintf("%s", sig[1].Ref()),
+ "-c", fmt.Sprintf("%s=passed", sig[1].Children()[1].Ref()),
+ "-r", fmt.Sprintf("%s=%s&&%s", sig[1].Ref(), sig[1].Children()[0].Ref(), sig[1].Children()[1].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test-3"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ Containers: []corev1.Container{
+ {
+ Name: sig[2].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[2].Ref(),
+ "-c", fmt.Sprintf("%s=passed", sig[2].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s", sig[0].Ref(), sig[2].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test-4"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ SecurityContext: &corev1.PodSecurityContext{
+ FSGroup: common.Ptr(defaultFsGroup),
+ },
+ }
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, res.Job.Spec.Template.Spec)
+}
+
+func TestProcessNegativeSteps(t *testing.T) {
+ wf := &testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ Steps: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{Name: "A", Shell: "shell-test"}},
+ {
+ StepBase: testworkflowsv1.StepBase{Name: "B", Negative: true},
+ Steps: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{Name: "C", Shell: "shell-test-2"}},
+ {StepBase: testworkflowsv1.StepBase{Name: "D", Shell: "shell-test-3"}},
+ },
+ },
+ {StepBase: testworkflowsv1.StepBase{Name: "E", Shell: "shell-test-4"}},
+ },
+ },
+ }
+
+ res, err := proc.Bundle(context.Background(), wf, execMachine)
+ sig := res.Signature
+
+ volumes := res.Job.Spec.Template.Spec.Volumes
+ volumeMounts := res.Job.Spec.Template.Spec.InitContainers[0].VolumeMounts
+
+ want := corev1.PodSpec{
+ RestartPolicy: corev1.RestartPolicyNever,
+ Volumes: volumes,
+ InitContainers: []corev1.Container{
+ {
+ Name: "tktw-init",
+ Image: defaultInitImage,
+ ImagePullPolicy: corev1.PullIfNotPresent,
+ Command: []string{"/bin/sh", "-c"},
+ Args: []string{"cp /init /.tktw/init && touch /.tktw/state && chmod 777 /.tktw/state && (echo -n ',0' > /dev/termination-log && echo 'Done' && exit 0) || (echo -n 'failed,1' > /dev/termination-log && exit 1)"},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ {
+ Name: sig[0].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[0].Ref(),
+ "-c", fmt.Sprintf("%s,%s,%s,%s=passed", sig[0].Ref(), sig[1].Children()[0].Ref(), sig[1].Children()[1].Ref(), sig[2].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s&&%s", sig[0].Ref(), sig[1].Ref(), sig[2].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ {
+ Name: sig[1].Children()[0].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[1].Children()[0].Ref(),
+ "-i", fmt.Sprintf("%s.v", sig[1].Ref()),
+ "-c", fmt.Sprintf("%s,%s,%s,%s.v=passed", sig[1].Ref(), sig[1].Children()[0].Ref(), sig[1].Children()[1].Ref(), sig[1].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s&&%s", sig[0].Ref(), sig[1].Ref(), sig[2].Ref()),
+ "-r", fmt.Sprintf("%s=!%s.v", sig[1].Ref(), sig[1].Ref()),
+ "-r", fmt.Sprintf("%s.v=%s&&%s", sig[1].Ref(), sig[1].Children()[0].Ref(), sig[1].Children()[1].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test-2"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ {
+ Name: sig[1].Children()[1].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[1].Children()[1].Ref(),
+ "-i", fmt.Sprintf("%s.v", sig[1].Ref()),
+ "-c", fmt.Sprintf("%s=passed", sig[1].Children()[1].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s&&%s", sig[0].Ref(), sig[1].Ref(), sig[2].Ref()),
+ "-r", fmt.Sprintf("%s=!%s.v", sig[1].Ref(), sig[1].Ref()),
+ "-r", fmt.Sprintf("%s.v=%s&&%s", sig[1].Ref(), sig[1].Children()[0].Ref(), sig[1].Children()[1].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test-3"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ Containers: []corev1.Container{
+ {
+ Name: sig[2].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[2].Ref(),
+ "-c", fmt.Sprintf("%s=passed", sig[2].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s&&%s", sig[0].Ref(), sig[1].Ref(), sig[2].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test-4"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ SecurityContext: &corev1.PodSecurityContext{
+ FSGroup: common.Ptr(defaultFsGroup),
+ },
+ }
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, res.Job.Spec.Template.Spec)
+}
+
+func TestProcessNegativeContainerStep(t *testing.T) {
+ wf := &testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ Steps: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{Shell: "shell-test"}},
+ {StepBase: testworkflowsv1.StepBase{Shell: "shell-test-2", Negative: true}},
+ },
+ },
+ }
+
+ res, err := proc.Bundle(context.Background(), wf, execMachine)
+ sig := res.Signature
+
+ volumes := res.Job.Spec.Template.Spec.Volumes
+ volumeMounts := res.Job.Spec.Template.Spec.InitContainers[0].VolumeMounts
+
+ want := corev1.PodSpec{
+ RestartPolicy: corev1.RestartPolicyNever,
+ Volumes: volumes,
+ InitContainers: []corev1.Container{
+ {
+ Name: "tktw-init",
+ Image: defaultInitImage,
+ ImagePullPolicy: corev1.PullIfNotPresent,
+ Command: []string{"/bin/sh", "-c"},
+ Args: []string{"cp /init /.tktw/init && touch /.tktw/state && chmod 777 /.tktw/state && (echo -n ',0' > /dev/termination-log && echo 'Done' && exit 0) || (echo -n 'failed,1' > /dev/termination-log && exit 1)"},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ {
+ Name: sig[0].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[0].Ref(),
+ "-c", fmt.Sprintf("%s,%s=passed", sig[0].Ref(), sig[1].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s", sig[0].Ref(), sig[1].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ Containers: []corev1.Container{
+ {
+ Name: sig[1].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[1].Ref(),
+ "-n", "true",
+ "-c", fmt.Sprintf("%s=passed", sig[1].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s", sig[0].Ref(), sig[1].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test-2"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+
+ SecurityContext: &corev1.PodSecurityContext{
+ FSGroup: common.Ptr(defaultFsGroup),
+ },
+ }
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, res.Job.Spec.Template.Spec)
+}
+
+func TestProcessOptionalContainerStep(t *testing.T) {
+ wf := &testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ Steps: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{Shell: "shell-test"}},
+ {StepBase: testworkflowsv1.StepBase{Shell: "shell-test-2", Optional: true}},
+ },
+ },
+ }
+
+ res, err := proc.Bundle(context.Background(), wf, execMachine)
+ sig := res.Signature
+
+ volumes := res.Job.Spec.Template.Spec.Volumes
+ volumeMounts := res.Job.Spec.Template.Spec.InitContainers[0].VolumeMounts
+
+ want := corev1.PodSpec{
+ RestartPolicy: corev1.RestartPolicyNever,
+ Volumes: volumes,
+ InitContainers: []corev1.Container{
+ {
+ Name: "tktw-init",
+ Image: defaultInitImage,
+ ImagePullPolicy: corev1.PullIfNotPresent,
+ Command: []string{"/bin/sh", "-c"},
+ Args: []string{"cp /init /.tktw/init && touch /.tktw/state && chmod 777 /.tktw/state && (echo -n ',0' > /dev/termination-log && echo 'Done' && exit 0) || (echo -n 'failed,1' > /dev/termination-log && exit 1)"},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ {
+ Name: sig[0].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[0].Ref(),
+ "-c", fmt.Sprintf("%s,%s=passed", sig[0].Ref(), sig[1].Ref()),
+ "-r", fmt.Sprintf("=%s", sig[0].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ Containers: []corev1.Container{
+ {
+ Name: sig[1].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[1].Ref(),
+ "-c", fmt.Sprintf("%s=passed", sig[1].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test-2"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ SecurityContext: &corev1.PodSecurityContext{
+ FSGroup: common.Ptr(defaultFsGroup),
+ },
+ }
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, res.Job.Spec.Template.Spec)
+}
+
+func TestProcessLocalContent(t *testing.T) {
+ wf := &testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ Steps: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{
+ Shell: "shell-test",
+ Content: &testworkflowsv1.Content{
+ Files: []testworkflowsv1.ContentFile{{
+ Path: "/some/path",
+ Content: `some-{{"{{"}}content`,
+ }},
+ },
+ }},
+ {StepBase: testworkflowsv1.StepBase{Shell: "shell-test-2"}},
+ },
+ },
+ }
+
+ res, err := proc.Bundle(context.Background(), wf, execMachine)
+ assert.NoError(t, err)
+
+ sig := res.Signature
+
+ volumes := res.Job.Spec.Template.Spec.Volumes
+ volumeMounts := res.Job.Spec.Template.Spec.InitContainers[0].VolumeMounts
+ volumeMountsWithContent := res.Job.Spec.Template.Spec.InitContainers[1].VolumeMounts
+
+ want := corev1.PodSpec{
+ RestartPolicy: corev1.RestartPolicyNever,
+ Volumes: volumes,
+ InitContainers: []corev1.Container{
+ {
+ Name: "tktw-init",
+ Image: defaultInitImage,
+ ImagePullPolicy: corev1.PullIfNotPresent,
+ Command: []string{"/bin/sh", "-c"},
+ Args: []string{"cp /init /.tktw/init && touch /.tktw/state && chmod 777 /.tktw/state && (echo -n ',0' > /dev/termination-log && echo 'Done' && exit 0) || (echo -n 'failed,1' > /dev/termination-log && exit 1)"},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ {
+ Name: sig[0].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[0].Ref(),
+ "-c", fmt.Sprintf("%s,%s=passed", sig[0].Ref(), sig[1].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s", sig[0].Ref(), sig[1].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMountsWithContent,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ Containers: []corev1.Container{
+ {
+ Name: sig[1].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[1].Ref(),
+ "-c", fmt.Sprintf("%s=passed", sig[1].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s", sig[0].Ref(), sig[1].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test-2"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ SecurityContext: &corev1.PodSecurityContext{
+ FSGroup: common.Ptr(defaultFsGroup),
+ },
+ }
+
+ assert.Equal(t, want, res.Job.Spec.Template.Spec)
+ assert.Equal(t, 2, len(volumeMounts))
+ assert.Equal(t, 3, len(volumeMountsWithContent))
+ assert.Equal(t, volumeMounts, volumeMountsWithContent[:2])
+ assert.Equal(t, "/some/path", volumeMountsWithContent[2].MountPath)
+ assert.Equal(t, 1, len(res.ConfigMaps))
+ assert.Equal(t, volumeMountsWithContent[2].Name, volumes[2].Name)
+ assert.Equal(t, volumes[2].ConfigMap.Name, res.ConfigMaps[0].Name)
+ assert.Equal(t, "some-{{content", res.ConfigMaps[0].Data[volumeMountsWithContent[2].SubPath])
+}
+
+func TestProcessGlobalContent(t *testing.T) {
+ wf := &testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Content: &testworkflowsv1.Content{
+ Files: []testworkflowsv1.ContentFile{{
+ Path: "/some/path",
+ Content: `some-{{"{{"}}content`,
+ }},
+ },
+ },
+ Steps: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{Shell: "shell-test"}},
+ {StepBase: testworkflowsv1.StepBase{Shell: "shell-test-2"}},
+ },
+ },
+ }
+
+ res, err := proc.Bundle(context.Background(), wf, execMachine)
+ assert.NoError(t, err)
+
+ sig := res.Signature
+
+ volumes := res.Job.Spec.Template.Spec.Volumes
+ volumeMounts := res.Job.Spec.Template.Spec.InitContainers[0].VolumeMounts
+
+ want := corev1.PodSpec{
+ RestartPolicy: corev1.RestartPolicyNever,
+ Volumes: volumes,
+ InitContainers: []corev1.Container{
+ {
+ Name: "tktw-init",
+ Image: defaultInitImage,
+ ImagePullPolicy: corev1.PullIfNotPresent,
+ Command: []string{"/bin/sh", "-c"},
+ Args: []string{"cp /init /.tktw/init && touch /.tktw/state && chmod 777 /.tktw/state && (echo -n ',0' > /dev/termination-log && echo 'Done' && exit 0) || (echo -n 'failed,1' > /dev/termination-log && exit 1)"},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ {
+ Name: sig[0].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[0].Ref(),
+ "-c", fmt.Sprintf("%s,%s=passed", sig[0].Ref(), sig[1].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s", sig[0].Ref(), sig[1].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ Containers: []corev1.Container{
+ {
+ Name: sig[1].Ref(),
+ ImagePullPolicy: "",
+ Image: defaultImage,
+ Command: []string{
+ "/.tktw/init",
+ sig[1].Ref(),
+ "-c", fmt.Sprintf("%s=passed", sig[1].Ref()),
+ "-r", fmt.Sprintf("=%s&&%s", sig[0].Ref(), sig[1].Ref()),
+ "--",
+ },
+ Args: []string{defaultShell, "-c", "shell-test-2"},
+ WorkingDir: "",
+ EnvFrom: []corev1.EnvFromSource(nil),
+ Env: []corev1.EnvVar{{Name: "CI", Value: "1"}},
+ Resources: corev1.ResourceRequirements{},
+ VolumeMounts: volumeMounts,
+ SecurityContext: &corev1.SecurityContext{
+ RunAsGroup: common.Ptr(defaultFsGroup),
+ },
+ },
+ },
+ SecurityContext: &corev1.PodSecurityContext{
+ FSGroup: common.Ptr(defaultFsGroup),
+ },
+ }
+
+ assert.Equal(t, want, res.Job.Spec.Template.Spec)
+ assert.Equal(t, 3, len(volumeMounts))
+ assert.Equal(t, "/some/path", volumeMounts[2].MountPath)
+ assert.Equal(t, 1, len(res.ConfigMaps))
+ assert.Equal(t, volumeMounts[2].Name, volumes[2].Name)
+ assert.Equal(t, volumes[2].ConfigMap.Name, res.ConfigMaps[0].Name)
+ assert.Equal(t, "some-{{content", res.ConfigMaps[0].Data[volumeMounts[2].SubPath])
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/refcounter.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/refcounter.go
new file mode 100644
index 00000000000..31cf6741bcc
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/refcounter.go
@@ -0,0 +1,32 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+import (
+ "fmt"
+ "strconv"
+
+ "k8s.io/apimachinery/pkg/util/rand"
+)
+
+type RefCounter interface {
+ NextRef() string
+}
+
+type refCounter struct {
+ refCount uint64
+}
+
+func NewRefCounter() RefCounter {
+ return &refCounter{}
+}
+
+func (r *refCounter) NextRef() string {
+ return fmt.Sprintf("r%s%s", rand.String(5), strconv.FormatUint(r.refCount, 36))
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/signature.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/signature.go
new file mode 100644
index 00000000000..d3eb59c6c27
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/signature.go
@@ -0,0 +1,131 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+import (
+ "encoding/json"
+ "maps"
+
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/api/v1/testkube"
+)
+
+type Signature interface {
+ Ref() string
+ Name() string
+ Category() string
+ Optional() bool
+ Negative() bool
+ Children() []Signature
+ ToInternal() testkube.TestWorkflowSignature
+}
+
+type signature struct {
+ RefValue string `json:"ref"`
+ NameValue string `json:"name,omitempty"`
+ CategoryValue string `json:"category,omitempty"`
+ OptionalValue bool `json:"optional,omitempty"`
+ NegativeValue bool `json:"negative,omitempty"`
+ ChildrenValue []Signature `json:"children,omitempty"`
+}
+
+func (s *signature) Ref() string {
+ return s.RefValue
+}
+
+func (s *signature) Name() string {
+ return s.NameValue
+}
+
+func (s *signature) Category() string {
+ return s.CategoryValue
+}
+
+func (s *signature) Optional() bool {
+ return s.OptionalValue
+}
+
+func (s *signature) Negative() bool {
+ return s.NegativeValue
+}
+
+func (s *signature) Children() []Signature {
+ return s.ChildrenValue
+}
+
+func (s *signature) ToInternal() testkube.TestWorkflowSignature {
+ return testkube.TestWorkflowSignature{
+ Ref: s.RefValue,
+ Name: s.NameValue,
+ Category: s.CategoryValue,
+ Optional: s.OptionalValue,
+ Negative: s.NegativeValue,
+ Children: MapSignatureListToInternal(s.ChildrenValue),
+ }
+}
+
+func MapSignatureListToInternal(v []Signature) []testkube.TestWorkflowSignature {
+ r := make([]testkube.TestWorkflowSignature, len(v))
+ for i := range v {
+ r[i] = v[i].ToInternal()
+ }
+ return r
+}
+
+func MapSignatureListToStepResults(v []Signature) map[string]testkube.TestWorkflowStepResult {
+ r := map[string]testkube.TestWorkflowStepResult{}
+ for _, s := range v {
+ r[s.Ref()] = testkube.TestWorkflowStepResult{
+ Status: common.Ptr(testkube.QUEUED_TestWorkflowStepStatus),
+ }
+ maps.Copy(r, MapSignatureListToStepResults(s.Children()))
+ }
+ return r
+}
+
+type rawSignature struct {
+ RefValue string `json:"ref"`
+ NameValue string `json:"name,omitempty"`
+ CategoryValue string `json:"category,omitempty"`
+ OptionalValue bool `json:"optional,omitempty"`
+ NegativeValue bool `json:"negative,omitempty"`
+ ChildrenValue []rawSignature `json:"children,omitempty"`
+}
+
+func rawSignatureToSignature(sig rawSignature) Signature {
+ ch := make([]Signature, len(sig.ChildrenValue))
+ for i, v := range sig.ChildrenValue {
+ ch[i] = rawSignatureToSignature(v)
+ }
+ return &signature{
+ RefValue: sig.RefValue,
+ NameValue: sig.NameValue,
+ CategoryValue: sig.CategoryValue,
+ OptionalValue: sig.OptionalValue,
+ NegativeValue: sig.NegativeValue,
+ ChildrenValue: ch,
+ }
+}
+
+func GetSignatureFromJSON(v []byte) ([]Signature, error) {
+ var sig []rawSignature
+ err := json.Unmarshal(v, &sig)
+ if err != nil {
+ return nil, err
+ }
+ res := make([]Signature, len(sig))
+ for i := range sig {
+ res[i] = rawSignatureToSignature(sig[i])
+ }
+ return res, err
+}
+
+func GetVirtualSignature(children []Signature) Signature {
+ return &signature{ChildrenValue: children}
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/stage.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/stage.go
new file mode 100644
index 00000000000..a4dcdc23df7
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/stage.go
@@ -0,0 +1,27 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+import (
+ "github.com/kubeshop/testkube/pkg/imageinspector"
+ "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+)
+
+//go:generate mockgen -destination=./mock_stage.go -package=testworkflowprocessor "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor" Stage
+type Stage interface {
+ StageMetadata
+ StageLifecycle
+ Len() int
+ Signature() Signature
+ Resolve(m ...expressionstcl.Machine) error
+ ContainerStages() []ContainerStage
+ GetImages() map[string]struct{}
+ ApplyImages(images map[string]*imageinspector.Info) error
+ Flatten() []Stage
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/stagelifecycle.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/stagelifecycle.go
new file mode 100644
index 00000000000..13b922ea77b
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/stagelifecycle.go
@@ -0,0 +1,111 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+import (
+ "strings"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+)
+
+type Condition struct {
+ Refs []string
+ Condition string `expr:"expression"`
+}
+
+type StageLifecycle interface {
+ Negative() bool
+ Optional() bool
+ Condition() string
+ RetryPolicy() testworkflowsv1.RetryPolicy
+ Timeout() string
+
+ SetNegative(negative bool) StageLifecycle
+ SetOptional(optional bool) StageLifecycle
+ SetCondition(expr string) StageLifecycle
+ AppendConditions(expr ...string) StageLifecycle
+ SetRetryPolicy(policy testworkflowsv1.RetryPolicy) StageLifecycle
+ SetTimeout(tpl string) StageLifecycle
+}
+
+type stageLifecycle struct {
+ negative bool
+ optional bool
+ condition string
+ retry testworkflowsv1.RetryPolicy
+ timeout string
+}
+
+func NewStageLifecycle() StageLifecycle {
+ return &stageLifecycle{}
+}
+
+func (s *stageLifecycle) Negative() bool {
+ return s.negative
+}
+
+func (s *stageLifecycle) Optional() bool {
+ return s.optional
+}
+
+func (s *stageLifecycle) Condition() string {
+ return s.condition
+}
+
+func (s *stageLifecycle) RetryPolicy() testworkflowsv1.RetryPolicy {
+ if s.retry.Count < 1 {
+ s.retry.Count = 0
+ }
+ return s.retry
+}
+
+func (s *stageLifecycle) Timeout() string {
+ return s.timeout
+}
+
+func (s *stageLifecycle) SetNegative(negative bool) StageLifecycle {
+ s.negative = negative
+ return s
+}
+
+func (s *stageLifecycle) SetOptional(optional bool) StageLifecycle {
+ s.optional = optional
+ return s
+}
+
+func (s *stageLifecycle) SetCondition(expr string) StageLifecycle {
+ s.condition = expr
+ return s
+}
+
+func (s *stageLifecycle) AppendConditions(expr ...string) StageLifecycle {
+ expr = append(expr, s.condition)
+ cond := []string(nil)
+ seen := map[string]bool{} // Assume pure accessors in condition, and preserve only unique
+ for _, e := range expr {
+ if e != "" && !seen[e] {
+ seen[e] = true
+ cond = append(cond, e)
+ }
+ }
+
+ s.condition = strings.Join(cond, "&&")
+
+ return s
+}
+
+func (s *stageLifecycle) SetRetryPolicy(policy testworkflowsv1.RetryPolicy) StageLifecycle {
+ s.retry = policy
+ return s
+}
+
+func (s *stageLifecycle) SetTimeout(tpl string) StageLifecycle {
+ s.timeout = tpl
+ return s
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/stagemetadata.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/stagemetadata.go
new file mode 100644
index 00000000000..032e712bb82
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/stagemetadata.go
@@ -0,0 +1,50 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+type StageMetadata interface {
+ Ref() string
+ Name() string
+ Category() string
+
+ SetName(name string) StageMetadata
+ SetCategory(category string) StageMetadata
+}
+
+type stageMetadata struct {
+ ref string
+ name string
+ category string
+}
+
+func NewStageMetadata(ref string) StageMetadata {
+ return &stageMetadata{ref: ref}
+}
+
+func (s *stageMetadata) Ref() string {
+ return s.ref
+}
+
+func (s *stageMetadata) Name() string {
+ return s.name
+}
+
+func (s *stageMetadata) Category() string {
+ return s.category
+}
+
+func (s *stageMetadata) SetName(name string) StageMetadata {
+ s.name = name
+ return s
+}
+
+func (s *stageMetadata) SetCategory(category string) StageMetadata {
+ s.category = category
+ return s
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowprocessor/utils.go b/pkg/tcl/testworkflowstcl/testworkflowprocessor/utils.go
new file mode 100644
index 00000000000..0f3de1bd74c
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowprocessor/utils.go
@@ -0,0 +1,143 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowprocessor
+
+import (
+ "fmt"
+ "strings"
+
+ batchv1 "k8s.io/api/batch/v1"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+)
+
+func AnnotateControlledBy(obj metav1.Object, testWorkflowId string) {
+ labels := obj.GetLabels()
+ if labels == nil {
+ labels = map[string]string{}
+ }
+ labels[ExecutionIdLabelName] = testWorkflowId
+ obj.SetLabels(labels)
+
+ // Annotate Pod template in the Job
+ if v, ok := obj.(*batchv1.Job); ok {
+ AnnotateControlledBy(&v.Spec.Template, testWorkflowId)
+ }
+}
+
+func getRef(stage Stage) string {
+ return stage.Ref()
+}
+
+func isNotOptional(stage Stage) bool {
+ return !stage.Optional()
+}
+
+func buildKubernetesContainers(stage Stage, init *initProcess, machines ...expressionstcl.Machine) (containers []corev1.Container, err error) {
+ if stage.Timeout() != "" {
+ init.AddTimeout(stage.Timeout(), stage.Ref())
+ }
+ if stage.Ref() != "" {
+ init.AddCondition(stage.Condition(), stage.Ref())
+ }
+ init.AddRetryPolicy(stage.RetryPolicy(), stage.Ref())
+
+ group, ok := stage.(GroupStage)
+ if ok {
+ recursiveRefs := common.MapSlice(group.RecursiveChildren(), getRef)
+ directRefResults := common.MapSlice(common.FilterSlice(group.Children(), isNotOptional), getRef)
+
+ init.AddCondition(stage.Condition(), recursiveRefs...)
+
+ if group.Negative() {
+ // Create virtual layer that will be put down into actual negative step
+ init.SetRef(stage.Ref() + ".v")
+ init.AddCondition(stage.Condition(), stage.Ref()+".v")
+ init.PrependInitialStatus(stage.Ref() + ".v")
+ init.AddResult("!"+stage.Ref()+".v", stage.Ref())
+ } else if stage.Ref() != "" {
+ init.PrependInitialStatus(stage.Ref())
+ }
+
+ if group.Optional() {
+ init.ResetResults()
+ }
+
+ if group.Negative() {
+ init.AddResult(strings.Join(directRefResults, "&&"), ""+stage.Ref()+".v")
+ } else {
+ init.AddResult(strings.Join(directRefResults, "&&"), ""+stage.Ref())
+ }
+
+ for i, ch := range group.Children() {
+ // Condition should be executed only in the first leaf
+ if i == 1 {
+ init.ResetCondition()
+ }
+ // Pass down to another group or container
+ sub, serr := buildKubernetesContainers(ch, init.Children(ch.Ref()), machines...)
+ if serr != nil {
+ return nil, fmt.Errorf("%s: %s: resolving children: %s", stage.Ref(), stage.Name(), serr.Error())
+ }
+ containers = append(containers, sub...)
+ }
+ return
+ }
+ c, ok := stage.(ContainerStage)
+ if !ok {
+ return nil, fmt.Errorf("%s: %s: stage that is neither container nor group", stage.Ref(), stage.Name())
+ }
+ err = c.Container().Detach().Resolve(machines...)
+ if err != nil {
+ return nil, fmt.Errorf("%s: %s: resolving container: %s", stage.Ref(), stage.Name(), err.Error())
+ }
+
+ cr, err := c.Container().ToKubernetesTemplate()
+ if err != nil {
+ return nil, fmt.Errorf("%s: %s: building container template: %s", stage.Ref(), stage.Name(), err.Error())
+ }
+ cr.Name = c.Ref()
+
+ if c.Optional() {
+ init.ResetResults()
+ }
+
+ init.
+ SetNegative(c.Negative()).
+ AddRetryPolicy(c.RetryPolicy(), c.Ref()).
+ SetCommand(cr.Command...).
+ SetArgs(cr.Args...)
+
+ for _, env := range cr.Env {
+ if strings.Contains(env.Value, "{{") {
+ init.AddComputedEnvs(env.Name)
+ }
+ }
+
+ if init.Error() != nil {
+ return nil, init.Error()
+ }
+
+ cr.Command = init.Command()
+ cr.Args = init.Args()
+
+ // Ensure the container will have proper access to FS
+ if cr.SecurityContext == nil {
+ cr.SecurityContext = &corev1.SecurityContext{}
+ }
+ if cr.SecurityContext.RunAsGroup == nil {
+ cr.SecurityContext.RunAsGroup = common.Ptr(defaultFsGroup)
+ }
+
+ containers = []corev1.Container{cr}
+ return
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowresolver/analyze.go b/pkg/tcl/testworkflowstcl/testworkflowresolver/analyze.go
new file mode 100644
index 00000000000..e509a6831aa
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowresolver/analyze.go
@@ -0,0 +1,61 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowresolver
+
+import (
+ "maps"
+ "strings"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+)
+
+func GetInternalTemplateName(name string) string {
+ return strings.ReplaceAll(name, "/", "--")
+}
+
+func GetDisplayTemplateName(name string) string {
+ return strings.ReplaceAll(name, "--", "/")
+}
+
+func listStepTemplates(cr testworkflowsv1.Step) map[string]struct{} {
+ v := make(map[string]struct{})
+ if cr.Template != nil {
+ v[GetInternalTemplateName(cr.Template.Name)] = struct{}{}
+ }
+ for i := range cr.Use {
+ v[GetInternalTemplateName(cr.Use[i].Name)] = struct{}{}
+ }
+ for i := range cr.Setup {
+ maps.Copy(v, listStepTemplates(cr.Setup[i]))
+ }
+ for i := range cr.Steps {
+ maps.Copy(v, listStepTemplates(cr.Steps[i]))
+ }
+ return v
+}
+
+func ListTemplates(cr *testworkflowsv1.TestWorkflow) map[string]struct{} {
+ if cr == nil {
+ return nil
+ }
+ v := make(map[string]struct{})
+ for i := range cr.Spec.Use {
+ v[GetInternalTemplateName(cr.Spec.Use[i].Name)] = struct{}{}
+ }
+ for i := range cr.Spec.Setup {
+ maps.Copy(v, listStepTemplates(cr.Spec.Setup[i]))
+ }
+ for i := range cr.Spec.Steps {
+ maps.Copy(v, listStepTemplates(cr.Spec.Steps[i]))
+ }
+ for i := range cr.Spec.After {
+ maps.Copy(v, listStepTemplates(cr.Spec.After[i]))
+ }
+ return v
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowresolver/analyze_test.go b/pkg/tcl/testworkflowstcl/testworkflowresolver/analyze_test.go
new file mode 100644
index 00000000000..9056fa24325
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowresolver/analyze_test.go
@@ -0,0 +1,85 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowresolver
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+)
+
+var (
+ refList = []testworkflowsv1.TemplateRef{
+ {Name: "official/something"},
+ {Name: "official--another"},
+ {Name: "official/another"},
+ {Name: "something"},
+ }
+ refListWant = map[string]struct{}{"official--something": {}, "official--another": {}, "something": {}}
+ refList2 = []testworkflowsv1.TemplateRef{
+ {Name: "official/something"},
+ {Name: "another"},
+ }
+ refList2Want = map[string]struct{}{"official--something": {}, "another": {}}
+ refList1Plus2Want = map[string]struct{}{"official--something": {}, "official--another": {}, "something": {}, "another": {}}
+)
+
+func TestGetInternalTemplateName(t *testing.T) {
+ assert.Equal(t, "keep-same-name", GetInternalTemplateName("keep-same-name"))
+ assert.Equal(t, "some--namespace", GetInternalTemplateName("some--namespace"))
+ assert.Equal(t, "some--namespace", GetInternalTemplateName("some/namespace"))
+ assert.Equal(t, "some--namespace--multiple", GetInternalTemplateName("some--namespace--multiple"))
+ assert.Equal(t, "some--namespace--multiple", GetInternalTemplateName("some/namespace--multiple"))
+ assert.Equal(t, "some--namespace--multiple", GetInternalTemplateName("some/namespace/multiple"))
+}
+
+func TestGetDisplayTemplateName(t *testing.T) {
+ assert.Equal(t, "keep-same-name", GetDisplayTemplateName("keep-same-name"))
+ assert.Equal(t, "some/namespace", GetDisplayTemplateName("some--namespace"))
+ assert.Equal(t, "some/namespace", GetDisplayTemplateName("some/namespace"))
+ assert.Equal(t, "some/namespace/multiple", GetDisplayTemplateName("some--namespace--multiple"))
+ assert.Equal(t, "some/namespace/multiple", GetDisplayTemplateName("some/namespace--multiple"))
+ assert.Equal(t, "some/namespace/multiple", GetDisplayTemplateName("some/namespace/multiple"))
+}
+
+func TestListTemplates(t *testing.T) {
+ assert.Equal(t, map[string]struct{}(nil), ListTemplates(nil))
+ assert.Equal(t, map[string]struct{}{}, ListTemplates(&testworkflowsv1.TestWorkflow{}))
+ assert.Equal(t, refListWant, ListTemplates(&testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{Use: refList},
+ }))
+ assert.Equal(t, refListWant, ListTemplates(&testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{Setup: []testworkflowsv1.Step{{Use: refList}}},
+ }))
+ assert.Equal(t, refListWant, ListTemplates(&testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{Steps: []testworkflowsv1.Step{{Use: refList}}},
+ }))
+ assert.Equal(t, refListWant, ListTemplates(&testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{After: []testworkflowsv1.Step{{Use: refList}}},
+ }))
+ assert.Equal(t, map[string]struct{}{"official--something": {}}, ListTemplates(&testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{After: []testworkflowsv1.Step{{Template: &refList[0]}}},
+ }))
+ assert.Equal(t, refListWant, ListTemplates(&testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{After: []testworkflowsv1.Step{
+ {Steps: []testworkflowsv1.Step{{Use: refList}}}}},
+ }))
+ assert.Equal(t, refList1Plus2Want, ListTemplates(&testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ Setup: []testworkflowsv1.Step{{Steps: []testworkflowsv1.Step{{Use: refList}}}},
+ After: []testworkflowsv1.Step{{Steps: []testworkflowsv1.Step{{Use: refList2}}}},
+ }}))
+ assert.Equal(t, refList2Want, ListTemplates(&testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ Setup: []testworkflowsv1.Step{{Steps: []testworkflowsv1.Step{{Use: refList2}}}},
+ After: []testworkflowsv1.Step{{Steps: []testworkflowsv1.Step{{Template: &refList2[0]}}}},
+ }}))
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowresolver/apply.go b/pkg/tcl/testworkflowstcl/testworkflowresolver/apply.go
new file mode 100644
index 00000000000..83a8f345861
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowresolver/apply.go
@@ -0,0 +1,231 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowresolver
+
+import (
+ "fmt"
+ "reflect"
+
+ "github.com/pkg/errors"
+ "k8s.io/apimachinery/pkg/util/intstr"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/internal/common"
+ "github.com/kubeshop/testkube/pkg/rand"
+ "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+)
+
+func buildTemplate(template testworkflowsv1.TestWorkflowTemplate, cfg map[string]intstr.IntOrString) (testworkflowsv1.TestWorkflowTemplate, error) {
+ v, err := ApplyWorkflowTemplateConfig(template.DeepCopy(), cfg)
+ if err != nil {
+ return template, err
+ }
+ return *v, err
+}
+
+func getTemplate(name string, templates map[string]testworkflowsv1.TestWorkflowTemplate) (tpl testworkflowsv1.TestWorkflowTemplate, err error) {
+ key := GetInternalTemplateName(name)
+ tpl, ok := templates[key]
+ if ok {
+ return tpl, nil
+ }
+ key = GetDisplayTemplateName(key)
+ tpl, ok = templates[key]
+ if ok {
+ return tpl, nil
+ }
+ return tpl, fmt.Errorf(`template "%s" not found`, name)
+}
+
+func getConfiguredTemplate(name string, cfg map[string]intstr.IntOrString, templates map[string]testworkflowsv1.TestWorkflowTemplate) (tpl testworkflowsv1.TestWorkflowTemplate, err error) {
+ tpl, err = getTemplate(name, templates)
+ if err != nil {
+ return tpl, err
+ }
+ return buildTemplate(tpl, cfg)
+}
+
+func InjectTemplate(workflow *testworkflowsv1.TestWorkflow, template testworkflowsv1.TestWorkflowTemplate) error {
+ if workflow == nil {
+ return nil
+ }
+ // Apply top-level configuration
+ workflow.Spec.Pod = MergePodConfig(template.Spec.Pod, workflow.Spec.Pod)
+ workflow.Spec.Job = MergeJobConfig(template.Spec.Job, workflow.Spec.Job)
+
+ // Apply basic configuration
+ workflow.Spec.Content = MergeContent(template.Spec.Content, workflow.Spec.Content)
+ workflow.Spec.Container = MergeContainerConfig(template.Spec.Container, workflow.Spec.Container)
+
+ // Include the steps from the template
+ setup := common.MapSlice(template.Spec.Setup, ConvertIndependentStepToStep)
+ workflow.Spec.Setup = append(setup, workflow.Spec.Setup...)
+ steps := common.MapSlice(template.Spec.Steps, ConvertIndependentStepToStep)
+ workflow.Spec.Steps = append(steps, workflow.Spec.Steps...)
+ after := common.MapSlice(template.Spec.After, ConvertIndependentStepToStep)
+ workflow.Spec.After = append(workflow.Spec.After, after...)
+ return nil
+}
+
+func InjectStepTemplate(step *testworkflowsv1.Step, template testworkflowsv1.TestWorkflowTemplate) error {
+ if step == nil {
+ return nil
+ }
+
+ // Apply basic configuration
+ step.Content = MergeContent(template.Spec.Content, step.Content)
+ step.Container = MergeContainerConfig(template.Spec.Container, step.Container)
+
+ // Fast-track when the template doesn't contain any steps to run
+ if len(template.Spec.Setup) == 0 && len(template.Spec.Steps) == 0 && len(template.Spec.After) == 0 {
+ return nil
+ }
+
+ // Decouple sub-steps from the template
+ setup := common.MapSlice(template.Spec.Setup, ConvertIndependentStepToStep)
+ steps := common.MapSlice(template.Spec.Steps, ConvertIndependentStepToStep)
+ after := common.MapSlice(template.Spec.After, ConvertIndependentStepToStep)
+
+ step.Setup = append(setup, step.Setup...)
+ step.Steps = append(steps, append(step.Steps, after...)...)
+
+ return nil
+}
+
+func applyTemplatesToStep(step testworkflowsv1.Step, templates map[string]testworkflowsv1.TestWorkflowTemplate) (testworkflowsv1.Step, error) {
+ // Apply regular templates
+ for i, ref := range step.Use {
+ tpl, err := getConfiguredTemplate(ref.Name, ref.Config, templates)
+ if err != nil {
+ return step, errors.Wrap(err, fmt.Sprintf(".use[%d]: resolving template", i))
+ }
+ err = InjectStepTemplate(&step, tpl)
+ if err != nil {
+ return step, errors.Wrap(err, fmt.Sprintf(".use[%d]: injecting template", i))
+ }
+ }
+ step.Use = nil
+
+ // Apply alternative template syntax
+ if step.Template != nil {
+ tpl, err := getConfiguredTemplate(step.Template.Name, step.Template.Config, templates)
+ if err != nil {
+ return step, errors.Wrap(err, ".template: resolving template")
+ }
+ isolate := testworkflowsv1.Step{}
+ err = InjectStepTemplate(&isolate, tpl)
+ if err != nil {
+ return step, errors.Wrap(err, ".template: injecting template")
+ }
+
+ if len(isolate.Setup) > 0 || len(isolate.Steps) > 0 {
+ if isolate.Container == nil && isolate.Content == nil && isolate.WorkingDir == nil {
+ step.Steps = append(append(isolate.Setup, isolate.Steps...), step.Steps...)
+ } else {
+ step.Steps = append([]testworkflowsv1.Step{isolate}, step.Steps...)
+ }
+ }
+
+ step.Template = nil
+ }
+
+ // Resolve templates in the sub-steps
+ var err error
+ for i := range step.Setup {
+ step.Setup[i], err = applyTemplatesToStep(step.Setup[i], templates)
+ if err != nil {
+ return step, errors.Wrap(err, fmt.Sprintf(".steps[%d]", i))
+ }
+ }
+ for i := range step.Steps {
+ step.Steps[i], err = applyTemplatesToStep(step.Steps[i], templates)
+ if err != nil {
+ return step, errors.Wrap(err, fmt.Sprintf(".steps[%d]", i))
+ }
+ }
+
+ return step, nil
+}
+
+func FlattenStepList(steps []testworkflowsv1.Step) []testworkflowsv1.Step {
+ changed := false
+ result := make([]testworkflowsv1.Step, 0, len(steps))
+ for _, step := range steps {
+ setup := step.Setup
+ sub := step.Steps
+ step.Setup = nil
+ step.Steps = nil
+ if reflect.ValueOf(step).IsZero() {
+ changed = true
+ result = append(result, append(setup, sub...)...)
+ } else {
+ step.Setup = setup
+ step.Steps = sub
+ result = append(result, step)
+ }
+ }
+ if !changed {
+ return steps
+ }
+ return result
+}
+
+func ApplyTemplates(workflow *testworkflowsv1.TestWorkflow, templates map[string]testworkflowsv1.TestWorkflowTemplate) error {
+ if workflow == nil {
+ return nil
+ }
+
+ // Encapsulate TestWorkflow configuration to not pass it into templates accidentally
+ random := rand.String(10)
+ err := expressionstcl.Simplify(workflow, expressionstcl.ReplacePrefixMachine("config.", random+"."))
+ if err != nil {
+ return err
+ }
+ defer expressionstcl.Simplify(workflow, expressionstcl.ReplacePrefixMachine(random+".", "config."))
+
+ // Apply top-level templates
+ for i, ref := range workflow.Spec.Use {
+ tpl, err := getConfiguredTemplate(ref.Name, ref.Config, templates)
+ if err != nil {
+ return errors.Wrap(err, fmt.Sprintf("spec.use[%d]: resolving template", i))
+ }
+ err = InjectTemplate(workflow, tpl)
+ if err != nil {
+ return errors.Wrap(err, fmt.Sprintf("spec.use[%d]: injecting template", i))
+ }
+ }
+ workflow.Spec.Use = nil
+
+ // Apply templates on the step level
+ for i := range workflow.Spec.Setup {
+ workflow.Spec.Setup[i], err = applyTemplatesToStep(workflow.Spec.Setup[i], templates)
+ if err != nil {
+ return errors.Wrap(err, fmt.Sprintf("spec.setup[%d]", i))
+ }
+ }
+ for i := range workflow.Spec.Steps {
+ workflow.Spec.Steps[i], err = applyTemplatesToStep(workflow.Spec.Steps[i], templates)
+ if err != nil {
+ return errors.Wrap(err, fmt.Sprintf("spec.steps[%d]", i))
+ }
+ }
+ for i := range workflow.Spec.After {
+ workflow.Spec.After[i], err = applyTemplatesToStep(workflow.Spec.After[i], templates)
+ if err != nil {
+ return errors.Wrap(err, fmt.Sprintf("spec.after[%d]", i))
+ }
+ }
+
+ // Simplify the lists
+ workflow.Spec.Setup = FlattenStepList(workflow.Spec.Setup)
+ workflow.Spec.Steps = FlattenStepList(workflow.Spec.Steps)
+ workflow.Spec.After = FlattenStepList(workflow.Spec.After)
+
+ return nil
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowresolver/apply_test.go b/pkg/tcl/testworkflowstcl/testworkflowresolver/apply_test.go
new file mode 100644
index 00000000000..67271f66aa6
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowresolver/apply_test.go
@@ -0,0 +1,559 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowresolver
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/util/intstr"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+)
+
+var (
+ tplPod = testworkflowsv1.TestWorkflowTemplate{
+ Spec: testworkflowsv1.TestWorkflowTemplateSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Pod: &testworkflowsv1.PodConfig{
+ Labels: map[string]string{
+ "v1": "v2",
+ },
+ },
+ },
+ },
+ }
+ tplPodConfig = testworkflowsv1.TestWorkflowTemplate{
+ Spec: testworkflowsv1.TestWorkflowTemplateSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Config: map[string]testworkflowsv1.ParameterSchema{
+ "department": {Type: testworkflowsv1.ParameterTypeString},
+ },
+ Pod: &testworkflowsv1.PodConfig{
+ Labels: map[string]string{
+ "department": "{{config.department}}",
+ },
+ },
+ },
+ },
+ }
+ tplEnv = testworkflowsv1.TestWorkflowTemplate{
+ Spec: testworkflowsv1.TestWorkflowTemplateSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Container: &testworkflowsv1.ContainerConfig{
+ Env: []corev1.EnvVar{
+ {Name: "test", Value: "the"},
+ },
+ },
+ },
+ },
+ }
+ tplSteps = testworkflowsv1.TestWorkflowTemplate{
+ Spec: testworkflowsv1.TestWorkflowTemplateSpec{
+ Setup: []testworkflowsv1.IndependentStep{
+ {StepBase: testworkflowsv1.StepBase{Name: "setup-tpl-test"}},
+ },
+ Steps: []testworkflowsv1.IndependentStep{
+ {StepBase: testworkflowsv1.StepBase{Name: "steps-tpl-test"}},
+ },
+ After: []testworkflowsv1.IndependentStep{
+ {StepBase: testworkflowsv1.StepBase{Name: "after-tpl-test"}},
+ },
+ },
+ }
+ tplStepsEnv = testworkflowsv1.TestWorkflowTemplate{
+ Spec: testworkflowsv1.TestWorkflowTemplateSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Container: &testworkflowsv1.ContainerConfig{
+ Env: []corev1.EnvVar{
+ {Name: "test", Value: "the"},
+ },
+ },
+ },
+ Setup: []testworkflowsv1.IndependentStep{
+ {StepBase: testworkflowsv1.StepBase{Name: "setup-tpl-test"}},
+ },
+ Steps: []testworkflowsv1.IndependentStep{
+ {StepBase: testworkflowsv1.StepBase{Name: "steps-tpl-test"}},
+ },
+ After: []testworkflowsv1.IndependentStep{
+ {StepBase: testworkflowsv1.StepBase{Name: "after-tpl-test"}},
+ },
+ },
+ }
+ tplStepsConfig = testworkflowsv1.TestWorkflowTemplate{
+ Spec: testworkflowsv1.TestWorkflowTemplateSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Config: map[string]testworkflowsv1.ParameterSchema{
+ "index": {Type: testworkflowsv1.ParameterTypeInteger},
+ },
+ },
+ Setup: []testworkflowsv1.IndependentStep{
+ {StepBase: testworkflowsv1.StepBase{Name: "setup-tpl-test-{{ config.index }}"}},
+ },
+ Steps: []testworkflowsv1.IndependentStep{
+ {StepBase: testworkflowsv1.StepBase{Name: "steps-tpl-test-{{ config.index }}"}},
+ },
+ After: []testworkflowsv1.IndependentStep{
+ {StepBase: testworkflowsv1.StepBase{Name: "after-tpl-test-{{ config.index }}"}},
+ },
+ },
+ }
+ templates = map[string]testworkflowsv1.TestWorkflowTemplate{
+ "pod": tplPod,
+ "podConfig": tplPodConfig,
+ "env": tplEnv,
+ "steps": tplSteps,
+ "stepsEnv": tplStepsEnv,
+ "stepsConfig": tplStepsConfig,
+ }
+ tplPodRef = testworkflowsv1.TemplateRef{Name: "pod"}
+ tplPodConfigRef = testworkflowsv1.TemplateRef{
+ Name: "podConfig",
+ Config: map[string]intstr.IntOrString{
+ "department": {Type: intstr.String, StrVal: "test-department"},
+ },
+ }
+ tplPodConfigRefEmpty = testworkflowsv1.TemplateRef{Name: "podConfig"}
+ tplEnvRef = testworkflowsv1.TemplateRef{Name: "env"}
+ tplStepsRef = testworkflowsv1.TemplateRef{Name: "steps"}
+ tplStepsEnvRef = testworkflowsv1.TemplateRef{Name: "stepsEnv"}
+ tplStepsConfigRef = testworkflowsv1.TemplateRef{Name: "stepsConfig", Config: map[string]intstr.IntOrString{
+ "index": {Type: intstr.Int, IntVal: 20},
+ }}
+ tplStepsConfigRefStringInvalid = testworkflowsv1.TemplateRef{Name: "stepsConfig", Config: map[string]intstr.IntOrString{
+ "index": {Type: intstr.String, StrVal: "text"},
+ }}
+ tplStepsConfigRefStringValid = testworkflowsv1.TemplateRef{Name: "stepsConfig", Config: map[string]intstr.IntOrString{
+ "index": {Type: intstr.String, StrVal: "10"},
+ }}
+ workflowPod = testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Pod: &testworkflowsv1.PodConfig{
+ Labels: map[string]string{
+ "the": "value",
+ },
+ },
+ },
+ },
+ }
+ workflowPodConfig = testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Config: map[string]testworkflowsv1.ParameterSchema{
+ "department": {Type: testworkflowsv1.ParameterTypeString},
+ },
+ Pod: &testworkflowsv1.PodConfig{
+ Labels: map[string]string{
+ "department": "{{config.department}}",
+ },
+ },
+ },
+ },
+ }
+ workflowSteps = testworkflowsv1.TestWorkflow{
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ Setup: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{Name: "setup-tpl"}},
+ },
+ Steps: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{Name: "steps-tpl"}},
+ },
+ After: []testworkflowsv1.Step{
+ {StepBase: testworkflowsv1.StepBase{Name: "after-tpl"}},
+ },
+ },
+ }
+ basicStep = testworkflowsv1.Step{
+ StepBase: testworkflowsv1.StepBase{
+ Name: "basic",
+ Shell: "shell-command",
+ Container: &testworkflowsv1.ContainerConfig{
+ Env: []corev1.EnvVar{
+ {Name: "XYZ", Value: "some-value"},
+ },
+ },
+ },
+ }
+ advancedStep = testworkflowsv1.Step{
+ StepBase: testworkflowsv1.StepBase{
+ Name: "basic",
+ Condition: "always",
+ Delay: "5s",
+ Shell: "another-shell-command",
+ Container: &testworkflowsv1.ContainerConfig{
+ Env: []corev1.EnvVar{
+ {Name: "XYZ", Value: "some-value"},
+ },
+ },
+ Artifacts: &testworkflowsv1.StepArtifacts{
+ Paths: []string{"a", "b", "c"},
+ },
+ },
+ Steps: []testworkflowsv1.Step{
+ basicStep,
+ },
+ }
+)
+
+func TestApplyTemplatesMissingTemplate(t *testing.T) {
+ wf := workflowSteps.DeepCopy()
+ wf.Spec.Use = []testworkflowsv1.TemplateRef{{Name: "unknown"}}
+ err := ApplyTemplates(wf, templates)
+
+ assert.Error(t, err)
+ assert.Equal(t, err.Error(), `spec.use[0]: resolving template: template "unknown" not found`)
+}
+
+func TestApplyTemplatesMissingConfig(t *testing.T) {
+ wf := workflowSteps.DeepCopy()
+ wf.Spec.Use = []testworkflowsv1.TemplateRef{tplPodConfigRefEmpty}
+ err := ApplyTemplates(wf, templates)
+
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), `spec.use[0]: resolving template:`)
+ assert.Contains(t, err.Error(), `config.department: unknown variable`)
+}
+
+func TestApplyTemplatesInvalidConfig(t *testing.T) {
+ wf := workflowSteps.DeepCopy()
+ wf.Spec.Use = []testworkflowsv1.TemplateRef{tplStepsConfigRefStringInvalid}
+ err := ApplyTemplates(wf, templates)
+
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), `spec.use[0]: resolving template: config.index`)
+ assert.Contains(t, err.Error(), `error while converting value to number`)
+}
+
+func TestApplyTemplatesConfig(t *testing.T) {
+ wf := workflowPod.DeepCopy()
+ wf.Spec.Use = []testworkflowsv1.TemplateRef{tplPodConfigRef}
+ err := ApplyTemplates(wf, templates)
+
+ want := workflowPod.DeepCopy()
+ want.Spec.Pod.Labels["department"] = "test-department"
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, wf)
+}
+
+func TestApplyTemplatesNoConfigMismatchNoOverride(t *testing.T) {
+ wf := workflowPodConfig.DeepCopy()
+ wf.Spec.Use = []testworkflowsv1.TemplateRef{tplPodConfigRef}
+ err := ApplyTemplates(wf, templates)
+
+ want := workflowPodConfig.DeepCopy()
+ want.Spec.Pod.Labels["department"] = "{{config.department}}"
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, wf)
+}
+
+func TestApplyTemplatesMergeTopLevelSteps(t *testing.T) {
+ wf := workflowSteps.DeepCopy()
+ wf.Spec.Use = []testworkflowsv1.TemplateRef{tplStepsRef}
+ err := ApplyTemplates(wf, templates)
+
+ want := workflowSteps.DeepCopy()
+ want.Spec.Setup = []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplSteps.Spec.Setup[0]),
+ want.Spec.Setup[0],
+ }
+ want.Spec.Steps = []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplSteps.Spec.Steps[0]),
+ want.Spec.Steps[0],
+ }
+ want.Spec.After = []testworkflowsv1.Step{
+ want.Spec.After[0],
+ ConvertIndependentStepToStep(tplSteps.Spec.After[0]),
+ }
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, wf)
+}
+
+func TestApplyTemplatesMergeMultipleTopLevelSteps(t *testing.T) {
+ wf := workflowSteps.DeepCopy()
+ wf.Spec.Use = []testworkflowsv1.TemplateRef{tplStepsRef, tplStepsConfigRef}
+ err := ApplyTemplates(wf, templates)
+
+ want := workflowSteps.DeepCopy()
+ want.Spec.Setup = []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplStepsConfig.Spec.Setup[0]),
+ ConvertIndependentStepToStep(tplSteps.Spec.Setup[0]),
+ want.Spec.Setup[0],
+ }
+ want.Spec.Setup[0].Name = "setup-tpl-test-20"
+ want.Spec.Steps = []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplStepsConfig.Spec.Steps[0]),
+ ConvertIndependentStepToStep(tplSteps.Spec.Steps[0]),
+ want.Spec.Steps[0],
+ }
+ want.Spec.Steps[0].Name = "steps-tpl-test-20"
+ want.Spec.After = []testworkflowsv1.Step{
+ want.Spec.After[0],
+ ConvertIndependentStepToStep(tplSteps.Spec.After[0]),
+ ConvertIndependentStepToStep(tplStepsConfig.Spec.After[0]),
+ }
+ want.Spec.After[2].Name = "after-tpl-test-20"
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, wf)
+}
+
+func TestApplyTemplatesMergeMultipleConfigurable(t *testing.T) {
+ wf := workflowSteps.DeepCopy()
+ wf.Spec.Use = []testworkflowsv1.TemplateRef{tplStepsConfigRefStringValid, tplStepsConfigRef}
+ err := ApplyTemplates(wf, templates)
+
+ want := workflowSteps.DeepCopy()
+ want.Spec.Setup = []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplStepsConfig.Spec.Setup[0]),
+ ConvertIndependentStepToStep(tplStepsConfig.Spec.Setup[0]),
+ want.Spec.Setup[0],
+ }
+ want.Spec.Setup[0].Name = "setup-tpl-test-20"
+ want.Spec.Setup[1].Name = "setup-tpl-test-10"
+ want.Spec.Steps = []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplStepsConfig.Spec.Steps[0]),
+ ConvertIndependentStepToStep(tplStepsConfig.Spec.Steps[0]),
+ want.Spec.Steps[0],
+ }
+ want.Spec.Steps[0].Name = "steps-tpl-test-20"
+ want.Spec.Steps[1].Name = "steps-tpl-test-10"
+ want.Spec.After = []testworkflowsv1.Step{
+ want.Spec.After[0],
+ ConvertIndependentStepToStep(tplStepsConfig.Spec.After[0]),
+ ConvertIndependentStepToStep(tplStepsConfig.Spec.After[0]),
+ }
+ want.Spec.After[1].Name = "after-tpl-test-10"
+ want.Spec.After[2].Name = "after-tpl-test-20"
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, wf)
+}
+
+func TestApplyTemplatesStepBasic(t *testing.T) {
+ s := *basicStep.DeepCopy()
+ s.Use = []testworkflowsv1.TemplateRef{tplEnvRef}
+ s, err := applyTemplatesToStep(s, templates)
+
+ want := *basicStep.DeepCopy()
+ want.Container.Env = append(tplEnv.Spec.Container.Env, want.Container.Env...)
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, s)
+}
+
+func TestApplyTemplatesStepIgnorePod(t *testing.T) {
+ s := *basicStep.DeepCopy()
+ s.Use = []testworkflowsv1.TemplateRef{tplPodRef}
+ s, err := applyTemplatesToStep(s, templates)
+
+ want := *basicStep.DeepCopy()
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, s)
+}
+
+func TestApplyTemplatesStepBasicIsolatedIgnore(t *testing.T) {
+ s := *basicStep.DeepCopy()
+ s.Template = &tplEnvRef
+ s, err := applyTemplatesToStep(s, templates)
+
+ want := *basicStep.DeepCopy()
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, s)
+}
+
+func TestApplyTemplatesStepBasicIsolated(t *testing.T) {
+ s := *basicStep.DeepCopy()
+ s.Template = &tplStepsRef
+ s, err := applyTemplatesToStep(s, templates)
+
+ want := *basicStep.DeepCopy()
+ want.Steps = append([]testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplSteps.Spec.Setup[0]),
+ ConvertIndependentStepToStep(tplSteps.Spec.Steps[0]),
+ ConvertIndependentStepToStep(tplSteps.Spec.After[0]),
+ }, want.Steps...)
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, s)
+}
+
+func TestApplyTemplatesStepBasicIsolatedWrapped(t *testing.T) {
+ s := *basicStep.DeepCopy()
+ s.Template = &tplStepsEnvRef
+ s, err := applyTemplatesToStep(s, templates)
+
+ want := *basicStep.DeepCopy()
+ want.Steps = append([]testworkflowsv1.Step{{
+ StepBase: testworkflowsv1.StepBase{
+ Container: tplStepsEnv.Spec.Container,
+ },
+ Setup: []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplStepsEnv.Spec.Setup[0]),
+ },
+ Steps: []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplStepsEnv.Spec.Steps[0]),
+ ConvertIndependentStepToStep(tplStepsEnv.Spec.After[0]),
+ },
+ }}, want.Steps...)
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, s)
+}
+
+func TestApplyTemplatesStepBasicSteps(t *testing.T) {
+ s := *basicStep.DeepCopy()
+ s.Use = []testworkflowsv1.TemplateRef{tplStepsRef}
+ s, err := applyTemplatesToStep(s, templates)
+
+ want := *basicStep.DeepCopy()
+ want.Setup = []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplSteps.Spec.Setup[0]),
+ }
+ want.Steps = append([]testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplSteps.Spec.Steps[0]),
+ }, append(want.Steps, []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplSteps.Spec.After[0]),
+ }...)...)
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, s)
+}
+
+func TestApplyTemplatesStepBasicMultipleSteps(t *testing.T) {
+ s := *basicStep.DeepCopy()
+ s.Use = []testworkflowsv1.TemplateRef{tplStepsRef, tplStepsConfigRef}
+ s, err := applyTemplatesToStep(s, templates)
+
+ want := *basicStep.DeepCopy()
+ want.Setup = []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplStepsConfig.Spec.Setup[0]),
+ ConvertIndependentStepToStep(tplSteps.Spec.Setup[0]),
+ }
+ want.Steps = append([]testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplStepsConfig.Spec.Steps[0]),
+ ConvertIndependentStepToStep(tplSteps.Spec.Steps[0]),
+ }, append(want.Steps, []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplSteps.Spec.After[0]),
+ ConvertIndependentStepToStep(tplStepsConfig.Spec.After[0]),
+ }...)...)
+ want.Setup[0].Name = "setup-tpl-test-20"
+ want.Steps[0].Name = "steps-tpl-test-20"
+ want.Steps[3].Name = "after-tpl-test-20"
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, s)
+}
+
+func TestApplyTemplatesStepAdvancedIsolated(t *testing.T) {
+ s := *advancedStep.DeepCopy()
+ s.Template = &tplStepsRef
+ s, err := applyTemplatesToStep(s, templates)
+
+ want := *advancedStep.DeepCopy()
+ want.Steps = append([]testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplSteps.Spec.Setup[0]),
+ ConvertIndependentStepToStep(tplSteps.Spec.Steps[0]),
+ ConvertIndependentStepToStep(tplSteps.Spec.After[0]),
+ }, want.Steps...)
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, s)
+}
+
+func TestApplyTemplatesStepAdvancedIsolatedWrapped(t *testing.T) {
+ s := *advancedStep.DeepCopy()
+ s.Template = &tplStepsEnvRef
+ s, err := applyTemplatesToStep(s, templates)
+
+ want := *advancedStep.DeepCopy()
+ want.Steps = append([]testworkflowsv1.Step{{
+ StepBase: testworkflowsv1.StepBase{
+ Container: tplStepsEnv.Spec.Container,
+ },
+ Setup: []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplStepsEnv.Spec.Setup[0]),
+ },
+ Steps: []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplStepsEnv.Spec.Steps[0]),
+ ConvertIndependentStepToStep(tplStepsEnv.Spec.After[0]),
+ },
+ }}, want.Steps...)
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, s)
+}
+
+func TestApplyTemplatesStepAdvancedSteps(t *testing.T) {
+ s := *advancedStep.DeepCopy()
+ s.Use = []testworkflowsv1.TemplateRef{tplStepsRef}
+ s, err := applyTemplatesToStep(s, templates)
+
+ want := *advancedStep.DeepCopy()
+ want.Setup = []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplSteps.Spec.Setup[0]),
+ }
+ want.Steps = append([]testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplSteps.Spec.Steps[0]),
+ }, append(want.Steps, []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplSteps.Spec.After[0]),
+ }...)...)
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, s)
+}
+
+func TestApplyTemplatesStepAdvancedMultipleSteps(t *testing.T) {
+ s := *advancedStep.DeepCopy()
+ s.Use = []testworkflowsv1.TemplateRef{tplStepsRef, tplStepsConfigRef}
+ s, err := applyTemplatesToStep(s, templates)
+
+ want := *advancedStep.DeepCopy()
+ want.Setup = []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplStepsConfig.Spec.Setup[0]),
+ ConvertIndependentStepToStep(tplSteps.Spec.Setup[0]),
+ }
+ want.Steps = append([]testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplStepsConfig.Spec.Steps[0]),
+ ConvertIndependentStepToStep(tplSteps.Spec.Steps[0]),
+ }, append(want.Steps, []testworkflowsv1.Step{
+ ConvertIndependentStepToStep(tplSteps.Spec.After[0]),
+ ConvertIndependentStepToStep(tplStepsConfig.Spec.After[0]),
+ }...)...)
+ want.Setup[0].Name = "setup-tpl-test-20"
+ want.Steps[0].Name = "steps-tpl-test-20"
+ want.Steps[4].Name = "after-tpl-test-20"
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, s)
+}
+
+func TestApplyTemplatesConfigOverflow(t *testing.T) {
+ wf := workflowPod.DeepCopy()
+ wf.Spec.Use = []testworkflowsv1.TemplateRef{{
+ Name: "podConfig",
+ Config: map[string]intstr.IntOrString{
+ "department": {Type: intstr.String, StrVal: "{{config.value}}"},
+ },
+ }}
+ err := ApplyTemplates(wf, templates)
+
+ want := workflowPod.DeepCopy()
+ want.Spec.Pod.Labels["department"] = "{{config.value}}"
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, wf)
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowresolver/config.go b/pkg/tcl/testworkflowstcl/testworkflowresolver/config.go
new file mode 100644
index 00000000000..27a010735c9
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowresolver/config.go
@@ -0,0 +1,86 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowresolver
+
+import (
+ "strconv"
+
+ "github.com/pkg/errors"
+ "k8s.io/apimachinery/pkg/util/intstr"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/pkg/tcl/expressionstcl"
+)
+
+var configFinalizer = expressionstcl.PrefixMachine("config.", expressionstcl.FinalizerFail)
+
+func castParameter(value intstr.IntOrString, schema testworkflowsv1.ParameterSchema) (expressionstcl.Expression, error) {
+ v := value.StrVal
+ if value.Type == intstr.Int {
+ v = strconv.Itoa(int(value.IntVal))
+ }
+ expr, err := expressionstcl.CompileTemplate(v)
+ if err != nil {
+ return nil, err
+ }
+ switch schema.Type {
+ case testworkflowsv1.ParameterTypeBoolean:
+ return expressionstcl.CastToBool(expr).Resolve()
+ case testworkflowsv1.ParameterTypeInteger:
+ return expressionstcl.CastToInt(expr).Resolve()
+ case testworkflowsv1.ParameterTypeNumber:
+ return expressionstcl.CastToFloat(expr).Resolve()
+ }
+ return expressionstcl.CastToString(expr).Resolve()
+}
+
+func createConfigMachine(cfg map[string]intstr.IntOrString, schema map[string]testworkflowsv1.ParameterSchema) (expressionstcl.Machine, error) {
+ machine := expressionstcl.NewMachine()
+ for k, v := range cfg {
+ expr, err := castParameter(v, schema[k])
+ if err != nil {
+ return nil, errors.Wrap(err, "config."+k)
+ }
+ machine.Register("config."+k, expr)
+ }
+ for k := range schema {
+ if schema[k].Default != nil {
+ expr, err := castParameter(*schema[k].Default, schema[k])
+ if err != nil {
+ return nil, errors.Wrap(err, "config."+k)
+ }
+ machine.Register("config."+k, expr)
+ }
+ }
+ return machine, nil
+}
+
+func ApplyWorkflowConfig(t *testworkflowsv1.TestWorkflow, cfg map[string]intstr.IntOrString) (*testworkflowsv1.TestWorkflow, error) {
+ if t == nil {
+ return t, nil
+ }
+ machine, err := createConfigMachine(cfg, t.Spec.Config)
+ if err != nil {
+ return nil, err
+ }
+ err = expressionstcl.Simplify(&t, machine, configFinalizer)
+ return t, err
+}
+
+func ApplyWorkflowTemplateConfig(t *testworkflowsv1.TestWorkflowTemplate, cfg map[string]intstr.IntOrString) (*testworkflowsv1.TestWorkflowTemplate, error) {
+ if t == nil {
+ return t, nil
+ }
+ machine, err := createConfigMachine(cfg, t.Spec.Config)
+ if err != nil {
+ return nil, err
+ }
+ err = expressionstcl.Simplify(&t, machine, configFinalizer)
+ return t, err
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowresolver/config_test.go b/pkg/tcl/testworkflowstcl/testworkflowresolver/config_test.go
new file mode 100644
index 00000000000..e50705e9a30
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowresolver/config_test.go
@@ -0,0 +1,251 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowresolver
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "k8s.io/apimachinery/pkg/util/intstr"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/internal/common"
+)
+
+func TestApplyConfigTestWorkflow(t *testing.T) {
+ cfg := map[string]intstr.IntOrString{
+ "foo": {Type: intstr.Int, IntVal: 30},
+ "bar": {Type: intstr.String, StrVal: "some value"},
+ "baz": {Type: intstr.String, StrVal: "some {{ 30 }} value"},
+ "foobar": {Type: intstr.String, StrVal: "some {{ unknown(300) }} value"},
+ }
+ want := &testworkflowsv1.TestWorkflow{
+ Description: "{{some description here }}",
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Pod: &testworkflowsv1.PodConfig{
+ ServiceAccountName: "abra 30",
+ Labels: map[string]string{
+ "some value-key": "some 30 value",
+ "other": "{{value}}",
+ },
+ },
+ },
+ Steps: []testworkflowsv1.Step{
+ {
+ StepBase: testworkflowsv1.StepBase{
+ Container: &testworkflowsv1.ContainerConfig{
+ WorkingDir: common.Ptr("some {{unknown(300)}} value {{another(500)}}"),
+ },
+ },
+ },
+ },
+ },
+ }
+ got, err := ApplyWorkflowConfig(&testworkflowsv1.TestWorkflow{
+ Description: "{{some description here }}",
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Pod: &testworkflowsv1.PodConfig{
+ ServiceAccountName: "abra {{config.foo}}",
+ Labels: map[string]string{
+ "{{config.bar}}-key": "{{config.baz}}",
+ "other": "{{value}}",
+ },
+ },
+ },
+ Steps: []testworkflowsv1.Step{
+ {
+ StepBase: testworkflowsv1.StepBase{
+ Container: &testworkflowsv1.ContainerConfig{
+ WorkingDir: common.Ptr("{{config.foobar}} {{another(500)}}"),
+ },
+ },
+ },
+ },
+ },
+ }, cfg)
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, got)
+}
+
+func TestApplyMissingConfig(t *testing.T) {
+ cfg := map[string]intstr.IntOrString{
+ "foo": {Type: intstr.Int, IntVal: 30},
+ "bar": {Type: intstr.String, StrVal: "some value"},
+ "foobar": {Type: intstr.String, StrVal: "some {{ unknown(300) }} value"},
+ }
+ _, err := ApplyWorkflowConfig(&testworkflowsv1.TestWorkflow{
+ Description: "{{some description here }}",
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Pod: &testworkflowsv1.PodConfig{
+ ServiceAccountName: "abra {{config.foo}}",
+ Labels: map[string]string{
+ "{{config.bar}}-key": "{{config.baz}}",
+ },
+ },
+ },
+ Steps: []testworkflowsv1.Step{
+ {
+ StepBase: testworkflowsv1.StepBase{
+ Container: &testworkflowsv1.ContainerConfig{
+ WorkingDir: common.Ptr("{{config.foobar}} {{another(500)}}"),
+ },
+ },
+ },
+ },
+ },
+ }, cfg)
+
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "Spec: TestWorkflowSpecBase: Pod: Labels: {{config.bar}}-key")
+ assert.Contains(t, err.Error(), "error while accessing config.baz: unknown variable")
+}
+
+func TestApplyConfigDefaults(t *testing.T) {
+ cfg := map[string]intstr.IntOrString{
+ "foo": {Type: intstr.Int, IntVal: 30},
+ "bar": {Type: intstr.String, StrVal: "some value"},
+ "foobar": {Type: intstr.String, StrVal: "some {{ unknown(300) }} value"},
+ }
+ want := &testworkflowsv1.TestWorkflow{
+ Description: "{{some description here }}",
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Config: map[string]testworkflowsv1.ParameterSchema{
+ "baz": {Default: &intstr.IntOrString{Type: intstr.String, StrVal: "something"}},
+ },
+ Pod: &testworkflowsv1.PodConfig{
+ ServiceAccountName: "abra 30",
+ Labels: map[string]string{
+ "some value-key": "something",
+ },
+ },
+ },
+ Steps: []testworkflowsv1.Step{
+ {
+ StepBase: testworkflowsv1.StepBase{
+ Container: &testworkflowsv1.ContainerConfig{
+ WorkingDir: common.Ptr("some {{unknown(300)}} value {{another(500)}}"),
+ },
+ },
+ },
+ },
+ },
+ }
+ got, err := ApplyWorkflowConfig(&testworkflowsv1.TestWorkflow{
+ Description: "{{some description here }}",
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Config: map[string]testworkflowsv1.ParameterSchema{
+ "baz": {Default: &intstr.IntOrString{Type: intstr.String, StrVal: "something"}},
+ },
+ Pod: &testworkflowsv1.PodConfig{
+ ServiceAccountName: "abra {{config.foo}}",
+ Labels: map[string]string{
+ "{{config.bar}}-key": "{{config.baz}}",
+ },
+ },
+ },
+ Steps: []testworkflowsv1.Step{
+ {
+ StepBase: testworkflowsv1.StepBase{
+ Container: &testworkflowsv1.ContainerConfig{
+ WorkingDir: common.Ptr("{{config.foobar}} {{another(500)}}"),
+ },
+ },
+ },
+ },
+ },
+ }, cfg)
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, got)
+}
+
+func TestInvalidInteger(t *testing.T) {
+ cfg := map[string]intstr.IntOrString{
+ "foo": {Type: intstr.String, StrVal: "some value"},
+ }
+ _, err := ApplyWorkflowConfig(&testworkflowsv1.TestWorkflow{
+ Description: "{{some description here }}",
+ Spec: testworkflowsv1.TestWorkflowSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Config: map[string]testworkflowsv1.ParameterSchema{
+ "foo": {Type: testworkflowsv1.ParameterTypeInteger},
+ },
+ Pod: &testworkflowsv1.PodConfig{
+ ServiceAccountName: "{{config.foo}}",
+ },
+ },
+ },
+ }, cfg)
+
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "config.foo: error")
+ assert.Contains(t, err.Error(), "error while converting value to number")
+}
+
+func TestApplyConfigTestWorkflowTemplate(t *testing.T) {
+ cfg := map[string]intstr.IntOrString{
+ "foo": {Type: intstr.Int, IntVal: 30},
+ "bar": {Type: intstr.String, StrVal: "some value"},
+ "baz": {Type: intstr.String, StrVal: "some {{ 30 }} value"},
+ "foobar": {Type: intstr.String, StrVal: "some {{ unknown(300) }} value"},
+ }
+ want := &testworkflowsv1.TestWorkflowTemplate{
+ Description: "{{some description here }}",
+ Spec: testworkflowsv1.TestWorkflowTemplateSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Pod: &testworkflowsv1.PodConfig{
+ ServiceAccountName: "abra 30",
+ Labels: map[string]string{
+ "some value-key": "some 30 value",
+ },
+ },
+ },
+ Steps: []testworkflowsv1.IndependentStep{
+ {
+ StepBase: testworkflowsv1.StepBase{
+ Container: &testworkflowsv1.ContainerConfig{
+ WorkingDir: common.Ptr("some {{unknown(300)}} value {{another(500)}}"),
+ },
+ },
+ },
+ },
+ },
+ }
+ got, err := ApplyWorkflowTemplateConfig(&testworkflowsv1.TestWorkflowTemplate{
+ Description: "{{some description here }}",
+ Spec: testworkflowsv1.TestWorkflowTemplateSpec{
+ TestWorkflowSpecBase: testworkflowsv1.TestWorkflowSpecBase{
+ Pod: &testworkflowsv1.PodConfig{
+ ServiceAccountName: "abra {{config.foo}}",
+ Labels: map[string]string{
+ "{{config.bar}}-key": "{{config.baz}}",
+ },
+ },
+ },
+ Steps: []testworkflowsv1.IndependentStep{
+ {
+ StepBase: testworkflowsv1.StepBase{
+ Container: &testworkflowsv1.ContainerConfig{
+ WorkingDir: common.Ptr("{{config.foobar}} {{another(500)}}"),
+ },
+ },
+ },
+ },
+ },
+ }, cfg)
+
+ assert.NoError(t, err)
+ assert.Equal(t, want, got)
+}
diff --git a/pkg/tcl/testworkflowstcl/testworkflowresolver/merge.go b/pkg/tcl/testworkflowstcl/testworkflowresolver/merge.go
new file mode 100644
index 00000000000..04c053453e5
--- /dev/null
+++ b/pkg/tcl/testworkflowstcl/testworkflowresolver/merge.go
@@ -0,0 +1,145 @@
+// Copyright 2024 Testkube.
+//
+// Licensed as a Testkube Pro file under the Testkube Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
+
+package testworkflowresolver
+
+import (
+ "maps"
+
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/util/intstr"
+
+ testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1"
+ "github.com/kubeshop/testkube/internal/common"
+)
+
+func MergePodConfig(dst, include *testworkflowsv1.PodConfig) *testworkflowsv1.PodConfig {
+ if dst == nil {
+ return include
+ } else if include == nil {
+ return dst
+ }
+ if len(include.Labels) > 0 && dst.Labels == nil {
+ dst.Labels = map[string]string{}
+ }
+ maps.Copy(dst.Labels, include.Labels)
+ if len(include.Annotations) > 0 && dst.Annotations == nil {
+ dst.Annotations = map[string]string{}
+ }
+ maps.Copy(dst.Annotations, include.Annotations)
+ if len(include.NodeSelector) > 0 && dst.NodeSelector == nil {
+ dst.NodeSelector = map[string]string{}
+ }
+ maps.Copy(dst.NodeSelector, include.NodeSelector)
+ dst.Volumes = append(dst.Volumes, include.Volumes...)
+ dst.ImagePullSecrets = append(dst.ImagePullSecrets, include.ImagePullSecrets...)
+ if include.ServiceAccountName != "" {
+ dst.ServiceAccountName = include.ServiceAccountName
+ }
+ return dst
+}
+
+func MergeJobConfig(dst, include *testworkflowsv1.JobConfig) *testworkflowsv1.JobConfig {
+ if dst == nil {
+ return include
+ } else if include == nil {
+ return dst
+ }
+ if len(include.Labels) > 0 && dst.Labels == nil {
+ dst.Labels = map[string]string{}
+ }
+ maps.Copy(dst.Labels, include.Labels)
+ if len(include.Annotations) > 0 && dst.Annotations == nil {
+ dst.Annotations = map[string]string{}
+ }
+ maps.Copy(dst.Annotations, include.Annotations)
+ return dst
+}
+
+func MergeContentGit(dst, include *testworkflowsv1.ContentGit) *testworkflowsv1.ContentGit {
+ if dst == nil {
+ return include
+ } else if include == nil {
+ return dst
+ }
+ return include
+}
+
+func MergeSecurityContext(dst, include *corev1.SecurityContext) *corev1.SecurityContext {
+ if dst == nil {
+ return include
+ } else if include == nil {
+ return dst
+ }
+ return include
+}
+
+func MergeContent(dst, include *testworkflowsv1.Content) *testworkflowsv1.Content {
+ if dst == nil {
+ return include
+ } else if include == nil {
+ return dst
+ }
+ dst.Files = append(dst.Files, include.Files...)
+ dst.Git = MergeContentGit(dst.Git, include.Git)
+ return dst
+}
+
+func MergeResources(dst, include *testworkflowsv1.Resources) *testworkflowsv1.Resources {
+ if dst == nil {
+ return include
+ } else if include == nil {
+ return dst
+ }
+ if dst.Requests == nil && len(include.Requests) > 0 {
+ dst.Requests = map[corev1.ResourceName]intstr.IntOrString{}
+ }
+ if dst.Limits == nil && len(include.Limits) > 0 {
+ dst.Limits = map[corev1.ResourceName]intstr.IntOrString{}
+ }
+ maps.Copy(dst.Requests, include.Requests)
+ maps.Copy(dst.Limits, include.Limits)
+ return dst
+}
+
+func MergeContainerConfig(dst, include *testworkflowsv1.ContainerConfig) *testworkflowsv1.ContainerConfig {
+ if dst == nil {
+ return include
+ } else if include == nil {
+ return dst
+ }
+ if include.WorkingDir != nil {
+ dst.WorkingDir = include.WorkingDir
+ }
+ if include.ImagePullPolicy != "" {
+ dst.ImagePullPolicy = include.ImagePullPolicy
+ }
+ dst.Env = append(dst.Env, include.Env...)
+ dst.EnvFrom = append(dst.EnvFrom, include.EnvFrom...)
+ dst.VolumeMounts = append(dst.VolumeMounts, include.VolumeMounts...)
+ if include.Image != "" {
+ dst.Image = include.Image
+ dst.Command = include.Command
+ dst.Args = include.Args
+ } else if include.Command != nil {
+ dst.Command = include.Command
+ dst.Args = include.Args
+ } else if include.Args != nil {
+ dst.Args = include.Args
+ }
+ dst.Resources = MergeResources(dst.Resources, include.Resources)
+ dst.SecurityContext = MergeSecurityContext(dst.SecurityContext, include.SecurityContext)
+ return dst
+}
+
+func ConvertIndependentStepToStep(step testworkflowsv1.IndependentStep) (res testworkflowsv1.Step) {
+ res.StepBase = step.StepBase
+ res.Setup = common.MapSlice(step.Setup, ConvertIndependentStepToStep)
+ res.Steps = common.MapSlice(step.Steps, ConvertIndependentStepToStep)
+ return res
+}
diff --git a/pkg/telemetry/payload.go b/pkg/telemetry/payload.go
index 60a7cf108ad..ddde292e62b 100644
--- a/pkg/telemetry/payload.go
+++ b/pkg/telemetry/payload.go
@@ -1,10 +1,10 @@
package telemetry
import (
- "os"
"runtime"
"strings"
+ "github.com/kubeshop/testkube/pkg/utils"
"github.com/kubeshop/testkube/pkg/utils/text"
)
@@ -31,6 +31,7 @@ type Params struct {
ClusterType string `json:"cluster_type,omitempty"`
Error string `json:"error,omitempty"`
ErrorType string `json:"error_type,omitempty"`
+ ErrorStackTrace string `json:"error_stacktrace,omitempty"`
}
type Event struct {
@@ -194,8 +195,8 @@ func AnonymizeHost(host string) string {
}
func getAgentContext() RunContext {
- orgID := os.Getenv("TESTKUBE_CLOUD_ORG_ID")
- envID := os.Getenv("TESTKUBE_CLOUD_ENV_ID")
+ orgID := utils.GetEnvVarWithDeprecation("TESTKUBE_PRO_ORG_ID", "TESTKUBE_CLOUD_ORG_ID", "")
+ envID := utils.GetEnvVarWithDeprecation("TESTKUBE_PRO_ENV_ID", "TESTKUBE_CLOUD_ENV_ID", "")
if orgID == "" || envID == "" {
return RunContext{}
diff --git a/pkg/telemetry/sender_sio.go b/pkg/telemetry/sender_sio.go
index c9253a4ec64..1218dfd223a 100644
--- a/pkg/telemetry/sender_sio.go
+++ b/pkg/telemetry/sender_sio.go
@@ -8,15 +8,19 @@ import (
"github.com/segmentio/analytics-go/v3"
"github.com/kubeshop/testkube/pkg/log"
+ "github.com/kubeshop/testkube/pkg/utils"
)
const SegmentioEnvVariableName = "TESTKUBE_SEGMENTIO_KEY"
const CloudEnvVariableName = "TESTKUBE_CLOUD_API_KEY"
+const ProEnvVariableName = "TESTKUBE_PRO_API_KEY"
// Brew builds can't be parametrized so we are embedding this one
var SegmentioKey = "jELokNFNcLeQhxdpGF47PcxCtOLpwVuu"
var CloudSegmentioKey = ""
+const AppBuild string = "oss"
+
func StdLogger() analytics.Logger {
return stdLogger{}
}
@@ -40,10 +44,9 @@ func SegmentioSender(client *http.Client, payload Payload) (out string, err erro
if key, ok := os.LookupEnv(SegmentioEnvVariableName); ok {
SegmentioKey = key
}
- if key, ok := os.LookupEnv(CloudEnvVariableName); ok {
- if key != "" {
- SegmentioKey = CloudSegmentioKey
- }
+ key := utils.GetEnvVarWithDeprecation(ProEnvVariableName, CloudEnvVariableName, "")
+ if key != "" {
+ SegmentioKey = CloudSegmentioKey
}
segmentio, err := analytics.NewWithConfig(SegmentioKey, analytics.Config{Logger: StdLogger()})
@@ -68,6 +71,13 @@ func mapEvent(userID string, event Event) analytics.Track {
Event: event.Name,
UserId: userID,
Properties: mapProperties(event.Params),
+ Context: &analytics.Context{
+ App: analytics.AppInfo{
+ Name: event.Params.AppName,
+ Version: event.Params.AppVersion,
+ Build: AppBuild,
+ },
+ },
}
}
@@ -85,7 +95,8 @@ func mapProperties(params Params) analytics.Properties {
Set("cloudEnvironmentId", params.Context.EnvironmentId).
Set("machineId", params.MachineID).
Set("clusterType", params.ClusterType).
- Set("errorType", params.ErrorType)
+ Set("errorType", params.ErrorType).
+ Set("errorStackTrace", params.ErrorStackTrace)
if params.DataSource != "" {
properties = properties.Set("dataSource", params.DataSource)
diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go
index e6a35418644..62907969bee 100644
--- a/pkg/telemetry/telemetry.go
+++ b/pkg/telemetry/telemetry.go
@@ -46,7 +46,7 @@ func SendCmdEvent(cmd *cobra.Command, version string) (string, error) {
return sendData(senders, payload)
}
-func SendCmdErrorEvent(cmd *cobra.Command, version, errType string) (string, error) {
+func SendCmdErrorEvent(cmd *cobra.Command, version, errType string, errorStackTrace string) (string, error) {
// get all sub-commands passed to cli
command := strings.TrimPrefix(cmd.CommandPath(), "kubectl-testkube ")
if command == "" {
@@ -72,6 +72,7 @@ func SendCmdErrorEvent(cmd *cobra.Command, version, errType string) (string, err
Context: getCurrentContext(),
ClusterType: GetClusterType(),
ErrorType: errType,
+ ErrorStackTrace: errorStackTrace,
},
}},
}
diff --git a/pkg/triggers/executor_test.go b/pkg/triggers/executor_test.go
index 371981fdcd7..ee93be8a0df 100644
--- a/pkg/triggers/executor_test.go
+++ b/pkg/triggers/executor_test.go
@@ -17,13 +17,14 @@ import (
testsuiteexecutionsv1 "github.com/kubeshop/testkube-operator/pkg/client/testsuiteexecutions/v1"
testsuitesv3 "github.com/kubeshop/testkube-operator/pkg/client/testsuites/v3"
"github.com/kubeshop/testkube/internal/app/api/metrics"
- "github.com/kubeshop/testkube/internal/featureflags"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/configmap"
"github.com/kubeshop/testkube/pkg/event"
"github.com/kubeshop/testkube/pkg/event/bus"
"github.com/kubeshop/testkube/pkg/executor/client"
+ "github.com/kubeshop/testkube/pkg/featureflags"
"github.com/kubeshop/testkube/pkg/log"
+ logsclient "github.com/kubeshop/testkube/pkg/logs/client"
"github.com/kubeshop/testkube/pkg/repository/config"
"github.com/kubeshop/testkube/pkg/repository/result"
"github.com/kubeshop/testkube/pkg/repository/testresult"
@@ -103,6 +104,8 @@ func TestExecute(t *testing.T) {
mockExecutor.EXPECT().Execute(gomock.Any(), gomock.Any(), gomock.Any()).Return(&mockExecutionResult, nil)
mockResultRepository.EXPECT().UpdateResult(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
+ mockLogsStream := logsclient.NewMockStream(mockCtrl)
+
sched := scheduler.NewScheduler(
metricsHandle,
mockExecutor,
@@ -122,6 +125,9 @@ func TestExecute(t *testing.T) {
mockBus,
"",
featureflags.FeatureFlags{},
+ mockLogsStream,
+ "",
+ "",
)
s := &Service{
triggerStatus: make(map[statusKey]*triggerStatus),
diff --git a/pkg/triggers/service_test.go b/pkg/triggers/service_test.go
index e2cf5a43e10..1a7192c4ae6 100644
--- a/pkg/triggers/service_test.go
+++ b/pkg/triggers/service_test.go
@@ -14,7 +14,6 @@ import (
executorv1 "github.com/kubeshop/testkube-operator/api/executor/v1"
testsv3 "github.com/kubeshop/testkube-operator/api/tests/v3"
testtriggersv1 "github.com/kubeshop/testkube-operator/api/testtriggers/v1"
- v1 "github.com/kubeshop/testkube-operator/api/testtriggers/v1"
executorsclientv1 "github.com/kubeshop/testkube-operator/pkg/client/executors/v1"
testsclientv3 "github.com/kubeshop/testkube-operator/pkg/client/tests/v3"
testsourcesv1 "github.com/kubeshop/testkube-operator/pkg/client/testsources/v1"
@@ -22,13 +21,14 @@ import (
testsuitesv3 "github.com/kubeshop/testkube-operator/pkg/client/testsuites/v3"
faketestkube "github.com/kubeshop/testkube-operator/pkg/clientset/versioned/fake"
"github.com/kubeshop/testkube/internal/app/api/metrics"
- "github.com/kubeshop/testkube/internal/featureflags"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/configmap"
"github.com/kubeshop/testkube/pkg/event"
"github.com/kubeshop/testkube/pkg/event/bus"
"github.com/kubeshop/testkube/pkg/executor/client"
+ "github.com/kubeshop/testkube/pkg/featureflags"
"github.com/kubeshop/testkube/pkg/log"
+ logsclient "github.com/kubeshop/testkube/pkg/logs/client"
"github.com/kubeshop/testkube/pkg/repository/config"
"github.com/kubeshop/testkube/pkg/repository/result"
"github.com/kubeshop/testkube/pkg/repository/testresult"
@@ -117,6 +117,8 @@ func TestService_Run(t *testing.T) {
testLogger := log.DefaultLogger
+ mockLogsStream := logsclient.NewMockStream(mockCtrl)
+
sched := scheduler.NewScheduler(
testMetrics,
mockExecutor,
@@ -136,6 +138,9 @@ func TestService_Run(t *testing.T) {
mockBus,
"",
featureflags.FeatureFlags{},
+ mockLogsStream,
+ "",
+ "",
)
mockLeaseBackend := NewMockLeaseBackend(mockCtrl)
@@ -207,7 +212,7 @@ func TestService_addTrigger(t *testing.T) {
s := Service{triggerStatus: make(map[statusKey]*triggerStatus)}
- testTrigger := v1.TestTrigger{
+ testTrigger := testtriggersv1.TestTrigger{
ObjectMeta: metav1.ObjectMeta{Name: "test-trigger-1", Namespace: "testkube"},
}
s.addTrigger(&testTrigger)
@@ -222,10 +227,10 @@ func TestService_removeTrigger(t *testing.T) {
s := Service{triggerStatus: make(map[statusKey]*triggerStatus)}
- testTrigger1 := v1.TestTrigger{
+ testTrigger1 := testtriggersv1.TestTrigger{
ObjectMeta: metav1.ObjectMeta{Name: "test-trigger-1", Namespace: "testkube"},
}
- testTrigger2 := v1.TestTrigger{
+ testTrigger2 := testtriggersv1.TestTrigger{
ObjectMeta: metav1.ObjectMeta{Name: "test-trigger-2", Namespace: "testkube"},
}
s.addTrigger(&testTrigger1)
@@ -247,15 +252,15 @@ func TestService_updateTrigger(t *testing.T) {
s := Service{triggerStatus: make(map[statusKey]*triggerStatus)}
- oldTestTrigger := v1.TestTrigger{
+ oldTestTrigger := testtriggersv1.TestTrigger{
ObjectMeta: metav1.ObjectMeta{Namespace: "testkube", Name: "test-trigger-1"},
- Spec: v1.TestTriggerSpec{Event: "created"},
+ Spec: testtriggersv1.TestTriggerSpec{Event: "created"},
}
s.addTrigger(&oldTestTrigger)
- newTestTrigger := v1.TestTrigger{
+ newTestTrigger := testtriggersv1.TestTrigger{
ObjectMeta: metav1.ObjectMeta{Namespace: "testkube", Name: "test-trigger-1"},
- Spec: v1.TestTriggerSpec{Event: "modified"},
+ Spec: testtriggersv1.TestTriggerSpec{Event: "modified"},
}
s.updateTrigger(&newTestTrigger)
diff --git a/pkg/triggers/watcher.go b/pkg/triggers/watcher.go
index 7413671c264..43719f8b8c5 100644
--- a/pkg/triggers/watcher.go
+++ b/pkg/triggers/watcher.go
@@ -9,7 +9,6 @@ import (
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/informers"
appsinformerv1 "k8s.io/client-go/informers/apps/v1"
coreinformerv1 "k8s.io/client-go/informers/core/v1"
@@ -19,6 +18,7 @@ import (
executorv1 "github.com/kubeshop/testkube-operator/api/executor/v1"
testsourcev1 "github.com/kubeshop/testkube-operator/api/testsource/v1"
+ "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor"
testsv3 "github.com/kubeshop/testkube-operator/api/tests/v3"
@@ -57,7 +57,7 @@ func newK8sInformers(clientset kubernetes.Interface, testKubeClientset versioned
testkubeNamespace string, watcherNamespaces []string) *k8sInformers {
var k8sInformers k8sInformers
if len(watcherNamespaces) == 0 {
- watcherNamespaces = append(watcherNamespaces, v1.NamespaceAll)
+ watcherNamespaces = append(watcherNamespaces, metav1.NamespaceAll)
}
for _, namespace := range watcherNamespaces {
@@ -275,8 +275,8 @@ func (s *Service) podEventHandler(ctx context.Context) cache.ResourceEventHandle
)
return
}
- if oldPod.Namespace == s.testkubeNamespace && oldPod.Labels["job-name"] != "" &&
- newPod.Namespace == s.testkubeNamespace && newPod.Labels["job-name"] != "" &&
+ if oldPod.Namespace == s.testkubeNamespace && oldPod.Labels["job-name"] != "" && oldPod.Labels[testkube.TestLabelTestName] != "" &&
+ newPod.Namespace == s.testkubeNamespace && newPod.Labels["job-name"] != "" && newPod.Labels[testkube.TestLabelTestName] != "" &&
oldPod.Labels["job-name"] == newPod.Labels["job-name"] {
s.checkExecutionPodStatus(ctx, oldPod.Labels["job-name"], []*corev1.Pod{oldPod, newPod})
}
@@ -288,7 +288,7 @@ func (s *Service) podEventHandler(ctx context.Context) cache.ResourceEventHandle
return
}
s.logger.Debugf("trigger service: watcher component: emiting event: pod %s/%s deleted", pod.Namespace, pod.Name)
- if pod.Namespace == s.testkubeNamespace && pod.Labels["job-name"] != "" {
+ if pod.Namespace == s.testkubeNamespace && pod.Labels["job-name"] != "" && pod.Labels[testkube.TestLabelTestName] != "" {
s.checkExecutionPodStatus(ctx, pod.Labels["job-name"], []*corev1.Pod{pod})
}
event := newWatcherEvent(testtrigger.EventDeleted, pod, testtrigger.ResourcePod,
@@ -301,6 +301,10 @@ func (s *Service) podEventHandler(ctx context.Context) cache.ResourceEventHandle
}
func (s *Service) checkExecutionPodStatus(ctx context.Context, executionID string, pods []*corev1.Pod) error {
+ if len(pods) > 0 && pods[0].Labels[testworkflowprocessor.ExecutionIdLabelName] != "" {
+ return nil
+ }
+
execution, err := s.resultRepository.Get(ctx, executionID)
if err != nil {
s.logger.Errorf("get execution returned an error %v while looking for execution id: %s", err, executionID)
diff --git a/pkg/ui/ui.go b/pkg/ui/ui.go
index bad58ffd09f..e6658035ed8 100644
--- a/pkg/ui/ui.go
+++ b/pkg/ui/ui.go
@@ -4,6 +4,10 @@ package ui
import (
"io"
"os"
+
+ "k8s.io/apimachinery/pkg/runtime/schema"
+
+ "github.com/kubeshop/testkube/internal/common"
)
const (
@@ -90,5 +94,20 @@ func Confirm(message string) bool { return ui.Confirm(
func Select(title string, options []string) string { return ui.Select(title, options) }
func TextInput(message string) string { return ui.TextInput(message) }
+func PrintCRD[T interface{}](cr T, kind string, groupVersion schema.GroupVersion) {
+ PrintCRDs([]T{cr}, kind, groupVersion)
+}
+
+func PrintCRDs[T interface{}](crs []T, kind string, groupVersion schema.GroupVersion) {
+ bytes, err := common.SerializeCRDs(crs, common.SerializeOptions{
+ OmitCreationTimestamp: true,
+ CleanMeta: true,
+ Kind: kind,
+ GroupVersion: &groupVersion,
+ })
+ ui.ExitOnError("serializing the crds", err)
+ _, _ = os.Stdout.Write(bytes)
+}
+
func UseStdout() { ui = uiOut }
func UseStderr() { ui = uiErr }
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
index 6881b120b98..d8981cbc921 100644
--- a/pkg/utils/utils.go
+++ b/pkg/utils/utils.go
@@ -5,6 +5,7 @@ import (
"crypto/rand"
"encoding/base64"
"math/big"
+ "os"
"path/filepath"
"regexp"
"strings"
@@ -141,3 +142,16 @@ func IsBase64Encoded(base64Val string) bool {
encoded := base64.StdEncoding.EncodeToString(decoded)
return base64Val == encoded
}
+
+// GetEnvVarWithDeprecation returns the value of the environment variable with the given key,
+// or the value of the environment variable with the given deprecated key, or the default value
+// if neither is set
+func GetEnvVarWithDeprecation(key, deprecatedKey, defaultVal string) string {
+ if val, ok := os.LookupEnv(key); ok {
+ return val
+ }
+ if val, ok := os.LookupEnv(deprecatedKey); ok {
+ return val
+ }
+ return defaultVal
+}
diff --git a/proto/service.proto b/proto/service.proto
index d6c5a78c4c9..bcecb576763 100644
--- a/proto/service.proto
+++ b/proto/service.proto
@@ -15,6 +15,7 @@ service TestKubeCloudAPI {
rpc Call(CommandRequest) returns (CommandResponse);
rpc ExecuteAsync(stream ExecuteResponse) returns (stream ExecuteRequest);
rpc GetLogsStream(stream LogsStreamResponse) returns (stream LogsStreamRequest);
+ rpc GetTestWorkflowNotificationsStream(stream TestWorkflowNotificationsResponse) returns (stream TestWorkflowNotificationsRequest);
}
enum LogsStreamRequestType {
@@ -22,6 +23,18 @@ enum LogsStreamRequestType {
STREAM_HEALTH_CHECK = 1;
}
+enum TestWorkflowNotificationsRequestType {
+ WORKFLOW_STREAM_LOG_MESSAGE = 0;
+ WORKFLOW_STREAM_HEALTH_CHECK = 1;
+}
+
+enum TestWorkflowNotificationType {
+ WORKFLOW_STREAM_ERROR = 0;
+ WORKFLOW_STREAM_LOG = 1;
+ WORKFLOW_STREAM_RESULT = 2;
+ WORKFLOW_STREAM_OUTPUT = 3;
+}
+
message LogsStreamRequest {
string stream_id = 1;
string execution_id = 2;
@@ -52,6 +65,21 @@ message ExecuteRequest {
string message_id = 5;
}
+message TestWorkflowNotificationsRequest {
+ string stream_id = 1;
+ string execution_id = 2;
+ TestWorkflowNotificationsRequestType request_type = 3;
+}
+
+message TestWorkflowNotificationsResponse {
+ string stream_id = 1;
+ uint32 seq_no = 2;
+ string timestamp = 3;
+ string ref = 4;
+ TestWorkflowNotificationType type = 5;
+ string message = 6; // based on type: log/error = inline, others = serialized to JSON
+}
+
message HeaderValue {
repeated string header = 1;
}
diff --git a/test/container-executor/executor-smoke/crd/cypress.yaml b/test/container-executor/executor-smoke/crd/cypress.yaml
index c48a40f58c8..fb7cfbde557 100644
--- a/test/container-executor/executor-smoke/crd/cypress.yaml
+++ b/test/container-executor/executor-smoke/crd/cypress.yaml
@@ -67,3 +67,38 @@ spec:
- ./
jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n"
activeDeadlineSeconds: 600
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: container-executor-cypress-v12.7.0-video-artifacts-only
+ labels:
+ core-tests: executors
+spec:
+ type: container-executor-cypress-v12.7.0/test
+ content:
+ type: git-dir
+ repository:
+ type: git-dir
+ uri: https://github.com/kubeshop/testkube
+ branch: main
+ path: test/cypress/executor-tests/cypress-12
+ workingDir: test/cypress/executor-tests/cypress-12
+ executionRequest:
+ variables:
+ CYPRESS_CUSTOM_ENV:
+ name: CYPRESS_CUSTOM_ENV
+ value: CYPRESS_CUSTOM_ENV_value
+ type: basic
+ args:
+ - --env
+ - NON_CYPRESS_ENV=NON_CYPRESS_ENV_value
+ - --config
+ - '{"screenshotsFolder":"/data/artifacts/screenshots","videosFolder":"/data/artifacts/videos"}'
+ artifactRequest:
+ storageClassName: standard
+ volumeMountPath: /data/artifacts/videos
+ dirs:
+ - ./
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n"
+ activeDeadlineSeconds: 600
diff --git a/test/container-executor/executor-smoke/crd/gradle.yaml b/test/container-executor/executor-smoke/crd/gradle.yaml
new file mode 100644
index 00000000000..a01732880ba
--- /dev/null
+++ b/test/container-executor/executor-smoke/crd/gradle.yaml
@@ -0,0 +1,22 @@
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: container-executor-gradle-jdk-11
+ labels:
+ core-tests: executors
+spec:
+ type: container-executor-gradle-8.5-jdk11/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube
+ branch: main
+ path: contrib/executor/gradle/examples/hello-gradle
+ workingDir: contrib/executor/gradle/examples/hello-gradle
+ executionRequest:
+ variables:
+ TESTKUBE_GRADLE:
+ name: TESTKUBE_GRADLE
+ value: "true"
+ type: basic
diff --git a/test/container-executor/executor-smoke/crd/jmeter.yaml b/test/container-executor/executor-smoke/crd/jmeter.yaml
new file mode 100644
index 00000000000..34f658eca75
--- /dev/null
+++ b/test/container-executor/executor-smoke/crd/jmeter.yaml
@@ -0,0 +1,26 @@
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: container-executor-jmeter-smoke
+ labels:
+ core-tests: executors
+spec:
+ type: container-executor-jmeter-5.5/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests/jmeter-executor-smoke.jmx
+ workingDir: test/jmeter/executor-tests
+ executionRequest:
+ executePostRunScriptBeforeScraping: true
+ postRunScript: "echo 'post-run script' && cd /data/artifacts && ls -lah"
+ args:
+ - "-n -t jmeter-executor-smoke.jmx -j /data/artifacts/jmeter.log -o /data/artifacts/report -l /data/artifacts/jtl-report.jtl -e"
+ artifactRequest:
+ storageClassName: standard
+ volumeMountPath: /data/artifacts
+ dirs:
+ - ./
diff --git a/test/container-executor/executor-smoke/crd/k6.yaml b/test/container-executor/executor-smoke/crd/k6.yaml
index ef3c4eb4149..94ab99f69e6 100644
--- a/test/container-executor/executor-smoke/crd/k6.yaml
+++ b/test/container-executor/executor-smoke/crd/k6.yaml
@@ -39,3 +39,38 @@ spec:
args: ["run", "k6-smoke-test-without-envs.js"]
jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 64Mi\n cpu: 128m\n"
activeDeadlineSeconds: 180
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: container-executor-k6-smoke-report
+ labels:
+ core-tests: executors
+spec:
+ type: container-executor-k6-0.49.0/test # 0.49.0 or higher is required for report
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube
+ branch: main
+ path: test/k6/executor-tests/k6-smoke-test-without-envs.js
+ workingDir: test/k6/executor-tests
+ executionRequest:
+ args: ["run", "k6-smoke-test-without-envs.js"]
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 64Mi\n cpu: 128m\n"
+ activeDeadlineSeconds: 180
+ variables:
+ K6_WEB_DASHBOARD:
+ name: K6_WEB_DASHBOARD
+ value: "true"
+ type: basic
+ K6_WEB_DASHBOARD_EXPORT:
+ name: K6_WEB_DASHBOARD_EXPORT
+ value: "/data/artifacts/k6-test-report.html"
+ type: basic
+ artifactRequest:
+ storageClassName: standard
+ volumeMountPath: /data/artifacts
+ dirs:
+ - ./
diff --git a/test/container-executor/executor-smoke/crd/maven.yaml b/test/container-executor/executor-smoke/crd/maven.yaml
new file mode 100644
index 00000000000..2eb4ff0ea62
--- /dev/null
+++ b/test/container-executor/executor-smoke/crd/maven.yaml
@@ -0,0 +1,22 @@
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: container-executor-maven-jdk-11
+ labels:
+ core-tests: executors
+spec:
+ type: container-executor-maven-3.9-jdk11/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube
+ branch: main
+ path: contrib/executor/maven/examples/hello-maven
+ workingDir: contrib/executor/maven/examples/hello-maven
+ executionRequest:
+ variables:
+ TESTKUBE_MAVEN:
+ name: TESTKUBE_MAVEN
+ value: "true"
+ type: basic
diff --git a/test/container-executor/executor-smoke/crd/playwright.yaml b/test/container-executor/executor-smoke/crd/playwright.yaml
index 79287346717..d7098cd0d6e 100644
--- a/test/container-executor/executor-smoke/crd/playwright.yaml
+++ b/test/container-executor/executor-smoke/crd/playwright.yaml
@@ -25,6 +25,11 @@ spec:
preRunScript: "npm ci"
args:
- "tests/smoke2.spec.js"
+ variables:
+ PLAYWRIGHT_HTML_REPORT:
+ name: PLAYWRIGHT_HTML_REPORT
+ value: "/data/artifacts/playwright-report"
+ type: basic
---
apiVersion: tests.testkube.io/v3
kind: Test
@@ -50,3 +55,8 @@ spec:
- ./
jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n"
activeDeadlineSeconds: 600
+ variables:
+ PLAYWRIGHT_HTML_REPORT:
+ name: PLAYWRIGHT_HTML_REPORT
+ value: "/data/artifacts/playwright-report"
+ type: basic
diff --git a/test/container-executor/executor-smoke/crd/soapui.yaml b/test/container-executor/executor-smoke/crd/soapui.yaml
new file mode 100644
index 00000000000..3a06247269c
--- /dev/null
+++ b/test/container-executor/executor-smoke/crd/soapui.yaml
@@ -0,0 +1,28 @@
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: container-executor-soapui-smoke
+ labels:
+ core-tests: executors
+spec:
+ type: container-executor-soapui-5.7/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube
+ branch: main
+ path: test/soapui/executor-smoke/soapui-smoke-test.xml
+ executionRequest:
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ variables:
+ COMMAND_LINE:
+ name: COMMAND_LINE
+ value: "-r -f /reports -a -j /data/repo/test/soapui/executor-smoke/soapui-smoke-incorrect-name.xml"
+ type: basic
+ # artifactRequest: # TODO: temporary disabled - not working for some reason
+ # storageClassName: standard
+ # volumeMountPath: /artifacts
+ # dirs:
+ # - ./
diff --git a/test/cypress/executor-tests/crd-workflow/smoke.yaml b/test/cypress/executor-tests/crd-workflow/smoke.yaml
new file mode 100644
index 00000000000..37421260d3d
--- /dev/null
+++ b/test/cypress/executor-tests/crd-workflow/smoke.yaml
@@ -0,0 +1,243 @@
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: cypress-workflow-smoke-13
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/cypress/executor-tests/cypress-13
+ container:
+ resources:
+ requests:
+ cpu: 2
+ memory: 2Gi
+ workingDir: /data/repo/test/cypress/executor-tests/cypress-13
+ steps:
+ - name: Run tests
+ run:
+ image: cypress/included:13.6.4
+ args:
+ - --env
+ - NON_CYPRESS_ENV=NON_CYPRESS_ENV_value
+ - --config
+ - '{"screenshotsFolder":"/data/artifacts/screenshots","videosFolder":"/data/artifacts/videos"}'
+ env:
+ - name: CYPRESS_CUSTOM_ENV
+ value: CYPRESS_CUSTOM_ENV_value
+ steps:
+ - name: Saving artifacts
+ workingDir: /data/artifacts
+ artifacts:
+ paths:
+ - '**/*'
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: cypress-workflow-smoke-13-video-recording-enabled
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/cypress/executor-tests/cypress-13
+ container:
+ resources:
+ requests:
+ cpu: 2
+ memory: 2Gi
+ workingDir: /data/repo/test/cypress/executor-tests/cypress-13
+ steps:
+ - name: Run tests
+ run:
+ image: cypress/included:13.6.4
+ args:
+ - --env
+ - NON_CYPRESS_ENV=NON_CYPRESS_ENV_value
+ - --config
+ - video=true
+ env:
+ - name: CYPRESS_CUSTOM_ENV
+ value: CYPRESS_CUSTOM_ENV_value
+ steps:
+ - name: Saving artifacts
+ workingDir: /data/repo/test/cypress/executor-tests/cypress-13/cypress/videos
+ artifacts:
+ paths:
+ - '**/*'
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: cypress-workflow-smoke-13-negative
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/cypress/executor-tests/cypress-13
+ container:
+ resources:
+ requests:
+ cpu: 2
+ memory: 2Gi
+ workingDir: /data/repo/test/cypress/executor-tests/cypress-13
+ steps:
+ - name: Run tests
+ run:
+ image: cypress/included:13.6.4
+ args:
+ - --env
+ - NON_CYPRESS_ENV=NON_CYPRESS_ENV_value
+ - --config
+ - '{"screenshotsFolder":"/data/artifacts/screenshots","videosFolder":"/data/artifacts/videos"}'
+ negative: true
+ - name: Saving artifacts
+ workingDir: /data/artifacts
+ artifacts:
+ paths:
+ - '**/*'
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: cypress-workflow-smoke-13-preofficial-trait
+ labels:
+ core-tests: workflows
+spec:
+ container:
+ resources:
+ requests:
+ cpu: 2
+ memory: 2Gi
+ workingDir: /data/repo/test/cypress/executor-tests/cypress-13
+ env:
+ - name: CYPRESS_CUSTOM_ENV # currently only possible on this level
+ value: "CYPRESS_CUSTOM_ENV_value"
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/cypress/executor-tests/cypress-13
+ steps:
+ - name: Run from trait
+ workingDir: /data/repo/test/cypress/executor-tests/cypress-13
+ template:
+ name: pre-official/cypress
+ config:
+ version: 13.5.0
+ params: "--env NON_CYPRESS_ENV=NON_CYPRESS_ENV_value --config '{\"screenshotsFolder\":\"/data/artifacts/screenshots\",\"videosFolder\":\"/data/artifacts/videos\"}'"
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: cypress-workflow-smoke-13-preofficial-trait-checkout-on-step
+ labels:
+ core-tests: workflows
+spec:
+ container:
+ resources:
+ requests:
+ cpu: 2
+ memory: 2Gi
+ workingDir: /data/repo/test/cypress/executor-tests/cypress-13
+ env:
+ - name: CYPRESS_CUSTOM_ENV # currently only possible on this level
+ value: "CYPRESS_CUSTOM_ENV_value"
+ steps:
+ - name: Run from trait
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/cypress/executor-tests/cypress-13
+ workingDir: /data/repo/test/cypress/executor-tests/cypress-13
+ template:
+ name: pre-official/cypress
+ config:
+ version: 13.5.0
+ params: "--env NON_CYPRESS_ENV=NON_CYPRESS_ENV_value --config '{\"screenshotsFolder\":\"/data/artifacts/screenshots\",\"videosFolder\":\"/data/artifacts/videos\"}'"
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: cypress-workflow-smoke-13-preofficial-trait-sub-step
+ labels:
+ core-tests: workflows
+spec:
+ container:
+ resources:
+ requests:
+ cpu: 2
+ memory: 2Gi
+ workingDir: /data/repo/test/cypress/executor-tests/cypress-13
+ env:
+ - name: CYPRESS_CUSTOM_ENV # currently only possible on this level
+ value: "CYPRESS_CUSTOM_ENV_value"
+ steps:
+ - name: Run cypress test
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/cypress/executor-tests/cypress-13
+ steps:
+ - name: Run from trait
+ workingDir: /data/repo/test/cypress/executor-tests/cypress-13
+ template:
+ name: pre-official/cypress
+ config:
+ version: 13.5.0
+ params: "--env NON_CYPRESS_ENV=NON_CYPRESS_ENV_value --config '{\"screenshotsFolder\":\"/data/artifacts/screenshots\",\"videosFolder\":\"/data/artifacts/videos\"}'"
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: cypress-workflow-smoke-12.7.0
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/cypress/executor-tests/cypress-12
+ container:
+ resources:
+ requests:
+ cpu: 2
+ memory: 2Gi
+ workingDir: /data/repo/test/cypress/executor-tests/cypress-12
+ steps:
+ - name: Run tests
+ run:
+ image: cypress/included:12.7.0
+ args:
+ - --env
+ - NON_CYPRESS_ENV=NON_CYPRESS_ENV_value
+ - --config
+ - '{"screenshotsFolder":"/data/artifacts/screenshots","videosFolder":"/data/artifacts/videos"}'
+ env:
+ - name: CYPRESS_CUSTOM_ENV
+ value: CYPRESS_CUSTOM_ENV_value
+ steps:
+ - name: Saving artifacts
+ workingDir: /data/artifacts
+ artifacts:
+ paths:
+ - '**/*'
diff --git a/test/examples/kubecon/test-workflows/cypress.yaml b/test/examples/kubecon/test-workflows/cypress.yaml
new file mode 100644
index 00000000000..31d044b4273
--- /dev/null
+++ b/test/examples/kubecon/test-workflows/cypress.yaml
@@ -0,0 +1,37 @@
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: cypress-video
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/cypress/executor-tests/cypress-13
+ container:
+ resources:
+ requests:
+ cpu: 2
+ memory: 2Gi
+ workingDir: /data/repo/test/cypress/executor-tests/cypress-13
+ steps:
+ - name: Run tests
+ run:
+ image: cypress/included:13.6.4
+ args:
+ - --env
+ - NON_CYPRESS_ENV=NON_CYPRESS_ENV_value
+ - --config
+ - video=true
+ env:
+ - name: CYPRESS_CUSTOM_ENV
+ value: CYPRESS_CUSTOM_ENV_value
+ steps:
+ - name: Saving artifacts
+ workingDir: /data/repo/test/cypress/executor-tests/cypress-13/cypress/videos
+ artifacts:
+ paths:
+ - '**/*'
diff --git a/test/examples/kubecon/test-workflows/gradle.yaml b/test/examples/kubecon/test-workflows/gradle.yaml
new file mode 100644
index 00000000000..a24a8230612
--- /dev/null
+++ b/test/examples/kubecon/test-workflows/gradle.yaml
@@ -0,0 +1,30 @@
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: gradle-java-test
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - contrib/executor/gradle/examples/hello-gradle
+ container:
+ resources:
+ requests:
+ cpu: 512m
+ memory: 512Mi
+ workingDir: /data/repo/contrib/executor/gradle/examples/hello-gradle
+ steps:
+ - name: Run tests
+ run:
+ image: gradle:8.5.0-jdk11
+ command:
+ - gradle
+ - --no-daemon
+ - test
+ env:
+ - name: TESTKUBE_GRADLE
+ value: "true"
diff --git a/test/examples/kubecon/test-workflows/jmeter.yaml b/test/examples/kubecon/test-workflows/jmeter.yaml
new file mode 100644
index 00000000000..f69298c2685
--- /dev/null
+++ b/test/examples/kubecon/test-workflows/jmeter.yaml
@@ -0,0 +1,30 @@
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: jmeter-report
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/jmeter/executor-tests/jmeter-executor-smoke.jmx
+ container:
+ resources:
+ requests:
+ cpu: 512m
+ memory: 512Mi
+ workingDir: /data/repo/test/jmeter/executor-tests
+ steps:
+ - name: Run tests
+ shell: jmeter -n -t jmeter-executor-smoke.jmx -j /data/artifacts/jmeter.log -o /data/artifacts/report -l /data/artifacts/jtl-report.jtl -e
+ container:
+ image: justb4/jmeter:5.5
+ steps:
+ - name: Save artifacts
+ workingDir: /data/artifacts
+ artifacts:
+ paths:
+ - '**/*'
diff --git a/test/examples/kubecon/test-workflows/k6.yaml b/test/examples/kubecon/test-workflows/k6.yaml
new file mode 100644
index 00000000000..004c0ff91af
--- /dev/null
+++ b/test/examples/kubecon/test-workflows/k6.yaml
@@ -0,0 +1,44 @@
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: k6-loadtest
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/k6/executor-tests/k6-smoke-test.js
+ container:
+ resources:
+ requests:
+ cpu: 128m
+ memory: 128Mi
+ workingDir: /data/repo/test/k6/executor-tests
+ steps:
+ - name: Run test
+ container:
+ image: grafana/k6:0.49.0
+ steps:
+ - shell: mkdir /data/artifacts
+ - run:
+ args:
+ - run
+ - k6-smoke-test.js
+ - -e
+ - K6_ENV_FROM_PARAM=K6_ENV_FROM_PARAM_value
+ env:
+ - name: K6_SYSTEM_ENV
+ value: K6_SYSTEM_ENV_value
+ - name: K6_WEB_DASHBOARD
+ value: "true"
+ - name: K6_WEB_DASHBOARD_EXPORT
+ value: "/data/artifacts/k6-test-report.html"
+ steps:
+ - name: Saving artifacts
+ workingDir: /data/artifacts
+ artifacts:
+ paths:
+ - '*'
diff --git a/test/examples/kubecon/test-workflows/playwright.yaml b/test/examples/kubecon/test-workflows/playwright.yaml
new file mode 100644
index 00000000000..76af17dadf9
--- /dev/null
+++ b/test/examples/kubecon/test-workflows/playwright.yaml
@@ -0,0 +1,41 @@
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: playwright
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/playwright/executor-tests/playwright-project
+ container:
+ resources:
+ requests:
+ cpu: 2
+ memory: 2Gi
+ workingDir: /data/repo/test/playwright/executor-tests/playwright-project
+ steps:
+ - name: Install dependencies
+ run:
+ image: mcr.microsoft.com/playwright:v1.32.3-focal
+ command:
+ - npm
+ args:
+ - ci
+ - name: Run tests
+ run:
+ image: mcr.microsoft.com/playwright:v1.32.3-focal
+ command:
+ - "npx"
+ args:
+ - "--yes"
+ - "playwright@1.32.3"
+ - "test"
+ - name: Save artifacts
+ workingDir: /data/repo/test/playwright/executor-tests/playwright-project
+ artifacts:
+ paths:
+ - playwright-report/**/*
diff --git a/test/examples/kubecon/test-workflows/postman.yaml b/test/examples/kubecon/test-workflows/postman.yaml
new file mode 100644
index 00000000000..74cabc70ccc
--- /dev/null
+++ b/test/examples/kubecon/test-workflows/postman.yaml
@@ -0,0 +1,28 @@
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: postman-test
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/postman/executor-tests/postman-executor-smoke.postman_collection.json
+ container:
+ resources:
+ requests:
+ cpu: 256m
+ memory: 128Mi
+ workingDir: /data/repo/test/postman/executor-tests
+ steps:
+ - name: Run test
+ run:
+ image: postman/newman:6-alpine
+ args:
+ - run
+ - postman-executor-smoke.postman_collection.json
+ - "--env-var"
+ - "TESTKUBE_POSTMAN_PARAM=TESTKUBE_POSTMAN_PARAM_value"
diff --git a/test/examples/kubecon/tests/cypress.yaml b/test/examples/kubecon/tests/cypress.yaml
new file mode 100644
index 00000000000..43aae645521
--- /dev/null
+++ b/test/examples/kubecon/tests/cypress.yaml
@@ -0,0 +1,51 @@
+apiVersion: executor.testkube.io/v1
+kind: Executor
+metadata:
+ name: cypress-v13-executor
+spec:
+ image: kubeshop/testkube-cypress-executor:cypress13
+ command: ["./node_modules/cypress/bin/cypress"]
+ args: [
+ "run",
+ "--reporter",
+ "junit",
+ "--reporter-options",
+ "mochaFile=,toConsole=false",
+ "--project",
+ "",
+ "--env",
+ ""
+ ]
+ types:
+ - cypress:v13/test
+ features:
+ - artifacts
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: cypress-video
+ labels:
+ core-tests: executors
+spec:
+ type: cypress:v13/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube
+ branch: main
+ path: test/cypress/executor-tests/cypress-13
+ executionRequest:
+ variables:
+ CYPRESS_CUSTOM_ENV:
+ name: CYPRESS_CUSTOM_ENV
+ value: CYPRESS_CUSTOM_ENV_value
+ type: basic
+ args:
+ - --env
+ - NON_CYPRESS_ENV=NON_CYPRESS_ENV_value
+ - --config
+ - video=true
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n"
+ activeDeadlineSeconds: 600
diff --git a/test/examples/kubecon/tests/gradle.yaml b/test/examples/kubecon/tests/gradle.yaml
new file mode 100644
index 00000000000..ca9296c6f49
--- /dev/null
+++ b/test/examples/kubecon/tests/gradle.yaml
@@ -0,0 +1,23 @@
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: gradle-java-test
+ labels:
+ core-tests: executors
+spec:
+ type: gradle/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: contrib/executor/gradle/examples/hello-gradle-jdk18
+ executionRequest:
+ variables:
+ TESTKUBE_GRADLE:
+ name: TESTKUBE_GRADLE
+ value: "true"
+ type: basic
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
diff --git a/test/examples/kubecon/tests/jmeter.yaml b/test/examples/kubecon/tests/jmeter.yaml
new file mode 100644
index 00000000000..ab477196736
--- /dev/null
+++ b/test/examples/kubecon/tests/jmeter.yaml
@@ -0,0 +1,18 @@
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeter-report
+ labels:
+ core-tests: executors
+spec:
+ type: jmeter/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests/jmeter-executor-smoke.jmx
+ executionRequest:
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
diff --git a/test/examples/kubecon/tests/k6.yaml b/test/examples/kubecon/tests/k6.yaml
new file mode 100644
index 00000000000..1312e8d7683
--- /dev/null
+++ b/test/examples/kubecon/tests/k6.yaml
@@ -0,0 +1,18 @@
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: k6-loadtest
+ labels:
+ core-tests: executors
+spec:
+ type: k6/script
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/k6/executor-tests/k6-smoke-test-without-envs.js
+ executionRequest:
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 256m\n"
+ activeDeadlineSeconds: 180
diff --git a/test/examples/kubecon/tests/playwright.yaml b/test/examples/kubecon/tests/playwright.yaml
new file mode 100644
index 00000000000..05893a3936b
--- /dev/null
+++ b/test/examples/kubecon/tests/playwright.yaml
@@ -0,0 +1,45 @@
+apiVersion: executor.testkube.io/v1
+kind: Executor
+metadata:
+ name: container-executor-playwright-v1.32.3-args
+spec:
+ image: mcr.microsoft.com/playwright:v1.32.3-focal
+ command: ["npx", "--yes", "playwright@1.32.3", "test", "--output", "/data/artifacts/playwright-results"]
+ executor_type: container
+ types:
+ - container-executor-playwright-v1.32.3-args/test
+ features:
+ - artifacts
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: playwright
+ labels:
+ core-tests: executors
+spec:
+ type: container-executor-playwright-v1.32.3-args/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube
+ branch: develop
+ path: test/playwright/executor-tests/playwright-project
+ workingDir: test/playwright/executor-tests/playwright-project
+ executionRequest:
+ artifactRequest:
+ storageClassName: standard
+ volumeMountPath: /data/artifacts
+ dirs:
+ - ./
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 2Gi\n cpu: 2\n"
+ activeDeadlineSeconds: 600
+ preRunScript: "npm ci"
+ args:
+ - "tests/smoke2.spec.js"
+ variables:
+ PLAYWRIGHT_HTML_REPORT:
+ name: PLAYWRIGHT_HTML_REPORT
+ value: "/data/artifacts/playwright-report"
+ type: basic
diff --git a/test/examples/kubecon/tests/postman.yaml b/test/examples/kubecon/tests/postman.yaml
new file mode 100644
index 00000000000..230ca3fdbb2
--- /dev/null
+++ b/test/examples/kubecon/tests/postman.yaml
@@ -0,0 +1,20 @@
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: postman-test
+ labels:
+ core-tests: executors
+spec:
+ type: postman/collection
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/postman/executor-tests/postman-executor-smoke.postman_collection.json
+ executionRequest:
+ args:
+ - --env-var
+ - TESTKUBE_POSTMAN_PARAM=TESTKUBE_POSTMAN_PARAM_value
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 256m\n"
diff --git a/test/examples/kubecon/tests/testsuites/k6.yaml b/test/examples/kubecon/tests/testsuites/k6.yaml
new file mode 100644
index 00000000000..38d94c25784
--- /dev/null
+++ b/test/examples/kubecon/tests/testsuites/k6.yaml
@@ -0,0 +1,19 @@
+apiVersion: tests.testkube.io/v3
+kind: TestSuite
+metadata:
+ name: k6-parallel
+spec:
+ description: "k6 parallel testsuite"
+ steps:
+ - stopOnFailure: false
+ execute:
+ - test: k6-loadtest
+ executionRequest:
+ args:
+ - "-vu"
+ - "1"
+ - test: k6-loadtest
+ executionRequest:
+ args:
+ - "-vu"
+ - "2"
diff --git a/test/executors/container-executor-gradle.yaml b/test/executors/container-executor-gradle.yaml
new file mode 100644
index 00000000000..91249ff6251
--- /dev/null
+++ b/test/executors/container-executor-gradle.yaml
@@ -0,0 +1,9 @@
+apiVersion: executor.testkube.io/v1
+kind: Executor
+metadata:
+ name: container-executor-gradle-8.5-jdk11
+spec:
+ image: gradle:8.5.0-jdk11
+ executor_type: container
+ types:
+ - container-executor-gradle-8.5-jdk11/test
diff --git a/test/executors/container-executor-jmeter.yaml b/test/executors/container-executor-jmeter.yaml
new file mode 100644
index 00000000000..92aadf31594
--- /dev/null
+++ b/test/executors/container-executor-jmeter.yaml
@@ -0,0 +1,13 @@
+apiVersion: executor.testkube.io/v1
+kind: Executor
+metadata:
+ name: container-executor-jmeter-5.5
+spec:
+ image: justb4/jmeter:5.5
+ command:
+ - "jmeter"
+ executor_type: container
+ types:
+ - container-executor-jmeter-5.5/test
+ features:
+ - artifacts
diff --git a/test/executors/container-executor-k6.yaml b/test/executors/container-executor-k6.yaml
index 88d1b6969b3..8ba41a02471 100644
--- a/test/executors/container-executor-k6.yaml
+++ b/test/executors/container-executor-k6.yaml
@@ -6,4 +6,16 @@ spec:
image: grafana/k6:0.43.1
executor_type: container
types:
- - container-executor-k6-0.43.1/test
\ No newline at end of file
+ - container-executor-k6-0.43.1/test
+---
+apiVersion: executor.testkube.io/v1
+kind: Executor
+metadata:
+ name: container-executor-k6-0.49.0
+spec:
+ image: grafana/k6:0.49.0
+ executor_type: container
+ types:
+ - container-executor-k6-0.49.0/test
+ features:
+ - artifacts
diff --git a/test/executors/container-executor-maven.yaml b/test/executors/container-executor-maven.yaml
new file mode 100644
index 00000000000..1b3f246749a
--- /dev/null
+++ b/test/executors/container-executor-maven.yaml
@@ -0,0 +1,10 @@
+apiVersion: executor.testkube.io/v1
+kind: Executor
+metadata:
+ name: container-executor-maven-3.9-jdk11
+spec:
+ image: maven:3.9.6-eclipse-temurin-11-focal
+ executor_type: container
+ types:
+ - container-executor-maven-3.9-jdk11/test
+ command: ["mvn", "test"]
diff --git a/test/executors/container-executor-playwright.yaml b/test/executors/container-executor-playwright.yaml
index 481b9baf340..a6b1c6e7ec7 100644
--- a/test/executors/container-executor-playwright.yaml
+++ b/test/executors/container-executor-playwright.yaml
@@ -4,7 +4,7 @@ metadata:
name: container-executor-playwright-v1.32.3-args
spec:
image: mcr.microsoft.com/playwright:v1.32.3-focal
- command: ["npx", "--yes", "playwright@1.32.3", "test", "--output", "/data/artifacts"]
+ command: ["npx", "--yes", "playwright@1.32.3", "test", "--output", "/data/artifacts/playwright-results"]
executor_type: container
types:
- container-executor-playwright-v1.32.3-args/test
@@ -19,7 +19,7 @@ spec:
image: mcr.microsoft.com/playwright:v1.32.3-focal
command: ["/bin/sh", "-c"]
args:
- - "npm ci && CI=1 npx --yes playwright@1.32.3 test --output /data/artifacts"
+ - "npm ci && CI=1 npx --yes playwright@1.32.3 test --output /data/artifacts/playwright-results"
executor_type: container
types:
- container-executor-playwright-v1.32.3/test
diff --git a/test/executors/container-executor-postman.yaml b/test/executors/container-executor-postman.yaml
index 8e40636595c..10591222791 100644
--- a/test/executors/container-executor-postman.yaml
+++ b/test/executors/container-executor-postman.yaml
@@ -7,3 +7,4 @@ spec:
executor_type: container
types:
- container-executor-postman-newman-6-alpine/test
+ command: ["newman"]
diff --git a/test/executors/container-executor-soapui.yaml b/test/executors/container-executor-soapui.yaml
new file mode 100644
index 00000000000..81fbf5f9feb
--- /dev/null
+++ b/test/executors/container-executor-soapui.yaml
@@ -0,0 +1,11 @@
+apiVersion: executor.testkube.io/v1
+kind: Executor
+metadata:
+ name: container-executor-soapui-5.7
+spec:
+ image: smartbear/soapuios-testrunner:5.7.2
+ executor_type: container
+ types:
+ - container-executor-soapui-5.7/test
+ features:
+ - artifacts
diff --git a/test/executors/gradle.yaml b/test/executors/gradle.yaml
index 9c563477f6f..b8694a6aafa 100644
--- a/test/executors/gradle.yaml
+++ b/test/executors/gradle.yaml
@@ -7,7 +7,14 @@ spec:
types:
- gradle:jdk18/project
- gradle:jdk18/test
- - gradle:jdk18/integrationTest
+ - gradle:jdk18/integrationTest
+ command: ["gradle"]
+ args: [
+ "--no-daemon",
+ "",
+ "-p",
+ ""
+ ]
---
apiVersion: executor.testkube.io/v1
kind: Executor
@@ -18,7 +25,14 @@ spec:
types:
- gradle:jdk17/project
- gradle:jdk17/test
- - gradle:jdk17/integrationTest
+ - gradle:jdk17/integrationTest
+ command: ["gradle"]
+ args: [
+ "--no-daemon",
+ "",
+ "-p",
+ ""
+ ]
---
apiVersion: executor.testkube.io/v1
kind: Executor
@@ -29,7 +43,14 @@ spec:
types:
- gradle:jdk11/project
- gradle:jdk11/test
- - gradle:jdk11/integrationTest
+ - gradle:jdk11/integrationTest
+ command: ["gradle"]
+ args: [
+ "--no-daemon",
+ "",
+ "-p",
+ ""
+ ]
---
apiVersion: executor.testkube.io/v1
kind: Executor
@@ -40,4 +61,11 @@ spec:
types:
- gradle:jdk8/project
- gradle:jdk8/test
- - gradle:jdk8/integrationTest
\ No newline at end of file
+ - gradle:jdk8/integrationTest
+ command: ["gradle"]
+ args: [
+ "--no-daemon",
+ "",
+ "-p",
+ ""
+ ]
diff --git a/test/executors/maven.yaml b/test/executors/maven.yaml
index 1f106339b19..c0ae589a9f0 100644
--- a/test/executors/maven.yaml
+++ b/test/executors/maven.yaml
@@ -7,7 +7,15 @@ spec:
types:
- maven:jdk18/project
- maven:jdk18/test
- - maven:jdk18/integration-test
+ - maven:jdk18/integration-test
+ command: ["mvn"]
+ args: [
+ "--settings",
+ "",
+ "",
+ "-Duser.home",
+ ""
+ ]
---
apiVersion: executor.testkube.io/v1
kind: Executor
@@ -18,7 +26,15 @@ spec:
types:
- maven:jdk11/project
- maven:jdk11/test
- - maven:jdk11/integration-test
+ - maven:jdk11/integration-test
+ command: ["mvn"]
+ args: [
+ "--settings",
+ "",
+ "",
+ "-Duser.home",
+ ""
+ ]
---
apiVersion: executor.testkube.io/v1
kind: Executor
@@ -29,4 +45,12 @@ spec:
types:
- maven:jdk8/project
- maven:jdk8/test
- - maven:jdk8/integration-test
\ No newline at end of file
+ - maven:jdk8/integration-test
+ command: ["mvn"]
+ args: [
+ "--settings",
+ "",
+ "",
+ "-Duser.home",
+ ""
+ ]
diff --git a/test/gradle/executor-smoke/crd-workflow/smoke.yaml b/test/gradle/executor-smoke/crd-workflow/smoke.yaml
new file mode 100644
index 00000000000..51f9ae2f481
--- /dev/null
+++ b/test/gradle/executor-smoke/crd-workflow/smoke.yaml
@@ -0,0 +1,57 @@
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: gradle-workflow-smoke-jdk11
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - contrib/executor/gradle/examples/hello-gradle
+ container:
+ resources:
+ requests:
+ cpu: 512m
+ memory: 512Mi
+ workingDir: /data/repo/contrib/executor/gradle/examples/hello-gradle
+ steps:
+ - name: Run tests
+ run:
+ image: gradle:8.5.0-jdk11
+ command:
+ - gradle
+ - --no-daemon
+ - test
+ env:
+ - name: TESTKUBE_GRADLE
+ value: "true"
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: gradle-workflow-smoke-jdk11-default-command # TODO: recheck if it's fixed - the step passes without being executed
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - contrib/executor/gradle/examples/hello-gradle
+ container:
+ resources:
+ requests:
+ cpu: 512m
+ memory: 512Mi
+ workingDir: /data/repo/contrib/executor/gradle/examples/hello-gradle
+ steps:
+ - name: Run tests
+ run:
+ image: gradle:8.5.0-jdk11
+ env:
+ - name: TESTKUBE_GRADLE
+ value: "true"
diff --git a/test/gradle/executor-smoke/crd/crd.yaml b/test/gradle/executor-smoke/crd/crd.yaml
index dd29941beff..185b85428d3 100644
--- a/test/gradle/executor-smoke/crd/crd.yaml
+++ b/test/gradle/executor-smoke/crd/crd.yaml
@@ -1,5 +1,27 @@
-# https://github.com/kubeshop/testkube-executor-gradle/tree/main/examples
-
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: gradle-executor-smoke
+ labels:
+ core-tests: executors
+spec:
+ type: gradle/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: contrib/executor/gradle/examples/hello-gradle-jdk18
+ executionRequest:
+ variables:
+ TESTKUBE_GRADLE:
+ name: TESTKUBE_GRADLE
+ value: "true"
+ type: basic
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+---
apiVersion: tests.testkube.io/v3
kind: Test
metadata:
@@ -12,9 +34,9 @@ spec:
type: git
repository:
type: git
- uri: https://github.com/kubeshop/testkube-executor-gradle.git
+ uri: https://github.com/kubeshop/testkube.git
branch: main
- path: examples/hello-gradle-jdk18
+ path: contrib/executor/gradle/examples/hello-gradle-jdk18
executionRequest:
variables:
TESTKUBE_GRADLE:
@@ -36,9 +58,9 @@ spec:
type: git
repository:
type: git
- uri: https://github.com/kubeshop/testkube-executor-gradle.git
+ uri: https://github.com/kubeshop/testkube.git
branch: main
- path: examples/hello-gradle
+ path: contrib/executor/gradle/examples/hello-gradle
executionRequest:
variables:
TESTKUBE_GRADLE:
@@ -60,9 +82,9 @@ spec:
type: git
repository:
type: git
- uri: https://github.com/kubeshop/testkube-executor-gradle.git
+ uri: https://github.com/kubeshop/testkube.git
branch: main
- path: examples/hello-gradle
+ path: contrib/executor/gradle/examples/hello-gradle
executionRequest:
variables:
TESTKUBE_GRADLE:
@@ -84,9 +106,9 @@ spec:
type: git
repository:
type: git
- uri: https://github.com/kubeshop/testkube-executor-gradle.git
+ uri: https://github.com/kubeshop/testkube.git
branch: main
- path: examples/hello-gradle
+ path: contrib/executor/gradle/examples/hello-gradle
executionRequest:
variables:
TESTKUBE_GRADLE:
@@ -108,9 +130,9 @@ spec:
type: git
repository:
type: git
- uri: https://github.com/kubeshop/testkube-executor-gradle.git
+ uri: https://github.com/kubeshop/testkube.git
branch: main
- path: examples/hello-gradle-jdk18
+ path: contrib/executor/gradle/examples/hello-gradle-jdk18
executionRequest:
negativeTest: true
jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
diff --git a/test/jmeter/executor-tests/crd-workflow/smoke.yaml b/test/jmeter/executor-tests/crd-workflow/smoke.yaml
new file mode 100644
index 00000000000..5aba34a1314
--- /dev/null
+++ b/test/jmeter/executor-tests/crd-workflow/smoke.yaml
@@ -0,0 +1,60 @@
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: jmeter-workflow-smoke
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/jmeter/executor-tests/jmeter-executor-smoke.jmx
+ container:
+ resources:
+ requests:
+ cpu: 512m
+ memory: 512Mi
+ workingDir: /data/repo/test/jmeter/executor-tests
+ steps:
+ - name: Run tests
+ run:
+ image: justb4/jmeter:5.5
+ command:
+ - jmeter
+ args:
+ - -n
+ - -t
+ - jmeter-executor-smoke.jmx
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: jmeter-workflow-smoke-shell-artifacts
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/jmeter/executor-tests/jmeter-executor-smoke.jmx
+ container:
+ resources:
+ requests:
+ cpu: 512m
+ memory: 512Mi
+ workingDir: /data/repo/test/jmeter/executor-tests
+ steps:
+ - name: Run tests
+ shell: jmeter -n -t jmeter-executor-smoke.jmx -j /data/artifacts/jmeter.log -o /data/artifacts/report -l /data/artifacts/jtl-report.jtl -e
+ container:
+ image: justb4/jmeter:5.5
+ steps:
+ - name: Save artifacts
+ workingDir: /data/artifacts
+ artifacts:
+ paths:
+ - '**/*'
diff --git a/test/jmeter/executor-tests/crd/other.yaml b/test/jmeter/executor-tests/crd/other.yaml
index 845797cdf83..5059d27893b 100644
--- a/test/jmeter/executor-tests/crd/other.yaml
+++ b/test/jmeter/executor-tests/crd/other.yaml
@@ -21,7 +21,7 @@ spec:
apiVersion: tests.testkube.io/v3
kind: Test
metadata:
- name: jmeterd-executor-smoke-incorrect-url-assertion-negative
+ name: jmeterd-executor-smoke-incorrect-url-assertion
labels:
core-tests: executors
spec:
@@ -34,14 +34,13 @@ spec:
branch: main
path: test/jmeter/executor-tests/jmeter-executor-smoke-incorrect-url.jmx
executionRequest:
- negativeTest: true
jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
activeDeadlineSeconds: 180
---
apiVersion: tests.testkube.io/v3
kind: Test
metadata:
- name: jmeterd-executor-smoke-incorrect-url-assertion-slaves-negative
+ name: jmeterd-executor-smoke-incorrect-url-assertion-slaves
labels:
core-tests: executors
spec:
@@ -54,7 +53,6 @@ spec:
branch: main
path: test/jmeter/executor-tests/jmeter-executor-smoke-incorrect-url.jmx
executionRequest:
- negativeTest: true
variables:
SLAVES_COUNT:
name: SLAVES_COUNT
@@ -74,7 +72,7 @@ spec:
apiVersion: tests.testkube.io/v3
kind: Test
metadata:
- name: jmeterd-executor-smoke-correct-url-failed-assertion-negative
+ name: jmeterd-executor-smoke-correct-url-failed-assertion
labels:
core-tests: executors
spec:
@@ -87,14 +85,13 @@ spec:
branch: main
path: test/jmeter/executor-tests/jmeter-executor-smoke-correct-url-failed-assertion.jmx
executionRequest:
- negativeTest: true
jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
activeDeadlineSeconds: 180
---
apiVersion: tests.testkube.io/v3
kind: Test
metadata:
- name: jmeterd-executor-smoke-failed-assertion-slaves-negative
+ name: jmeterd-executor-smoke-failed-assertion-slaves
labels:
core-tests: executors
spec:
@@ -107,7 +104,6 @@ spec:
branch: main
path: test/jmeter/executor-tests/jmeter-executor-smoke-correct-url-failed-assertion.jmx
executionRequest:
- negativeTest: true
variables:
SLAVES_COUNT:
name: SLAVES_COUNT
@@ -151,3 +147,62 @@ spec:
limits:
cpu: 500m
memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-slave-0 # standalone mode
+ labels:
+ core-tests: executors
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests/jmeter-executor-smoke.jmx
+ executionRequest:
+ variables:
+ SLAVES_COUNT:
+ name: SLAVES_COUNT
+ value: "0"
+ type: basic
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-slave-not-set # standalone mode
+ labels:
+ core-tests: executors
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests/jmeter-executor-smoke.jmx
+ executionRequest:
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
diff --git a/test/jmeter/executor-tests/crd/smoke.yaml b/test/jmeter/executor-tests/crd/smoke.yaml
index 67b174a63f4..f656063dd6f 100644
--- a/test/jmeter/executor-tests/crd/smoke.yaml
+++ b/test/jmeter/executor-tests/crd/smoke.yaml
@@ -114,7 +114,7 @@ spec:
apiVersion: tests.testkube.io/v3
kind: Test
metadata:
- name: jmeterd-executor-smoke-slave-1
+ name: jmeterd-executor-smoke-slave-1 # standalone mode
labels:
core-tests: executors
spec:
@@ -174,3 +174,63 @@ spec:
limits:
cpu: 500m
memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-env-and-property-values
+ labels:
+ core-tests: executors
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests/jmeter-executor-smoke-env-and-property.jmx
+ executionRequest:
+ variables:
+ URL_ENV:
+ name: URL_ENV
+ value: "testkube.io"
+ type: basic
+ args:
+ - "-JURL_PROPERTY=testkube.io"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-env-and-property-values-sl-0
+ labels:
+ core-tests: executors
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests/jmeter-executor-smoke-env-and-property.jmx
+ executionRequest:
+ variables:
+ SLAVES_COUNT:
+ name: SLAVES_COUNT
+ value: "0"
+ type: basic
+ URL_ENV:
+ name: URL_ENV
+ value: "testkube.io"
+ type: basic
+ ANOTHER_CUSTOM_ENV:
+ name: ANOTHER_CUSTOM_ENV
+ value: "SOME_CUSTOM_ENV"
+ type: basic
+ args:
+ - "-JURL_PROPERTY=testkube.io"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
diff --git a/test/jmeter/executor-tests/crd/special-cases.yaml b/test/jmeter/executor-tests/crd/special-cases.yaml
new file mode 100644
index 00000000000..5204ea3303a
--- /dev/null
+++ b/test/jmeter/executor-tests/crd/special-cases.yaml
@@ -0,0 +1,581 @@
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-custom-envs-replication # TODO: validation on the test side
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests/jmeter-executor-smoke.jmx
+ executionRequest:
+ variables:
+ SLAVES_COUNT:
+ name: SLAVES_COUNT
+ value: "2"
+ type: basic
+ CUSTOM_ENV_VARIABLE:
+ name: CUSTOM_ENV_VARIABLE
+ value: CUSTOM_ENV_VARIABLE_value
+ type: basic
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-env-value-in-args
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests
+ executionRequest:
+ variables:
+ JMETER_SCRIPT:
+ name: JMETER_SCRIPT
+ value: jmeter-executor-smoke.jmx
+ type: basic
+ args:
+ - "${JMETER_SCRIPT}"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-directory-1
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests
+ executionRequest:
+ args:
+ - "jmeter-executor-smoke.jmx"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-directory-2
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests
+ executionRequest:
+ args:
+ - "jmeter-executor-smoke-2.jmx"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-slaves-sharedbetweenpods # can be run only at cluster with storageClassName (NFS volume), not included in TestSuite
+ labels:
+ core-tests: executors
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests/jmeter-executor-smoke.jmx
+ executionRequest:
+ executePostRunScriptBeforeScraping: true
+ postRunScript: "echo \"postrun script\" && echo \"artifact file - contents\" > /data/output/artifact-`uuidgen`.txt"
+ artifactRequest:
+ storageClassName: standard-rwx
+ masks:
+ - .*\.txt
+ sharedBetweenPods: true
+ variables:
+ SLAVES_COUNT:
+ name: SLAVES_COUNT
+ value: "2"
+ type: basic
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ # activeDeadlineSeconds: 180 TODO: increase - too low to create volume
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-directory-t-o
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: develop
+ path: test/jmeter/executor-tests
+ executionRequest:
+ args:
+ - "-t"
+ - "/data/repo/test/jmeter/executor-tests/jmeter-executor-smoke-2.jmx"
+ - "-o"
+ - "/data/output/custom-report-directory"
+ - "-l"
+ - "/data/output/custom-report.jtl"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-directory-t-o-slaves-2
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: develop
+ path: test/jmeter/executor-tests
+ executionRequest:
+ args:
+ - "-t"
+ - "/data/repo/test/jmeter/executor-tests/jmeter-executor-smoke-2.jmx"
+ - "-o"
+ - "/data/output/custom-report-directory"
+ - "-l"
+ - "/data/output/custom-report.jtl"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ variables:
+ SLAVES_COUNT:
+ name: SLAVES_COUNT
+ value: "2"
+ type: basic
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-directory-wdir-t-o-slaves-2
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: develop
+ path: test/jmeter/executor-tests
+ workingDir: test/jmeter/executor-tests
+ executionRequest:
+ args:
+ - "-t"
+ - "/data/repo/test/jmeter/executor-tests/jmeter-executor-smoke-2.jmx"
+ - "-o"
+ - "/data/output/custom-report-directory"
+ - "-l"
+ - "/data/output/custom-report.jtl"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ variables:
+ SLAVES_COUNT:
+ name: SLAVES_COUNT
+ value: "2"
+ type: basic
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-incorrect-file-path-negative
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests
+ executionRequest:
+ negativeTest: true
+ args:
+ - "-t"
+ - "/data/repo/test/jmeter/executor-tests/some-incorrect-file-name.jmx"
+ - "-o"
+ - "/data/output/custom-report.jtl"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-duplicated-args
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests
+ executionRequest:
+ args:
+ - "-t"
+ - "/data/repo/test/jmeter/executor-tests/jmeter-executor-smoke.jmx"
+ - "-t"
+ - "/data/repo/test/jmeter/executor-tests/jmeter-executor-smoke-2.jmx"
+ - "-o"
+ - "/data/output/custom-report.jtl"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-duplicated-args-2
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests
+ executionRequest:
+ args:
+ - "-t"
+ - "/data/repo/test/jmeter/executor-tests/jmeter-executor-smoke.jmx"
+ - "-o"
+ - "/data/output/custom-report.jtl"
+ - "-e"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-args-override-l-e
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests
+ executionRequest:
+ argsMode: override
+ args:
+ - "-n"
+ - "-t"
+ - "/data/repo/test/jmeter/executor-tests/jmeter-executor-smoke.jmx"
+ - "-o"
+ - "/data/output/custom-report.jtl"
+ - "-l"
+ - "/data/output/report.jtl"
+ - "-e"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-incorrect-url-2
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: develop
+ path: test/jmeter/executor-tests/jmeter-executor-smoke-incorrect-url-2.jmx
+ executionRequest:
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeter-executor-smoke-incorrect-url-2
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeter/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: develop
+ path: test/jmeter/executor-tests/jmeter-executor-smoke-incorrect-url-2.jmx
+ executionRequest:
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-args-override
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests
+ executionRequest:
+ argsMode: override
+ args:
+ - "-n"
+ - "-t"
+ - "/data/repo/test/jmeter/executor-tests/jmeter-executor-smoke.jmx"
+ - "-o"
+ - "/data/output/custom-report.jtl"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-args-override-workingdir
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests
+ workingDir: test/jmeter/executor-tests
+ executionRequest:
+ argsMode: override
+ args:
+ - "-n"
+ - "-t"
+ - "jmeter-executor-smoke.jmx"
+ - "-o"
+ - "/data/output/custom-report.jtl"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: jmeterd-executor-smoke-runner-artifacts-dir
+ labels:
+ core-tests: special-cases-jmeter
+spec:
+ type: jmeterd/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/jmeter/executor-tests
+ executionRequest:
+ argsMode: override
+ args:
+ - "-n"
+ - "-t"
+ - "/data/repo/test/jmeter/executor-tests/jmeter-executor-smoke.jmx"
+ - "-o"
+ - "/data/artifacts-custom/custom-report.jtl"
+ - "-l"
+ - "/data/artifacts-custom/report.jtl"
+ - "-e"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 512Mi\n cpu: 512m\n"
+ activeDeadlineSeconds: 180
+ slavePodRequest:
+ resources:
+ requests:
+ cpu: 400m
+ memory: 512Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+ variables:
+ RUNNER_ARTIFACTS_DIR:
+ name: RUNNER_ARTIFACTS_DIR
+ value: "/data/artifacts-custom"
+ type: basic
diff --git a/test/jmeter/executor-tests/jmeter-executor-smoke-2.jmx b/test/jmeter/executor-tests/jmeter-executor-smoke-2.jmx
new file mode 100644
index 00000000000..6dc469a6d51
--- /dev/null
+++ b/test/jmeter/executor-tests/jmeter-executor-smoke-2.jmx
@@ -0,0 +1,94 @@
+
+
+
+
+
+ false
+ false
+
+
+
+
+
+
+
+ continue
+
+ false
+ 1
+
+ 1
+ 1
+ 1668426657000
+ 1668426657000
+ false
+
+
+
+
+
+
+
+
+ testkube.kubeshop.io
+
+
+
+
+
+
+ GET
+ true
+ false
+ true
+ false
+ false
+
+
+
+
+
+ 200
+
+ Assertion.response_code
+ false
+ 8
+
+
+
+ false
+
+ saveConfig
+
+
+ true
+ true
+ true
+
+ true
+ true
+ true
+ true
+ false
+ true
+ true
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ 0
+ true
+ true
+
+
+
+
+
+
+
+
+
diff --git a/test/jmeter/executor-tests/jmeter-executor-smoke-incorrect-url-2.jmx b/test/jmeter/executor-tests/jmeter-executor-smoke-incorrect-url-2.jmx
new file mode 100644
index 00000000000..13d2e00ec97
--- /dev/null
+++ b/test/jmeter/executor-tests/jmeter-executor-smoke-incorrect-url-2.jmx
@@ -0,0 +1,94 @@
+
+
+
+
+
+ false
+ false
+
+
+
+
+
+
+
+ continue
+
+ false
+ 1
+
+ 1
+ 1
+ 1668426657000
+ 1668426657000
+ false
+
+
+
+
+
+
+
+
+ testkube.kubeshop.io
+
+
+
+
+
+ /some-incorrect-url
+ GET
+ true
+ false
+ true
+ false
+ false
+
+
+
+
+
+ 200
+
+ Assertion.response_code
+ false
+ 8
+
+
+
+ false
+
+ saveConfig
+
+
+ true
+ true
+ true
+
+ true
+ true
+ true
+ true
+ false
+ true
+ true
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ 0
+ true
+ true
+
+
+
+
+
+
+
+
+
diff --git a/test/k6/executor-tests/crd-workflow/smoke.yaml b/test/k6/executor-tests/crd-workflow/smoke.yaml
new file mode 100644
index 00000000000..a6379eb87c4
--- /dev/null
+++ b/test/k6/executor-tests/crd-workflow/smoke.yaml
@@ -0,0 +1,139 @@
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: k6-workflow-smoke
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/k6/executor-tests/k6-smoke-test.js
+ container:
+ resources:
+ requests:
+ cpu: 128m
+ memory: 128Mi
+ workingDir: /data/repo/test/k6/executor-tests
+ steps:
+ - name: Run test
+ run:
+ image: grafana/k6:0.43.1
+ args:
+ - run
+ - k6-smoke-test.js
+ - -e
+ - K6_ENV_FROM_PARAM=K6_ENV_FROM_PARAM_value
+ env:
+ - name: K6_SYSTEM_ENV
+ value: K6_SYSTEM_ENV_value
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: k6-workflow-smoke-preofficial-trait
+ labels:
+ core-tests: workflows
+spec:
+ container:
+ resources:
+ requests:
+ cpu: 128m
+ memory: 128Mi
+ workingDir: /data/repo/test/k6/executor-tests
+ env:
+ - name: K6_SYSTEM_ENV # currently only possible on this level
+ value: K6_SYSTEM_ENV_value
+ steps:
+ - name: Checkout
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/k6/executor-tests/k6-smoke-test.js
+ - name: Run from trait
+ workingDir: /data/repo/test/k6/executor-tests
+ template:
+ name: pre-official/k6
+ config:
+ version: 0.48.0
+ params: "k6-smoke-test.js -e K6_ENV_FROM_PARAM=K6_ENV_FROM_PARAM_value"
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: k6-workflow-smoke-preofficial-trait-without-checkout-step
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/k6/executor-tests/k6-smoke-test.js
+ container:
+ resources:
+ requests:
+ cpu: 128m
+ memory: 128Mi
+ workingDir: /data/repo/test/k6/executor-tests
+ env:
+ - name: K6_SYSTEM_ENV # currently only possible on this level
+ value: K6_SYSTEM_ENV_value
+ steps:
+ - name: Run from trait
+ workingDir: /data/repo/test/k6/executor-tests
+ template:
+ name: pre-official/k6
+ config:
+ version: 0.48.0
+ params: "k6-smoke-test.js -e K6_ENV_FROM_PARAM=K6_ENV_FROM_PARAM_value"
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: k6-workflow-smoke-artifacts
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/k6/executor-tests/k6-smoke-test.js
+ container:
+ resources:
+ requests:
+ cpu: 128m
+ memory: 128Mi
+ workingDir: /data/repo/test/k6/executor-tests
+ steps:
+ - name: Run test
+ container:
+ image: grafana/k6:0.49.0
+ steps:
+ - shell: mkdir /data/artifacts
+ - run:
+ args:
+ - run
+ - k6-smoke-test.js
+ - -e
+ - K6_ENV_FROM_PARAM=K6_ENV_FROM_PARAM_value
+ env:
+ - name: K6_SYSTEM_ENV
+ value: K6_SYSTEM_ENV_value
+ - name: K6_WEB_DASHBOARD
+ value: "true"
+ - name: K6_WEB_DASHBOARD_EXPORT
+ value: "/data/artifacts/k6-test-report.html"
+ steps:
+ - name: Saving artifacts
+ workingDir: /data/artifacts
+ artifacts:
+ paths:
+ - '*'
diff --git a/test/maven/executor-smoke/crd-workflow/smoke.yaml b/test/maven/executor-smoke/crd-workflow/smoke.yaml
new file mode 100644
index 00000000000..68defca22be
--- /dev/null
+++ b/test/maven/executor-smoke/crd-workflow/smoke.yaml
@@ -0,0 +1,29 @@
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: maven-workflow-smoke-jdk11
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - contrib/executor/maven/examples/hello-maven
+ container:
+ resources:
+ requests:
+ cpu: 256m
+ memory: 256Mi
+ workingDir: /data/repo/contrib/executor/maven/examples/hello-maven
+ steps:
+ - name: Run tests
+ run:
+ image: maven:3.9.6-eclipse-temurin-11-focal
+ command:
+ - "mvn"
+ - "test"
+ env:
+ - name: TESTKUBE_MAVEN
+ value: "true"
diff --git a/test/maven/executor-smoke/crd/crd.yaml b/test/maven/executor-smoke/crd/crd.yaml
index 69da5c03cb2..37ca9ae400c 100644
--- a/test/maven/executor-smoke/crd/crd.yaml
+++ b/test/maven/executor-smoke/crd/crd.yaml
@@ -1,5 +1,27 @@
-# https://github.com/kubeshop/testkube-executor-maven/tree/main/examples
-
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: maven-executor-smoke
+ labels:
+ core-tests: executors
+spec:
+ type: maven/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: contrib/executor/maven/examples/hello-maven-jdk18
+ executionRequest:
+ variables:
+ TESTKUBE_MAVEN:
+ name: TESTKUBE_MAVEN
+ value: "true"
+ type: basic
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 256Mi\n cpu: 256m\n"
+ activeDeadlineSeconds: 180
+---
apiVersion: tests.testkube.io/v3
kind: Test
metadata:
diff --git a/test/playwright/executor-tests/crd-workflow/smoke.yaml b/test/playwright/executor-tests/crd-workflow/smoke.yaml
new file mode 100644
index 00000000000..d1cdb686b83
--- /dev/null
+++ b/test/playwright/executor-tests/crd-workflow/smoke.yaml
@@ -0,0 +1,200 @@
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: playwright-workflow-smoke-v1.32.3
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/playwright/executor-tests/playwright-project
+ container:
+ resources:
+ requests:
+ cpu: 2
+ memory: 2Gi
+ workingDir: /data/repo/test/playwright/executor-tests/playwright-project
+ steps:
+ - name: Install dependencies
+ run:
+ image: mcr.microsoft.com/playwright:v1.32.3-focal
+ command:
+ - npm
+ args:
+ - ci
+ - name: Run tests
+ run:
+ image: mcr.microsoft.com/playwright:v1.32.3-focal
+ command:
+ - "npx"
+ args:
+ - "--yes"
+ - "playwright@1.32.3"
+ - "test"
+ - name: Save artifacts
+ workingDir: /data/repo/test/playwright/executor-tests/playwright-project
+ artifacts:
+ paths:
+ - playwright-report/**/*
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: playwright-workflow-smoke-v1.32.3-custom-report-dir
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/playwright/executor-tests/playwright-project
+ container:
+ resources:
+ requests:
+ cpu: 2
+ memory: 2Gi
+ workingDir: /data/repo/test/playwright/executor-tests/playwright-project
+ steps:
+ - name: Install dependencies
+ run:
+ image: mcr.microsoft.com/playwright:v1.32.3-focal
+ command:
+ - npm
+ args:
+ - ci
+ - name: Run tests
+ run:
+ image: mcr.microsoft.com/playwright:v1.32.3-focal
+ command:
+ - "npx"
+ args:
+ - "--yes"
+ - "playwright@1.32.3"
+ - "test"
+ - "--output"
+ - "/data/artifacts"
+ env:
+ - name: PLAYWRIGHT_HTML_REPORT
+ value: /data/artifacts/playwright-report
+ steps:
+ - name: Save artifacts
+ workingDir: /data/artifacts
+ artifacts:
+ paths:
+ - '**/*'
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: playwright-workflow-smoke-v1.32.3-shell
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/playwright/executor-tests/playwright-project
+ container:
+ resources:
+ requests:
+ cpu: 2
+ memory: 2Gi
+ workingDir: /data/repo/test/playwright/executor-tests/playwright-project
+ steps:
+ - name: Install dependencies
+ run:
+ image: mcr.microsoft.com/playwright:v1.32.3-focal
+ command:
+ - npm
+ args:
+ - ci
+ - name: Run tests
+ shell: "npx --yes playwright@1.32.3 test --output /data/artifacts && cd /data/artifacts && ls -lah"
+ container:
+ image: mcr.microsoft.com/playwright:v1.32.3-focal
+ env:
+ - name: PLAYWRIGHT_HTML_REPORT
+ value: /data/artifacts/playwright-report
+ steps:
+ - name: Save artifacts
+ workingDir: /data/artifacts
+ artifacts:
+ paths:
+ - '**/*'
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: playwright-workflow-smoke-artifacts-double-asterisk
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/playwright/executor-tests/playwright-project
+ container:
+ resources:
+ requests:
+ cpu: 2
+ memory: 2Gi
+ workingDir: /data/repo/test/playwright/executor-tests/playwright-project
+ steps:
+ - name: Install dependencies
+ run:
+ image: mcr.microsoft.com/playwright:v1.32.3-focal
+ command:
+ - npm
+ args:
+ - ci
+ - name: Run tests
+ run:
+ image: mcr.microsoft.com/playwright:v1.32.3-focal
+ command:
+ - "npx"
+ args:
+ - "--yes"
+ - "playwright@1.32.3"
+ - "test"
+ - name: Save artifacts
+ artifacts:
+ paths:
+ - /data/repo/**/playwright-report/**/*
+---
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: playwright-workflow-smoke-official-trait
+ labels:
+ core-tests: workflows
+spec:
+ container:
+ resources:
+ requests:
+ cpu: 2
+ memory: 2Gi
+ workingDir: /data/repo/test/playwright/executor-tests/playwright-project
+ steps:
+ - name: Run from trait
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/playwright/executor-tests/playwright-project
+ workingDir: /data/repo/test/playwright/executor-tests/playwright-project
+ template:
+ name: official/playwright
+ config:
+ # params: --workers 4
+ tag: v1.32.3-jammy
diff --git a/test/postman/executor-tests/crd-workflow/smoke.yaml b/test/postman/executor-tests/crd-workflow/smoke.yaml
new file mode 100644
index 00000000000..be7b2bfed78
--- /dev/null
+++ b/test/postman/executor-tests/crd-workflow/smoke.yaml
@@ -0,0 +1,108 @@
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: postman-workflow-smoke
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/postman/executor-tests/postman-executor-smoke.postman_collection.json
+ container:
+ resources:
+ requests:
+ cpu: 256m
+ memory: 128Mi
+ workingDir: /data/repo/test/postman/executor-tests
+ steps:
+ - name: Run test
+ run:
+ image: postman/newman:6-alpine
+ args:
+ - run
+ - postman-executor-smoke.postman_collection.json
+ - "--env-var"
+ - "TESTKUBE_POSTMAN_PARAM=TESTKUBE_POSTMAN_PARAM_value"
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: postman-workflow-smoke-without-envs
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/postman/executor-tests/postman-executor-smoke-without-envs.postman_collection.json
+ container:
+ resources:
+ requests:
+ cpu: 256m
+ memory: 128Mi
+ workingDir: /data/repo/test/postman/executor-tests
+ steps:
+ - name: Run test
+ run:
+ image: postman/newman:6-alpine
+ args:
+ - run
+ - postman-executor-smoke-without-envs.postman_collection.json
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: postman-workflow-smoke-preofficial-trait
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/postman/executor-tests/postman-executor-smoke.postman_collection.json
+ container:
+ resources:
+ requests:
+ cpu: 256m
+ memory: 128Mi
+ workingDir: /data/repo/test/postman/executor-tests
+ steps:
+ - name: Run from trait
+ workingDir: /data/repo/test/postman/executor-tests
+ template:
+ name: pre-official/postman
+ config:
+ params: "postman-executor-smoke.postman_collection.json --env-var TESTKUBE_POSTMAN_PARAM=TESTKUBE_POSTMAN_PARAM_value"
+---
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: postman-workflow-smoke-preofficial-trait-without-envs
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/postman/executor-tests/postman-executor-smoke-without-envs.postman_collection.json
+ container:
+ resources:
+ requests:
+ cpu: 256m
+ memory: 128Mi
+ workingDir: /data/repo/test/postman/executor-tests
+ steps:
+ - name: Run from trait
+ template:
+ name: pre-official/postman
+ config:
+ params: "postman-executor-smoke-without-envs.postman_collection.json"
diff --git a/test/scripts/executor-tests/run.sh b/test/scripts/executor-tests/run.sh
index a5919a28e9e..c0c8904b4a0 100755
--- a/test/scripts/executor-tests/run.sh
+++ b/test/scripts/executor-tests/run.sh
@@ -47,9 +47,9 @@ create_update_testsuite_json() { # testsuite_name testsuite_path
if [ "$schedule" = true ] ; then # workaround for appending schedule
random_minute="$(($RANDOM % 59))"
- cat $2 | kubectl testkube --namespace $namespace $type testsuite --name $1 --label app=testkube --schedule "$random_minute */4 * * *"
+ cat $2 | kubectl testkube --namespace $namespace $type testsuite --name $1 --schedule "$random_minute */4 * * *"
else
- cat $2 | kubectl testkube --namespace $namespace $type testsuite --name $1 --label app=testkube
+ cat $2 | kubectl testkube --namespace $namespace $type testsuite --name $1
fi
}
@@ -58,7 +58,7 @@ create_update_testsuite() { # testsuite_name testsuite_path
if [ "$schedule" = true ] ; then # workaround for appending schedule
random_minute="$(($RANDOM % 59))"
- kubectl testkube --namespace $namespace update testsuite --name $1 --label app=testkube --schedule "$random_minute */4 * * *"
+ kubectl testkube --namespace $namespace update testsuite --name $1 --schedule "$random_minute */4 * * *"
fi
}
@@ -111,6 +111,31 @@ common_run() { # name, test_crd_file, testsuite_name, testsuite_file, custom_exe
fi
}
+common_workflow_run() { # name, workflow_crd_file, custom_workflow_template_crd_file
+ name=$1
+ workflow_crd_file=$2
+ custom_workflow_template_crd_file=$3
+
+ print_title "$name"
+
+ if [ "$delete" = true ] ; then
+ if [ ! -z "$custom_executor_crd_file" ] ; then
+ kubectl --namespace $namespace delete -f $custom_workflow_template_crd_file --ignore-not-found=true
+ fi
+ kubectl --namespace $namespace delete -f $workflow_crd_file --ignore-not-found=true
+ fi
+
+ if [ "$create" = true ] ; then
+ if [ ! -z "$custom_workflow_template_crd_file" ] ; then
+ # Workflow Template
+ kubectl --namespace $namespace apply -f $custom_workflow_template_crd_file
+ fi
+
+ # Workflow
+ kubectl --namespace $namespace apply -f $workflow_crd_file
+ fi
+}
+
artillery-smoke() {
name="artillery"
test_crd_file="test/artillery/executor-smoke/crd/crd.yaml"
@@ -142,6 +167,28 @@ container-cypress-smoke() {
common_run "$name" "$test_crd_file" "$testsuite_name" "$testsuite_file" "$custom_executor_crd_file"
}
+container-gradle-smoke() {
+ name="Container executor - Gradle"
+ test_crd_file="test/container-executor/executor-smoke/crd/gradle.yaml"
+ testsuite_name="executor-container-gradle-smoke-tests"
+ testsuite_file="test/suites/executor-container-gradle-smoke-tests.yaml"
+
+ custom_executor_crd_file="test/executors/container-executor-gradle.yaml"
+
+ common_run "$name" "$test_crd_file" "$testsuite_name" "$testsuite_file" "$custom_executor_crd_file"
+}
+
+container-jmeter-smoke() {
+ name="Container executor - JMeter"
+ test_crd_file="test/container-executor/executor-smoke/crd/jmeter.yaml"
+ testsuite_name="executor-container-jmeter-smoke-tests"
+ testsuite_file="test/suites/executor-container-jmeter-smoke-tests.yaml"
+
+ custom_executor_crd_file="test/executors/container-executor-jmeter.yaml"
+
+ common_run "$name" "$test_crd_file" "$testsuite_name" "$testsuite_file" "$custom_executor_crd_file"
+}
+
container-k6-smoke() {
name="Container executor - K6"
test_crd_file="test/container-executor/executor-smoke/crd/k6.yaml"
@@ -153,6 +200,17 @@ container-k6-smoke() {
common_run "$name" "$test_crd_file" "$testsuite_name" "$testsuite_file" "$custom_executor_crd_file"
}
+container-maven-smoke() {
+ name="Container executor - Maven"
+ test_crd_file="test/container-executor/executor-smoke/crd/maven.yaml"
+ testsuite_name="executor-container-maven-smoke-tests"
+ testsuite_file="test/suites/executor-container-maven-smoke-tests.yaml"
+
+ custom_executor_crd_file="test/executors/container-executor-maven.yaml"
+
+ common_run "$name" "$test_crd_file" "$testsuite_name" "$testsuite_file" "$custom_executor_crd_file"
+}
+
container-playwright-smoke() {
name="Container executor - Playwright"
test_crd_file="test/container-executor/executor-smoke/crd/playwright.yaml"
@@ -175,6 +233,17 @@ container-postman-smoke() {
common_run "$name" "$test_crd_file" "$testsuite_name" "$testsuite_file" "$custom_executor_crd_file"
}
+container-soapui-smoke() {
+ name="Container executor - SoapUI"
+ test_crd_file="test/container-executor/executor-smoke/crd/soapui.yaml"
+ testsuite_name="executor-container-soapui-smoke-tests"
+ testsuite_file="test/suites/executor-container-soapui-smoke-tests.yaml"
+
+ custom_executor_crd_file="test/executors/container-executor-soapui.yaml"
+
+ common_run "$name" "$test_crd_file" "$testsuite_name" "$testsuite_file" "$custom_executor_crd_file"
+}
+
curl-smoke() {
name="curl"
test_crd_file="test/curl/executor-tests/crd/smoke.yaml"
@@ -329,15 +398,87 @@ special-cases-large-artifacts() {
common_run "$name" "$test_crd_file" "$testsuite_name" "$testsuite_file" "$custom_executor_crd_file"
}
+special-cases-jmeter() {
+ name="Special Cases - JMeter/JMeterd"
+ test_crd_file="test/jmeter/executor-tests/crd/special-cases.yaml"
+ testsuite_name="jmeter-special-cases"
+ testsuite_file="test/suites/special-cases/jmeter-special-cases.yaml"
+
+ common_run "$name" "$test_crd_file" "$testsuite_name" "$testsuite_file"
+}
+
+workflow-cypress-smoke() {
+ name="Test Workflow - Cypress"
+ workflow_crd_file="test/cypress/executor-tests/crd-workflow/smoke.yaml"
+ custom_workflow_template_crd_file="test/test-workflow-templates/cypress.yaml"
+
+ common_workflow_run "$name" "$workflow_crd_file" "$custom_workflow_template_crd_file"
+}
+
+workflow-gradle-smoke() {
+ name="Test Workflow - Gradle"
+ workflow_crd_file="test/gradle/executor-smoke/crd-workflow/smoke.yaml"
+
+ common_workflow_run "$name" "$workflow_crd_file"
+}
+
+workflow-jmeter-smoke() {
+ name="Test Workflow - JMeter"
+ workflow_crd_file="test/jmeter/executor-tests/crd-workflow/smoke.yaml"
+
+ common_workflow_run "$name" "$workflow_crd_file"
+}
+
+workflow-k6-smoke() {
+ name="Test Workflow - k6"
+ workflow_crd_file="test/k6/executor-tests/crd-workflow/smoke.yaml"
+ custom_workflow_template_crd_file="test/test-workflow-templates/k6.yaml"
+
+ common_workflow_run "$name" "$workflow_crd_file" "$custom_workflow_template_crd_file"
+}
+
+workflow-maven-smoke() {
+ name="Test Workflow - Maven"
+ workflow_crd_file="test/maven/executor-smoke/crd-workflow/smoke.yaml"
+
+ common_workflow_run "$name" "$workflow_crd_file"
+}
+
+workflow-playwright-smoke() {
+ name="Test Workflow - Playwright"
+ workflow_crd_file="test/playwright/executor-tests/crd-workflow/smoke.yaml"
+
+ common_workflow_run "$name" "$workflow_crd_file"
+}
+
+workflow-postman-smoke() {
+ name="Test Workflow - Postman"
+ workflow_crd_file="test/postman/executor-tests/crd-workflow/smoke.yaml"
+ custom_workflow_template_crd_file="test/test-workflow-templates/postman.yaml"
+
+ common_workflow_run "$name" "$workflow_crd_file" "$custom_workflow_template_crd_file"
+}
+
+workflow-soapui-smoke() {
+ name="Test Workflow - SoapUI"
+ workflow_crd_file="test/soapui/executor-smoke/crd-workflow/smoke.yaml"
+
+ common_workflow_run "$name" "$workflow_crd_file"
+}
+
main() {
case $executor_type in
all)
artillery-smoke
container-curl-smoke
container-cypress-smoke
+ container-gradle-smoke
+ container-jmeter-smoke
container-k6-smoke
+ container-maven-smoke
container-postman-smoke
container-playwright-smoke
+ container-soapui-smoke
curl-smoke
cypress-smoke
ginkgo-smoke
@@ -356,9 +497,13 @@ main() {
artillery-smoke
container-curl-smoke
container-cypress-smoke
+ container-gradle-smoke
+ container-jmeter-smoke
container-k6-smoke
+ container-maven-smoke
container-postman-smoke
container-playwright-smoke
+ container-soapui-smoke
curl-smoke
cypress-smoke
ginkgo-smoke
@@ -375,6 +520,17 @@ main() {
special-cases-failures
special-cases-large-logs
special-cases-large-artifacts
+ special-cases-jmeter
+ ;;
+ workflow)
+ workflow-cypress-smoke
+ workflow-gradle-smoke
+ workflow-jmeter-smoke
+ workflow-k6-smoke
+ workflow-maven-smoke
+ workflow-playwright-smoke
+ workflow-postman-smoke
+ workflow-soapui-smoke
;;
*)
$executor_type
@@ -390,4 +546,4 @@ main() {
fi
}
-main
\ No newline at end of file
+main
diff --git a/test/soapui/executor-smoke/crd-workflow/smoke.yaml b/test/soapui/executor-smoke/crd-workflow/smoke.yaml
new file mode 100644
index 00000000000..d4404ad25b5
--- /dev/null
+++ b/test/soapui/executor-smoke/crd-workflow/smoke.yaml
@@ -0,0 +1,25 @@
+apiVersion: testworkflows.testkube.io/v1
+kind: TestWorkflow
+metadata:
+ name: soapui-workflow-smoke
+ labels:
+ core-tests: workflows
+spec:
+ content:
+ git:
+ uri: https://github.com/kubeshop/testkube
+ revision: main
+ paths:
+ - test/soapui/executor-smoke/soapui-smoke-test.xml
+ container:
+ resources:
+ requests:
+ cpu: 512m
+ memory: 256Mi
+ steps:
+ - name: Run tests
+ run:
+ image: smartbear/soapuios-testrunner:5.7.2 # workingDir can't be used because of entrypoint script
+ env:
+ - name: COMMAND_LINE
+ value: "/data/repo/test/soapui/executor-smoke/soapui-smoke-test.xml"
diff --git a/test/special-cases/edge-cases-expected-fails.yaml b/test/special-cases/edge-cases-expected-fails.yaml
index 3dcf831b28c..ca8240b1bda 100644
--- a/test/special-cases/edge-cases-expected-fails.yaml
+++ b/test/special-cases/edge-cases-expected-fails.yaml
@@ -359,3 +359,89 @@ spec:
preRunScript: "echo \"===== pre-run script\""
postRunScript: "echo \"===== post-run script - EXPECTED FAIL\" && exit 128"
jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 256m\n"
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: expected-fail-container-pre-run-script
+ labels:
+ core-tests: expected-fail
+spec:
+ type: container-executor-postman-newman-6-alpine/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/postman/executor-tests/postman-executor-smoke.postman_collection.json
+ workingDir: test/postman/executor-tests
+ executionRequest:
+ args: ["run", "postman-executor-smoke.postman_collection.json", "--env-var", "TESTKUBE_POSTMAN_PARAM=TESTKUBE_POSTMAN_PARAM_value"]
+ preRunScript: "echo \"===== pre-run script - EXPECTED FAIL\" && exit 128"
+ postRunScript: "echo \"===== post-run script\""
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 256m\n"
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: expected-fail-container-post-run-script
+ labels:
+ core-tests: expected-fail
+spec:
+ type: container-executor-postman-newman-6-alpine/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/postman/executor-tests/postman-executor-smoke.postman_collection.json
+ workingDir: test/postman/executor-tests
+ executionRequest:
+ args: ["run", "postman-executor-smoke.postman_collection.json", "--env-var", "TESTKUBE_POSTMAN_PARAM=TESTKUBE_POSTMAN_PARAM_value"]
+ preRunScript: "echo \"===== pre-run script\""
+ postRunScript: "echo \"===== post-run script - EXPECTED FAIL\" && exit 128"
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 256m\n"
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: expected-fail-pre-post-run-script
+ labels:
+ core-tests: expected-fail
+spec:
+ type: postman/collection
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/postman/executor-tests/postman-executor-smoke-negative.postman_collection.json
+ executionRequest:
+ preRunScript: "echo \"===== pre-run script\""
+ postRunScript: "echo \"===== post-run script\""
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 256m\n"
+---
+apiVersion: tests.testkube.io/v3
+kind: Test
+metadata:
+ name: expected-fail-container-pre-post-run-script
+ labels:
+ core-tests: expected-fail
+spec:
+ type: container-executor-postman-newman-6-alpine/test
+ content:
+ type: git
+ repository:
+ type: git
+ uri: https://github.com/kubeshop/testkube.git
+ branch: main
+ path: test/postman/executor-tests/postman-executor-smoke-negative.postman_collection.json
+ workingDir: test/postman/executor-tests
+ executionRequest:
+ args: ["run", "postman-executor-smoke.postman_collection.json"]
+ preRunScript: "echo \"===== pre-run script\""
+ postRunScript: "echo \"===== post-run script\""
+ jobTemplate: "apiVersion: batch/v1\nkind: Job\nspec:\n template:\n spec:\n containers:\n - name: \"{{ .Name }}\"\n image: {{ .Image }}\n resources:\n requests:\n memory: 128Mi\n cpu: 256m\n"
diff --git a/test/suites/executor-container-cypress-smoke-tests.yaml b/test/suites/executor-container-cypress-smoke-tests.yaml
index 950c7abb34b..9ddbe173422 100644
--- a/test/suites/executor-container-cypress-smoke-tests.yaml
+++ b/test/suites/executor-container-cypress-smoke-tests.yaml
@@ -13,3 +13,6 @@ spec:
- stopOnFailure: false
execute:
- test: container-executor-cypress-v12.7.0-smoke-git-dir
+ - stopOnFailure: false
+ execute:
+ - test: container-executor-cypress-v12.7.0-video-artifacts-only
diff --git a/test/suites/executor-container-gradle-smoke-tests.yaml b/test/suites/executor-container-gradle-smoke-tests.yaml
new file mode 100644
index 00000000000..c642decd13e
--- /dev/null
+++ b/test/suites/executor-container-gradle-smoke-tests.yaml
@@ -0,0 +1,12 @@
+apiVersion: tests.testkube.io/v3
+kind: TestSuite
+metadata:
+ name: executor-container-gradle-smoke-tests
+ labels:
+ core-tests: executors
+spec:
+ description: "container executor gradle smoke tests"
+ steps:
+ - stopOnFailure: false
+ execute:
+ - test: container-executor-gradle-jdk-11
diff --git a/test/suites/executor-container-jmeter-smoke-tests.yaml b/test/suites/executor-container-jmeter-smoke-tests.yaml
new file mode 100644
index 00000000000..8ac7b8cd6a1
--- /dev/null
+++ b/test/suites/executor-container-jmeter-smoke-tests.yaml
@@ -0,0 +1,12 @@
+apiVersion: tests.testkube.io/v3
+kind: TestSuite
+metadata:
+ name: executor-container-jmeter-smoke-tests
+ labels:
+ app: testkube
+spec:
+ description: "container executor jmeter smoke tests"
+ steps:
+ - stopOnFailure: false
+ execute:
+ - test: container-executor-jmeter-smoke
diff --git a/test/suites/executor-container-k6-smoke-tests.yaml b/test/suites/executor-container-k6-smoke-tests.yaml
index be78b84cc9e..9e0dc75a857 100644
--- a/test/suites/executor-container-k6-smoke-tests.yaml
+++ b/test/suites/executor-container-k6-smoke-tests.yaml
@@ -13,3 +13,6 @@ spec:
- stopOnFailure: false
execute:
- test: container-executor-k6-smoke-git-file
+ - stopOnFailure: false
+ execute:
+ - test: container-executor-k6-smoke-report
diff --git a/test/suites/executor-container-maven-smoke-tests.yaml b/test/suites/executor-container-maven-smoke-tests.yaml
new file mode 100644
index 00000000000..34b944eb83d
--- /dev/null
+++ b/test/suites/executor-container-maven-smoke-tests.yaml
@@ -0,0 +1,12 @@
+apiVersion: tests.testkube.io/v3
+kind: TestSuite
+metadata:
+ name: executor-container-maven-smoke-tests
+ labels:
+ core-tests: executors
+spec:
+ description: "container executor maven smoke tests"
+ steps:
+ - stopOnFailure: false
+ execute:
+ - test: container-executor-maven-jdk-11
diff --git a/test/suites/executor-container-soapui-smoke-tests.yaml b/test/suites/executor-container-soapui-smoke-tests.yaml
new file mode 100644
index 00000000000..d8ad9936b1e
--- /dev/null
+++ b/test/suites/executor-container-soapui-smoke-tests.yaml
@@ -0,0 +1,12 @@
+apiVersion: tests.testkube.io/v3
+kind: TestSuite
+metadata:
+ name: executor-container-soapui-smoke-tests
+ labels:
+ app: testkube
+spec:
+ description: "container executor soapui smoke tests"
+ steps:
+ - stopOnFailure: false
+ execute:
+ - test: container-executor-soapui-smoke
diff --git a/test/suites/executor-gradle-smoke-tests.yaml b/test/suites/executor-gradle-smoke-tests.yaml
index 592d3875bdf..cbc683cf255 100644
--- a/test/suites/executor-gradle-smoke-tests.yaml
+++ b/test/suites/executor-gradle-smoke-tests.yaml
@@ -7,6 +7,9 @@ metadata:
spec:
description: "gradle executor smoke tests"
steps:
+ - stopOnFailure: false
+ execute:
+ - test: gradle-executor-smoke
- stopOnFailure: false
execute:
- test: gradle-executor-smoke-jdk18
diff --git a/test/suites/executor-jmeter-other-tests.yaml b/test/suites/executor-jmeter-other-tests.yaml
index 6ed16725314..761c7d3bc22 100644
--- a/test/suites/executor-jmeter-other-tests.yaml
+++ b/test/suites/executor-jmeter-other-tests.yaml
@@ -5,23 +5,29 @@ metadata:
labels:
core-tests: executors
spec:
- description: "jmeter and jmeterd executor - other tests and edge-cases"
+ description: "jmeter and jmeterd executor - other tests"
steps:
- stopOnFailure: false
execute:
- test: jmeter-executor-smoke-incorrect-url-assertion-negative
- stopOnFailure: false
execute:
- - test: jmeterd-executor-smoke-incorrect-url-assertion-negative
+ - test: jmeterd-executor-smoke-incorrect-url-assertion
- stopOnFailure: false
execute:
- - test: jmeterd-executor-smoke-incorrect-url-assertion-slaves-negative
+ - test: jmeterd-executor-smoke-incorrect-url-assertion-slaves
- stopOnFailure: false
execute:
- - test: jmeterd-executor-smoke-correct-url-failed-assertion-negative
+ - test: jmeterd-executor-smoke-correct-url-failed-assertion
- stopOnFailure: false
execute:
- - test: jmeterd-executor-smoke-failed-assertion-slaves-negative
+ - test: jmeterd-executor-smoke-failed-assertion-slaves
- stopOnFailure: false
execute:
- test: jmeterd-executor-smoke-failure-exit-code-0-negative
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-slave-0
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-slave-not-set
diff --git a/test/suites/executor-jmeter-smoke-tests.yaml b/test/suites/executor-jmeter-smoke-tests.yaml
index f71f53907a5..df0a0fad3af 100644
--- a/test/suites/executor-jmeter-smoke-tests.yaml
+++ b/test/suites/executor-jmeter-smoke-tests.yaml
@@ -28,3 +28,6 @@ spec:
- stopOnFailure: false
execute:
- test: jmeterd-executor-smoke-slaves
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-env-and-property-values
diff --git a/test/suites/executor-maven-smoke-tests.yaml b/test/suites/executor-maven-smoke-tests.yaml
index 84dc640ca9a..15398f9dc0e 100644
--- a/test/suites/executor-maven-smoke-tests.yaml
+++ b/test/suites/executor-maven-smoke-tests.yaml
@@ -7,6 +7,9 @@ metadata:
spec:
description: "maven executor smoke tests"
steps:
+ - stopOnFailure: false
+ execute:
+ - test: maven-executor-smoke
- stopOnFailure: false
execute:
- test: maven-executor-smoke-jdk18
diff --git a/test/suites/special-cases/edge-cases-expected-fails.yaml b/test/suites/special-cases/edge-cases-expected-fails.yaml
index 555d9e66193..748f22d29d1 100644
--- a/test/suites/special-cases/edge-cases-expected-fails.yaml
+++ b/test/suites/special-cases/edge-cases-expected-fails.yaml
@@ -64,3 +64,15 @@ spec:
- stopOnFailure: false
execute:
- test: expected-fail-post-run-script
+ - stopOnFailure: false
+ execute:
+ - test: expected-fail-container-pre-run-script
+ - stopOnFailure: false
+ execute:
+ - test: expected-fail-container-post-run-script
+ - stopOnFailure: false
+ execute:
+ - test: expected-fail-pre-post-run-script
+ - stopOnFailure: false
+ execute:
+ - test: expected-fail-container-pre-post-run-script
diff --git a/test/suites/special-cases/jmeter-special-cases.yaml b/test/suites/special-cases/jmeter-special-cases.yaml
new file mode 100644
index 00000000000..d40e7678b6b
--- /dev/null
+++ b/test/suites/special-cases/jmeter-special-cases.yaml
@@ -0,0 +1,45 @@
+apiVersion: tests.testkube.io/v3
+kind: TestSuite
+metadata:
+ name: jmeter-special-cases
+ labels:
+ core-tests: special-cases
+spec:
+ description: "jmeter and jmeterd executor - special-cases"
+ steps:
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-custom-envs-replication
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-env-value-in-args
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-directory-1
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-directory-2
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-directory-t-o
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-directory-t-o-slaves-2
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-incorrect-file-path-negative
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-incorrect-url-2
+ - stopOnFailure: false
+ execute:
+ - test: jmeter-executor-smoke-incorrect-url-2
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-args-override
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-args-override-workingdir
+ - stopOnFailure: false
+ execute:
+ - test: jmeterd-executor-smoke-runner-artifacts-dir
diff --git a/test/test-workflow-templates/cypress.yaml b/test/test-workflow-templates/cypress.yaml
new file mode 100644
index 00000000000..816d915e976
--- /dev/null
+++ b/test/test-workflow-templates/cypress.yaml
@@ -0,0 +1,29 @@
+kind: TestWorkflowTemplate
+apiVersion: testworkflows.testkube.io/v1
+metadata:
+ name: pre-official--cypress
+spec:
+ config:
+ dependencies_command:
+ description: Command to install dependencies
+ type: string
+ default: npm install
+ version:
+ description: Cypress version to use
+ type: string
+ default: 13.6.4
+ params:
+ description: Additional params for the cypress run command
+ type: string
+ default: ""
+ steps:
+ - name: Install dependencies
+ container:
+ image: cypress/included:{{ config.version }}
+ shell: '{{ config.dependencies_command }}'
+
+
+ - name: Run Cypress tests
+ container:
+ image: cypress/included:{{ config.version }}
+ shell: npx cypress run {{ config.params }}
diff --git a/test/test-workflow-templates/k6.yaml b/test/test-workflow-templates/k6.yaml
new file mode 100644
index 00000000000..78e242dbbe2
--- /dev/null
+++ b/test/test-workflow-templates/k6.yaml
@@ -0,0 +1,19 @@
+kind: TestWorkflowTemplate
+apiVersion: testworkflows.testkube.io/v1
+metadata:
+ name: pre-official--k6
+spec:
+ config:
+ version:
+ description: k6 version to use
+ type: string
+ default: 0.49.0
+ params:
+ description: Additional params for the k6 run command
+ type: string
+ default: ""
+ steps:
+ - name: Run k6 tests
+ container:
+ image: grafana/k6:{{ config.version }}
+ shell: k6 run {{ config.params }}
diff --git a/test/test-workflow-templates/postman.yaml b/test/test-workflow-templates/postman.yaml
new file mode 100644
index 00000000000..cd65e9d3dd7
--- /dev/null
+++ b/test/test-workflow-templates/postman.yaml
@@ -0,0 +1,19 @@
+kind: TestWorkflowTemplate
+apiVersion: testworkflows.testkube.io/v1
+metadata:
+ name: pre-official--postman
+spec:
+ config:
+ version:
+ description: Postman version to use
+ type: string
+ default: 6-alpine
+ params:
+ description: Additional params for the Postman (Newman) run command
+ type: string
+ default: ""
+ steps:
+ - name: Run Postman tests
+ container:
+ image: postman/newman:{{ config.version }}
+ shell: newman run {{ config.params }}