<|start_filename|>controllers/cloudstorages/aws.go<|end_filename|>
package cloudstorages

import (
	"fmt"
)

type AWSCloudStorageProvider struct {
	providerName string
}

func (p *AWSCloudStorageProvider) GetName() string {
	return p.providerName
}

func (p *AWSCloudStorageProvider) GetCloudStoragePath(bucket string, gatlingName string, subDir string) string {
	// Format s3:<bucket>/<gatling-name>/<sub-dir>
	return fmt.Sprintf("s3:%s/%s/%s", bucket, gatlingName, subDir)
}

func (p *AWSCloudStorageProvider) GetCloudStorageReportURL(bucket string, gatlingName string, subDir string) string {
	// Format https://<bucket>.s3.amazonaws.com/<gatling-name>/<sub-dir>/index.html
	return fmt.Sprintf("https://%s.s3.amazonaws.com/%s/%s/index.html", bucket, gatlingName, subDir)
}

func (p *AWSCloudStorageProvider) GetGatlingTransferResultCommand(resultsDirectoryPath string, region string, storagePath string) string {
	template := `
RESULTS_DIR_PATH=%s
rclone config create s3 s3 env_auth=true region %s
for source in $(find ${RESULTS_DIR_PATH} -type f -name *.log)
do
	rclone copyto ${source} --s3-no-check-bucket --s3-env-auth %s/${HOSTNAME}.log
done
`
	return fmt.Sprintf(template, resultsDirectoryPath, region, storagePath)
}

func (p *AWSCloudStorageProvider) GetGatlingAggregateResultCommand(resultsDirectoryPath string, region string, storagePath string) string {
	template := `
GATLING_AGGREGATE_DIR=%s
rclone config create s3 s3 env_auth=true region %s
rclone copy --s3-no-check-bucket --s3-env-auth %s ${GATLING_AGGREGATE_DIR}
`
	return fmt.Sprintf(template, resultsDirectoryPath, region, storagePath)
}

func (p *AWSCloudStorageProvider) GetGatlingTransferReportCommand(resultsDirectoryPath string, region string, storagePath string) string {
	template := `
GATLING_AGGREGATE_DIR=%s
rclone config create s3 s3 env_auth=true region %s
rclone copy ${GATLING_AGGREGATE_DIR} --exclude "*.log" --s3-no-check-bucket --s3-env-auth %s
`
	return fmt.Sprintf(template, resultsDirectoryPath, region, storagePath)
}


<|start_filename|>controllers/cloudstorages/gcp.go<|end_filename|>
package cloudstorages

import "fmt"

type GCPCloudStorageProvider struct {
	providerName string
}

func (p *GCPCloudStorageProvider) GetName() string {
	return p.providerName
}

func (p *GCPCloudStorageProvider) GetCloudStoragePath(bucket string, gatlingName string, subDir string) string {
	return fmt.Sprintf("gs://%s/%s/%s", bucket, gatlingName, subDir)
}

func (p *GCPCloudStorageProvider) GetCloudStorageReportURL(bucket string, gatlingName string, subDir string) string {
	// Format http(s)://<bucket>.storage.googleapis.com/<gatling-name>/<sub-dir>/index.html
	// or http(s)://storage.googleapis.com/<bucket>/<gatling-name>/<sub-dir>/index.html
	return fmt.Sprintf("https://storage.googleapis.com/%s/%s/%s/index.html", bucket, gatlingName, subDir)
}

// param: region is not used in GCP GCS, thus set just dummy value
func (p *GCPCloudStorageProvider) GetGatlingTransferResultCommand(resultsDirectoryPath string, region string, storagePath string) string {
	template := `
RESULTS_DIR_PATH=%s
# 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} %s/${HOSTNAME}.log
done
`
	return fmt.Sprintf(template, resultsDirectoryPath, storagePath)
}

// param: region is not used in GCP GCS, thus set just dummy value
func (p *GCPCloudStorageProvider) GetGatlingAggregateResultCommand(resultsDirectoryPath string, region string, storagePath string) string {
	template := `
GATLING_AGGREGATE_DIR=%s
# assumes gcs bucket using uniform bucket-level access control
rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive
rclone copy %s ${GATLING_AGGREGATE_DIR}
`
	return fmt.Sprintf(template, resultsDirectoryPath, storagePath)
}

// param: region is not used in GCP GCS, thus set just dummy value
func (p *GCPCloudStorageProvider) GetGatlingTransferReportCommand(resultsDirectoryPath string, region string, storagePath string) string {
	template := `
GATLING_AGGREGATE_DIR=%s
# assumes gcs bucket using uniform bucket-level access control
rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive
rclone copy ${GATLING_AGGREGATE_DIR} --exclude "*.log" %s
`
	return fmt.Sprintf(template, resultsDirectoryPath, storagePath)
}


<|start_filename|>controllers/commands.go<|end_filename|>
package controllers

import (
	"fmt"

	cloudstorages "github.com/st-tech/gatling-operator/controllers/cloudstorages"
)

func getGatlingWaiterCommand(parallelism *int32, gatlingNamespace string, gatlingName string) string {
	template := `
PARALLELISM=%d
NAMESPACE=%s
JOB_NAME=%s
POD_NAME=$(cat /etc/pod-info/name)

kubectl label pods -n $NAMESPACE $POD_NAME gatling-waiter=initialized

while true; do
  READY_PODS=$(kubectl get pods -n $NAMESPACE --selector=job-name=$JOB_NAME-runner,gatling-waiter=initialized --no-headers | grep -c ".*");
  echo "$READY_PODS/$PARALLELISM pods are ready";
  if  [ $READY_PODS -eq $PARALLELISM ]; then
    break;
  fi;
  sleep 1;
done
`
	return fmt.Sprintf(template,
		*parallelism,
		gatlingNamespace,
		gatlingName,
	)
}

func getGatlingRunnerCommand(
	simulationsDirectoryPath string, tempSimulationsDirectoryPath string, resourcesDirectoryPath string,
	resultsDirectoryPath string, startTime string, simulationClass string, generateLocalReport bool) string {

	template := `
SIMULATIONS_DIR_PATH=%s
TEMP_SIMULATIONS_DIR_PATH=%s
RESOURCES_DIR_PATH=%s
RESULTS_DIR_PATH=%s
START_TIME="%s"
if [ -z "${START_TIME}" ]; then
  START_TIME=$(date +"%%Y-%%m-%%d %%H:%%M:%%S" --utc)
fi
start_time_stamp=$(date -d "${START_TIME}" +"%%s")
current_time_stamp=$(date +"%%s")
echo "Wait until ${START_TIME}"
until [ ${current_time_stamp} -ge ${start_time_stamp} ];
do
  current_time_stamp=$(date +"%%s")
  echo "it's ${current_time_stamp} now and waiting until ${start_time_stamp} ..."
  sleep 1;
done
if [ ! -d ${SIMULATIONS_DIR_PATH} ]; then
  mkdir -p ${SIMULATIONS_DIR_PATH}
fi
if [ -d ${TEMP_SIMULATIONS_DIR_PATH} ]; then
  cp -p ${TEMP_SIMULATIONS_DIR_PATH}/*.scala ${SIMULATIONS_DIR_PATH}
fi
if [ ! -d ${RESOURCES_DIR_PATH} ]; then
  mkdir -p ${RESOURCES_DIR_PATH}
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
`
	generateLocalReportOption := "-nr"
	if generateLocalReport {
		generateLocalReportOption = ""
	}

	return fmt.Sprintf(template,
		simulationsDirectoryPath,
		tempSimulationsDirectoryPath,
		resourcesDirectoryPath,
		resultsDirectoryPath,
		startTime,
		simulationClass,
		generateLocalReportOption)
}

func getGatlingTransferResultCommand(resultsDirectoryPath string, provider string, region string, storagePath string) string {
	var command string
	cspp := cloudstorages.GetProvider(provider)
	if cspp != nil {
		command = (*cspp).GetGatlingTransferResultCommand(resultsDirectoryPath, region, storagePath)
	}
	return command
}

func getGatlingAggregateResultCommand(resultsDirectoryPath string, provider string, region string, storagePath string) string {
	var command string
	cspp := cloudstorages.GetProvider(provider)
	if cspp != nil {
		command = (*cspp).GetGatlingAggregateResultCommand(resultsDirectoryPath, region, storagePath)
	}
	return command
}

func getGatlingGenerateReportCommand(resultsDirectoryPath string) string {
	template := `
GATLING_AGGREGATE_DIR=%s
DIR_NAME=$(dirname ${GATLING_AGGREGATE_DIR})
BASE_NAME=$(basename ${GATLING_AGGREGATE_DIR})
gatling.sh -rf ${DIR_NAME} -ro ${BASE_NAME}
`
	return fmt.Sprintf(template, resultsDirectoryPath)
}

func getGatlingTransferReportCommand(resultsDirectoryPath string, provider string, region string, storagePath string) string {
	var command string
	cspp := cloudstorages.GetProvider(provider)
	if cspp != nil {
		command = (*cspp).GetGatlingTransferReportCommand(resultsDirectoryPath, region, storagePath)
	}
	return command
}


<|start_filename|>gatling/Dockerfile<|end_filename|>
# This is modified based on the original file:
# https://github.com/denvazh/gatling/tree/master/3.2.1
#
# Gatling is a highly capable load testing tool.
#
# Documentation: https://gatling.io/docs/3.2/
# Cheat sheet: https://gatling.io/docs/3.2/cheat-sheet/

FROM openjdk:8-jdk-alpine

# working directory for gatling
WORKDIR /opt

# gating version
ENV GATLING_VERSION 3.2.1

# create directory for gatling install
RUN mkdir -p gatling

