├── .devcontainer └── devcontainer.json ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ ├── feature_request.yaml │ └── trivy-results.tpl ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ └── daily-vul-scan.yml ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── api └── v1alpha1 │ ├── gatling_types.go │ ├── groupversion_info.go │ └── zz_generated.deepcopy.go ├── assets ├── gatling-html-report.png ├── gatling-operator-arch.svg ├── gatling-operator-arch.xml ├── gatling-operator-pod-before-v0.8.1.svg ├── gatling-operator-pod.svg ├── gatling-operator-pod.xml └── slack-notification.png ├── config ├── crd-ref-docs │ ├── config.yaml │ └── templates │ │ └── markdown │ │ ├── gv_details.tpl │ │ ├── gv_list.tpl │ │ ├── type.tpl │ │ └── type_members.tpl ├── crd │ ├── bases │ │ └── gatling-operator.tech.zozo.com_gatlings.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_gatlings.yaml │ │ └── webhook_in_gatlings.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ └── manager_config_patch.yaml ├── kind │ └── cluster.yaml ├── manager │ ├── controller_manager_config.yaml │ ├── kustomization.yaml │ └── manager.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── gatling_editor_role.yaml │ ├── gatling_viewer_role.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml └── samples │ ├── gatling-notification-slack-secrets.yaml │ ├── gatling-operator_v1alpha1_gatling01.yaml │ ├── gatling-operator_v1alpha1_gatling02.yaml │ ├── gatling-operator_v1alpha1_gatling03.yaml │ ├── gatling-worker-service-account.yaml │ └── kustomization.yaml ├── controllers ├── gatling_controller.go ├── gatling_controller_test.go └── suite_test.go ├── docs ├── api.md ├── architecture.md ├── build-guide.md ├── quickstart-guide.md └── user-guide.md ├── gatling ├── Dockerfile ├── conf │ ├── gatling.conf │ └── logback.xml ├── sample │ └── resources │ │ ├── goods_ids.csv │ │ └── user_ids.csv └── user-files │ ├── resources │ └── myresources.csv │ └── simulations │ ├── MyBasicSimulation.scala │ └── PersistentVolumeSampleSimulation.scala ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── main.go └── pkg ├── cloudstorages ├── azure.go ├── azure_test.go ├── cloudstorage.go ├── cloudstorage_test.go ├── gcp.go ├── gcp_test.go ├── s3.go ├── s3_test.go └── suite_test.go ├── commands ├── commands.go ├── commands_test.go └── suite_test.go ├── notificationservices ├── notificationservice.go ├── notificationservice_test.go ├── slack.go └── suite_test.go └── utils └── utils.go /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile 3 | { 4 | "name": "Gatling Operator Dev Container", 5 | "build": { 6 | // Sets the run context to one level up instead of the .devcontainer folder. 7 | "context": "..", 8 | // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. 9 | "dockerfile": "../Dockerfile.dev" 10 | }, 11 | "mounts": [ 12 | // Mount the host's Docker socket. 13 | // Official document: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker.md 14 | "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" 15 | ], 16 | "runArgs": [ 17 | "--name=gatling-operator-dev-container", 18 | "--hostname=gatling-operator-dev-container", 19 | // Set network mode to host to communicate with other containers. 20 | "--network=host" 21 | ], 22 | "containerEnv": { 23 | "IN_DEV_CONTAINER": "true" 24 | }, 25 | // Restore the local kubectl config to a dev container. 26 | "postStartCommand": "if [ -d ${containerWorkspaceFolder}/.kube ]; then cp -r ${containerWorkspaceFolder}/.kube $HOME/.kube; fi", 27 | "customizations": { 28 | "vscode": { 29 | "extensions": [ 30 | "streetsidesoftware.code-spell-checker", 31 | "mhutchie.git-graph" 32 | ] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore all files which are not go type 3 | !**/*.go 4 | !**/*.mod 5 | !**/*.sum 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: checkboxes 7 | id: terms 8 | attributes: 9 | label: Pre-requisites 10 | options: 11 | - label: I'd like to contribute the fix myself 12 | - type: textarea 13 | id: description 14 | attributes: 15 | label: What happened/what you expected to happen? 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: logs 20 | attributes: 21 | label: Relevant log output 22 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 23 | render: shell 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for gatling-operator 3 | title: "[Feature Request]: " 4 | labels: ["feature"] 5 | body: 6 | - type: textarea 7 | id: summary 8 | attributes: 9 | label: Summary 10 | validations: 11 | required: true 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/trivy-results.tpl: -------------------------------------------------------------------------------- 1 | {{- $severity_icon := dict "CRITICAL" "🔴" "HIGH" "🟠" "MEDIUM" "🟡" "UNKNOWN" "🟤" -}} 2 | {{- $vulns_count := 0 }} 3 | 4 | {{- range . -}} 5 | ## {{ .Target }} 6 | 7 | ### {{ .Type }} [{{ .Class }}] 8 | 9 | {{ if .Vulnerabilities -}} 10 | | Title | Severity | CVE | Package Name | Installed Version | Fixed Version | PrimaryURL | 11 | | :--: | :--: | :--: | :--: | :--: | :--: | :-- | 12 | {{- range .Vulnerabilities }} 13 | | {{ .Title -}} 14 | | {{ get $severity_icon .Severity }}{{ .Severity -}} 15 | | {{ .VulnerabilityID -}} 16 | | {{ .PkgName -}} 17 | | {{ .InstalledVersion -}} 18 | | {{ .FixedVersion -}} 19 | | {{ .PrimaryURL -}} 20 | | 21 | {{- $vulns_count = add1 $vulns_count -}} 22 | {{- end }} 23 | 24 | {{ else -}} 25 | _No vulnerabilities found_ 26 | 27 | {{ end }} 28 | 29 | {{- end }} 30 | --- 31 | **Total count of vulnerabilities: {{ $vulns_count }}** 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | _Provide a description of what has been changed_ 4 | 5 | ### Checklist 6 | 7 | _Please check if applicable_ 8 | 9 | - [ ] Tests have been added (if applicable, ie. when operator codes are added or modified) 10 | - [ ] Relevant docs have been added or modified (if applicable, ie. when new features are added or current features are modified) 11 | 12 | 15 | Relevant issue # 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*.*.*" 9 | pull_request: 10 | branches: 11 | - main 12 | paths-ignore: 13 | - '**.md' 14 | - '.gitignore' 15 | workflow_dispatch: 16 | 17 | env: 18 | REGISTRY: ghcr.io 19 | IMAGE_NAME: ${{ github.repository }} 20 | 21 | jobs: 22 | test: 23 | name: Test 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Setup Go 30 | uses: actions/setup-go@v5 31 | with: 32 | go-version: '1.21' 33 | 34 | - run: go version 35 | 36 | - name: Get Go Paths 37 | id: go-paths 38 | run: | 39 | echo ::set-output name=mod_cache::$(go env GOMODCACHE) 40 | echo ::set-output name=build_cache::$(go env GOCACHE) 41 | 42 | - name: Go modules and build cache 43 | uses: actions/cache@v4 44 | with: 45 | path: | 46 | ${{ steps.go-paths.outputs.mod_cache }} 47 | ${{ steps.go-paths.outputs.build_cache }} 48 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 49 | restore-keys: | 50 | ${{ runner.os }}-go- 51 | 52 | - name: Go modules sync 53 | run: go mod tidy 54 | 55 | - name: Run Tests 56 | run: make test 57 | 58 | release: 59 | name: Build Push Release 60 | if: startsWith( github.ref, 'refs/tags/' ) 61 | runs-on: ubuntu-latest 62 | needs: test 63 | steps: 64 | - name: Checkout 65 | uses: actions/checkout@v4 66 | 67 | - name: Setup Go 68 | uses: actions/setup-go@v5 69 | with: 70 | go-version: '1.21' 71 | 72 | - name: Login to GitHub Container Registry 73 | uses: docker/login-action@v1 74 | with: 75 | registry: ${{ env.REGISTRY }} 76 | username: ${{ github.actor }} 77 | password: ${{ secrets.GITHUB_TOKEN }} 78 | 79 | - name: Get the version 80 | id: get_version 81 | run: | 82 | echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v} 83 | echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/} 84 | 85 | - name: Set up QEMU 86 | uses: docker/setup-qemu-action@v3 87 | 88 | - name: Set up Docker Buildx 89 | uses: docker/setup-buildx-action@v3 90 | 91 | - name: Create release YAML (gatling-operator.yaml) 92 | env: 93 | VERSION: ${{ steps.get_version.outputs.VERSION }} 94 | run: | 95 | IMAGE_ID=$REGISTRY/$IMAGE_NAME 96 | make manifests-release IMG=$IMAGE_ID:$VERSION 97 | 98 | - name: Publish images to the registry 99 | env: 100 | VERSION: ${{ steps.get_version.outputs.VERSION }} 101 | run: | 102 | IMAGE_ID=$REGISTRY/$IMAGE_NAME 103 | echo IMAGE_ID=$IMAGE_ID 104 | echo VERSION=$VERSION 105 | make docker-push IMG=$IMAGE_ID:$VERSION 106 | 107 | - name: Create Release 108 | uses: softprops/action-gh-release@v1 109 | with: 110 | name: ${{ steps.get_version.outputs.TAG_NAME }} 111 | draft: false 112 | prerelease: false 113 | generate_release_notes: true 114 | files: gatling-operator.yaml 115 | -------------------------------------------------------------------------------- /.github/workflows/daily-vul-scan.yml: -------------------------------------------------------------------------------- 1 | name: daily vulnerability scan 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | env: 8 | IMAGE_NAME: zozo-gatling-operator 9 | TRIVY_RESULTS_MARKDOWN: trivy-results.md 10 | 11 | permissions: 12 | contents: read 13 | issues: write 14 | 15 | jobs: 16 | build-scan-and-save-results: 17 | name: Build, scan, and save results 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup Go 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version-file: ./go.mod 27 | cache: true 28 | 29 | - name: Go modules sync 30 | run: go mod tidy 31 | 32 | - name: Set up Docker Buildx 33 | uses: docker/setup-buildx-action@v3 34 | 35 | - name: Build an image from Dockerfile 36 | run: | 37 | make docker-build IMG="${{ env.IMAGE_NAME }}:${{ github.sha }}" 38 | 39 | - name: Run Trivy vulnerability scanner 40 | uses: aquasecurity/trivy-action@0.18.0 41 | with: 42 | scan-type: image 43 | image-ref: "${{ env.IMAGE_NAME }}:${{ github.sha }}" 44 | exit-code: 0 45 | ignore-unfixed: true 46 | vuln-type: os,library 47 | severity: HIGH,CRITICAL 48 | timeout: 10m0s 49 | scanners: vuln,secret,config 50 | format: template 51 | template: "@.github/ISSUE_TEMPLATE/trivy-results.tpl" 52 | output: ${{ env.TRIVY_RESULTS_MARKDOWN }} 53 | 54 | - name: Extract total count of vulnerabilities 55 | id: extract-total-cnt-of-vulns 56 | run: | 57 | if [[ $(cat "${{ env.TRIVY_RESULTS_MARKDOWN }}") =~ Total\ count\ of\ vulnerabilities:\ ([0-9]+) ]]; then 58 | result=${BASH_REMATCH[0]} 59 | echo "$result" 60 | total_cnt_of_vulns=${BASH_REMATCH[1]} 61 | echo "total_cnt_of_vulns=$total_cnt_of_vulns" >> "$GITHUB_OUTPUT" 62 | else 63 | echo "Error: Failed to extract total count of vulnerabilities" 64 | exit 1 65 | fi 66 | 67 | - name: Insert YAML front matter into the results markdown 68 | if: ${{ fromJson(steps.extract-total-cnt-of-vulns.outputs.total_cnt_of_vulns) > 0 }} 69 | run: | 70 | sed -i '1i\ 71 | ---\ 72 | title: "Security Alert by Trivy"\ 73 | labels: "trivy, vulnerability"\ 74 | ---\ 75 | ' "${{ env.TRIVY_RESULTS_MARKDOWN }}" 76 | 77 | - name: Create or update the trivy results issue 78 | if: ${{ fromJson(steps.extract-total-cnt-of-vulns.outputs.total_cnt_of_vulns) > 0 }} 79 | uses: JasonEtco/create-an-issue@v2 80 | env: 81 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 82 | with: 83 | filename: ${{ env.TRIVY_RESULTS_MARKDOWN }} 84 | update_existing: true 85 | search_existing: open 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Kubernetes Generated files - skip generated files, except for vendored files 18 | 19 | !vendor/**/zz_generated.* 20 | .kube 21 | 22 | # editor and IDE paraphernalia 23 | .idea 24 | *.swp 25 | *.swo 26 | *~ 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "devcontainer" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.21-bullseye as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go mod download 11 | 12 | # Copy the go source 13 | COPY main.go main.go 14 | COPY api/ api/ 15 | COPY controllers/ controllers/ 16 | COPY pkg/ pkg/ 17 | 18 | # Build 19 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=$(dpkg --print-architecture) go build -a -o manager main.go 20 | 21 | # Use distroless as minimal base image to package the manager binary 22 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 23 | FROM gcr.io/distroless/static:nonroot 24 | WORKDIR / 25 | COPY --from=builder /workspace/manager . 26 | USER 65532:65532 27 | 28 | ENTRYPOINT ["/manager"] 29 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | ENV GO_VERSION 1.21.7 4 | ENV KUBECTL_VERSION v1.21.10 5 | 6 | # KEEP the value as arm64. 7 | # This environment variable is for arm64, but should be left as is for any architecture 8 | # References: 9 | # https://github.com/etcd-io/etcd/issues/10677 10 | # https://github.com/k0sproject/k0s/issues/424 11 | ENV ETCD_UNSUPPORTED_ARCH arm64 12 | 13 | # Development tools 14 | RUN apt-get update && apt-get upgrade -y && apt-get install -y \ 15 | wget \ 16 | git \ 17 | make \ 18 | gcc 19 | 20 | # Docker CLI 21 | # Referring to https://docs.docker.com/engine/install/ubuntu/#installation-methods 22 | RUN apt-get install -y \ 23 | ca-certificates \ 24 | curl \ 25 | gnupg \ 26 | && install -m 0755 -d /etc/apt/keyrings \ 27 | && curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ 28 | | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ 29 | && chmod a+r /etc/apt/keyrings/docker.gpg \ 30 | && echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ 31 | "$(. /etc/os-release && echo "${VERSION_CODENAME}")" stable" \ 32 | | tee /etc/apt/sources.list.d/docker.list > /dev/null \ 33 | && apt-get update && apt-get install -y \ 34 | docker-ce-cli 35 | 36 | # kubectl 37 | # Referring to https://kubernetes.io/ja/docs/tasks/tools/install-kubectl/ 38 | RUN curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/"$(dpkg --print-architecture)"/kubectl" \ 39 | && chmod +x ./kubectl \ 40 | && mv ./kubectl /usr/local/bin/kubectl 41 | 42 | # golang 43 | # Referring to https://go.dev/doc/install 44 | RUN wget "https://go.dev/dl/go${GO_VERSION}.linux-"$(dpkg --print-architecture)".tar.gz" \ 45 | && tar -C /usr/local -xzf "go${GO_VERSION}.linux-"$(dpkg --print-architecture)".tar.gz" 46 | ENV PATH "${PATH}:/usr/local/go/bin" 47 | ENV PATH "${PATH}:/root/go/bin" 48 | 49 | # kind 50 | # References: 51 | # https://github.com/kind-ci/examples/blob/master/.github/workflows/kind.yml 52 | # https://kind.sigs.k8s.io/docs/user/resources/ 53 | RUN GO111MODULE=on go install sigs.k8s.io/kind@latest 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © ZOZO, Inc. 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 | 2 | # Image URL to use all building/pushing image targets 3 | IMAGE_TAG := $(shell /bin/date "+%Y%m%d-%H%M%S") 4 | # Image URL should be like this when it gets to open sourced: ghcr.io/st-tech/gatling-operator:$(IMAGE_TAG) 5 | IMG ?= gatling-operator:$(IMAGE_TAG) 6 | # Image URL should be like this when it gets to open sourced: ghcr.io/st-tech/gatling:$(IMAGE_TAG) 7 | SAMPLE_IMG := gatling:$(IMAGE_TAG) 8 | # Release version 9 | VERSION := latest 10 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 11 | CRD_OPTIONS ?= "crd" 12 | KIND_CLUSTER_NAME ?= "gatling-cluster" 13 | K8S_NODE_IMAGE ?= v1.32.0 14 | ENVTEST_K8S_VERSION ?= 1.30.0 15 | 16 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 17 | ifeq (,$(shell go env GOBIN)) 18 | GOBIN=$(shell go env GOPATH)/bin 19 | else 20 | GOBIN=$(shell go env GOBIN) 21 | endif 22 | 23 | # Setting SHELL to bash allows bash commands to be executed by recipes. 24 | # This is a requirement for 'setup-envtest.sh' in the test target. 25 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 26 | SHELL = /usr/bin/env bash -o pipefail 27 | .SHELLFLAGS = -ec 28 | 29 | KIND_CLUSTER_CONFIG_DIR=$(shell pwd)/config/kind 30 | KUBECONFIG_BACKUP_DIR=$(shell pwd)/.kube 31 | 32 | all: build 33 | 34 | ##@ General 35 | 36 | # The help target prints out all targets with their descriptions organized 37 | # beneath their categories. The categories are represented by '##@' and the 38 | # target descriptions by '##'. The awk commands is responsible for reading the 39 | # entire set of makefiles included in this invocation, looking for lines of the 40 | # file as xyz: ## something, and then pretty-format the target and help. Then, 41 | # if there's a line with ##@ something, that gets pretty-printed as a category. 42 | # More info on the usage of ANSI control characters for terminal formatting: 43 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 44 | # More info on the awk command: 45 | # http://linuxcommand.org/lc3_adv_awk.php 46 | 47 | help: ## Display this help. 48 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 49 | 50 | kind-create: ## Create a kind cluster named ${KIND_CLUSTER_NAME} locally if necessary and save the kubectl config. 51 | ifeq (1, $(shell kind get clusters | grep ${KIND_CLUSTER_NAME} | wc -l | tr -d ' ')) 52 | @echo "Cluster already exists" 53 | else 54 | @echo "Creating Cluster" 55 | kind create cluster --name ${KIND_CLUSTER_NAME} --image=kindest/node:${K8S_NODE_IMAGE} --config ${KIND_CLUSTER_CONFIG_DIR}/cluster.yaml 56 | ifeq ($(IN_DEV_CONTAINER), true) 57 | @echo "kubeconfig backup =>" 58 | mkdir -p ${KUBECONFIG_BACKUP_DIR} && kind get kubeconfig --name ${KIND_CLUSTER_NAME} > ${KUBECONFIG_BACKUP_DIR}/kind-conifg.yaml 59 | endif 60 | endif 61 | 62 | ##@ Development 63 | 64 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 65 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases 66 | 67 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 68 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 69 | 70 | manifests-release: manifests kustomize ## Generate all-in-one manifest for release 71 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 72 | $(KUSTOMIZE) build config/default > gatling-operator.yaml 73 | 74 | docs: crd-ref-docs ## Generate API reference documentation from CRD types 75 | cd config/crd-ref-docs 76 | $(CRD_REF_DOCS) --source-path=api --config=config/crd-ref-docs/config.yaml --renderer=markdown --templates-dir=config/crd-ref-docs/templates/markdown --output-path=docs/api.md 77 | 78 | fmt: ## Run go fmt against code. 79 | go fmt ./... 80 | 81 | vet: ## Run go vet against code. 82 | go vet ./... 83 | 84 | test: manifests generate fmt vet setup-envtest ## Run tests. 85 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use -p path $(ENVTEST_K8S_VERSION))" go test ./... -coverprofile cover.out 86 | 87 | ##@ Build 88 | 89 | build: generate fmt vet ## Build manager binary. 90 | go build -o bin/manager main.go 91 | 92 | run: manifests generate fmt vet ## Run a controller from your host. 93 | go run ./main.go 94 | 95 | docker-build: test ## Build docker image with the manager. 96 | docker build -t ${IMG} . 97 | 98 | docker-push: test ## Push docker image with the manager. 99 | docker buildx build --platform linux/amd64,linux/arm64 -t ${IMG} . --push 100 | 101 | kind-load-image: kind-create docker-build ## Load local docker image into the kind cluster 102 | @echo "Loading image into kind" 103 | kind load docker-image ${IMG} --name ${KIND_CLUSTER_NAME} -v 1 104 | 105 | kind-load-sample-image: kind-create sample-docker-build ## Load local docker image for sample Gatling into the kind cluster 106 | @echo "Loading sample image into kind" 107 | kind load docker-image ${SAMPLE_IMG} --name ${KIND_CLUSTER_NAME} -v 1 108 | 109 | sample-docker-build: ## Build docker image for sample Gatling 110 | cd gatling && docker build -t ${SAMPLE_IMG} . 111 | 112 | sample-docker-push: sample-docker-build ## Push docker image for sample Gatling 113 | docker push ${SAMPLE_IMG} 114 | 115 | ##@ Deployment 116 | 117 | install-crd: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 118 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 119 | 120 | uninstall-crd: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. 121 | $(KUSTOMIZE) build config/crd | kubectl delete -f - 122 | 123 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 124 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 125 | $(KUSTOMIZE) build config/default | kubectl apply -f - 126 | 127 | kind-deploy: kind-load-image deploy ## Deploy controller to the kind cluster specified in ~/.kube/config. 128 | 129 | sample-deploy: kustomize ## Install sample Gatling CR into the k8s cluster specified in ~/.kube/config. 130 | $(KUSTOMIZE) build config/samples | sed -e "s,^\([[:space:]]*gatlingImage: \).*,\1${SAMPLE_IMG},g" | kubectl apply -f - 131 | 132 | kind-sample-deploy: kind-load-sample-image sample-deploy ## Install sample Gatling CR into the kind cluster specified in ~/.kube/config. 133 | 134 | undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. 135 | $(KUSTOMIZE) build config/default | kubectl delete -f - 136 | 137 | CONTROLLER_GEN = $(shell pwd)/bin/controller-gen 138 | controller-gen: ## Download controller-gen locally if necessary. 139 | $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.14.0) 140 | 141 | KUSTOMIZE = $(shell pwd)/bin/kustomize 142 | kustomize: ## Download kustomize locally if necessary. 143 | $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5@v5.5.0) 144 | 145 | CRD_REF_DOCS = $(shell pwd)/bin/crd-ref-docs 146 | crd-ref-docs: ## Download crd-ref-docs locally if necessary. 147 | $(call go-install-tool,$(CRD_REF_DOCS),github.com/elastic/crd-ref-docs@master) 148 | 149 | ENVTEST = $(shell pwd)/bin/setup-envtest 150 | setup-envtest: ## Download setup-envtest locally if necessary. 151 | $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest) 152 | 153 | # go-install-tool will 'go get' any package $2 and install it to $1. 154 | PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) 155 | define go-install-tool 156 | @[ -f $(1) ] || { \ 157 | set -e ;\ 158 | TMP_DIR=$$(mktemp -d) ;\ 159 | cd $$TMP_DIR ;\ 160 | go mod init tmp ;\ 161 | echo "Downloading $(2)" ;\ 162 | GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\ 163 | rm -rf $$TMP_DIR ;\ 164 | } 165 | endef 166 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: tech.zozo.com 2 | layout: 3 | - go.kubebuilder.io/v3 4 | projectName: gatling-operator 5 | repo: github.com/st-tech/gatling-operator 6 | resources: 7 | - api: 8 | crdVersion: v1 9 | namespaced: true 10 | controller: true 11 | domain: tech.zozo.com 12 | group: gatling-operator 13 | kind: Gatling 14 | path: github.com/st-tech/gatling-operator/api/v1alpha1 15 | version: v1alpha1 16 | version: "3" 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gatling Operator 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/st-tech/gatling-operator)](https://goreportcard.com/report/github.com/st-tech/gatling-operator) [![CI](https://github.com/st-tech/gatling-operator/actions/workflows/ci.yml/badge.svg?branch=main&event=push)](https://github.com/st-tech/gatling-operator/actions/workflows/ci.yml) [![daily vulnerability scan](https://github.com/st-tech/gatling-operator/actions/workflows/daily-vul-scan.yml/badge.svg?branch=main)](https://github.com/st-tech/gatling-operator/actions/workflows/daily-vul-scan.yml) ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/st-tech/gatling-operator) 4 | 5 | [Gatling](https://gatling.io/) is an open source load testing tool that allows to analyze and measure the performance of a variety of services. [Gatling Operator](https://github.com/st-tech/gatling-operator) is a Kubernetes Operator for running automated distributed Gatling load testing. 6 | 7 | ## How Gatling Operator works 8 | 9 | The desired state of a distributed Gatling load testing is described through a Kubernetes custom resource (`Gatling CR` in figure below). Based on Gatling CR, actions in the lifecycle of distributed Gatling load testing such as running load testing, generating reports, sending notification message, and cleaning up the resources are executed by relevant custom controller (`Gatling Controller` in figure below). 10 | 11 | ![](assets/gatling-operator-arch.svg) 12 | 13 | ## Features 14 | 15 | - Allows Gatling load testing scenario, resources, Gatling configurations files to be added in 3 ways: 16 | - Bundle them with Gatling runtime packages in a Gatling container 17 | - Run the simulations through build tool plugin (e.g. `gradle gatlingRun`) in a Docker container 18 | - Add them as multi-line definition in Gatling CR 19 | - Scaling Gatling load testing 20 | - Horizontal scaling: number of pods running in parallel during a load testing can be configured 21 | - Vertical scaling: CPU and RAM resource allocation for Gatling runner Pod can be configured 22 | - Allows Gatling load testing to start running at a specific time 23 | - By default, the Gatling load testing starts running as soon as the runner Pod's init container gets ready. By specifying the start time, the Gatling load testing waits to start running until the specified time 24 | - Gatling Pod attributions 25 | - Gatling runtime container image 26 | - [rclone](https://rclone.org/) container image 27 | - CPU and RAM resource allocation request and limit 28 | - `Affinity` (such as Node affinity) and `Tolerations` to be used by the scheduler to decide where a pod can be placed in the cluster 29 | - `Service accounts` for Pods 30 | - Reports 31 | - Automated generating aggregated Gatling HTML reports and storing them to Cloud Storages such as `Amazon S3`, `Google Cloud Storage`, `Azure Blob Storage`. via [rclone](https://rclone.org/) 32 | - Allows credentials info for accessing the remote storage to be specified via Secret resource 33 | - Notification 34 | - Automated posting webhook message and sending Gatling load testing result via notification providers such as `Slack` 35 | - Allows webhook URL info to be specified via Secret resource 36 | - Automated cleaning up Gatling resources 37 | 38 | ## Requirements 39 | 40 | - Kubernetes: version >= 1.18 41 | 42 | > note: the versions below 1.18 might work but are not tested 43 | 44 | ## Quick Start 45 | 46 | - [Quick Start Guide](docs/quickstart-guide.md) 47 | 48 | ## Documentations 49 | 50 | - [Architecture](docs/architecture.md) 51 | - [Gatling CRD Reference](docs/api.md) 52 | - [User Guide](docs/user-guide.md) 53 | - [How to build Gatling Operator](docs/build-guide.md) 54 | 55 | ## Contributing 56 | 57 | Please make a GitHub issue or pull request to help us build the operator. 58 | 59 | ## Changelog 60 | 61 | Please see the [list of releases](https://github.com/st-tech/gatling-operator/releases) for information on changes between releases. 62 | -------------------------------------------------------------------------------- /api/v1alpha1/gatling_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © ZOZO, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | corev1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // GatlingSpec defines the desired state of Gatling 25 | type GatlingSpec struct { 26 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 27 | // Important: Run "make" to regenerate code after modifying this file 28 | 29 | // (Optional) The flag of generating gatling report. Defaults to `false` 30 | // +kubebuilder:default=false 31 | // +kubebuilder:validation:Optional 32 | GenerateReport bool `json:"generateReport,omitempty"` 33 | 34 | // (Optional) The flag of generating gatling report at each pod 35 | // +kubebuilder:default=false 36 | // +kubebuilder:validation:Optional 37 | GenerateLocalReport bool `json:"generateLocalReport,omitempty"` 38 | 39 | // (Optional) The flag of notifying gatling report. Defaults to `false` 40 | // +kubebuilder:default=false 41 | // +kubebuilder:validation:Optional 42 | NotifyReport bool `json:"notifyReport,omitempty"` 43 | 44 | // (Optional) The flag of cleanup gatling resources after the job done. Defaults to `false` 45 | // +kubebuilder:default=false 46 | // +kubebuilder:validation:Optional 47 | CleanupAfterJobDone bool `json:"cleanupAfterJobDone,omitempty"` 48 | 49 | // (Optional) Gatling Pod specification. 50 | // +kubebuilder:validation:Optional 51 | PodSpec PodSpec `json:"podSpec,omitempty"` 52 | 53 | // (Optional) Cloud Storage Provider specification. 54 | // +kubebuilder:validation:Optional 55 | CloudStorageSpec CloudStorageSpec `json:"cloudStorageSpec,omitempty"` 56 | 57 | // (Optional) PersistentVolume specification. 58 | // +kubebuilder:validation:Optional 59 | PersistentVolumeSpec PersistentVolumeSpec `json:"persistentVolume,omitempty"` 60 | 61 | // (Optional) PersistentVolumeClaim specification. 62 | // +kubebuilder:validation:Optional 63 | PersistentVolumeClaimSpec PersistentVolumeClaimSpec `json:"persistentVolumeClaim,omitempty"` 64 | 65 | // (Optional) Notification Service specification. 66 | // +kubebuilder:validation:Optional 67 | NotificationServiceSpec NotificationServiceSpec `json:"notificationServiceSpec,omitempty"` 68 | 69 | // (Required) Test Scenario specification 70 | // +kubebuilder:validation:Required 71 | TestScenarioSpec TestScenarioSpec `json:"testScenarioSpec"` 72 | } 73 | 74 | // PodSpec defines type to configure Gatling Pod specification. For the idea of PodSpec, refer to [bitpoke/mysql-operator](https://github.com/bitpoke/mysql-operator/blob/master/pkg/apis/mysql/v1alpha1/mysqlcluster_types.go) 75 | type PodSpec struct { 76 | // (Optional) The image that will be used for Gatling container. Defaults to `ghcr.io/st-tech/gatling:latest` 77 | // +kubebuilder:validation:Optional 78 | GatlingImage string `json:"gatlingImage,omitempty"` 79 | 80 | // (Optional) The image that will be used for rclone conatiner. Defaults to `rclone/rclone:latest` 81 | // +kubebuilder:validation:Optional 82 | RcloneImage string `json:"rcloneImage,omitempty"` 83 | 84 | // (Optional) Resources specifies the resource limits of the container. 85 | // +kubebuilder:validation:Optional 86 | Resources corev1.ResourceRequirements `json:"resources,omitempty"` 87 | 88 | // (Optional) Affinity specification. 89 | // +kubebuilder:validation:Optional 90 | Affinity corev1.Affinity `json:"affinity,omitempty"` 91 | 92 | // (Optional) Tolerations specification. 93 | // +kubebuilder:validation:Optional 94 | Tolerations []corev1.Toleration `json:"tolerations,omitempty"` 95 | 96 | // (Required) ServiceAccountName specification. 97 | // +kubebuilder:validation:Required 98 | ServiceAccountName string `json:"serviceAccountName"` 99 | 100 | // (Optional) volumes specification. 101 | // +kubebuilder:validation:Optional 102 | Volumes []corev1.Volume `json:"volumes,omitempty"` 103 | 104 | // (Optional) SecurityContext specification. 105 | // +kubebuilder:validation:Optional 106 | SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` 107 | 108 | // (Optional) RunnerContainerSecurityContext specifies the SecurityContext of the Gatling runner container. 109 | // +kubebuilder:validation:Optional 110 | RunnerContainerSecurityContext *corev1.SecurityContext `json:"runnerContainerSecurityContext,omitempty"` 111 | } 112 | 113 | // TestScenarioSpec defines the load testing scenario 114 | type TestScenarioSpec struct { 115 | // (Optional) Test Start time. 116 | // +kubebuilder:validation:Optional 117 | StartTime string `json:"startTime,omitempty"` 118 | 119 | // (Optional) Number of pods running at the same time. Defaults to `1` (Minimum `1`) 120 | // +kubebuilder:default=1 121 | // +kubebuilder:validation:Minimum=1 122 | // +kubebuilder:validation:Optional 123 | Parallelism int32 `json:"parallelism,omitempty"` 124 | 125 | // (Optional) Gatling simulation format, supports `bundle` and `gradle`. Defaults to `bundle` 126 | // +kubebuilder:validation:Optional 127 | // +kubebuilder:validation:Enum=bundle;gradle 128 | SimulationsFormat string `json:"simulationsFormat,omitempty"` 129 | 130 | // (Optional) Gatling Resources directory path where simulation files are stored. Defaults to `/opt/gatling/user-files/simulations` 131 | // +kubebuilder:validation:Optional 132 | SimulationsDirectoryPath string `json:"simulationsDirectoryPath,omitempty"` 133 | 134 | // (Optional) Gatling Simulation directory path where resources are stored. Defaults to `/opt/gatling/user-files/resources` 135 | // +kubebuilder:validation:Optional 136 | ResourcesDirectoryPath string `json:"resourcesDirectoryPath,omitempty"` 137 | 138 | // (Optional) Gatling Results directory path where results are stored. Defaults to `/opt/gatling/results` 139 | // +kubebuilder:validation:Optional 140 | ResultsDirectoryPath string `json:"resultsDirectoryPath,omitempty"` 141 | 142 | // (Required) Simulation Class Name. 143 | // +kubebuilder:validation:Required 144 | SimulationClass string `json:"simulationClass"` 145 | 146 | // (Optional) Simulation Data. 147 | // +kubebuilder:validation:Optional 148 | SimulationData map[string]string `json:"simulationData,omitempty"` 149 | 150 | // (Optional) Resource Data. 151 | // +kubebuilder:validation:Optional 152 | ResourceData map[string]string `json:"resourceData,omitempty"` 153 | 154 | // (Optional) Gatling Configurations. 155 | // +kubebuilder:validation:Optional 156 | GatlingConf map[string]string `json:"gatlingConf,omitempty"` 157 | 158 | // (Optional) Environment variables used for running load testing scenario. 159 | // +optional 160 | Env []corev1.EnvVar `json:"env,omitempty"` 161 | 162 | // (Optional) Pod volumes to mount into the container's filesystem. 163 | // +kubebuilder:validation:Optional 164 | VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` 165 | } 166 | 167 | // CloudStorageSpec defines Cloud Storage Provider specification. 168 | type CloudStorageSpec struct { 169 | // (Required) Provider specifies the cloud provider that will be used. 170 | // Supported providers: `aws`, `gcp`, and `azure` 171 | // +kubebuilder:validation:Required 172 | Provider string `json:"provider"` 173 | 174 | // (Required) Storage Bucket Name. 175 | // +kubebuilder:validation:Required 176 | Bucket string `json:"bucket"` 177 | 178 | // (Optional) Region Name. 179 | // +kubebuilder:validation:Optional 180 | Region string `json:"region,omitempty"` 181 | 182 | // (Optional) Environment variables used for connecting to the cloud providers. 183 | // +kubebuilder:validation:Optional 184 | Env []corev1.EnvVar `json:"env,omitempty"` 185 | } 186 | 187 | // NotificationServiceSpec defines Notification Service Provider specification. 188 | type NotificationServiceSpec struct { 189 | // (Required) Provider specifies notification service provider. 190 | // Supported providers: `slack` 191 | // +kubebuilder:validation:Required 192 | Provider string `json:"provider"` 193 | 194 | // (Required) The name of secret in which all key/value sets needed for the notification are stored. 195 | // +kubebuilder:validation:Required 196 | SecretName string `json:"secretName"` 197 | } 198 | 199 | type PersistentVolumeSpec struct { 200 | // (Required) The name of the PersistentVolume. 201 | // +kubebuilder:validation:Required 202 | Name string `json:"name"` 203 | 204 | // (Required) PersistentVolumeSpec is the specification of a persistent volume. 205 | // +kubebuilder:validation:Required 206 | Spec corev1.PersistentVolumeSpec `json:"spec"` 207 | } 208 | 209 | type PersistentVolumeClaimSpec struct { 210 | // (Required) The name of the PersistentVolumeClaim. 211 | // +kubebuilder:validation:Required 212 | Name string `json:"name"` 213 | 214 | // (Required) PersistentVolumeClaimSpec is the specification of a persistent volume. 215 | // +kubebuilder:validation:Required 216 | Spec corev1.PersistentVolumeClaimSpec `json:"spec"` 217 | } 218 | 219 | // GatlingStatus defines the observed state of Gatling 220 | type GatlingStatus struct { 221 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 222 | // Important: Run "make" to regenerate code after modifying this file 223 | 224 | // Active is list of currently running jobs 225 | // The number of actively running pods for the gatling job 226 | // +optional 227 | Active int32 `json:"active,omitempty"` 228 | 229 | // The number of pods which reached phase Succeeded for the gatling job 230 | // +optional 231 | Succeeded int32 `json:"succeeded,omitempty"` 232 | 233 | // The number of pods which reached phase Failed for the gatling job 234 | // +optional 235 | Failed int32 `json:"failed,omitempty"` 236 | 237 | // Runner job name 238 | // +optional 239 | RunnerJobName string `json:"runnerJobName,omitempty"` 240 | 241 | // Runner start time (UnixTime epoc) 242 | // +optional 243 | RunnerStartTime int32 `json:"runnerStartTime,omitempty"` 244 | 245 | // Is runner job completed (default false) 246 | // +optional 247 | RunnerCompleted bool `json:"runnerCompleted,omitempty"` 248 | 249 | // The number of successfully completed runner pods. The format is (completed#/parallelism#) 250 | // +optional 251 | RunnerCompletions string `json:"runnerCompletions,omitempty"` 252 | 253 | // Reporter job name 254 | // +optional 255 | ReporterJobName string `json:"reporterJobName,omitempty"` 256 | 257 | // Reporter start time (UnixTime epoc) 258 | // +optional 259 | ReporterStartTime int32 `json:"reporterStartTime,omitempty"` 260 | 261 | // Is report generation completed (default false) 262 | // +optional 263 | ReportCompleted bool `json:"reportCompleted,omitempty"` 264 | 265 | // Report Storage Path 266 | // +optional 267 | ReportStoragePath string `json:"reportStoragePath,omitempty"` 268 | 269 | // Report Url 270 | // +optional 271 | ReportUrl string `json:"reportUrl,omitempty"` 272 | 273 | // Is notification completed (default false) 274 | // +optional 275 | NotificationCompleted bool `json:"notificationCompleted,omitempty"` 276 | 277 | // Error message 278 | // +optional 279 | Error string `json:"error,omitempty"` 280 | } 281 | 282 | //+kubebuilder:object:root=true 283 | //+kubebuilder:subresource:status 284 | //+kubebuilder:printcolumn:name="Runned",type=string,JSONPath=`.status.runnerCompletions` 285 | //+kubebuilder:printcolumn:name="Reported",type=boolean,JSONPath=`.status.reportCompleted` 286 | //+kubebuilder:printcolumn:name="Notified",type=boolean,JSONPath=`.status.notificationCompleted` 287 | //+kubebuilder:printcolumn:name="ReportURL",type=string,JSONPath=`.status.reportUrl` 288 | //+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" 289 | 290 | // Gatling is the Schema for the gatlings API 291 | type Gatling struct { 292 | metav1.TypeMeta `json:",inline"` 293 | metav1.ObjectMeta `json:"metadata,omitempty"` 294 | 295 | // GatlingSpec defines the desired state of Gatling 296 | Spec GatlingSpec `json:"spec,omitempty"` 297 | // GatlingStatus defines the observed state of Gatling 298 | Status GatlingStatus `json:"status,omitempty"` 299 | } 300 | 301 | //+kubebuilder:object:root=true 302 | 303 | // GatlingList contains a list of Gatling 304 | type GatlingList struct { 305 | metav1.TypeMeta `json:",inline"` 306 | metav1.ListMeta `json:"metadata,omitempty"` 307 | Items []Gatling `json:"items"` 308 | } 309 | 310 | func init() { 311 | SchemeBuilder.Register(&Gatling{}, &GatlingList{}) 312 | } 313 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © ZOZO, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the gatling-operator v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=gatling-operator.tech.zozo.com 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "gatling-operator.tech.zozo.com", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Copyright © ZOZO, Inc. 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 | http://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 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1alpha1 22 | 23 | import ( 24 | "k8s.io/api/core/v1" 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *CloudStorageSpec) DeepCopyInto(out *CloudStorageSpec) { 30 | *out = *in 31 | if in.Env != nil { 32 | in, out := &in.Env, &out.Env 33 | *out = make([]v1.EnvVar, len(*in)) 34 | for i := range *in { 35 | (*in)[i].DeepCopyInto(&(*out)[i]) 36 | } 37 | } 38 | } 39 | 40 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudStorageSpec. 41 | func (in *CloudStorageSpec) DeepCopy() *CloudStorageSpec { 42 | if in == nil { 43 | return nil 44 | } 45 | out := new(CloudStorageSpec) 46 | in.DeepCopyInto(out) 47 | return out 48 | } 49 | 50 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 51 | func (in *Gatling) DeepCopyInto(out *Gatling) { 52 | *out = *in 53 | out.TypeMeta = in.TypeMeta 54 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 55 | in.Spec.DeepCopyInto(&out.Spec) 56 | out.Status = in.Status 57 | } 58 | 59 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Gatling. 60 | func (in *Gatling) DeepCopy() *Gatling { 61 | if in == nil { 62 | return nil 63 | } 64 | out := new(Gatling) 65 | in.DeepCopyInto(out) 66 | return out 67 | } 68 | 69 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 70 | func (in *Gatling) DeepCopyObject() runtime.Object { 71 | if c := in.DeepCopy(); c != nil { 72 | return c 73 | } 74 | return nil 75 | } 76 | 77 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 78 | func (in *GatlingList) DeepCopyInto(out *GatlingList) { 79 | *out = *in 80 | out.TypeMeta = in.TypeMeta 81 | in.ListMeta.DeepCopyInto(&out.ListMeta) 82 | if in.Items != nil { 83 | in, out := &in.Items, &out.Items 84 | *out = make([]Gatling, len(*in)) 85 | for i := range *in { 86 | (*in)[i].DeepCopyInto(&(*out)[i]) 87 | } 88 | } 89 | } 90 | 91 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatlingList. 92 | func (in *GatlingList) DeepCopy() *GatlingList { 93 | if in == nil { 94 | return nil 95 | } 96 | out := new(GatlingList) 97 | in.DeepCopyInto(out) 98 | return out 99 | } 100 | 101 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 102 | func (in *GatlingList) DeepCopyObject() runtime.Object { 103 | if c := in.DeepCopy(); c != nil { 104 | return c 105 | } 106 | return nil 107 | } 108 | 109 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 110 | func (in *GatlingSpec) DeepCopyInto(out *GatlingSpec) { 111 | *out = *in 112 | in.PodSpec.DeepCopyInto(&out.PodSpec) 113 | in.CloudStorageSpec.DeepCopyInto(&out.CloudStorageSpec) 114 | in.PersistentVolumeSpec.DeepCopyInto(&out.PersistentVolumeSpec) 115 | in.PersistentVolumeClaimSpec.DeepCopyInto(&out.PersistentVolumeClaimSpec) 116 | out.NotificationServiceSpec = in.NotificationServiceSpec 117 | in.TestScenarioSpec.DeepCopyInto(&out.TestScenarioSpec) 118 | } 119 | 120 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatlingSpec. 121 | func (in *GatlingSpec) DeepCopy() *GatlingSpec { 122 | if in == nil { 123 | return nil 124 | } 125 | out := new(GatlingSpec) 126 | in.DeepCopyInto(out) 127 | return out 128 | } 129 | 130 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 131 | func (in *GatlingStatus) DeepCopyInto(out *GatlingStatus) { 132 | *out = *in 133 | } 134 | 135 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatlingStatus. 136 | func (in *GatlingStatus) DeepCopy() *GatlingStatus { 137 | if in == nil { 138 | return nil 139 | } 140 | out := new(GatlingStatus) 141 | in.DeepCopyInto(out) 142 | return out 143 | } 144 | 145 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 146 | func (in *NotificationServiceSpec) DeepCopyInto(out *NotificationServiceSpec) { 147 | *out = *in 148 | } 149 | 150 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NotificationServiceSpec. 151 | func (in *NotificationServiceSpec) DeepCopy() *NotificationServiceSpec { 152 | if in == nil { 153 | return nil 154 | } 155 | out := new(NotificationServiceSpec) 156 | in.DeepCopyInto(out) 157 | return out 158 | } 159 | 160 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 161 | func (in *PersistentVolumeClaimSpec) DeepCopyInto(out *PersistentVolumeClaimSpec) { 162 | *out = *in 163 | in.Spec.DeepCopyInto(&out.Spec) 164 | } 165 | 166 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PersistentVolumeClaimSpec. 167 | func (in *PersistentVolumeClaimSpec) DeepCopy() *PersistentVolumeClaimSpec { 168 | if in == nil { 169 | return nil 170 | } 171 | out := new(PersistentVolumeClaimSpec) 172 | in.DeepCopyInto(out) 173 | return out 174 | } 175 | 176 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 177 | func (in *PersistentVolumeSpec) DeepCopyInto(out *PersistentVolumeSpec) { 178 | *out = *in 179 | in.Spec.DeepCopyInto(&out.Spec) 180 | } 181 | 182 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PersistentVolumeSpec. 183 | func (in *PersistentVolumeSpec) DeepCopy() *PersistentVolumeSpec { 184 | if in == nil { 185 | return nil 186 | } 187 | out := new(PersistentVolumeSpec) 188 | in.DeepCopyInto(out) 189 | return out 190 | } 191 | 192 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 193 | func (in *PodSpec) DeepCopyInto(out *PodSpec) { 194 | *out = *in 195 | in.Resources.DeepCopyInto(&out.Resources) 196 | in.Affinity.DeepCopyInto(&out.Affinity) 197 | if in.Tolerations != nil { 198 | in, out := &in.Tolerations, &out.Tolerations 199 | *out = make([]v1.Toleration, len(*in)) 200 | for i := range *in { 201 | (*in)[i].DeepCopyInto(&(*out)[i]) 202 | } 203 | } 204 | if in.Volumes != nil { 205 | in, out := &in.Volumes, &out.Volumes 206 | *out = make([]v1.Volume, len(*in)) 207 | for i := range *in { 208 | (*in)[i].DeepCopyInto(&(*out)[i]) 209 | } 210 | } 211 | if in.SecurityContext != nil { 212 | in, out := &in.SecurityContext, &out.SecurityContext 213 | *out = new(v1.PodSecurityContext) 214 | (*in).DeepCopyInto(*out) 215 | } 216 | if in.RunnerContainerSecurityContext != nil { 217 | in, out := &in.RunnerContainerSecurityContext, &out.RunnerContainerSecurityContext 218 | *out = new(v1.SecurityContext) 219 | (*in).DeepCopyInto(*out) 220 | } 221 | } 222 | 223 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodSpec. 224 | func (in *PodSpec) DeepCopy() *PodSpec { 225 | if in == nil { 226 | return nil 227 | } 228 | out := new(PodSpec) 229 | in.DeepCopyInto(out) 230 | return out 231 | } 232 | 233 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 234 | func (in *TestScenarioSpec) DeepCopyInto(out *TestScenarioSpec) { 235 | *out = *in 236 | if in.SimulationData != nil { 237 | in, out := &in.SimulationData, &out.SimulationData 238 | *out = make(map[string]string, len(*in)) 239 | for key, val := range *in { 240 | (*out)[key] = val 241 | } 242 | } 243 | if in.ResourceData != nil { 244 | in, out := &in.ResourceData, &out.ResourceData 245 | *out = make(map[string]string, len(*in)) 246 | for key, val := range *in { 247 | (*out)[key] = val 248 | } 249 | } 250 | if in.GatlingConf != nil { 251 | in, out := &in.GatlingConf, &out.GatlingConf 252 | *out = make(map[string]string, len(*in)) 253 | for key, val := range *in { 254 | (*out)[key] = val 255 | } 256 | } 257 | if in.Env != nil { 258 | in, out := &in.Env, &out.Env 259 | *out = make([]v1.EnvVar, len(*in)) 260 | for i := range *in { 261 | (*in)[i].DeepCopyInto(&(*out)[i]) 262 | } 263 | } 264 | if in.VolumeMounts != nil { 265 | in, out := &in.VolumeMounts, &out.VolumeMounts 266 | *out = make([]v1.VolumeMount, len(*in)) 267 | for i := range *in { 268 | (*in)[i].DeepCopyInto(&(*out)[i]) 269 | } 270 | } 271 | } 272 | 273 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestScenarioSpec. 274 | func (in *TestScenarioSpec) DeepCopy() *TestScenarioSpec { 275 | if in == nil { 276 | return nil 277 | } 278 | out := new(TestScenarioSpec) 279 | in.DeepCopyInto(out) 280 | return out 281 | } 282 | -------------------------------------------------------------------------------- /assets/gatling-html-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st-tech/gatling-operator/50c1af41032381762ba859405765cd3efdb02f82/assets/gatling-html-report.png -------------------------------------------------------------------------------- /assets/gatling-operator-arch.xml: -------------------------------------------------------------------------------- 1 | 7Vxbc5s4FP41mU0eyAAyFz8mTpq2k2zceLfb5qWDQca0GLlCxHZ//UogrgI7jsFO3SQzMToSkjjfuSPnBAxmyxtszad3yIH+iSo7yxNwdaKqSq+v0g9GWXEKMEFCcbHncFpOGHm/ICfKnBp5DgxLAwlCPvHmZaKNggDapESzMEaL8rAJ8surzi2XryjnhJFt+VAY9p/nkGlCNVUjp7+HnjtNV1b0ftIzs9LBfOJwajloUSCB6xMwwAiR5Gq2HECfcS/lS3Lfu4bebGMYBuQ5NzxZ89tf2vj9pXL7+Gv47fG9dq1IOt8bWaUPDB36/LyJMJkiFwWWf51TLzGKAgeyWWXaysfcIjSnRIUSv0NCVhxMKyKIkqZk5vNeumG8+sLvjxtfWeNcS5tXy2Ln1Yq37Ag/xeuyScTH5xwJUYRtuOaZUzGysAvJmnFcdBlDCgtw5t5ANIN0c3QAhr5FvKeywFhc7txsXA4NveDobIGU8YZU4zjwqpAy35BqHNd7VUjxeZ8sP+Ir3VjE9wL3RNV9+hiXY0yvXHb1EFH/QlvyRzQW8M3RY1xcTD0CR3MrZtiC+sUyUiHB6EfmTAClTDzfHyAf4Xg24GjQdHrZyEKPqY6Brq8D6gliApdrWct7JdXgapO5Zu6oFrmfy2jTgo/T5I7gyKRtG80pCXCLanQuy2pZlXRF2aBMcWsIsUf5ATEnvlypWndA/NYh8uhWMklQ02AmFQRNK0+RaDW/K8f4AmNrVRg2ZwPC5nUUQy6tY2rleGXD8HRbuYQlG8jlLWPJDiL4AoswRE7XFsGC5sSuswi6bcLxpC2LUDYIeo09UPdqD9Q3e9BpmNOuPWhLB8GbDq53yntVQr33epRQYlqoVwLcvrlfLWw9hG3SQr2ihb29eGUqSlu65V73brn3ZhJygORDmwQgWmhFGp9Tyr9zH1kOK9tBCoJFILsOvVnEVAEFtOEjN4xreCJy9NOaMQyCccg+Bj6K2P0jgjCr0lXBpPwjdYilOAQogBXQOMnyPTegTZuiwizCJUPDsy3/gnfMPMeJDVmdiJTtWQsIK4pWQlhTRYDrEjG1M5v/kmJTZ4GXVjL4G4z9DtEVr9tsrmHIbXuA3bTR3MI4wjnF4qjqGD2tHK0evIqRiseWeByLu+rpajk+UA/trjTRXYnmLXAu2Nsi5hR8Kww9u8zcRsZsNAKFx9bWPPWO0aLAdVDJ2RLzJ0SLwkSgmvxVJ+o4+dPEUO/4waom2C8Ga8+Zemp6C2CpksUCwSu0CHgomER8xxq7gV4lHVJqzJ3SE2Wps+BNE+MBVbJL0TmO3c4RgwL6ZVDSIkYJlL1G1GJQkEYDExSrdI6D/jNCaYcUxgHyBR2gqPNl3plGEEqibzTrpX8b4wyOO4EhKfTTJ0kWT/OvY5UHqVeWh9oYsb9PcVBrdDTOoG944sxWZZBarouhyzPpY9dbwTEaZo3emnsEytAa9TacM4Cy0kUbuswEYIBhAn+mzHL53XOD7tZUU9KB5Z22Ljw+nJCDi46UBWdcdqR+jc3X92nzDX13m2/WyUkGdkrIReV08HBWQH5cIw1rTX67GSdUHA0adRlnXzeA1VIFQKlEwVLtm0tznymn+ZIzQL97Ac14bgHtudnYno5r9Q+O1R54nlYJXwnT+/LBmf46FORO8sH4dmgE9w/fI8UA2Lp4TE/ldv2K0QBll9mvmMOOqwf9V3XcqyQDmw52NMpA52+FK5mtlNW2t60dCTNlR+7bh79WyH/fA81w6ZFsDnqdTKFqvJlPwRp79KtGN3Zj2xMF1UBcUdS1klgdr4I9HPQzxCpZ58H5gN6Pke+zXO5gIXobAbein2sn9WaoEHKrspwOKwbdRmdBt1hju0OBRxCOiymsgBLNnTi5ribLBYweYm5bJGLHFGwKhBdEKAr9lQDEkWTOor6atViKSHaWOZvNVZfdKix1tRu1qepSeVcui2LjTQrkSqHFI38xCULzpHTHDrzIDmpU8uaa0h9b0gGVE3DZm/VNaX1ncpl+m7Egl4BJzxCFTCgXcDxF6Ae9msEwjI8slU80NUhQk6gcJ6yaXrU3dQfaeh1VeYH97bPku8MP7+7kxQLa9xezu5rvAQ2ikKBZbAV4fNY+Gq+k6K6AftWbqzUeIHu33DYi5k+ij6bg42fwGH6ZfQq/fwnuaxApm8gaB2AnXGbGH7vjU02JbTNdXi5dnrFrxjg55q4UJuxltwUIzyxf9BrCQcQGM5wr9163d0DOnI5Acjck9lmzw9rehYVTa84u7RX1xk6sJRsi3XGiF7fjjGDZP9xYW+4j5tNhY0hcqVpPNPYrKDQ7JxX/8GCjQE9+WgrHque+a6xjv+Y8gVJN31vTRSCg87uUaWgcj4l4VCgmv/P8fPrsPBE30ZTC+w9a7VH6VVf50mqPMNNzqz1bf51A2HJf3m5nlRt2Tv8/3N14IRlJXx9DYOPlnW//822dxy9m6X+Qz697Z9aVy19X9C5A8jci3oQyiX91YIjRk+fkxZPcq5yOfGrszwS88BTNxjSR3/1N5mF9gl4p1NdGzHqvrtqSDW0dr+YCWtGz9+LTgNCHleya1Vm2Sob+8GRY0s16M9nFt0RoM/+/NInJzf+9D7j+Hw== -------------------------------------------------------------------------------- /assets/gatling-operator-pod.xml: -------------------------------------------------------------------------------- 1 | 5Vrbdto4FP0a1kof6PIFc3kskGQu6bQd1uQyb4otjFvbcmQR8Hz9SLYsWZYgJAOEMslDpK2LrbPPPjqS03Enyfoag2zxGQUw7jhWsO64047j2CPPoX8YUnCk544qJMRRwDEJzKJ/IActji6jAOZKR4JQTKJMBX2UptAnCgYwRiu12xzF6lMzEPInWhKY+SCGWre7KCCLCh06A4n/AqNwUT/Z7vP1JaDuzCfOFyBAqwbkXnbcCUaIVKVkPYExs15tl2rc1YZW8WIYpmSXAent+umvqzvrbgxAsfqjV9xnv3edfjXNM4iXfMX8bUlRmwCjZRpANovVccerRUTgLAM+a11R1im2IElMazYt5gSjH8JUEpmgGOFyPrdf/tCWeRTHDXzusV+KByBflM9jw+coJY1O1Q/F9eVzizxDTOC6AXFzXEOUQIIL2oW3dp0e56aogdrtVpJrb8ixRZPnAQcB969QzC4poAXOwisYsYdHZiRFKdTJmEwuvaurfdnZadnZ9nQ72/2Bwc7WHuw8fCL92cL97db9O79PvuXf79MvXdtg5n5MmHkykCr27j8tmUjHfmWeT7QRh48XHp2Cvh19vKUUP7AyM5xVMtPNK2rYsBThBMRyRloK2d/Zgto3oB1uUbxMYP0mj7jucAGTjBTTCH/Q22qEmqF69RpueQ0NQBkr+kUcUffBL7vOY+VnN48CAP6PsPS+L0tCZ4FGH9uTPEee4jSOwWVsgzRHh1Kme3BhKoFvF5VOvcvhtLcnldo7iNR1TcFwHyI1B0NLMzEM6P7MqwiTBQpRCuJLiY5VEmSfG4QybtvvkJCCJxtgSdBLxMB1RO4b5Qc2+UeP16Zr/qyyUtSVlFrgvllpjGJVOays1eM2UpmjJfbhDlsHATiE5GVPZqbc6hkYxoBEz2oqZCKZD/2KIvrO0qP6LY+qFV1PUa2Ij2qmLu2JaqkJ12x7XLVkbabS6cSC3u6HPU36IWDxL+yuANU43hII7NcGgr0lN1Zbz6YIatKzdzA56xHzXeTsL/GzIOftevN0uW1LNfYnt/9EgrfRl/GSnqFO0ZeHH9VU4BRc2X2DKyt+d4htSmw5cpd5UDYZ85ZTb29yS3tobHbm7e3tsum/l2zMm4s9dFXnco67tzjDE/SkvbJr7Dc6qaCoX4OIoAjzZUy6BIM0n0N8kvHRbidatj0yxMfREePjQDNofUxlFzrG4zVr6Oalj7Kzsu1la/2k/GsasdET2hfQ8yfOG6ffauYNp1/maDfgEcYqHyCOwpSWfWr98kjMrB75IP7EG5IoCKo8BNJ3A4/lfExnGQsIpdm8ccebUiRm04/FEbl1dNudZ3GbyZ/WaV4Ymo9urq1tkf3eqyOhMoOjDkfzeQ4PEgBNF14v3MTs5CqfqYNscpWtFyXn7Cp276f1k9Hr/eRwN3aTGC3Zhd2MIAxCmP8k13Kbrty3XNIf7v6917oWfvcbPrEE6WJ3mFJBoesqHSgTEpYQ0AK1JeO9RSddOzFt+Vsu8ji0e3wx+YeaBO6BHf3OXmPH9GWknT3vjxz9EuZPCILN3JwvNfbw1LjRLxUmKCv+j9y4gxY3PT2qHZWb+lrEsG8G0XN7t1K3Kr6ntYFdU3hqSkNe9hUFG5N2AWvP1LZRASurOFu3an+ptmzNrQaGD6iHcyt9r/TrTNs+Xx669nDUir2u4axdf4o5DhXOZipemyODC0skxsaSzJVLsc9BEsVFNTxBKapSZ6WLjAdlONicaZecaeGi43jtfzDyqHEYWv7zjKjVxvJKc1FkysrsxT1mHo/a+KW+tuhbe8mbpnHkNBUnoqUpkbpLRYnoUkqF1iqxMNguq03BMLTUB2uQolHgSjgM4tJhoCoe1sjlwxobAmItFR2eFBEDrRJprK9Szi4WFE3CeFJeHhOY6NkdjeQ0hcTdQWP6Sm5ydia5uo3LThIi2WPyE3jYeIE2t2VVENwEVbfj/TT/3O30dzaBUf9GY0h8jhwX9f/YEPrTP9+cDRP2wDomFx0uiMZdj5SCe/kv -------------------------------------------------------------------------------- /assets/slack-notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st-tech/gatling-operator/50c1af41032381762ba859405765cd3efdb02f82/assets/slack-notification.png -------------------------------------------------------------------------------- /config/crd-ref-docs/config.yaml: -------------------------------------------------------------------------------- 1 | processor: 2 | ignoreTypes: 3 | - "GatlingList$" 4 | ignoreFields: 5 | - "status$" 6 | - "TypeMeta$" 7 | 8 | render: 9 | kubernetesVersion: 1.22 10 | -------------------------------------------------------------------------------- /config/crd-ref-docs/templates/markdown/gv_details.tpl: -------------------------------------------------------------------------------- 1 | {{- define "gvDetails" -}} 2 | {{- $gv := . -}} 3 | 4 | ## {{ $gv.GroupVersionString }} 5 | 6 | {{ $gv.Doc }} 7 | 8 | {{- if $gv.Kinds }} 9 | ### Resource Types 10 | {{- range $gv.SortedKinds }} 11 | - {{ $gv.TypeForKind . | markdownRenderTypeLink }} 12 | {{- end }} 13 | {{ end }} 14 | 15 | {{ range $gv.SortedTypes }} 16 | {{ template "type" . }} 17 | {{ end }} 18 | 19 | {{- end -}} 20 | -------------------------------------------------------------------------------- /config/crd-ref-docs/templates/markdown/gv_list.tpl: -------------------------------------------------------------------------------- 1 | {{- define "gvList" -}} 2 | {{- $groupVersions := . -}} 3 | 4 | # API Reference 5 | 6 | ## Packages 7 | {{- range $groupVersions }} 8 | - {{ markdownRenderGVLink . }} 9 | {{- end }} 10 | 11 | {{ range $groupVersions }} 12 | {{ template "gvDetails" . }} 13 | {{ end }} 14 | 15 | {{- end -}} 16 | -------------------------------------------------------------------------------- /config/crd-ref-docs/templates/markdown/type.tpl: -------------------------------------------------------------------------------- 1 | {{- define "type" -}} 2 | {{- $type := . -}} 3 | {{- if markdownShouldRenderType $type -}} 4 | 5 | #### {{ $type.Name }} 6 | 7 | {{ if $type.IsAlias }}_Underlying type:_ `{{ markdownRenderTypeLink $type.UnderlyingType }}`{{ end }} 8 | 9 | {{ $type.Doc }} 10 | 11 | {{ if $type.References -}} 12 | _Appears in:_ 13 | {{- range $type.SortedReferences }} 14 | - {{ markdownRenderTypeLink . }} 15 | {{- end }} 16 | {{- end }} 17 | 18 | {{ if $type.Members -}} 19 | | Field | Description | 20 | | --- | --- | 21 | {{ if $type.GVK -}} 22 | | `apiVersion` _string_ | `{{ $type.GVK.Group }}/{{ $type.GVK.Version }}` 23 | | `kind` _string_ | `{{ $type.GVK.Kind }}` 24 | {{ end -}} 25 | 26 | {{ range $type.Members -}} 27 | | `{{ .Name }}` _{{ markdownRenderType .Type }}_ | {{ template "type_members" . }} | 28 | {{ end -}} 29 | 30 | {{ end -}} 31 | 32 | {{- end -}} 33 | {{- end -}} 34 | -------------------------------------------------------------------------------- /config/crd-ref-docs/templates/markdown/type_members.tpl: -------------------------------------------------------------------------------- 1 | {{- define "type_members" -}} 2 | {{- $field := . -}} 3 | {{- if eq $field.Name "metadata" -}} 4 | Refer to Kubernetes API documentation for fields of `metadata`. 5 | {{- else -}} 6 | {{ $field.Doc }} 7 | {{- end -}} 8 | {{- end -}} 9 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/gatling-operator.tech.zozo.com_gatlings.yaml 6 | # +kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patchesStrategicMerge: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | # - patches/webhook_in_gatlings.yaml 12 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | # - patches/cainjection_in_gatlings.yaml 17 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 18 | 19 | # the following config is for teaching kustomize how to do kustomization for CRDs. 20 | configurations: 21 | - kustomizeconfig.yaml 22 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_gatlings.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: gatlings.gatling-operator.tech.zozo.com 8 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_gatlings.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: gatlings.gatling-operator.tech.zozo.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: gatling-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: gatling-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | # commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | # - ../webhook 22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 23 | # - ../certmanager 24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 25 | # - ../prometheus 26 | 27 | patchesStrategicMerge: 28 | # Protect the /metrics endpoint by putting it behind auth. 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, please comment the following line. 31 | - manager_auth_proxy_patch.yaml 32 | 33 | # Mount the controller config file for loading manager configurations 34 | # through a ComponentConfig type 35 | # - manager_config_patch.yaml 36 | 37 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 38 | # crd/kustomization.yaml 39 | # - manager_webhook_patch.yaml 40 | 41 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 42 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 43 | # 'CERTMANAGER' needs to be enabled to use ca injection 44 | # - webhookcainjection_patch.yaml 45 | 46 | # the following config is for teaching kustomize how to do var substitution 47 | vars: 48 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 49 | # - name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 50 | # objref: 51 | # kind: Certificate 52 | # group: cert-manager.io 53 | # version: v1 54 | # name: serving-cert # this name should match the one in certificate.yaml 55 | # fieldref: 56 | # fieldpath: metadata.namespace 57 | # - name: CERTIFICATE_NAME 58 | # objref: 59 | # kind: Certificate 60 | # group: cert-manager.io 61 | # version: v1 62 | # name: serving-cert # this name should match the one in certificate.yaml 63 | # - name: SERVICE_NAMESPACE # namespace of the service 64 | # objref: 65 | # kind: Service 66 | # version: v1 67 | # name: webhook-service 68 | # fieldref: 69 | # fieldpath: metadata.namespace 70 | # - name: SERVICE_NAME 71 | # objref: 72 | # kind: Service 73 | # version: v1 74 | # name: webhook-service 75 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=10" 19 | ports: 20 | - containerPort: 8443 21 | name: https 22 | - name: manager 23 | args: 24 | - "--health-probe-bind-address=:8081" 25 | - "--metrics-bind-address=127.0.0.1:8080" 26 | - "--leader-elect" 27 | - "--max-concurrent-reconciles=1" 28 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | args: 12 | - "--config=controller_manager_config.yaml" 13 | volumeMounts: 14 | - name: manager-config 15 | mountPath: /controller_manager_config.yaml 16 | subPath: controller_manager_config.yaml 17 | volumes: 18 | - name: manager-config 19 | configMap: 20 | name: manager-config 21 | -------------------------------------------------------------------------------- /config/kind/cluster.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | - role: worker 6 | -------------------------------------------------------------------------------- /config/manager/controller_manager_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 2 | kind: ControllerManagerConfig 3 | health: 4 | healthProbeBindAddress: :8081 5 | metrics: 6 | bindAddress: 127.0.0.1:8080 7 | webhook: 8 | port: 9443 9 | leaderElection: 10 | leaderElect: true 11 | resourceName: 0d2e74aa.tech.zozo.com 12 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | 4 | # This file will get modified by kustomize file. Please don't commit any changes to the git repo! 5 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: system 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: controller-manager 12 | namespace: system 13 | labels: 14 | control-plane: controller-manager 15 | spec: 16 | selector: 17 | matchLabels: 18 | control-plane: controller-manager 19 | replicas: 1 20 | template: 21 | metadata: 22 | labels: 23 | control-plane: controller-manager 24 | spec: 25 | securityContext: 26 | runAsNonRoot: true 27 | containers: 28 | - command: 29 | - /manager 30 | args: 31 | - --leader-elect 32 | image: controller:latest 33 | name: manager 34 | securityContext: 35 | allowPrivilegeEscalation: false 36 | livenessProbe: 37 | httpGet: 38 | path: /healthz 39 | port: 8081 40 | initialDelaySeconds: 15 41 | periodSeconds: 20 42 | readinessProbe: 43 | httpGet: 44 | path: /readyz 45 | port: 8081 46 | initialDelaySeconds: 5 47 | periodSeconds: 10 48 | resources: 49 | limits: 50 | cpu: 100m 51 | memory: 100Mi 52 | requests: 53 | cpu: 100m 54 | memory: 100Mi 55 | serviceAccountName: controller-manager 56 | terminationGracePeriodSeconds: 10 57 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | scheme: https 15 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 16 | tlsConfig: 17 | insecureSkipVerify: true 18 | selector: 19 | matchLabels: 20 | control-plane: controller-manager 21 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | -------------------------------------------------------------------------------- /config/rbac/gatling_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit gatlings. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: gatling-editor-role 6 | rules: 7 | - apiGroups: 8 | - gatling-operator.tech.zozo.com 9 | resources: 10 | - gatlings 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - gatling-operator.tech.zozo.com 21 | resources: 22 | - gatlings/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/gatling_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view gatlings. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: gatling-viewer-role 6 | rules: 7 | - apiGroups: 8 | - gatling-operator.tech.zozo.com 9 | resources: 10 | - gatlings 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - gatling-operator.tech.zozo.com 17 | resources: 18 | - gatlings/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | # - auth_proxy_service.yaml 16 | # - auth_proxy_role.yaml 17 | # - auth_proxy_role_binding.yaml 18 | # - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | - persistentvolumes 12 | - persistentvolumeclaims 13 | verbs: 14 | - get 15 | - list 16 | - watch 17 | - create 18 | - update 19 | - patch 20 | - delete 21 | - apiGroups: 22 | - coordination.k8s.io 23 | resources: 24 | - leases 25 | verbs: 26 | - get 27 | - list 28 | - watch 29 | - create 30 | - update 31 | - patch 32 | - delete 33 | - apiGroups: 34 | - "" 35 | resources: 36 | - events 37 | verbs: 38 | - create 39 | - patch 40 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - batch 9 | resources: 10 | - jobs 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps 23 | verbs: 24 | - create 25 | - delete 26 | - get 27 | - list 28 | - patch 29 | - update 30 | - watch 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - persistentvolumeclaims 35 | verbs: 36 | - create 37 | - delete 38 | - get 39 | - list 40 | - patch 41 | - update 42 | - watch 43 | - apiGroups: 44 | - "" 45 | resources: 46 | - persistentvolumes 47 | verbs: 48 | - create 49 | - delete 50 | - get 51 | - list 52 | - patch 53 | - update 54 | - watch 55 | - apiGroups: 56 | - "" 57 | resources: 58 | - secrets 59 | verbs: 60 | - get 61 | - list 62 | - watch 63 | - apiGroups: 64 | - gatling-operator.tech.zozo.com 65 | resources: 66 | - gatlings 67 | verbs: 68 | - create 69 | - delete 70 | - get 71 | - list 72 | - patch 73 | - update 74 | - watch 75 | - apiGroups: 76 | - gatling-operator.tech.zozo.com 77 | resources: 78 | - gatlings/finalizers 79 | verbs: 80 | - update 81 | - apiGroups: 82 | - gatling-operator.tech.zozo.com 83 | resources: 84 | - gatlings/status 85 | verbs: 86 | - get 87 | - patch 88 | - update 89 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | -------------------------------------------------------------------------------- /config/samples/gatling-notification-slack-secrets.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | # dummy base64ed slack incoming webhook url 4 | incoming-webhook-url: aHR0cHM6Ly9ob29rcy5zbGFjay5jb20vc2VydmljZXMvQUJDREVGR0hJL1hYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWA== 5 | kind: Secret 6 | metadata: 7 | name: gatling-notification-slack-secrets 8 | type: Opaque 9 | -------------------------------------------------------------------------------- /config/samples/gatling-operator_v1alpha1_gatling01.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: gatling-operator.tech.zozo.com/v1alpha1 2 | kind: Gatling 3 | metadata: 4 | name: gatling-sample01 5 | spec: 6 | generateReport: false # The flag of generating gatling report 7 | generateLocalReport: false # The flag of generating gatling report for each pod 8 | notifyReport: false # The flag of notifying gatling report 9 | cleanupAfterJobDone: true # The flag of cleaning up gatling jobs resources after the job done 10 | podSpec: 11 | securityContext: 12 | sysctls: 13 | - name: net.ipv4.ip_local_port_range 14 | value: "1024 65535" 15 | runnerContainerSecurityContext: 16 | runAsUser: 1000 17 | runAsGroup: 1000 18 | serviceAccountName: "gatling-operator-worker" 19 | gatlingImage: ghcr.io/st-tech/gatling:latest # Optional. Default: ghcr.io/st-tech/gatling:latest. The image that will be used for Gatling container. 20 | rcloneImage: rclone/rclone # Optional. Default: rclone/rclone:latest. The image that will be used for rclone conatiner. 21 | resources: # Optional. Resources specifies the resource limits of the container. 22 | limits: 23 | cpu: "500m" 24 | memory: "500Mi" 25 | affinity: # Optional. Affinity specification 26 | nodeAffinity: 27 | requiredDuringSchedulingIgnoredDuringExecution: 28 | nodeSelectorTerms: 29 | - matchExpressions: 30 | - key: kubernetes.io/os 31 | operator: In 32 | values: 33 | - linux 34 | tolerations: 35 | - key: "node-type" 36 | operator: "Equal" 37 | value: "non-kube-system" 38 | effect: "NoSchedule" 39 | cloudStorageSpec: 40 | ############################################################# 41 | # Storage Provider - aws (AMAZON S3) 42 | ############################################################# 43 | provider: "aws" # Provider specifies the cloud provider that will be used. Supported providers: "aws", "gcp", "azure" 44 | bucket: "gatling-operator-reports" # S3 Bucket name on which Gatlilng report files are stored 45 | region: "ap-northeast-1" # Optional. Default: "ap-northeast-1" for aws provider. Region name 46 | #env: # Optional. Environment variables to be used for connecting to the cloud providers 47 | # # For S3 see also the env variables for auth: https://rclone.org/s3/#authentication 48 | # - name: AWS_ACCESS_KEY_ID 49 | # value: xxxxxxxxxxxxxxx 50 | # - name: AWS_SECRET_ACCESS_KEY 51 | # valueFrom: 52 | # secretKeyRef: 53 | # name: aws-credentail-secrets 54 | # key: AWS_SECRET_ACCESS_KEY 55 | # 56 | ############################################################# 57 | # Storage Provider - gcp (Google Cloud Storage) 58 | ############################################################# 59 | # provider: "gcp" 60 | # bucket: "gatling-operator-reports" # GCS bucket name on which Gatlilng report files are stored 61 | # 62 | ############################################################# 63 | # Storage Provider - azure (Azure Blob Storage) 64 | ############################################################# 65 | #provider: "azure" 66 | #bucket: "gatling-operator-reports" # Azure Blob Storage container name on which Gatlilng report files are stored 67 | #env: # Optional. Environment variables to be used for connecting to the cloud providers 68 | # - name: AZUREBLOB_ACCOUNT # Azure Blob Storage Account Name 69 | # value: xxxxxxxxxxxxxxx 70 | # - name: AZUREBLOB_KEY # Azure Blob Access Key. Leave blank to use SAS URL 71 | # valueFrom: 72 | # secretKeyRef: 73 | # name: azure-credentail-secrets 74 | # key: AZUREBLOBSS_KEY 75 | # - name: AZUREBLOB_SAS_URL # SAS URL. "Read, Write, List" permissions are required 76 | # valueFrom: 77 | # secretKeyRef: 78 | # name: azure-credentail-secrets 79 | # key: AZUREBLOB_SAS_URL 80 | notificationServiceSpec: 81 | provider: "slack" # Notification provider name. Supported provider: "slack" 82 | secretName: "gatling-notification-slack-secrets" # The name of secret in which all key/value sets needed for the notification are stored 83 | testScenarioSpec: 84 | # startTime: 2021-09-10 08:45:31 # Optional. Start time of running test scenario in UTC. Format: %Y-%m-%d %H:%M:%S 85 | parallelism: 3 # Optional. Default: 1. Number of pods running at any instant 86 | # simulationsDirectoryPath: "/dir-path-to-simulation" # Optional. Default: /opt/gatling/user-files/simulations 87 | # resourcesDirectoryPath: "dir-path-to-resources" # Optional. Default: /opt/gatling/user-files/resources 88 | # resultsDirectoryPath: "dir-path-to-results" # Optional. Default: /opt/gatling/results. 89 | simulationClass: "MyBasicSimulation" # Gatling simulation class name 90 | # simulationData: # Optional. Default: empty string map. Simulation Scala data to be created as ConfigMap that is mounted on simulations dir 91 | # resourceData: # Optional. Default: empty string map. Resource data used with Simulation scala to be created as ConfigMap that is mounted on resource dir 92 | # gatlingConf: # Optional. Default: empty string map. Gatling Conf data to be created as ConfigMap that is mounted on gatling conf dir 93 | env: # Optional. Environment variables to be used in Gatling Simulation Scala 94 | - name: ENV 95 | value: "dev" 96 | - name: CONCURRENCY 97 | value: "1" 98 | - name: DURATION 99 | value: "1" 100 | -------------------------------------------------------------------------------- /config/samples/gatling-operator_v1alpha1_gatling03.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: gatling-operator.tech.zozo.com/v1alpha1 2 | kind: Gatling 3 | metadata: 4 | name: gatling-sample03 5 | spec: 6 | generateReport: false # The flag of generating gatling report 7 | generateLocalReport: false # The flag of generating gatling report for each pod 8 | notifyReport: false # The flag of notifying gatling report 9 | cleanupAfterJobDone: true # The flag of cleaning up gatling jobs resources after the job done 10 | podSpec: 11 | serviceAccountName: "gatling-operator-worker" 12 | gatlingImage: ghcr.io/st-tech/gatling:latest # Optional. Default: ghcr.io/st-tech/gatling:latest. The image that will be used for Gatling container. 13 | rcloneImage: rclone/rclone # Optional. Default: rclone/rclone:latest. The image that will be used for rclone conatiner. 14 | resources: # Optional. Resources specifies the resource limits of the container. 15 | limits: 16 | cpu: "500m" 17 | memory: "500Mi" 18 | volumes: 19 | - name: resource-vol 20 | persistentVolumeClaim: 21 | claimName: resource-pvc 22 | affinity: # Optional. Affinity specification 23 | nodeAffinity: 24 | requiredDuringSchedulingIgnoredDuringExecution: 25 | nodeSelectorTerms: 26 | - matchExpressions: 27 | - key: kubernetes.io/os 28 | operator: In 29 | values: 30 | - linux 31 | ######################################################################################### 32 | # PersistentVolume 33 | ######################################################################################### 34 | # To try this scenario with kind, add the extraMounts setting to config/kind/cluster.yaml 35 | # --------------------------------------------------------------------------------------- 36 | # kind: Cluster 37 | # apiVersion: kind.x-k8s.io/v1alpha4 38 | # nodes: 39 | # - role: control-plane 40 | # - role: worker 41 | # + extraMounts: 42 | # + - hostPath: /Users 43 | # + containerPath: /Users 44 | ######################################################################################### 45 | persistentVolume: 46 | name: resource-pv 47 | spec: 48 | volumeMode: Filesystem 49 | accessModes: 50 | - ReadWriteOnce 51 | storageClassName: "" 52 | capacity: 53 | storage: 1Gi 54 | local: 55 | path: /Users/xxxxxx/github/st-tech/gatling-operator/gatling/sample/resources # path of the local environment you want to mount as a persistent volume 56 | nodeAffinity: 57 | required: 58 | nodeSelectorTerms: 59 | - matchExpressions: 60 | - key: kubernetes.io/os 61 | operator: In 62 | values: 63 | - linux 64 | persistentVolumeClaim: 65 | name: resource-pvc 66 | spec: 67 | accessModes: 68 | - ReadWriteOnce 69 | storageClassName: "" 70 | volumeName: resource-pv 71 | resources: 72 | requests: 73 | storage: 1Gi 74 | cloudStorageSpec: 75 | ############################################################# 76 | # Storage Provider - aws (AMAZON S3) 77 | ############################################################# 78 | provider: "aws" # Provider specifies the cloud provider that will be used. Supported providers: "aws", "gcp", "azure" 79 | bucket: "gatling-operator-reports" # S3 Bucket name on which Gatlilng report files are stored 80 | region: "ap-northeast-1" # Optional. Default: "ap-northeast-1" for aws provider. Region name 81 | #env: # Optional. Environment variables to be used for connecting to the cloud providers 82 | # # For S3 see also the env variables for auth: https://rclone.org/s3/#authentication 83 | # - name: AWS_ACCESS_KEY_ID 84 | # value: xxxxxxxxxxxxxxx 85 | # - name: AWS_SECRET_ACCESS_KEY 86 | # valueFrom: 87 | # secretKeyRef: 88 | # name: aws-credentail-secrets 89 | # key: AWS_SECRET_ACCESS_KEY 90 | # 91 | ############################################################# 92 | # Storage Provider - gcp (Google Cloud Storage) 93 | ############################################################# 94 | # provider: "gcp" 95 | # bucket: "gatling-operator-reports" # GCS bucket name on which Gatlilng report files are stored 96 | # 97 | ############################################################# 98 | # Storage Provider - azure (Azure Blob Storage) 99 | ############################################################# 100 | #provider: "azure" 101 | #bucket: "gatling-operator-reports" # Azure Blob Storage container name on which Gatlilng report files are stored 102 | #env: # Optional. Environment variables to be used for connecting to the cloud providers 103 | # - name: AZUREBLOB_ACCOUNT # Azure Blob Storage Account Name 104 | # value: xxxxxxxxxxxxxxx 105 | # - name: AZUREBLOB_KEY # Azure Blob Access Key. Leave blank to use SAS URL 106 | # valueFrom: 107 | # secretKeyRef: 108 | # name: azure-credentail-secrets 109 | # key: AZUREBLOB_KEY 110 | # - name: AZUREBLOB_SAS_URL # SAS URL. "Read, Write, List" permissions are required 111 | # valueFrom: 112 | # secretKeyRef: 113 | # name: azure-credentail-secrets 114 | # key: AZUREBLOB_SAS_URL 115 | notificationServiceSpec: 116 | provider: "slack" # Notification provider name. Supported provider: "slack" 117 | secretName: "gatling-notification-slack-secrets" # The name of secret in which all key/value sets needed for the notification are stored 118 | testScenarioSpec: 119 | # startTime: 2021-09-10 08:45:31 # Optional. Start time of running test scenario in UTC. Format: %Y-%m-%d %H:%M:%S 120 | parallelism: 3 # Optional. Default: 1. Number of pods running at any instant 121 | # simulationsDirectoryPath: "/dir-path-to-simulation" # Optional. Default: /opt/gatling/user-files/simulations 122 | # resourcesDirectoryPath: "dir-path-to-resources" # Optional. Default: /opt/gatling/user-files/resources 123 | # resultsDirectoryPath: "dir-path-to-results" # Optional. Default: /opt/gatling/results. 124 | simulationClass: "PersistentVolumeSampleSimulation" # Gatling simulation class name 125 | env: 126 | - name: ENV 127 | value: "dev" 128 | - name: CONCURRENCY 129 | value: "2" 130 | - name: DURATION 131 | value: "10" 132 | volumeMounts: 133 | - name: resource-vol 134 | mountPath: /opt/gatling/user-files/resources/pv 135 | -------------------------------------------------------------------------------- /config/samples/gatling-worker-service-account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: gatling-operator-worker 5 | --- 6 | apiVersion: rbac.authorization.k8s.io/v1 7 | kind: Role 8 | metadata: 9 | name: pod-reader 10 | rules: 11 | - apiGroups: [""] 12 | resources: ["pods"] 13 | verbs: ["get", "list", "patch"] 14 | --- 15 | apiVersion: rbac.authorization.k8s.io/v1 16 | kind: RoleBinding 17 | metadata: 18 | name: read-pods 19 | subjects: 20 | - kind: ServiceAccount 21 | name: gatling-operator-worker 22 | apiGroup: "" 23 | roleRef: 24 | kind: Role 25 | name: pod-reader 26 | apiGroup: "" 27 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - gatling-notification-slack-secrets.yaml 3 | - gatling-operator_v1alpha1_gatling01.yaml 4 | - gatling-worker-service-account.yaml 5 | -------------------------------------------------------------------------------- /controllers/gatling_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | 11 | gatlingv1alpha1 "github.com/st-tech/gatling-operator/api/v1alpha1" 12 | batchv1 "k8s.io/api/batch/v1" 13 | corev1 "k8s.io/api/core/v1" 14 | "k8s.io/apimachinery/pkg/api/resource" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/utils/pointer" 17 | //+kubebuilder:scaffold:imports 18 | ) 19 | 20 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 21 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 22 | 23 | var _ = Context("Inside of a new namespace", func() { 24 | ctx := context.TODO() 25 | ns := SetupTest(ctx) 26 | gatlingName := "test-gatling" 27 | 28 | Describe("when no existing resources exist", func() { 29 | 30 | It("should create a new Gatling resource with the specified name and a runner Job", func() { 31 | gatling := &gatlingv1alpha1.Gatling{ 32 | ObjectMeta: metav1.ObjectMeta{ 33 | Name: gatlingName, 34 | Namespace: ns.Name, 35 | }, 36 | Spec: gatlingv1alpha1.GatlingSpec{ 37 | GenerateReport: false, 38 | NotifyReport: false, 39 | CleanupAfterJobDone: false, 40 | PodSpec: gatlingv1alpha1.PodSpec{ 41 | SecurityContext: &corev1.PodSecurityContext{ 42 | Sysctls: []corev1.Sysctl{{Name: "net.ipv4.ip_local_port_range", Value: "1024 65535"}}, 43 | }, 44 | RunnerContainerSecurityContext: &corev1.SecurityContext{ 45 | RunAsUser: pointer.Int64Ptr(1000), 46 | RunAsGroup: pointer.Int64Ptr(1000), 47 | }, 48 | }, 49 | TestScenarioSpec: gatlingv1alpha1.TestScenarioSpec{ 50 | SimulationClass: "MyBasicSimulation", 51 | }, 52 | }, 53 | } 54 | err := k8sClient.Create(ctx, gatling) 55 | Expect(err).NotTo(HaveOccurred(), "failed to create test Gatling resource") 56 | 57 | job := &batchv1.Job{} 58 | Eventually(func() error { 59 | return k8sClient.Get( 60 | ctx, client.ObjectKey{Namespace: ns.Name, Name: gatlingName + "-runner"}, job) 61 | }).Should(Succeed()) 62 | //fmt.Printf("parallelism = %d", *job.Spec.Parallelism) 63 | 64 | Expect(job.Spec.Parallelism).Should(Equal(pointer.Int32Ptr(1))) 65 | Expect(job.Spec.Completions).Should(Equal(pointer.Int32Ptr(1))) 66 | }) 67 | 68 | It("should create a new Gatling resource with the specified name and a runner Job with 2 parallelism", func() { 69 | gatling := &gatlingv1alpha1.Gatling{ 70 | ObjectMeta: metav1.ObjectMeta{ 71 | Name: gatlingName, 72 | Namespace: ns.Name, 73 | }, 74 | Spec: gatlingv1alpha1.GatlingSpec{ 75 | GenerateReport: false, 76 | NotifyReport: false, 77 | CleanupAfterJobDone: false, 78 | TestScenarioSpec: gatlingv1alpha1.TestScenarioSpec{ 79 | SimulationClass: "MyBasicSimulation", 80 | Parallelism: 2, 81 | }, 82 | }, 83 | } 84 | err := k8sClient.Create(ctx, gatling) 85 | Expect(err).NotTo(HaveOccurred(), "failed to create test Gatling resource") 86 | 87 | job := &batchv1.Job{} 88 | Eventually(func() error { 89 | return k8sClient.Get( 90 | ctx, client.ObjectKey{Namespace: ns.Name, Name: gatlingName + "-runner"}, job) 91 | }).Should(Succeed()) 92 | fmt.Printf("parallelism = %d", *job.Spec.Parallelism) 93 | 94 | Expect(job.Spec.Parallelism).Should(Equal(pointer.Int32Ptr(2))) 95 | Expect(job.Spec.Completions).Should(Equal(pointer.Int32Ptr(2))) 96 | }) 97 | 98 | It("should create a New Gatling resource with PersistentVolume resources", func() { 99 | pvFS := corev1.PersistentVolumeFilesystem 100 | gatling := &gatlingv1alpha1.Gatling{ 101 | ObjectMeta: metav1.ObjectMeta{ 102 | Name: gatlingName, 103 | Namespace: ns.Name, 104 | }, 105 | Spec: gatlingv1alpha1.GatlingSpec{ 106 | GenerateReport: false, 107 | NotifyReport: false, 108 | CleanupAfterJobDone: false, 109 | PodSpec: gatlingv1alpha1.PodSpec{ 110 | Volumes: []corev1.Volume{ 111 | { 112 | Name: "resource-vol", 113 | VolumeSource: corev1.VolumeSource{ 114 | PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 115 | ClaimName: "resource-pvc", 116 | }, 117 | }, 118 | }, 119 | }, 120 | }, 121 | PersistentVolumeSpec: gatlingv1alpha1.PersistentVolumeSpec{ 122 | Name: "resource-pv", 123 | Spec: corev1.PersistentVolumeSpec{ 124 | VolumeMode: &pvFS, 125 | AccessModes: []corev1.PersistentVolumeAccessMode{ 126 | corev1.ReadWriteOnce, 127 | }, 128 | StorageClassName: "", 129 | Capacity: corev1.ResourceList{ 130 | corev1.ResourceStorage: resource.MustParse("1Gi"), 131 | }, 132 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 133 | Local: &corev1.LocalVolumeSource{Path: "/tmp"}, 134 | }, 135 | NodeAffinity: &corev1.VolumeNodeAffinity{ 136 | Required: &corev1.NodeSelector{ 137 | NodeSelectorTerms: []corev1.NodeSelectorTerm{ 138 | { 139 | MatchExpressions: []corev1.NodeSelectorRequirement{{Key: "kubernetes.io/os", Operator: corev1.NodeSelectorOpIn, Values: []string{"linux"}}}, 140 | }, 141 | }, 142 | }, 143 | }, 144 | }, 145 | }, 146 | PersistentVolumeClaimSpec: gatlingv1alpha1.PersistentVolumeClaimSpec{ 147 | Name: "resource-pvc", 148 | Spec: corev1.PersistentVolumeClaimSpec{ 149 | AccessModes: []corev1.PersistentVolumeAccessMode{ 150 | corev1.ReadWriteOnce, 151 | }, 152 | VolumeName: "resource-pv", 153 | Resources: corev1.ResourceRequirements{ 154 | Requests: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse("1Gi")}, 155 | }, 156 | }, 157 | }, 158 | TestScenarioSpec: gatlingv1alpha1.TestScenarioSpec{ 159 | SimulationClass: "PersistentVolumeSampleSimulation", 160 | Parallelism: 1, 161 | VolumeMounts: []corev1.VolumeMount{ 162 | { 163 | Name: "resource-vol", 164 | MountPath: "/opt/gatling/user-files/resources/pv", 165 | }, 166 | }, 167 | }, 168 | }, 169 | } 170 | err := k8sClient.Create(ctx, gatling) 171 | Expect(err).NotTo(HaveOccurred(), "failed to create test Gatling resource") 172 | 173 | job := &batchv1.Job{} 174 | Eventually(func() error { 175 | return k8sClient.Get( 176 | ctx, client.ObjectKey{Namespace: ns.Name, Name: gatlingName + "-runner"}, job) 177 | }).Should(Succeed()) 178 | 179 | pv := &corev1.PersistentVolume{} 180 | Eventually(func() error { 181 | return k8sClient.Get( 182 | ctx, client.ObjectKey{Namespace: ns.Name, Name: "resource-pv"}, pv) 183 | }).Should(Succeed()) 184 | 185 | pvc := &corev1.PersistentVolumeClaim{} 186 | Eventually(func() error { 187 | return k8sClient.Get( 188 | ctx, client.ObjectKey{Namespace: ns.Name, Name: "resource-pvc"}, pvc) 189 | }).Should(Succeed()) 190 | }) 191 | 192 | }) 193 | }) 194 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © ZOZO, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "math/rand" 22 | "path/filepath" 23 | "testing" 24 | "time" 25 | 26 | . "github.com/onsi/ginkgo" 27 | . "github.com/onsi/gomega" 28 | corev1 "k8s.io/api/core/v1" 29 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | "k8s.io/client-go/kubernetes/scheme" 31 | "k8s.io/client-go/rest" 32 | ctrl "sigs.k8s.io/controller-runtime" 33 | "sigs.k8s.io/controller-runtime/pkg/client" 34 | "sigs.k8s.io/controller-runtime/pkg/controller" 35 | "sigs.k8s.io/controller-runtime/pkg/envtest" 36 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 37 | logf "sigs.k8s.io/controller-runtime/pkg/log" 38 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 39 | 40 | gatling1alpha1 "github.com/st-tech/gatling-operator/api/v1alpha1" 41 | //+kubebuilder:scaffold:imports 42 | ) 43 | 44 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 45 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 46 | 47 | var cfg *rest.Config 48 | var k8sClient client.Client 49 | var testEnv *envtest.Environment 50 | 51 | func TestAPIs(t *testing.T) { 52 | RegisterFailHandler(Fail) 53 | 54 | RunSpecsWithDefaultAndCustomReporters(t, 55 | "Controller Suite", 56 | []Reporter{printer.NewlineReporter{}}) 57 | } 58 | 59 | var _ = BeforeSuite(func() { 60 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 61 | 62 | By("bootstrapping test environment") 63 | testEnv = &envtest.Environment{ 64 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 65 | ErrorIfCRDPathMissing: true, 66 | } 67 | 68 | // Start etcd and kube-apiserver 69 | var err error 70 | cfg, err = testEnv.Start() 71 | Expect(err).NotTo(HaveOccurred()) 72 | Expect(cfg).NotTo(BeNil()) 73 | 74 | err = gatling1alpha1.AddToScheme(scheme.Scheme) 75 | Expect(err).NotTo(HaveOccurred()) 76 | 77 | //+kubebuilder:scaffold:scheme 78 | 79 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 80 | Expect(err).NotTo(HaveOccurred()) 81 | Expect(k8sClient).NotTo(BeNil()) 82 | 83 | }, 60) 84 | 85 | var _ = AfterSuite(func() { 86 | By("tearing down the test environment") 87 | // End etcd and kube-apiserver 88 | err := testEnv.Stop() 89 | Expect(err).NotTo(HaveOccurred()) 90 | }) 91 | 92 | /* 93 | * The following functions refer to 94 | * https://github.com/jetstack/kubebuilder-sample-controller/blob/b39c135ee30d23aa8d8dbeaee60c2fb3639c6f65/controllers/suite_test.go 95 | */ 96 | // SetupTest will set up a testing environment. 97 | // This includes: 98 | // * creating a Namespace to be used during the test 99 | // * starting the 'GatlingReconciler' 100 | // * stopping the 'GatlingReconciler' after the test ends 101 | // Call this function at the start of each of your tests. 102 | func SetupTest(ctx context.Context) *corev1.Namespace { 103 | var stopFunc func() 104 | ns := &corev1.Namespace{} 105 | 106 | BeforeEach(func() { 107 | *ns = corev1.Namespace{ 108 | ObjectMeta: metav1.ObjectMeta{Name: "testns-" + randStringRunes(5)}, 109 | } 110 | 111 | err := k8sClient.Create(ctx, ns) 112 | Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") 113 | 114 | time.Sleep(100 * time.Millisecond) 115 | 116 | mgr, err := ctrl.NewManager(cfg, ctrl.Options{}) 117 | Expect(err).NotTo(HaveOccurred(), "failed to create manager") 118 | 119 | controllers := &GatlingReconciler{ 120 | Client: mgr.GetClient(), 121 | Scheme: mgr.GetScheme(), 122 | } 123 | 124 | err = controllers.SetupWithManager(mgr, controller.Options{MaxConcurrentReconciles: 1}) 125 | Expect(err).NotTo(HaveOccurred(), "failed to setup controller") 126 | 127 | ctx, cancel := context.WithCancel(ctx) 128 | stopFunc = cancel 129 | go func() { 130 | err := mgr.Start(ctx) 131 | Expect(err).NotTo(HaveOccurred(), "failed to start manager") 132 | }() 133 | time.Sleep(100 * time.Millisecond) 134 | }) 135 | 136 | AfterEach(func() { 137 | stopFunc() 138 | err := k8sClient.Delete(ctx, ns) 139 | Expect(err).NotTo(HaveOccurred(), "failed to delete test namespace") 140 | }) 141 | 142 | return ns 143 | } 144 | 145 | func init() { 146 | rand.Seed(time.Now().UnixNano()) 147 | } 148 | 149 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890") 150 | 151 | func randStringRunes(n int) string { 152 | b := make([]rune, n) 153 | for i := range b { 154 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 155 | } 156 | return string(b) 157 | } 158 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ## Packages 4 | - [gatling-operator.tech.zozo.com/v1alpha1](#gatling-operatortechzozocomv1alpha1) 5 | 6 | 7 | ## gatling-operator.tech.zozo.com/v1alpha1 8 | 9 | Package v1alpha1 contains API Schema definitions for the gatling-operator v1alpha1 API group 10 | 11 | ### Resource Types 12 | - [Gatling](#gatling) 13 | 14 | 15 | 16 | #### CloudStorageSpec 17 | 18 | 19 | 20 | CloudStorageSpec defines Cloud Storage Provider specification. 21 | 22 | _Appears in:_ 23 | - [GatlingSpec](#gatlingspec) 24 | 25 | | Field | Description | 26 | | --- | --- | 27 | | `provider` _string_ | (Required) Provider specifies the cloud provider that will be used. Supported providers: `aws`, `gcp`, and `azure` | 28 | | `bucket` _string_ | (Required) Storage Bucket Name. | 29 | | `region` _string_ | (Optional) Region Name. | 30 | | `env` _[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#envvar-v1-core) array_ | (Optional) Environment variables used for connecting to the cloud providers. | 31 | 32 | 33 | #### Gatling 34 | 35 | 36 | 37 | Gatling is the Schema for the gatlings API 38 | 39 | 40 | 41 | | Field | Description | 42 | | --- | --- | 43 | | `apiVersion` _string_ | `gatling-operator.tech.zozo.com/v1alpha1` 44 | | `kind` _string_ | `Gatling` 45 | | `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | 46 | | `spec` _[GatlingSpec](#gatlingspec)_ | GatlingSpec defines the desired state of Gatling | 47 | 48 | 49 | #### GatlingSpec 50 | 51 | 52 | 53 | GatlingSpec defines the desired state of Gatling 54 | 55 | _Appears in:_ 56 | - [Gatling](#gatling) 57 | 58 | | Field | Description | 59 | | --- | --- | 60 | | `generateReport` _boolean_ | (Optional) The flag of generating gatling report. Defaults to `false` | 61 | | `generateLocalReport` _boolean_ | (Optional) The flag of generating gatling report at each pod | 62 | | `notifyReport` _boolean_ | (Optional) The flag of notifying gatling report. Defaults to `false` | 63 | | `cleanupAfterJobDone` _boolean_ | (Optional) The flag of cleanup gatling resources after the job done. Defaults to `false` | 64 | | `podSpec` _[PodSpec](#podspec)_ | (Optional) Gatling Pod specification. | 65 | | `cloudStorageSpec` _[CloudStorageSpec](#cloudstoragespec)_ | (Optional) Cloud Storage Provider specification. | 66 | | `persistentVolume` _[PersistentVolumeSpec](#persistentvolumespec)_ | (Optional) PersistentVolume specification. | 67 | | `persistentVolumeClaim` _[PersistentVolumeClaimSpec](#persistentvolumeclaimspec)_ | (Optional) PersistentVolumeClaim specification. | 68 | | `notificationServiceSpec` _[NotificationServiceSpec](#notificationservicespec)_ | (Optional) Notification Service specification. | 69 | | `testScenarioSpec` _[TestScenarioSpec](#testscenariospec)_ | (Required) Test Scenario specification | 70 | 71 | 72 | 73 | 74 | #### NotificationServiceSpec 75 | 76 | 77 | 78 | NotificationServiceSpec defines Notification Service Provider specification. 79 | 80 | _Appears in:_ 81 | - [GatlingSpec](#gatlingspec) 82 | 83 | | Field | Description | 84 | | --- | --- | 85 | | `provider` _string_ | (Required) Provider specifies notification service provider. Supported providers: `slack` | 86 | | `secretName` _string_ | (Required) The name of secret in which all key/value sets needed for the notification are stored. | 87 | 88 | 89 | #### PersistentVolumeClaimSpec 90 | 91 | 92 | 93 | 94 | 95 | _Appears in:_ 96 | - [GatlingSpec](#gatlingspec) 97 | 98 | | Field | Description | 99 | | --- | --- | 100 | | `name` _string_ | (Required) The name of the PersistentVolumeClaim. | 101 | | `spec` _[PersistentVolumeClaimSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#persistentvolumeclaimspec-v1-core)_ | (Required) PersistentVolumeClaimSpec is the specification of a persistent volume. | 102 | 103 | 104 | #### PersistentVolumeSpec 105 | 106 | 107 | 108 | 109 | 110 | _Appears in:_ 111 | - [GatlingSpec](#gatlingspec) 112 | 113 | | Field | Description | 114 | | --- | --- | 115 | | `name` _string_ | (Required) The name of the PersistentVolume. | 116 | | `spec` _[PersistentVolumeSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#persistentvolumespec-v1-core)_ | (Required) PersistentVolumeSpec is the specification of a persistent volume. | 117 | 118 | 119 | #### PodSpec 120 | 121 | 122 | 123 | PodSpec defines type to configure Gatling Pod specification. For the idea of PodSpec, refer to [bitpoke/mysql-operator](https://github.com/bitpoke/mysql-operator/blob/master/pkg/apis/mysql/v1alpha1/mysqlcluster_types.go) 124 | 125 | _Appears in:_ 126 | - [GatlingSpec](#gatlingspec) 127 | 128 | | Field | Description | 129 | | --- | --- | 130 | | `gatlingImage` _string_ | (Optional) The image that will be used for Gatling container. Defaults to `ghcr.io/st-tech/gatling:latest` | 131 | | `rcloneImage` _string_ | (Optional) The image that will be used for rclone conatiner. Defaults to `rclone/rclone:latest` | 132 | | `resources` _[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#resourcerequirements-v1-core)_ | (Optional) Resources specifies the resource limits of the container. | 133 | | `affinity` _[Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#affinity-v1-core)_ | (Optional) Affinity specification. | 134 | | `tolerations` _[Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#toleration-v1-core) array_ | (Optional) Tolerations specification. | 135 | | `serviceAccountName` _string_ | (Required) ServiceAccountName specification. | 136 | | `volumes` _[Volume](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#volume-v1-core) array_ | (Optional) volumes specification. | 137 | | `securityContext` _[PodSecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#podsecuritycontext-v1-core)_ | (Optional) SecurityContext specification. | 138 | | `runnerContainerSecurityContext` _[SecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#securitycontext-v1-core)_ | (Optional) RunnerContainerSecurityContext specifies the SecurityContext of the Gatling runner container. | 139 | 140 | 141 | #### TestScenarioSpec 142 | 143 | 144 | 145 | TestScenarioSpec defines the load testing scenario 146 | 147 | _Appears in:_ 148 | - [GatlingSpec](#gatlingspec) 149 | 150 | | Field | Description | 151 | | --- | --- | 152 | | `startTime` _string_ | (Optional) Test Start time. | 153 | | `parallelism` _integer_ | (Optional) Number of pods running at the same time. Defaults to `1` (Minimum `1`) | 154 | | `simulationsFormat` _string_ | (Optional) Gatling simulation format, supports `bundle` and `gradle`. Defaults to `bundle` | 155 | | `simulationsDirectoryPath` _string_ | (Optional) Gatling Resources directory path where simulation files are stored. Defaults to `/opt/gatling/user-files/simulations` | 156 | | `resourcesDirectoryPath` _string_ | (Optional) Gatling Simulation directory path where resources are stored. Defaults to `/opt/gatling/user-files/resources` | 157 | | `resultsDirectoryPath` _string_ | (Optional) Gatling Results directory path where results are stored. Defaults to `/opt/gatling/results` | 158 | | `simulationClass` _string_ | (Required) Simulation Class Name. | 159 | | `simulationData` _object (keys:string, values:string)_ | (Optional) Simulation Data. | 160 | | `resourceData` _object (keys:string, values:string)_ | (Optional) Resource Data. | 161 | | `gatlingConf` _object (keys:string, values:string)_ | (Optional) Gatling Configurations. | 162 | | `env` _[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#envvar-v1-core) array_ | (Optional) Environment variables used for running load testing scenario. | 163 | | `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#volumemount-v1-core) array_ | (Optional) Pod volumes to mount into the container's filesystem. | 164 | 165 | 166 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Gatling Operator Architecture and Design 2 | 3 | 4 | 5 | - [Gatling Operator Architecture and Design](#gatling-operator-architecture-and-design) 6 | - [Architecture Overview](#architecture-overview) 7 | - [Gatling Runner Pod as Multi Container Pod](#gatling-runner-pod-as-multi-container-pod) 8 | 9 | 10 | 11 | ## Architecture Overview 12 | 13 | The diagram below shows how Gatling Operator works in conjunction with Gatling Custom Resource (CR). 14 | Based on Gatling CR, actions in the lifecycle of distributed Gatling load testing (such as running load testing, generating reports, sending notification message, and cleaning up the resources) are executed by relevant custom controller (`Gatling Controller` in figure below). 15 | 16 | ![](../assets/gatling-operator-arch.svg) 17 | 18 | 1. Create Gatling Runner Job 19 | - Gatling Runner Job runs Gatling Runner Pods in parallel. The parallelism field (`.spec.testScenarioSpec.parallelism`) specifies the maximum desired number of Pods the Job should run concurrently 20 | - The Gatling Runner Pod executes Gatling load testing scenario and uploads generated Gatling report file (`simulation.log`) to user-selected Cloud Storage (can be configured in `.spec.cloudStorageSpec`). 21 | 2. Create Gatling Reporter Job (Optional) 22 | - Once that the Gatling Runner Job is complete, Gatling Controller creates The Gatling Reporter Job if `.spec.generateReport` is set to true 23 | - Gatling Reporter Job runs Gatling Reporter Pod 24 | - Gatling Reporter Pod download all uploaded simulation.log on the Cloud Storage to its local filesystem, then generates an aggregated HTML report from the simulation.log(s) and upload the HTML report to the Cloud Storage 25 | 3. Post Webhook Message to Notification Service Provider (Optional) 26 | - Onece that all prior Jobs are complete, Gatling Controller posts webhook message to selected Notification Service Provider (can be configured in `.spec.notificationServiceSpec`) in order to notify Gatling load testing result 27 | - It's executed only if `.spec.notifyReport` is set to true 28 | 4. Cleanup all Gatling CR relevant resources (Optional) 29 | - Gatling Controller finally cleanup all Gatling CR related resources such as Jobs and Pods only if `.spec.cleanupAfterJobDone` is set to true 30 | 31 | ## Gatling Runner Pod as Multi Container Pod 32 | 33 | Gatling Runner Pod runs multiple containers. The diagram below shows how those containers in each phase in Gatling Runner Pod works. 34 | 35 | ![](../assets/gatling-operator-pod.svg) 36 | 37 | - gatling-waiter 38 | - Waits node scaling before starting to execute Gatling load testing scenario 39 | - In the case of running multiple Gatling Runner Pods, there is no guarantee that all Pods will be scheduled at the same timing. Thus, prior to the gatling-runner, gatling-waiter comes into play to achieve synchronization of the timing of all gatling-runners' execution by waiting for all Pods to be ready 40 | - gatling-runner 41 | - Executes Gatling load testing scenario 42 | - Stores its generated report file (`simulation.log`) to an emptyDir volume 43 | - gatling-result-transferer 44 | - Uploads the simulation.log file to to user-selected Cloud Storage (can be configured in `.spec.cloudStorageSpec`) 45 | 46 | 📝 It is noted that **from gatling-operator-v0.8.1** gatling-waiter runs as an init container and both gatling-runner and gatling-result-transferer runs as main containers in a Gatling Runner Pod. However, **before gatling-operator-v0.8.1** both gatling-waiter and gatling-runner run as init containers and gatling-result-transferer as a main container in the case of generating an aggregated Gatling result report while gatling-runner runs as a main container in the case of not generating the report. 47 | -------------------------------------------------------------------------------- /docs/build-guide.md: -------------------------------------------------------------------------------- 1 | # How to build and run Gatling Operator 2 | 3 | This guide explains how to build Gatling Operator from its source code and run a sample in your local environment. 4 | In this guide, we use makefile to build and run Gatling Operator. 5 | 6 | - [How to build and run Gatling Operator](#how-to-build-and-run-gatling-operator) 7 | - [Pre-requisites](#pre-requisites) 8 | - [Get the Source code](#get-the-source-code) 9 | - [Install the tools](#install-the-tools) 10 | - [Create a Kubernetes cluster](#create-a-kubernetes-cluster) 11 | - [Build \& Deploy Gatling Operator](#build--deploy-gatling-operator) 12 | - [Create All in One manifest](#create-all-in-one-manifest) 13 | 14 | ## Pre-requisites 15 | 16 | ### Get the Source code 17 | 18 | The main repository is `st-tech/gatling-operator`. 19 | This contains the Gatling Operator source code and the build scripts. 20 | 21 | ``` 22 | git clone https://github.com/st-tech/gatling-operator 23 | ``` 24 | 25 | ### Install the tools 26 | 27 | - [kubectl](https://kubernetes.io/docs/tasks/tools/) 28 | - [kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) 29 | - [go](https://go.dev/doc/install) 30 | 31 | ## Create a Kubernetes cluster 32 | 33 | Create a Kubernetes cluster for test Gatling Operator sample using kind. 34 | 35 | ```bash 36 | make kind-create 37 | ``` 38 | 39 |
40 | sample output 41 | 42 | ```bash 43 | ❯ make kind-install 44 | 45 | No kind clusters found. 46 | Creating Cluster 47 | kind create cluster --name "gatling-cluster" --image=kindest/node:v1.19.11 --config ~/github/gatling-operator/config/kind/cluster.yaml 48 | Creating cluster "gatling-cluster" ... 49 | ✓ Ensuring node image (kindest/node:v1.19.11) 🖼 50 | ✓ Preparing nodes 📦 📦 📦 51 | ✓ Writing configuration 📜 52 | ✓ Starting control-plane 🕹️ 53 | ✓ Installing CNI 🔌 54 | ✓ Installing StorageClass 💾 55 | ✓ Joining worker nodes 🚜 56 | Set kubectl context to "kind-gatling-cluster" 57 | You can now use your cluster with: 58 | 59 | kubectl cluster-info --context kind-gatling-cluster 60 | ``` 61 | 62 |
63 | 64 | `make kind-create` command creates a Kubernetes cluster named `gatling-cluster` using kind. 65 | You can check the cluster details with the following commands. 66 | Get node information with kubectl and check that one control plane and one worker are ready. 67 | 68 | ```bash 69 | ❯ kind get clusters 70 | gatling-cluster 71 | ❯ kubectl get node 72 | NAME STATUS ROLES AGE VERSION 73 | gatling-cluster-control-plane Ready master 150m v1.19.11 74 | gatling-cluster-worker Ready 150m v1.19.11 75 | ``` 76 | 77 | If your cluster contexts are not set, you can set the contexts with the following command. 78 | 79 | ```bash 80 | kubectl config get-contexts 81 | kubectl config use-context kind-gatling-cluster 82 | ``` 83 | 84 | ## Build & Deploy Gatling Operator 85 | 86 | 1. Build Gatling Operator with makefile. 87 | 88 | ```bash 89 | make build 90 | ``` 91 | 92 | 2. Install CRD to your Kubernetes cluster. 93 | 94 | ```bash 95 | make install-crd 96 | ``` 97 | 98 | You can check the Gatling Operator CRD with the following command. 99 | 100 | ```bash 101 | ❯ kubectl get crd 102 | NAME CREATED AT 103 | gatlings.gatling-operator.tech.zozo.com 2023-08-01T04:43:54Z 104 | ``` 105 | 106 | 3. Try to run Controller Manager in local 107 | 108 | ```bash 109 | make run 110 | ``` 111 | 112 | The commands runs Gatling Operator Controller Manager in your local environment. You can stop in ctrl+c. 113 | 114 | ``` 115 | ~/github/gatling-operator/bin/controller-gen "crd:trivialVersions=true,preserveUnknownFields=false" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases 116 | ~/github/gatling-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..." 117 | go fmt ./... 118 | api/v1alpha1/zz_generated.deepcopy.go 119 | go vet ./... 120 | go run ./main.go 121 | 2021-10-05T11:26:35.704+0900 INFO controller-runtime.metrics metrics server is starting to listen {"addr": ":8080"} 122 | 2021-10-05T11:26:35.705+0900 INFO setup starting manager 123 | 2021-10-05T11:26:35.705+0900 INFO controller-runtime.manager starting metrics server {"path": "/metrics"} 124 | 2021-10-05T11:26:35.705+0900 INFO controller-runtime.manager.controller.gatling Starting EventSource {"reconciler group": "gatling-operator.tech.zozo.com", "reconciler kind": "Gatling", "source": "kind source: /, Kind="} 125 | 2021-10-05T11:26:35.810+0900 INFO controller-runtime.manager.controller.gatling Starting Controller {"reconciler group": "gatling-operator.tech.zozo.com", "reconciler kind": "Gatling"} 126 | 2021-10-05T11:26:35.810+0900 INFO controller-runtime.manager.controller.gatling Starting workers {"reconciler group": "gatling-operator.tech.zozo.com", "reconciler kind": "Gatling", "worker count": 1} 127 | ... snip... 128 | ``` 129 | 130 | 4. Build Docker image 131 | 132 | ``` 133 | : This build command image tag is %Y%m%d-%H%M%S format Timestamp 134 | make docker-build 135 | : You can define Image name and tag in this command 136 | make docker-build IMG=/zozo-gatling-operator: 137 | ``` 138 | 139 |
140 | Sample 141 | 142 | ```bash 143 | ❯ DOCKER_REGISTRY=1234567890.dkr.ecr.ap-northeast-1.amazonaws.com 144 | ❯ DOCKER_IMAGE_REPO=zozo-gatling-operator 145 | ❯ DOCKER_IMAGE_TAG=v0.0.1 146 | ❯ make docker-build IMG=${DOCKER_REGISTRY}/${DOCKER_IMAGE_REPO}:${DOCKER_IMAGE_TAG} 147 | ❯ docker images 148 | REPOSITORY TAG IMAGE ID CREATED SIZE 149 | 1234567890.dkr.ecr.ap-northeast-1.amazonaws.com/zozo-gatling-operator v0.0.1 c66287dc8dc4 3 hours ago 46.2MB 150 | ``` 151 | 152 |
153 | 154 | 5. Deploy Controller to Cluster 155 | 156 | - Deploy to Local Kind Cluster 157 | 158 | ```bash 159 | make kind-deploy 160 | ``` 161 | 162 | - Deploy to Remote k8s Cluster 163 | 164 | ```bash 165 | make deploy IMG=${DOCKER_REGISTRY}/${DOCKER_IMAGE_REPO}:${DOCKER_IMAGE_TAG} 166 | ``` 167 | 168 | - You can check gatling-operator controller forom the following command. 169 | 170 | ```bash 171 | ❯ kubectl get pods -n gatling-system 172 | NAME READY STATUS RESTARTS AGE 173 | gatling-operator-controller-manager-579bd7bc49-h46l2 2/2 Running 0 31m 174 | ``` 175 | 176 | 6. Deploy sample scenario and check Gatling Operator works 177 | 178 | ```bash 179 | kustomize build config/samples | kubectl apply -f - 180 | ``` 181 | 182 | ## Create All in One manifest 183 | 184 | This command creates an all in one manifest for Gatling Operator. 185 | All in One manifest create CRD and Gatling Operator Controller. 186 | 187 | ```bash 188 | make manifests-release IMG=/zozo-gatling-operator: 189 | ``` 190 | 191 | You can apply Gatling Operator to your Kubernetes cluster with the following command. 192 | 193 | ```bash 194 | kubectl apply -f gatling-operator.yaml 195 | ``` 196 | -------------------------------------------------------------------------------- /docs/quickstart-guide.md: -------------------------------------------------------------------------------- 1 | # Quick Start Guide 2 | 3 | 4 | 5 | - [Quick Start Guide](#quick-start-guide) 6 | - [Prerequisites](#prerequisites) 7 | - [Create a Cluster](#create-a-cluster) 8 | - [Install Gatling Operator](#install-gatling-operator) 9 | - [Run your first load testing by deploying Gatling CR](#run-your-first-load-testing-by-deploying-gatling-cr) 10 | - [Configure Cloud Storage Provider for storing Gatling reports](#configure-cloud-storage-provider-for-storing-gatling-reports) 11 | - [Configure Notification Service Provider and notify Gatling load testing results via Slack](#configure-notification-service-provider-and-notify-gatling-load-testing-results-via-slack) 12 | 13 | 14 | 15 | 16 | The quick start guide helps to quickly deploy Gatling Operator and start a simple distributed load testing using Gatling Operator. 17 | 18 | ## Prerequisites 19 | 20 | - Install [kubectl](https://kubernetes.io/docs/tasks/tools/) and [kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) 21 | - Clone the [gatling-operator](https://github.com/st-tech/gatling-operator) repository 22 | 23 | ## Create a Cluster 24 | Create a cluster using kind. 25 | 1.18 or higher version is recommended for the Kubernetes cluster. Node Images for kind can be found in the [release notes](https://github.com/kubernetes-sigs/kind/releases). 26 | 27 | ```bash 28 | kind create cluster 29 | ``` 30 | 31 | ## Install Gatling Operator 32 | 33 | ```bash 34 | kubectl apply -f https://github.com/st-tech/gatling-operator/releases/download/v0.9.0/gatling-operator.yaml 35 | ``` 36 | 37 | Expected output would be like this: 38 | 39 | ```bash 40 | namespace/gatling-system created 41 | customresourcedefinition.apiextensions.k8s.io/gatlings.gatling-operator.tech.zozo.com created 42 | serviceaccount/gatling-operator-controller-manager created 43 | role.rbac.authorization.k8s.io/gatling-operator-leader-election-role created 44 | clusterrole.rbac.authorization.k8s.io/gatling-operator-manager-role created 45 | rolebinding.rbac.authorization.k8s.io/gatling-operator-leader-election-rolebinding created 46 | clusterrolebinding.rbac.authorization.k8s.io/gatling-operator-manager-rolebinding created 47 | deployment.apps/gatling-operator-controller-manager created 48 | ``` 49 | 50 | All resources required for the Gatling operator, such as CRD and controller manager, are deployed using the command above. Now you're ready to deploy Gatling CR to run Gatling load testing. 51 | 52 | The command above applies a Gatling Operator manifest of v0.9.0. Please change the version if necessary. You can check the version from the [Release page](https://github.com/st-tech/gatling-operator/releases). 53 | 54 | ## Run your first load testing by deploying Gatling CR 55 | 56 | Deploy a sample Gatling CR, as well as all resources required for the Gatling CR in the repository. 57 | ``` 58 | kustomize build config/samples | kubectl apply -f - 59 | ``` 60 | 61 | Expected output would be like this: 62 | 63 | ```bash 64 | serviceaccount/gatling-operator-worker unchanged 65 | role.rbac.authorization.k8s.io/pod-reader unchanged 66 | rolebinding.rbac.authorization.k8s.io/read-pods configured 67 | secret/gatling-notification-slack-secrets unchanged 68 | gatling.gatling-operator.tech.zozo.com/gatling-sample01 created 69 | ``` 70 | 71 | 72 | After deploying the Gatling CR, Gatling Controller creates a Gatling Runner job. The job then runs the Gatling Runner Pods to execute Gatling load test scenario in parallel. 73 | 74 | ``` 75 | $ kubectl get gatling,job,pod 76 | ``` 77 | 78 | Expected output would be like this: 79 | 80 | ``` 81 | NAME RUNNED REPORTED NOTIFIED REPORTURL AGE 82 | gatling.gatling-operator.tech.zozo.com/gatling-sample01 0/3 https://gatling-operator-reports.s3.amazonaws.com/gatling-sample01/318807881/index.html 24s 83 | 84 | NAME COMPLETIONS DURATION AGE 85 | job.batch/gatling-sample01-runner 0/3 24s 24s 86 | 87 | NAME READY STATUS RESTARTS AGE 88 | pod/gatling-sample01-runner-8rhl4 2/2 Running 0 24s 89 | pod/gatling-sample01-runner-cg8rt 2/2 Running 0 24s 90 | pod/gatling-sample01-runner-tkplh 2/2 Running 0 24s 91 | ``` 92 | 93 | You can also see from the Pod logs that Gatling is running. 94 | 95 | ``` 96 | kubectl logs gatling-sample01-runner-tkplh -c gatling-runner -f 97 | ``` 98 | 99 | Expected output would be like this: 100 | 101 | ```bash 102 | Wait until 2022-11-28 00:34:56 103 | GATLING_HOME is set to /opt/gatling 104 | Simulation MyBasicSimulation started... 105 | 106 | ================================================================================ 107 | 2022-11-28 00:37:17 5s elapsed 108 | ---- Requests ------------------------------------------------------------------ 109 | > Global (OK=2 KO=0 ) 110 | > request_1 (OK=1 KO=0 ) 111 | > request_1 Redirect 1 (OK=1 KO=0 ) 112 | 113 | ---- Scenario Name ------------------------------------------------------------- 114 | [--------------------------------------------------------------------------] 0% 115 | waiting: 0 / active: 1 / done: 0 116 | ================================================================================ 117 | 118 | 119 | ================================================================================ 120 | 2022-11-28 00:37:22 10s elapsed 121 | ---- Requests ------------------------------------------------------------------ 122 | > Global (OK=3 KO=0 ) 123 | > request_1 (OK=1 KO=0 ) 124 | > request_1 Redirect 1 (OK=1 KO=0 ) 125 | > request_2 (OK=1 KO=0 ) 126 | 127 | ---- Scenario Name ------------------------------------------------------------- 128 | [--------------------------------------------------------------------------] 0% 129 | waiting: 0 / active: 1 / done: 0 130 | ================================================================================ 131 | 132 | 133 | ================================================================================ 134 | 2022-11-28 00:37:27 14s elapsed 135 | ---- Requests ------------------------------------------------------------------ 136 | > Global (OK=6 KO=0 ) 137 | > request_1 (OK=1 KO=0 ) 138 | > request_1 Redirect 1 (OK=1 KO=0 ) 139 | > request_2 (OK=1 KO=0 ) 140 | > request_3 (OK=1 KO=0 ) 141 | > request_4 (OK=1 KO=0 ) 142 | > request_4 Redirect 1 (OK=1 KO=0 ) 143 | 144 | ---- Scenario Name ------------------------------------------------------------- 145 | [##########################################################################]100% 146 | waiting: 0 / active: 0 / done: 1 147 | ================================================================================ 148 | 149 | Simulation MyBasicSimulation completed in 15 seconds 150 | ``` 151 | 152 | As configured in [the sample manifest](https://github.com/st-tech/gatling-operator/blob/85e69840274214c47e63f65a5c807dd541dff245/config/samples/gatling-operator_v1alpha1_gatling01.yaml#L6-L8), an aggregated Gatling HTML report is not created, nor a notification message is posted. 153 | 154 | You can generate the Gatling HTML report by enabling `.spec.generateReport` flag and setting the `.spec.cloudStorageSpec`. Also you can posting the notification message by enabling `.spec.notifyReport` and setting `.spec.notificationServiceSpec`. 155 | 156 | ## Configure Cloud Storage Provider for storing Gatling reports 157 | 158 | As a next step, let's generate the Gatling HTML reports for the Gatling load testing. 159 | 160 | First of all, set `.spec.generateReport` in Gatling CR to `true` in order to generate an aggregated Gatling report. In addition, set `.spec.cleanupAfterJobDone` to `true` in order not to cleanup Gatling resources after the Gatling job done. 161 | 162 | ```yaml 163 | apiVersion: gatling-operator.tech.zozo.com/v1alpha1 164 | kind: Gatling 165 | metadata: 166 | name: gatling-sample01 167 | spec: 168 | generateReport: true # The flag of generating Gatling report 169 | generateLocalReport: false # The flag of generating Gatling report for each pod 170 | notifyReport: false # The flag of notifying Gatling report 171 | cleanupAfterJobDone: true # The flag of cleanup Gatling resources after the job done 172 | ``` 173 | 174 | Then, configure Cloud Storage Provider for storing Gatling reports in `.spec.cloudStorageSpec`. In this case, let's store the reports to Amazon S3 bucket. 175 | Suppose that you want to store the reports to a bucket named `gatling-operator-reports` of Amazon S3 located in `ap-northeast-1` region, configure each fields in `.spec.cloudStorageSpec` like this: 176 | 177 | ```yaml 178 | apiVersion: gatling-operator.tech.zozo.com/v1alpha1 179 | kind: Gatling 180 | metadata: 181 | name: gatling-sample01 182 | spec: 183 | cloudStorageSpec: 184 | provider: "aws" 185 | bucket: "gatling-operator-reports" 186 | region: "ap-northeast-1" 187 | ``` 188 | 189 | You might want to add more configurations in `.spec.cloudStorageSpec` for Gatling Pods to access the Amazon S3 bucket. It depends on S3 authentication methods you choose. Please check [Set Amazon S3 as Cloud Storage](user-guide.md#set-amazon-s3-as-cloud-storage) for more details. 190 | 191 | 📝 **Note**: There are multiple Cloud Storage Provider options. Currently `Amazon S3`, `Google Cloud Storage`, and `Azure Blob Storage` are supported. Please check [User Guide](./user-guide.md) for more details. 192 | 193 | Once finish adding the Cloud Storage Provider configuration in the Gatling CR named `gatling-sample01`, deploy it and wait until it's done. 194 | 195 | ```bash 196 | kustomize build config/samples | kubectl apply -f - 197 | ``` 198 | 199 | You can obtain the Gatling report URL (`reportUrl`) by checking `.status`. 200 | 201 | ```bash 202 | kubectl get gatling gatling-sample01 -o jsonpath='{@.status}' |jq 203 | 204 | { 205 | "reportCompleted": true 206 | "reportStoragePath": "s3:gatling-operator-reports/gatling-sample01/318807881" 207 | "reportUrl": "https://gatling-operator-reports.s3.amazonaws.com/gatling-sample01/318807881/index.html" 208 | "reporterJobName": gatling-sample01-reporter 209 | "reporterStartTime": 1669595852, 210 | "runnerCompleted": true, 211 | "runnerCompletions": "3/3", 212 | "runnerJobName": "gatling-sample01-runner", 213 | "runnerStartTime": 1669595687, 214 | "succeeded": 3 215 | } 216 | ``` 217 | 218 | Here is a sample Gatling HTML report: 219 | 220 | ![](../assets/gatling-html-report.png) 221 | 222 | Finally, cleanup the Gatling CR manually 223 | 224 | ```bash 225 | kubectl delete gatling gatling-sample01 226 | ``` 227 | 228 | 229 | ## Configure Notification Service Provider and notify Gatling load testing results via Slack 230 | 231 | In this step, let's add Notification Service Provider configuration to the same Gatling CR named `gatling-sample01` in order to notify Gatling load testing results via Slack. 232 | 233 | First of all, set `.spec.notifyReport` to `true` in order to enable Notification Service Provider. 234 | 235 | ```yaml 236 | apiVersion: gatling-operator.tech.zozo.com/v1alpha1 237 | kind: Gatling 238 | metadata: 239 | name: gatling-sample01 240 | spec: 241 | generateReport: true # The flag of generating Gatling report 242 | generateLocalReport: false # The flag of generating Gatling report for each pod 243 | notifyReport: true # The flag of notifying Gatling report 244 | cleanupAfterJobDone: true # The flag of cleanup Gatling resources after the job done 245 | ``` 246 | 247 | Then, configure Notification Service Provider in `.spec.notificationServiceSpec` to notify Gatling load testing result via Slack. Suppose that you want to store credential info (Slack webhook URL) in Kubernetes Secret named `gatling-notification-slack-secrets`, you configure each fields in `.spec.notificationServiceSpec` like this: 248 | 249 | ```yaml 250 | apiVersion: gatling-operator.tech.zozo.com/v1alpha1 251 | kind: Gatling 252 | metadata: 253 | name: gatling-sample01 254 | spec: 255 | notificationServiceSpec: 256 | provider: "slack" 257 | secretName: "gatling-notification-slack-secrets" 258 | ``` 259 | 260 | You need to set Slack webhook URL value (in base64 encoded string) in the Secret for a Slack channel to which you want to deliver the message. The key name for the Slack webhook URL must be `incoming-webhook-url`. 261 | 262 | ```yaml 263 | apiVersion: v1 264 | data: 265 | incoming-webhook-url: 266 | kind: Secret 267 | metadata: 268 | name: gatling-notification-slack-secrets 269 | type: Opaque 270 | ``` 271 | 272 | Once finish adding the Notification Service Provider configuration in the Gatling CR named `gatling-sample01`, deploy it again in the same way you did previously and wait until it's done. 273 | 274 | ```bash 275 | kustomize build config/samples | kubectl apply -f - 276 | ``` 277 | 278 | You'll get a Slack message like this: 279 | 280 | ![](../assets/slack-notification.png) 281 | 282 | Please check [User Guide](./user-guide.md) for more details on Gatling CR configurations. 283 | -------------------------------------------------------------------------------- /gatling/Dockerfile: -------------------------------------------------------------------------------- 1 | # This is modified based on the original file: 2 | # https://github.com/denvazh/gatling/tree/master/3.2.1 3 | # 4 | # Gatling is a highly capable load testing tool. 5 | 6 | FROM openjdk:21-jdk-slim-bullseye 7 | 8 | # create user/group 9 | RUN groupadd -g 1000 gatling && \ 10 | useradd -l -u 1000 -m gatling -g gatling 11 | 12 | # working directory for gatling 13 | WORKDIR /opt 14 | 15 | # gating version 16 | ENV GATLING_VERSION 3.10.5 17 | 18 | # create directory for gatling install 19 | RUN mkdir -p gatling 20 | 21 | # install gatling 22 | RUN apt-get update && apt-get upgrade -y && apt-get install -y wget unzip && \ 23 | mkdir -p /tmp/downloads && \ 24 | wget -q -O /tmp/downloads/gatling-$GATLING_VERSION.zip \ 25 | https://repo1.maven.org/maven2/io/gatling/highcharts/gatling-charts-highcharts-bundle/$GATLING_VERSION/gatling-charts-highcharts-bundle-$GATLING_VERSION-bundle.zip && \ 26 | mkdir -p /tmp/archive && cd /tmp/archive && \ 27 | unzip /tmp/downloads/gatling-$GATLING_VERSION.zip && \ 28 | mv /tmp/archive/gatling-charts-highcharts-bundle-$GATLING_VERSION/* /opt/gatling/ && \ 29 | rm -rf /opt/gatling/user-files/simulations/computerdatabase /tmp/* && \ 30 | chown -R gatling:gatling /opt/gatling 31 | 32 | # change context to gatling directory 33 | WORKDIR /opt/gatling 34 | 35 | # set directories below to be mountable from host 36 | VOLUME ["/opt/gatling/conf", "/opt/gatling/results", "/opt/gatling/user-files"] 37 | 38 | # copy local files to gatling directory 39 | COPY user-files/simulations user-files/simulations 40 | COPY user-files/resources user-files/resources 41 | COPY conf conf 42 | 43 | # set environment variables 44 | ENV PATH /opt/gatling/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 45 | ENV GATLING_HOME /opt/gatling 46 | 47 | ENTRYPOINT ["gatling.sh"] 48 | -------------------------------------------------------------------------------- /gatling/conf/gatling.conf: -------------------------------------------------------------------------------- 1 | ######################### 2 | # Gatling Configuration # 3 | ######################### 4 | 5 | # This file contains all the settings configurable for Gatling with their default values 6 | 7 | gatling { 8 | core { 9 | #outputDirectoryBaseName = "" # The prefix for each simulation result folder (then suffixed by the report generation timestamp) 10 | #runDescription = "" # The description for this simulation run, displayed in each report 11 | #encoding = "utf-8" # Encoding to use throughout Gatling for file and string manipulation 12 | #simulationClass = "" # The FQCN of the simulation to run (when used in conjunction with noReports, the simulation for which assertions will be validated) 13 | #elFileBodiesCacheMaxCapacity = 200 # Cache size for request body EL templates, set to 0 to disable 14 | #rawFileBodiesCacheMaxCapacity = 200 # Cache size for request body Raw templates, set to 0 to disable 15 | #rawFileBodiesInMemoryMaxSize = 1000 # Below this limit, raw file bodies will be cached in memory 16 | #pebbleFileBodiesCacheMaxCapacity = 200 # Cache size for request body Peeble templates, set to 0 to disable 17 | #feederAdaptiveLoadModeThreshold = 100 # File size threshold (in MB). Below load eagerly in memory, above use batch mode with default buffer size 18 | #shutdownTimeout = 10000 # Milliseconds to wait for the actor system to shutdown 19 | extract { 20 | regex { 21 | #cacheMaxCapacity = 200 # Cache size for the compiled regexes, set to 0 to disable caching 22 | } 23 | xpath { 24 | #cacheMaxCapacity = 200 # Cache size for the compiled XPath queries, set to 0 to disable caching 25 | } 26 | jsonPath { 27 | #cacheMaxCapacity = 200 # Cache size for the compiled jsonPath queries, set to 0 to disable caching 28 | } 29 | css { 30 | #cacheMaxCapacity = 200 # Cache size for the compiled CSS selectors queries, set to 0 to disable caching 31 | } 32 | } 33 | directory { 34 | #simulations = "" # If set, directory where simulation classes are located 35 | #resources = "" # If set, directory where resources, such as feeder files and request bodies, are located 36 | #reportsOnly = "" # If set, name of report folder to look for in order to generate its report 37 | #binaries = "" # If set, name of the folder where compiles classes are located: Defaults to GATLING_HOME/target. 38 | #results = results # Name of the folder where all reports folder are located 39 | } 40 | } 41 | socket { 42 | #connectTimeout = 10000 # Timeout in millis for establishing a TCP socket 43 | #tcpNoDelay = true 44 | #soKeepAlive = false # if TCP keepalive configured at OS level should be used 45 | #soReuseAddress = false 46 | } 47 | netty { 48 | #useNativeTransport = true # if Netty native transport should be used instead of Java NIO 49 | #allocator = "pooled" # switch to unpooled for unpooled ByteBufAllocator 50 | #maxThreadLocalCharBufferSize = 200000 # Netty's default is 16k 51 | } 52 | ssl { 53 | #useOpenSsl = true # if OpenSSL should be used instead of JSSE (only the latter can be debugged with -Djava.net.debug=ssl) 54 | #useOpenSslFinalizers = false # if OpenSSL contexts should be freed with Finalizer or if using RefCounted is fine 55 | #handshakeTimeout = 10000 # TLS handshake timeout in millis 56 | #useInsecureTrustManager = true # Use an insecure TrustManager that trusts all server certificates 57 | #enabledProtocols = [] # Array of enabled protocols for HTTPS, if empty use Netty's defaults 58 | #enabledCipherSuites = [] # Array of enabled cipher suites for HTTPS, if empty enable all available ciphers 59 | #sessionCacheSize = 0 # SSLSession cache size, set to 0 to use JDK's default 60 | #sessionTimeout = 0 # SSLSession timeout in seconds, set to 0 to use JDK's default (24h) 61 | #enableSni = true # When set to true, enable Server Name indication (SNI) 62 | keyStore { 63 | #type = "" # Type of SSLContext's KeyManagers store 64 | #file = "" # Location of SSLContext's KeyManagers store 65 | #password = "" # Password for SSLContext's KeyManagers store 66 | #algorithm = "" # Algorithm used SSLContext's KeyManagers store 67 | } 68 | trustStore { 69 | #type = "" # Type of SSLContext's TrustManagers store 70 | #file = "" # Location of SSLContext's TrustManagers store 71 | #password = "" # Password for SSLContext's TrustManagers store 72 | #algorithm = "" # Algorithm used by SSLContext's TrustManagers store 73 | } 74 | } 75 | charting { 76 | #noReports = false # When set to true, don't generate HTML reports 77 | #maxPlotPerSeries = 1000 # Number of points per graph in Gatling reports 78 | #useGroupDurationMetric = false # Switch group timings from cumulated response time to group duration. 79 | indicators { 80 | #lowerBound = 800 # Lower bound for the requests' response time to track in the reports and the console summary 81 | #higherBound = 1200 # Higher bound for the requests' response time to track in the reports and the console summary 82 | #percentile1 = 50 # Value for the 1st percentile to track in the reports, the console summary and Graphite 83 | #percentile2 = 75 # Value for the 2nd percentile to track in the reports, the console summary and Graphite 84 | #percentile3 = 95 # Value for the 3rd percentile to track in the reports, the console summary and Graphite 85 | #percentile4 = 99 # Value for the 4th percentile to track in the reports, the console summary and Graphite 86 | } 87 | } 88 | http { 89 | #fetchedCssCacheMaxCapacity = 200 # Cache size for CSS parsed content, set to 0 to disable 90 | #fetchedHtmlCacheMaxCapacity = 200 # Cache size for HTML parsed content, set to 0 to disable 91 | #perUserCacheMaxCapacity = 200 # Per virtual user cache size, set to 0 to disable 92 | #warmUpUrl = "https://gatling.io" # The URL to use to warm-up the HTTP stack (blank means disabled) 93 | #enableGA = true # Very light Google Analytics (Gatling and Java version), please support 94 | #pooledConnectionIdleTimeout = 60000 # Timeout in millis for a connection to stay idle in the pool 95 | #requestTimeout = 60000 # Timeout in millis for performing an HTTP request 96 | #enableHostnameVerification = false # When set to true, enable hostname verification: SSLEngine.setHttpsEndpointIdentificationAlgorithm("HTTPS") 97 | dns { 98 | #queryTimeout = 5000 # Timeout in millis of each DNS query in millis 99 | #maxQueriesPerResolve = 6 # Maximum allowed number of DNS queries for a given name resolution 100 | } 101 | } 102 | jms { 103 | #replyTimeoutScanPeriod = 1000 # scan period for timedout reply messages 104 | } 105 | data { 106 | #writers = [console, file] # The list of DataWriters to which Gatling write simulation data (currently supported : console, file, graphite) 107 | console { 108 | #light = false # When set to true, displays a light version without detailed request stats 109 | #writePeriod = 5 # Write interval, in seconds 110 | } 111 | file { 112 | #bufferSize = 8192 # FileDataWriter's internal data buffer size, in bytes 113 | } 114 | leak { 115 | #noActivityTimeout = 30 # Period, in seconds, for which Gatling may have no activity before considering a leak may be happening 116 | } 117 | graphite { 118 | #light = false # only send the all* stats 119 | #host = "localhost" # The host where the Carbon server is located 120 | #port = 2003 # The port to which the Carbon server listens to (2003 is default for plaintext, 2004 is default for pickle) 121 | #protocol = "tcp" # The protocol used to send data to Carbon (currently supported : "tcp", "udp") 122 | #rootPathPrefix = "gatling" # The common prefix of all metrics sent to Graphite 123 | #bufferSize = 8192 # Internal data buffer size, in bytes 124 | #writePeriod = 1 # Write period, in seconds 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /gatling/conf/logback.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} [%-5level] %logger{15} - %msg%n%rEx 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /gatling/sample/resources/goods_ids.csv: -------------------------------------------------------------------------------- 1 | goods_id,goods_name 2 | 1,hoge 3 | 2,fuga 4 | 3,sample 5 | 4,test 6 | 5,goods 7 | -------------------------------------------------------------------------------- /gatling/sample/resources/user_ids.csv: -------------------------------------------------------------------------------- 1 | user_id 2 | 1 3 | 2 4 | 3 5 | 4 6 | 5 7 | -------------------------------------------------------------------------------- /gatling/user-files/resources/myresources.csv: -------------------------------------------------------------------------------- 1 | alphabet 2 | "a" 3 | "b" 4 | "c" 5 | -------------------------------------------------------------------------------- /gatling/user-files/simulations/MyBasicSimulation.scala: -------------------------------------------------------------------------------- 1 | // Sample originally from https://github.com/gatling/gatling/blob/400a125d7995d1b895c4cc4847ff15059d252948/gatling-bundle/src/main/scala/computerdatabase/BasicSimulation.scala 2 | /* 3 | * Copyright 2011-2021 GatlingCorp (https://gatling.io) 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * 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, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import scala.concurrent.duration._ 18 | 19 | import io.gatling.core.Predef._ 20 | import io.gatling.http.Predef._ 21 | 22 | class MyBasicSimulation extends Simulation { 23 | 24 | val usersPerSec = sys.env.getOrElse("CONCURRENCY", "1").toInt 25 | val durationSec = sys.env.getOrElse("DURATION", "10").toInt 26 | 27 | val httpProtocol = http 28 | // Here is the root for all relative URLs 29 | .baseUrl("http://computer-database.gatling.io") 30 | // Here are the common headers 31 | .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") 32 | .doNotTrackHeader("1") 33 | .acceptLanguageHeader("en-US,en;q=0.5") 34 | .acceptEncodingHeader("gzip, deflate") 35 | .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0") 36 | 37 | // A scenario is a chain of requests and pauses 38 | val scn = scenario("Scenario Name") 39 | .exec( 40 | http("request_1") 41 | .get("/") 42 | ) 43 | // Note that Gatling has recorded real time pauses 44 | .pause(7) 45 | .exec( 46 | http("request_2") 47 | .get("/computers?f=macbook") 48 | ) 49 | .pause(2) 50 | .exec( 51 | http("request_3") 52 | .get("/computers/6") 53 | ) 54 | .pause(3) 55 | .exec( 56 | http("request_4") 57 | // Here's an example of a POST request 58 | .post("/computers") 59 | // Note the triple double quotes: used in Scala for protecting a whole chain of characters (no need for backslash) 60 | .formParam("name", "Beautiful Computer") 61 | .formParam("introduced", "2012-05-30") 62 | .formParam("discontinued", "") 63 | .formParam("company", "37") 64 | ) 65 | 66 | setUp( 67 | scn.inject( 68 | constantUsersPerSec(usersPerSec) during(durationSec seconds) 69 | ).protocols(httpProtocol) 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /gatling/user-files/simulations/PersistentVolumeSampleSimulation.scala: -------------------------------------------------------------------------------- 1 | // Sample originally from https://github.com/gatling/gatling/blob/400a125d7995d1b895c4cc4847ff15059d252948/gatling-bundle/src/main/scala/computerdatabase/BasicSimulation.scala 2 | /* 3 | * Copyright 2011-2021 GatlingCorp (https://gatling.io) 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * 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, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import scala.concurrent.duration._ 18 | 19 | import io.gatling.core.Predef._ 20 | import io.gatling.http.Predef._ 21 | 22 | class PersistentVolumeSampleSimulation extends Simulation { 23 | 24 | val usersPerSec = sys.env.getOrElse("CONCURRENCY", "1").toInt 25 | val durationSec = sys.env.getOrElse("DURATION", "10").toInt 26 | 27 | val feeder_user_ids = csv("pv/user_ids.csv") 28 | val feeder_goods_ids = csv("pv/goods_ids.csv") 29 | val feeder_myresources = csv("myresources.csv") 30 | 31 | // A scenario is a chain of requests and pauses 32 | val scn = scenario("Scenario Name") 33 | .feed(feeder_user_ids.circular) 34 | .feed(feeder_goods_ids.circular) 35 | .feed(feeder_myresources.circular) 36 | .exec { session => 37 | println(s"User ${session("user_id").as[String]} is buying goods ${session("goods_id").as[String]} ${session("goods_name").as[String]}") 38 | println(s"myresource: ${session("alphabet").as[String]}") 39 | session 40 | } 41 | // Note that Gatling has recorded real time pauses 42 | .pause(1) 43 | 44 | setUp( 45 | scn.inject( 46 | constantUsersPerSec(usersPerSec) during(durationSec seconds) 47 | ) 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/st-tech/gatling-operator 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/go-logr/logr v1.2.0 7 | github.com/onsi/ginkgo v1.16.5 8 | github.com/onsi/gomega v1.18.1 9 | k8s.io/api v0.24.1 10 | k8s.io/apimachinery v0.24.1 11 | k8s.io/client-go v0.24.1 12 | k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 13 | sigs.k8s.io/controller-runtime v0.12.1 14 | ) 15 | 16 | require ( 17 | cloud.google.com/go v0.81.0 // indirect 18 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 19 | github.com/Azure/go-autorest/autorest v0.11.18 // indirect 20 | github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect 21 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 22 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 23 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 24 | github.com/PuerkitoBio/purell v1.1.1 // indirect 25 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 26 | github.com/beorn7/perks v1.0.1 // indirect 27 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 28 | github.com/davecgh/go-spew v1.1.1 // indirect 29 | github.com/emicklei/go-restful v2.16.0+incompatible // indirect 30 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 31 | github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect 32 | github.com/fsnotify/fsnotify v1.5.1 // indirect 33 | github.com/go-logr/zapr v1.2.0 // indirect 34 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 35 | github.com/go-openapi/jsonreference v0.19.5 // indirect 36 | github.com/go-openapi/swag v0.19.14 // indirect 37 | github.com/gogo/protobuf v1.3.2 // indirect 38 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 39 | github.com/golang/protobuf v1.5.2 // indirect 40 | github.com/google/gnostic v0.5.7-v3refs // indirect 41 | github.com/google/go-cmp v0.5.5 // indirect 42 | github.com/google/gofuzz v1.1.0 // indirect 43 | github.com/google/uuid v1.1.2 // indirect 44 | github.com/imdario/mergo v0.3.12 // indirect 45 | github.com/josharian/intern v1.0.0 // indirect 46 | github.com/json-iterator/go v1.1.12 // indirect 47 | github.com/mailru/easyjson v0.7.6 // indirect 48 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 49 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 50 | github.com/modern-go/reflect2 v1.0.2 // indirect 51 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 52 | github.com/nxadm/tail v1.4.8 // indirect 53 | github.com/pkg/errors v0.9.1 // indirect 54 | github.com/prometheus/client_golang v1.12.1 // indirect 55 | github.com/prometheus/client_model v0.2.0 // indirect 56 | github.com/prometheus/common v0.32.1 // indirect 57 | github.com/prometheus/procfs v0.7.3 // indirect 58 | github.com/spf13/pflag v1.0.5 // indirect 59 | go.uber.org/atomic v1.7.0 // indirect 60 | go.uber.org/multierr v1.6.0 // indirect 61 | go.uber.org/zap v1.19.1 // indirect 62 | golang.org/x/crypto v0.21.0 // indirect 63 | golang.org/x/net v0.23.0 // indirect 64 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect 65 | golang.org/x/sys v0.18.0 // indirect 66 | golang.org/x/term v0.18.0 // indirect 67 | golang.org/x/text v0.14.0 // indirect 68 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect 69 | gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect 70 | google.golang.org/appengine v1.6.7 // indirect 71 | google.golang.org/protobuf v1.33.0 // indirect 72 | gopkg.in/inf.v0 v0.9.1 // indirect 73 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 74 | gopkg.in/yaml.v2 v2.4.0 // indirect 75 | gopkg.in/yaml.v3 v3.0.0 // indirect 76 | k8s.io/apiextensions-apiserver v0.24.0 // indirect 77 | k8s.io/component-base v0.24.0 // indirect 78 | k8s.io/klog/v2 v2.60.1 // indirect 79 | k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect 80 | sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect 81 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect 82 | sigs.k8s.io/yaml v1.3.0 // indirect 83 | ) 84 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © ZOZO, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © ZOZO, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "os" 22 | 23 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 24 | // to ensure that exec-entrypoint and run can make use of them. 25 | _ "k8s.io/client-go/plugin/pkg/client/auth" 26 | 27 | "k8s.io/apimachinery/pkg/runtime" 28 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 29 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/controller" 32 | "sigs.k8s.io/controller-runtime/pkg/healthz" 33 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 34 | 35 | gatlingoperatorv1alpha1 "github.com/st-tech/gatling-operator/api/v1alpha1" 36 | "github.com/st-tech/gatling-operator/controllers" 37 | //+kubebuilder:scaffold:imports 38 | ) 39 | 40 | var ( 41 | scheme = runtime.NewScheme() 42 | setupLog = ctrl.Log.WithName("setup") 43 | ) 44 | 45 | func init() { 46 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 47 | 48 | utilruntime.Must(gatlingoperatorv1alpha1.AddToScheme(scheme)) 49 | //+kubebuilder:scaffold:scheme 50 | } 51 | 52 | func main() { 53 | var metricsAddr string 54 | var enableLeaderElection bool 55 | var probeAddr string 56 | var maxConcurrentReconciles int 57 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 58 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 59 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 60 | "Enable leader election for controller manager. "+ 61 | "Enabling this will ensure there is only one active controller manager.") 62 | flag.IntVar(&maxConcurrentReconciles, "max-concurrent-reconciles", 1, 63 | "Maximum number of concurrent Reconciles") 64 | opts := zap.Options{ 65 | Development: true, 66 | } 67 | opts.BindFlags(flag.CommandLine) 68 | flag.Parse() 69 | 70 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 71 | 72 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 73 | Scheme: scheme, 74 | MetricsBindAddress: metricsAddr, 75 | Port: 9443, 76 | HealthProbeBindAddress: probeAddr, 77 | LeaderElection: enableLeaderElection, 78 | LeaderElectionID: "0d2e74aa.tech.zozo.com", 79 | }) 80 | if err != nil { 81 | setupLog.Error(err, "unable to start manager") 82 | os.Exit(1) 83 | } 84 | 85 | if err = (&controllers.GatlingReconciler{ 86 | Client: mgr.GetClient(), 87 | Scheme: mgr.GetScheme(), 88 | }).SetupWithManager(mgr, controller.Options{MaxConcurrentReconciles: maxConcurrentReconciles}); err != nil { 89 | setupLog.Error(err, "unable to create controller", "controller", "Gatling") 90 | os.Exit(1) 91 | } 92 | //+kubebuilder:scaffold:builder 93 | 94 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 95 | setupLog.Error(err, "unable to set up health check") 96 | os.Exit(1) 97 | } 98 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 99 | setupLog.Error(err, "unable to set up ready check") 100 | os.Exit(1) 101 | } 102 | 103 | setupLog.Info("starting manager") 104 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 105 | setupLog.Error(err, "problem running manager") 106 | os.Exit(1) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pkg/cloudstorages/azure.go: -------------------------------------------------------------------------------- 1 | package cloudstorages 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type AzureCloudStorageProvider struct { 8 | providerName string 9 | storageAccount string 10 | } 11 | 12 | func (p *AzureCloudStorageProvider) init(args []EnvVars) { 13 | if len(args) > 0 { 14 | var envs EnvVars = args[0] 15 | for _, env := range envs { 16 | if env.Name == "AZUREBLOB_ACCOUNT" { 17 | p.storageAccount = env.Value 18 | break 19 | } 20 | } 21 | } 22 | } 23 | 24 | func (p *AzureCloudStorageProvider) GetName() string { 25 | return p.providerName 26 | } 27 | 28 | func (p *AzureCloudStorageProvider) GetCloudStoragePath(bucket string, gatlingName string, subDir string) string { 29 | // Format azureblob:// 30 | return fmt.Sprintf("az:%s/%s/%s", bucket, gatlingName, subDir) 31 | } 32 | 33 | func (p *AzureCloudStorageProvider) GetCloudStorageReportURL(bucket string, gatlingName string, subDir string) string { 34 | // Format https://.blob.core.windows.net/bucket//subdir/hoge.log 35 | return fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s/%s/index.html", p.storageAccount, bucket, gatlingName, subDir) 36 | } 37 | 38 | func (p *AzureCloudStorageProvider) GetGatlingTransferResultCommand(resultsDirectoryPath string, region string, storagePath string) string { 39 | // region param isn't used 40 | template := ` 41 | export RCLONE_AZUREBLOB_ACCOUNT=${AZUREBLOB_ACCOUNT} 42 | export RCLONE_AZUREBLOB_KEY=${AZUREBLOB_KEY} 43 | export RCLONE_AZUREBLOB_SAS_URL=${AZUREBLOB_SAS_URL} 44 | RESULTS_DIR_PATH=%s 45 | rclone config create az azureblob env_auth=true 46 | while true; do 47 | if [ -f "${RESULTS_DIR_PATH}/FAILED" ]; then 48 | echo "Skip transfering gatling results" 49 | break 50 | fi 51 | if [ -f "${RESULTS_DIR_PATH}/COMPLETED" ]; then 52 | for source in $(find ${RESULTS_DIR_PATH} -type f -name *.log) 53 | do 54 | rclone copyto ${source} %s/${HOSTNAME}.log 55 | done 56 | break 57 | fi 58 | sleep 1; 59 | done 60 | ` 61 | return fmt.Sprintf(template, resultsDirectoryPath, storagePath) 62 | } 63 | 64 | func (p *AzureCloudStorageProvider) GetGatlingAggregateResultCommand(resultsDirectoryPath string, region string, storagePath string) string { 65 | // region param isn't used 66 | template := ` 67 | export RCLONE_AZUREBLOB_ACCOUNT=${AZUREBLOB_ACCOUNT} 68 | export RCLONE_AZUREBLOB_KEY=${AZUREBLOB_KEY} 69 | export RCLONE_AZUREBLOB_SAS_URL=${AZUREBLOB_SAS_URL} 70 | GATLING_AGGREGATE_DIR=%s 71 | rclone config create az azureblob env_auth=true 72 | rclone copy %s ${GATLING_AGGREGATE_DIR} 73 | ` 74 | return fmt.Sprintf(template, resultsDirectoryPath, storagePath) 75 | } 76 | 77 | func (p *AzureCloudStorageProvider) GetGatlingTransferReportCommand(resultsDirectoryPath string, region string, storagePath string) string { 78 | // region param isn't used 79 | template := ` 80 | export RCLONE_AZUREBLOB_ACCOUNT=${AZUREBLOB_ACCOUNT} 81 | export RCLONE_AZUREBLOB_KEY=${AZUREBLOB_KEY} 82 | export RCLONE_AZUREBLOB_SAS_URL=${AZUREBLOB_SAS_URL} 83 | GATLING_AGGREGATE_DIR=%s 84 | rclone config create az azureblob env_auth=true 85 | rclone copy ${GATLING_AGGREGATE_DIR} --exclude "*.log" %s 86 | ` 87 | return fmt.Sprintf(template, resultsDirectoryPath, storagePath) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/cloudstorages/azure_test.go: -------------------------------------------------------------------------------- 1 | package cloudstorages 2 | 3 | import ( 4 | "fmt" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var storageAccount string = "testaccount" 11 | 12 | var cspArgs []EnvVars = []EnvVars{ 13 | { 14 | { 15 | Name: "AZUREBLOB_ACCOUNT", 16 | Value: storageAccount, 17 | }, 18 | }, 19 | } 20 | 21 | var _ = Describe("GetName", func() { 22 | var ( 23 | provider string 24 | expectedValue string 25 | ) 26 | BeforeEach(func() { 27 | provider = "azure" 28 | expectedValue = "azure" 29 | }) 30 | Context("Provider is azure", func() { 31 | It("should get provider name = azure", func() { 32 | csp := &AzureCloudStorageProvider{providerName: provider} 33 | csp.init(cspArgs) 34 | Expect(csp.GetName()).To(Equal(expectedValue)) 35 | }) 36 | }) 37 | }) 38 | 39 | var _ = Describe("GetCloudStoragePath", func() { 40 | var ( 41 | provider string 42 | bucket string 43 | gatlingName string 44 | subDir string 45 | expectedValue string 46 | ) 47 | BeforeEach(func() { 48 | provider = "azure" 49 | bucket = "testBucket" 50 | gatlingName = "testGatling" 51 | subDir = "subDir" 52 | expectedValue = fmt.Sprintf("az:%s/%s/%s", bucket, gatlingName, subDir) 53 | }) 54 | Context("Provider is azure", func() { 55 | It("path is azure blob storage container", func() { 56 | csp := &AzureCloudStorageProvider{providerName: provider} 57 | csp.init(cspArgs) 58 | Expect(csp.GetCloudStoragePath(bucket, gatlingName, subDir)).To(Equal(expectedValue)) 59 | }) 60 | }) 61 | }) 62 | 63 | var _ = Describe("GetCloudStorageReportURL", func() { 64 | var ( 65 | provider string 66 | bucket string 67 | gatlingName string 68 | subDir string 69 | expectedValue string 70 | ) 71 | BeforeEach(func() { 72 | provider = "azure" 73 | bucket = "testBucket" 74 | gatlingName = "testGatling" 75 | subDir = "subDir" 76 | expectedValue = fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s/%s/index.html", 77 | storageAccount, bucket, gatlingName, subDir) 78 | }) 79 | Context("Provider is azure", func() { 80 | It("path is azure s3 bucket", func() { 81 | csp := &AzureCloudStorageProvider{providerName: provider} 82 | csp.init(cspArgs) 83 | Expect(csp.GetCloudStorageReportURL(bucket, gatlingName, subDir)).To(Equal(expectedValue)) 84 | }) 85 | }) 86 | }) 87 | 88 | var _ = Describe("GetGatlingTransferResultCommand", func() { 89 | var ( 90 | provider string 91 | resultsDirectoryPath string 92 | region string 93 | storagePath string 94 | expectedValue string 95 | ) 96 | BeforeEach(func() { 97 | provider = "azure" 98 | resultsDirectoryPath = "testResultsDirectoryPath" 99 | region = "" 100 | storagePath = "testStoragePath" 101 | expectedValue = fmt.Sprintf(` 102 | export RCLONE_AZUREBLOB_ACCOUNT=${AZUREBLOB_ACCOUNT} 103 | export RCLONE_AZUREBLOB_KEY=${AZUREBLOB_KEY} 104 | export RCLONE_AZUREBLOB_SAS_URL=${AZUREBLOB_SAS_URL} 105 | RESULTS_DIR_PATH=%s 106 | rclone config create az azureblob env_auth=true 107 | while true; do 108 | if [ -f "${RESULTS_DIR_PATH}/FAILED" ]; then 109 | echo "Skip transfering gatling results" 110 | break 111 | fi 112 | if [ -f "${RESULTS_DIR_PATH}/COMPLETED" ]; then 113 | for source in $(find ${RESULTS_DIR_PATH} -type f -name *.log) 114 | do 115 | rclone copyto ${source} %s/${HOSTNAME}.log 116 | done 117 | break 118 | fi 119 | sleep 1; 120 | done 121 | `, resultsDirectoryPath, storagePath) 122 | }) 123 | Context("Provider is azure", func() { 124 | It("returns commands with azure blob storage rclone config", func() { 125 | csp := &AzureCloudStorageProvider{providerName: provider} 126 | csp.init(cspArgs) 127 | fmt.Println(csp.GetGatlingTransferResultCommand(resultsDirectoryPath, region, storagePath)) 128 | Expect(csp.GetGatlingTransferResultCommand(resultsDirectoryPath, region, storagePath)).To(Equal(expectedValue)) 129 | }) 130 | }) 131 | }) 132 | 133 | var _ = Describe("GetGatlingAggregateResultCommand", func() { 134 | var ( 135 | provider string 136 | resultsDirectoryPath string 137 | region string 138 | storagePath string 139 | expectedValue string 140 | ) 141 | BeforeEach(func() { 142 | provider = "azure" 143 | resultsDirectoryPath = "testResultsDirectoryPath" 144 | region = "" 145 | storagePath = "testStoragePath" 146 | expectedValue = fmt.Sprintf(` 147 | export RCLONE_AZUREBLOB_ACCOUNT=${AZUREBLOB_ACCOUNT} 148 | export RCLONE_AZUREBLOB_KEY=${AZUREBLOB_KEY} 149 | export RCLONE_AZUREBLOB_SAS_URL=${AZUREBLOB_SAS_URL} 150 | GATLING_AGGREGATE_DIR=%s 151 | rclone config create az azureblob env_auth=true 152 | rclone copy %s ${GATLING_AGGREGATE_DIR} 153 | `, resultsDirectoryPath, storagePath) 154 | }) 155 | Context("Provider is azure", func() { 156 | It("returns commands with azure blob storage rclone config", func() { 157 | csp := &AzureCloudStorageProvider{providerName: provider} 158 | csp.init(cspArgs) 159 | Expect(csp.GetGatlingAggregateResultCommand(resultsDirectoryPath, region, storagePath)).To(Equal(expectedValue)) 160 | }) 161 | }) 162 | }) 163 | 164 | var _ = Describe("GetGatlingTransferReportCommand", func() { 165 | var ( 166 | provider string 167 | resultsDirectoryPath string 168 | region string 169 | storagePath string 170 | expectedValue string 171 | ) 172 | BeforeEach(func() { 173 | provider = "azure" 174 | resultsDirectoryPath = "testResultsDirectoryPath" 175 | region = "" 176 | storagePath = "testStoragePath" 177 | expectedValue = fmt.Sprintf(` 178 | export RCLONE_AZUREBLOB_ACCOUNT=${AZUREBLOB_ACCOUNT} 179 | export RCLONE_AZUREBLOB_KEY=${AZUREBLOB_KEY} 180 | export RCLONE_AZUREBLOB_SAS_URL=${AZUREBLOB_SAS_URL} 181 | GATLING_AGGREGATE_DIR=%s 182 | rclone config create az azureblob env_auth=true 183 | rclone copy ${GATLING_AGGREGATE_DIR} --exclude "*.log" %s 184 | `, resultsDirectoryPath, storagePath) 185 | }) 186 | Context("Provider is azure", func() { 187 | It("returns commands with azure blob storage rclone config", func() { 188 | csp := &AzureCloudStorageProvider{providerName: provider} 189 | Expect(csp.GetGatlingTransferReportCommand(resultsDirectoryPath, region, storagePath)).To(Equal(expectedValue)) 190 | }) 191 | }) 192 | }) 193 | -------------------------------------------------------------------------------- /pkg/cloudstorages/cloudstorage.go: -------------------------------------------------------------------------------- 1 | package cloudstorages 2 | 3 | import ( 4 | "sync" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | ) 8 | 9 | type EnvVars []corev1.EnvVar 10 | 11 | type CloudStorageProvider interface { 12 | init(args []EnvVars) 13 | GetName() string 14 | GetCloudStoragePath(bucket string, gatlingName string, subDir string) string 15 | GetCloudStorageReportURL(bucket string, gatlingName string, subDir string) string 16 | GetGatlingTransferResultCommand(resultsDirectoryPath string, region string, storagePath string) string 17 | GetGatlingAggregateResultCommand(resultsDirectoryPath string, region string, storagePath string) string 18 | GetGatlingTransferReportCommand(resultsDirectoryPath string, region string, storagePath string) string 19 | } 20 | 21 | // use sync.Map to achieve thread safe read and write to map 22 | var cloudStorageProvidersSyncMap = &sync.Map{} 23 | 24 | func GetProvider(provider string, args ...EnvVars) *CloudStorageProvider { 25 | v, ok := cloudStorageProvidersSyncMap.Load(provider) 26 | if !ok { 27 | var csp CloudStorageProvider 28 | switch provider { 29 | case "aws": 30 | csp = &S3CloudStorageProvider{providerName: provider} 31 | case "s3": 32 | csp = &S3CloudStorageProvider{providerName: provider} 33 | case "gcp": 34 | csp = &GCPCloudStorageProvider{providerName: provider} 35 | case "azure": 36 | csp = &AzureCloudStorageProvider{providerName: provider} 37 | default: 38 | return nil 39 | } 40 | csp.init(args) 41 | v, _ = cloudStorageProvidersSyncMap.LoadOrStore(provider, &csp) 42 | } 43 | return v.(*CloudStorageProvider) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/cloudstorages/cloudstorage_test.go: -------------------------------------------------------------------------------- 1 | package cloudstorages 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("GetProvider", func() { 9 | var ( 10 | provider string 11 | expectedValue string 12 | ) 13 | Context("Provider is aws", func() { 14 | BeforeEach(func() { 15 | provider = "aws" 16 | expectedValue = "aws" 17 | }) 18 | It("should get a pointer of S3CloudStorageProvider that has ProviderName field value = aws", func() { 19 | cspp := GetProvider(provider) 20 | Expect(cspp).NotTo(BeNil()) 21 | Expect((*cspp).GetName()).To(Equal(expectedValue)) 22 | }) 23 | }) 24 | 25 | Context("Provider is gcp", func() { 26 | BeforeEach(func() { 27 | provider = "gcp" 28 | expectedValue = "gcp" 29 | }) 30 | It("should get a pointer of GCPCloudStorageProvider that has ProviderName field value = gcp", func() { 31 | cspp := GetProvider(provider) 32 | Expect(cspp).NotTo(BeNil()) 33 | Expect((*cspp).GetName()).To(Equal(expectedValue)) 34 | }) 35 | }) 36 | 37 | Context("Provider is s3", func() { 38 | BeforeEach(func() { 39 | provider = "s3" 40 | expectedValue = "s3" 41 | }) 42 | It("should get a pointer of S3CloudStorageProvider that has ProviderName field value = s3", func() { 43 | cspp := GetProvider(provider) 44 | Expect(cspp).NotTo(BeNil()) 45 | Expect((*cspp).GetName()).To(Equal(expectedValue)) 46 | }) 47 | }) 48 | 49 | Context("Provider is non-supported one", func() { 50 | BeforeEach(func() { 51 | provider = "foo" 52 | }) 53 | It("should get nil pointer ", func() { 54 | cspp := GetProvider(provider) 55 | // If it should be nil, use BeNil() instead of Equal(nil) ref: https://onsi.github.io/gomega/ 56 | Expect(cspp).To(BeNil()) 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /pkg/cloudstorages/gcp.go: -------------------------------------------------------------------------------- 1 | package cloudstorages 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type GCPCloudStorageProvider struct { 8 | providerName string 9 | } 10 | 11 | func (p *GCPCloudStorageProvider) init(args []EnvVars) { /* do nothing */ } 12 | 13 | func (p *GCPCloudStorageProvider) GetName() string { 14 | return p.providerName 15 | } 16 | 17 | func (p *GCPCloudStorageProvider) GetCloudStoragePath(bucket string, gatlingName string, subDir string) string { 18 | return fmt.Sprintf("gs://%s/%s/%s", bucket, gatlingName, subDir) 19 | } 20 | 21 | func (p *GCPCloudStorageProvider) GetCloudStorageReportURL(bucket string, gatlingName string, subDir string) string { 22 | // Format http(s)://.storage.googleapis.com///index.html 23 | // or http(s)://storage.googleapis.com////index.html 24 | return fmt.Sprintf("https://storage.googleapis.com/%s/%s/%s/index.html", bucket, gatlingName, subDir) 25 | } 26 | 27 | // param: region is not used in GCP GCS, thus set just dummy value 28 | func (p *GCPCloudStorageProvider) GetGatlingTransferResultCommand(resultsDirectoryPath string, region string, storagePath string) string { 29 | template := ` 30 | RESULTS_DIR_PATH=%s 31 | # assumes gcs bucket using uniform bucket-level access control 32 | rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive 33 | while true; do 34 | if [ -f "${RESULTS_DIR_PATH}/FAILED" ]; then 35 | echo "Skip transfering gatling results" 36 | break 37 | fi 38 | if [ -f "${RESULTS_DIR_PATH}/COMPLETED" ]; then 39 | # assumes each pod only contain single gatling log file but use for loop to use find command result 40 | for source in $(find ${RESULTS_DIR_PATH} -type f -name *.log) 41 | do 42 | rclone copyto ${source} %s/${HOSTNAME}.log 43 | done 44 | break 45 | fi 46 | sleep 1; 47 | done 48 | ` 49 | return fmt.Sprintf(template, resultsDirectoryPath, storagePath) 50 | } 51 | 52 | // param: region is not used in GCP GCS, thus set just dummy value 53 | func (p *GCPCloudStorageProvider) GetGatlingAggregateResultCommand(resultsDirectoryPath string, region string, storagePath string) string { 54 | template := ` 55 | GATLING_AGGREGATE_DIR=%s 56 | # assumes gcs bucket using uniform bucket-level access control 57 | rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive 58 | rclone copy %s ${GATLING_AGGREGATE_DIR} 59 | ` 60 | return fmt.Sprintf(template, resultsDirectoryPath, storagePath) 61 | } 62 | 63 | // param: region is not used in GCP GCS, thus set just dummy value 64 | func (p *GCPCloudStorageProvider) GetGatlingTransferReportCommand(resultsDirectoryPath string, region string, storagePath string) string { 65 | template := ` 66 | GATLING_AGGREGATE_DIR=%s 67 | # assumes gcs bucket using uniform bucket-level access control 68 | rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive 69 | rclone copy ${GATLING_AGGREGATE_DIR} --exclude "*.log" %s 70 | ` 71 | return fmt.Sprintf(template, resultsDirectoryPath, storagePath) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/cloudstorages/gcp_test.go: -------------------------------------------------------------------------------- 1 | package cloudstorages 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("GetName", func() { 9 | var ( 10 | provider string 11 | expectedValue string 12 | ) 13 | BeforeEach(func() { 14 | provider = "gcp" 15 | expectedValue = "gcp" 16 | }) 17 | Context("Provider is gcp", func() { 18 | It("should get provider name = gcp", func() { 19 | csp := &GCPCloudStorageProvider{providerName: provider} 20 | Expect(csp.GetName()).To(Equal(expectedValue)) 21 | }) 22 | }) 23 | }) 24 | 25 | var _ = Describe("GetCloudStoragePath", func() { 26 | var ( 27 | provider string 28 | bucket string 29 | gatlingName string 30 | subDir string 31 | expectedValue string 32 | ) 33 | BeforeEach(func() { 34 | provider = "gcp" 35 | bucket = "testBucket" 36 | gatlingName = "testGatling" 37 | subDir = "subDir" 38 | expectedValue = "gs://testBucket/testGatling/subDir" 39 | }) 40 | Context("Provider is gcp", func() { 41 | It("path is gcp gcs bucket", func() { 42 | csp := &GCPCloudStorageProvider{providerName: provider} 43 | Expect(csp.GetCloudStoragePath(bucket, gatlingName, subDir)).To(Equal(expectedValue)) 44 | }) 45 | }) 46 | }) 47 | 48 | var _ = Describe("GetCloudStorageReportURL", func() { 49 | var ( 50 | provider string 51 | bucket string 52 | gatlingName string 53 | subDir string 54 | expectedValue string 55 | ) 56 | BeforeEach(func() { 57 | provider = "gcp" 58 | bucket = "testBucket" 59 | gatlingName = "testGatling" 60 | subDir = "subDir" 61 | expectedValue = "https://storage.googleapis.com/testBucket/testGatling/subDir/index.html" 62 | }) 63 | Context("Provider is gcp", func() { 64 | It("path is gcp gcs bucket", func() { 65 | csp := &GCPCloudStorageProvider{providerName: provider} 66 | Expect(csp.GetCloudStorageReportURL(bucket, gatlingName, subDir)).To(Equal(expectedValue)) 67 | }) 68 | }) 69 | }) 70 | 71 | var _ = Describe("GetGatlingTransferResultCommand", func() { 72 | var ( 73 | provider string 74 | resultsDirectoryPath string 75 | region string 76 | storagePath string 77 | expectedValue string 78 | ) 79 | BeforeEach(func() { 80 | provider = "gcp" 81 | resultsDirectoryPath = "testResultsDirectoryPath" 82 | region = "" 83 | storagePath = "testStoragePath" 84 | expectedValue = ` 85 | RESULTS_DIR_PATH=testResultsDirectoryPath 86 | # assumes gcs bucket using uniform bucket-level access control 87 | rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive 88 | while true; do 89 | if [ -f "${RESULTS_DIR_PATH}/FAILED" ]; then 90 | echo "Skip transfering gatling results" 91 | break 92 | fi 93 | if [ -f "${RESULTS_DIR_PATH}/COMPLETED" ]; then 94 | # assumes each pod only contain single gatling log file but use for loop to use find command result 95 | for source in $(find ${RESULTS_DIR_PATH} -type f -name *.log) 96 | do 97 | rclone copyto ${source} testStoragePath/${HOSTNAME}.log 98 | done 99 | break 100 | fi 101 | sleep 1; 102 | done 103 | ` 104 | }) 105 | Context("Provider is gcp", func() { 106 | It("returns commands with gcs rclone config", func() { 107 | csp := &GCPCloudStorageProvider{providerName: provider} 108 | Expect(csp.GetGatlingTransferResultCommand(resultsDirectoryPath, region, storagePath)).To(Equal(expectedValue)) 109 | }) 110 | }) 111 | }) 112 | 113 | var _ = Describe("GetGatlingAggregateResultCommand", func() { 114 | var ( 115 | provider string 116 | resultsDirectoryPath string 117 | region string 118 | storagePath string 119 | expectedValue string 120 | ) 121 | BeforeEach(func() { 122 | provider = "gcp" 123 | resultsDirectoryPath = "testResultsDirectoryPath" 124 | region = "" 125 | storagePath = "testStoragePath" 126 | expectedValue = ` 127 | GATLING_AGGREGATE_DIR=testResultsDirectoryPath 128 | # assumes gcs bucket using uniform bucket-level access control 129 | rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive 130 | rclone copy testStoragePath ${GATLING_AGGREGATE_DIR} 131 | ` 132 | }) 133 | Context("Provider is gcp", func() { 134 | It("returns commands with gcs rclone config", func() { 135 | gcp := &GCPCloudStorageProvider{providerName: provider} 136 | Expect(gcp.GetGatlingAggregateResultCommand(resultsDirectoryPath, region, storagePath)).To(Equal(expectedValue)) 137 | }) 138 | }) 139 | }) 140 | 141 | var _ = Describe("GetGatlingTransferReportCommand", func() { 142 | var ( 143 | provider string 144 | resultsDirectoryPath string 145 | region string 146 | storagePath string 147 | expectedValue string 148 | ) 149 | BeforeEach(func() { 150 | provider = "gcp" 151 | resultsDirectoryPath = "testResultsDirectoryPath" 152 | region = "" 153 | storagePath = "testStoragePath" 154 | expectedValue = ` 155 | GATLING_AGGREGATE_DIR=testResultsDirectoryPath 156 | # assumes gcs bucket using uniform bucket-level access control 157 | rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive 158 | rclone copy ${GATLING_AGGREGATE_DIR} --exclude "*.log" testStoragePath 159 | ` 160 | }) 161 | Context("Provider is gcp", func() { 162 | It("returns commands with gcs rclone config", func() { 163 | gcp := &GCPCloudStorageProvider{providerName: provider} 164 | Expect(gcp.GetGatlingTransferReportCommand(resultsDirectoryPath, region, storagePath)).To(Equal(expectedValue)) 165 | }) 166 | }) 167 | }) 168 | -------------------------------------------------------------------------------- /pkg/cloudstorages/s3.go: -------------------------------------------------------------------------------- 1 | package cloudstorages 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type S3CloudStorageProvider struct { 9 | providerName string 10 | customS3ProviderHost string 11 | } 12 | 13 | func (p *S3CloudStorageProvider) init(args []EnvVars) { 14 | if len(args) > 0 { 15 | var envs EnvVars = args[0] 16 | for _, env := range envs { 17 | if env.Name == "RCLONE_S3_ENDPOINT" { 18 | p.customS3ProviderHost = p.checkAndRemoveProtocol(env.Value) 19 | break 20 | } 21 | } 22 | } 23 | } 24 | 25 | func (p *S3CloudStorageProvider) checkAndRemoveProtocol(url string) string { 26 | idx := strings.Index(url, "://") 27 | if idx == -1 { 28 | return url 29 | } 30 | return url[idx+3:] 31 | } 32 | 33 | func (p *S3CloudStorageProvider) GetName() string { 34 | return p.providerName 35 | } 36 | 37 | func (p *S3CloudStorageProvider) GetCloudStoragePath(bucket string, gatlingName string, subDir string) string { 38 | // Format s3:// 39 | return fmt.Sprintf("s3:%s/%s/%s", bucket, gatlingName, subDir) 40 | } 41 | 42 | func (p *S3CloudStorageProvider) GetCloudStorageReportURL(bucket string, gatlingName string, subDir string) string { 43 | // Format https://.///index.html 44 | defaultS3ProviderHost := "s3.amazonaws.com" 45 | s3ProviderHost := defaultS3ProviderHost 46 | if p.customS3ProviderHost != "" { 47 | s3ProviderHost = p.customS3ProviderHost 48 | } 49 | 50 | return fmt.Sprintf("https://%s.%s/%s/%s/index.html", bucket, s3ProviderHost, gatlingName, subDir) 51 | } 52 | 53 | func (p *S3CloudStorageProvider) GetGatlingTransferResultCommand(resultsDirectoryPath string, region string, storagePath string) string { 54 | template := ` 55 | RESULTS_DIR_PATH=%s 56 | rclone config create s3 s3 env_auth=true region %s 57 | while true; do 58 | if [ -f "${RESULTS_DIR_PATH}/FAILED" ]; then 59 | echo "Skip transfering gatling results" 60 | break 61 | fi 62 | if [ -f "${RESULTS_DIR_PATH}/COMPLETED" ]; then 63 | for source in $(find ${RESULTS_DIR_PATH} -type f -name *.log) 64 | do 65 | rclone copyto ${source} --s3-no-check-bucket --s3-env-auth %s/${HOSTNAME}.log 66 | done 67 | break 68 | fi 69 | sleep 1; 70 | done 71 | ` 72 | return fmt.Sprintf(template, resultsDirectoryPath, region, storagePath) 73 | } 74 | 75 | func (p *S3CloudStorageProvider) GetGatlingAggregateResultCommand(resultsDirectoryPath string, region string, storagePath string) string { 76 | template := ` 77 | GATLING_AGGREGATE_DIR=%s 78 | rclone config create s3 s3 env_auth=true region %s 79 | rclone copy --s3-no-check-bucket --s3-env-auth %s ${GATLING_AGGREGATE_DIR} 80 | ` 81 | return fmt.Sprintf(template, resultsDirectoryPath, region, storagePath) 82 | } 83 | 84 | func (p *S3CloudStorageProvider) GetGatlingTransferReportCommand(resultsDirectoryPath string, region string, storagePath string) string { 85 | template := ` 86 | GATLING_AGGREGATE_DIR=%s 87 | rclone config create s3 s3 env_auth=true region %s 88 | rclone copy ${GATLING_AGGREGATE_DIR} --exclude "*.log" --s3-no-check-bucket --s3-env-auth %s 89 | ` 90 | return fmt.Sprintf(template, resultsDirectoryPath, region, storagePath) 91 | } 92 | -------------------------------------------------------------------------------- /pkg/cloudstorages/s3_test.go: -------------------------------------------------------------------------------- 1 | package cloudstorages 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("GetName", func() { 9 | var ( 10 | provider string 11 | expectedValue string 12 | ) 13 | BeforeEach(func() { 14 | provider = "aws" 15 | expectedValue = "aws" 16 | }) 17 | Context("Provider is aws", func() { 18 | It("should get provider name = aws", func() { 19 | csp := &S3CloudStorageProvider{providerName: provider} 20 | Expect(csp.GetName()).To(Equal(expectedValue)) 21 | }) 22 | }) 23 | }) 24 | 25 | var _ = Describe("GetCloudStoragePath", func() { 26 | var ( 27 | provider string 28 | bucket string 29 | gatlingName string 30 | subDir string 31 | expectedValue string 32 | ) 33 | BeforeEach(func() { 34 | provider = "aws" 35 | bucket = "testBucket" 36 | gatlingName = "testGatling" 37 | subDir = "subDir" 38 | expectedValue = "s3:testBucket/testGatling/subDir" 39 | }) 40 | Context("Provider is aws", func() { 41 | It("path is aws s3 bucket", func() { 42 | csp := &S3CloudStorageProvider{providerName: provider} 43 | Expect(csp.GetCloudStoragePath(bucket, gatlingName, subDir)).To(Equal(expectedValue)) 44 | }) 45 | }) 46 | }) 47 | 48 | var _ = Describe("GetCloudStorageReportURL", func() { 49 | var ( 50 | provider string 51 | bucket string 52 | gatlingName string 53 | subDir string 54 | ) 55 | BeforeEach(func() { 56 | provider = "s3" 57 | bucket = "testBucket" 58 | gatlingName = "testGatling" 59 | subDir = "subDir" 60 | }) 61 | Context("Provider is s3", func() { 62 | It("path is aws s3 bucket if RCLONE_S3_ENDPOINT not defined", func() { 63 | csp := &S3CloudStorageProvider{providerName: provider} 64 | Expect(csp.GetCloudStorageReportURL(bucket, gatlingName, subDir)).To(Equal("https://testBucket.s3.amazonaws.com/testGatling/subDir/index.html")) 65 | }) 66 | 67 | It("path is S3 bucket with custom provider endpoint", func() { 68 | csp := &S3CloudStorageProvider{providerName: provider} 69 | csp.init([]EnvVars{ 70 | { 71 | { 72 | Name: "RCLONE_S3_ENDPOINT", 73 | Value: "https://s3.de.io.cloud.ovh.net", 74 | }, 75 | }, 76 | }) 77 | Expect(csp.GetCloudStorageReportURL(bucket, gatlingName, subDir)).To(Equal("https://testBucket.s3.de.io.cloud.ovh.net/testGatling/subDir/index.html")) 78 | }) 79 | }) 80 | 81 | }) 82 | 83 | var _ = Describe("GetGatlingTransferResultCommand", func() { 84 | var ( 85 | provider string 86 | resultsDirectoryPath string 87 | region string 88 | storagePath string 89 | expectedValue string 90 | ) 91 | BeforeEach(func() { 92 | provider = "aws" 93 | resultsDirectoryPath = "testResultsDirectoryPath" 94 | region = "ap-northeast-1" 95 | storagePath = "testStoragePath" 96 | expectedValue = ` 97 | RESULTS_DIR_PATH=testResultsDirectoryPath 98 | rclone config create s3 s3 env_auth=true region ap-northeast-1 99 | while true; do 100 | if [ -f "${RESULTS_DIR_PATH}/FAILED" ]; then 101 | echo "Skip transfering gatling results" 102 | break 103 | fi 104 | if [ -f "${RESULTS_DIR_PATH}/COMPLETED" ]; then 105 | for source in $(find ${RESULTS_DIR_PATH} -type f -name *.log) 106 | do 107 | rclone copyto ${source} --s3-no-check-bucket --s3-env-auth testStoragePath/${HOSTNAME}.log 108 | done 109 | break 110 | fi 111 | sleep 1; 112 | done 113 | ` 114 | }) 115 | Context("Provider is aws", func() { 116 | It("returns commands with s3 rclone config", func() { 117 | csp := &S3CloudStorageProvider{providerName: provider} 118 | Expect(csp.GetGatlingTransferResultCommand(resultsDirectoryPath, region, storagePath)).To(Equal(expectedValue)) 119 | }) 120 | }) 121 | }) 122 | 123 | var _ = Describe("GetGatlingAggregateResultCommand", func() { 124 | var ( 125 | provider string 126 | resultsDirectoryPath string 127 | region string 128 | storagePath string 129 | expectedValue string 130 | ) 131 | BeforeEach(func() { 132 | provider = "aws" 133 | resultsDirectoryPath = "testResultsDirectoryPath" 134 | region = "ap-northeast-1" 135 | storagePath = "testStoragePath" 136 | expectedValue = ` 137 | GATLING_AGGREGATE_DIR=testResultsDirectoryPath 138 | rclone config create s3 s3 env_auth=true region ap-northeast-1 139 | rclone copy --s3-no-check-bucket --s3-env-auth testStoragePath ${GATLING_AGGREGATE_DIR} 140 | ` 141 | }) 142 | Context("Provider is aws", func() { 143 | It("returns commands with s3 rclone config", func() { 144 | csp := &S3CloudStorageProvider{providerName: provider} 145 | Expect(csp.GetGatlingAggregateResultCommand(resultsDirectoryPath, region, storagePath)).To(Equal(expectedValue)) 146 | }) 147 | }) 148 | }) 149 | 150 | var _ = Describe("GetGatlingTransferReportCommand", func() { 151 | var ( 152 | provider string 153 | resultsDirectoryPath string 154 | region string 155 | storagePath string 156 | expectedValue string 157 | ) 158 | BeforeEach(func() { 159 | provider = "aws" 160 | resultsDirectoryPath = "testResultsDirectoryPath" 161 | region = "ap-northeast-1" 162 | storagePath = "testStoragePath" 163 | expectedValue = ` 164 | GATLING_AGGREGATE_DIR=testResultsDirectoryPath 165 | rclone config create s3 s3 env_auth=true region ap-northeast-1 166 | rclone copy ${GATLING_AGGREGATE_DIR} --exclude "*.log" --s3-no-check-bucket --s3-env-auth testStoragePath 167 | ` 168 | }) 169 | Context("Provider is aws", func() { 170 | It("returns commands with s3 rclone config", func() { 171 | csp := &S3CloudStorageProvider{providerName: provider} 172 | Expect(csp.GetGatlingTransferReportCommand(resultsDirectoryPath, region, storagePath)).To(Equal(expectedValue)) 173 | }) 174 | }) 175 | }) 176 | -------------------------------------------------------------------------------- /pkg/cloudstorages/suite_test.go: -------------------------------------------------------------------------------- 1 | package cloudstorages 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestBucket(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "CloudStorages Suite") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/commands/commands.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | cloudstorages "github.com/st-tech/gatling-operator/pkg/cloudstorages" 7 | ) 8 | 9 | func GetGatlingWaiterCommand(parallelism *int32, gatlingNamespace string, gatlingName string) string { 10 | template := ` 11 | PARALLELISM=%d 12 | NAMESPACE=%s 13 | JOB_NAME=%s 14 | POD_NAME=$(cat /etc/pod-info/name) 15 | 16 | kubectl label pods -n $NAMESPACE $POD_NAME gatling-waiter=initialized 17 | 18 | while true; do 19 | READY_PODS=$(kubectl get pods -n $NAMESPACE --selector=job-name=$JOB_NAME-runner,gatling-waiter=initialized --no-headers | grep -c ".*"); 20 | echo "$READY_PODS/$PARALLELISM pods are ready"; 21 | if [ $READY_PODS -eq $PARALLELISM ]; then 22 | break; 23 | fi; 24 | sleep 1; 25 | done 26 | ` 27 | return fmt.Sprintf(template, 28 | *parallelism, 29 | gatlingNamespace, 30 | gatlingName, 31 | ) 32 | } 33 | 34 | func GetGatlingRunnerCommand( 35 | simulationsFormat string, simulationsDirectoryPath string, tempSimulationsDirectoryPath string, 36 | resourcesDirectoryPath string, resultsDirectoryPath string, startTime string, simulationClass string, 37 | generateLocalReport bool) string { 38 | 39 | template := ` 40 | SIMULATIONS_FORMAT=%s 41 | SIMULATIONS_DIR_PATH=%s 42 | TEMP_SIMULATIONS_DIR_PATH=%s 43 | RESOURCES_DIR_PATH=%s 44 | RESULTS_DIR_PATH=%s 45 | START_TIME="%s" 46 | SIMULATION_CLASS=%s 47 | RUN_STATUS_FILE="${RESULTS_DIR_PATH}/COMPLETED" 48 | if [ -z "${START_TIME}" ]; then 49 | START_TIME=$(date +"%%Y-%%m-%%d %%H:%%M:%%S" --utc) 50 | fi 51 | start_time_stamp=$(date -d "${START_TIME}" +"%%s") 52 | current_time_stamp=$(date +"%%s") 53 | echo "Wait until ${START_TIME}" 54 | until [ ${current_time_stamp} -ge ${start_time_stamp} ]; 55 | do 56 | current_time_stamp=$(date +"%%s") 57 | echo "it's ${current_time_stamp} now and waiting until ${start_time_stamp} ..." 58 | sleep 1; 59 | done 60 | if [ ! -d ${SIMULATIONS_DIR_PATH} ]; then 61 | mkdir -p ${SIMULATIONS_DIR_PATH} 62 | fi 63 | if [ -d ${TEMP_SIMULATIONS_DIR_PATH} ]; then 64 | cp -p ${TEMP_SIMULATIONS_DIR_PATH}/*.scala ${SIMULATIONS_DIR_PATH} 65 | fi 66 | if [ ! -d ${RESOURCES_DIR_PATH} ]; then 67 | mkdir -p ${RESOURCES_DIR_PATH} 68 | fi 69 | if [ ! -d ${RESULTS_DIR_PATH} ]; then 70 | mkdir -p ${RESULTS_DIR_PATH} 71 | fi 72 | 73 | if [ ${SIMULATIONS_FORMAT} = "bundle" ]; then 74 | gatling.sh -sf ${SIMULATIONS_DIR_PATH} -s ${SIMULATION_CLASS} -rsf ${RESOURCES_DIR_PATH} -rf ${RESULTS_DIR_PATH} %s %s 75 | elif [ ${SIMULATIONS_FORMAT} = "gradle" ]; then 76 | gradle -Dgatling.core.directory.results=${RESULTS_DIR_PATH} gatlingRun-${SIMULATION_CLASS} 77 | fi 78 | 79 | GATLING_EXIT_STATUS=$? 80 | if [ $GATLING_EXIT_STATUS -ne 0 ]; then 81 | RUN_STATUS_FILE="${RESULTS_DIR_PATH}/FAILED" 82 | echo "gatling.sh has failed!" 1>&2 83 | fi 84 | touch ${RUN_STATUS_FILE} 85 | exit $GATLING_EXIT_STATUS 86 | ` 87 | generateLocalReportOption := "-nr" 88 | if generateLocalReport { 89 | generateLocalReportOption = "" 90 | } 91 | 92 | runModeOptionLocal := "-rm local" 93 | 94 | return fmt.Sprintf(template, 95 | simulationsFormat, 96 | simulationsDirectoryPath, 97 | tempSimulationsDirectoryPath, 98 | resourcesDirectoryPath, 99 | resultsDirectoryPath, 100 | startTime, 101 | simulationClass, 102 | generateLocalReportOption, 103 | runModeOptionLocal) 104 | } 105 | 106 | func GetGatlingTransferResultCommand(resultsDirectoryPath string, provider string, region string, storagePath string) string { 107 | var command string 108 | cspp := cloudstorages.GetProvider(provider) 109 | if cspp != nil { 110 | command = (*cspp).GetGatlingTransferResultCommand(resultsDirectoryPath, region, storagePath) 111 | } 112 | return command 113 | } 114 | 115 | func GetGatlingAggregateResultCommand(resultsDirectoryPath string, provider string, region string, storagePath string) string { 116 | var command string 117 | cspp := cloudstorages.GetProvider(provider) 118 | if cspp != nil { 119 | command = (*cspp).GetGatlingAggregateResultCommand(resultsDirectoryPath, region, storagePath) 120 | } 121 | return command 122 | } 123 | 124 | func GetGatlingGenerateReportCommand(resultsDirectoryPath string) string { 125 | template := ` 126 | GATLING_AGGREGATE_DIR=%s 127 | DIR_NAME=$(dirname ${GATLING_AGGREGATE_DIR}) 128 | BASE_NAME=$(basename ${GATLING_AGGREGATE_DIR}) 129 | gatling.sh -rf ${DIR_NAME} -ro ${BASE_NAME} 130 | ` 131 | return fmt.Sprintf(template, resultsDirectoryPath) 132 | } 133 | 134 | func GetGatlingTransferReportCommand(resultsDirectoryPath string, provider string, region string, storagePath string) string { 135 | var command string 136 | cspp := cloudstorages.GetProvider(provider) 137 | if cspp != nil { 138 | command = (*cspp).GetGatlingTransferReportCommand(resultsDirectoryPath, region, storagePath) 139 | } 140 | return command 141 | } 142 | -------------------------------------------------------------------------------- /pkg/commands/commands_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("GetGatlingWaiterCommand", func() { 9 | It("getExceptValue", func() { 10 | expectedValue := ` 11 | PARALLELISM=1 12 | NAMESPACE=gatling-system 13 | JOB_NAME=gatling-test 14 | POD_NAME=$(cat /etc/pod-info/name) 15 | 16 | kubectl label pods -n $NAMESPACE $POD_NAME gatling-waiter=initialized 17 | 18 | while true; do 19 | READY_PODS=$(kubectl get pods -n $NAMESPACE --selector=job-name=$JOB_NAME-runner,gatling-waiter=initialized --no-headers | grep -c ".*"); 20 | echo "$READY_PODS/$PARALLELISM pods are ready"; 21 | if [ $READY_PODS -eq $PARALLELISM ]; then 22 | break; 23 | fi; 24 | sleep 1; 25 | done 26 | ` 27 | var parallelism int32 = 1 28 | Expect(GetGatlingWaiterCommand(¶llelism, "gatling-system", "gatling-test")).To(Equal(expectedValue)) 29 | }) 30 | }) 31 | 32 | var _ = Describe("GetGatlingRunnerCommand", func() { 33 | var ( 34 | simulationsFormat string 35 | simulationsDirectoryPath string 36 | tempSimulationsDirectoryPath string 37 | resourcesDirectoryPath string 38 | resultsDirectoryPath string 39 | startTime string 40 | simulationClass string 41 | generateLocalReport bool 42 | expectedValue string 43 | ) 44 | 45 | BeforeEach(func() { 46 | simulationsFormat = "bundle" 47 | simulationsDirectoryPath = "testSimulationDirectoryPath" 48 | tempSimulationsDirectoryPath = "testTempSimulationsDirectoryPath" 49 | resourcesDirectoryPath = "testResourcesDirectoryPath" 50 | resultsDirectoryPath = "testResultsDirectoryPath" 51 | startTime = "2021-09-10 08:45:31" 52 | simulationClass = "testSimulationClass" 53 | }) 54 | 55 | It("GetCommandsWithLocalReport", func() { 56 | generateLocalReport = true 57 | expectedValue = ` 58 | SIMULATIONS_FORMAT=bundle 59 | SIMULATIONS_DIR_PATH=testSimulationDirectoryPath 60 | TEMP_SIMULATIONS_DIR_PATH=testTempSimulationsDirectoryPath 61 | RESOURCES_DIR_PATH=testResourcesDirectoryPath 62 | RESULTS_DIR_PATH=testResultsDirectoryPath 63 | START_TIME="2021-09-10 08:45:31" 64 | SIMULATION_CLASS=testSimulationClass 65 | RUN_STATUS_FILE="${RESULTS_DIR_PATH}/COMPLETED" 66 | if [ -z "${START_TIME}" ]; then 67 | START_TIME=$(date +"%Y-%m-%d %H:%M:%S" --utc) 68 | fi 69 | start_time_stamp=$(date -d "${START_TIME}" +"%s") 70 | current_time_stamp=$(date +"%s") 71 | echo "Wait until ${START_TIME}" 72 | until [ ${current_time_stamp} -ge ${start_time_stamp} ]; 73 | do 74 | current_time_stamp=$(date +"%s") 75 | echo "it's ${current_time_stamp} now and waiting until ${start_time_stamp} ..." 76 | sleep 1; 77 | done 78 | if [ ! -d ${SIMULATIONS_DIR_PATH} ]; then 79 | mkdir -p ${SIMULATIONS_DIR_PATH} 80 | fi 81 | if [ -d ${TEMP_SIMULATIONS_DIR_PATH} ]; then 82 | cp -p ${TEMP_SIMULATIONS_DIR_PATH}/*.scala ${SIMULATIONS_DIR_PATH} 83 | fi 84 | if [ ! -d ${RESOURCES_DIR_PATH} ]; then 85 | mkdir -p ${RESOURCES_DIR_PATH} 86 | fi 87 | if [ ! -d ${RESULTS_DIR_PATH} ]; then 88 | mkdir -p ${RESULTS_DIR_PATH} 89 | fi 90 | 91 | if [ ${SIMULATIONS_FORMAT} = "bundle" ]; then 92 | gatling.sh -sf ${SIMULATIONS_DIR_PATH} -s ${SIMULATION_CLASS} -rsf ${RESOURCES_DIR_PATH} -rf ${RESULTS_DIR_PATH} -rm local 93 | elif [ ${SIMULATIONS_FORMAT} = "gradle" ]; then 94 | gradle -Dgatling.core.directory.results=${RESULTS_DIR_PATH} gatlingRun-${SIMULATION_CLASS} 95 | fi 96 | 97 | GATLING_EXIT_STATUS=$? 98 | if [ $GATLING_EXIT_STATUS -ne 0 ]; then 99 | RUN_STATUS_FILE="${RESULTS_DIR_PATH}/FAILED" 100 | echo "gatling.sh has failed!" 1>&2 101 | fi 102 | touch ${RUN_STATUS_FILE} 103 | exit $GATLING_EXIT_STATUS 104 | ` 105 | Expect(GetGatlingRunnerCommand(simulationsFormat, simulationsDirectoryPath, tempSimulationsDirectoryPath, resourcesDirectoryPath, resultsDirectoryPath, startTime, simulationClass, generateLocalReport)).To(Equal(expectedValue)) 106 | }) 107 | 108 | It("GetCommandWithoutLocalReport", func() { 109 | generateLocalReport = false 110 | expectedValue = ` 111 | SIMULATIONS_FORMAT=bundle 112 | SIMULATIONS_DIR_PATH=testSimulationDirectoryPath 113 | TEMP_SIMULATIONS_DIR_PATH=testTempSimulationsDirectoryPath 114 | RESOURCES_DIR_PATH=testResourcesDirectoryPath 115 | RESULTS_DIR_PATH=testResultsDirectoryPath 116 | START_TIME="2021-09-10 08:45:31" 117 | SIMULATION_CLASS=testSimulationClass 118 | RUN_STATUS_FILE="${RESULTS_DIR_PATH}/COMPLETED" 119 | if [ -z "${START_TIME}" ]; then 120 | START_TIME=$(date +"%Y-%m-%d %H:%M:%S" --utc) 121 | fi 122 | start_time_stamp=$(date -d "${START_TIME}" +"%s") 123 | current_time_stamp=$(date +"%s") 124 | echo "Wait until ${START_TIME}" 125 | until [ ${current_time_stamp} -ge ${start_time_stamp} ]; 126 | do 127 | current_time_stamp=$(date +"%s") 128 | echo "it's ${current_time_stamp} now and waiting until ${start_time_stamp} ..." 129 | sleep 1; 130 | done 131 | if [ ! -d ${SIMULATIONS_DIR_PATH} ]; then 132 | mkdir -p ${SIMULATIONS_DIR_PATH} 133 | fi 134 | if [ -d ${TEMP_SIMULATIONS_DIR_PATH} ]; then 135 | cp -p ${TEMP_SIMULATIONS_DIR_PATH}/*.scala ${SIMULATIONS_DIR_PATH} 136 | fi 137 | if [ ! -d ${RESOURCES_DIR_PATH} ]; then 138 | mkdir -p ${RESOURCES_DIR_PATH} 139 | fi 140 | if [ ! -d ${RESULTS_DIR_PATH} ]; then 141 | mkdir -p ${RESULTS_DIR_PATH} 142 | fi 143 | 144 | if [ ${SIMULATIONS_FORMAT} = "bundle" ]; then 145 | gatling.sh -sf ${SIMULATIONS_DIR_PATH} -s ${SIMULATION_CLASS} -rsf ${RESOURCES_DIR_PATH} -rf ${RESULTS_DIR_PATH} -nr -rm local 146 | elif [ ${SIMULATIONS_FORMAT} = "gradle" ]; then 147 | gradle -Dgatling.core.directory.results=${RESULTS_DIR_PATH} gatlingRun-${SIMULATION_CLASS} 148 | fi 149 | 150 | GATLING_EXIT_STATUS=$? 151 | if [ $GATLING_EXIT_STATUS -ne 0 ]; then 152 | RUN_STATUS_FILE="${RESULTS_DIR_PATH}/FAILED" 153 | echo "gatling.sh has failed!" 1>&2 154 | fi 155 | touch ${RUN_STATUS_FILE} 156 | exit $GATLING_EXIT_STATUS 157 | ` 158 | Expect(GetGatlingRunnerCommand(simulationsFormat, simulationsDirectoryPath, tempSimulationsDirectoryPath, resourcesDirectoryPath, resultsDirectoryPath, startTime, simulationClass, generateLocalReport)).To(Equal(expectedValue)) 159 | }) 160 | }) 161 | 162 | var _ = Describe("GetGatlingTransferResultCommand", func() { 163 | var ( 164 | resultsDirectoryPath string 165 | provider string 166 | region string 167 | storagePath string 168 | expectedValue string 169 | ) 170 | 171 | BeforeEach(func() { 172 | resultsDirectoryPath = "testResultsDirectoryPath" 173 | region = "ap-northeast-1" 174 | storagePath = "testStoragePath" 175 | }) 176 | 177 | Context("Provider is aws", func() { 178 | BeforeEach(func() { 179 | provider = "aws" 180 | expectedValue = ` 181 | RESULTS_DIR_PATH=testResultsDirectoryPath 182 | rclone config create s3 s3 env_auth=true region ap-northeast-1 183 | while true; do 184 | if [ -f "${RESULTS_DIR_PATH}/FAILED" ]; then 185 | echo "Skip transfering gatling results" 186 | break 187 | fi 188 | if [ -f "${RESULTS_DIR_PATH}/COMPLETED" ]; then 189 | for source in $(find ${RESULTS_DIR_PATH} -type f -name *.log) 190 | do 191 | rclone copyto ${source} --s3-no-check-bucket --s3-env-auth testStoragePath/${HOSTNAME}.log 192 | done 193 | break 194 | fi 195 | sleep 1; 196 | done 197 | ` 198 | }) 199 | It("provider is aws", func() { 200 | Expect(GetGatlingTransferResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue)) 201 | }) 202 | }) 203 | 204 | Context("Provider is gcp", func() { 205 | BeforeEach(func() { 206 | provider = "gcp" 207 | expectedValue = ` 208 | RESULTS_DIR_PATH=testResultsDirectoryPath 209 | # assumes gcs bucket using uniform bucket-level access control 210 | rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive 211 | while true; do 212 | if [ -f "${RESULTS_DIR_PATH}/FAILED" ]; then 213 | echo "Skip transfering gatling results" 214 | break 215 | fi 216 | if [ -f "${RESULTS_DIR_PATH}/COMPLETED" ]; then 217 | # assumes each pod only contain single gatling log file but use for loop to use find command result 218 | for source in $(find ${RESULTS_DIR_PATH} -type f -name *.log) 219 | do 220 | rclone copyto ${source} testStoragePath/${HOSTNAME}.log 221 | done 222 | break 223 | fi 224 | sleep 1; 225 | done 226 | ` 227 | }) 228 | It("Provider is gcp", func() { 229 | Expect(GetGatlingTransferResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue)) 230 | }) 231 | }) 232 | 233 | Context("Provider is non-supported one", func() { 234 | BeforeEach(func() { 235 | provider = "foo" 236 | expectedValue = "" 237 | }) 238 | It("Provide is non-supported one", func() { 239 | Expect(GetGatlingTransferResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue)) 240 | }) 241 | }) 242 | 243 | Context("Provider is empty", func() { 244 | BeforeEach(func() { 245 | provider = "" 246 | expectedValue = "" 247 | }) 248 | It("Provider is empty", func() { 249 | Expect(GetGatlingTransferResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue)) 250 | }) 251 | }) 252 | }) 253 | 254 | var _ = Describe("GetGatlingAggregateResultCommand", func() { 255 | var ( 256 | resultsDirectoryPath string 257 | provider string 258 | region string 259 | storagePath string 260 | expectedValue string 261 | ) 262 | 263 | BeforeEach(func() { 264 | resultsDirectoryPath = "testResultsDirectoryPath" 265 | region = "ap-northeast-1" 266 | storagePath = "testStoragePath" 267 | }) 268 | 269 | Context("Provider is aws", func() { 270 | BeforeEach(func() { 271 | provider = "aws" 272 | expectedValue = ` 273 | GATLING_AGGREGATE_DIR=testResultsDirectoryPath 274 | rclone config create s3 s3 env_auth=true region ap-northeast-1 275 | rclone copy --s3-no-check-bucket --s3-env-auth testStoragePath ${GATLING_AGGREGATE_DIR} 276 | ` 277 | }) 278 | It("provider is aws", func() { 279 | Expect(GetGatlingAggregateResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue)) 280 | }) 281 | }) 282 | 283 | Context("Provider is gcp", func() { 284 | BeforeEach(func() { 285 | provider = "gcp" 286 | expectedValue = ` 287 | GATLING_AGGREGATE_DIR=testResultsDirectoryPath 288 | # assumes gcs bucket using uniform bucket-level access control 289 | rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive 290 | rclone copy testStoragePath ${GATLING_AGGREGATE_DIR} 291 | ` 292 | }) 293 | It("Provider is gcp", func() { 294 | Expect(GetGatlingAggregateResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue)) 295 | }) 296 | }) 297 | 298 | Context("Provider is non-supported one", func() { 299 | BeforeEach(func() { 300 | provider = "foo" 301 | expectedValue = "" 302 | }) 303 | It("Provide is non-supported one", func() { 304 | Expect(GetGatlingAggregateResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue)) 305 | }) 306 | }) 307 | 308 | Context("Provider is empty", func() { 309 | BeforeEach(func() { 310 | provider = "" 311 | expectedValue = "" 312 | }) 313 | It("Provider is empty", func() { 314 | Expect(GetGatlingAggregateResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue)) 315 | }) 316 | }) 317 | }) 318 | 319 | var _ = Describe("GetGatlingGenerateReportCommand", func() { 320 | var ( 321 | resultsDirectoryPath string 322 | expectedValue string 323 | ) 324 | 325 | BeforeEach(func() { 326 | resultsDirectoryPath = "testResultsDirectoryPath" 327 | expectedValue = ` 328 | GATLING_AGGREGATE_DIR=testResultsDirectoryPath 329 | DIR_NAME=$(dirname ${GATLING_AGGREGATE_DIR}) 330 | BASE_NAME=$(basename ${GATLING_AGGREGATE_DIR}) 331 | gatling.sh -rf ${DIR_NAME} -ro ${BASE_NAME} 332 | ` 333 | }) 334 | 335 | It("getExceptValue", func() { 336 | Expect(GetGatlingGenerateReportCommand(resultsDirectoryPath)).To(Equal(expectedValue)) 337 | }) 338 | }) 339 | 340 | var _ = Describe("GetGatlingTransferReportCommand", func() { 341 | var ( 342 | resultsDirectoryPath string 343 | provider string 344 | region string 345 | storagePath string 346 | expectedValue string 347 | ) 348 | 349 | BeforeEach(func() { 350 | resultsDirectoryPath = "testResultsDirectoryPath" 351 | region = "ap-northeast-1" 352 | storagePath = "testStoragePath" 353 | }) 354 | 355 | Context("Provider is aws", func() { 356 | BeforeEach(func() { 357 | provider = "aws" 358 | expectedValue = ` 359 | GATLING_AGGREGATE_DIR=testResultsDirectoryPath 360 | rclone config create s3 s3 env_auth=true region ap-northeast-1 361 | rclone copy ${GATLING_AGGREGATE_DIR} --exclude "*.log" --s3-no-check-bucket --s3-env-auth testStoragePath 362 | ` 363 | }) 364 | It("provider is aws", func() { 365 | Expect(GetGatlingTransferReportCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue)) 366 | }) 367 | }) 368 | 369 | Context("Provider is gcp", func() { 370 | BeforeEach(func() { 371 | provider = "gcp" 372 | expectedValue = ` 373 | GATLING_AGGREGATE_DIR=testResultsDirectoryPath 374 | # assumes gcs bucket using uniform bucket-level access control 375 | rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive 376 | rclone copy ${GATLING_AGGREGATE_DIR} --exclude "*.log" testStoragePath 377 | ` 378 | }) 379 | It("Provider is gcp", func() { 380 | Expect(GetGatlingTransferReportCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue)) 381 | }) 382 | }) 383 | 384 | Context("Provider is non-supported one", func() { 385 | BeforeEach(func() { 386 | provider = "foo" 387 | expectedValue = "" 388 | }) 389 | It("Provide is non-supported one", func() { 390 | Expect(GetGatlingTransferReportCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue)) 391 | }) 392 | }) 393 | 394 | Context("Provider is empty", func() { 395 | BeforeEach(func() { 396 | provider = "" 397 | expectedValue = "" 398 | }) 399 | It("Provider is empty", func() { 400 | Expect(GetGatlingTransferReportCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue)) 401 | }) 402 | }) 403 | }) 404 | -------------------------------------------------------------------------------- /pkg/commands/suite_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestBucket(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Commands Suite") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/notificationservices/notificationservice.go: -------------------------------------------------------------------------------- 1 | package notificationservices 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type NotificationServiceProvider interface { 8 | GetName() string 9 | Notify(gatlingName string, reportURL string, secretData map[string][]byte) error 10 | } 11 | 12 | // use sync.Map to achieve thread safe read and write to map 13 | var notificationServiceProvidersSyncMap = &sync.Map{} 14 | 15 | func GetProvider(provider string) *NotificationServiceProvider { 16 | v, ok := notificationServiceProvidersSyncMap.Load(provider) 17 | if !ok { 18 | var nsp NotificationServiceProvider 19 | switch provider { 20 | case "slack": 21 | nsp = &SlackNotificationServiceProvider{providerName: provider} 22 | default: 23 | return nil 24 | } 25 | v, _ = notificationServiceProvidersSyncMap.LoadOrStore(provider, &nsp) 26 | } 27 | return v.(*NotificationServiceProvider) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/notificationservices/notificationservice_test.go: -------------------------------------------------------------------------------- 1 | package notificationservices 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("GetProvider", func() { 9 | var ( 10 | provider string 11 | expectedValue string 12 | ) 13 | Context("Provider is slack", func() { 14 | BeforeEach(func() { 15 | provider = "slack" 16 | expectedValue = "slack" 17 | }) 18 | It("should get a pointer of SlackNotificationServiceProvider that has ProviderName field value = slack", func() { 19 | nspp := GetProvider(provider) 20 | Expect(nspp).NotTo(BeNil()) 21 | Expect((*nspp).GetName()).To(Equal(expectedValue)) 22 | }) 23 | }) 24 | 25 | Context("Provider is non-supported one", func() { 26 | BeforeEach(func() { 27 | provider = "foo" 28 | }) 29 | It("should get nil pointer ", func() { 30 | nspp := GetProvider(provider) 31 | // If it should be nil, use BeNil() instead of Equal(nil) ref: https://onsi.github.io/gomega/ 32 | Expect(nspp).To(BeNil()) 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /pkg/notificationservices/slack.go: -------------------------------------------------------------------------------- 1 | package notificationservices 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | 10 | utils "github.com/st-tech/gatling-operator/pkg/utils" 11 | ) 12 | 13 | type SlackNotificationServiceProvider struct { 14 | providerName string 15 | } 16 | 17 | func (p *SlackNotificationServiceProvider) GetName() string { 18 | return p.providerName 19 | } 20 | 21 | func (p *SlackNotificationServiceProvider) Notify(gatlingName string, reportURL string, secretData map[string][]byte) error { 22 | 23 | webhookURL, exists := utils.GetMapValue("incoming-webhook-url", secretData) 24 | if !exists { 25 | return errors.New("Insufficient secret data for slack: incoming-webhook-url is missing") 26 | } 27 | payloadTextFormat := ` 28 | [%s] Gatling has completed successfully! 29 | Report URL: %s 30 | ` 31 | payloadText := fmt.Sprintf(payloadTextFormat, gatlingName, reportURL) 32 | if err := slackNotify(webhookURL, payloadText); err != nil { 33 | return err 34 | } 35 | return nil 36 | } 37 | 38 | type payload struct { 39 | Text string `json:"text"` 40 | } 41 | 42 | func slackNotify(webhookURL string, payloadText string) error { 43 | p := payload{ 44 | Text: payloadText, 45 | } 46 | s, err := json.Marshal(p) 47 | if err != nil { 48 | return err 49 | } 50 | resp, err := http.PostForm(webhookURL, 51 | url.Values{"payload": {string(s)}}) 52 | if err != nil { 53 | return err 54 | } 55 | defer resp.Body.Close() 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/notificationservices/suite_test.go: -------------------------------------------------------------------------------- 1 | package notificationservices 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestBucket(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "NotificationServices Suite") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "hash/fnv" 6 | "os" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | func Hash(s string) uint32 { 12 | h := fnv.New32a() 13 | if _, err := h.Write([]byte(s)); err != nil { 14 | return 0 15 | } 16 | return h.Sum32() 17 | } 18 | 19 | func GetEpocTime() int32 { 20 | return int32(time.Now().Unix()) 21 | } 22 | 23 | func GetMapValue(key string, dataMap map[string][]byte) (string, bool) { 24 | if v, exists := dataMap[key]; exists { 25 | return string(v), true 26 | } 27 | return "", false 28 | } 29 | 30 | // Add key-value to map 31 | func AddMapValue(key string, value string, dataMap map[string]string, overwrite bool) map[string]string { 32 | if _, ok := dataMap[key]; ok && !overwrite { 33 | return dataMap 34 | } 35 | if dataMap == nil { 36 | dataMap = map[string]string{} 37 | } 38 | dataMap[key] = value 39 | return dataMap 40 | 41 | } 42 | 43 | func GetNumEnv(key string, defaultValue int) int { 44 | value := os.Getenv(key) 45 | if value == "" { 46 | return defaultValue 47 | } 48 | valueNum, err := strconv.Atoi(value) 49 | if err != nil { 50 | fmt.Fprintf(os.Stderr, "ENV '%s' is not valid\n", key) 51 | return defaultValue 52 | } 53 | return valueNum 54 | } 55 | --------------------------------------------------------------------------------