From 02d1c6f2f44ad0862cd32ebc96ac48b46d3c97c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20SZKIBA?= Date: Fri, 5 Jul 2024 16:43:49 +0200 Subject: [PATCH] feat: reintroduce k6exec CLI + Docker image --- .github/workflows/release.yml | 41 ++++++ .gitignore | 5 +- .goreleaser.yaml | 103 ++++++++++++++ Dockerfile.goreleaser | 12 ++ README.md | 252 ++++++++++++++++++++++++++++++++++ cmd/cmd.go | 135 ++++++++++++++++++ cmd/help.md | 153 +++++++++++++++++++++ cmd/k6exec/error.go | 41 ++++++ cmd/k6exec/main.go | 57 ++++++++ cmd/state.go | 137 ++++++++++++++++++ go.mod | 11 +- go.sum | 20 +++ releases/v0.1.2.md | 5 + test.db | Bin 0 -> 12288 bytes tools/gendoc/main.go | 12 ++ 15 files changed, 982 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.yaml create mode 100644 Dockerfile.goreleaser create mode 100644 cmd/cmd.go create mode 100644 cmd/help.md create mode 100644 cmd/k6exec/error.go create mode 100644 cmd/k6exec/main.go create mode 100644 cmd/state.go create mode 100644 releases/v0.1.2.md create mode 100644 test.db create mode 100644 tools/gendoc/main.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..37c28de --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +name: release +on: + push: + tags: + - "v*" +permissions: + contents: write + packages: write + +jobs: + release: + runs-on: ubuntu-latest + env: + REGISTRY: ghcr.io + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.22.2" + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: "amd64,arm64" + - name: Login to GitHub packages + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: "2.0.1" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index d7f283e..2800dc7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ /node_modules /package.json /yarn.lock -/coverage.txt \ No newline at end of file +/coverage.txt +/k6exec +/k6exec.exe +/dist diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..d02c158 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,103 @@ +project_name: k6exec +version: 2 +env: + - IMAGE_OWNER=ghcr.io/grafana +before: + hooks: + - go mod tidy +builds: + - env: + - CGO_ENABLED=0 + goos: ["darwin", "linux", "windows"] + goarch: ["amd64", "arm64"] + ldflags: + - "-s -w -X main.version={{.Version}} -X main.appname={{.ProjectName}}" + dir: cmd/k6exec +source: + enabled: true + name_template: "{{ .ProjectName }}_{{ .Version }}_source" + +archives: + - id: bundle + format: tar.gz + format_overrides: + - goos: windows + format: zip + +checksum: + name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" + +snapshot: + name_template: "{{ incpatch .Version }}-next+{{.ShortCommit}}{{if .IsGitDirty}}.dirty{{else}}{{end}}" + +changelog: + sort: asc + abbrev: -1 + filters: + exclude: + - "^chore:" + - "^docs:" + - "^test:" + +git: + ignore_tags: + - "nightly" + - "before-grafana" + +dockers: + - id: amd64 + dockerfile: Dockerfile.goreleaser + use: buildx + image_templates: + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:{{ .Tag }}-amd64" + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:v{{ .Major }}-amd64" + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:v{{ .Major }}.{{ .Minor }}-amd64" + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:latest-amd64" + + build_flag_templates: + - "--platform=linux/amd64" + - "--pull" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.licenses=AGPL-3.0-only" + - id: arm64 + dockerfile: Dockerfile.goreleaser + use: buildx + image_templates: + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:{{ .Tag }}-arm64" + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:v{{ .Major }}-arm64" + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:v{{ .Major }}.{{ .Minor }}-arm64" + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:latest-arm64" + + build_flag_templates: + - "--platform=linux/arm64" + - "--pull" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--label=org.opencontainers.image.licenses=AGPL-3.0-only" + +docker_manifests: + - id: tag + name_template: "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:{{ .Tag }}" + image_templates: + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:{{ .Tag }}-amd64" + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:{{ .Tag }}-arm64" + - id: major + name_template: "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:v{{ .Major }}" + image_templates: + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:v{{ .Major }}-amd64" + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:v{{ .Major }}-arm64" + - id: major-minor + name_template: "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:v{{ .Major }}.{{ .Minor }}" + image_templates: + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:v{{ .Major }}.{{ .Minor }}-amd64" + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:v{{ .Major }}.{{ .Minor }}-arm64" + - id: latest + name_template: "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:latest" + image_templates: + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:latest-amd64" + - "{{ .Env.IMAGE_OWNER }}/{{ .ProjectName }}:latest-arm64" diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser new file mode 100644 index 0000000..2123d4d --- /dev/null +++ b/Dockerfile.goreleaser @@ -0,0 +1,12 @@ +FROM debian:12.6-slim +VOLUME /cache +VOLUME /state +RUN adduser -D -u 1000 -g 1000 -h /home/k6exec k6exec && \ + mkdir -p /cache/k6exec /state/k6exec && \ + chown -R 1000:1000 /cache /state +ENV CGO_ENABLED=1 XDG_CACHE_HOME="/cache" XDG_STATE_HOME="/state" +COPY k6exec /usr/bin/k6exec + +USER 1000 +WORKDIR /home/k6exec +ENTRYPOINT ["k6exec"] diff --git a/README.md b/README.md index 6274d4f..13511fa 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ +[![GitHub Release](https://img.shields.io/github/v/release/grafana/k6exec)](https://github.com/grafana/k6exec/releases/) [![Go Reference](https://pkg.go.dev/badge/github.com/grafana/k6exec.svg)](https://pkg.go.dev/github.com/grafana/k6exec) [![Go Report Card](https://goreportcard.com/badge/github.com/grafana/k6exec)](https://goreportcard.com/report/github.com/grafana/k6exec) [![GitHub Actions](https://github.com/grafana/k6exec/actions/workflows/test.yml/badge.svg)](https://github.com/grafana/k6exec/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/grafana/k6exec/graph/badge.svg?token=6MP3G02V9C)](https://codecov.io/gh/grafana/k6exec) +![GitHub Downloads](https://img.shields.io/github/downloads/grafana/k6exec/total)