# install gatling
RUN apk add --update wget bash libc6-compat && \
  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/*

# change context to gatling directory
WORKDIR  /opt/gatling

# set directories below to be mountable from host
VOLUME ["/opt/gatling/conf", "/opt/gatling/results", "/opt/gatling/user-files"]

# copy local files to gatling directory
COPY user-files/simulations user-files/simulations
COPY user-files/resources user-files/resources
COPY conf conf

# set environment variables
ENV PATH /opt/gatling/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ENV GATLING_HOME /opt/gatling

ENTRYPOINT ["gatling.sh"]


<|start_filename|>controllers/notificationservices/notificationservice.go<|end_filename|>
package notificationservices

import (
	"sync"
)

type NotificationServiceProvider interface {
	GetName() string
	Notify(gatlingName string, reportURL string, secretData map[string][]byte) error
}

// use sync.Map to achieve thread safe read and write to map
var notificationServiceProvidersSyncMap = &sync.Map{}

func GetProvider(provider string) *NotificationServiceProvider {
	v, ok := notificationServiceProvidersSyncMap.Load(provider)
	if !ok {
		var nsp NotificationServiceProvider
		switch provider {
		case "slack":
			nsp = &SlackNotificationServiceProvider{providerName: provider}
		default:
			return nil
		}
		v, _ = notificationServiceProvidersSyncMap.LoadOrStore(provider, &nsp)
	}
	return v.(*NotificationServiceProvider)
}


<|start_filename|>controllers/notificationservices/notificationservice_test.go<|end_filename|>
package notificationservices

import (
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("GetProvider", func() {
	var (
		provider      string
		expectedValue string
	)
	Context("Provider is slack", func() {
		BeforeEach(func() {
			provider = "slack"
			expectedValue = "slack"
		})
		It("should get a pointer of SlackNotificationServiceProvider that has ProviderName field value = slack", func() {
			nspp := GetProvider(provider)
			Expect(nspp).NotTo(BeNil())
			Expect((*nspp).GetName()).To(Equal(expectedValue))
		})
	})

	Context("Provider is non-supported one", func() {
		BeforeEach(func() {
			provider = "foo"
		})
		It("should get nil pointer ", func() {
			nspp := GetProvider(provider)
			// If it should be nil, use BeNil() instead of Equal(nil) ref: https://onsi.github.io/gomega/
			Expect(nspp).To(BeNil())
		})
	})
})


<|start_filename|>controllers/utils/utils.go<|end_filename|>
package utils

import (
	"hash/fnv"
	"time"
)

func Hash(s string) uint32 {
	h := fnv.New32a()
	if _, err := h.Write([]byte(s)); err != nil {
		return 0
	}
	return h.Sum32()
}

func GetEpocTime() int32 {
	return int32(time.Now().Unix())
}

func GetMapValue(key string, dataMap map[string][]byte) (string, bool) {
	if v, exists := dataMap[key]; exists {
		return string(v), true
	}
	return "", false
}


<|start_filename|>controllers/commands_test.go<|end_filename|>
package controllers

import (
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("getGatlingWaiterCommand", func() {
	It("getExceptValue", func() {
		expectedValue := `
PARALLELISM=1
NAMESPACE=gatling-system
JOB_NAME=gatling-test
POD_NAME=$(cat /etc/pod-info/name)

kubectl label pods -n $NAMESPACE $POD_NAME gatling-waiter=initialized

while true; do
  READY_PODS=$(kubectl get pods -n $NAMESPACE --selector=job-name=$JOB_NAME-runner,gatling-waiter=initialized --no-headers | grep -c ".*");
  echo "$READY_PODS/$PARALLELISM pods are ready";
  if  [ $READY_PODS -eq $PARALLELISM ]; then
    break;
  fi;
  sleep 1;
done
`
		var parallelism int32 = 1
		Expect(getGatlingWaiterCommand(&parallelism, "gatling-system", "gatling-test")).To(Equal(expectedValue))
	})
})

var _ = Describe("getGatlingRunnerCommand", func() {
	var (
		simulationsDirectoryPath     string
		tempSimulationsDirectoryPath string
		resourcesDirectoryPath       string
		resultsDirectoryPath         string
		startTime                    string
		simulationClass              string
		generateLocalReport          bool
		expectedValue                string
	)

	BeforeEach(func() {
		simulationsDirectoryPath = "testSimulationDirectoryPath"
		tempSimulationsDirectoryPath = "testTempSimulationsDirectoryPath"
		resourcesDirectoryPath = "testResourcesDirectoryPath"
		resultsDirectoryPath = "testResultsDirectoryPath"
		startTime = "2021-09-10 08:45:31"
		simulationClass = "testSimulationClass"
	})

	It("getCommandsWithLocalReport", func() {
		generateLocalReport = true
		expectedValue = `
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"
if [ -z "${START_TIME}" ]; then
  START_TIME=$(date +"%Y-%m-%d %H:%M:%S" --utc)
fi
start_time_stamp=$(date -d "${START_TIME}" +"%s")
current_time_stamp=$(date +"%s")
echo "Wait until ${START_TIME}"
until [ ${current_time_stamp} -ge ${start_time_stamp} ];
do
  current_time_stamp=$(date +"%s")
  echo "it's ${current_time_stamp} now and waiting until ${start_time_stamp} ..."
  sleep 1;
done
if [ ! -d ${SIMULATIONS_DIR_PATH} ]; then
  mkdir -p ${SIMULATIONS_DIR_PATH}
fi
if [ -d ${TEMP_SIMULATIONS_DIR_PATH} ]; then
  cp -p ${TEMP_SIMULATIONS_DIR_PATH}/*.scala ${SIMULATIONS_DIR_PATH}
fi
if [ ! -d ${RESOURCES_DIR_PATH} ]; then
  mkdir -p ${RESOURCES_DIR_PATH}
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} 
`
		Expect(getGatlingRunnerCommand(simulationsDirectoryPath, tempSimulationsDirectoryPath, resourcesDirectoryPath, resultsDirectoryPath, startTime, simulationClass, generateLocalReport)).To(Equal(expectedValue))
	})

	It("getCommandWithoutLocalReport", func() {
		generateLocalReport = false
		expectedValue = `
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"
if [ -z "${START_TIME}" ]; then
  START_TIME=$(date +"%Y-%m-%d %H:%M:%S" --utc)
fi
start_time_stamp=$(date -d "${START_TIME}" +"%s")
current_time_stamp=$(date +"%s")
echo "Wait until ${START_TIME}"
until [ ${current_time_stamp} -ge ${start_time_stamp} ];
do
  current_time_stamp=$(date +"%s")
  echo "it's ${current_time_stamp} now and waiting until ${start_time_stamp} ..."
  sleep 1;
done
if [ ! -d ${SIMULATIONS_DIR_PATH} ]; then
  mkdir -p ${SIMULATIONS_DIR_PATH}
fi
if [ -d ${TEMP_SIMULATIONS_DIR_PATH} ]; then
  cp -p ${TEMP_SIMULATIONS_DIR_PATH}/*.scala ${SIMULATIONS_DIR_PATH}
fi
if [ ! -d ${RESOURCES_DIR_PATH} ]; then
  mkdir -p ${RESOURCES_DIR_PATH}
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
`
		Expect(getGatlingRunnerCommand(simulationsDirectoryPath, tempSimulationsDirectoryPath, resourcesDirectoryPath, resultsDirectoryPath, startTime, simulationClass, generateLocalReport)).To(Equal(expectedValue))
	})
})

var _ = Describe("getGatlingTransferResultCommand", func() {
	var (
		resultsDirectoryPath string
		provider             string
		region               string
		storagePath          string
		expectedValue        string
	)

	BeforeEach(func() {
		resultsDirectoryPath = "testResultsDirectoryPath"
		region = "ap-northeast-1"
		storagePath = "testStoragePath"
	})

	Context("Provider is aws", func() {
		BeforeEach(func() {
			provider = "aws"
			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
done
`
		})
		It("provider is aws", func() {
			Expect(getGatlingTransferResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue))
		})
	})

	Context("Provider is gcp", func() {
		BeforeEach(func() {
			provider = "gcp"
			expectedValue = `
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
done
`
		})
		It("Provider is gcp", func() {
			Expect(getGatlingTransferResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue))
		})
	})

	Context("Provider is non-supported one", func() {
		BeforeEach(func() {
			provider = "foo"
			expectedValue = ""
		})
		It("Provide is non-supported one", func() {
			Expect(getGatlingTransferResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue))
		})
	})

	Context("Provider is empty", func() {
		BeforeEach(func() {
			provider = ""
			expectedValue = ""
		})
		It("Provider is empty", func() {
			Expect(getGatlingTransferResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue))
		})
	})
})

var _ = Describe("getGatlingAggregateResultCommand", func() {
	var (
		resultsDirectoryPath string
		provider             string
		region               string
		storagePath          string
		expectedValue        string
	)

	BeforeEach(func() {
		resultsDirectoryPath = "testResultsDirectoryPath"
		region = "ap-northeast-1"
		storagePath = "testStoragePath"
	})

	Context("Provider is aws", func() {
		BeforeEach(func() {
			provider = "aws"
			expectedValue = `
GATLING_AGGREGATE_DIR=testResultsDirectoryPath
rclone config create s3 s3 env_auth=true region ap-northeast-1
rclone copy --s3-no-check-bucket --s3-env-auth testStoragePath ${GATLING_AGGREGATE_DIR}
`
		})
		It("provider is aws", func() {
			Expect(getGatlingAggregateResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue))
		})
	})

	Context("Provider is gcp", func() {
		BeforeEach(func() {
			provider = "gcp"
			expectedValue = `
GATLING_AGGREGATE_DIR=testResultsDirectoryPath
# assumes gcs bucket using uniform bucket-level access control
rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive
rclone copy testStoragePath ${GATLING_AGGREGATE_DIR}
`
		})
		It("Provider is gcp", func() {
			Expect(getGatlingAggregateResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue))
		})
	})

	Context("Provider is non-supported one", func() {
		BeforeEach(func() {
			provider = "foo"
			expectedValue = ""
		})
		It("Provide is non-supported one", func() {
			Expect(getGatlingAggregateResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue))
		})
	})

	Context("Provider is empty", func() {
		BeforeEach(func() {
			provider = ""
			expectedValue = ""
		})
		It("Provider is empty", func() {
			Expect(getGatlingAggregateResultCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue))
		})
	})
})

var _ = Describe("getGatlingGenerateReportCommand", func() {
	var (
		resultsDirectoryPath string
		expectedValue        string
	)

	BeforeEach(func() {
		resultsDirectoryPath = "testResultsDirectoryPath"
		expectedValue = `
GATLING_AGGREGATE_DIR=testResultsDirectoryPath
DIR_NAME=$(dirname ${GATLING_AGGREGATE_DIR})
BASE_NAME=$(basename ${GATLING_AGGREGATE_DIR})
gatling.sh -rf ${DIR_NAME} -ro ${BASE_NAME}
`
	})

	It("getExceptValue", func() {
		Expect(getGatlingGenerateReportCommand(resultsDirectoryPath)).To(Equal(expectedValue))
	})
})

var _ = Describe("getGatlingTransferReportCommand", func() {
	var (
		resultsDirectoryPath string
		provider             string
		region               string
		storagePath          string
		expectedValue        string
	)

	BeforeEach(func() {
		resultsDirectoryPath = "testResultsDirectoryPath"
		region = "ap-northeast-1"
		storagePath = "testStoragePath"
	})

	Context("Provider is aws", func() {
		BeforeEach(func() {
			provider = "aws"
			expectedValue = `
GATLING_AGGREGATE_DIR=testResultsDirectoryPath
rclone config create s3 s3 env_auth=true region ap-northeast-1
rclone copy ${GATLING_AGGREGATE_DIR} --exclude "*.log" --s3-no-check-bucket --s3-env-auth testStoragePath
`
		})
		It("provider is aws", func() {
			Expect(getGatlingTransferReportCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue))
		})
	})

	Context("Provider is gcp", func() {
		BeforeEach(func() {
			provider = "gcp"
			expectedValue = `
GATLING_AGGREGATE_DIR=testResultsDirectoryPath
# assumes gcs bucket using uniform bucket-level access control
rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive
rclone copy ${GATLING_AGGREGATE_DIR} --exclude "*.log" testStoragePath
`
		})
		It("Provider is gcp", func() {
			Expect(getGatlingTransferReportCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue))
		})
	})

	Context("Provider is non-supported one", func() {
		BeforeEach(func() {
			provider = "foo"
			expectedValue = ""
		})
		It("Provide is non-supported one", func() {
			Expect(getGatlingTransferReportCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue))
		})
	})

	Context("Provider is empty", func() {
		BeforeEach(func() {
			provider = ""
			expectedValue = ""
		})
		It("Provider is empty", func() {
			Expect(getGatlingTransferReportCommand(resultsDirectoryPath, provider, region, storagePath)).To(Equal(expectedValue))
		})
	})
})


<|start_filename|>controllers/cloudstorages/cloudstorage.go<|end_filename|>
package cloudstorages

import (
	"sync"
)

type CloudStorageProvider interface {
	GetName() string
	GetCloudStoragePath(bucket string, gatlingName string, subDir string) string
	GetCloudStorageReportURL(bucket string, gatlingName string, subDir string) string
	GetGatlingTransferResultCommand(resultsDirectoryPath string, region string, storagePath string) string
	GetGatlingAggregateResultCommand(resultsDirectoryPath string, region string, storagePath string) string
	GetGatlingTransferReportCommand(resultsDirectoryPath string, region string, storagePath string) string
}

// use sync.Map to achieve thread safe read and write to map
var cloudStorageProvidersSyncMap = &sync.Map{}

func GetProvider(provider string) *CloudStorageProvider {
	v, ok := cloudStorageProvidersSyncMap.Load(provider)
	if !ok {
		var csp CloudStorageProvider
		switch provider {
		case "aws":
			csp = &AWSCloudStorageProvider{providerName: provider}
		case "gcp":
			csp = &GCPCloudStorageProvider{providerName: provider}
		default:
			return nil
		}
		v, _ = cloudStorageProvidersSyncMap.LoadOrStore(provider, &csp)
	}
	return v.(*CloudStorageProvider)
}


<|start_filename|>api/v1alpha1/gatling_types.go<|end_filename|>
/*
Copyright &copy; ZOZO, Inc.

Licensed 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.
*/

