diff --git a/README.md b/README.md index 2bbd6a1..be57151 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,9 @@ The desired state of a distributed Gatling load testing is described through a K ## Features -- Allows Gatling load testing scenario, resources, Gatling configurations files to be added in 2 ways: +- Allows Gatling load testing scenario, resources, Gatling configurations files to be added in 3 ways: - Bundle them with Gatling runtime packages in a Gatling container + - Run the simulations through build tool plugin (e.g. `gradle gatlingRun`) in a Docker container - Add them as multi-line definition in Gatling CR - Scaling Gatling load testing - Horizontal scaling: number of pods running in parallel during a load testing can be configured diff --git a/api/v1alpha1/gatling_types.go b/api/v1alpha1/gatling_types.go index 9dcbfc1..13a6157 100644 --- a/api/v1alpha1/gatling_types.go +++ b/api/v1alpha1/gatling_types.go @@ -114,6 +114,11 @@ type TestScenarioSpec struct { // +kubebuilder:validation:Optional Parallelism int32 `json:"parallelism,omitempty"` + // (Optional) Gatling simulation format, supports `bundle` and `gradle`. Defaults to `bundle` + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Enum=bundle;gradle + SimulationsFormat string `json:"simulationsFormat,omitempty"` + // (Optional) Gatling Resources directory path where simulation files are stored. Defaults to `/opt/gatling/user-files/simulations` // +kubebuilder:validation:Optional SimulationsDirectoryPath string `json:"simulationsDirectoryPath,omitempty"` diff --git a/config/crd/bases/gatling-operator.tech.zozo.com_gatlings.yaml b/config/crd/bases/gatling-operator.tech.zozo.com_gatlings.yaml index 5201164..e72e55a 100644 --- a/config/crd/bases/gatling-operator.tech.zozo.com_gatlings.yaml +++ b/config/crd/bases/gatling-operator.tech.zozo.com_gatlings.yaml @@ -4096,6 +4096,13 @@ spec: description: (Optional) Gatling Resources directory path where simulation files are stored. Defaults to `/opt/gatling/user-files/simulations` type: string + simulationsFormat: + description: (Optional) Gatling simulation format, supports `bundle` + and `gradle`. Defaults to `bundle` + enum: + - bundle + - gradle + type: string startTime: description: (Optional) Test Start time. type: string diff --git a/controllers/gatling_controller.go b/controllers/gatling_controller.go index cb7c458..08c74cb 100644 --- a/controllers/gatling_controller.go +++ b/controllers/gatling_controller.go @@ -49,6 +49,7 @@ const ( maxJobRunWaitTimeInSeconds = 10800 // 10800 sec (3 hours) defaultGatlingImage = "ghcr.io/st-tech/gatling:latest" defaultRcloneImage = "rclone/rclone:latest" + defaultSimulationFormat = "bundle" defaultSimulationsDirectoryPath = "/opt/gatling/user-files/simulations" defaultResourcesDirectoryPath = "/opt/gatling/user-files/resources" defaultResultsDirectoryPath = "/opt/gatling/results" @@ -521,6 +522,7 @@ func (r *GatlingReconciler) newGatlingRunnerJobForCR(gatling *gatlingv1alpha1.Ga ) gatlingRunnerCommand := commands.GetGatlingRunnerCommand( + r.getSimulationFormat(gatling), r.getSimulationsDirectoryPath(gatling), r.getTempSimulationsDirectoryPath(gatling), r.getResourcesDirectoryPath(gatling), @@ -1072,6 +1074,14 @@ func (r *GatlingReconciler) getPodServiceAccountName(gatling *gatlingv1alpha1.Ga return serviceAccountName } +func (r *GatlingReconciler) getSimulationFormat(gatling *gatlingv1alpha1.Gatling) string { + format := defaultSimulationFormat + if &gatling.Spec.TestScenarioSpec != nil && gatling.Spec.TestScenarioSpec.SimulationsFormat != "" { + format = gatling.Spec.TestScenarioSpec.SimulationsFormat + } + return format +} + func (r *GatlingReconciler) getSimulationsDirectoryPath(gatling *gatlingv1alpha1.Gatling) string { path := defaultSimulationsDirectoryPath if &gatling.Spec.TestScenarioSpec != nil && gatling.Spec.TestScenarioSpec.SimulationsDirectoryPath != "" { diff --git a/docs/api.md b/docs/api.md index c511b5d..5bdeb50 100644 --- a/docs/api.md +++ b/docs/api.md @@ -24,7 +24,8 @@ _Appears in:_ | Field | Description | | --- | --- | -| `provider` _string_ | (Required) Provider specifies the cloud provider that will be used. Supported providers: `aws`, `gcp`, and `azure` | +| `provider` _string_ | (Required) Provider specifies the cloud provider that will be used. +Supported providers: `aws`, `gcp`, and `azure` | | `bucket` _string_ | (Required) Storage Bucket Name. | | `region` _string_ | (Optional) Region Name. | | `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. | @@ -82,7 +83,8 @@ _Appears in:_ | Field | Description | | --- | --- | -| `provider` _string_ | (Required) Provider specifies notification service provider. Supported providers: `slack` | +| `provider` _string_ | (Required) Provider specifies notification service provider. +Supported providers: `slack` | | `secretName` _string_ | (Required) The name of secret in which all key/value sets needed for the notification are stored. | @@ -149,6 +151,7 @@ _Appears in:_ | --- | --- | | `startTime` _string_ | (Optional) Test Start time. | | `parallelism` _integer_ | (Optional) Number of pods running at the same time. Defaults to `1` (Minimum `1`) | +| `simulationsFormat` _string_ | (Optional) Gatling simulation format, supports `bundle` and `gradle`. Defaults to `bundle` | | `simulationsDirectoryPath` _string_ | (Optional) Gatling Resources directory path where simulation files are stored. Defaults to `/opt/gatling/user-files/simulations` | | `resourcesDirectoryPath` _string_ | (Optional) Gatling Simulation directory path where resources are stored. Defaults to `/opt/gatling/user-files/resources` | | `resultsDirectoryPath` _string_ | (Optional) Gatling Results directory path where results are stored. Defaults to `/opt/gatling/results` | diff --git a/docs/user-guide.md b/docs/user-guide.md index dcf7c55..27ae18c 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -51,9 +51,10 @@ As described in Configuration Overview, there are 2 things that you need to cons For `Gatling docker image`, you can use default image `ghcr.io/st-tech/gatling:latest`, or you can create custom image to use. -For `Gatling load testing related files`, you have 3 options: +For `Gatling load testing related files`, you have 4 options: - Create custom image to bundle Gatling load testing files with Java runtime and Gatling standalone bundle package +- Create custom image to bundle all Gatling related files with a gradle gatling plugin - Add Gatling load testing files as multi-line definitions in `.spec.testScenatioSpec` part of `Gatling CR` - Set up persistent volume in `.persistentVolume` and `.persistentVolumeClaim` in `Gatling CR` and load test files from the persistent volume in Gatling load test files. @@ -179,6 +180,80 @@ spec: ...omit... ``` +### Create Custom Gatling Image with Gradle Gatling plugin + +Example project can be found [here](https://github.com/gatling/gatling-gradle-plugin-demo-kotlin). + +Let's start with cloning the repo: +```bash +git clone git@github.com:gatling/gatling-gradle-plugin-demo-kotlin.git +cd gatling-gradle-plugin-demo-kotlin +``` + +In order to run this example on Gatling Operator, we have to build a custom docker image with both gradle and gatling baked in. + +Let's create a `Dockerfile` in the root directory of the `gatling-gradle-plugin-demo-kotlin` project: +```bash +FROM azul/zulu-openjdk:21-latest + +# dependency versions +ENV GATLING_VERSION 3.10.5 +ENV GRADLE_VERSION 8.7 + +# install gatling +RUN mkdir /opt/gatling && \ + apt-get update && apt-get upgrade -y && apt-get install -y wget unzip && \ + mkdir -p /tmp/downloads && \ + wget -q -O /tmp/downloads/gatling-$GATLING_VERSION.zip \ + https://repo1.maven.org/maven2/io/gatling/highcharts/gatling-charts-highcharts-bundle/$GATLING_VERSION/gatling-charts-highcharts-bundle-$GATLING_VERSION-bundle.zip && \ + mkdir -p /tmp/archive && cd /tmp/archive && \ + unzip /tmp/downloads/gatling-$GATLING_VERSION.zip && \ + mv /tmp/archive/gatling-charts-highcharts-bundle-$GATLING_VERSION/* /opt/gatling/ && \ + rm -rf /opt/gatling/user-files/simulations/computerdatabase /tmp/* + +# install gradle +RUN mkdir /opt/gradle && \ + wget -q -O /tmp/gradle-$GRADLE_VERSION.zip https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip && \ + unzip -d /opt/gradle /tmp/gradle-$GRADLE_VERSION.zip && \ + rm -rf /tmp/* + +# change context to gatling directory +WORKDIR /opt/gatling + +# copy gradle files to gatling directory +COPY . . + +# set environment variables +ENV PATH /opt/gatling/bin:/opt/gradle/gradle-$GRADLE_VERSION/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +ENV GATLING_HOME /opt/gatling + +ENTRYPOINT ["gatling.sh"] +``` + +Now we have to build the custom image: +```bash +# Build Docker image +docker build -t /gatling: . +# Push the image to your container registry +docker push /gatling: +``` + +Finally, specify the image in `.spec.podSpec.gatlingImage` of Gatling CR and change the value of property `testScenarioSpec.simulationsFormat` to use it in your distributed load testing. + +```yaml +apiVersion: gatling-operator.tech.zozo.com/v1alpha1 +kind: Gatling +metadata: + name: gatling-gradle +spec: + podSpec: + serviceAccountName: "gatling-operator-worker" + gatlingImage: /gatling: + testScenarioSpec: + simulationsFormat: gradle +...omit... +``` + ### Add Gatling Load Testing Files in Gatling CR As explained previously, instead of bundling Gatling load testing files in the Gatling docker image, you can add them as multi-line definitions in `.spec.testScenatioSpec` of `Gatling CR`, based on which Gatling Controller automatically creates `ConfigMap` resources and injects Gatling runner Pod with the files. diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 89cf49c..aacf3c6 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -32,15 +32,18 @@ done } func GetGatlingRunnerCommand( - simulationsDirectoryPath string, tempSimulationsDirectoryPath string, resourcesDirectoryPath string, - resultsDirectoryPath string, startTime string, simulationClass string, generateLocalReport bool) string { + simulationsFormat string, simulationsDirectoryPath string, tempSimulationsDirectoryPath string, + resourcesDirectoryPath string, resultsDirectoryPath string, startTime string, simulationClass string, + generateLocalReport bool) string { template := ` +SIMULATIONS_FORMAT=%s SIMULATIONS_DIR_PATH=%s TEMP_SIMULATIONS_DIR_PATH=%s RESOURCES_DIR_PATH=%s RESULTS_DIR_PATH=%s START_TIME="%s" +SIMULATION_CLASS=%s RUN_STATUS_FILE="${RESULTS_DIR_PATH}/COMPLETED" if [ -z "${START_TIME}" ]; then START_TIME=$(date +"%%Y-%%m-%%d %%H:%%M:%%S" --utc) @@ -66,7 +69,12 @@ fi if [ ! -d ${RESULTS_DIR_PATH} ]; then mkdir -p ${RESULTS_DIR_PATH} fi -gatling.sh -sf ${SIMULATIONS_DIR_PATH} -s %s -rsf ${RESOURCES_DIR_PATH} -rf ${RESULTS_DIR_PATH} %s %s + +if [ ${SIMULATIONS_FORMAT} = "bundle" ]; then + gatling.sh -sf ${SIMULATIONS_DIR_PATH} -s ${SIMULATION_CLASS} -rsf ${RESOURCES_DIR_PATH} -rf ${RESULTS_DIR_PATH} %s %s +elif [ ${SIMULATIONS_FORMAT} = "gradle" ]; then + gradle -Dgatling.core.directory.results=${RESULTS_DIR_PATH} gatlingRun-${SIMULATION_CLASS} +fi GATLING_EXIT_STATUS=$? if [ $GATLING_EXIT_STATUS -ne 0 ]; then @@ -84,6 +92,7 @@ exit $GATLING_EXIT_STATUS runModeOptionLocal := "-rm local" return fmt.Sprintf(template, + simulationsFormat, simulationsDirectoryPath, tempSimulationsDirectoryPath, resourcesDirectoryPath, diff --git a/pkg/commands/commands_test.go b/pkg/commands/commands_test.go index 29e97b8..84b9104 100644 --- a/pkg/commands/commands_test.go +++ b/pkg/commands/commands_test.go @@ -31,6 +31,7 @@ done var _ = Describe("GetGatlingRunnerCommand", func() { var ( + simulationsFormat string simulationsDirectoryPath string tempSimulationsDirectoryPath string resourcesDirectoryPath string @@ -42,6 +43,7 @@ var _ = Describe("GetGatlingRunnerCommand", func() { ) BeforeEach(func() { + simulationsFormat = "bundle" simulationsDirectoryPath = "testSimulationDirectoryPath" tempSimulationsDirectoryPath = "testTempSimulationsDirectoryPath" resourcesDirectoryPath = "testResourcesDirectoryPath" @@ -53,11 +55,13 @@ var _ = Describe("GetGatlingRunnerCommand", func() { It("GetCommandsWithLocalReport", func() { generateLocalReport = true expectedValue = ` +SIMULATIONS_FORMAT=bundle SIMULATIONS_DIR_PATH=testSimulationDirectoryPath TEMP_SIMULATIONS_DIR_PATH=testTempSimulationsDirectoryPath RESOURCES_DIR_PATH=testResourcesDirectoryPath RESULTS_DIR_PATH=testResultsDirectoryPath START_TIME="2021-09-10 08:45:31" +SIMULATION_CLASS=testSimulationClass RUN_STATUS_FILE="${RESULTS_DIR_PATH}/COMPLETED" if [ -z "${START_TIME}" ]; then START_TIME=$(date +"%Y-%m-%d %H:%M:%S" --utc) @@ -83,25 +87,34 @@ fi if [ ! -d ${RESULTS_DIR_PATH} ]; then mkdir -p ${RESULTS_DIR_PATH} fi -gatling.sh -sf ${SIMULATIONS_DIR_PATH} -s testSimulationClass -rsf ${RESOURCES_DIR_PATH} -rf ${RESULTS_DIR_PATH} -if [ $? -ne 0 ]; then +if [ ${SIMULATIONS_FORMAT} = "bundle" ]; then + gatling.sh -sf ${SIMULATIONS_DIR_PATH} -s ${SIMULATION_CLASS} -rsf ${RESOURCES_DIR_PATH} -rf ${RESULTS_DIR_PATH} -rm local +elif [ ${SIMULATIONS_FORMAT} = "gradle" ]; then + gradle -Dgatling.core.directory.results=${RESULTS_DIR_PATH} gatlingRun-${SIMULATION_CLASS} +fi + +GATLING_EXIT_STATUS=$? +if [ $GATLING_EXIT_STATUS -ne 0 ]; then RUN_STATUS_FILE="${RESULTS_DIR_PATH}/FAILED" echo "gatling.sh has failed!" 1>&2 fi touch ${RUN_STATUS_FILE} +exit $GATLING_EXIT_STATUS ` - Expect(GetGatlingRunnerCommand(simulationsDirectoryPath, tempSimulationsDirectoryPath, resourcesDirectoryPath, resultsDirectoryPath, startTime, simulationClass, generateLocalReport)).To(Equal(expectedValue)) + Expect(GetGatlingRunnerCommand(simulationsFormat, simulationsDirectoryPath, tempSimulationsDirectoryPath, resourcesDirectoryPath, resultsDirectoryPath, startTime, simulationClass, generateLocalReport)).To(Equal(expectedValue)) }) It("GetCommandWithoutLocalReport", func() { generateLocalReport = false expectedValue = ` +SIMULATIONS_FORMAT=bundle SIMULATIONS_DIR_PATH=testSimulationDirectoryPath TEMP_SIMULATIONS_DIR_PATH=testTempSimulationsDirectoryPath RESOURCES_DIR_PATH=testResourcesDirectoryPath RESULTS_DIR_PATH=testResultsDirectoryPath START_TIME="2021-09-10 08:45:31" +SIMULATION_CLASS=testSimulationClass RUN_STATUS_FILE="${RESULTS_DIR_PATH}/COMPLETED" if [ -z "${START_TIME}" ]; then START_TIME=$(date +"%Y-%m-%d %H:%M:%S" --utc) @@ -127,15 +140,22 @@ fi if [ ! -d ${RESULTS_DIR_PATH} ]; then mkdir -p ${RESULTS_DIR_PATH} fi -gatling.sh -sf ${SIMULATIONS_DIR_PATH} -s testSimulationClass -rsf ${RESOURCES_DIR_PATH} -rf ${RESULTS_DIR_PATH} -nr -if [ $? -ne 0 ]; then +if [ ${SIMULATIONS_FORMAT} = "bundle" ]; then + gatling.sh -sf ${SIMULATIONS_DIR_PATH} -s ${SIMULATION_CLASS} -rsf ${RESOURCES_DIR_PATH} -rf ${RESULTS_DIR_PATH} -nr -rm local +elif [ ${SIMULATIONS_FORMAT} = "gradle" ]; then + gradle -Dgatling.core.directory.results=${RESULTS_DIR_PATH} gatlingRun-${SIMULATION_CLASS} +fi + +GATLING_EXIT_STATUS=$? +if [ $GATLING_EXIT_STATUS -ne 0 ]; then RUN_STATUS_FILE="${RESULTS_DIR_PATH}/FAILED" echo "gatling.sh has failed!" 1>&2 fi touch ${RUN_STATUS_FILE} +exit $GATLING_EXIT_STATUS ` - Expect(GetGatlingRunnerCommand(simulationsDirectoryPath, tempSimulationsDirectoryPath, resourcesDirectoryPath, resultsDirectoryPath, startTime, simulationClass, generateLocalReport)).To(Equal(expectedValue)) + Expect(GetGatlingRunnerCommand(simulationsFormat, simulationsDirectoryPath, tempSimulationsDirectoryPath, resourcesDirectoryPath, resultsDirectoryPath, startTime, simulationClass, generateLocalReport)).To(Equal(expectedValue)) }) }) @@ -160,9 +180,19 @@ var _ = Describe("GetGatlingTransferResultCommand", func() { expectedValue = ` RESULTS_DIR_PATH=testResultsDirectoryPath rclone config create s3 s3 env_auth=true region ap-northeast-1 -for source in $(find ${RESULTS_DIR_PATH} -type f -name *.log) -do - rclone copyto ${source} --s3-no-check-bucket --s3-env-auth testStoragePath/${HOSTNAME}.log +while true; do + if [ -f "${RESULTS_DIR_PATH}/FAILED" ]; then + echo "Skip transfering gatling results" + break + fi + if [ -f "${RESULTS_DIR_PATH}/COMPLETED" ]; then + for source in $(find ${RESULTS_DIR_PATH} -type f -name *.log) + do + rclone copyto ${source} --s3-no-check-bucket --s3-env-auth testStoragePath/${HOSTNAME}.log + done + break + fi + sleep 1; done ` }) @@ -178,10 +208,20 @@ done RESULTS_DIR_PATH=testResultsDirectoryPath # assumes gcs bucket using uniform bucket-level access control rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive -# assumes each pod only contain single gatling log file but use for loop to use find command result -for source in $(find ${RESULTS_DIR_PATH} -type f -name *.log) -do - rclone copyto ${source} testStoragePath/${HOSTNAME}.log +while true; do + if [ -f "${RESULTS_DIR_PATH}/FAILED" ]; then + echo "Skip transfering gatling results" + break + fi + if [ -f "${RESULTS_DIR_PATH}/COMPLETED" ]; then + # assumes each pod only contain single gatling log file but use for loop to use find command result + for source in $(find ${RESULTS_DIR_PATH} -type f -name *.log) + do + rclone copyto ${source} testStoragePath/${HOSTNAME}.log + done + break + fi + sleep 1; done ` }) diff --git a/pkg/commands/suite_test.go b/pkg/commands/suite_test.go new file mode 100644 index 0000000..b9e2a48 --- /dev/null +++ b/pkg/commands/suite_test.go @@ -0,0 +1,13 @@ +package commands + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestBucket(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Commands Suite") +}