├── .dockerignore ├── .gitattributes ├── .github └── workflows │ ├── docker-publish.yml │ ├── gradle.yml │ ├── maven.yml │ ├── testkube-pro.yaml │ └── testkube.yaml ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── Tiltfile ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── k8s ├── applications │ ├── gradle-demo │ │ ├── gradle-integration-test.yaml │ │ ├── gradle-test.yaml │ │ └── kustomization.yaml │ ├── k6-demo │ │ ├── k6-nginx.yaml │ │ ├── k6-test-nginx-gitops.yaml │ │ ├── k6-test-nginx.yaml │ │ └── kustomization.yaml │ ├── kustomization.yaml │ ├── maven-demo │ │ └── maven-test.yaml │ ├── testsuite.yaml │ ├── testtrigger.yaml │ └── zap-demo │ │ ├── zap-api-test.yaml │ │ └── zap-api-testtrigger.yaml ├── base │ ├── kustomization.yaml │ ├── microservice-deployment.yaml │ └── microservice-service.yaml ├── clusters │ └── testkube-cluster │ │ └── flux-system │ │ ├── applications-sync.yaml │ │ ├── gotk-components.yaml │ │ ├── gotk-sync.yaml │ │ ├── infrastructure-sync.yaml │ │ ├── kustomization.yaml │ │ └── microservice-sync.yaml ├── infrastructure │ ├── dashboard │ │ ├── dashboard-adminuser.yaml │ │ └── kustomization.yaml │ ├── kustomization.yaml │ └── testkube │ │ ├── karate-executor.yaml │ │ ├── kustomization.yaml │ │ ├── namespace.yaml │ │ ├── testkube-release.yaml │ │ ├── testkube-source.yaml │ │ └── zap-executor.yaml ├── overlays │ ├── dev │ │ ├── hoverfly-deployment.yaml │ │ ├── hoverfly-service.yaml │ │ └── kustomization.yaml │ └── int │ │ ├── 2-replicas.yaml │ │ ├── kustomization.yaml │ │ └── loadbalancer.yaml └── zap │ ├── zap-cronjob.yaml │ ├── zap-daemon.yaml │ ├── zap-namespace.yaml │ └── zap-webswing.yaml ├── mvnw ├── mvnw.cmd ├── pom.xml ├── settings.gradle └── src ├── intTest └── java │ └── hands │ └── on │ └── testkube │ └── RemoteTestkubeTest.java ├── k6 ├── k6-test-microservice.js ├── k6-test-nginx.js └── k6-test-scenarios.js ├── karate └── chuck-norris.feature ├── main ├── java │ └── hands │ │ └── on │ │ └── testkube │ │ ├── HandsOnTestkubeApplication.java │ │ └── HandsOnTestkubeController.java └── resources │ └── application.properties ├── postman └── postman_collection.json ├── test └── java │ └── hands │ └── on │ └── testkube │ ├── ExampleTest.java │ └── RemoteTestkubeIT.java └── zap ├── zap-api.yaml └── zap-baseline.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | .dccache 2 | .vscode/ 3 | .gradle/ 4 | .idea/ 5 | 6 | target/ 7 | build/ 8 | bin/ 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | *.cmd text eol=crlf 7 | 8 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: 'Docker Publish' 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | tags: [ 'v*.*.*' ] 7 | pull_request: 8 | branches: [ "main" ] 9 | 10 | env: 11 | # Use docker.io for Docker Hub if empty 12 | REGISTRY: ghcr.io 13 | # github.repository as / 14 | IMAGE_NAME: ${{ github.repository }} 15 | 16 | jobs: 17 | docker-build-publish: 18 | 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: read 22 | packages: write 23 | # This is used to complete the identity challenge 24 | # with sigstore/fulcio when running outside of PRs. 25 | id-token: write 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v3 30 | 31 | # Install the cosign tool except on PR 32 | # https://github.com/sigstore/cosign-installer 33 | - name: Install cosign 34 | if: github.event_name != 'pull_request' 35 | uses: sigstore/cosign-installer@main 36 | with: 37 | cosign-release: 'v1.9.0' 38 | 39 | # Workaround: https://github.com/docker/build-push-action/issues/461 40 | - name: Setup Docker buildx 41 | uses: docker/setup-buildx-action@v2 42 | 43 | # Login against a Docker registry except on PR 44 | # https://github.com/docker/login-action 45 | - name: Log into registry ${{ env.REGISTRY }} 46 | if: github.event_name != 'pull_request' 47 | uses: docker/login-action@v2 48 | with: 49 | registry: ${{ env.REGISTRY }} 50 | username: ${{ github.actor }} 51 | password: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | # Extract metadata (tags, labels) for Docker 54 | # https://github.com/docker/metadata-action 55 | - name: Extract Docker metadata 56 | id: meta 57 | uses: docker/metadata-action@v4 58 | with: 59 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 60 | tags: | 61 | type=semver,pattern={{version}} 62 | type=semver,pattern={{major}}.{{minor}} 63 | type=semver,pattern={{major}} 64 | type=ref,event=branch 65 | type=raw,value=latest,enable={{is_default_branch}} 66 | 67 | # Build and push Docker image with Buildx (don't push on PR) 68 | # https://github.com/docker/build-push-action 69 | - name: Build and push Docker image 70 | id: build-and-push 71 | uses: docker/build-push-action@v3 72 | with: 73 | context: . 74 | push: ${{ github.event_name != 'pull_request' }} 75 | tags: ${{ steps.meta.outputs.tags }} 76 | labels: ${{ steps.meta.outputs.labels }} 77 | 78 | # Sign the resulting Docker image digest except on PRs. 79 | # This will only write to the public Rekor transparency log when the Docker 80 | # repository is public to avoid leaking data. If you would like to publish 81 | # transparency data even for private images, pass --force to cosign below. 82 | # https://github.com/sigstore/cosign 83 | # - name: Sign the published Docker image 84 | # if: ${{ github.event_name != 'pull_request' }} 85 | # env: 86 | # COSIGN_EXPERIMENTAL: "true" 87 | # This step uses the identity token to provision an ephemeral certificate 88 | # against the sigstore community Fulcio instance. 89 | # run: cosign sign ${{ steps.meta.outputs.tags }}@${{ steps.build-and-push.outputs.digest }} 90 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Gradle 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - 'src/**' 8 | - 'build.gradle' 9 | branches: [ "main" ] 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up JDK 17 22 | uses: actions/setup-java@v3 23 | with: 24 | java-version: '17' 25 | distribution: 'temurin' 26 | cache: 'gradle' 27 | 28 | - name: Setup with Gradle 29 | uses: gradle/gradle-build-action@v2 30 | 31 | - name: Execute Gradle build 32 | run: ./gradlew build --no-daemon 33 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Maven 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - 'src/**' 8 | - 'pom.xml' 9 | branches: [ "main" ] 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up JDK 17 19 | uses: actions/setup-java@v3 20 | with: 21 | java-version: '17' 22 | distribution: 'temurin' 23 | cache: 'maven' 24 | 25 | - name: Build with Maven 26 | run: ./mvnw -B package --file pom.xml 27 | -------------------------------------------------------------------------------- /.github/workflows/testkube-pro.yaml: -------------------------------------------------------------------------------- 1 | name: Run Testkube Pro Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | test: 8 | permissions: 9 | contents: 'read' 10 | id-token: 'write' 11 | 12 | name: Run Testkube Pro 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: kubeshop/setup-testkube@v1 17 | with: 18 | organization: ${{ secrets.TkOrganization }} 19 | environment: ${{ secrets.TkEnvironment }} 20 | token: ${{ secrets.TkToken }} 21 | 22 | - run: | 23 | testkube run testworkflow gradle-test -f 24 | -------------------------------------------------------------------------------- /.github/workflows/testkube.yaml: -------------------------------------------------------------------------------- 1 | name: Run Testkube Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | # push: 6 | # paths: 7 | # - 'src/**' 8 | # branches: 9 | # - main 10 | 11 | env: 12 | GKE_CLUSTER_NAME: ${{ secrets.GKE_CLUSTER_NAME }} # Add your cluster name here. 13 | GKE_ZONE: ${{ secrets.GKE_ZONE }} # Add your cluster zone here. 14 | 15 | jobs: 16 | test: 17 | permissions: 18 | contents: 'read' 19 | id-token: 'write' 20 | 21 | name: Run Testkube 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v2 27 | with: 28 | fetch-depth: 0 29 | 30 | - name: Configure Git 31 | run: | 32 | git config user.name "$GITHUB_ACTOR" 33 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 34 | 35 | # Authenticate with Google Cloud so we can deploy to the cluster 36 | - id: 'auth' 37 | uses: 'google-github-actions/auth@v2' 38 | with: 39 | project_id: ${{ secrets.GKE_PROJECT }} 40 | credentials_json: '${{ secrets.GKE_SA_KEY }}' 41 | 42 | - name: 'Set up Cloud SDK' 43 | uses: 'google-github-actions/setup-gcloud@v2' 44 | 45 | # Configure Docker to use the gcloud command-line tool as a credential 46 | # helper for authentication 47 | - run: |- 48 | gcloud --quiet auth configure-docker 49 | 50 | # Get the GKE credentials so we can deploy to the cluster 51 | - uses: 'google-github-actions/get-gke-credentials@v2' 52 | with: 53 | cluster_name: ${{ env.GKE_CLUSTER_NAME }} 54 | location: ${{ env.GKE_ZONE }} 55 | 56 | # Run Testkube test on a GKE cluster 57 | - uses: kubeshop/setup-testkube@v1 58 | - run: | 59 | testkube run testworkflow gradle-test -f 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dccache 2 | .vscode/ 3 | .gradle/ 4 | .idea/ 5 | 6 | target/ 7 | build/ 8 | bin/ 9 | 10 | *key.json 11 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lreimer/hands-on-testkube/8267794ef85614a7cc9c48523995dc52a29d3a39/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:17-jdk as builder 2 | 3 | WORKDIR /spring 4 | 5 | COPY gradle/ gradle/ 6 | COPY gradlew . 7 | COPY build.gradle . 8 | COPY settings.gradle . 9 | COPY src/ src/ 10 | 11 | RUN ./gradlew assemble 12 | 13 | # base image 14 | FROM gcr.io/distroless/java17-debian11 15 | 16 | # port 17 | EXPOSE 8080 8081 18 | 19 | # jar file 20 | COPY --from=builder /spring/build/libs/hands-on-testkube-1.0.0.jar hands-on-testkube.jar 21 | 22 | # entry 23 | CMD ["hands-on-testkube.jar"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 M.-Leander Reimer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | prepare-gke-cluster: 2 | @gcloud config set compute/zone europe-west1-b 3 | @gcloud config set container/use_client_certificate False 4 | 5 | create-gke-cluster: 6 | @gcloud container clusters create testkube-cluster --num-nodes=3 --enable-autoscaling --min-nodes=3 --max-nodes=5 --machine-type=n2-standard-8 --cluster-version=1.30 7 | @kubectl create clusterrolebinding cluster-admin-binding --clusterrole=cluster-admin --user=$$(gcloud config get-value core/account) 8 | @kubectl cluster-info 9 | 10 | bootstrap-flux2: 11 | @flux bootstrap github \ 12 | --owner=$(GITHUB_USER) \ 13 | --repository=hands-on-testkube \ 14 | --branch=main \ 15 | --path=./k8s/clusters/testkube-cluster \ 16 | --components-extra=image-reflector-controller,image-automation-controller \ 17 | --read-write-key \ 18 | --personal 19 | 20 | delete-gke-cluster: 21 | @gcloud container clusters delete testkube-cluster --async --quiet -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hands-on TestKube 2 | 3 | Demo repository for TestKube - a opinionated and friendly Kubernetes testing framework! 4 | 5 | This project contains test sources for various languages, frameworks and tools: 6 | - Load tests to be run with k6 7 | - JUnit 5 based unit tests to be run with either Maven or Gradle 8 | - Integration tests using REST-assured to be run with either Maven or Gradle 9 | - Security test using the ZAP attack proxy 10 | - Karate acceptance tests to be run with the Karate executor 11 | 12 | ## Bootstrapping 13 | 14 | ```bash 15 | # For a simple local setup use the following instructions 16 | kubectl testkube init 17 | 18 | # to use Testkube Pro setup and connect an agent 19 | # look at the UI for command with correct tokens 20 | testkube pro init --agent-token=tkcagnt_etc.pp --org-id=tkcorg_etc.pp --env-id=tkcenv_etc.pp 21 | ``` 22 | 23 | For a GKE based setup using Flux as GitOps tool, use the following instructions: 24 | ```bash 25 | # define required ENV variables for the next steps to work 26 | export GITHUB_USER=lreimer 27 | export GITHUB_TOKEN= 28 | 29 | # setup a GKE cluster with Flux2 30 | make create-gke-cluster 31 | make bootstrap-flux2 32 | 33 | # modify Flux kustomization and add 34 | # - infrastructure-sync.yaml 35 | # - applications-sync.yaml 36 | # - notification-receiver.yaml 37 | # - receiver-service.yaml 38 | # - webhook-token.yaml 39 | # - image-update-automation.yaml 40 | 41 | # if the TestKube Helm release fails to reconciliate 42 | flux suspend hr testkube 43 | flux resume hr testkube 44 | 45 | # you also need to create the webhook for the Git Repository 46 | # Payload URL: http:/// 47 | # Secret: the webhook-token value 48 | # kubectl -n flux-system get svc/receiver 49 | # kubectl -n flux-system get receiver/webapp 50 | 51 | make delete-gke-cluster 52 | ``` 53 | 54 | ## TestKube UI and CLI 55 | 56 | ```bash 57 | kubectl testkube --help 58 | kubectl testkube dashboard 59 | ``` 60 | 61 | ## Postman Executor Example 62 | 63 | ```bash 64 | kubectl testkube create test --file src/postman/postman_collection.json --type postman/collection --name postman-test 65 | kubectl testkube run test postman-test 66 | 67 | kubectl testkube create test --file src/postman/postman_collection.json --type postman/collection --name postman-scheduled-test --schedule="*/5 * * * *" 68 | ``` 69 | 70 | ## K6 Executor Example 71 | 72 | ```bash 73 | # create simple k6 file based script 74 | kubectl testkube create test --file src/k6/k6-test-scenarios.js --type "k6/script" --name k6-test-script 75 | kubectl testkube run test --watch k6-test-script 76 | 77 | # create Nginx service and k6 load test 78 | kubectl testkube create test --file src/k6/k6-test-nginx.js --type "k6/script" --name k6-test-nginx 79 | kubectl testkube run test --env TARGET_HOSTNAME=nginx-service.default.svc.cluster.local --watch k6-test-nginx 80 | kubectl testkube run test --env K6_OUT=influxdb=http://influxdb-service:8086/k6 --env TARGET_HOSTNAME=nginx-service.default.svc.cluster.local --watch k6-test-nginx 81 | 82 | # create a generic k6 test for this repository 83 | kubectl testkube create test --git-uri https://github.com/lreimer/hands-on-testkube.git --git-branch main --git-path src/k6/ --type "k6/script" --name k6-test-script-git 84 | kubectl testkube run test --args src/k6/k6-test-scenarios.js --watch k6-test-script-git 85 | 86 | kubectl testkube create test --git-uri https://github.com/lreimer/hands-on-testkube.git --git-branch main --git-path src/k6/ --type "k6/script" --name k6-test-nginx 87 | kubectl testkube run test --args src/k6/k6-test-nginx.js --watch k6-test-nginx 88 | ``` 89 | 90 | ## Gradle Executor Example 91 | 92 | ```bash 93 | # create a Gradle test for this repository 94 | kubectl testkube create test --git-uri https://github.com/lreimer/hands-on-testkube.git --git-branch main --type "gradle/test" --name gradle-test 95 | kubectl testkube run test --watch gradle-test 96 | 97 | # create a Gradle integrationTest for this repository 98 | kubectl testkube create test --git-uri https://github.com/lreimer/hands-on-testkube.git --git-branch main --type "gradle/integrationTest" --name gradle-integration-test 99 | kubectl testkube run test --watch gradle-integration-test 100 | 101 | # or create a Gradle project and pass test task via args 102 | kubectl testkube create test --git-uri https://github.com/lreimer/hands-on-testkube.git --git-branch main --type "gradle/project" --name gradle-project 103 | kubectl testkube run test --args integrationTest --watch gradle-project 104 | ``` 105 | 106 | ## Maven Executor Example 107 | 108 | ```bash 109 | # create a Maven test for this repository 110 | kubectl testkube create test --git-uri https://github.com/lreimer/hands-on-testkube.git --git-branch main --type "maven/test" --name maven-test 111 | kubectl testkube run test --watch maven-test 112 | 113 | # create a Maven integration test for this repository 114 | kubectl testkube create test --git-uri https://github.com/lreimer/hands-on-testkube.git --git-branch main --type "maven/integration-test" --name maven-integration-test 115 | kubectl testkube run test --watch maven-integration-test 116 | 117 | # or create a Maven project and pass test goal via args 118 | kubectl testkube create test --git-uri https://github.com/lreimer/hands-on-testkube.git --git-branch main --type "maven/project" --name maven-project 119 | kubectl testkube run test --args integration-test --watch maven-project 120 | ``` 121 | 122 | ## ZAP Executor Example 123 | 124 | ```bash 125 | # register the executor manually or via GitOps 126 | kubectl apply -n testkube -f k8s/infrastructure/testkube/zap-executor.yaml 127 | 128 | # run a ZAP OpenAPI scan against microservice 129 | kubectl testkube create test --file src/zap/zap-api.yaml --type "zap/api" --name zap-api-test 130 | kubectl testkube run test --watch zap-api-test 131 | 132 | # run a ZAP Baseline scan against microservice 133 | kubectl testkube create test --file src/zap/zap-baseline.yaml --type "zap/baseline" --name zap-baseline-test 134 | kubectl testkube run test --watch zap-baseline-test 135 | ``` 136 | 137 | ## Karate Executor Example 138 | 139 | ```bash 140 | # register the executor manually or via GitOps 141 | kubectl apply -n testkube -f k8s/infrastructure/testkube/karate-executor.yaml 142 | 143 | # create and run a Karate test agains Chuck Norris REST API 144 | kubectl testkube create test --file src/karate/chuck-norris.feature --type "karate/feature" --name karate-test 145 | kubectl testkube run test --watch karate-test 146 | ``` 147 | 148 | ## TestSource Example 149 | 150 | ```bash 151 | kubectl testkube create testsource --name hands-on-testkube --git-uri https://github.com/lreimer/hands-on-testkube.git --git-branch main 152 | 153 | kubectl testkube create test --source hands-on-testkube --type "k6/script" --name k6-testsource 154 | kubectl testkube run test --args src/k6/k6-test-scenarios.js --watch k6-testsource 155 | ``` 156 | 157 | ## TestSuite Example 158 | 159 | ```bash 160 | # manually create and run a test suite from JSON 161 | kubectl testkube create testsuite --name hands-on-testkube --file src/suite.json 162 | kubectl get testsuites -n testkube hands-on-testkube -o yaml 163 | kubectl testkube run testsuite hands-on-testkube 164 | 165 | # alternatively, create test suite via GitOps 166 | # see k8s/applications/testsuite.yaml 167 | # this uses the new test workflow CRD 168 | ``` 169 | 170 | ## GitHub Actions Example 171 | 172 | ```bash 173 | # see .github/workflows/testkube.yaml 174 | # see https://github.com/lreimer/hands-on-testkube/actions/workflows/testkube.yaml 175 | ``` 176 | 177 | ## Test Triggers Example 178 | 179 | ```bash 180 | # register the test triggers manually or via GitOps 181 | kubectl apply -n testkube -f k8s/applications/testtrigger.yaml 182 | kubectl apply -n testkube -f k8s/applications/zap-demo/zap-api-testtrigger.yaml 183 | 184 | # then make changes to the Nginx or Microservice deployment 185 | kubectl scale deployment nginx-deployment --replicas=3 186 | kubectl scale deployment hands-on-testkube --replicas=3 187 | ``` 188 | 189 | ## Maintainer 190 | 191 | M.-Leander Reimer (@lreimer), 192 | 193 | ## License 194 | 195 | This software is provided under the MIT open source license, read the `LICENSE` 196 | file for details. -------------------------------------------------------------------------------- /Tiltfile: -------------------------------------------------------------------------------- 1 | # -*- mode: Python -*- 2 | # allow_k8s_contexts('rancher-desktop') 3 | 4 | local_resource('hands-on-testkube-build', './gradlew ass', dir='.', deps=['./build.gradle', './src/'], labels=['Spring']) 5 | 6 | # to disable push with rancher desktop we need to use custom_build instead of docker_build 7 | # docker_build('hands-on-testkube', '.', dockerfile='Dockerfile', only=['./Dockerfile', './build/libs/']) 8 | custom_build('hands-on-testkube', 'docker build -t $EXPECTED_REF .', ['./Dockerfile', './build/libs/'], disable_push=True) 9 | 10 | k8s_yaml(kustomize('./k8s/overlays/dev/')) 11 | k8s_resource(workload='hands-on-testkube', port_forwards=[port_forward(18080, 8080, 'HTTP API'), port_forward(18081, 8081, 'Management API')], labels=['Spring']) 12 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.7.4' 3 | id 'io.spring.dependency-management' version '1.0.14.RELEASE' 4 | id 'java' 5 | } 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | sourceSets { 12 | intTest { 13 | compileClasspath += sourceSets.main.output 14 | runtimeClasspath += sourceSets.main.output 15 | } 16 | } 17 | 18 | configurations { 19 | intTestImplementation.extendsFrom implementation 20 | intTestRuntimeOnly.extendsFrom runtimeOnly 21 | } 22 | 23 | dependencies { 24 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 25 | implementation 'org.springframework.boot:spring-boot-starter-web' 26 | implementation 'org.springdoc:springdoc-openapi-webmvc-core:1.6.11' 27 | 28 | testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2' 29 | testImplementation 'io.rest-assured:rest-assured:4.5.1' 30 | 31 | intTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.2' 32 | intTestImplementation 'io.rest-assured:rest-assured:4.5.1' 33 | } 34 | 35 | group = 'hands.on.testkube' 36 | version = '1.0.0' 37 | sourceCompatibility = '11' 38 | 39 | tasks.withType(JavaCompile) { 40 | options.encoding = 'UTF-8' 41 | } 42 | 43 | tasks.register('integrationTest', Test) { 44 | description = 'Runs integration tests.' 45 | group = 'verification' 46 | 47 | testClassesDirs = sourceSets.intTest.output.classesDirs 48 | classpath = sourceSets.intTest.runtimeClasspath 49 | } 50 | 51 | // integrationTest.dependsOn test 52 | check.dependsOn integrationTest 53 | 54 | tasks.withType(Test) { 55 | useJUnitPlatform() 56 | } 57 | 58 | tasks.named("bootBuildImage") { 59 | imageName = "${project.name}:${project.version}" 60 | } 61 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lreimer/hands-on-testkube/8267794ef85614a7cc9c48523995dc52a29d3a39/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /k8s/applications/gradle-demo/gradle-integration-test.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tests.testkube.io/v3 2 | kind: Test 3 | metadata: 4 | generation: 1 5 | name: gradle-integration-test-gitops 6 | namespace: testkube 7 | spec: 8 | content: 9 | repository: 10 | branch: main 11 | type: git 12 | uri: https://github.com/lreimer/hands-on-testkube.git 13 | type: git-dir 14 | executionRequest: {} 15 | type: gradle/integrationTest -------------------------------------------------------------------------------- /k8s/applications/gradle-demo/gradle-test.yaml: -------------------------------------------------------------------------------- 1 | kind: TestWorkflow 2 | apiVersion: testworkflows.testkube.io/v1 3 | metadata: 4 | name: gradle-test 5 | namespace: testkube 6 | labels: 7 | test-workflow-templates: "yes" 8 | testkube.io/name: Gradle 9 | spec: 10 | use: 11 | - name: official/gradle/v1 12 | content: 13 | git: 14 | uri: https://github.com/lreimer/hands-on-testkube.git 15 | container: 16 | workingDir: /data/repo 17 | job: 18 | activeDeadlineSeconds: 60 19 | steps: 20 | - name: Saving artifacts 21 | condition: always 22 | artifacts: 23 | paths: 24 | - build/test-results/test/** 25 | status: {} 26 | -------------------------------------------------------------------------------- /k8s/applications/gradle-demo/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - gradle-integration-test.yaml 5 | -------------------------------------------------------------------------------- /k8s/applications/k6-demo/k6-nginx.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: nginx-service 5 | namespace: default 6 | labels: 7 | app: nginx 8 | spec: 9 | type: NodePort 10 | ports: 11 | - port: 80 12 | protocol: TCP 13 | selector: 14 | app: nginx 15 | --- 16 | apiVersion: apps/v1 17 | kind: Deployment 18 | metadata: 19 | name: nginx-deployment 20 | namespace: default 21 | labels: 22 | app: nginx 23 | spec: 24 | selector: 25 | matchLabels: 26 | app: nginx 27 | replicas: 2 28 | template: 29 | metadata: 30 | labels: 31 | app: nginx 32 | spec: 33 | containers: 34 | - name: nginx 35 | image: nginx:1.19.4-alpine 36 | ports: 37 | - containerPort: 80 -------------------------------------------------------------------------------- /k8s/applications/k6-demo/k6-test-nginx-gitops.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tests.testkube.io/v3 2 | kind: Test 3 | metadata: 4 | name: k6-test-nginx-gitops 5 | namespace: testkube 6 | generation: 1 7 | spec: 8 | content: 9 | data: | 10 | import {check, sleep, group} from "k6"; 11 | import http from "k6/http"; 12 | 13 | export let options = { 14 | vus: 10, 15 | duration: '30s', 16 | batchPerHost: 4, 17 | insecureSkipTLSVerify: true, 18 | thresholds: { 19 | 'http_req_duration{kind:html}': ['avg<=250', 'p(95)<500'], 20 | } 21 | }; 22 | 23 | export default function () { 24 | group("static", function () { 25 | check(http.get(`http://${__ENV.TARGET_HOSTNAME}:80`, { 26 | tags: {'kind': 'html'}, 27 | }), { 28 | "status is 200": (res) => res.status === 200, 29 | }); 30 | }); 31 | sleep(1); 32 | } 33 | type: string 34 | executionRequest: 35 | variables: 36 | TARGET_HOSTNAME: 37 | name: TARGET_HOSTNAME 38 | type: basic 39 | value: nginx-service.default.svc.cluster.local 40 | valueFrom: {} 41 | type: k6/script -------------------------------------------------------------------------------- /k8s/applications/k6-demo/k6-test-nginx.yaml: -------------------------------------------------------------------------------- 1 | kind: TestWorkflow 2 | apiVersion: testworkflows.testkube.io/v1 3 | metadata: 4 | name: k6-test-nginx 5 | namespace: testkube 6 | spec: 7 | events: 8 | - cronjob: 9 | cron: 0 * * * * 10 | content: 11 | files: 12 | - path: k6.js 13 | content: >- 14 | import {check, sleep, group} from "k6"; 15 | import http from "k6/http"; 16 | 17 | export let options = { 18 | vus: 10, 19 | duration: '30s', 20 | batchPerHost: 4, 21 | insecureSkipTLSVerify: true, 22 | thresholds: { 23 | 'http_req_duration{kind:html}': ['avg<=250', 'p(95)<500'], 24 | } 25 | }; 26 | 27 | export default function () { 28 | group("static", function () { 29 | check(http.get(`http://${__ENV.TARGET_HOSTNAME}:80`, { 30 | tags: {'kind': 'html'}, 31 | }), { 32 | "status is 200": (res) => res.status === 200, 33 | }); 34 | }); 35 | sleep(1); 36 | } 37 | use: 38 | - name: official/k6/v1 39 | config: {} 40 | container: 41 | workingDir: /data 42 | env: 43 | - name: TARGET_HOSTNAME 44 | value: nginx-service.default.svc.cluster.local 45 | -------------------------------------------------------------------------------- /k8s/applications/k6-demo/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - k6-nginx.yaml 5 | - k6-test-nginx-gitops.yaml 6 | -------------------------------------------------------------------------------- /k8s/applications/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - gradle-demo/ 5 | - k6-demo/ 6 | - testsuite.yaml 7 | - testtrigger.yaml 8 | -------------------------------------------------------------------------------- /k8s/applications/maven-demo/maven-test.yaml: -------------------------------------------------------------------------------- 1 | kind: TestWorkflow 2 | apiVersion: testworkflows.testkube.io/v1 3 | metadata: 4 | name: maven-test 5 | labels: 6 | docs: example 7 | testkube.io/name: Maven 8 | testkube.io/group: demo 9 | spec: 10 | content: 11 | git: 12 | uri: https://github.com/lreimer/hands-on-testkube.git 13 | use: 14 | - name: official/maven/v1 15 | config: {} 16 | container: 17 | workingDir: /data/repo 18 | job: 19 | activeDeadlineSeconds: 60 20 | steps: 21 | - name: Saving artifacts 22 | condition: always 23 | artifacts: 24 | paths: 25 | - target/surefire-reports/** 26 | -------------------------------------------------------------------------------- /k8s/applications/testsuite.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: testworkflows.testkube.io/v1 2 | kind: TestWorkflow 3 | metadata: 4 | name: demo-testsuite 5 | labels: 6 | docs: example 7 | spec: 8 | steps: 9 | - execute: 10 | workflows: 11 | - name: gradle-test 12 | - delay: 1000ms 13 | - execute: 14 | workflows: 15 | - name: k6-sample 16 | - name: postman-sample -------------------------------------------------------------------------------- /k8s/applications/testtrigger.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tests.testkube.io/v1 2 | kind: TestTrigger 3 | metadata: 4 | name: testtrigger-gitops 5 | namespace: testkube 6 | spec: 7 | resource: deployment 8 | resourceSelector: 9 | namespace: default 10 | labelSelector: 11 | matchLabels: 12 | app: nginx 13 | event: modified 14 | action: run 15 | execution: testsuite 16 | testSelector: 17 | name: hands-on-testkube-gitops 18 | namespace: testkube -------------------------------------------------------------------------------- /k8s/applications/zap-demo/zap-api-test.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tests.testkube.io/v3 2 | kind: Test 3 | metadata: 4 | name: zap-api-test 5 | namespace: testkube 6 | spec: 7 | type: zap/api 8 | content: 9 | data: | 10 | api: 11 | # -t the target API definition 12 | target: http://hands-on-testkube.default.svc.cluster.local:8080/openapi/ 13 | # -f the API format, openapi, soap, or graphql 14 | format: openapi 15 | # -O the hostname to override in the (remote) OpenAPI spec 16 | hostname: http://hands-on-testkube.default.svc.cluster.local:8080/ 17 | # -S safe mode this will skip the active scan and perform a baseline scan 18 | safe: true 19 | # -d show debug messages 20 | debug: false 21 | # -s short output 22 | short: false 23 | # -l minimum level to show: PASS, IGNORE, INFO, WARN or FAIL 24 | level: INFO 25 | # delay in seconds to wait for passive scanning 26 | delay: 5 27 | # max time in minutes to wait for ZAP to start and the passive scan to run 28 | time: 60 29 | # -I should ZAP fail on warnings 30 | fail_on_warn: false 31 | type: string 32 | executionRequest: {} -------------------------------------------------------------------------------- /k8s/applications/zap-demo/zap-api-testtrigger.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tests.testkube.io/v1 2 | kind: TestTrigger 3 | metadata: 4 | name: zap-api-testtrigger 5 | namespace: testkube 6 | spec: 7 | resource: deployment 8 | resourceSelector: 9 | namespace: default 10 | name: hands-on-testkube 11 | event: modified 12 | action: run 13 | execution: test 14 | testSelector: 15 | name: zap-api-test 16 | namespace: testkube -------------------------------------------------------------------------------- /k8s/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | commonLabels: 5 | app: hands-on-testkube 6 | framework: spring 7 | 8 | buildMetadata: [managedByLabel] 9 | 10 | resources: 11 | - microservice-deployment.yaml 12 | - microservice-service.yaml 13 | 14 | configMapGenerator: 15 | - name: application-configmap 16 | literals: 17 | - INFO_DEVELOPER=lreimer 18 | - INFO_GIT_URL=https://github.com/lreimer/hands-on-testkube 19 | -------------------------------------------------------------------------------- /k8s/base/microservice-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hands-on-testkube 5 | labels: 6 | app: hands-on-testkube 7 | type: microservice 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: hands-on-testkube 13 | template: 14 | metadata: 15 | labels: 16 | app: hands-on-testkube 17 | spec: 18 | containers: 19 | - name: hands-on-testkube 20 | image: hands-on-testkube 21 | resources: 22 | requests: 23 | memory: "256Mi" 24 | cpu: "0.5" 25 | limits: 26 | memory: "512Mi" 27 | cpu: "2" 28 | ports: 29 | - name: http 30 | containerPort: 8080 31 | - name: management 32 | containerPort: 8081 33 | envFrom: 34 | - configMapRef: 35 | name: application-configmap 36 | -------------------------------------------------------------------------------- /k8s/base/microservice-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: hands-on-testkube 5 | labels: 6 | app: hands-on-testkube 7 | type: microservice 8 | spec: 9 | selector: 10 | app: hands-on-testkube 11 | type: ClusterIP 12 | sessionAffinity: None 13 | ports: 14 | - name: http 15 | protocol: TCP 16 | port: 8080 17 | targetPort: http 18 | - name: management 19 | protocol: TCP 20 | port: 8081 21 | targetPort: management 22 | -------------------------------------------------------------------------------- /k8s/clusters/testkube-cluster/flux-system/applications-sync.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.toolkit.fluxcd.io/v1beta1 2 | kind: Kustomization 3 | metadata: 4 | name: applications 5 | namespace: flux-system 6 | spec: 7 | interval: 5m0s 8 | dependsOn: 9 | - name: microservice 10 | sourceRef: 11 | kind: GitRepository 12 | name: flux-system 13 | path: ./k8s/applications 14 | prune: true 15 | validation: client 16 | -------------------------------------------------------------------------------- /k8s/clusters/testkube-cluster/flux-system/gotk-sync.yaml: -------------------------------------------------------------------------------- 1 | # This manifest was generated by flux. DO NOT EDIT. 2 | --- 3 | apiVersion: source.toolkit.fluxcd.io/v1 4 | kind: GitRepository 5 | metadata: 6 | name: flux-system 7 | namespace: flux-system 8 | spec: 9 | interval: 1m0s 10 | ref: 11 | branch: main 12 | secretRef: 13 | name: flux-system 14 | url: ssh://git@github.com/lreimer/hands-on-testkube 15 | --- 16 | apiVersion: kustomize.toolkit.fluxcd.io/v1 17 | kind: Kustomization 18 | metadata: 19 | name: flux-system 20 | namespace: flux-system 21 | spec: 22 | interval: 10m0s 23 | path: ./k8s/clusters/testkube-cluster 24 | prune: true 25 | sourceRef: 26 | kind: GitRepository 27 | name: flux-system 28 | -------------------------------------------------------------------------------- /k8s/clusters/testkube-cluster/flux-system/infrastructure-sync.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.toolkit.fluxcd.io/v1beta1 2 | kind: Kustomization 3 | metadata: 4 | name: infrastructure 5 | namespace: flux-system 6 | spec: 7 | interval: 5m0s 8 | sourceRef: 9 | kind: GitRepository 10 | name: flux-system 11 | path: ./k8s/infrastructure 12 | prune: true -------------------------------------------------------------------------------- /k8s/clusters/testkube-cluster/flux-system/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - gotk-components.yaml 5 | - gotk-sync.yaml 6 | - microservice-sync.yaml 7 | #- infrastructure-sync.yaml 8 | #- applications-sync.yaml 9 | -------------------------------------------------------------------------------- /k8s/clusters/testkube-cluster/flux-system/microservice-sync.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.toolkit.fluxcd.io/v1beta1 2 | kind: Kustomization 3 | metadata: 4 | name: microservice 5 | namespace: flux-system 6 | spec: 7 | interval: 5m0s 8 | #dependsOn: 9 | # - name: infrastructure 10 | sourceRef: 11 | kind: GitRepository 12 | name: flux-system 13 | path: ./k8s/overlays/int 14 | prune: true 15 | -------------------------------------------------------------------------------- /k8s/infrastructure/dashboard/dashboard-adminuser.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: admin-user 6 | namespace: kubernetes-dashboard 7 | --- 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRoleBinding 10 | metadata: 11 | name: admin-user 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: cluster-admin 16 | subjects: 17 | - kind: ServiceAccount 18 | name: admin-user 19 | namespace: kubernetes-dashboard -------------------------------------------------------------------------------- /k8s/infrastructure/dashboard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml 5 | - dashboard-adminuser.yaml -------------------------------------------------------------------------------- /k8s/infrastructure/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - dashboard/ 5 | - testkube/ -------------------------------------------------------------------------------- /k8s/infrastructure/testkube/karate-executor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: executor.testkube.io/v1 2 | kind: Executor 3 | metadata: 4 | name: karate-executor 5 | namespace: testkube 6 | spec: 7 | executor_type: job 8 | image: lreimer/testkube-karate-executor:latest 9 | types: 10 | - karate/feature 11 | - karate/project 12 | features: 13 | - artifacts -------------------------------------------------------------------------------- /k8s/infrastructure/testkube/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - namespace.yaml 5 | - testkube-source.yaml 6 | - testkube-release.yaml 7 | # - zap-executor.yaml 8 | # - karate-executor.yaml -------------------------------------------------------------------------------- /k8s/infrastructure/testkube/namespace.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: testkube -------------------------------------------------------------------------------- /k8s/infrastructure/testkube/testkube-release.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: helm.toolkit.fluxcd.io/v2beta1 2 | kind: HelmRelease 3 | metadata: 4 | name: testkube 5 | namespace: flux-system 6 | spec: 7 | interval: 5m0s 8 | releaseName: testkube 9 | targetNamespace: testkube 10 | chart: 11 | spec: 12 | chart: testkube 13 | sourceRef: 14 | kind: HelmRepository 15 | name: testkube 16 | namespace: flux-system 17 | version: "1.9.26" 18 | # values: 19 | # testkube-dashboard: 20 | # enabled: "true" 21 | # ingress: 22 | # enabled: "true" 23 | # testkube-api: 24 | # ingress: 25 | # enabled: "true" 26 | 27 | -------------------------------------------------------------------------------- /k8s/infrastructure/testkube/testkube-source.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: source.toolkit.fluxcd.io/v1beta1 2 | kind: HelmRepository 3 | metadata: 4 | name: testkube 5 | namespace: flux-system 6 | spec: 7 | interval: 5m 8 | url: https://kubeshop.github.io/helm-charts -------------------------------------------------------------------------------- /k8s/infrastructure/testkube/zap-executor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: executor.testkube.io/v1 2 | kind: Executor 3 | metadata: 4 | name: zap-executor 5 | namespace: testkube 6 | spec: 7 | executor_type: job 8 | image: lreimer/testkube-zap-executor:latest 9 | types: 10 | - zap/api 11 | - zap/baseline 12 | - zap/full 13 | features: 14 | - artifacts -------------------------------------------------------------------------------- /k8s/overlays/dev/hoverfly-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | type: hoverfly 6 | name: hoverfly 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | type: hoverfly 12 | template: 13 | metadata: 14 | labels: 15 | type: hoverfly 16 | spec: 17 | containers: 18 | - name: hoverfly 19 | image: spectolabs/hoverfly:v1.3.2 20 | ports: 21 | - containerPort: 8500 22 | - containerPort: 8888 23 | -------------------------------------------------------------------------------- /k8s/overlays/dev/hoverfly-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | type: hoverfly 6 | name: hoverfly 7 | spec: 8 | ports: 9 | - name: proxy 10 | port: 8500 11 | targetPort: 8500 12 | - name: webserver 13 | port: 8888 14 | targetPort: 8888 15 | selector: 16 | type: hoverfly 17 | -------------------------------------------------------------------------------- /k8s/overlays/dev/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: default 5 | 6 | commonLabels: 7 | env: dev 8 | 9 | resources: 10 | # you can also specify a Git repo URL here 11 | - ../../base/ 12 | - hoverfly-deployment.yaml 13 | - hoverfly-service.yaml 14 | 15 | configMapGenerator: 16 | - name: application-configmap 17 | behavior: merge 18 | literals: 19 | - INFO_APP_ENV=dev 20 | -------------------------------------------------------------------------------- /k8s/overlays/int/2-replicas.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hands-on-testkube 5 | spec: 6 | replicas: 2 7 | -------------------------------------------------------------------------------- /k8s/overlays/int/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: default 5 | 6 | commonLabels: 7 | env: int 8 | 9 | resources: 10 | # you can also specify a Git repo URL here 11 | - ../../base/ 12 | 13 | images: 14 | - name: hands-on-testkube 15 | newName: ghcr.io/lreimer/hands-on-testkube 16 | newTag: 1.0.0 17 | 18 | configMapGenerator: 19 | - name: application-configmap 20 | behavior: merge 21 | literals: 22 | - INFO_APP_ENV=int 23 | 24 | patchesStrategicMerge: 25 | - 2-replicas.yaml 26 | 27 | patchesJson6902: 28 | - target: 29 | version: v1 30 | kind: Service 31 | name: hands-on-testkube 32 | path: loadbalancer.yaml 33 | -------------------------------------------------------------------------------- /k8s/overlays/int/loadbalancer.yaml: -------------------------------------------------------------------------------- 1 | - op: replace 2 | path: /spec/type 3 | value: LoadBalancer 4 | -------------------------------------------------------------------------------- /k8s/zap/zap-cronjob.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cronjob.batch/v1beta1 2 | kind: CronJob 3 | metadata: 4 | name: zap-cronjob 5 | namespace: zap 6 | spec: 7 | schedule: "*/2 * * * *" # every 2 minutes 8 | jobTemplate: 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: "zap" 14 | image: "owasp/zap2docker-stable:2.10.0" 15 | args: [ 16 | "zap-api-scan.py", 17 | "-t", "http://hands-on-testkube.default.svc.cluster.local:8080/openapi/", 18 | "-f", "openapi", 19 | "-l", "INFO", 20 | "-I" 21 | ] 22 | restartPolicy: "Never" -------------------------------------------------------------------------------- /k8s/zap/zap-daemon.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: zap-daemon 5 | namespace: zap 6 | spec: 7 | type: NodePort 8 | ports: 9 | - port: 9080 10 | targetPort: 9080 11 | protocol: TCP 12 | selector: 13 | app: zap 14 | mode: daemon 15 | --- 16 | apiVersion: v1 17 | kind: Pod 18 | metadata: 19 | name: zap-daemon 20 | namespace: zap 21 | labels: 22 | app: zap 23 | mode: daemon 24 | spec: 25 | containers: 26 | - name: zap-daemon 27 | image: owasp/zap2docker-stable:2.10.0 28 | args: ['zap.sh', '-daemon', 29 | '-port', '9080', 30 | '-host', '0.0.0.0', 31 | '-config', 'api.addrs.addr.name=.*', 32 | '-config', 'api.addrs.addr.regex=true', 33 | '-config', 'api.key=1qay2wsx3edc'] 34 | ports: 35 | - containerPort: 9080 36 | resources: 37 | limits: 38 | cpu: "2" 39 | memory: "1024Mi" 40 | requests: 41 | cpu: "0.5" 42 | memory: "256Mi" 43 | -------------------------------------------------------------------------------- /k8s/zap/zap-namespace.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: zap -------------------------------------------------------------------------------- /k8s/zap/zap-webswing.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: zap-webswing 5 | namespace: zap 6 | spec: 7 | type: NodePort 8 | ports: 9 | - name: http 10 | port: 8080 11 | targetPort: 8080 12 | protocol: TCP 13 | - name: api 14 | port: 8090 15 | targetPort: 8090 16 | protocol: TCP 17 | selector: 18 | app: zap 19 | mode: webswing 20 | --- 21 | apiVersion: v1 22 | kind: Pod 23 | metadata: 24 | name: zap-webswing 25 | namespace: zap 26 | labels: 27 | app: zap 28 | mode: webswing 29 | spec: 30 | containers: 31 | - name: zap-webswing 32 | image: owasp/zap2docker-stable:2.12.0 33 | args: ['zap-webswing.sh'] 34 | ports: 35 | - containerPort: 8080 36 | - containerPort: 8090 37 | resources: 38 | limits: 39 | cpu: "2" 40 | memory: "1024Mi" 41 | requests: 42 | cpu: "0.5" 43 | memory: "256Mi" 44 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 4.0.0 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 2.7.4 11 | 12 | 13 | 14 | hands.on.testkube 15 | hands-on-testkube 16 | 1.0.0 17 | 18 | 19 | 11 20 | 5.7.2 21 | UTF-8 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-actuator 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-test 37 | test 38 | 39 | 40 | 41 | 42 | 43 | org.junit.jupiter 44 | junit-jupiter-engine 45 | ${junit-jupiter.version} 46 | test 47 | 48 | 49 | 50 | io.rest-assured 51 | rest-assured 52 | 4.5.1 53 | test 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-maven-plugin 63 | 64 | 65 | 66 | 67 | org.apache.maven.plugins 68 | maven-surefire-plugin 69 | 3.0.0-M5 70 | 71 | 72 | 73 | 74 | org.apache.maven.plugins 75 | maven-failsafe-plugin 76 | 3.0.0-M5 77 | 78 | 79 | 80 | integration-test 81 | verify 82 | 83 | 84 | 85 | 86 | 87 | 88 | org.apache.maven.plugins 89 | maven-compiler-plugin 90 | 3.8.1 91 | 92 | 11 93 | 11 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'hands-on-testkube' 2 | -------------------------------------------------------------------------------- /src/intTest/java/hands/on/testkube/RemoteTestkubeTest.java: -------------------------------------------------------------------------------- 1 | package hands.on.testkube; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static io.restassured.RestAssured.given; 6 | 7 | public class RemoteTestkubeTest { 8 | 9 | @Test 10 | void testRemoteTestKube() { 11 | given().baseUri("https://kubeshop.github.io").port(443) 12 | .when().get("/testkube/") 13 | .then().statusCode(200); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/k6/k6-test-microservice.js: -------------------------------------------------------------------------------- 1 | import {check, sleep, group} from "k6"; 2 | import http from "k6/http"; 3 | 4 | export let options = { 5 | vus: 10, 6 | duration: '60s', 7 | batchPerHost: 4, 8 | insecureSkipTLSVerify: true, 9 | thresholds: { 10 | 'http_req_duration{kind:html}': ['avg<=250', 'p(95)<500'], 11 | } 12 | }; 13 | 14 | export default function () { 15 | group("api", function () { 16 | check(http.get(`http://${__ENV.TARGET_HOSTNAME}:8080/api/ping`, { 17 | tags: {'kind': 'html'}, 18 | }), { 19 | "status is 200": (res) => res.status === 200, 20 | }); 21 | }); 22 | sleep(1); 23 | } 24 | -------------------------------------------------------------------------------- /src/k6/k6-test-nginx.js: -------------------------------------------------------------------------------- 1 | import {check, sleep, group} from "k6"; 2 | import http from "k6/http"; 3 | 4 | export let options = { 5 | vus: 10, 6 | duration: '30s', 7 | batchPerHost: 4, 8 | insecureSkipTLSVerify: true, 9 | thresholds: { 10 | 'http_req_duration{kind:html}': ['avg<=250', 'p(95)<500'], 11 | } 12 | }; 13 | 14 | export default function () { 15 | group("static", function () { 16 | check(http.get(`http://${__ENV.TARGET_HOSTNAME}:80`, { 17 | tags: {'kind': 'html'}, 18 | }), { 19 | "status is 200": (res) => res.status === 200, 20 | }); 21 | }); 22 | sleep(1); 23 | } 24 | -------------------------------------------------------------------------------- /src/k6/k6-test-scenarios.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { sleep, check } from 'k6'; 3 | 4 | export let options = { 5 | insecureSkipTLSVerify: true, 6 | thresholds: { 7 | 'http_req_duration{kind:html}': ['avg<=250', 'p(95)<500'], 8 | 'checks{type:testkube}': ['rate>0.95'], 9 | 'checks{type:qaware}': ['rate>0.95'], 10 | }, 11 | scenarios: { 12 | testkube: { 13 | executor: 'constant-vus', 14 | exec: 'testkube', 15 | vus: 5, 16 | duration: '10s', 17 | tags: { type: 'testkube' }, 18 | }, 19 | qaware: { 20 | executor: 'per-vu-iterations', 21 | exec: 'qaware', 22 | vus: 5, 23 | iterations: 10, 24 | startTime: '5s', 25 | maxDuration: '1m', 26 | tags: { type: 'qaware' }, 27 | }, 28 | }, 29 | }; 30 | 31 | export function testkube() { 32 | check(http.get('https://kubeshop.github.io/testkube/', { 33 | tags: {'kind': 'html'}, 34 | }), { 35 | "Testkube is OK": (res) => res.status === 200, 36 | }); 37 | sleep(1); 38 | } 39 | 40 | export function qaware() { 41 | check(http.get('https://www.qaware.de/code-im-blut', { 42 | tags: {'kind': 'html'}, 43 | }), { 44 | "QAware is OK": (res) => res.status === 200, 45 | }); 46 | sleep(1); 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/karate/chuck-norris.feature: -------------------------------------------------------------------------------- 1 | Feature: Testing the Chuck Norris Joke API 2 | 3 | Background: 4 | * url 'http://api.icndb.com/jokes/' 5 | 6 | Scenario: Testing random jokes GET endpoint 7 | Given url 'http://api.icndb.com/jokes/random/' 8 | When method GET 9 | Then status 200 10 | 11 | Scenario: Testing 5 random jokes GET endpoint 12 | Given url 'http://api.icndb.com/jokes/random/3' 13 | When method GET 14 | Then status 200 15 | 16 | Scenario: Testing random jokes using path and parameter 17 | Given path 'random' 18 | And param firstName = 'Leander' 19 | And param lastName = 'Reimer' 20 | When method GET 21 | Then status 200 22 | And match response contains { type: 'success'} -------------------------------------------------------------------------------- /src/main/java/hands/on/testkube/HandsOnTestkubeApplication.java: -------------------------------------------------------------------------------- 1 | package hands.on.testkube; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class HandsOnTestkubeApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(HandsOnTestkubeApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/hands/on/testkube/HandsOnTestkubeController.java: -------------------------------------------------------------------------------- 1 | package hands.on.testkube; 2 | 3 | import org.springframework.http.MediaType; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestParam; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | @RestController 11 | @RequestMapping("/api") 12 | public class HandsOnTestkubeController { 13 | 14 | @GetMapping(value = "/echo", produces = MediaType.APPLICATION_JSON_VALUE) 15 | public ResponseEntity echo(@RequestParam(name = "message", defaultValue = "Hello Testkube.") String message) { 16 | return ResponseEntity.ok(message); 17 | } 18 | 19 | @GetMapping(value = "/ping", produces = MediaType.APPLICATION_JSON_VALUE) 20 | public ResponseEntity ping() { 21 | return ResponseEntity.ok("pong"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # /openapi endpoint custom path 2 | springdoc.api-docs.path=/openapi 3 | 4 | # port used to expose actuator 5 | management.server.port=8081 6 | management.endpoints.web.exposure.include=* 7 | management.info.env.enabled=true 8 | 9 | info.app.name=Hands-on Testkube 10 | info.app.conference=SAA 2022 11 | -------------------------------------------------------------------------------- /src/postman/postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "8af42c21-3e31-49c1-8b27-d6e60623a180", 4 | "name": "Kubeshop", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Home", 10 | "event": [ 11 | { 12 | "listen": "test", 13 | "script": { 14 | "exec": [ 15 | "pm.test(\"Body matches string\", function () {", 16 | " pm.expect(pm.response.text()).to.include(\"Accelerator\");", 17 | "});" 18 | ], 19 | "type": "text/javascript" 20 | } 21 | } 22 | ], 23 | "request": { 24 | "method": "GET", 25 | "header": [], 26 | "url": { 27 | "raw": "https://kubeshop.io/", 28 | "protocol": "https", 29 | "host": [ 30 | "kubeshop", 31 | "io" 32 | ], 33 | "path": [ 34 | "" 35 | ] 36 | } 37 | }, 38 | "response": [] 39 | }, 40 | { 41 | "name": "Team", 42 | "event": [ 43 | { 44 | "listen": "test", 45 | "script": { 46 | "exec": [ 47 | "pm.test(\"Status code is 200\", function () {", 48 | " pm.response.to.have.status(200);", 49 | "});" 50 | ], 51 | "type": "text/javascript" 52 | } 53 | } 54 | ], 55 | "request": { 56 | "method": "GET", 57 | "header": [], 58 | "url": { 59 | "raw": "https://kubeshop.io/our-team", 60 | "protocol": "https", 61 | "host": [ 62 | "kubeshop", 63 | "io" 64 | ], 65 | "path": [ 66 | "our-team" 67 | ] 68 | } 69 | }, 70 | "response": [] 71 | } 72 | ] 73 | } -------------------------------------------------------------------------------- /src/test/java/hands/on/testkube/ExampleTest.java: -------------------------------------------------------------------------------- 1 | package hands.on.testkube; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import static org.junit.jupiter.api.Assertions.*; 5 | 6 | class ExampleTest { 7 | @Test 8 | void aTestMethod() { 9 | assertTrue(true, "Test should be true"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/hands/on/testkube/RemoteTestkubeIT.java: -------------------------------------------------------------------------------- 1 | package hands.on.testkube; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static io.restassured.RestAssured.given; 6 | 7 | public class RemoteTestkubeIT { 8 | 9 | @Test 10 | void testRemoteTestKube() { 11 | given().baseUri("https://kubeshop.github.io").port(443) 12 | .when().get("/testkube/") 13 | .then().statusCode(200); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/zap/zap-api.yaml: -------------------------------------------------------------------------------- 1 | api: 2 | # -t the target API definition 3 | target: http://hands-on-testkube.default.svc.cluster.local:8080/openapi/ 4 | # -f the API format, openapi, soap, or graphql 5 | format: openapi 6 | # -O the hostname to override in the (remote) OpenAPI spec 7 | hostname: http://hands-on-testkube.default.svc.cluster.local:8080 8 | # -S safe mode this will skip the active scan and perform a baseline scan 9 | safe: true 10 | # -c config file 11 | # config: examples/zap-api.conf 12 | # -d show debug messages 13 | debug: false 14 | # -s short output 15 | short: true 16 | # -l minimum level to show: PASS, IGNORE, INFO, WARN or FAIL 17 | level: INFO 18 | # -c context file 19 | # context: examples/context.config 20 | # username to use for authenticated scans 21 | # user: anonymous 22 | 23 | # delay in seconds to wait for passive scanning 24 | delay: 5 25 | # max time in minutes to wait for ZAP to start and the passive scan to run 26 | time: 60 27 | 28 | # ZAP command line options 29 | # zap_options: -config aaa=bbb 30 | 31 | # -I should ZAP fail on warnings 32 | fail_on_warn: false -------------------------------------------------------------------------------- /src/zap/zap-baseline.yaml: -------------------------------------------------------------------------------- 1 | baseline: 2 | # -t target URL including the protocol 3 | target: https://www.example.com/ 4 | # -c -u config file or URL 5 | config: examples/zap-baseline.conf 6 | # -m the number of minutes to spider for (default 1) 7 | minutes: 3 8 | # -D delay in seconds to wait for passive scanning 9 | delay: -1 10 | # -d show debug messages 11 | debug: true 12 | # -s short output 13 | short: true 14 | # -l minimum level to show: PASS, IGNORE, INFO, WARN or FAIL 15 | level: INFO 16 | # -j use the Ajax spider in addition to the traditional one 17 | ajax: false --------------------------------------------------------------------------------