package v1alpha1

import (
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// GatlingSpec defines the desired state of Gatling
type GatlingSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// (Optional) The flag of generating gatling report.  Defaults to `false`
	// +kubebuilder:default=false
	// +kubebuilder:validation:Optional
	GenerateReport bool `json:"generateReport,omitempty"`

	// (Optional) The flag of generating gatling report at each pod
	// +kubebuilder:default=false
	// +kubebuilder:validation:Optional
	GenerateLocalReport bool `json:"generateLocalReport,omitempty"`

	// (Optional) The flag of notifying gatling report. Defaults to `false`
	// +kubebuilder:default=false
	// +kubebuilder:validation:Optional
	NotifyReport bool `json:"notifyReport,omitempty"`

	// (Optional) The flag of cleanup gatling resources after the job done. Defaults to `false`
	// +kubebuilder:default=false
	// +kubebuilder:validation:Optional
	CleanupAfterJobDone bool `json:"cleanupAfterJobDone,omitempty"`

	// (Optional) Gatling Pod specification.
	// +kubebuilder:validation:Optional
	PodSpec PodSpec `json:"podSpec,omitempty"`

	// (Optional) Cloud Storage Provider specification.
	// +kubebuilder:validation:Optional
	CloudStorageSpec CloudStorageSpec `json:"cloudStorageSpec,omitempty"`

	// (Optional) Notification Service specification.
	// +kubebuilder:validation:Optional
	NotificationServiceSpec NotificationServiceSpec `json:"notificationServiceSpec,omitempty"`

	// (Required) Test Scenario specification
	// +kubebuilder:validation:Required
	TestScenarioSpec TestScenarioSpec `json:"testScenarioSpec"`
}

// PodSpec defines type to configure Gatling Pod specification. For the idea of PodSpec, refer to [bitpoke/mysql-operator](https://github.com/bitpoke/mysql-operator/blob/master/pkg/apis/mysql/v1alpha1/mysqlcluster_types.go)
type PodSpec struct {
	// (Optional) The image that will be used for Gatling container. Defaults to `ghcr.io/st-tech/gatling:latest`
	// +kubebuilder:validation:Optional
	GatlingImage string `json:"gatlingImage,omitempty"`

	// (Optional) The image that will be used for rclone conatiner. Defaults to `rclone/rclone:latest`
	// +kubebuilder:validation:Optional
	RcloneImage string `json:"rcloneImage,omitempty"`

	// (Optional) Resources specifies the resource limits of the container.
	// +kubebuilder:validation:Optional
	Resources corev1.ResourceRequirements `json:"resources,omitempty"`

	// (Optional) Affinity specification.
	// +kubebuilder:validation:Optional
	Affinity corev1.Affinity `json:"affinity,omitempty"`

	// (Optional) Tolerations specification.
	// +kubebuilder:validation:Optional
	Tolerations []corev1.Toleration `json:"tolerations,omitempty"`

	// (Required) ServiceAccountName specification.
	// +kubebuilder:validation:Required
	ServiceAccountName string `json:"serviceAccountName"`
}

// TestScenarioSpec defines the load testing scenario
type TestScenarioSpec struct {
	// (Optional) Test Start time.
	// +kubebuilder:validation:Optional
	StartTime string `json:"startTime,omitempty"`

	// (Optional) Number of pods running at the same time. Defaults to `1` (Mininum `1`)
	// +kubebuilder:default=1
	// +kubebuilder:validation:Minimum=1
	// +kubebuilder:validation:Optional
	Parallelism int32 `json:"parallelism,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"`

	// (Optional) Gatling Simulation directory path where resources are stored. Defaults to `/opt/gatling/user-files/resources`
	// +kubebuilder:validation:Optional
	ResourcesDirectoryPath string `json:"resourcesDirectoryPath,omitempty"`

	// (Optional) Gatling Results directory path where results are stored. Defaults to `/opt/gatling/results`
	// +kubebuilder:validation:Optional
	ResultsDirectoryPath string `json:"resultsDirectoryPath,omitempty"`

	// (Required) Simulation Class Name.
	// +kubebuilder:validation:Required
	SimulationClass string `json:"simulationClass"`

	// (Optional) Simulation Data.
	// +kubebuilder:validation:Optional
	SimulationData map[string]string `json:"simulationData,omitempty"`

	// (Optional) Resource Data.
	// +kubebuilder:validation:Optional
	ResourceData map[string]string `json:"resourceData,omitempty"`

	// (Optional) Gatling Configurations.
	// +kubebuilder:validation:Optional
	GatlingConf map[string]string `json:"gatlingConf,omitempty"`

	// (Optional) Environment variables used for running load testing scenario.
	// +optional
	Env []corev1.EnvVar `json:"env,omitempty"`
}

// CloudStorageSpec defines Cloud Storage Provider specification.
type CloudStorageSpec struct {
	// (Required) Provider specifies the cloud provider that will be used.
	// Supported providers: `aws`, `gcp`
	// +kubebuilder:validation:Required
	Provider string `json:"provider"`

	// (Required) Storage Bucket Name.
	// +kubebuilder:validation:Required
	Bucket string `json:"bucket"`

	// (Optional) Region Name.
	// +kubebuilder:validation:Optional
	Region string `json:"region,omitempty"`

	// (Optional) Environment variables used for connecting to the cloud providers.
	// +kubebuilder:validation:Optional
	Env []corev1.EnvVar `json:"env,omitempty"`
}

// NotificationServiceSpec defines Notification Service Provider specification.
type NotificationServiceSpec struct {
	// (Required) Provider specifies notification service provider.
	// Supported providers: `slack`
	// +kubebuilder:validation:Required
	Provider string `json:"provider"`

	// (Required) The name of secret in which all key/value sets needed for the notification are stored.
	// +kubebuilder:validation:Required
	SecretName string `json:"secretName"`
}

// GatlingStatus defines the observed state of Gatling
type GatlingStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// Active is list of currently running jobs
	// The number of actively running pods for the gatling job
	// +optional
	Active int32 `json:"active,omitempty"`

	// The number of pods which reached phase Succeeded for the gatling job
	// +optional
	Succeeded int32 `json:"succeeded,omitempty"`

	// The number of pods which reached phase Failed for the gatling job
	// +optional
	Failed int32 `json:"failed,omitempty"`

	// Runner job name
	// +optional
	RunnerJobName string `json:"runnerJobName,omitempty"`

	// Runner start time (UnixTime epoc)
	// +optional
	RunnerStartTime int32 `json:"runnerStartTime,omitempty"`

	// Is runner job completed (default false)
	// +optional
	RunnerCompleted bool `json:"runnerCompleted,omitempty"`

	// Reporter job name
	// +optional
	ReporterJobName string `json:"reporterJobName,omitempty"`

	// Reporter start time (UnixTime epoc)
	// +optional
	ReporterStartTime int32 `json:"reporterStartTime,omitempty"`

	// Is report generation completed (default false)
	// +optional
	ReportCompleted bool `json:"reportCompleted,omitempty"`

	// Report Storage Path
	// +optional
	ReportStoragePath string `json:"reportStoragePath,omitempty"`

	// Report Url
	// +optional
	ReportUrl string `json:"reportUrl,omitempty"`

	// Is notification completed (default false)
	// +optional
	NotificationCompleted bool `json:"notificationCompleted,omitempty"`

	// Error message
	// +optional
	Error string `json:"error,omitempty"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// Gatling is the Schema for the gatlings API
type Gatling struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	// GatlingSpec defines the desired state of Gatling
	Spec GatlingSpec `json:"spec,omitempty"`
	// GatlingStatus defines the observed state of Gatling
	Status GatlingStatus `json:"status,omitempty"`
}

//+kubebuilder:object:root=true

// GatlingList contains a list of Gatling
type GatlingList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitempty"`
	Items           []Gatling `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Gatling{}, &GatlingList{})
}


<|start_filename|>controllers/cloudstorages/aws_test.go<|end_filename|>
package cloudstorages

import (
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("GetName", func() {
	var (
		provider      string
		expectedValue string
	)
	BeforeEach(func() {
		provider = "aws"
		expectedValue = "aws"
	})
	Context("Provider is aws", func() {
		It("should get provider name = aws", func() {
			csp := &AWSCloudStorageProvider{providerName: provider}
			Expect(csp.GetName()).To(Equal(expectedValue))
		})
	})
})

var _ = Describe("GetCloudStoragePath", func() {
	var (
		provider      string
		bucket        string
		gatlingName   string
		subDir        string
		expectedValue string
	)
	BeforeEach(func() {
		provider = "aws"
		bucket = "testBucket"
		gatlingName = "testGatling"
		subDir = "subDir"
		expectedValue = "s3:testBucket/testGatling/subDir"
	})
	Context("Provider is aws", func() {
		It("path is aws s3 bucket", func() {
			csp := &AWSCloudStorageProvider{providerName: provider}
			Expect(csp.GetCloudStoragePath(bucket, gatlingName, subDir)).To(Equal(expectedValue))
		})
	})
})

var _ = Describe("GetCloudStorageReportURL", func() {
	var (
		provider      string
		bucket        string
		gatlingName   string
		subDir        string
		expectedValue string
	)
	BeforeEach(func() {
		provider = "aws"
		bucket = "testBucket"
		gatlingName = "testGatling"
		subDir = "subDir"
		expectedValue = "https://testBucket.s3.amazonaws.com/testGatling/subDir/index.html"
	})
	Context("Provider is aws", func() {
		It("path is aws s3 bucket", func() {
			csp := &AWSCloudStorageProvider{providerName: provider}
			Expect(csp.GetCloudStorageReportURL(bucket, gatlingName, subDir)).To(Equal(expectedValue))
		})
	})
})

var _ = Describe("GetGatlingTransferResultCommand", func() {
	var (
		provider             string
		resultsDirectoryPath string
		region               string
		storagePath          string
		expectedValue        string
	)
	BeforeEach(func() {
		provider = "aws"
		resultsDirectoryPath = "testResultsDirectoryPath"
		region = "ap-northeast-1"
		storagePath = "testStoragePath"
		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
done
`
	})
	Context("Provider is aws", func() {
		It("returns commands with s3 rclone config", func() {
			csp := &AWSCloudStorageProvider{providerName: provider}
			Expect(csp.GetGatlingTransferResultCommand(resultsDirectoryPath, region, storagePath)).To(Equal(expectedValue))
		})
	})
})

var _ = Describe("GetGatlingAggregateResultCommand", func() {
	var (
		provider             string
		resultsDirectoryPath string
		region               string
		storagePath          string
		expectedValue        string
	)
	BeforeEach(func() {
		provider = "aws"
		resultsDirectoryPath = "testResultsDirectoryPath"
		region = "ap-northeast-1"
		storagePath = "testStoragePath"
		expectedValue = `
GATLING_AGGREGATE_DIR=testResultsDirectoryPath
rclone config create s3 s3 env_auth=true region ap-northeast-1
rclone copy --s3-no-check-bucket --s3-env-auth testStoragePath ${GATLING_AGGREGATE_DIR}
`
	})
	Context("Provider is aws", func() {
		It("returns commands with s3 rclone config", func() {
			csp := &AWSCloudStorageProvider{providerName: provider}
			Expect(csp.GetGatlingAggregateResultCommand(resultsDirectoryPath, region, storagePath)).To(Equal(expectedValue))
		})
	})
})