k6exec

@@ -9,6 +11,208 @@ k6exec is a launcher library for k6 with seamless use of extensions. The launcher will always run the k6 test script with the appropriate k6 binary, which contains the extensions used by the script. Extensions can also be recognized from the environment variable (default `K6_DEPENDENCIES`) or from the `dependencies` property of the manifest file. +k6exec is primarily used as a go library. In addition, it also contains a command-line tool, which is suitable for listing the dependencies of k6 test scripts. + +The command line tool can be integrated into other command line tools as a subcommand. For this purpose, the library also contains the functionality of the command line tool as a factrory function that returns [cobra.Command](https://pkg.go.dev/github.com/spf13/cobra#Command). + +## Install + +Precompiled binaries can be downloaded and installed from the [Releases](https://github.com/grafana/k6exec/releases) page. + +If you have a go development environment, the installation can also be done with the following command: + +``` +go install github.com/grafana/k6exec/cmd/k6exec@latest +``` + +Docker images are available on the GitHub [Packages](https://github.com/grafana/k6exec/pkgs/container/k6exec): + +``` +docker pull ghcr.io/grafana/k6x:latest +``` + +## Usage + + +## k6exec + +Run k6 with extensions + +### Synopsis + +Run k6 with a seamless extension user experience. + +`k6exec` is a [k6] launcher that automatically provides [k6] with the [extensions] used by the test. In order to do this, it analyzes the script arguments of the `run` and `archive` subcommands, detects the extensions to be used and their version constraints. + +The launcher acts as a drop-in replacement for the `k6` command. For more convenient use, it is advisable to create an alias or shell script called `k6` for the launcher. The alias can be used in exactly the same way as the `k6` command, with the difference that it generates the real `k6` on the fly based on the extensions you want to use. + +Any k6 command can be used. Use the `help` command to list the available k6 commands. + +Since k6exec tries to emulate the `k6` command line, the `help` command or the `--help` flag cannot be used to display help from `k6exec` command itself. The `k6exec` help can be displayed using the `--usage` flag: + + k6exec --usage + +### Prerequisites + +k6exec tries to provide the appropriate k6 executable after detecting the extension dependencies. This can be done using a build service or a native builder. + +#### Build Service + +No additional installation is required to use the build service, just provide the build service URL. + +The build service URL can be specified in the `K6_BUILD_SERVICE_URL` environment variable or by using the `--build-service-url` flag. + +There is no default URL for the build service, otherwise k6exec will automatically provide k6 with the native builder. + +#### Native Builder + +To use the native builder, you only need to install the [Go language toolkit](https://go.dev/doc/install). + +The native builder uses a k6 extension catalog to resolve extension URLs and versions. The extension catalog URL has a default value. A different extension catalog URL can be specified in the `K6_EXTENSION_CATALOG_URL` environment variable or by using the `--extension-catalog-url` flag. + +### Pragma + +Version constraints can be specified using the JavaScript `"use ..."` pragma syntax for k6 and extensions. Put the following lines at the beginning of the test script: + +```js +"use k6 >= v0.52"; +"use k6 with k6/x/faker > 0.2"; +``` + +Any number of `"use k6"` pragmas can be used. + +> **Note** +> The use of pragmas is completely optional for JavaScript type extensions, it is only necessary if you want to specify version constraints. + +The pragma syntax can also be used to specify an extension dependency that is not referenced in an import expression. A typical example of this is the Output type extension such as [xk6-top]: + +```js +"use k6 with top >= 0.1"; +``` + +Read the version constraints syntax in the [Version Constraints](#version-constraints) section + +### Environment + +The extensions to be used and optionally their version constraints can also be specified in the `K6_DEPENDENCIES` environment variable. The value of the environment variable K6_DEPENDENCIES is a list of elements separated by semicolons. Each element specifies an extension (or k6 itself) and optionally its version constraint. + +``` +k6>=0.52;k6/x/faker>=0.3;k6/x/sql>=0.4 +``` + +### Manifest + +The manifest file is a JSON file, the `dependencies` property of which can specify extension dependencies and version constraints. The value of the `dependencies` property is a JSON object. The property names of this object are the extension names (or k6) and the values โ€‹โ€‹are the version constraints. + +```json +{ + "dependencies": { + "k6": ">=0.52", + "k6/x/faker": ">=0.3", + "k6/x/sql": ">=0.4" + } +} +``` + +The manifest file is a file named `package.json`, which is located closest to the k6 test script or the current directory, depending on whether the given subcommand has a test script argument (e.g. run, archive) or not (e.g. version). The `package.json` file is searched for up to the root of the directory hierarchy. + +### Limitations + +Version constraints can be specified in several sources ([pragma](#pragma), [environment](#environment), [manifest](#manifest)) but cannot be overwritten. That is, for a given extension, the version constraints from different sources must either be equal, or only one source can contain a version constraint. + +### Version Constraints + +*This section is based on the [Masterminds/semver] documentation.* + +#### Basic Comparisons + +There are two elements to the comparisons. First, a comparison string is a list +of space or comma separated AND comparisons. These are then separated by || (OR) +comparisons. For example, `">= 1.2 < 3.0.0 || >= 4.2.3"` is looking for a +comparison that's greater than or equal to 1.2 and less than 3.0.0 or is +greater than or equal to 4.2.3. + +The basic comparisons are: + +* `=`: equal (aliased to no operator) +* `!=`: not equal +* `>`: greater than +* `<`: less than +* `>=`: greater than or equal to +* `<=`: less than or equal to + +#### Hyphen Range Comparisons + +There are multiple methods to handle ranges and the first is hyphens ranges. +These look like: + +* `1.2 - 1.4.5` which is equivalent to `>= 1.2 <= 1.4.5` +* `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5` + +#### Wildcards In Comparisons + +The `x`, `X`, and `*` characters can be used as a wildcard character. This works +for all comparison operators. When used on the `=` operator it falls +back to the patch level comparison (see tilde below). For example, + +* `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` +* `>= 1.2.x` is equivalent to `>= 1.2.0` +* `<= 2.x` is equivalent to `< 3` +* `*` is equivalent to `>= 0.0.0` + +#### Tilde Range Comparisons (Patch) + +The tilde (`~`) comparison operator is for patch level ranges when a minor +version is specified and major level changes when the minor number is missing. +For example, + +* `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0` +* `~1` is equivalent to `>= 1, < 2` +* `~2.3` is equivalent to `>= 2.3, < 2.4` +* `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` +* `~1.x` is equivalent to `>= 1, < 2` + +#### Caret Range Comparisons (Major) + +The caret (`^`) comparison operator is for major level changes once a stable +(1.0.0) release has occurred. Prior to a 1.0.0 release the minor versions acts +as the API stability level. This is useful when comparisons of API versions as a +major change is API breaking. For example, + +* `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0` +* `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0` +* `^2.3` is equivalent to `>= 2.3, < 3` +* `^2.x` is equivalent to `>= 2.0.0, < 3` +* `^0.2.3` is equivalent to `>=0.2.3 <0.3.0` +* `^0.2` is equivalent to `>=0.2.0 <0.3.0` +* `^0.0.3` is equivalent to `>=0.0.3 <0.0.4` +* `^0.0` is equivalent to `>=0.0.0 <0.1.0` +* `^0` is equivalent to `>=0.0.0 <1.0.0` + +[k6]: https://k6.io +[extensions]: https://grafana.com/docs/k6/latest/extensions/ +[xk6-top]: https://github.com/szkiba/xk6-top +[Masterminds/semver]: https://github.com/Masterminds/semver + + +``` +k6exec [flags] [command] +``` + +### Flags + +``` + --build-service-url string URL of the k6 build service to be used + --extension-catalog-url string URL of the k6 extension catalog to be used + -h, --help help for k6 + --no-color disable colored output + -q, --quiet disable progress updates + --usage print launcher usage + -v, --verbose enable verbose logging +``` + + + ## Development ### Tasks @@ -17,6 +221,14 @@ This section contains a description of the tasks performed during development. C
Click to expand +#### readme + +Update documentation in README.md. + +```sh +go run ./tools/gendoc README.md +``` + #### lint Run the static analyzer. @@ -41,6 +253,46 @@ View the test coverage report. go tool cover -html=coverage.txt ``` +#### build + +Build the executable binary. + +This is the easiest way to create an executable binary (although the release process uses the goreleaser tool to create release versions). + +```sh +go build -ldflags="-w -s" -o k6exec ./cmd/k6exec +``` + +#### snapshot + +Creating an executable binary with a snapshot version. + +The goreleaser command-line tool is used during the release process. During development, it is advisable to create binaries with the same tool from time to time. + +```sh +goreleaser build --snapshot --clean --single-target -o k6exec +``` + +#### docker + +Building a Docker image. Before building the image, it is advisable to perform a snapshot build using goreleaser. To build the image, it is advisable to use the same `Docker.goreleaser` file that `goreleaser` uses during release. + +Requires: snapshot + +```sh +docker build -t k6exec -f Dockerfile.goreleaser . +``` + +#### examples + +Run all scripts in the `examples` directory with a fresh build. + +Requires: clean, snapshot + +```sh +find examples -type f | xargs -n 1 ./k6exec run +``` + #### clean Delete the build directory. diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..072ba74 --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,135 @@ +// Package cmd contains run cobra command factory function. +package cmd + +import ( + _ "embed" + "log/slog" + + "github.com/spf13/cobra" +) + +//go:embed help.md +var help string + +// New creates new cobra command for exec command. +func New(levelVar *slog.LevelVar) *cobra.Command { + state := newState(levelVar) + + root := &cobra.Command{ + Use: "k6exec [flags] [command]", + Short: "Run k6 with extensions", + Long: help, + SilenceUsage: true, + SilenceErrors: true, + DisableAutoGenTag: true, + CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, + PreRunE: func(cmd *cobra.Command, args []string) error { + if state.usage { + return nil + } + + state.AppName = cmd.Name() + + return state.preRunE(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if state.usage { + return cmd.Help() + } + + return state.runE(cmd, args) + }, + PersistentPreRunE: state.persistentPreRunE, + } + + root.SetVersionTemplate(`{{with .Name}}{{printf "%s " .}}{{end}}{{printf "%s\n" .Version}}`) + + for _, name := range commands { + root.AddCommand(newSubcommand(name, state)) + } + + flags := root.PersistentFlags() + + flags.StringVar( + &state.extensionCatalogURL, + "extension-catalog-url", + state.extensionCatalogURL, + "URL of the k6 extension catalog to be used", + ) + flags.StringVar( + &state.buildServiceURL, + "build-service-url", + state.buildServiceURL, + "URL of the k6 build service to be used", + ) + flags.BoolVarP(&state.verbose, "verbose", "v", false, "enable verbose logging") + flags.BoolVarP(&state.quiet, "quiet", "q", false, "disable progress updates") + flags.BoolVar(&state.nocolor, "no-color", false, "disable colored output") + flags.BoolVar(&state.usage, "usage", false, "print launcher usage") + + root.InitDefaultHelpFlag() + root.Flags().Lookup("help").Usage = "help for k6" + + root.MarkFlagsMutuallyExclusive("extension-catalog-url", "build-service-url") + + return root +} + +func scriptArg(cmd *cobra.Command, args []string) (string, bool) { + if len(cmd.Annotations) == 0 { + return "", false + } + + if _, use := cmd.Annotations[useExtensions]; !use { + return "", false + } + + if len(args) == 0 { + return "", false + } + + last := args[len(args)-1] + if len(last) == 0 || last[0] == '-' { + return "", false + } + + return last, true +} + +func newSubcommand(name string, state *state) *cobra.Command { + cmd := &cobra.Command{ + Use: name, + PreRunE: state.preRunE, + RunE: state.runE, + SilenceErrors: true, + SilenceUsage: true, + FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true}, + Hidden: true, + } + cmd.SetHelpFunc(state.helpFunc) + + if name == "run" || name == "archive" { + cmd.Annotations = map[string]string{useExtensions: "true"} + } + + return cmd +} + +const useExtensions = "useExtensions" + +var commands = []string{ //nolint:gochecknoglobals + "help", + "resume", + "scale", + "cloud", + "completion", + "inspect", + "pause", + "status", + "login", + "stats", + "version", + "new", + "run", + "archive", +} diff --git a/cmd/help.md b/cmd/help.md new file mode 100644 index 0000000..1a2a244 --- /dev/null +++ b/cmd/help.md @@ -0,0 +1,153 @@ +Run k6 with a seamless extension user experience. + +`k6exec` is a [k6] launcher that automatically provides [k6] with the [extensions] used by the test. In order to do this, it analyzes the script arguments of the `run` and `archive` subcommands, detects the extensions to be used and their version constraints. + +The launcher acts as a drop-in replacement for the `k6` command. For more convenient use, it is advisable to create an alias or shell script called `k6` for the launcher. The alias can be used in exactly the same way as the `k6` command, with the difference that it generates the real `k6` on the fly based on the extensions you want to use. + +Any k6 command can be used. Use the `help` command to list the available k6 commands. + +Since k6exec tries to emulate the `k6` command line, the `help` command or the `--help` flag cannot be used to display help from `k6exec` command itself. The `k6exec` help can be displayed using the `--usage` flag: + + k6exec --usage + +### Prerequisites + +k6exec tries to provide the appropriate k6 executable after detecting the extension dependencies. This can be done using a build service or a native builder. + +#### Build Service + +No additional installation is required to use the build service, just provide the build service URL. + +The build service URL can be specified in the `K6_BUILD_SERVICE_URL` environment variable or by using the `--build-service-url` flag. + +There is no default URL for the build service, otherwise k6exec will automatically provide k6 with the native builder. + +#### Native Builder + +To use the native builder, you only need to install the [Go language toolkit](https://go.dev/doc/install). + +The native builder uses a k6 extension catalog to resolve extension URLs and versions. The extension catalog URL has a default value. A different extension catalog URL can be specified in the `K6_EXTENSION_CATALOG_URL` environment variable or by using the `--extension-catalog-url` flag. + +### Pragma + +Version constraints can be specified using the JavaScript `"use ..."` pragma syntax for k6 and extensions. Put the following lines at the beginning of the test script: + +```js +"use k6 >= v0.52"; +"use k6 with k6/x/faker > 0.2"; +``` + +Any number of `"use k6"` pragmas can be used. + +> **Note** +> The use of pragmas is completely optional for JavaScript type extensions, it is only necessary if you want to specify version constraints. + +The pragma syntax can also be used to specify an extension dependency that is not referenced in an import expression. A typical example of this is the Output type extension such as [xk6-top]: + +```js +"use k6 with top >= 0.1"; +``` + +Read the version constraints syntax in the [Version Constraints](#version-constraints) section + +### Environment + +The extensions to be used and optionally their version constraints can also be specified in the `K6_DEPENDENCIES` environment variable. The value of the environment variable K6_DEPENDENCIES is a list of elements separated by semicolons. Each element specifies an extension (or k6 itself) and optionally its version constraint. + +``` +k6>=0.52;k6/x/faker>=0.3;k6/x/sql>=0.4 +``` + +### Manifest + +The manifest file is a JSON file, the `dependencies` property of which can specify extension dependencies and version constraints. The value of the `dependencies` property is a JSON object. The property names of this object are the extension names (or k6) and the values โ€‹โ€‹are the version constraints. + +```json +{ + "dependencies": { + "k6": ">=0.52", + "k6/x/faker": ">=0.3", + "k6/x/sql": ">=0.4" + } +} +``` + +The manifest file is a file named `package.json`, which is located closest to the k6 test script or the current directory, depending on whether the given subcommand has a test script argument (e.g. run, archive) or not (e.g. version). The `package.json` file is searched for up to the root of the directory hierarchy. + +### Limitations + +Version constraints can be specified in several sources ([pragma](#pragma), [environment](#environment), [manifest](#manifest)) but cannot be overwritten. That is, for a given extension, the version constraints from different sources must either be equal, or only one source can contain a version constraint. + +### Version Constraints + +*This section is based on the [Masterminds/semver] documentation.* + +#### Basic Comparisons + +There are two elements to the comparisons. First, a comparison string is a list +of space or comma separated AND comparisons. These are then separated by || (OR) +comparisons. For example, `">= 1.2 < 3.0.0 || >= 4.2.3"` is looking for a +comparison that's greater than or equal to 1.2 and less than 3.0.0 or is +greater than or equal to 4.2.3. + +The basic comparisons are: + +* `=`: equal (aliased to no operator) +* `!=`: not equal +* `>`: greater than +* `<`: less than +* `>=`: greater than or equal to +* `<=`: less than or equal to + +#### Hyphen Range Comparisons + +There are multiple methods to handle ranges and the first is hyphens ranges. +These look like: + +* `1.2 - 1.4.5` which is equivalent to `>= 1.2 <= 1.4.5` +* `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5` + +#### Wildcards In Comparisons + +The `x`, `X`, and `*` characters can be used as a wildcard character. This works +for all comparison operators. When used on the `=` operator it falls +back to the patch level comparison (see tilde below). For example, + +* `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` +* `>= 1.2.x` is equivalent to `>= 1.2.0` +* `<= 2.x` is equivalent to `< 3` +* `*` is equivalent to `>= 0.0.0` + +#### Tilde Range Comparisons (Patch) + +The tilde (`~`) comparison operator is for patch level ranges when a minor +version is specified and major level changes when the minor number is missing. +For example, + +* `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0` +* `~1` is equivalent to `>= 1, < 2` +* `~2.3` is equivalent to `>= 2.3, < 2.4` +* `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` +* `~1.x` is equivalent to `>= 1, < 2` + +#### Caret Range Comparisons (Major) + +The caret (`^`) comparison operator is for major level changes once a stable +(1.0.0) release has occurred. Prior to a 1.0.0 release the minor versions acts +as the API stability level. This is useful when comparisons of API versions as a +major change is API breaking. For example, + +* `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0` +* `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0` +* `^2.3` is equivalent to `>= 2.3, < 3` +* `^2.x` is equivalent to `>= 2.0.0, < 3` +* `^0.2.3` is equivalent to `>=0.2.3 <0.3.0` +* `^0.2` is equivalent to `>=0.2.0 <0.3.0` +* `^0.0.3` is equivalent to `>=0.0.3 <0.0.4` +* `^0.0` is equivalent to `>=0.0.0 <0.1.0` +* `^0` is equivalent to `>=0.0.0 <1.0.0` + +[k6]: https://k6.io +[extensions]: https://grafana.com/docs/k6/latest/extensions/ +[xk6-top]: https://github.com/szkiba/xk6-top +[Masterminds/semver]: https://github.com/Masterminds/semver diff --git a/cmd/k6exec/error.go b/cmd/k6exec/error.go new file mode 100644 index 0000000..00dac75 --- /dev/null +++ b/cmd/k6exec/error.go @@ -0,0 +1,41 @@ +package main + +import ( + "errors" + "os" + + "golang.org/x/term" +) + +type formatableError = interface { + error + Format(width int, color bool) string +} + +func formatError(err error) string { + width, color := formatOptions(int(os.Stderr.Fd())) //nolint:forbidigo + + var perr formatableError + if errors.As(err, &perr) { + return perr.Format(width, color) + } + + return err.Error() +} + +func formatOptions(fd int) (int, bool) { + color := false + width := 0 + + if term.IsTerminal(fd) { + if os.Getenv("NO_COLOR") != "true" { //nolint:forbidigo + color = true + } + + if w, _, err := term.GetSize(fd); err == nil { + width = w + } + } + + return width, color +} diff --git a/cmd/k6exec/main.go b/cmd/k6exec/main.go new file mode 100644 index 0000000..aa0ad49 --- /dev/null +++ b/cmd/k6exec/main.go @@ -0,0 +1,57 @@ +// Package main contains the main function for k6exec. +package main + +import ( + "log/slog" + "os" + "strings" + + "github.com/grafana/k6exec/cmd" + sloglogrus "github.com/samber/slog-logrus/v2" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +//nolint:gochecknoglobals +var ( + appname = "k6exec" + version = "dev" +) + +func initLogging(app string) *slog.LevelVar { + levelVar := new(slog.LevelVar) + + logrus.SetLevel(logrus.DebugLevel) + + logger := slog.New(sloglogrus.Option{Level: levelVar}.NewLogrusHandler()) + logger = logger.With("app", app) + + slog.SetDefault(logger) + + return levelVar +} + +func main() { + runCmd(newCmd(os.Args[1:], initLogging(appname))) //nolint:forbidigo +} + +func newCmd(args []string, levelVar *slog.LevelVar) *cobra.Command { + cmd := cmd.New(levelVar) + cmd.Use = strings.ReplaceAll(cmd.Use, "exec", appname) + cmd.Version = version + + if len(args) == 1 && (args[0] == "-h" || args[0] == "--help") { + args[0] = "help" + } + + cmd.SetArgs(args) + + return cmd +} + +func runCmd(cmd *cobra.Command) { + if err := cmd.Execute(); err != nil { + slog.Error(formatError(err)) + os.Exit(1) //nolint:forbidigo + } +} diff --git a/cmd/state.go b/cmd/state.go new file mode 100644 index 0000000..08db724 --- /dev/null +++ b/cmd/state.go @@ -0,0 +1,137 @@ +package cmd + +import ( + "context" + "log/slog" + "net/url" + "os" + "os/exec" + + "github.com/grafana/k6deps" + "github.com/grafana/k6exec" + "github.com/spf13/cobra" +) + +type state struct { + k6exec.Options + buildServiceURL string + extensionCatalogURL string + verbose bool + quiet bool + nocolor bool + usage bool + levelVar *slog.LevelVar + + cmd *exec.Cmd +} + +//nolint:forbidigo +func newState(levelVar *slog.LevelVar) *state { + s := new(state) + + s.levelVar = levelVar + + if value, found := os.LookupEnv("K6_BUILD_SERVICE_URL"); found { + s.buildServiceURL = value + } + + if value, found := os.LookupEnv("K6_EXTENSION_CATALOG_URL"); found { + s.extensionCatalogURL = value + } + + return s +} + +func (s *state) persistentPreRunE(_ *cobra.Command, _ []string) error { + if len(s.buildServiceURL) > 0 { + val, err := url.Parse(s.buildServiceURL) + if err != nil { + return err + } + + s.Options.BuildServiceURL = val + } + + if len(s.extensionCatalogURL) > 0 { + val, err := url.Parse(s.extensionCatalogURL) + if err != nil { + return err + } + + s.Options.ExtensionCatalogURL = val + } + + if s.verbose && s.levelVar != nil { + s.levelVar.Set(slog.LevelDebug) + } + + return nil +} + +func (s *state) preRunE(sub *cobra.Command, args []string) error { + var ( + deps k6deps.Dependencies + err error + dopts k6deps.Options + ) + + if scriptname, hasScript := scriptArg(sub, args); hasScript { + dopts.Script.Name = scriptname + } + + deps, err = k6deps.Analyze(&dopts) + if err != nil { + return err + } + + cmdargs := make([]string, 0, len(args)) + + if sub.Name() != s.Options.AppName { + cmdargs = append(cmdargs, sub.Name()) + } + + if s.verbose { + cmdargs = append(cmdargs, "-v") + } + + if s.quiet { + cmdargs = append(cmdargs, "-q") + } + + if s.nocolor { + cmdargs = append(cmdargs, "--no-color") + } + + cmdargs = append(cmdargs, args...) + + cmd, err := k6exec.Command(context.Background(), cmdargs, deps, &s.Options) + if err != nil { + return err + } + + cmd.Stderr = os.Stderr //nolint:forbidigo + cmd.Stdout = os.Stdout //nolint:forbidigo + cmd.Stdin = os.Stdin //nolint:forbidigo + + s.cmd = cmd + + return nil +} + +func (s *state) runE(_ *cobra.Command, _ []string) error { + defer k6exec.CleanupState(&s.Options) //nolint:errcheck + + return s.cmd.Run() +} + +func (s *state) helpFunc(cmd *cobra.Command, args []string) { + err := s.preRunE(cmd, append(args, "-h")) + if err != nil { + cmd.PrintErr(err) + } + + err = s.runE(cmd, args) + if err != nil { + cmd.PrintErr(err) + } +} diff --git a/go.mod b/go.mod index 2360d2e..accc0c3 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,14 @@ go 1.22.2 require ( github.com/adrg/xdg v0.4.0 + github.com/grafana/clireadme v0.1.0 github.com/grafana/k6build v0.2.0 github.com/grafana/k6deps v0.1.2 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 + github.com/samber/slog-logrus/v2 v2.4.0 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.8.0 + golang.org/x/term v0.18.0 ) require ( @@ -17,8 +22,12 @@ require ( github.com/grafana/k6catalog v0.1.0 // indirect github.com/grafana/k6foundry v0.1.3 // indirect github.com/grafana/k6pack v0.2.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/samber/lo v1.44.0 // indirect + github.com/samber/slog-common v0.17.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect golang.org/x/mod v0.18.0 // indirect golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect ) diff --git a/go.sum b/go.sum index 29d02f8..03c0e5b 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0 github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -13,6 +14,8 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/grafana/clireadme v0.1.0 h1:KYEYSnYdSzmHf3bufaK6fQZ5j4dzvM/T+G6Ba+qNnAM= +github.com/grafana/clireadme v0.1.0/go.mod h1:Wy4KIG2ZBGMYAYyF9l7qAy+yoJVasqk/txsRgoRI3gc= github.com/grafana/k6build v0.2.0 h1:4IRinD5iuPW7+XR5590UduPwm1hBAwH2bpdkMADifP8= github.com/grafana/k6build v0.2.0/go.mod h1:DXItIZzDI1gnMOC0+oSE2OsjNJtR4ahLHYC8EQ643T8= github.com/grafana/k6catalog v0.1.0 h1:jLmbmB3EUJ+zyQG3hWy6dWbtMjvTkvJNx1d4LX8it6I= @@ -25,12 +28,25 @@ github.com/grafana/k6pack v0.2.1 h1:S9EkeFuRMnfwP/lHrKnlgctlNDiUKgKU1bEKbIfOUro= github.com/grafana/k6pack v0.2.1/go.mod h1:BEy4y0GE+gXbdp8EldJGXd1g1Py3wBBxDE2AwzHsMxI= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.44.0 h1:5il56KxRE+GHsm1IR+sZ/6J42NODigFiqCWpSc2dybA= +github.com/samber/lo v1.44.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/samber/slog-common v0.17.0 h1:HdRnk7QQTa9ByHlLPK3llCBo8ZSX3F/ZyeqVI5dfMtI= +github.com/samber/slog-common v0.17.0/go.mod h1:mZSJhinB4aqHziR0SKPqpVZjJ0JO35JfH+dDIWqaCBk= +github.com/samber/slog-logrus/v2 v2.4.0 h1:Ms7caRozJdaEpTIVuXZrYRdtzmUzsfE+eqeLpoe29JM= +github.com/samber/slog-logrus/v2 v2.4.0/go.mod h1:xN6h40pDGXSJDgZsttF9KtaIV7dtpjeoBDpw8TpvRr8= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= @@ -41,6 +57,10 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/releases/v0.1.2.md b/releases/v0.1.2.md new file mode 100644 index 0000000..9a2297b --- /dev/null +++ b/releases/v0.1.2.md @@ -0,0 +1,5 @@ +k6exec `v0.1.2` is here ๐ŸŽ‰! + +This is an internal maintenance release that reintroduces CLI functionality. +In addition, k6exec is also available as a Docker image in GitHub [Pacakges](https://github.com/grafana/k6exec/pkgs/container/k6exec). + diff --git a/test.db b/test.db new file mode 100644 index 0000000000000000000000000000000000000000..464e72dc853b1b76ccb17953fc30f6ce3feea3b9 GIT binary patch literal 12288 zcmeI&u};E37zgmXwx}^7=VDk$_!0&ofQyN;JzOvMu$N|m;dIO^nMIQb+# ziErQ=cqM|ufRiyX|1Z7vuJ_%S=C|FYXQ#HGNV>j_MqWY(Y=?2qj))jz8P#;v=D}6R zm=Dx59sd*=wjVubRhA~Um^fEA1Oy-e0SG_<0uX=z1Rwwb2teQu1=`w1u2SK>#Jdb+ zJPy>R_#&3$i46OzL%rUyOxGgUJhm-b$t{K6NLJO#@