Skip to content

Commit

Permalink
feat: setup script (#16)
Browse files Browse the repository at this point in the history
* setup script functionality

* add bash to image

* update regex in error

* EOF newlines
  • Loading branch information
andrewbenington committed Aug 16, 2023
1 parent 640792f commit ad6611d
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 48 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ unit-tests.xml
.env

# binary
release/
release/

# MacOS
.DS_STORE
15 changes: 8 additions & 7 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@ export const options = {
The following parameters are used to configure the image:

| Name | Description | Required | Default |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------- |
| `script_path` | path to the k6 script file. must be a JavaScript file satisfying the pattern `^([.]{0,2}/)?[a-zA-Z0-9-_/]\*[a-zA-Z0-9]\.js$`. | `true` | `N/A` |
| `output_path` | path to the output file that will be created. directories will be created as necessary. if empty, no output file will be generated. must be a JSON file satisfying the pattern `^([.]{0,2}/)?[a-zA-Z0-9-_/]\*[a-zA-Z0-9]\.json$`. | `false` | `N/A` |
| `fail_on_threshold_breach` | if `false`, the pipeline step will not fail even if thresholds are breached. | `false` | `true` |
| `projektor_compat_mode` | if `true`, output will be generated with the `--summary-output` flag instead of the `--out` flag. this is necessary for results uploaded to a [Projektor](https://projektor.dev/) server. | `false` | `false` |
| `log_progress` | if `true`, k6 progress bar output will print to the Vela pipeline. Not recommended for numerous or long-running tests, as logging becomes excessive. | `false` | `false` |
| Name | Description | Required | Default |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------- |
| `script_path` | path to the k6 script file. must be a JavaScript file satisfying the pattern `^(\./\|(\.\./)+)?[a-zA-Z0-9-_/]*[a-zA-Z0-9]\.js$`. | `true` | `N/A` |
| `output_path` | path to the output file that will be created. directories will be created as necessary. if empty, no output file will be generated. must be a JSON file satisfying the pattern `^(\./\|(\.\./)+)?[a-zA-Z0-9-_/]*[a-zA-Z0-9]\.json$`. | `false` | `N/A` |
| `setup_script_path` | path to an optional setup script file to be run before tests. must be a shell script (sh or bash) with execute permissions matching the pattern `^(\./\|(\.\./)+)?[a-zA-Z0-9-_/]*[a-zA-Z0-9]\.sh$`. | `false` | `N/A` |
| `fail_on_threshold_breach` | if `false`, the pipeline step will not fail even if thresholds are breached. | `false` | `true` |
| `projektor_compat_mode` | if `true`, output will be generated with the `--summary-output` flag instead of the `--out` flag. this is necessary for results uploaded to a [Projektor](https://projektor.dev/) server. | `false` | `false` |
| `log_progress` | if `true`, k6 progress bar output will print to the Vela pipeline. Not recommended for numerous or long-running tests, as logging becomes excessive. | `false` | `false` |
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ FROM docker.io/grafana/k6:0.46.0@sha256:2f40a302ec1e1e3cc96b9a3871bf5d7d4697e9ec

FROM alpine:3.18@sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4a54ba44a as certs

RUN apk add --update --no-cache ca-certificates
RUN apk add --update --no-cache ca-certificates bash

COPY --from=k6-image /usr/bin/k6 /usr/bin/k6

Expand Down
25 changes: 22 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ clean: clean-all go-tidy ## Clean up the application and test output
.PHONY: build
build: build-all ## Compile the application

.PHONY: build-docker
build-docker: ## Compile the application for testing locally with Docker
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o $(BIN_NAME)
.PHONY: docker
docker: build-linux docker-build ## Build a Docker image for local testing

.PHONY: test
test: test-all ## Run all unit tests
Expand Down Expand Up @@ -116,3 +115,23 @@ build-static-ci:
@echo
@echo "### Building CI static release/vela-k6 binary"
go build -a -ldflags '-s -w -extldflags "-static" ${LD_FLAGS}' -o $(BIN_LOCATION) $(BIN_NAME)

# The `build-linux` target is intended to compile
# the Go source code into a linux-compatible binary.
#
# Usage: `make build-linux`
.PHONY: build-linux
build-linux:
@echo
@echo "### Building release/vela-k6 binary for linux"
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -a -ldflags '${LD_FLAGS}' -o $(BIN_LOCATION) $(BIN_NAME)

# The `docker-build` target is intended to build
# the Docker image for the plugin.
#
# Usage: `make docker-build`
.PHONY: docker-build
docker-build: build-linux
@echo
@echo "### Building vela-k6:local image"
@docker build --no-cache -t vela-k6:local .
5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ func main() {
log.Fatalf("FATAL: %s\n", err)
}

err = plugin.RunSetupScript(cfg)
if err != nil {
log.Fatalf("FATAL: %s\n", err)
}

err = plugin.RunPerfTests(cfg)
if err != nil {
log.Fatalf("FATAL: %s\n", err)
Expand Down
7 changes: 1 addition & 6 deletions plugin/mock.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package plugin

import (
"fmt"
"io"
"os/exec"
"strings"
Expand All @@ -21,11 +20,7 @@ func (m *MockCommand) Wait() error {
}

func (m *MockCommand) String() (str string) {
for _, arg := range m.args {
str = fmt.Sprintf("%s %s", str, arg)
}

return
return ""
}

func (m *MockCommand) StdoutPipe() (io.ReadCloser, error) {
Expand Down
78 changes: 70 additions & 8 deletions plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ func checkOSStat(path string) error {
}

var (
validFilePattern = regexp.MustCompile(`^(\./|(\.\./)+)?[a-zA-Z0-9-_/]*[a-zA-Z0-9]\.(json|js)$`)
validJSFilePattern = regexp.MustCompile(`^(\./|(\.\./)+)?[a-zA-Z0-9-_/]*[a-zA-Z0-9]\.js$`)
validJSONFilePattern = regexp.MustCompile(`^(\./|(\.\./)+)?[a-zA-Z0-9-_/]*[a-zA-Z0-9]\.json$`)
validShellFilePattern = regexp.MustCompile(`^(\./|(\.\./)+)?[a-zA-Z0-9-_/]*[a-zA-Z0-9]\.sh$`)
// buildCommand can be swapped out for a mock function for unit testing.
buildCommand = buildExecCommand
// verifyFileExists can be swapped out for a mock function for unit testing.
Expand All @@ -55,23 +57,36 @@ var (
// output path is invalid, OutputPath is set to "".
func ConfigFromEnv() (*Config, error) {
cfg := &Config{}
cfg.ScriptPath = sanitizeFilePath(os.Getenv("PARAMETER_SCRIPT_PATH"))
cfg.OutputPath = sanitizeFilePath(os.Getenv("PARAMETER_OUTPUT_PATH"))
cfg.ScriptPath = sanitizeScriptPath(os.Getenv("PARAMETER_SCRIPT_PATH"))
cfg.OutputPath = sanitizeOutputPath(os.Getenv("PARAMETER_OUTPUT_PATH"))
cfg.SetupScriptPath = sanitizeSetupPath(os.Getenv("PARAMETER_SETUP_SCRIPT_PATH"))
cfg.FailOnThresholdBreach = !strings.EqualFold(os.Getenv("PARAMETER_FAIL_ON_THRESHOLD_BREACH"), "false")
cfg.ProjektorCompatMode = strings.EqualFold(os.Getenv("PARAMETER_PROJEKTOR_COMPAT_MODE"), "true")
cfg.LogProgress = strings.EqualFold(os.Getenv("PARAMETER_LOG_PROGRESS"), "true")

if cfg.ScriptPath == "" || !strings.HasSuffix(cfg.ScriptPath, ".js") {
return nil, fmt.Errorf("invalid script file. provide the filepath to a JavaScript file in plugin parameter 'script_path' (e.g. 'script_path: \"/k6-test/script.js\"'). the filepath must follow the regular expression `^[a-zA-Z0-9-_/]*[a-zA-Z0-9]+\\.(json|js)$`")
return nil, fmt.Errorf("invalid script file. provide the filepath to a JavaScript file in plugin parameter 'script_path' (e.g. 'script_path: \"/k6-test/script.js\"'). the filepath must follow the regular expression `%s`", validJSFilePattern)
}

return cfg, nil
}

// sanitizeFilePath returns the input string if it satisfies the pattern
// for a valid filepath, and an empty string otherwise.
func sanitizeFilePath(input string) string {
return validFilePattern.FindString(input)
// sanitizeScriptPath returns the input string if it satisfies the pattern
// for a valid JS filepath, and an empty string otherwise.
func sanitizeScriptPath(input string) string {
return validJSFilePattern.FindString(input)
}

// sanitizeOutputPath returns the input string if it satisfies the pattern
// for a valid JSON filepath, and an empty string otherwise.
func sanitizeOutputPath(input string) string {
return validJSONFilePattern.FindString(input)
}

// sanitizeSetupPath returns the input string if it satisfies the pattern
// for a valid .sh filepath, and an empty string otherwise.
func sanitizeSetupPath(input string) string {
return validShellFilePattern.FindString(input)
}

// buildK6Command returns a shellCommand that will execute K6 tests
Expand Down Expand Up @@ -103,6 +118,52 @@ func buildK6Command(cfg *Config) (cmd shellCommand, err error) {
return
}

// RunSetupScript runs the setup script located at the cfg.SetupScriptPath
// if the path is not empty.
func RunSetupScript(cfg *Config) error {
if cfg.SetupScriptPath == "" {
log.Println("No setup script specified, skipping.")
return nil
}

err := verifyFileExists(cfg.SetupScriptPath)
if err != nil {
return fmt.Errorf("read setup script file at %s: %w", cfg.SetupScriptPath, err)
}

cmd := buildCommand(cfg.SetupScriptPath)

stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("get stdout pipe: %w", err)
}

stderr, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("get stderr pipe: %w", err)
}

if err := cmd.Start(); err != nil {
return fmt.Errorf("start command: %w", err)
}

log.Println("Running setup script...")

wg := sync.WaitGroup{}
wg.Add(2)

go readLinesFromPipe(stdout, &wg)
go readLinesFromPipe(stderr, &wg)
wg.Wait()

err = cmd.Wait()
if err != nil {
return fmt.Errorf("run setup script: %w", err)
}

return nil
}

// RunPerfTests runs the K6 performance test script located at the
// cfg.ScriptPath and saves the output to cfg.OutputPath if it is present
// and a valid filepath.
Expand Down Expand Up @@ -184,6 +245,7 @@ func readLinesFromPipe(pipe io.ReadCloser, wg *sync.WaitGroup) {
type Config struct {
ScriptPath string
OutputPath string
SetupScriptPath string
FailOnThresholdBreach bool
ProjektorCompatMode bool
LogProgress bool
Expand Down
Loading

0 comments on commit ad6611d

Please sign in to comment.