var _ = Describe("GetGatlingTransferReportCommand", func() {
	var (
		provider             string
		resultsDirectoryPath string
		region               string
		storagePath          string
		expectedValue        string
	)
	BeforeEach(func() {
		provider = "aws"
		resultsDirectoryPath = "testResultsDirectoryPath"
		region = "ap-northeast-1"
		storagePath = "testStoragePath"
		expectedValue = `
GATLING_AGGREGATE_DIR=testResultsDirectoryPath
rclone config create s3 s3 env_auth=true region ap-northeast-1
rclone copy ${GATLING_AGGREGATE_DIR} --exclude "*.log" --s3-no-check-bucket --s3-env-auth testStoragePath
`
	})
	Context("Provider is aws", func() {
		It("returns commands with s3 rclone config", func() {
			csp := &AWSCloudStorageProvider{providerName: provider}
			Expect(csp.GetGatlingTransferReportCommand(resultsDirectoryPath, region, storagePath)).To(Equal(expectedValue))
		})
	})
})


<|start_filename|>controllers/gatling_controller.go<|end_filename|>
/*
Copyright &copy; ZOZO, Inc.

Licensed 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.
*/

package controllers

import (
	"context"
	"errors"
	"fmt"
	"math"
	"time"

	"github.com/go-logr/logr"
	apierr "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/rand"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller"
	"sigs.k8s.io/controller-runtime/pkg/event"
	"sigs.k8s.io/controller-runtime/pkg/predicate"

	gatlingv1alpha1 "github.com/st-tech/gatling-operator/api/v1alpha1"
	cloudstorages "github.com/st-tech/gatling-operator/controllers/cloudstorages"
	notificationservices "github.com/st-tech/gatling-operator/controllers/notificationservices"
	utils "github.com/st-tech/gatling-operator/controllers/utils"
	batchv1 "k8s.io/api/batch/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
	requeueIntervalInSeconds           = 5     // 5 sec
	maxJobCreationWaitTimeInSeconds    = 600   // 600 sec (10 min)
	maxJobRunWaitTimeInSeconds         = 10800 // 10800 sec (3 hours)
	defaultGatlingImage                = "ghcr.io/st-tech/gatling:latest"
	defaultRcloneImage                 = "rclone/rclone:latest"
	defaultSimulationsDirectoryPath    = "/opt/gatling/user-files/simulations"
	defaultResourcesDirectoryPath      = "/opt/gatling/user-files/resources"
	defaultResultsDirectoryPath        = "/opt/gatling/results"
	defaultNotificationServiceProvider = "slack"
)

// GatlingReconciler reconciles a Gatling object
type GatlingReconciler struct {
	client.Client
	Log    logr.Logger
	Scheme *runtime.Scheme
}

//+kubebuilder:rbac:groups="batch",resources=jobs,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups="core",resources=configmaps,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups="core",resources=secrets,verbs=get;list;watch
//+kubebuilder:rbac:groups=gatling-operator.tech.zozo.com,resources=gatlings,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=gatling-operator.tech.zozo.com,resources=gatlings/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=gatling-operator.tech.zozo.com,resources=gatlings/finalizers,verbs=update

func (r *GatlingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := ctrl.LoggerFrom(ctx).WithName("gatling").WithName("Reconcile")
	// fetching gatling Resource from in-memory-cache
	gatling := &gatlingv1alpha1.Gatling{}
	if err := r.Get(ctx, req.NamespacedName, gatling); err != nil {
		log.Error(err, "Unable to fetch Gatling, thus no longer requeue")
		return doNotRequeue(client.IgnoreNotFound(err))
	}

	log.Info("Reconciling Gatling")
	// r.dumpGatlingStatus(gatling, log)
	if r.isGatlingCompleted(gatling) {
		log.Info("Gatling job has completed!", "name", gatling.ObjectMeta.Name, "namespace", gatling.ObjectMeta.Namespace)

		// Clean up Job resources if neccessary
		if gatling.Spec.CleanupAfterJobDone {
			log.Info(fmt.Sprintf("Cleaning up gatlig %s", gatling.Name))
			r.cleanupGatling(ctx, req, gatling.Name)
		}
		return doNotRequeue(nil)
	}
	// Reconciling for running Gatling Jobs
	if !gatling.Status.RunnerCompleted {
		requeue, err := r.gatlingRunnerReconcile(ctx, req, gatling, log)
		if requeue {
			return doRequeue(requeueIntervalInSeconds*time.Second, err)
		}
		if err != nil {
			if gatling.Spec.CleanupAfterJobDone {
				r.cleanupJob(ctx, req, gatling.Status.RunnerJobName)
			}
			return doNotRequeue(err)
		}
	}
	// Reconciling for reporting
	if gatling.Spec.GenerateReport && gatling.Status.RunnerCompleted && !gatling.Status.ReportCompleted {
		requeue, err := r.gatlingReporterReconcile(ctx, req, gatling, log)
		if requeue {
			return doRequeue(requeueIntervalInSeconds*time.Second, err)
		}
		if err != nil {
			if gatling.Spec.CleanupAfterJobDone {
				r.cleanupJob(ctx, req, gatling.Status.ReporterJobName)
			}
			return doNotRequeue(err)
		}
	}
	// Reconciling for notification
	if gatling.Spec.NotifyReport &&
		((gatling.Spec.GenerateReport && gatling.Status.ReportCompleted) || !gatling.Spec.GenerateReport) &&
		!gatling.Status.NotificationCompleted {
		requeue, err := r.gatlingNotificationReconcile(ctx, req, gatling, log)
		if requeue {
			return doRequeue(requeueIntervalInSeconds*time.Second, err)
		}
		if err != nil {
			return doNotRequeue(err)
		}
	}

	return doNotRequeue(nil)
}

// Implementation of reconciler logic for the runner job
func (r *GatlingReconciler) gatlingRunnerReconcile(ctx context.Context, req ctrl.Request, gatling *gatlingv1alpha1.Gatling, log logr.Logger) (bool, error) {
	// Create Simulation Data ConfigMap if defined to create in CR
	if &gatling.Spec.TestScenarioSpec.SimulationData != nil && len(gatling.Spec.TestScenarioSpec.SimulationData) > 0 {
		configMapName := gatling.Name + "-simulations-data"
		foundConfigMap := &corev1.ConfigMap{}
		if err := r.Get(ctx, client.ObjectKey{Name: configMapName, Namespace: req.Namespace}, foundConfigMap); err != nil {
			if apierr.IsNotFound(err) {
				simulationDataConfigMap := r.newConfigMapForCR(gatling, configMapName, &gatling.Spec.TestScenarioSpec.SimulationData)
				if err := r.createObject(ctx, gatling, simulationDataConfigMap); err != nil {
					log.Error(err, fmt.Sprintf("Failed to creating new ConfigMap: namespace %s name %s", simulationDataConfigMap.GetNamespace(), simulationDataConfigMap.GetName()))
					return true, err
				}
			} else {
				return true, err
			}
		}
	}
	// Create Resource Data ConfigMap if defined to create in CR
	if &gatling.Spec.TestScenarioSpec.ResourceData != nil && len(gatling.Spec.TestScenarioSpec.ResourceData) > 0 {
		configMapName := gatling.Name + "-resources-data"
		foundConfigMap := &corev1.ConfigMap{}
		if err := r.Get(ctx, client.ObjectKey{Name: configMapName, Namespace: req.Namespace}, foundConfigMap); err != nil {
			if apierr.IsNotFound(err) {
				resourceDataConfigMap := r.newConfigMapForCR(gatling, configMapName, &gatling.Spec.TestScenarioSpec.ResourceData)
				if err := r.createObject(ctx, gatling, resourceDataConfigMap); err != nil {
					log.Error(err, fmt.Sprintf("Failed to creating new ConfigMap: namespace %s name %s", resourceDataConfigMap.GetNamespace(), resourceDataConfigMap.GetName()))
					return true, err
				}
			} else {
				return true, err
			}
		}
	}
	// Create GatlingConf ConfigMap if defined to create in CR
	if &gatling.Spec.TestScenarioSpec.GatlingConf != nil && len(gatling.Spec.TestScenarioSpec.GatlingConf) > 0 {
		configMapName := gatling.Name + "-gatling-conf"
		foundConfigMap := &corev1.ConfigMap{}
		if err := r.Get(ctx, client.ObjectKey{Name: configMapName, Namespace: req.Namespace}, foundConfigMap); err != nil {
			if apierr.IsNotFound(err) {
				gatlingConfConfigMap := r.newConfigMapForCR(gatling, configMapName, &gatling.Spec.TestScenarioSpec.GatlingConf)
				if err := r.createObject(ctx, gatling, gatlingConfConfigMap); err != nil {
					log.Error(err, fmt.Sprintf("Failed to creating new ConfigMap: namespace %s name %s", gatlingConfConfigMap.GetNamespace(), gatlingConfConfigMap.GetName()))
					return true, err
				}
			} else {
				return true, err
			}
		}
	}
	if gatling.Status.RunnerJobName == "" {
		var storagePath = ""
		// Get cloud storage info only if gatling.spec.generateReport is true
		if gatling.Spec.GenerateReport {
			path, _, err := r.getCloudStorageInfo(ctx, gatling)
			if err != nil {
				log.Error(err, "Failed to update gatling status, and requeue")
				return true, err
			}
			storagePath = path
		}
		// Define and create new Job object
		runnerJob := r.newGatlingRunnerJobForCR(gatling, storagePath, log)
		if err := r.createObject(ctx, gatling, runnerJob); err != nil {
			log.Error(err, fmt.Sprintf("Failed to creating new job, and requeue: namespace %s name %s", runnerJob.GetNamespace(), runnerJob.GetName()))
			return true, err
		}
		// Update Status
		gatling.Status.RunnerStartTime = utils.GetEpocTime()
		gatling.Status.RunnerJobName = runnerJob.GetName()
		gatling.Status.Active = runnerJob.Status.Active
		gatling.Status.Failed = runnerJob.Status.Failed
		gatling.Status.Succeeded = runnerJob.Status.Succeeded
		gatling.Status.RunnerCompleted = false
		gatling.Status.ReportCompleted = false
		gatling.Status.NotificationCompleted = false
		if err := r.updateGatlingStatus(ctx, gatling); err != nil {
			return true, err
		}
	}
	foundJob := &batchv1.Job{}
	err := r.Get(ctx, client.ObjectKey{Name: gatling.Status.RunnerJobName, Namespace: req.Namespace}, foundJob)
	if err != nil && apierr.IsNotFound(err) {
		duration := utils.GetEpocTime() - gatling.Status.RunnerStartTime
		if duration > maxJobCreationWaitTimeInSeconds {
			msg := fmt.Sprintf("Runs out of time (%d sec) in creating the runner job", maxJobCreationWaitTimeInSeconds)
			log.Error(err, msg, "namespace", req.Namespace, "name", gatling.Status.RunnerJobName)
			gatling.Status.Error = msg
			if err := r.updateGatlingStatus(ctx, gatling); err != nil {
				return true, err
			}
			return false, err // no longer requeue
		}
		log.Info("The runner job has not been created yet, and requeue", "namespace", req.Namespace, "name", gatling.Status.RunnerJobName)
		return true, err
	} else if err != nil {
		log.Error(err, "Failed to get the runner job, and requeue", "namespace", req.Namespace, "name", gatling.Status.RunnerJobName)
		return true, err
	}
	// Set foundJob status to gatling status
	gatling.Status.Active = foundJob.Status.Active
	gatling.Status.Failed = foundJob.Status.Failed
	gatling.Status.Succeeded = foundJob.Status.Succeeded

	// Check if the job runs out of time in running the job
	duration := utils.GetEpocTime() - gatling.Status.RunnerStartTime
	if duration > maxJobRunWaitTimeInSeconds {
		msg := fmt.Sprintf("Runs out of time (%d sec) in running the runner job", maxJobCreationWaitTimeInSeconds)
		log.Error(nil, msg, "namespace", req.Namespace, "name", gatling.Status.ReporterJobName)
		gatling.Status.Error = msg
		if err := r.updateGatlingStatus(ctx, gatling); err != nil {
			return true, err
		}
		return false, errors.New(msg) // no longer requeue
	}
	// Check if the runner job has completed
	log.Info("Check if the runner job has completed", "namespace", foundJob.GetNamespace(), "name", foundJob.GetName())
	if r.isJobCompleted(foundJob) {
		if foundJob.Status.Succeeded == gatling.Spec.TestScenarioSpec.Parallelism {
			log.Info(fmt.Sprintf("Job has successfuly completed! ( successded %d )", foundJob.Status.Succeeded), "namespace", foundJob.GetNamespace(), "name", foundJob.GetName())
			gatling.Status.RunnerCompleted = true
			if err := r.updateGatlingStatus(ctx, gatling); err != nil {
				log.Error(err, "Failed to update gatling status")
				return true, err
			}
			return true, nil
		} else {
			msg := fmt.Sprintf("Failed to complete runner job ( failed %d / backofflimit %d ). Please review logs", foundJob.Status.Failed, *foundJob.Spec.BackoffLimit)
			log.Error(nil, msg)
			gatling.Status.Error = msg
			if err := r.updateGatlingStatus(ctx, gatling); err != nil {
				return true, err
			}
			return false, errors.New(msg) // no longer requeue
		}
	}
	log.Info(fmt.Sprintf("Runner job is still running ( Job status: active=%d failed=%d succeeded=%d )", foundJob.Status.Active, foundJob.Status.Failed, foundJob.Status.Succeeded))
	if err := r.updateGatlingStatus(ctx, gatling); err != nil {
		log.Error(err, "Failed to update gatling status, but not requeue") // NOTE: this isn't critical
		return true, err
	}
	return true, nil
}

