diff --git a/contrib/executor/jmeterd/.dockerignore b/contrib/executor/jmeterd/.dockerignore new file mode 100644 index 00000000000..e181b4bbb6b --- /dev/null +++ b/contrib/executor/jmeterd/.dockerignore @@ -0,0 +1,10 @@ +.git +.gitignore +.golangci.yml +CODE_OF_CONDUCT.md +CONTRIBUTING.md +LICENSE +Makefile +README.md +temp +data \ No newline at end of file diff --git a/contrib/executor/jmeterd/.env.sample b/contrib/executor/jmeterd/.env.sample new file mode 100644 index 00000000000..9b3e4d4d166 --- /dev/null +++ b/contrib/executor/jmeterd/.env.sample @@ -0,0 +1,6 @@ +# used if storage backend is behind HTTPS, should be set to false for local development +RUNNER_SSL=false +# used to enable/disable scrapper, should be set to false for local development +RUNNER_SCRAPPERENABLED=false +# path to the data/ directory where JMeter will run and store results +RUNNER_DATADIR=./data diff --git a/contrib/executor/jmeterd/.gitignore b/contrib/executor/jmeterd/.gitignore new file mode 100644 index 00000000000..a67117cf392 --- /dev/null +++ b/contrib/executor/jmeterd/.gitignore @@ -0,0 +1,35 @@ +### Go template +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +### JetBrains +.idea +*.iml + +# Helm +Chart.lock + +.DS_Store +bin/ +site +dist/ +data/ +temp/ + +.vscode + +### Environment +.env \ No newline at end of file diff --git a/contrib/executor/jmeterd/.golangci.yml b/contrib/executor/jmeterd/.golangci.yml new file mode 100644 index 00000000000..a0ae72e0c50 --- /dev/null +++ b/contrib/executor/jmeterd/.golangci.yml @@ -0,0 +1,29 @@ +run: + timeout: 5m + +linters: + disable-all: true + enable: + - errcheck + - goimports + - govet + - staticcheck + - revive + - unused + - errname + - errorlint + - gocyclo + - gofmt + - goimports + - misspell + - predeclared + +linters-settings: + govet: + check-shadowing: true + lll: + line-length: 150 + misspell: + locale: US + goimports: + local-prefixes: github.com/kubeshop/testkube,github.com/kubeshop/testkube-executor-jmeter diff --git a/contrib/executor/jmeterd/CODE_OF_CONDUCT.md b/contrib/executor/jmeterd/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..fa481824840 --- /dev/null +++ b/contrib/executor/jmeterd/CODE_OF_CONDUCT.md @@ -0,0 +1,30 @@ +# testkube (by Kubeshop) Community Code of Conduct + +Testkube follows the CNCF Code of Conduct. The text of the CNCF CoC is replicated below. If you notice that this is out of date, please file an issue. + +If you notice a violation of the Code of Conduct at an event or meeting, in Slack, or in another communication mechanism, reach out to the Testkube Code of Conduct Committee. You can reach us by email at contact@kubeshop.io Your anonymity will be protected. + +# CNCF Community Code of Conduct v1.0 + +## Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. + +## Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery +- Personal attacks +- Trolling or insulting/derogatory comments +- Public or private harassment +- Publishing other's private information, such as physical or electronic addresses, without explicit permission +- Other unethical or unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. + +This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior in Testkube may be reported by contacting the Testkube Code of Conduct Committee via contact@kubeshop.io. + +This Code of Conduct is adapted from the Contributor Covenant (https://contributor-covenant.org), version 1.2.0, available at https://contributor-covenant.org/version/1/2/0/ diff --git a/contrib/executor/jmeterd/CONTRIBUTING.md b/contrib/executor/jmeterd/CONTRIBUTING.md new file mode 100644 index 00000000000..8588d097621 --- /dev/null +++ b/contrib/executor/jmeterd/CONTRIBUTING.md @@ -0,0 +1,44 @@ +# Contribution to Testkube + +Thanks for reaching out contribution 🎉 + +If you're new in Open-source community there is nice guide how to start contributing to projects: +https://github.com/firstcontributions/first-contributions + +## Code of Conduct + +This project and everyone participating in it is governed by the Testkube [code of conduct](CODE_OF_CONDUCT.md) + +## Have questions or idea? + +We're using github discussions for managing pre-development ideas and clarifications, feel free to add one at our [Q&A discussion page](https://github.com/kubeshop/testkube/discussions/categories/q-a) + +New ideas should be placed in [Ideas discussion page](https://github.com/kubeshop/testkube/discussions/categories/ideas) + + + +## General guidance for contributing to Testkube project + +You're very welcome to help in testkube development, there is a lot of incoming work to do :). + +We're trying hard to limit technical debt from the beginning so we defined simple rules into Testkube repo to help with it. + +### For golang based components + +- Always use gofmt (there is only one true way of doing code formatting ;) ). +- Follow golang good practices (proverbs) in your code. +- Tests are your friend (we will target 80% CC in our code). +- Use clean names, don't brake basic design patterns and rules. + +### For infrastructure / Kubernetes based components + +- We're using helm charts to build and share Testkube +- Comment non-obvious decisions +- Use current Helm/Kubernetes versions + + +## How can I help? + +- By fixing [one of many Issues](https://github.com/kubeshop/testkube/issues) - simply fork our repo and create new Pull Request with new code changes. +- By helping to reach out valid results [from discussions](https://github.com/kubeshop/testkube/discussions) + diff --git a/contrib/executor/jmeterd/LICENSE b/contrib/executor/jmeterd/LICENSE new file mode 100644 index 00000000000..4c691a5bad3 --- /dev/null +++ b/contrib/executor/jmeterd/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Kubeshop + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/contrib/executor/jmeterd/Makefile b/contrib/executor/jmeterd/Makefile new file mode 100644 index 00000000000..9f5c69b5ecb --- /dev/null +++ b/contrib/executor/jmeterd/Makefile @@ -0,0 +1,64 @@ +REPOSITORY ?= kubeshop +NAME ?= testkube-jmeterd-executor +SLAVES_NAME ?= testkube-jmeterd-slaves +LOCAL_TAG ?= 999.0.0 +BIN_DIR ?= $(HOME)/bin + +.PHONY: build +build: + go build -o $(BIN_DIR)/$(NAME) cmd/agent/main.go + +.PHONY: build-local-linux +build-local-linux: + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/runner cmd/agent/main.go + +.PHONY: run +run: + EXECUTOR_PORT=8082 go run cmd/agent/main.go ${run_args} + +.PHONY: docker-build +docker-build: + docker build -t $(REPOSITORY)/$(NAME) -f build/agent/Dockerfile . + +.PHONY: docker-build-local +docker-build-local: build-local-linux + docker build -t $(REPOSITORY)/$(NAME):$(LOCAL_TAG) -f build/agent/local.Dockerfile . + +docker-build-slaves: + docker build -t $(REPOSITORY)/$(SLAVES_NAME):$(LOCAL_TAG) -f build/slaves/Dockerfile . + +.PHONY: kind-load-local +kind-load-local: build-local-linux + kind load docker-image kubeshop/testkube-jmeterd-executor:999.0.0 + +.PHONY: test +test: + go test ./... -cover + +.PHONY: integration-test +integration-test: + INTEGRATION=y gotestsum --format short-verbose -- -run _Integration -cover ./... + +.PHONY: cover +cover: + @go test -failfast -count=1 -v -tags test -coverprofile=./testCoverage.txt ./... && go tool cover -html=./testCoverage.txt -o testCoverage.html && rm ./testCoverage.txt + open testCoverage.html + +.PHONY: version-bump +version-bump: version-bump-patch + +.PHONY: version-bump-patch +version-bump-patch: + go run cmd/tools/main.go bump -k patch + +.PHONY: version-bump-minor +version-bump-minor: + go run cmd/tools/main.go bump -k minor + +.PHONY: version-bump-major +version-bump-major: + go run cmd/tools/main.go bump -k major + +.PHONY: version-bump-dev +version-bump-dev: + go run cmd/tools/main.go bump --dev diff --git a/contrib/executor/jmeterd/README.md b/contrib/executor/jmeterd/README.md new file mode 100644 index 00000000000..c765aa90435 --- /dev/null +++ b/contrib/executor/jmeterd/README.md @@ -0,0 +1,156 @@ +![Testkube Logo](https://raw.githubusercontent.com/kubeshop/testkube/main/assets/testkube-color-gray.png) + +[![Go Report Card](https://goreportcard.com/badge/github.com/kubeshop/testkube-executor-jmeter)](https://goreportcard.com/report/github.com/kubeshop/testkube-executor-jmeter) +[![Go Reference](https://pkg.go.dev/badge/github.com/kubeshop/testkube-executor-jmeter.svg)](https://pkg.go.dev/github.com/kubeshop/testkube-executor-jmeter) +[![License](https://img.shields.io/github/license/kubeshop/testkube-executor-jmeter)]() + +# Distributed JMeter Executor +An extension of Jmeter Executor which can run the Jmeter Tests in distributed environment. + +## What is an Executor? + +Executor is nothing more than a program wrapped into Docker container which gets JSON (testube.Execution) OpenAPI based document as an input and returns a stream of JSON output lines (testkube.ExecutorOutput), +where each output line is simply wrapped in this JSON, similar to the structured logging idea. + + +## Intro + +It's basic Distributed JMeter executor which is able to run simple JMeter scenarios in distributed environments. +Please define your JMeter file as file (string, or git file). + +## Local development + +### Prerequisites + +Make sure the following tools are installed on your machine and available in your PATH: +* [JMeter](https://jmeter.apache.org/download_jmeter.cgi) - pure Java application designed to load test functional behavior and measure performance + +### Setup +1. Create a directory called `data/` where JMeter will run and store results (best practice is to create it in the project root because it is git-ignored) +2. Create a JMeter XML project file and save it as a file named `test-content` in the newly created `data/` directory +3. Create an execution JSON file and save it as a file named `execution.json` based on the template below (best practice is to save it in the `temp/` folder in the project root because it is git-ignored) + ```json + { + "id": "jmeter-test", + "args": [], + "variables": {}, + "content": { + "type": "string" + } + } + ``` +4. You need to provide the `RUNNER_SCRAPPERENABLED`, `RUNNER_SSL` and `RUNNER_DATADIR` environment variables and run the Executor using the `make run run_args="-f|--file "` make command where `-f|--file ` argument is the path to the `execution.json` file you created in step 3. + ```bash + RUNNER_SCRAPPERENABLED=false RUNNER_SSL=false RUNNER_DATADIR="./data" make run run_args="-f temp/execution.json" + ``` + +#### Execution JSON + +Execution JSON stores information required for an Executor to run the configured tests. + +Breakdown of the Execution JSON: +```json +{ + "args": ["-n", "-t", "test.jmx"], + "variables": { + "example": { + "type": "basic", + "name": "example", + "value": "some-value" + } + }, + "content": { + "type": "string" + } +} +``` +* **args** - array of strings which will be passed to JMeter as arguments + * example: `["-n", "-t", "test.jmx"]` +* **variables** - map of variables which will be passed to JMeter as arguments + * example: `{"example": {"type": "basic", "name": "example", "value": "some-value"}}` +* **content.type** - used to specify that JMeter XML is provided as a text file + +#### Environment Variables +```bash +RUNNER_SSL=false # used if storage backend is behind HTTPS, should be set to false for local development +RUNNER_SCRAPPERENABLED=false # used to enable/disable scrapper, should be set to false for local development +RUNNER_DATADIR= # path to the data/ directory where JMeter will run and store results +``` + +## Testing in Kubernetes + +### Prerequisites +* Kubernetes cluster with Testkube installed (best practice is to install it in the `testkube` namespace) + +### Guide + +After validating locally that the Executor changes work as expected, next step is to test whether Testkube can successfully schedule a Test using the new Executor image. + +NOTE: The following commands assume that Testkube is installed in the `testkube` namespace, if you have it installed in a different namespace, please adjust the `--namespace` flag accordingly. + +The following steps need to be executed in order for Testkube to use the new Executor image: +1. Build the new Executor image using the `make docker-build-local` command. By default, the image will be tagged as `kubeshop/testkube-executor-jmeter:999.0.0` unless a `LOCAL_TAG` environment variable is provided before the command. +2. Now you need to make the image accessible in Kubernetes, there are a couple of approaches: + * *kind* - `kind load docker-image --name ` (e.g. `kind load docker-image testkube-executor-jmeter:999.0.0 --name testkube-k8s-cluster`) + * *minikube* - `minikube image load --profile ` (e.g. `minikube image load testkube-executor-jmeter:999.0.0 --profile k8s-cluster-test`) + * *Docker Desktop* - just by building the image locally, it becomes accessible in the Docker Desktop Kubernetes cluster + * *other* - you can push the image to a registry and then Testkube will pull it in Kubernetes (assuming it has credentials for it if needed) +3. Edit the Job Template and change the `imagePullPolicy` to `IfNotPresent` + * Edit the ConfigMap `testkube-api-server` either by running `kubectl edit configmap testkube-api-server --namespace testkube` or by using a tool like Monokle + * Find the `job-template.yml` key and change the `imagePullPolicy` field in the `containers` section to `IfNotPresent` +4. Edit the Executors configuration and change the base image to use the newly created image: + * Edit the ConfigMap `testkube-api-server` either by running `kubectl edit configmap testkube-api-server --namespace testkube` or by using a tool like Monokle + * Find the `executors.json` key and change the `executor.image` field to use the newly created image for the JMeter Executor (`name` field is `jmeter-executor`) +5. Restart the API Server by running `kubectl rollout restart deployment testkube-api-server --namespace testkube` + +Testkube should now use the new image for the Executor and you can schedule a Test with your preferred method. + +### Supported Environment Variables + +1. **MASTER_OVERRIDE_JVM_ARGS / SLAVES_OVERRIDE_JVM_ARGS**: Used to override default memory options for JMeter master/slaves. Example: `MASTER_OVERRIDE_JVM_ARGS=-Xmn256m -Xms512m -Xmx512m`. + +2. **SLAVES_COUNT**: Specifies the number of slave pods required for Distributed JMeter tests. Example: `SLAVES_COUNT=3`. Default value of `SLAVES_COUNT` is 1. + +3. **MASTER_ADDITIONAL_JVM_ARGS / SLAVES_ADDITIONAL_JMETER_ARGS**: Allows exporting additional JVM arguments for slaves/master. Example: `MASTER_ADDITIONAL_JVM_ARGS=-Xmx1024m -Xms512m -XX:MaxMetaspaceSize=256m`. + +4. **SLAVES_ADDITIONAL_JMETER_ARGS**: Provides additional JVM arguments for JMeter server / slaves. Example: `SLAVES_ADDITIONAL_JMETER_ARGS=jmeter-server -Jserver.rmi.ssl.disable=true -Dserver_port=60000`. + + +Below guide will provide you the details about how to run a Jmeter test in distributed environment. + +1. File option: + + When you provide a test (.jmx) file to `Distributed JMeter ( Jmeter in distributed environment )`, the executor of `Distributed JMeter` will spawn number of slaves pods specified by user through `SLAVES_COUNT` environment variable as desribed above and run the test on all the slave pods. + +2. Git Option: + Using Git flow of the executor we can have following benifits of `Distributed JMeter` executor which is not possible with JMeter executor: + + - Additional file required by a particular test like a CSV or JSON file can be provided through git repo. + There is an example of using a CSV file by test (.jmx) file in the `example` folder of `Distributed JMeter`. + - Dynamic plugins required for a test by keeping the plugins inside the test folder in a directory named as `plugins` in the git repo. + - Custom values of the paramters present in `user.properties` can be provided by using custom `user.properties` file in the git repo. + + + For using the Git option and to avail all the above features, user should have the following directory structure in the git repo: + + ``` + github.com/`/`/--- + + |-test1/--- + |- testfile1.jmx + |- userdata.csv ( or any other additional file ) + |- user.properties + |- plugins/--- + |- plugin-manager.jar + |- + + |-test2/--- + |- testfile2.jmx + |- userdata.json ( or any other additional file ) + |- user.properties + |- plugins/--- + |- plugin-manager.jar + |- + + ``` + diff --git a/contrib/executor/jmeterd/build/agent/Dockerfile b/contrib/executor/jmeterd/build/agent/Dockerfile new file mode 100644 index 00000000000..6c3325f8ab6 --- /dev/null +++ b/contrib/executor/jmeterd/build/agent/Dockerfile @@ -0,0 +1,11 @@ +# syntax=docker/dockerfile:1 +FROM kubeshop/jmeter:5.5 +COPY jmeterd /bin/runner + +RUN microdnf update -y && microdnf install -y ca-certificates git && microdnf clean all +ENV ENTRYPOINT_CMD="/executor_entrypoint.sh" +WORKDIR /root/ +COPY ./contrib/executor/jmeterd/scripts/entrypoint.sh /executor_entrypoint.sh +COPY scripts/jmeter-master.sh /executor_entrypoint_master.sh + +ENTRYPOINT ["/bin/runner"] diff --git a/contrib/executor/jmeterd/build/agent/local.Dockerfile b/contrib/executor/jmeterd/build/agent/local.Dockerfile new file mode 100644 index 00000000000..aeefa13e996 --- /dev/null +++ b/contrib/executor/jmeterd/build/agent/local.Dockerfile @@ -0,0 +1,16 @@ +# syntax=docker/dockerfile:1 + +FROM kubeshop/jmeter:5.5 + +RUN microdnf update -y && microdnf install -y ca-certificates git && microdnf clean all + +WORKDIR /root/ + +ENV ENTRYPOINT_CMD="/executor_entrypoint.sh" + +COPY dist/runner /bin/runner +COPY scripts/entrypoint.sh /executor_entrypoint.sh +COPY scripts/jmeter-master.sh /executor_entrypoint_master.sh + +ENTRYPOINT ["/bin/runner"] + diff --git a/contrib/executor/jmeterd/build/slaves/Dockerfile b/contrib/executor/jmeterd/build/slaves/Dockerfile new file mode 100644 index 00000000000..20db6aeef78 --- /dev/null +++ b/contrib/executor/jmeterd/build/slaves/Dockerfile @@ -0,0 +1,9 @@ +FROM kubeshop/jmeter:5.5 + + +EXPOSE 1099 60001 +ENV SSL_DISABLED true + +COPY ./scripts/jmeter-slaves.sh /jmeter_slaves_entrypoint.sh +RUN chmod +x /jmeter_slaves_entrypoint.sh +ENTRYPOINT /jmeter_slaves_entrypoint.sh \ No newline at end of file diff --git a/contrib/executor/jmeterd/cmd/agent/main.go b/contrib/executor/jmeterd/cmd/agent/main.go new file mode 100644 index 00000000000..cb61a04966f --- /dev/null +++ b/contrib/executor/jmeterd/cmd/agent/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "os" + + "github.com/pkg/errors" + + "github.com/kubeshop/testkube/contrib/executor/jmeterd/pkg/runner" + "github.com/kubeshop/testkube/pkg/envs" + "github.com/kubeshop/testkube/pkg/executor/agent" + "github.com/kubeshop/testkube/pkg/executor/output" +) + +func main() { + ctx := context.Background() + params, err := envs.LoadTestkubeVariables() + if err != nil { + output.PrintError(os.Stderr, errors.Errorf("could not initialize Distributed JMeter Executor environment variables: %v", err)) + os.Exit(1) + } + r, err := runner.NewRunner(ctx, params) + if err != nil { + output.PrintError(os.Stderr, errors.Wrap(err, "error instantiating Distributed JMeter Executor")) + os.Exit(1) + } + agent.Run(ctx, r, os.Args) +} diff --git a/contrib/executor/jmeterd/cmd/tools/main.go b/contrib/executor/jmeterd/cmd/tools/main.go new file mode 100644 index 00000000000..39a186a395d --- /dev/null +++ b/contrib/executor/jmeterd/cmd/tools/main.go @@ -0,0 +1,22 @@ +package main + +import "github.com/kubeshop/testkube/cmd/tools/commands" + +var ( + commit string + version string + builtBy string + date string +) + +func init() { + // pass data from goreleaser to commands package + commands.Version = version + commands.BuiltBy = builtBy + commands.Commit = commit + commands.Date = date +} + +func main() { + commands.Execute() +} diff --git a/contrib/executor/jmeterd/examples/executor.yaml b/contrib/executor/jmeterd/examples/executor.yaml new file mode 100644 index 00000000000..176bda2d9e7 --- /dev/null +++ b/contrib/executor/jmeterd/examples/executor.yaml @@ -0,0 +1,11 @@ +apiVersion: executor.testkube.io/v1 +kind: Executor +metadata: + name: jmeterd-executor + namespace: testkube +spec: + features: + - artifacts + image: kubeshop/testkube-jmeterd-executor:dev-008 + types: + - jmeterd/test diff --git a/contrib/executor/jmeterd/examples/gitflow/README.md b/contrib/executor/jmeterd/examples/gitflow/README.md new file mode 100644 index 00000000000..c0de6a197e8 --- /dev/null +++ b/contrib/executor/jmeterd/examples/gitflow/README.md @@ -0,0 +1,9 @@ +## Steps to execute this Test + +1. Push all the files and directories in a github repo in a test folder +2. Set an env variable `DATA_CONFIG = /data/repo/` +3. While creating test in the testkube dashboard pass the args `-GJMETER_UC1_NBUSERS=5 jmeter-properties-external.jmx` +4. Create the test as `jmeterd/test` type with `Git` option as per above configuration. +5. Fill the required details like `github repo link`, `username` and `GITHUB_TOKEN` +5. Add your desired number of slave pods by setting the env `SLAVES_COUNT`. +6. Run the test. diff --git a/contrib/executor/jmeterd/examples/gitflow/jmeter-properties-external.jmx b/contrib/executor/jmeterd/examples/gitflow/jmeter-properties-external.jmx new file mode 100644 index 00000000000..27fc617b0d7 --- /dev/null +++ b/contrib/executor/jmeterd/examples/gitflow/jmeter-properties-external.jmx @@ -0,0 +1,203 @@ + + + + + Kubeshop site simple perf test + false + true + false + + + + PATH + /pricing + = + + + + + + + + + + UC1_NBUSERS + ${__property(JMETER_UC1_NBUSERS,,2)} + = + + + UC1_RAMPUP + ${__property(JMETER_UC1_RAMPUP,,2)} + = + + + URI_PATH + ${__property(JMETER_URI_PATH,,/pricing)} + = + + + PROJECT_HOME + ${__BeanShell(import org.apache.jmeter.services.FileServer; FileServer.getFileServer().getBaseDir();)} + = + + + + + + , + + ${__env(DATA_CONFIG,,${__eval(${PROJECT_HOME})})}/data/Credentials.csv + false + false + true + shareMode.group + false + USERNAME,PASSWORD + + + + continue + + false + 1 + + ${UC1_NBUSERS} + ${UC1_RAMPUP} + false + + + true + + + + false + false + + + + String uc1_nbusers = vars.get("UC1_NBUSERS"); +String uc1_rampup = vars.get("UC1_RAMPUP"); +String uri_path = vars.get("URI_PATH"); +String username = vars.get("USERNAME"); +String password = vars.get("PASSWORD"); +log.info("================================="); +log.info("UC1_NBUSERS: " + uc1_nbusers); +log.info("UC1_RAMPUP: " + uc1_rampup); +log.info("URI_PATH: " + uri_path); +log.info("USERNAME: " + username); +log.info("PASSWORD: " + password); +log.info("================================="); + + + false + + + + 6 + true + false + + + + + + + testkube.io + + https + + ${PATH} + GET + true + false + true + false + + + + + + + + SOME_NONExisting_String + + + Assertion.response_data + false + 20 + + + + + + + + testkube.io + + https + + ${PATH} + GET + true + false + true + false + + + + + + + + SOME_NONExisting_String + + + Assertion.response_data + false + 20 + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 2 + true + true + true + true + true + true + + + + + + + + + diff --git a/contrib/executor/jmeterd/examples/gitflow/plugins/JMeterPlugins-Extras.jar b/contrib/executor/jmeterd/examples/gitflow/plugins/JMeterPlugins-Extras.jar new file mode 100644 index 00000000000..7b62b70643d Binary files /dev/null and b/contrib/executor/jmeterd/examples/gitflow/plugins/JMeterPlugins-Extras.jar differ diff --git a/contrib/executor/jmeterd/examples/gitflow/plugins/jmeter-parallel-0.11.jar b/contrib/executor/jmeterd/examples/gitflow/plugins/jmeter-parallel-0.11.jar new file mode 100644 index 00000000000..cd79edec962 Binary files /dev/null and b/contrib/executor/jmeterd/examples/gitflow/plugins/jmeter-parallel-0.11.jar differ diff --git a/contrib/executor/jmeterd/examples/gitflow/plugins/jmeter-plugins-dummy-0.4.jar b/contrib/executor/jmeterd/examples/gitflow/plugins/jmeter-plugins-dummy-0.4.jar new file mode 100644 index 00000000000..2bb188708c6 Binary files /dev/null and b/contrib/executor/jmeterd/examples/gitflow/plugins/jmeter-plugins-dummy-0.4.jar differ diff --git a/contrib/executor/jmeterd/examples/gitflow/plugins/jmeter-plugins-functions-2.2.jar b/contrib/executor/jmeterd/examples/gitflow/plugins/jmeter-plugins-functions-2.2.jar new file mode 100644 index 00000000000..99866f754aa Binary files /dev/null and b/contrib/executor/jmeterd/examples/gitflow/plugins/jmeter-plugins-functions-2.2.jar differ diff --git a/contrib/executor/jmeterd/examples/gitflow/plugins/jmeter-plugins-manager-1.9.jar b/contrib/executor/jmeterd/examples/gitflow/plugins/jmeter-plugins-manager-1.9.jar new file mode 100644 index 00000000000..eac78dcefda Binary files /dev/null and b/contrib/executor/jmeterd/examples/gitflow/plugins/jmeter-plugins-manager-1.9.jar differ diff --git a/contrib/executor/jmeterd/examples/gitflow/user.properties b/contrib/executor/jmeterd/examples/gitflow/user.properties new file mode 100644 index 00000000000..89d54a422f8 --- /dev/null +++ b/contrib/executor/jmeterd/examples/gitflow/user.properties @@ -0,0 +1,174 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Sample user.properties file + +#--------------------------------------------------------------------------- +# Classpath configuration +#--------------------------------------------------------------------------- +# +# List of paths (separated by ;) to search for additional JMeter plugin classes, +# for example new GUI elements and samplers. +# A path item can either be a jar file or a directory. +# Any jar file in such a directory will be automatically included, +# jar files in sub directories are ignored. +# The given value is in addition to any jars found in the lib/ext directory. +# Do not use this for utility or plugin dependency jars. +#search_paths=/app1/lib;/app2/lib + +# List of paths that JMeter will search for utility and plugin dependency classes. +# Use your platform path separator (java.io.File.pathSeparatorChar in Java) to separate multiple paths. +# A path item can either be a jar file or a directory. +# Any jar file in such a directory will be automatically included, +# jar files in sub directories are ignored. +# The given value is in addition to any jars found in the lib directory. +# All entries will be added to the class path of the system class loader +# and also to the path of the JMeter internal loader. +# Paths with spaces may cause problems for the JVM +#Example for windows (; separator) +#user.classpath=../classes;../lib;../app1/jar1.jar;../app2/jar2.jar +#Example for linux (:separator) +#user.classpath=../classes:../lib:../app1/jar1.jar:../app2/jar2.jar + +# List of paths (separated by ;) that JMeter will search for utility +# and plugin dependency classes. +# A path item can either be a jar file or a directory. +# Any jar file in such a directory will be automatically included, +# jar files in sub directories are ignored. +# The given value is in addition to any jars found in the lib directory +# or given by the user.classpath property. +# All entries will be added to the path of the JMeter internal loader only. +# For plugin dependencies using plugin_dependency_paths should be preferred over +# user.classpath. +#plugin_dependency_paths=../dependencies/lib;../app1/jar1.jar;../app2/jar2.jar + +#--------------------------------------------------------------------------- +# Reporting configuration +#--------------------------------------------------------------------------- + +# Configure this property to change the report title +#jmeter.reportgenerator.report_title=Apache JMeter Dashboard + +# Used to generate a report based on a date range if needed +# Default date format (from SimpleDateFormat Java API and Locale.ENGLISH) +#jmeter.reportgenerator.date_format=yyyyMMddHHmmss +# Date range start date using date_format property +#jmeter.reportgenerator.start_date= +# Date range end date using date_format property +#jmeter.reportgenerator.end_date= + +# Change this parameter if you want to change the granularity of over time graphs. +# Set to 60000 ms by default +#jmeter.reportgenerator.overall_granularity=60000 + +# Sets the size of the sliding window used by percentile evaluation. +# Caution : higher value provides a better accuracy but needs more memory. +#jmeter.reportgenerator.statistic_window = 20000 + +# Change this parameter if you want to change the granularity of Response time distribution +# Set to 100 ms by default +#jmeter.reportgenerator.graph.responseTimeDistribution.property.set_granularity=100 + +# Change this parameter if you want to keep only some samples. +# Regular Expression which Indicates which samples to keep for graphs and statistics generation. +# Empty value means no filtering +#jmeter.reportgenerator.sample_filter= + +# Change this parameter if you want to override the APDEX satisfaction threshold. +# Set to 500 ms by default +#jmeter.reportgenerator.apdex_satisfied_threshold=500 + +# Change this parameter if you want to override the APDEX tolerance threshold. +# Set to 1500 ms by default +#jmeter.reportgenerator.apdex_tolerated_threshold=1500 + +# Indicates which graph series are filtered (regular expression) +# In the below example we filter on Search and Order samples +# Note that the end of the pattern should always include (-success|-failure)?$ +# TransactionsPerSecondGraphConsumer suffixes transactions with "-success" or "-failure" depending +# on the result +#jmeter.reportgenerator.exporter.html.series_filter=^(Search|Order)(-success|-failure)?$ + +# Indicates whether only controller samples are displayed on graphs that support it. +#jmeter.reportgenerator.exporter.html.show_controllers_only=false + +# This property is used by menu item "Export transactions for report" +# It is used to select which transactions by default will be exported +#jmeter.reportgenerator.exported_transactions_pattern=[a-zA-Z0-9_\\-{}\\$\\.]*[-_][0-9]* + + +## Custom graph definition +#jmeter.reportgenerator.graph.custom_mm_hit.classname=org.apache.jmeter.report.processor.graph.impl.CustomGraphConsumer +#jmeter.reportgenerator.graph.custom_mm_hit.title=Graph Title +#jmeter.reportgenerator.graph.custom_mm_hit.property.set_Y_Axis=Response Time (ms) +#jmeter.reportgenerator.graph.custom_mm_hit.property.set_X_Axis=Over Time +#jmeter.reportgenerator.graph.custom_mm_hit.property.set_granularity=${jmeter.reportgenerator.overall_granularity} +#jmeter.reportgenerator.graph.custom_mm_hit.property.setSampleVariableName=VarName +#jmeter.reportgenerator.graph.custom_mm_hit.property.setContentMessage=Message for graph point label + +######################################################################## +################## DISTRIBUTED TESTING CONFIGURATION ################## +######################################################################## +# Type of keystore : JKS +# +#server.rmi.ssl.keystore.type=JKS +# +# Keystore file that contains private key +# +#server.rmi.ssl.keystore.file=rmi_keystore.jks +# +# Password of keystore +# +#server.rmi.ssl.keystore.password=changeit +# +# Key alias +# +#server.rmi.ssl.keystore.alias=rmi +# +# Type of truststore : JKS +# +#server.rmi.ssl.truststore.type=JKS +# +# Keystore file that contains certificate +# +#server.rmi.ssl.truststore.file=rmi_keystore.jks +# +# Password of Trust store +# +#server.rmi.ssl.truststore.password=changeit +# +# Set this if you don't want to use SSL for RMI +# +#server.rmi.ssl.disable=false + +jmeter.reportgenerator.overall_granularity=1000 +jmeter.reportgenerator.exporter.html.series_filter=^([0-9]*_.*)(-success|-failure)?$ + + +proxy.cert.validity=31 +node.name=Node1 + +#Cookie management +CookieManager.check.cookies=false + +jmeter.save.saveservice.assertion_results_failure_message=true +jmeter.save.saveservice.response_message=true +jmeter.save.saveservice.successful=true +jmeter.save.saveservice.thread_name=true +jmeter.save.saveservice.time=true +jmeter.save.saveservice.timestamp_format=ms +jmeter.save.saveservice.assertion_results=all diff --git a/contrib/executor/jmeterd/examples/jmeter.log b/contrib/executor/jmeterd/examples/jmeter.log new file mode 100644 index 00000000000..dc8b8dc8510 --- /dev/null +++ b/contrib/executor/jmeterd/examples/jmeter.log @@ -0,0 +1,62 @@ +2022-11-03 09:23:58,163 INFO o.a.j.u.JMeterUtils: Setting Locale to en_EN +2022-11-03 09:23:58,175 INFO o.a.j.JMeter: Loading user properties from: /opt/homebrew/Cellar/jmeter/5.5/libexec/bin/user.properties +2022-11-03 09:23:58,175 INFO o.a.j.JMeter: Loading system properties from: /opt/homebrew/Cellar/jmeter/5.5/libexec/bin/system.properties +2022-11-03 09:23:58,178 INFO o.a.j.JMeter: Copyright (c) 1998-2022 The Apache Software Foundation +2022-11-03 09:23:58,178 INFO o.a.j.JMeter: Version 5.5 +2022-11-03 09:23:58,178 INFO o.a.j.JMeter: java.version=19 +2022-11-03 09:23:58,178 INFO o.a.j.JMeter: java.vm.name=OpenJDK 64-Bit Server VM +2022-11-03 09:23:58,178 INFO o.a.j.JMeter: os.name=Mac OS X +2022-11-03 09:23:58,178 INFO o.a.j.JMeter: os.arch=aarch64 +2022-11-03 09:23:58,178 INFO o.a.j.JMeter: os.version=12.4 +2022-11-03 09:23:58,178 INFO o.a.j.JMeter: file.encoding=UTF-8 +2022-11-03 09:23:58,178 INFO o.a.j.JMeter: java.awt.headless=true +2022-11-03 09:23:58,178 INFO o.a.j.JMeter: Max memory =1073741824 +2022-11-03 09:23:58,178 INFO o.a.j.JMeter: Available Processors =10 +2022-11-03 09:23:58,183 INFO o.a.j.JMeter: Default Locale=English (EN) +2022-11-03 09:23:58,183 INFO o.a.j.JMeter: JMeter Locale=English (EN) +2022-11-03 09:23:58,183 INFO o.a.j.JMeter: JMeterHome=/opt/homebrew/Cellar/jmeter/5.5/libexec +2022-11-03 09:23:58,183 INFO o.a.j.JMeter: user.dir =/Users/exu/code/kube/testkube-executor-jmeter/examples +2022-11-03 09:23:58,183 INFO o.a.j.JMeter: PWD =/Users/exu/code/kube/testkube-executor-jmeter/examples +2022-11-03 09:23:58,185 INFO o.a.j.JMeter: IP: 192.168.1.7 Name: 192.168.1.7 FullName: 192.168.1.7 +2022-11-03 09:23:58,190 INFO o.a.j.s.FileServer: Default base='/Users/exu/code/kube/testkube-executor-jmeter/examples' +2022-11-03 09:23:58,190 INFO o.a.j.s.FileServer: Set new base='/Users/exu/code/kube/testkube-executor-jmeter/examples' +2022-11-03 09:23:58,291 INFO o.a.j.s.SaveService: Testplan (JMX) version: 2.2. Testlog (JTL) version: 2.2 +2022-11-03 09:23:58,307 INFO o.a.j.s.SaveService: Using SaveService properties version 5.0 +2022-11-03 09:23:58,309 INFO o.a.j.s.SaveService: Using SaveService properties file encoding UTF-8 +2022-11-03 09:23:58,310 INFO o.a.j.s.SaveService: Loading file: kubeshop.jmx +2022-11-03 09:23:58,333 INFO o.a.j.p.h.s.HTTPSamplerBase: Parser for text/html is org.apache.jmeter.protocol.http.parser.LagartoBasedHtmlParser +2022-11-03 09:23:58,333 INFO o.a.j.p.h.s.HTTPSamplerBase: Parser for application/xhtml+xml is org.apache.jmeter.protocol.http.parser.LagartoBasedHtmlParser +2022-11-03 09:23:58,333 INFO o.a.j.p.h.s.HTTPSamplerBase: Parser for application/xml is org.apache.jmeter.protocol.http.parser.LagartoBasedHtmlParser +2022-11-03 09:23:58,333 INFO o.a.j.p.h.s.HTTPSamplerBase: Parser for text/xml is org.apache.jmeter.protocol.http.parser.LagartoBasedHtmlParser +2022-11-03 09:23:58,333 INFO o.a.j.p.h.s.HTTPSamplerBase: Parser for text/vnd.wap.wml is org.apache.jmeter.protocol.http.parser.RegexpHTMLParser +2022-11-03 09:23:58,334 INFO o.a.j.p.h.s.HTTPSamplerBase: Parser for text/css is org.apache.jmeter.protocol.http.parser.CssParser +2022-11-03 09:23:58,340 INFO o.a.j.JMeter: Creating summariser +2022-11-03 09:23:58,349 INFO o.a.j.e.StandardJMeterEngine: Running the test! +2022-11-03 09:23:58,350 INFO o.a.j.s.SampleEvent: List of sample_variables: [] +2022-11-03 09:23:58,350 INFO o.a.j.s.SampleEvent: List of sample_variables: [] +2022-11-03 09:23:58,352 INFO o.a.j.e.u.CompoundVariable: Note: Function class names must contain the string: '.functions.' +2022-11-03 09:23:58,352 INFO o.a.j.e.u.CompoundVariable: Note: Function class names must not contain the string: '.gui.' +2022-11-03 09:23:58,382 INFO o.a.j.JMeter: Running test (1667463838382) +2022-11-03 09:23:58,395 INFO o.a.j.e.StandardJMeterEngine: Starting ThreadGroup: 1 : Thread Group +2022-11-03 09:23:58,395 INFO o.a.j.e.StandardJMeterEngine: Starting 1 threads for group Thread Group. +2022-11-03 09:23:58,395 INFO o.a.j.e.StandardJMeterEngine: Thread will continue on error +2022-11-03 09:23:58,395 INFO o.a.j.t.ThreadGroup: Starting thread group... number=1 threads=1 ramp-up=1 delayedStart=false +2022-11-03 09:23:58,397 INFO o.a.j.t.ThreadGroup: Started thread group number 1 +2022-11-03 09:23:58,397 INFO o.a.j.e.StandardJMeterEngine: All thread groups have been started +2022-11-03 09:23:58,398 INFO o.a.j.t.JMeterThread: Thread started: Thread Group 1-1 +2022-11-03 09:23:58,406 INFO o.a.j.p.h.s.HTTPHCAbstractImpl: Local host = 192.168.1.7 +2022-11-03 09:23:58,409 INFO o.a.j.p.h.s.HTTPHC4Impl: HTTP request retry count = 0 +2022-11-03 09:23:58,410 INFO o.a.j.s.SampleResult: Note: Sample TimeStamps are START times +2022-11-03 09:23:58,410 INFO o.a.j.s.SampleResult: sampleresult.default.encoding is set to ISO-8859-1 +2022-11-03 09:23:58,410 INFO o.a.j.s.SampleResult: sampleresult.useNanoTime=true +2022-11-03 09:23:58,410 INFO o.a.j.s.SampleResult: sampleresult.nanoThreadSleep=5000 +2022-11-03 09:23:58,456 INFO o.a.j.p.h.s.h.LazyLayeredConnectionSocketFactory: Setting up HTTPS TrustAll Socket Factory +2022-11-03 09:23:58,459 INFO o.a.j.u.JsseSSLManager: Using default SSL protocol: TLS +2022-11-03 09:23:58,459 INFO o.a.j.u.JsseSSLManager: SSL session context: per-thread +2022-11-03 09:23:58,538 INFO o.a.j.u.SSLManager: JmeterKeyStore Location: type JKS +2022-11-03 09:23:58,540 INFO o.a.j.u.SSLManager: KeyStore created OK +2022-11-03 09:23:58,540 WARN o.a.j.u.SSLManager: Keystore file not found, loading empty keystore +2022-11-03 09:23:58,813 INFO o.a.j.t.JMeterThread: Thread is done: Thread Group 1-1 +2022-11-03 09:23:58,813 INFO o.a.j.t.JMeterThread: Thread finished: Thread Group 1-1 +2022-11-03 09:23:58,815 INFO o.a.j.e.StandardJMeterEngine: Notifying test listeners of end of test +2022-11-03 09:23:58,816 INFO o.a.j.r.Summariser: summary = 1 in 00:00:00 = 2.3/s Avg: 362 Min: 362 Max: 362 Err: 0 (0.00%) diff --git a/contrib/executor/jmeterd/examples/kubeshop.jmx b/contrib/executor/jmeterd/examples/kubeshop.jmx new file mode 100644 index 00000000000..11e991a820f --- /dev/null +++ b/contrib/executor/jmeterd/examples/kubeshop.jmx @@ -0,0 +1,76 @@ + + + + + Kubeshop site simple perf test + false + true + false + + + + PATH + /pricing + = + + + + + + + + continue + + false + 1 + + 1 + 1 + false + + + true + + + + + + + false + $PATH + = + true + PATH + + + + testkube.io + 80 + https + + https://testkube.io + GET + true + false + true + false + + + + + + + + Testkube + + + Assertion.response_data + false + 16 + + + + + + + diff --git a/contrib/executor/jmeterd/examples/kubeshop_failed.jmx b/contrib/executor/jmeterd/examples/kubeshop_failed.jmx new file mode 100644 index 00000000000..908b3ebe828 --- /dev/null +++ b/contrib/executor/jmeterd/examples/kubeshop_failed.jmx @@ -0,0 +1,76 @@ + + + + + Kubeshop site simple perf test + false + true + false + + + + PATH + /pricing + = + + + + + + + + continue + + false + 1 + + 1 + 1 + false + + + true + + + + + + + false + $PATH + = + true + PATH + + + + testkube.io + 80 + https + + https://testkube.io + GET + true + false + true + false + + + + + + + + SOME_NONExisting_String + + + Assertion.response_data + false + 16 + + + + + + + diff --git a/contrib/executor/jmeterd/examples/results.jtl b/contrib/executor/jmeterd/examples/results.jtl new file mode 100644 index 00000000000..4a595e7d018 --- /dev/null +++ b/contrib/executor/jmeterd/examples/results.jtl @@ -0,0 +1,4 @@ +timeStamp,elapsed,label,responseCode,responseMessage,threadName,dataType,success,failureMessage,bytes,sentBytes,grpThreads,allThreads,URL,Latency,IdleTime,Connect +1667463814102,382,HTTP Request,200,OK,Thread Group 1-1,text,true,,66428,109,1,1,https://testkube.io,326,0,235 +1667463836936,365,HTTP Request,200,OK,Thread Group 1-1,text,true,,66428,109,1,1,https://testkube.io,309,0,222 +1667463838447,362,HTTP Request,200,OK,Thread Group 1-1,text,true,,66428,109,1,1,https://testkube.io,309,0,219 diff --git a/contrib/executor/jmeterd/pkg/README.md b/contrib/executor/jmeterd/pkg/README.md new file mode 100644 index 00000000000..633b8777e6e --- /dev/null +++ b/contrib/executor/jmeterd/pkg/README.md @@ -0,0 +1,59 @@ +# `/pkg` + +Library code that's ok to use by external applications (e.g., `/pkg/mypubliclib`). Other projects will import these libraries expecting them to work, so think twice before you put something here :-) Note that the `internal` directory is a better way to ensure your private packages are not importable because it's enforced by Go. The `/pkg` directory is still a good way to explicitly communicate that the code in that directory is safe for use by others. The [`I'll take pkg over internal`](https://travisjeffery.com/b/2019/11/i-ll-take-pkg-over-internal/) blog post by Travis Jeffery provides a good overview of the `pkg` and `internal` directories and when it might make sense to use them. + +It's also a way to group Go code in one place when your root directory contains lots of non-Go components and directories making it easier to run various Go tools (as mentioned in these talks: [`Best Practices for Industrial Programming`](https://www.youtube.com/watch?v=PTE4VJIdHPg) from GopherCon EU 2018, [GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps](https://www.youtube.com/watch?v=oL6JBUk6tj0) and [GoLab 2018 - Massimiliano Pippi - Project layout patterns in Go](https://www.youtube.com/watch?v=3gQa1LWwuzk)). + +Note that this is not a universally accepted pattern and for every popular repo that uses it you can find 10 that don't. It's up to you to decide if you want to use this pattern or not. Regardless of whether or not it's a good pattern more people will know what you mean than not. It might a bit confusing for some of the new Go devs, but it's a pretty simple confusion to resolve and that's one of the goals for this project layout repo. + +Ok not to use it if your app project is really small and where an extra level of nesting doesn't add much value (unless you really want to). Think about it when it's getting big enough and your root directory gets pretty busy (especially if you have a lot of non-Go app components). + +The `pkg` directory origins: The old Go source code used to use `pkg` for its packages and then various Go projects in the community started copying the pattern (see [`this`](https://twitter.com/bradfitz/status/1039512487538970624) Brad Fitzpatrick's tweet for more context). + + +Examples: + +* https://github.com/prometheus/prometheus/tree/master/pkg +* https://github.com/jaegertracing/jaeger/tree/master/pkg +* https://github.com/istio/istio/tree/master/pkg +* https://github.com/GoogleContainerTools/kaniko/tree/master/pkg +* https://github.com/google/gvisor/tree/master/pkg +* https://github.com/google/syzkaller/tree/master/pkg +* https://github.com/perkeep/perkeep/tree/master/pkg +* https://github.com/minio/minio/tree/master/pkg +* https://github.com/heptio/ark/tree/master/pkg +* https://github.com/argoproj/argo/tree/master/pkg +* https://github.com/heptio/sonobuoy/tree/master/pkg +* https://github.com/helm/helm/tree/master/pkg +* https://github.com/kubernetes/kubernetes/tree/master/pkg +* https://github.com/kubernetes/kops/tree/master/pkg +* https://github.com/moby/moby/tree/master/pkg +* https://github.com/grafana/grafana/tree/master/pkg +* https://github.com/influxdata/influxdb/tree/master/pkg +* https://github.com/cockroachdb/cockroach/tree/master/pkg +* https://github.com/derekparker/delve/tree/master/pkg +* https://github.com/etcd-io/etcd/tree/master/pkg +* https://github.com/oklog/oklog/tree/master/pkg +* https://github.com/flynn/flynn/tree/master/pkg +* https://github.com/jesseduffield/lazygit/tree/master/pkg +* https://github.com/gopasspw/gopass/tree/master/pkg +* https://github.com/sosedoff/pgweb/tree/master/pkg +* https://github.com/GoogleContainerTools/skaffold/tree/master/pkg +* https://github.com/knative/serving/tree/master/pkg +* https://github.com/grafana/loki/tree/master/pkg +* https://github.com/bloomberg/goldpinger/tree/master/pkg +* https://github.com/Ne0nd0g/merlin/tree/master/pkg +* https://github.com/jenkins-x/jx/tree/master/pkg +* https://github.com/DataDog/datadog-agent/tree/master/pkg +* https://github.com/dapr/dapr/tree/master/pkg +* https://github.com/cortexproject/cortex/tree/master/pkg +* https://github.com/dexidp/dex/tree/master/pkg +* https://github.com/pusher/oauth2_proxy/tree/master/pkg +* https://github.com/pdfcpu/pdfcpu/tree/master/pkg +* https://github.com/weaveworks/kured/tree/master/pkg +* https://github.com/weaveworks/footloose/tree/master/pkg +* https://github.com/weaveworks/ignite/tree/master/pkg +* https://github.com/tmrts/boilr/tree/master/pkg +* https://github.com/kata-containers/runtime/tree/master/pkg +* https://github.com/okteto/okteto/tree/master/pkg +* https://github.com/solo-io/squash/tree/master/pkg diff --git a/contrib/executor/jmeterd/pkg/jmeterenv/env.go b/contrib/executor/jmeterd/pkg/jmeterenv/env.go new file mode 100644 index 00000000000..fb968ab1b38 --- /dev/null +++ b/contrib/executor/jmeterd/pkg/jmeterenv/env.go @@ -0,0 +1,37 @@ +package jmeterenv + +import ( + "strings" + + "github.com/kubeshop/testkube/pkg/api/v1/testkube" +) + +const ( + MasterOverrideJvmArgs = "MASTER_OVERRIDE_JVM_ARGS" + MasterAdditionalJvmArgs = "MASTER_ADDITIONAL_JVM_ARGS" + SlavesOverrideJvmArgs = "SLAVES_OVERRIDE_JVM_ARGS" + SlavesAdditionalJvmArgs = "SLAVES_ADDITIONAL_JVM_ARGS" + SlavesAdditionalJmeterArgs = "SLAVES_ADDITIONAL_JMETER_ARGS" + SlavesCount = "SLAVES_COUNT" + MasterPrefix = "MASTER_" + SlavesPrefix = "SLAVES_" +) + +// ExtractSlaveEnvVariables removes slave environment variables from the given map and returns them separately. +func ExtractSlaveEnvVariables(variables map[string]testkube.Variable) map[string]testkube.Variable { + slaveVariables := make(map[string]testkube.Variable) + + // Iterate through the variables to extract slave environment variables. + for k, v := range variables { + switch { + case strings.HasPrefix(k, SlavesPrefix): + slaveVariables[k] = v + delete(variables, k) // Remove slave variable from the main variables map. + case strings.HasPrefix(k, MasterPrefix): + continue + default: + slaveVariables[k] = v + } + } + return slaveVariables +} diff --git a/contrib/executor/jmeterd/pkg/runner/mapper.go b/contrib/executor/jmeterd/pkg/runner/mapper.go new file mode 100644 index 00000000000..078cf287113 --- /dev/null +++ b/contrib/executor/jmeterd/pkg/runner/mapper.go @@ -0,0 +1,82 @@ +package runner + +import ( + "fmt" + + "github.com/kubeshop/testkube/contrib/executor/jmeter/pkg/parser" + "github.com/kubeshop/testkube/pkg/api/v1/testkube" +) + +func mapResultsToExecutionResults(out []byte, results parser.Results) (result testkube.ExecutionResult) { + result.Status = testkube.ExecutionStatusPassed + if results.HasError { + result.Status = testkube.ExecutionStatusFailed + result.ErrorMessage = results.LastErrorMessage + } + + result.Output = string(out) + result.OutputType = "text/plain" + + for _, r := range results.Results { + result.Steps = append( + result.Steps, + testkube.ExecutionStepResult{ + Name: r.Label, + Duration: r.Duration.String(), + Status: mapResultStatus(r), + AssertionResults: []testkube.AssertionResult{{ + Name: r.Label, + Status: mapResultStatus(r), + }}, + }) + } + + return result +} + +func mapTestResultsToExecutionResults(out []byte, results parser.TestResults) (result testkube.ExecutionResult) { + result.Status = testkube.ExecutionStatusPassed + + result.Output = string(out) + result.OutputType = "text/plain" + + samples := append(results.HTTPSamples, results.Samples...) + for _, r := range samples { + if !r.Success { + result.Status = testkube.ExecutionStatusFailed + if r.AssertionResult != nil { + result.ErrorMessage = r.AssertionResult.FailureMessage + } + } + + result.Steps = append( + result.Steps, + testkube.ExecutionStepResult{ + Name: r.Label, + Duration: fmt.Sprintf("%dms", r.Time), + Status: mapTestResultStatus(r.Success), + AssertionResults: []testkube.AssertionResult{{ + Name: r.Label, + Status: mapTestResultStatus(r.Success), + }}, + }) + } + + return result +} + +func mapResultStatus(result parser.Result) string { + if result.Success { + return string(testkube.PASSED_ExecutionStatus) + } + + return string(testkube.FAILED_ExecutionStatus) +} + +func mapTestResultStatus(success bool) string { + if success { + return string(testkube.PASSED_ExecutionStatus) + } + + return string(testkube.FAILED_ExecutionStatus) +} diff --git a/contrib/executor/jmeterd/pkg/runner/runner.go b/contrib/executor/jmeterd/pkg/runner/runner.go new file mode 100644 index 00000000000..02033c67261 --- /dev/null +++ b/contrib/executor/jmeterd/pkg/runner/runner.go @@ -0,0 +1,265 @@ +package runner + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + + "github.com/kubeshop/testkube/contrib/executor/jmeter/pkg/parser" + "github.com/kubeshop/testkube/contrib/executor/jmeterd/pkg/jmeterenv" + "github.com/kubeshop/testkube/contrib/executor/jmeterd/pkg/slaves" + "github.com/kubeshop/testkube/pkg/api/v1/testkube" + "github.com/kubeshop/testkube/pkg/envs" + "github.com/kubeshop/testkube/pkg/executor" + "github.com/kubeshop/testkube/pkg/executor/content" + "github.com/kubeshop/testkube/pkg/executor/env" + "github.com/kubeshop/testkube/pkg/executor/output" + "github.com/kubeshop/testkube/pkg/executor/runner" + "github.com/kubeshop/testkube/pkg/executor/scraper" + "github.com/kubeshop/testkube/pkg/executor/scraper/factory" + "github.com/kubeshop/testkube/pkg/ui" +) + +func NewRunner(ctx context.Context, params envs.Params) (*JMeterDRunner, error) { + output.PrintLog(fmt.Sprintf("%s Preparing test runner", ui.IconTruck)) + + var err error + r := &JMeterDRunner{ + Params: params, + } + + r.Scraper, err = factory.TryGetScrapper(ctx, params) + if err != nil { + return nil, err + } + + return r, nil +} + +// JMeterDRunner runner +type JMeterDRunner struct { + Params envs.Params + Scraper scraper.Scraper +} + +var _ runner.Runner = &JMeterDRunner{} + +func (r *JMeterDRunner) Run(ctx context.Context, execution testkube.Execution) (result testkube.ExecutionResult, err error) { + if r.Scraper != nil { + defer r.Scraper.Close() + } + output.PrintEvent( + fmt.Sprintf("%s Running with config", ui.IconTruck), + "scraperEnabled", r.Params.ScrapperEnabled, + "dataDir", r.Params.DataDir, + "SSL", r.Params.Ssl, + "endpoint", r.Params.Endpoint, + ) + + envManager := env.NewManagerWithVars(execution.Variables) + envManager.GetReferenceVars(envManager.Variables) + + path, workingDir, err := content.GetPathAndWorkingDir(execution.Content, r.Params.DataDir) + if err != nil { + output.PrintLogf("%s Failed to resolve absolute directory for %s, using the path directly", ui.IconWarning, r.Params.DataDir) + } + + fileInfo, err := os.Stat(path) + if err != nil { + return result, err + } + + if fileInfo.IsDir() { + scriptName := execution.Args[len(execution.Args)-1] + if workingDir != "" { + path = filepath.Join(r.Params.DataDir, "repo") + if execution.Content != nil && execution.Content.Repository != nil { + scriptName = filepath.Join(execution.Content.Repository.Path, scriptName) + } + } + + output.PrintLogf("execution arg before %s", execution.Args) + execution.Args = execution.Args[:len(execution.Args)-1] + output.PrintLogf("execution arg afrer %s", execution.Args) + output.PrintLogf("%s It is a directory test - trying to find file from the last executor argument %s in directory %s", ui.IconWorld, scriptName, path) + + // sanity checking for test script + scriptFile := filepath.Join(path, workingDir, scriptName) + fileInfo, errFile := os.Stat(scriptFile) + if errors.Is(errFile, os.ErrNotExist) || fileInfo.IsDir() { + output.PrintLogf("%s Could not find file %s in the directory, error: %s", ui.IconCross, scriptName, errFile) + return *result.Err(errors.Errorf("could not find file %s in the directory: %v", scriptName, errFile)), nil + } + path = scriptFile + } + + slavesEnvVariables := jmeterenv.ExtractSlaveEnvVariables(envManager.Variables) + // compose parameters passed to JMeter with -J + params := make([]string, 0, len(envManager.Variables)) + for _, value := range envManager.Variables { + if value.Name == jmeterenv.MasterOverrideJvmArgs || value.Name == jmeterenv.MasterAdditionalJvmArgs { + //Skip JVM ARGS to be appended in the command + continue + } + params = append(params, fmt.Sprintf("-J%s=%s", value.Name, value.Value)) + + } + + runPath := r.Params.DataDir + if workingDir != "" { + runPath = workingDir + } + + parentTestFolder := filepath.Join(filepath.Dir(path)) + // Set env plugin env variable to set custom plugin directory + // with this path custom plugin will be copied to jmeter's plugin directory + err = os.Setenv("JMETER_PARENT_TEST_FOLDER", parentTestFolder) + if err != nil { + output.PrintLogf("%s Failed to set parent test folder directory %s", ui.IconWarning, parentTestFolder) + } + // Add user plugins folder in slaves env variables + slavesEnvVariables["JMETER_PARENT_TEST_FOLDER"] = testkube.NewBasicVariable("JMETER_PARENT_TEST_FOLDER", parentTestFolder) + + outputDir := filepath.Join(runPath, "output") + // clean output directory it already exists, only useful for local development + _, err = os.Stat(outputDir) + if err == nil { + if err = os.RemoveAll(outputDir); err != nil { + output.PrintLogf("%s Failed to clean output directory %s", ui.IconWarning, outputDir) + } + } + // recreate output directory with wide permissions so JMeter can create report files + if err = os.Mkdir(outputDir, 0777); err != nil { + return *result.Err(errors.Wrapf(err, "error creating directory %s", runPath)), nil + } + + jtlPath := filepath.Join(outputDir, "report.jtl") + reportPath := filepath.Join(outputDir, "report") + jmeterLogPath := filepath.Join(outputDir, "jmeter.log") + args := execution.Args + for i := range args { + if args[i] == "" { + args[i] = path + } + + if args[i] == "" { + args[i] = jtlPath + } + + if args[i] == "" { + args[i] = reportPath + } + + if args[i] == "" { + args[i] = jmeterLogPath + } + } + + slavesClient, err := slaves.NewClient(execution, r.Params, slavesEnvVariables) + if err != nil { + return *result.WithErrors(errors.Wrap(err, "error creating slaves client")), nil + } + + //creating slaves provided in SLAVES_COUNT env variable + slavesNameIpMap, err := slavesClient.CreateSlaves(ctx) + if err != nil { + return *result.WithErrors(errors.Wrap(err, "error creating slaves")), nil + } + defer slavesClient.DeleteSlaves(ctx, slavesNameIpMap) + + args = append(args, fmt.Sprintf("-R %v", slaves.GetSlavesIpString(slavesNameIpMap))) + + for i := range args { + if args[i] == "" { + newArgs := make([]string, len(args)+len(params)-1) + copy(newArgs, args[:i]) + copy(newArgs[i:], params) + copy(newArgs[i+len(params):], args[i+1:]) + args = newArgs + break + } + } + + output.PrintLogf("%s Using arguments: %v", ui.IconWorld, args) + + entryPoint := getEntryPoint() + for i := range execution.Command { + if execution.Command[i] == "" { + execution.Command[i] = entryPoint + } + } + + command, args := executor.MergeCommandAndArgs(execution.Command, args) + // run JMeter inside repo directory ignore execution error in case of failed test + output.PrintLogf("%s Test run command %s %s", ui.IconRocket, command, strings.Join(args, " ")) + out, err := executor.Run(runPath, command, envManager, args...) + if err != nil { + return *result.WithErrors(errors.Errorf("jmeter run error: %v", err)), nil + } + out = envManager.ObfuscateSecrets(out) + + output.PrintLogf("%s Getting report %s", ui.IconFile, jtlPath) + f, err := os.Open(jtlPath) + if err != nil { + return *result.WithErrors(errors.Errorf("getting jtl report error: %v", err)), nil + } + + results, err := parser.ParseCSV(f) + f.Close() + + var executionResult testkube.ExecutionResult + if err != nil { + data, err := os.ReadFile(jtlPath) + if err != nil { + return *result.WithErrors(errors.Errorf("getting jtl report error: %v", err)), nil + } + + testResults, err := parser.ParseXML(data) + if err != nil { + return *result.WithErrors(errors.Errorf("parsing jtl report error: %v", err)), nil + } + + executionResult = mapTestResultsToExecutionResults(out, testResults) + } else { + executionResult = mapResultsToExecutionResults(out, results) + } + + output.PrintLogf("%s Mapped JMeter results to Execution Results...", ui.IconCheckMark) + + // scrape artifacts first even if there are errors above + if r.Params.ScrapperEnabled { + directories := []string{ + outputDir, + } + if execution.ArtifactRequest != nil && len(execution.ArtifactRequest.Dirs) != 0 { + directories = append(directories, execution.ArtifactRequest.Dirs...) + } + + output.PrintLogf("Scraping directories: %v", directories) + if err := r.Scraper.Scrape(ctx, directories, execution); err != nil { + return *executionResult.Err(err), errors.Wrap(err, "error scraping artifacts for JMeter executor") + } + } + + return executionResult, nil +} + +func getEntryPoint() (entrypoint string) { + if entrypoint = os.Getenv("ENTRYPOINT_CMD"); entrypoint != "" { + return entrypoint + } + wd, err := os.Getwd() + if err != nil { + wd = "." + } + return filepath.Join(wd, "scripts/entrypoint.sh") +} + +// GetType returns runner type +func (r *JMeterDRunner) GetType() runner.Type { + return runner.TypeMain +} diff --git a/contrib/executor/jmeterd/pkg/slaves/client.go b/contrib/executor/jmeterd/pkg/slaves/client.go new file mode 100644 index 00000000000..2e064204362 --- /dev/null +++ b/contrib/executor/jmeterd/pkg/slaves/client.go @@ -0,0 +1,223 @@ +package slaves + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + + "github.com/kubeshop/testkube/contrib/executor/jmeterd/pkg/jmeterenv" + "github.com/kubeshop/testkube/pkg/api/v1/testkube" + "github.com/kubeshop/testkube/pkg/envs" + "github.com/kubeshop/testkube/pkg/executor/output" + "github.com/kubeshop/testkube/pkg/k8sclient" +) + +const ( + podsTimeout = 5 * time.Minute +) + +type Interface interface { + CreateSlaves(ctx context.Context) error + DeleteSlaves(ctx context.Context, slaveNameIpMap map[string]string) error +} + +type Client struct { + clientSet *kubernetes.Clientset + namespace string + execution testkube.Execution + envParams envs.Params + envVariables map[string]testkube.Variable +} + +// NewClient is a method to create new slave client +func NewClient(execution testkube.Execution, envParams envs.Params, slavesEnvVariables map[string]testkube.Variable) (*Client, error) { + clientSet, err := k8sclient.ConnectToK8s() + if err != nil { + return nil, err + } + + return &Client{ + clientSet: clientSet, + namespace: execution.TestNamespace, + execution: execution, + envParams: envParams, + envVariables: slavesEnvVariables, + }, nil +} + +// CreateSlaves creates slaves as per count provided in the SLAVES_CLOUNT env variable. +// Default SLAVES_COUNT would be 1 if not provided in the env variables +func (client *Client) CreateSlaves(ctx context.Context) (map[string]string, error) { + slavesCount, err := getSlavesCount(client.envVariables[jmeterenv.SlavesCount]) + if err != nil { + return nil, errors.Errorf("Getting error while fetching slaves count from env variable SLAVES_COUNT : %v", err) + } + + output.PrintLogf("Creating Slaves %v Pods", slavesCount) + + podIPAddressChan := make(chan map[string]string, slavesCount) + errorChan := make(chan error, slavesCount) + podIPAddresses := make(map[string]string) + + for i := 1; i <= slavesCount; i++ { + go client.createSlavePod(ctx, i, podIPAddressChan, errorChan) + } + + for i := 0; i < slavesCount; i++ { + select { + case ipAddress := <-podIPAddressChan: + for podName, podIp := range ipAddress { + podIPAddresses[podName] = podIp + } + case err := <-errorChan: + if err != nil { + return nil, err + } + } + } + + return podIPAddresses, nil +} + +// createSlavePod creates a slave pod and sends its IP address on the podIPAddressChan +// channel when the pod is in the ready state. +func (client *Client) createSlavePod(ctx context.Context, currentSlavesCount int, podIPAddressChan chan<- map[string]string, errorChan chan<- error) { + slavePod, err := client.getSlavePodConfiguration(currentSlavesCount) + if err != nil { + errorChan <- err + return + } + + p, err := client.clientSet.CoreV1().Pods(client.namespace).Create(ctx, slavePod, metav1.CreateOptions{}) + if err != nil { + errorChan <- err + return + } + + // Wait for the pod to become ready + conditionFunc := isPodReady(ctx, client.clientSet, p.Name, client.namespace) + + err = wait.PollImmediate(time.Second, podsTimeout, conditionFunc) + if err != nil { + errorChan <- err + return + } + + p, err = client.clientSet.CoreV1().Pods(client.namespace).Get(ctx, p.Name, metav1.GetOptions{}) + if err != nil { + errorChan <- err + return + } + podNameIpMap := map[string]string{ + p.Name: p.Status.PodIP, + } + podIPAddressChan <- podNameIpMap +} + +func (client *Client) getSlavePodConfiguration(currentSlavesCount int) (*v1.Pod, error) { + runnerExecutionStr, err := json.Marshal(client.execution) + if err != nil { + return nil, err + } + + podName := ValidateAndGetSlavePodName(client.execution.TestName, client.execution.Id, currentSlavesCount) + if err != nil { + return nil, err + } + return &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + }, + Spec: v1.PodSpec{ + RestartPolicy: v1.RestartPolicyAlways, + InitContainers: []v1.Container{ + { + Name: "init", + Image: "kubeshop/testkube-init-executor:1.14.3", + Command: []string{"/bin/runner", string(runnerExecutionStr)}, + Env: getSlaveRunnerEnv(client.envParams, client.execution), + ImagePullPolicy: v1.PullIfNotPresent, + VolumeMounts: []v1.VolumeMount{ + { + MountPath: "/data", + Name: "data-volume", + }, + }, + }, + }, + Containers: []v1.Container{ + { + Name: "main", + Image: "kubeshop/testkube-jmeterd-slaves:999.0.0", + Env: getSlaveConfigurationEnv(client.envVariables), + ImagePullPolicy: v1.PullIfNotPresent, + Ports: []v1.ContainerPort{ + { + ContainerPort: serverPort, + Name: "server-port", + }, { + ContainerPort: localPort, + Name: "local-port", + }, + }, + VolumeMounts: []v1.VolumeMount{ + { + MountPath: "/data", + Name: "data-volume", + }, + }, + LivenessProbe: &v1.Probe{ + ProbeHandler: v1.ProbeHandler{ + TCPSocket: &v1.TCPSocketAction{ + Port: intstr.FromInt(serverPort), + }, + }, + FailureThreshold: 3, + PeriodSeconds: 5, + SuccessThreshold: 1, + TimeoutSeconds: 1, + }, + ReadinessProbe: &v1.Probe{ + ProbeHandler: v1.ProbeHandler{ + TCPSocket: &v1.TCPSocketAction{ + Port: intstr.FromInt(serverPort), + }, + }, + FailureThreshold: 3, + InitialDelaySeconds: 10, + PeriodSeconds: 5, + TimeoutSeconds: 1, + }, + }, + }, + Volumes: []v1.Volume{ + { + Name: "data-volume", + VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}, + }, + }, + }, + }, nil +} + +// DeleteSlaves do the cleanup slaves pods after execution of test +func (client *Client) DeleteSlaves(ctx context.Context, slaveNameIpMap map[string]string) error { + for slaveName := range slaveNameIpMap { + output.PrintLog(fmt.Sprintf("Deleting slave %v", slaveName)) + err := client.clientSet.CoreV1().Pods(client.namespace).Delete(ctx, slaveName, metav1.DeleteOptions{}) + if err != nil { + output.PrintLogf("Error deleting slave pods %v", err.Error()) + return err + } + + } + return nil +} diff --git a/contrib/executor/jmeterd/pkg/slaves/utils.go b/contrib/executor/jmeterd/pkg/slaves/utils.go new file mode 100644 index 00000000000..d349d3176d7 --- /dev/null +++ b/contrib/executor/jmeterd/pkg/slaves/utils.go @@ -0,0 +1,153 @@ +package slaves + +import ( + "context" + "fmt" + "strconv" + "strings" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + + "github.com/kubeshop/testkube/pkg/api/v1/testkube" + "github.com/kubeshop/testkube/pkg/envs" + "github.com/kubeshop/testkube/pkg/executor/output" +) + +const ( + defaultSlavesCount = 1 + serverPort = 1099 + localPort = 60001 +) + +func getSlaveRunnerEnv(envParams envs.Params, runnerExecution testkube.Execution) []v1.EnvVar { + + gitEnvs := []v1.EnvVar{} + if runnerExecution.Content.Type_ == "git" && runnerExecution.Content.Repository.UsernameSecret != nil && runnerExecution.Content.Repository.TokenSecret != nil { + gitEnvs = append(gitEnvs, v1.EnvVar{ + + Name: "RUNNER_GITUSERNAME", + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: runnerExecution.Content.Repository.UsernameSecret.Name, + }, + Key: runnerExecution.Content.Repository.UsernameSecret.Key, + }, + }, + }, v1.EnvVar{ + Name: "RUNNER_GITTOKEN", + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: runnerExecution.Content.Repository.TokenSecret.Name, + }, + Key: runnerExecution.Content.Repository.TokenSecret.Key, + }, + }, + }, + ) + } + + return append([]v1.EnvVar{ + { + Name: "RUNNER_ENDPOINT", + Value: envParams.Endpoint, + }, { + Name: "RUNNER_ACCESSKEYID", + Value: envParams.AccessKeyID, + }, { + Name: "RUNNER_SECRETACCESSKEY", + Value: envParams.SecretAccessKey, + }, { + Name: "RUNNER_TOKEN", + Value: envParams.Token, + }, { + Name: "RUNNER_BUCKET", + Value: envParams.Bucket, + }, { + Name: "RUNNER_SSL", + Value: fmt.Sprintf("%v", envParams.Ssl), + }, { + Name: "RUNNER_SCRAPPERENABLED", + Value: fmt.Sprintf("%v", envParams.ScrapperEnabled), + }, { + Name: "RUNNER_DATADIR", + Value: envParams.DataDir, + }, { + Name: "RUNNER_CLOUD_MODE", + Value: fmt.Sprintf("%v", envParams.CloudMode), + }, { + Name: "RUNNER_CLOUD_API_KEY", + Value: envParams.CloudAPIKey, + }, { + Name: "RUNNER_CLOUD_API_TLS_INSECURE", + Value: fmt.Sprintf("%v", envParams.CloudAPITLSInsecure), + }, { + Name: "RUNNER_CLOUD_API_URL", + Value: envParams.CloudAPIURL, + }, + }, gitEnvs...) +} + +func getSlaveConfigurationEnv(slaveEnv map[string]testkube.Variable) []v1.EnvVar { + envVars := []v1.EnvVar{} + for envKey, t := range slaveEnv { + envVars = append(envVars, v1.EnvVar{Name: envKey, Value: t.Value}) + } + return envVars +} + +func isPodReady(ctx context.Context, c kubernetes.Interface, podName, namespace string) wait.ConditionFunc { + return func() (bool, error) { + pod, err := c.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) + if err != nil { + return false, err + } + + for _, condition := range pod.Status.Conditions { + isReadyType := condition.Type == v1.PodReady + isConditionTrue := condition.Status == v1.ConditionTrue + isRunningPhase := pod.Status.Phase == v1.PodRunning + ipNotEmpty := pod.Status.PodIP != "" + if isReadyType && isConditionTrue && isRunningPhase && ipNotEmpty { + return true, nil + } + } + return false, nil + } +} + +func getSlavesCount(count testkube.Variable) (int, error) { + if count.Value == "" { + output.PrintLogf("Slaves count not provided in the SLAVES_COUNT env variable. Defaulting to %v slaves", defaultSlavesCount) + return defaultSlavesCount, nil + } + + rplicaCount, err := strconv.Atoi(count.Value) + if err != nil { + return 0, err + } + return rplicaCount, err +} + +func GetSlavesIpString(podNameIpMap map[string]string) string { + podIps := []string{} + for _, ip := range podNameIpMap { + podIps = append(podIps, ip) + } + return strings.Join(podIps, ",") +} + +func ValidateAndGetSlavePodName(testName string, executionId string, currentSlaveCount int) string { + slavePodName := fmt.Sprintf("%s-slave-%v-%s", testName, currentSlaveCount, executionId) + if len(slavePodName) > 64 { + //Get first 20 chars from testName name if pod name > 64 + shortExecutionName := testName[:20] + slavePodName = fmt.Sprintf("%s-slave-%v-%s", shortExecutionName, currentSlaveCount, executionId) + } + return slavePodName + +} diff --git a/contrib/executor/jmeterd/scripts/entrypoint.sh b/contrib/executor/jmeterd/scripts/entrypoint.sh new file mode 100755 index 00000000000..25e844b147d --- /dev/null +++ b/contrib/executor/jmeterd/scripts/entrypoint.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +EXECUTOR_CUSTOM_PLUGINS_FOLDER="${RUNNER_DATADIR}/uploads/plugins" + +if [ -d $EXECUTOR_CUSTOM_PLUGINS_FOLDER ]; +then + echo "Copying custom plugins from ${EXECUTOR_CUSTOM_PLUGINS_FOLDER} to ${JMETER_HOME}/lib/ext" + for plugin in ${EXECUTOR_CUSTOM_PLUGINS_FOLDER}/*.jar; do + echo "Copying plugin: $plugin" + cp $plugin ${JMETER_HOME}/lib/ext + done; +else + echo "No custom plugins found at ${EXECUTOR_CUSTOM_PLUGINS_FOLDER}" +fi + +if [ -f "/executor_entrypoint_master.sh" ]; +then + echo "Executing custom entrypoint script at /entrypoint.sh" + /executor_entrypoint_master.sh $@ +else + echo "Executing JMeter command directly: jmeter $@" + jmeter $@ +fi + diff --git a/contrib/executor/jmeterd/scripts/jmeter-master.sh b/contrib/executor/jmeterd/scripts/jmeter-master.sh new file mode 100755 index 00000000000..640a69f5681 --- /dev/null +++ b/contrib/executor/jmeterd/scripts/jmeter-master.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +echo "********************************************************" +echo "* Installing JMeter Plugins *" +echo "********************************************************" +echo + + + +if [ -d $JMETER_CUSTOM_PLUGINS_FOLDER ] +then + echo "Installing custom plugins from ${JMETER_CUSTOM_PLUGINS_FOLDER}" + for plugin in ${JMETER_CUSTOM_PLUGINS_FOLDER}/*.jar; do + echo "Copying plugin $plugin to ${JMETER_HOME}/lib/ext/${plugin}" + cp $plugin ${JMETER_HOME}/lib/ext + done; +else + echo "No custom plugins found in ${JMETER_CUSTOM_PLUGINS_FOLDER}" +fi +echo + + + + +if [ -d ${JMETER_PARENT_TEST_FOLDER}/plugins ] +then + echo "Installing user plugins from ${JMETER_PARENT_TEST_FOLDER}/plugins" + for plugin in ${JMETER_PARENT_TEST_FOLDER}/plugins/*.jar; do + echo "Copying plugin $plugin to ${JMETER_HOME}/lib/ext/" + cp $plugin ${JMETER_HOME}/lib/ext + done; +else + echo "No user plugins provided as directory ${JMETER_PARENT_TEST_FOLDER}/plugins is not present" +fi +echo + +if [ -f ${JMETER_PARENT_TEST_FOLDER}/user.properties ] +then + echo "Copying user properties file from ${JMETER_PARENT_TEST_FOLDER}/user.properties" + cp ${JMETER_PARENT_TEST_FOLDER}/user.properties ${JMETER_HOME}/bin/ +else + echo "File user.properties not present in ${JMETER_PARENT_TEST_FOLDER}" +fi +echo + + +echo "********************************************************" +echo "* Initializing JMeter Master *" +echo "********************************************************" +echo + +freeMem=$(awk '/MemAvailable/ { print int($2/1024) }' /proc/meminfo) + +[[ -z ${JVM_XMN} ]] && JVM_XMN=$(($freeMem*2/10)) +[[ -z ${JVM_XMS} ]] && JVM_XMS=$(($freeMem*8/10)) +[[ -z ${JVM_XMX} ]] && JVM_XMX=$(($freeMem*8/10)) + +echo "Setting default JVM_ARGS=-Xmn${JVM_XMN}m -Xms${JVM_XMS}m -Xmx${JVM_XMX}m" +export JVM_ARGS="-Xmn${JVM_XMN}m -Xms${JVM_XMS}m -Xmx${JVM_XMX}m" + +if [ -n "$MASTER_OVERRIDE_JVM_ARGS" ]; then + echo "Overriding JVM_ARGS=${MASTER_OVERRIDE_JVM_ARGS}" + export JVM_ARGS="${MASTER_OVERRIDE_JVM_ARGS}" +fi + +if [ -n "$MASTER_ADDITIONAL_JVM_ARGS" ]; then + echo "Appending additional JVM args: ${MASTER_ADDITIONAL_JVM_ARGS}" + export JVM_ARGS="${JVM_ARGS} ${MASTER_ADDITIONAL_JVM_ARGS}" +fi + +echo "Available memory: ${freeMem} MB" +echo "Configured JVM_ARGS=${JVM_ARGS}" +echo + +echo "********************************************************" +echo "* Preparing JMeter Test Execution *" +echo "********************************************************" +echo + +# Keep entrypoint simple: we must pass the standard JMeter arguments +EXTRA_ARGS=-Dlog4j2.formatMsgNoLookups=true + + +echo "********************************************************" +echo "* Executing JMeter tests *" +echo "********************************************************" +echo + +if [ -z "$SSL_DISABLED" ]; then + SSL_DISABLED=true +fi + +CONN_ARGS="-Jserver.rmi.ssl.disable=${SSL_DISABLED}" +echo "Executing command: jmeter $@ ${CONN_ARGS} " +echo +echo "Started CMD" +jmeter $@ ${CONN_ARGS} + +echo "END Finished JMeter test on $(date) for test ${file}" +echo + +echo "********************************************************" +echo "* JMeter test executions finished *" +echo "********************************************************" +echo diff --git a/contrib/executor/jmeterd/scripts/jmeter-slaves.sh b/contrib/executor/jmeterd/scripts/jmeter-slaves.sh new file mode 100644 index 00000000000..ed2a7aa6087 --- /dev/null +++ b/contrib/executor/jmeterd/scripts/jmeter-slaves.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +echo "********************************************************" +echo "* Installing JMeter Plugins *" +echo "********************************************************" +echo + +if [ -d $JMETER_CUSTOM_PLUGINS_FOLDER ] +then + echo "Installing custom plugins from ${JMETER_CUSTOM_PLUGINS_FOLDER}" + for plugin in ${JMETER_CUSTOM_PLUGINS_FOLDER}/*.jar; do + echo "Copying plugin $plugin to ${JMETER_HOME}/lib/ext/${plugin}" + cp $plugin ${JMETER_HOME}/lib/ext + done; +else + echo "No custom plugins found in ${JMETER_CUSTOM_PLUGINS_FOLDER}" +fi +echo + +if [ -d ${JMETER_PARENT_TEST_FOLDER}/plugins ] +then + echo "Installing user plugins from ${JMETER_PARENT_TEST_FOLDER}/plugins" + for plugin in ${JMETER_PARENT_TEST_FOLDER}/plugins/*.jar; do + echo "Copying plugin $plugin to ${JMETER_HOME}/lib/ext/" + cp $plugin ${JMETER_HOME}/lib/ext + done; +else + echo "No user plugins provided as directory ${JMETER_PARENT_TEST_FOLDER}/plugins is not present" +fi +echo + +echo + +echo "********************************************************" +echo "* Initializing JMeter Master *" +echo "********************************************************" +echo + +freeMem=`awk '/MemAvailable/ { print int($2/1024) }' /proc/meminfo` + +[[ -z ${JVM_XMN} ]] && JVM_XMN=$(($freeMem/10*2)) +[[ -z ${JVM_XMS} ]] && JVM_XMS=$(($freeMem/10*8)) +[[ -z ${JVM_XMX} ]] && JVM_XMX=$(($freeMem/10*8)) + +echo "Setting default JVM_ARGS=-Xmn${JVM_XMN}m -Xms${JVM_XMS}m -Xmx${JVM_XMX}m" +export JVM_ARGS="-Xmn${JVM_XMN}m -Xms${JVM_XMS}m -Xmx${JVM_XMX}m" + +if [ -n "$OVERRIDE_JVM_ARGS" ]; then + echo "Overriding JVM_ARGS=${OVERRIDE_JVM_ARGS}" + export JVM_ARGS="${OVERRIDE_JVM_ARGS}" +fi + +if [ -n "$ADDITIONAL_JVM_ARGS" ]; then + echo "Appending additional JVM args: ${ADDITIONAL_JVM_ARGS}" + export JVM_ARGS="${JVM_ARGS} ${ADDITIONAL_JVM_ARGS}" +fi + +echo "Available memory: ${freeMem} MB" +echo "Configured JVM_ARGS=${JVM_ARGS}" +echo + +echo "********************************************************" +echo "* Starting JMeter Server *" +echo "********************************************************" +echo + +SERVER_ARGS="-Dserver.rmi.localport=60001 -Dserver_port=1099 -Jserver.rmi.ssl.disable=${SSL_DISABLED}" +echo "Running command: jmeter-server ${SERVER_ARGS} ${SLAVES_ADDITIONAL_JMETER_ARGS}" +echo + +jmeter-server ${SERVER_ARGS} ${SLAVES_ADDITIONAL_JMETER_ARGS} \ No newline at end of file