// Implementation of reconciler logic for the reporter job
func (r *GatlingReconciler) gatlingReporterReconcile(ctx context.Context, req ctrl.Request, gatling *gatlingv1alpha1.Gatling, log logr.Logger) (bool, error) {
	// Check if cloud storage info is given, and skip the reporter job if prerequistes are not made
	if r.getCloudStorageProvider(gatling) == "" || (r.getCloudStorageRegion(gatling) == "" && r.getCloudStorageProvider(gatling) == "aws") || r.getCloudStorageBucket(gatling) == "" {
		log.Error(nil, "Minimum cloud storage info is not given, thus skip reporting reconcile, and requeue")
		gatling.Status.ReportCompleted = true
		gatling.Status.NotificationCompleted = false
		if err := r.updateGatlingStatus(ctx, gatling); err != nil {
			return true, err
		}
		return true, nil
	}

	storagePath, _, err := r.getCloudStorageInfo(ctx, gatling)
	if err != nil {
		log.Error(err, "Failed to get gatling storage info, and requeue")
		return true, err
	}
	if gatling.Status.ReporterJobName == "" {
		// Define and crewate new Job object
		reporterJob := r.newGatlingReporterJobForCR(gatling, storagePath, log)
		if err := r.createObject(ctx, gatling, reporterJob); err != nil {
			log.Error(err, fmt.Sprintf("Failed to creating new reporter job for gatling %s", gatling.Name))
			return true, err
		}
		// Update Status
		gatling.Status.ReporterStartTime = utils.GetEpocTime()
		gatling.Status.ReporterJobName = reporterJob.GetName()
		gatling.Status.ReportCompleted = false
		gatling.Status.NotificationCompleted = false
		if err := r.updateGatlingStatus(ctx, gatling); err != nil {
			return true, err
		}
	}
	foundJob := &batchv1.Job{}
	err = r.Get(ctx, client.ObjectKey{Name: gatling.Status.ReporterJobName, Namespace: req.Namespace}, foundJob)
	if err != nil && apierr.IsNotFound(err) {
		// Check if the job runs out of time in creating the job
		duration := utils.GetEpocTime() - gatling.Status.ReporterStartTime
		if duration > maxJobCreationWaitTimeInSeconds {
			msg := fmt.Sprintf("Runs out of time (%d sec) in creating the reporter job", maxJobCreationWaitTimeInSeconds)
			log.Error(err, msg, "namespace", req.Namespace, "name", gatling.Status.ReporterJobName)
			gatling.Status.Error = msg
			if err := r.updateGatlingStatus(ctx, gatling); err != nil {
				return true, err
			}
			return false, err // no longer requeue
		}
		log.Info("The reporter job has not been created yet, and requeue", "namespace", req.Namespace, "name", gatling.Status.ReporterJobName)
		return true, err
	} else if err != nil {
		log.Error(err, "Failed to get the reporter job, and requeue", "namespace", req.Namespace, "name", gatling.Status.ReporterJobName)
		return true, err
	}
	// Check if the job runs out of time in running the job
	duration := utils.GetEpocTime() - gatling.Status.ReporterStartTime
	if duration > maxJobRunWaitTimeInSeconds {
		msg := fmt.Sprintf("Runs out of time (%d sec) in running the reporter job, and no longer requeue", maxJobCreationWaitTimeInSeconds)
		log.Error(nil, msg, "namespace", req.Namespace, "name", gatling.Status.ReporterJobName)
		gatling.Status.Error = msg
		if err := r.updateGatlingStatus(ctx, gatling); err != nil {
			return true, err
		}
		return false, errors.New(msg) // no longer requeue
	}
	// Check if the reporter job has completed
	log.Info("Check if the reporter job has completed", "namespace", foundJob.GetNamespace(), "name", foundJob.GetName())
	if r.isJobCompleted(foundJob) {
		if foundJob.Status.Succeeded == 1 {
			log.Info(fmt.Sprintf("Job has successfuly completed! ( successded %d )", foundJob.Status.Succeeded), "namespace", foundJob.GetNamespace(), "name", foundJob.GetName())
			gatling.Status.ReportCompleted = true
			if err := r.updateGatlingStatus(ctx, gatling); err != nil {
				log.Error(err, "Failed to update gatling status, but not requeue")
				return true, err
			}
			return true, nil
		} else {
			msg := fmt.Sprintf("Failed to complete reporter job( failed %d / backofflimit %d ). Please review logs", foundJob.Status.Failed, *foundJob.Spec.BackoffLimit)
			log.Error(nil, msg)
			gatling.Status.Error = msg
			if err := r.updateGatlingStatus(ctx, gatling); err != nil {
				return true, err
			}
			return false, errors.New(msg) // no longer requeue
		}
	}
	log.Info(fmt.Sprintf("Reporter job is still running ( Job status: active=%d failed=%d succeeded=%d )", foundJob.Status.Active, foundJob.Status.Failed, foundJob.Status.Succeeded))
	return true, nil
}

// Implementation of reconciler logic for the notification
func (r *GatlingReconciler) gatlingNotificationReconcile(ctx context.Context, req ctrl.Request, gatling *gatlingv1alpha1.Gatling, log logr.Logger) (bool, error) {
	var reportURL = "none"
	// Get cloud storage info only if gatling.spec.generateReport is true
	if gatling.Spec.GenerateReport {
		_, url, err := r.getCloudStorageInfo(ctx, gatling)
		if err != nil {
			log.Error(err, "Failed to get gatling storage info, and requeue")
			return true, err
		}
		reportURL = url
	}
	if err := r.sendNotification(ctx, gatling, reportURL); err != nil {
		log.Error(err, "Failed to sendNotification, but and requeue")
		return true, err
	}
	// Update gatling status on notification
	gatling.Status.NotificationCompleted = true
	if err := r.updateGatlingStatus(ctx, gatling); err != nil {
		log.Error(err, "Failed to update gatling status, and requeue")
		return true, err
	}
	log.Info("Notification has successfully been sent!")
	return true, nil
}

func doRequeue(requeueAfter time.Duration, err error) (ctrl.Result, error) {
	return ctrl.Result{Requeue: true, RequeueAfter: requeueAfter}, err
}

func doNotRequeue(err error) (ctrl.Result, error) {
	return ctrl.Result{}, err
}

func (r *GatlingReconciler) newConfigMapForCR(gatling *gatlingv1alpha1.Gatling, configMapName string, configMapData *map[string]string) *corev1.ConfigMap {
	labels := map[string]string{
		"app": gatling.Name,
	}
	data := map[string]string{}
	if configMapData != nil {
		data = *configMapData
	}
	return &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      configMapName,
			Namespace: gatling.Namespace,
			Labels:    labels,
		},
		Data: data,
	}
}

func (r *GatlingReconciler) newGatlingRunnerJobForCR(gatling *gatlingv1alpha1.Gatling, storagePath string, log logr.Logger) *batchv1.Job {
	labels := map[string]string{
		"app": gatling.Name,
	}

	gatlingWaiterCommand := getGatlingWaiterCommand(
		r.getGatlingRunnerJobParallelism(gatling),
		gatling.Namespace,
		gatling.Name,
	)

	gatlingRunnerCommand := getGatlingRunnerCommand(
		r.getSimulationsDirectoryPath(gatling),
		r.getTempSimulationsDirectoryPath(gatling),
		r.getResourcesDirectoryPath(gatling),
		r.getResultsDirectoryPath(gatling),
		r.getGatlingRunnerJobStartTime(gatling),
		gatling.Spec.TestScenarioSpec.SimulationClass,
		r.getGenerateLocalReport(gatling))
	log.Info("gatlingRunnerCommand:", "comand", gatlingRunnerCommand)

	envVars := []corev1.EnvVar{}
	if &gatling.Spec.TestScenarioSpec != nil && &gatling.Spec.TestScenarioSpec.Env != nil {
		envVars = gatling.Spec.TestScenarioSpec.Env
	}
	if gatling.Spec.GenerateReport {
		gatlingTransferResultCommand := getGatlingTransferResultCommand(
			r.getResultsDirectoryPath(gatling),
			r.getCloudStorageProvider(gatling),
			r.getCloudStorageRegion(gatling),
			storagePath)
		log.Info("gatlingTransferResultCommand:", "command", gatlingTransferResultCommand)
		cloudStorageEnvVars := []corev1.EnvVar{}
		if &gatling.Spec.CloudStorageSpec != nil && &gatling.Spec.CloudStorageSpec.Env != nil {
			cloudStorageEnvVars = gatling.Spec.CloudStorageSpec.Env
		}
		return &batchv1.Job{
			ObjectMeta: metav1.ObjectMeta{
				Name:      gatling.Name + "-runner",
				Namespace: gatling.Namespace,
				Labels:    labels,
			},
			Spec: batchv1.JobSpec{
				Parallelism: r.getGatlingRunnerJobParallelism(gatling),
				Completions: r.getGatlingRunnerJobParallelism(gatling),
				Template: corev1.PodTemplateSpec{
					Spec: corev1.PodSpec{
						Affinity:           r.getPodAffinity(gatling),
						Tolerations:        r.getPodTolerations(gatling),
						ServiceAccountName: r.getPodServiceAccountName(gatling),
						InitContainers: []corev1.Container{
							{
								Name:      "gatling-waiter",
								Image:     "bitnami/kubectl:1.21.8",
								Command:   []string{"/bin/sh", "-c"},
								Args:      []string{gatlingWaiterCommand},
								Resources: r.getPodResources(gatling),
								VolumeMounts: []corev1.VolumeMount{
									{
										Name:      "pod-info",
										MountPath: "/etc/pod-info",
									},
								},
							},
							{
								Name:         "gatling-runner",
								Image:        r.getGatlingContainerImage(gatling),
								Command:      []string{"/bin/sh", "-c"},
								Args:         []string{gatlingRunnerCommand},
								Env:          envVars,
								Resources:    r.getPodResources(gatling),
								VolumeMounts: r.getGatlingRunnerJobVolumeMounts(gatling),
							},
						},
						Containers: []corev1.Container{
							{
								Name:    "gatling-result-transferer",
								Image:   r.getRcloneContainerImage(gatling),
								Command: []string{"/bin/sh", "-c"},
								Args:    []string{gatlingTransferResultCommand},
								Env:     cloudStorageEnvVars,
								VolumeMounts: []corev1.VolumeMount{
									{
										Name:      "results-data-volume",
										MountPath: r.getResultsDirectoryPath(gatling),
									},
								},
							},
						},
						RestartPolicy: "Never",
						Volumes:       r.getGatlingRunnerJobVolumes(gatling),
					},
				},
			},
		}
	}

	return &batchv1.Job{
		ObjectMeta: metav1.ObjectMeta{
			Name:      gatling.Name + "-runner",
			Namespace: gatling.Namespace,
			Labels:    labels,
		},
		Spec: batchv1.JobSpec{
			Parallelism: &gatling.Spec.TestScenarioSpec.Parallelism,
			Completions: &gatling.Spec.TestScenarioSpec.Parallelism,
			Template: corev1.PodTemplateSpec{
				Spec: corev1.PodSpec{
					Affinity:           r.getPodAffinity(gatling),
					Tolerations:        r.getPodTolerations(gatling),
					ServiceAccountName: r.getPodServiceAccountName(gatling),
					InitContainers: []corev1.Container{
						{
							Name:      "gatling-waiter",
							Image:     "bitnami/kubectl:1.21.8",
							Command:   []string{"/bin/sh", "-c"},
							Args:      []string{gatlingWaiterCommand},
							Resources: r.getPodResources(gatling),
							VolumeMounts: []corev1.VolumeMount{
								{
									Name:      "pod-info",
									MountPath: "/etc/pod-info",
								},
							},
						},
					},
					Containers: []corev1.Container{
						{
							Name:         "gatling-runner",
							Image:        r.getGatlingContainerImage(gatling),
							Command:      []string{"/bin/sh", "-c"},
							Args:         []string{gatlingRunnerCommand},
							Env:          envVars,
							Resources:    r.getPodResources(gatling),
							VolumeMounts: r.getGatlingRunnerJobVolumeMounts(gatling),
						},
					},
					RestartPolicy: "Never",
					Volumes:       r.getGatlingRunnerJobVolumes(gatling),
				},
			},
		},
	}
}

func (r *GatlingReconciler) newGatlingReporterJobForCR(gatling *gatlingv1alpha1.Gatling, storagePath string, log logr.Logger) *batchv1.Job {
	labels := map[string]string{
		"app": gatling.Name,
	}
	gatlingAggregateResultCommand := getGatlingAggregateResultCommand(
		r.getResultsDirectoryPath(gatling),
		r.getCloudStorageProvider(gatling),
		r.getCloudStorageRegion(gatling),
		storagePath)
	log.Info("gatlingAggregateResultCommand", "command", gatlingAggregateResultCommand)

	gatlingGenerateReportCommand := getGatlingGenerateReportCommand(r.getResultsDirectoryPath(gatling))
	log.Info("gatlingGenerateReportCommand", "command", gatlingGenerateReportCommand)

	gatlingTransferReportCommand := getGatlingTransferReportCommand(
		r.getResultsDirectoryPath(gatling),
		r.getCloudStorageProvider(gatling),
		r.getCloudStorageRegion(gatling),
		storagePath)
	log.Info("gatlingTransferReportCommand", "command", gatlingTransferReportCommand)

	cloudStorageEnvVars := []corev1.EnvVar{}
	if &gatling.Spec.CloudStorageSpec != nil && &gatling.Spec.CloudStorageSpec.Env != nil {
		cloudStorageEnvVars = gatling.Spec.CloudStorageSpec.Env
	}
	//Non parallel job
	return &batchv1.Job{
		ObjectMeta: metav1.ObjectMeta{
			Name:      gatling.Name + "-reporter",
			Namespace: gatling.Namespace,
			Labels:    labels,
		},
		Spec: batchv1.JobSpec{
			Template: corev1.PodTemplateSpec{
				Spec: corev1.PodSpec{
					Affinity:           r.getPodAffinity(gatling),
					Tolerations:        r.getPodTolerations(gatling),
					ServiceAccountName: r.getPodServiceAccountName(gatling),
					InitContainers: []corev1.Container{
						{
							Name:    "gatling-result-aggregator",
							Image:   r.getRcloneContainerImage(gatling),
							Command: []string{"/bin/sh", "-c"},
							Args:    []string{gatlingAggregateResultCommand},
							Env:     cloudStorageEnvVars,
							VolumeMounts: []corev1.VolumeMount{
								{
									Name:      "aggregate-data-volume",
									MountPath: r.getResultsDirectoryPath(gatling),
								},
							},
						},
						{
							Name:    "gatling-report-generator",
							Image:   r.getGatlingContainerImage(gatling),
							Command: []string{"/bin/sh", "-c"},
							Args:    []string{gatlingGenerateReportCommand},
							VolumeMounts: []corev1.VolumeMount{
								{
									Name:      "aggregate-data-volume",
									MountPath: r.getResultsDirectoryPath(gatling),
								},
							},
						},
					},
					Containers: []corev1.Container{
						{
							Name:    "gatling-report-transferer",
							Image:   r.getRcloneContainerImage(gatling),
							Command: []string{"/bin/sh", "-c"},
							Args:    []string{gatlingTransferReportCommand},
							Env:     cloudStorageEnvVars,
							VolumeMounts: []corev1.VolumeMount{
								{
									Name:      "aggregate-data-volume",
									MountPath: r.getResultsDirectoryPath(gatling),
								},
							},
						},
					},
					RestartPolicy: "Never",
					Volumes: []corev1.Volume{
						{
							Name: "aggregate-data-volume",
							VolumeSource: corev1.VolumeSource{
								EmptyDir: &corev1.EmptyDirVolumeSource{},
							},
						},
					},
				},
			},
		},
	}
}

// VolumeMounts for Galling Job
func (r *GatlingReconciler) getGatlingRunnerJobVolumeMounts(gatling *gatlingv1alpha1.Gatling) []corev1.VolumeMount {
	volumeMounts := make([]corev1.VolumeMount, 0)
	if &gatling.Spec.TestScenarioSpec.SimulationData != nil && len(gatling.Spec.TestScenarioSpec.SimulationData) > 0 {
		volumeMounts = append(volumeMounts, corev1.VolumeMount{
			Name:      "simulations-data-volume",
			MountPath: r.getTempSimulationsDirectoryPath(gatling),
		})
	}
	if &gatling.Spec.TestScenarioSpec.ResourceData != nil && len(gatling.Spec.TestScenarioSpec.ResourceData) > 0 {
		volumeMounts = append(volumeMounts, corev1.VolumeMount{
			Name:      "resources-data-volume",
			MountPath: r.getResourcesDirectoryPath(gatling),
		})
	}
	if gatling.Spec.GenerateReport {
		volumeMounts = append(volumeMounts, corev1.VolumeMount{
			Name:      "results-data-volume",
			MountPath: r.getResultsDirectoryPath(gatling),
		})
	}
	return volumeMounts
}

// ConfigMap Volume Source
func (r *GatlingReconciler) getGatlingRunnerJobVolumes(gatling *gatlingv1alpha1.Gatling) []corev1.Volume {
	volumes := make([]corev1.Volume, 0)
	if &gatling.Spec.TestScenarioSpec.SimulationData != nil && len(gatling.Spec.TestScenarioSpec.SimulationData) > 0 {
		volumes = append(volumes, corev1.Volume{
			Name: "simulations-data-volume",
			VolumeSource: corev1.VolumeSource{
				ConfigMap: &corev1.ConfigMapVolumeSource{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: gatling.Name + "-simulations-data", //ConfigMap name
					},
				},
			},
		})
	}
	if &gatling.Spec.TestScenarioSpec.ResourceData != nil && len(gatling.Spec.TestScenarioSpec.ResourceData) > 0 {
		volumes = append(volumes, corev1.Volume{
			Name: "resources-data-volume",
			VolumeSource: corev1.VolumeSource{
				ConfigMap: &corev1.ConfigMapVolumeSource{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: gatling.Name + "-resources-data", //ConfigMap name
					},
				},
			},
		})
	}
	if gatling.Spec.GenerateReport {
		volumes = append(volumes, corev1.Volume{
			Name: "results-data-volume",
			VolumeSource: corev1.VolumeSource{
				EmptyDir: &corev1.EmptyDirVolumeSource{},
			},
		})
	}

	volumes = append(volumes, corev1.Volume{
		Name: "pod-info",
		VolumeSource: corev1.VolumeSource{
			DownwardAPI: &corev1.DownwardAPIVolumeSource{
				Items: []corev1.DownwardAPIVolumeFile{
					{
						Path: "name",
						FieldRef: &corev1.ObjectFieldSelector{
							FieldPath: "metadata.name",
						},
					},
				},
			},
		},
	})
	return volumes
}

func (r *GatlingReconciler) getCloudStorageInfo(ctx context.Context, gatling *gatlingv1alpha1.Gatling) (string, string, error) {
	var storagePath string
	var reportURL string
	if gatling.Status.ReportStoragePath != "" && gatling.Status.ReportUrl != "" {
		storagePath = gatling.Status.ReportStoragePath
		reportURL = gatling.Status.ReportUrl
	} else {
		// Assign new Gatling Cloud Storage Path and report URL,
		// and save them on Gatling Status fields
		subDir := fmt.Sprint(utils.Hash(fmt.Sprintf("%s%d", gatling.Name, rand.Intn(math.MaxInt32))))
		cspp := cloudstorages.GetProvider(r.getCloudStorageProvider(gatling))
		if cspp != nil {
			storagePath = (*cspp).GetCloudStoragePath(r.getCloudStorageBucket(gatling), gatling.Name, subDir)
			reportURL = (*cspp).GetCloudStorageReportURL(r.getCloudStorageBucket(gatling), gatling.Name, subDir)
		}
		gatling.Status.ReportStoragePath = storagePath
		gatling.Status.ReportUrl = reportURL
		if err := r.updateGatlingStatus(ctx, gatling); err != nil {
			return storagePath, reportURL, err
		}
	}
	return storagePath, reportURL, nil
}

func (r *GatlingReconciler) sendNotification(ctx context.Context, gatling *gatlingv1alpha1.Gatling, reportURL string) error {
	secretName := r.getNotificationServiceSecretName(gatling)
	foundSecret := &corev1.Secret{}
	if err := r.Get(ctx, client.ObjectKey{Name: secretName, Namespace: gatling.Namespace}, foundSecret); err != nil {
		// secret is not found or failed to get the secret
		return err
	}
	nspp := notificationservices.GetProvider(r.getNotificationServiceProvider(gatling))
	if nspp != nil {
		return (*nspp).Notify(gatling.Name, reportURL, foundSecret.Data)
	}
	return nil
}

func (r *GatlingReconciler) createObject(ctx context.Context, gatling *gatlingv1alpha1.Gatling, object client.Object) error {
	if err := r.Get(
		ctx,
		client.ObjectKey{Name: object.GetName(), Namespace: object.GetNamespace()},
		object); err != nil && apierr.IsNotFound(err) {
		// Set gatling instance as the owner and controller and controller
		if err := ctrl.SetControllerReference(gatling, object, r.Scheme); err != nil {
			return err
		}
		if err = r.Create(ctx, object); err != nil {
			return err
		}
		return nil // Object create successfully
	} else if err != nil {
		return err
	}
	return nil // Object already exists
}

func (r *GatlingReconciler) isJobCompleted(job *batchv1.Job) bool {
	for _, c := range job.Status.Conditions {
		if (c.Type == batchv1.JobComplete || c.Type == batchv1.JobFailed) && c.Status == corev1.ConditionTrue {
			return true
		}
	}
	return false
}

func (r *GatlingReconciler) cleanupJob(ctx context.Context, req ctrl.Request, jobName string) error {
	foundJob := &batchv1.Job{}
	if err := r.Get(ctx, client.ObjectKey{Name: jobName, Namespace: req.Namespace}, foundJob); err != nil {
		return err
	}
	if err := r.Delete(ctx, foundJob, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
		return err
	}
	return nil
}

func (r *GatlingReconciler) cleanupGatling(ctx context.Context, req ctrl.Request, gatlingName string) error {
	foundGatling := &gatlingv1alpha1.Gatling{}
	if err := r.Get(ctx, client.ObjectKey{Name: gatlingName, Namespace: req.Namespace}, foundGatling); err != nil {
		return err
	}
	if err := r.Delete(ctx, foundGatling, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
		return err
	}
	return nil
}

func (r *GatlingReconciler) updateGatlingStatus(ctx context.Context, gatling *gatlingv1alpha1.Gatling) error {
	if err := r.Status().Update(ctx, gatling); err != nil {
		return err
	}
	return nil
}

func (r *GatlingReconciler) dumpGatlingStatus(gatling *gatlingv1alpha1.Gatling, log logr.Logger) {
	log.Info(fmt.Sprintf("GatlingStatus: Active %d Succeeded %d Failed %d ReportCompleted %t NotificationCompleted %t ReportUrl %s Error %v",
		gatling.Status.Active,
		gatling.Status.Succeeded,
		gatling.Status.Failed,
		gatling.Status.ReportCompleted,
		gatling.Status.NotificationCompleted,
		gatling.Status.ReportUrl,
		gatling.Status.Error))
}

func (r *GatlingReconciler) isGatlingCompleted(gatling *gatlingv1alpha1.Gatling) bool {
	if !gatling.Status.RunnerCompleted ||
		(gatling.Spec.GenerateReport && !gatling.Status.ReportCompleted) ||
		(gatling.Spec.NotifyReport && !gatling.Status.NotificationCompleted) {
		return false
	}
	return true
}

func (r *GatlingReconciler) getCloudStorageProvider(gatling *gatlingv1alpha1.Gatling) string {
	provider := ""
	if &gatling.Spec.CloudStorageSpec != nil && gatling.Spec.CloudStorageSpec.Provider != "" {
		provider = gatling.Spec.CloudStorageSpec.Provider
	}
	return provider
}

func (r *GatlingReconciler) getCloudStorageRegion(gatling *gatlingv1alpha1.Gatling) string {
	region := ""
	if &gatling.Spec.CloudStorageSpec != nil && gatling.Spec.CloudStorageSpec.Region != "" {
		region = gatling.Spec.CloudStorageSpec.Region
	}
	return region
}

func (r *GatlingReconciler) getCloudStorageBucket(gatling *gatlingv1alpha1.Gatling) string {
	bucket := ""
	if &gatling.Spec.CloudStorageSpec != nil && gatling.Spec.CloudStorageSpec.Bucket != "" {
		bucket = gatling.Spec.CloudStorageSpec.Bucket
	}
	return bucket
}

func (r *GatlingReconciler) getNotificationServiceProvider(gatling *gatlingv1alpha1.Gatling) string {
	provider := defaultNotificationServiceProvider
	if &gatling.Spec.NotificationServiceSpec != nil && gatling.Spec.NotificationServiceSpec.Provider != "" {
		provider = gatling.Spec.NotificationServiceSpec.Provider
	}
	return provider
}

func (r *GatlingReconciler) getNotificationServiceSecretName(gatling *gatlingv1alpha1.Gatling) string {
	secretName := ""
	if &gatling.Spec.NotificationServiceSpec != nil && gatling.Spec.NotificationServiceSpec.SecretName != "" {
		secretName = gatling.Spec.NotificationServiceSpec.SecretName
	}
	return secretName
}

func (r *GatlingReconciler) getGatlingRunnerJobStartTime(gatling *gatlingv1alpha1.Gatling) string {
	var startTime string
	if &gatling.Spec.TestScenarioSpec != nil && gatling.Spec.TestScenarioSpec.StartTime != "" {
		startTime = gatling.Spec.TestScenarioSpec.StartTime
	}
	return startTime
}

func (r *GatlingReconciler) getGatlingRunnerJobParallelism(gatling *gatlingv1alpha1.Gatling) *int32 {
	var parallelism int32
	if &gatling.Spec.TestScenarioSpec != nil && gatling.Spec.TestScenarioSpec.Parallelism != 0 {
		parallelism = gatling.Spec.TestScenarioSpec.Parallelism
	}
	return &parallelism
}

func (r *GatlingReconciler) getGatlingContainerImage(gatling *gatlingv1alpha1.Gatling) string {
	image := defaultGatlingImage
	if &gatling.Spec.PodSpec != nil && gatling.Spec.PodSpec.GatlingImage != "" {
		image = gatling.Spec.PodSpec.GatlingImage
	}
	return image
}

func (r *GatlingReconciler) getRcloneContainerImage(gatling *gatlingv1alpha1.Gatling) string {
	image := defaultRcloneImage
	if &gatling.Spec.PodSpec != nil && gatling.Spec.PodSpec.RcloneImage != "" {
		image = gatling.Spec.PodSpec.RcloneImage
	}
	return image
}

func (r *GatlingReconciler) getPodResources(gatling *gatlingv1alpha1.Gatling) corev1.ResourceRequirements {
	resources := corev1.ResourceRequirements{}
	if &gatling.Spec.PodSpec != nil && &gatling.Spec.PodSpec.Resources != nil {
		resources = gatling.Spec.PodSpec.Resources
	}
	return resources
}

func (r *GatlingReconciler) getPodAffinity(gatling *gatlingv1alpha1.Gatling) *corev1.Affinity {
	affinity := corev1.Affinity{}
	if &gatling.Spec.PodSpec != nil && &gatling.Spec.PodSpec.Affinity != nil {
		affinity = gatling.Spec.PodSpec.Affinity
	}
	return &affinity
}

func (r *GatlingReconciler) getPodTolerations(gatling *gatlingv1alpha1.Gatling) []corev1.Toleration {
	tolerations := []corev1.Toleration{}
	if &gatling.Spec.PodSpec != nil && &gatling.Spec.PodSpec.Tolerations != nil {
		tolerations = gatling.Spec.PodSpec.Tolerations
	}
	return tolerations
}

func (r *GatlingReconciler) getPodServiceAccountName(gatling *gatlingv1alpha1.Gatling) string {
	serviceAccountName := ""
	if &gatling.Spec.PodSpec != nil && &gatling.Spec.PodSpec.ServiceAccountName != nil {
		serviceAccountName = gatling.Spec.PodSpec.ServiceAccountName
	}
	return serviceAccountName
}

func (r *GatlingReconciler) getSimulationsDirectoryPath(gatling *gatlingv1alpha1.Gatling) string {
	path := defaultSimulationsDirectoryPath
	if &gatling.Spec.TestScenarioSpec != nil && gatling.Spec.TestScenarioSpec.SimulationsDirectoryPath != "" {
		path = gatling.Spec.TestScenarioSpec.SimulationsDirectoryPath
	}
	return path
}

func (r *GatlingReconciler) getTempSimulationsDirectoryPath(gatling *gatlingv1alpha1.Gatling) string {
	return fmt.Sprintf("%s-temp", r.getSimulationsDirectoryPath(gatling))
}

func (r *GatlingReconciler) getResourcesDirectoryPath(gatling *gatlingv1alpha1.Gatling) string {
	path := defaultResourcesDirectoryPath
	if &gatling.Spec.TestScenarioSpec != nil && gatling.Spec.TestScenarioSpec.ResourcesDirectoryPath != "" {
		path = gatling.Spec.TestScenarioSpec.ResourcesDirectoryPath
	}
	return path
}

func (r *GatlingReconciler) getResultsDirectoryPath(gatling *gatlingv1alpha1.Gatling) string {
	path := defaultResultsDirectoryPath
	if &gatling.Spec.TestScenarioSpec != nil && gatling.Spec.TestScenarioSpec.ResultsDirectoryPath != "" {
		path = gatling.Spec.TestScenarioSpec.ResultsDirectoryPath
	}
	return path
}

func (r *GatlingReconciler) getGenerateLocalReport(gatling *gatlingv1alpha1.Gatling) bool {
	if &gatling.Spec.GenerateLocalReport == nil {
		return false
	}
	return gatling.Spec.GenerateLocalReport
}

// SetupWithManager sets up the controller with the Manager.
func (r *GatlingReconciler) SetupWithManager(mgr ctrl.Manager, options controller.Options) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&gatlingv1alpha1.Gatling{}).
		WithEventFilter(predicate.Funcs{
			DeleteFunc: func(e event.DeleteEvent) bool {
				// Suppress Delete events as we don't take any action in the reconciliation loop
				// when invoked after the gatlingv1alpha1.Gatling is actually deleted
				return false
			},
		}).
		WithOptions(options).
		Complete(r)
}


<|start_filename|>controllers/suite_test.go<|end_filename|>
/*
Copyright &copy; ZOZO, Inc.

Licensed 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.
*/

package controllers

import (
	"context"
	"math/rand"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"

	gatling1alpha1 "github.com/st-tech/gatling-operator/api/v1alpha1"
	//+kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment

func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecsWithDefaultAndCustomReporters(t,
		"Controller Suite",
		[]Reporter{printer.NewlineReporter{}})
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: true,
	}

	// Start etcd and kube-apiserver
	var err error
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	err = gatling1alpha1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	//+kubebuilder:scaffold:scheme

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

}, 60)

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	// End etcd and kube-apiserver
	err := testEnv.Stop()
	Expect(err).NotTo(HaveOccurred())
})

/*
* The following functions refer to
* https://github.com/jetstack/kubebuilder-sample-controller/blob/b39c135ee30d23aa8d8dbeaee60c2fb3639c6f65/controllers/suite_test.go
 */
// SetupTest will set up a testing environment.
// This includes:
// * creating a Namespace to be used during the test
// * starting the 'GatlingReconciler'
// * stopping the 'GatlingReconciler' after the test ends
// Call this function at the start of each of your tests.
func SetupTest(ctx context.Context) *corev1.Namespace {
	var stopFunc func()
	ns := &corev1.Namespace{}

	BeforeEach(func() {
		*ns = corev1.Namespace{
			ObjectMeta: metav1.ObjectMeta{Name: "testns-" + randStringRunes(5)},
		}

		err := k8sClient.Create(ctx, ns)
		Expect(err).NotTo(HaveOccurred(), "failed to create test namespace")

		time.Sleep(100 * time.Millisecond)

		mgr, err := ctrl.NewManager(cfg, ctrl.Options{})
		Expect(err).NotTo(HaveOccurred(), "failed to create manager")

		controllers := &GatlingReconciler{
			Client: mgr.GetClient(),
			Scheme: mgr.GetScheme(),
		}

		err = controllers.SetupWithManager(mgr, controller.Options{MaxConcurrentReconciles: 1})
		Expect(err).NotTo(HaveOccurred(), "failed to setup controller")

		ctx, cancel := context.WithCancel(ctx)
		stopFunc = cancel
		go func() {
			err := mgr.Start(ctx)
			Expect(err).NotTo(HaveOccurred(), "failed to start manager")
		}()
		time.Sleep(100 * time.Millisecond)
	})

	AfterEach(func() {
		stopFunc()
		err := k8sClient.Delete(ctx, ns)
		Expect(err).NotTo(HaveOccurred(), "failed to delete test namespace")
	})

	return ns
}

func init() {
	rand.Seed(time.Now().UnixNano())
}

var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890")

func randStringRunes(n int) string {
	b := make([]rune, n)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}


<|start_filename|>controllers/gatling_controller_test.go<|end_filename|>
package controllers

import (
	"context"
	"fmt"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"sigs.k8s.io/controller-runtime/pkg/client"

	gatlingv1alpha1 "github.com/st-tech/gatling-operator/api/v1alpha1"
	batchv1 "k8s.io/api/batch/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"
	//+kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var _ = Context("Inside of a new namespace", func() {
	ctx := context.TODO()
	ns := SetupTest(ctx)
	gatlingName := "test-gatling"

	Describe("when no existing resources exist", func() {

		It("should create a new Gatling resource with the specified name and a runner Job", func() {
			gatling := &gatlingv1alpha1.Gatling{
				ObjectMeta: metav1.ObjectMeta{
					Name:      gatlingName,
					Namespace: ns.Name,
				},
				Spec: gatlingv1alpha1.GatlingSpec{
					GenerateReport:      false,
					NotifyReport:        false,
					CleanupAfterJobDone: false,
					TestScenarioSpec: gatlingv1alpha1.TestScenarioSpec{
						SimulationClass: "MyBasicSimulation",
					},
				},
			}
			err := k8sClient.Create(ctx, gatling)
			Expect(err).NotTo(HaveOccurred(), "failed to create test Gatling resource")

			job := &batchv1.Job{}
			Eventually(func() error {
				return k8sClient.Get(
					ctx, client.ObjectKey{Namespace: ns.Name, Name: gatlingName + "-runner"}, job)
			}).Should(Succeed())
			//fmt.Printf("parallelism = %d", *job.Spec.Parallelism)

			Expect(job.Spec.Parallelism).Should(Equal(pointer.Int32Ptr(1)))
			Expect(job.Spec.Completions).Should(Equal(pointer.Int32Ptr(1)))
		})

		It("should create a new Gatling resource with the specified name and a runner Job with 2 parallelism", func() {
			gatling := &gatlingv1alpha1.Gatling{
				ObjectMeta: metav1.ObjectMeta{
					Name:      gatlingName,
					Namespace: ns.Name,
				},
				Spec: gatlingv1alpha1.GatlingSpec{
					GenerateReport:      false,
					NotifyReport:        false,
					CleanupAfterJobDone: false,
					TestScenarioSpec: gatlingv1alpha1.TestScenarioSpec{
						SimulationClass: "MyBasicSimulation",
						Parallelism:     2,
					},
				},
			}
			err := k8sClient.Create(ctx, gatling)
			Expect(err).NotTo(HaveOccurred(), "failed to create test Gatling resource")

			job := &batchv1.Job{}
			Eventually(func() error {
				return k8sClient.Get(
					ctx, client.ObjectKey{Namespace: ns.Name, Name: gatlingName + "-runner"}, job)
			}).Should(Succeed())
			fmt.Printf("parallelism = %d", *job.Spec.Parallelism)

			Expect(job.Spec.Parallelism).Should(Equal(pointer.Int32Ptr(2)))
			Expect(job.Spec.Completions).Should(Equal(pointer.Int32Ptr(2)))
		})

	})
})


<|start_filename|>controllers/cloudstorages/gcp_test.go<|end_filename|>
package cloudstorages

import (
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("GetName", func() {
	var (
		provider      string
		expectedValue string
	)
	BeforeEach(func() {
		provider = "gcp"
		expectedValue = "gcp"
	})
	Context("Provider is gcp", func() {
		It("should get provider name = gcp", func() {
			csp := &GCPCloudStorageProvider{providerName: provider}
			Expect(csp.GetName()).To(Equal(expectedValue))
		})
	})
})

var _ = Describe("GetCloudStoragePath", func() {
	var (
		provider      string
		bucket        string
		gatlingName   string
		subDir        string
		expectedValue string
	)
	BeforeEach(func() {
		provider = "gcp"
		bucket = "testBucket"
		gatlingName = "testGatling"
		subDir = "subDir"
		expectedValue = "gs://testBucket/testGatling/subDir"
	})
	Context("Provider is gcp", func() {
		It("path is gcp gcs bucket", func() {
			csp := &GCPCloudStorageProvider{providerName: provider}
			Expect(csp.GetCloudStoragePath(bucket, gatlingName, subDir)).To(Equal(expectedValue))
		})
	})
})

var _ = Describe("GetCloudStorageReportURL", func() {
	var (
		provider      string
		bucket        string
		gatlingName   string
		subDir        string
		expectedValue string
	)
	BeforeEach(func() {
		provider = "gcp"
		bucket = "testBucket"
		gatlingName = "testGatling"
		subDir = "subDir"
		expectedValue = "https://storage.googleapis.com/testBucket/testGatling/subDir/index.html"
	})
	Context("Provider is gcp", func() {
		It("path is gcp gcs bucket", func() {
			csp := &GCPCloudStorageProvider{providerName: provider}
			Expect(csp.GetCloudStorageReportURL(bucket, gatlingName, subDir)).To(Equal(expectedValue))
		})
	})
})

var _ = Describe("GetGatlingTransferResultCommand", func() {
	var (
		provider             string
		resultsDirectoryPath string
		region               string
		storagePath          string
		expectedValue        string
	)
	BeforeEach(func() {
		provider = "gcp"
		resultsDirectoryPath = "testResultsDirectoryPath"
		region = ""
		storagePath = "testStoragePath"
		expectedValue = `
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
done
`
	})
	Context("Provider is gcp", func() {
		It("returns commands with gcs rclone config", func() {
			csp := &GCPCloudStorageProvider{providerName: provider}
			Expect(csp.GetGatlingTransferResultCommand(resultsDirectoryPath, region, storagePath)).To(Equal(expectedValue))
		})
	})
})

var _ = Describe("GetGatlingAggregateResultCommand", func() {
	var (
		provider             string
		resultsDirectoryPath string
		region               string
		storagePath          string
		expectedValue        string
	)
	BeforeEach(func() {
		provider = "gcp"
		resultsDirectoryPath = "testResultsDirectoryPath"
		region = ""
		storagePath = "testStoragePath"
		expectedValue = `
GATLING_AGGREGATE_DIR=testResultsDirectoryPath
# assumes gcs bucket using uniform bucket-level access control
rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive
rclone copy testStoragePath ${GATLING_AGGREGATE_DIR}
`
	})
	Context("Provider is gcp", func() {
		It("returns commands with gcs rclone config", func() {
			gcp := &GCPCloudStorageProvider{providerName: provider}
			Expect(gcp.GetGatlingAggregateResultCommand(resultsDirectoryPath, region, storagePath)).To(Equal(expectedValue))
		})
	})
})

var _ = Describe("GetGatlingTransferReportCommand", func() {
	var (
		provider             string
		resultsDirectoryPath string
		region               string
		storagePath          string
		expectedValue        string
	)
	BeforeEach(func() {
		provider = "gcp"
		resultsDirectoryPath = "testResultsDirectoryPath"
		region = ""
		storagePath = "testStoragePath"
		expectedValue = `
GATLING_AGGREGATE_DIR=testResultsDirectoryPath
# assumes gcs bucket using uniform bucket-level access control
rclone config create gs "google cloud storage" bucket_policy_only true --non-interactive
rclone copy ${GATLING_AGGREGATE_DIR} --exclude "*.log" testStoragePath
`
	})
	Context("Provider is gcp", func() {
		It("returns commands with gcs rclone config", func() {
			gcp := &GCPCloudStorageProvider{providerName: provider}
			Expect(gcp.GetGatlingTransferReportCommand(resultsDirectoryPath, region, storagePath)).To(Equal(expectedValue))
		})
	})
})


<|start_filename|>controllers/notificationservices/slack.go<|end_filename|>
package notificationservices

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"

	utils "github.com/st-tech/gatling-operator/controllers/utils"
)

type SlackNotificationServiceProvider struct {
	providerName string
}

func (p *SlackNotificationServiceProvider) GetName() string {
	return p.providerName
}

func (p *SlackNotificationServiceProvider) Notify(gatlingName string, reportURL string, secretData map[string][]byte) error {

	webhookURL, exists := utils.GetMapValue("incoming-webhook-url", secretData)
	if !exists {
		return errors.New("Insufficient secret data for slack: incoming-webhook-url is missing")
	}
	payloadTextFormat := `
[%s] Gatling has completed successfully!
Report URL: %s
`
	payloadText := fmt.Sprintf(payloadTextFormat, gatlingName, reportURL)
	if err := slackNotify(webhookURL, payloadText); err != nil {
		return err
	}
	return nil
}

type payload struct {
	Text string `json:"text"`
}

func slackNotify(webhookURL string, payloadText string) error {
	p := payload{
		Text: payloadText,
	}
	s, err := json.Marshal(p)
	if err != nil {
		return err
	}
	resp, err := http.PostForm(webhookURL,
		url.Values{"payload": {string(s)}})
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return nil
}


<|start_filename|>api/v1alpha1/zz_generated.deepcopy.go<|end_filename|>
// +build !ignore_autogenerated

/*
Copyright &copy; ZOZO, Inc.

Licensed 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.
*/

// Code generated by controller-gen. DO NOT EDIT.

package v1alpha1

import (
	"k8s.io/api/core/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CloudStorageSpec) DeepCopyInto(out *CloudStorageSpec) {
	*out = *in
	if in.Env != nil {
		in, out := &in.Env, &out.Env
		*out = make([]v1.EnvVar, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudStorageSpec.
func (in *CloudStorageSpec) DeepCopy() *CloudStorageSpec {
	if in == nil {
		return nil
	}
	out := new(CloudStorageSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Gatling) DeepCopyInto(out *Gatling) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	out.Status = in.Status
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Gatling.
func (in *Gatling) DeepCopy() *Gatling {
	if in == nil {
		return nil
	}
	out := new(Gatling)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Gatling) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GatlingList) DeepCopyInto(out *GatlingList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Gatling, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatlingList.
func (in *GatlingList) DeepCopy() *GatlingList {
	if in == nil {
		return nil
	}
	out := new(GatlingList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *GatlingList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GatlingSpec) DeepCopyInto(out *GatlingSpec) {
	*out = *in
	in.PodSpec.DeepCopyInto(&out.PodSpec)
	in.CloudStorageSpec.DeepCopyInto(&out.CloudStorageSpec)
	out.NotificationServiceSpec = in.NotificationServiceSpec
	in.TestScenarioSpec.DeepCopyInto(&out.TestScenarioSpec)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatlingSpec.
func (in *GatlingSpec) DeepCopy() *GatlingSpec {
	if in == nil {
		return nil
	}
	out := new(GatlingSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GatlingStatus) DeepCopyInto(out *GatlingStatus) {
	*out = *in
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatlingStatus.
func (in *GatlingStatus) DeepCopy() *GatlingStatus {
	if in == nil {
		return nil
	}
	out := new(GatlingStatus)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NotificationServiceSpec) DeepCopyInto(out *NotificationServiceSpec) {
	*out = *in
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NotificationServiceSpec.
func (in *NotificationServiceSpec) DeepCopy() *NotificationServiceSpec {
	if in == nil {
		return nil
	}
	out := new(NotificationServiceSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PodSpec) DeepCopyInto(out *PodSpec) {
	*out = *in
	in.Resources.DeepCopyInto(&out.Resources)
	in.Affinity.DeepCopyInto(&out.Affinity)
	if in.Tolerations != nil {
		in, out := &in.Tolerations, &out.Tolerations
		*out = make([]v1.Toleration, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodSpec.
func (in *PodSpec) DeepCopy() *PodSpec {
	if in == nil {
		return nil
	}
	out := new(PodSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TestScenarioSpec) DeepCopyInto(out *TestScenarioSpec) {
	*out = *in
	if in.SimulationData != nil {
		in, out := &in.SimulationData, &out.SimulationData
		*out = make(map[string]string, len(*in))
		for key, val := range *in {
			(*out)[key] = val
		}
	}
	if in.ResourceData != nil {
		in, out := &in.ResourceData, &out.ResourceData
		*out = make(map[string]string, len(*in))
		for key, val := range *in {
			(*out)[key] = val
		}
	}
	if in.GatlingConf != nil {
		in, out := &in.GatlingConf, &out.GatlingConf
		*out = make(map[string]string, len(*in))
		for key, val := range *in {
			(*out)[key] = val
		}
	}
	if in.Env != nil {
		in, out := &in.Env, &out.Env
		*out = make([]v1.EnvVar, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestScenarioSpec.
func (in *TestScenarioSpec) DeepCopy() *TestScenarioSpec {
	if in == nil {
		return nil
	}
	out := new(TestScenarioSpec)
	in.DeepCopyInto(out)
	return out
}
