Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/k6runner/version/testdata/other-file-noexec
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
I'm just a regular text file. Don't stare at me, I'm shy.
3 changes: 3 additions & 0 deletions internal/k6runner/version/testdata/override/sm-k6-custom
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

echo '{"commit":"9.9.9","go_arch":"amd64","go_os":"linux","go_version":"go1.24.6","version":"v9.9.9"}'
3 changes: 3 additions & 0 deletions internal/k6runner/version/testdata/sm-k6-v1
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

echo '{"commit":"1111111111","go_arch":"amd64","go_os":"linux","go_version":"go1.24.6","version":"v1.2.3"}'
3 changes: 3 additions & 0 deletions internal/k6runner/version/testdata/sm-k6-v2
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

echo '{"commit":"2222222222","go_arch":"amd64","go_os":"linux","go_version":"go1.24.6","version":"v2.0.0"}'
3 changes: 3 additions & 0 deletions internal/k6runner/version/testdata/subdir/sm-k6-v0
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

echo '{"commit":"00000000","go_arch":"amd64","go_os":"linux","go_version":"go1.24.6","version":"v0.0.0"}'
171 changes: 171 additions & 0 deletions internal/k6runner/version/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package version

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"

"github.com/rs/zerolog"
)

// Repository points to a collection of k6 binaries.
type Repository struct {
// Root points to a folder in the local filesystem that will be scanned for k6 binaries.
// All executable files in said folder should be k6 binaries, as they will all be executed with `--version` to map
// their actual versions.
Root string
// Override is the path to a specific k6 binary. If set, all Repository operations will return this path.
Override string

// Logger.
Logger zerolog.Logger

mtx sync.Mutex
entries []Entry
}

const binaryMustContain = "k6"

type Entry struct {
Path string
Version string
}

type k6Version struct {
Commit string `json:"commit"`
GoArch string `json:"go_arch"`
GoOs string `json:"go_os"`
GoVersion string `json:"go_version"`
Version string `json:"version"`
}

func (r *Repository) Entries() ([]Entry, error) {
err := r.scan(false)
if err != nil {
return nil, fmt.Errorf("scanning for binaries: %w", err)
}

r.mtx.Lock()
defer r.mtx.Unlock()

if len(r.entries) == 0 {
return nil, nil
}

entries := make([]Entry, len(r.entries))
copy(entries, r.entries)

return entries, nil
}

func (r *Repository) scan(force bool) error {
r.mtx.Lock()
defer r.mtx.Unlock()

if len(r.entries) > 0 && !force {
return nil
}

binaries, err := r.binaries()
if err != nil {
return err
}

for _, bin := range binaries {
version, err := runK6Version(bin)
if err != nil {
return fmt.Errorf("finding version for %q: %w", bin, err)
}

r.entries = append(r.entries, Entry{
Path: bin,
Version: version.Version,
})
}

return nil
}

func (r *Repository) binaries() ([]string, error) {
if r.Override != "" {
r.Logger.Warn().Str("k6", r.Override).Msg("Overriding k6 binary autoselection")

return []string{r.Override}, nil
}

var binaries []string

files, err := fs.ReadDir(os.DirFS(r.Root), ".")
if err != nil {
return nil, fmt.Errorf("reading k6 repository root: %w", err)
}

for _, file := range files {
path := filepath.Join(r.Root, file.Name())

if file.IsDir() {
r.Logger.Warn().Str("root", r.Root).Str("directory", path).Msg("Foreign directory found inside k6 repository root")
continue
}

info, err := file.Info()
if err != nil {
return nil, fmt.Errorf("reading file info: %w", err)
}

if info.Mode().Perm()&0o111 == 0 {
// This is not an exhaustive check: It is possible that the file is executable, but not by the user running
// this code, in which case the error will be thrown later. This is a best-effort pass to detect stray
// files.
r.Logger.Warn().Str("root", r.Root).Str("path", path).Msg("Found non-executable file inside k6 repository root")
continue
}

if !strings.Contains(file.Name(), binaryMustContain) {
// Ignore binaries that do not contain a specific substring in the name. In the next step we will execute
// every found binary with `--version --json`, so as a safety check to avoid executing unknown binaries if
// we're pointed to the wrong directory (like /usr/bin) we look for a specific name here.
r.Logger.Warn().Str("root", r.Root).Str("path", path).Msg("Foreign binary found inside k6 repository root")
continue
}

binaries = append(binaries, path)
}

return binaries, nil
}

func runK6Version(k6Path string) (*k6Version, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

cmd := exec.CommandContext(ctx, k6Path, "--version", "--json")
cmd.Env = []string{
"K6_AUTO_EXTENSION_RESOLUTION=false",
// By not explicitly appending os.Env, all other env vars are cleared here.
}

stdout := &bytes.Buffer{}
cmd.Stdout = stdout

err := cmd.Run()
if err != nil {
return nil, fmt.Errorf("running k6: %w", err)
}

k6v := k6Version{}
err = json.Unmarshal(stdout.Bytes(), &k6v)
if err != nil {
return nil, fmt.Errorf("parsing json: %w", err)
}

return &k6v, nil
}
58 changes: 58 additions & 0 deletions internal/k6runner/version/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package version_test

import (
"slices"
"testing"

"github.com/grafana/synthetic-monitoring-agent/internal/k6runner/version"
)

func TestOverride(t *testing.T) {
t.Parallel()

repo := version.Repository{
Root: "./testdata/",
Override: "./testdata/override/sm-k6-custom",
}

versions, err := repo.Entries()
if err != nil {
t.Fatalf("retrieving entries: %v", err)
}

if len(versions) != 1 {
t.Fatalf("Expected just the overridden version, got %d", len(versions))
}

if v := versions[0].Version; v != "v9.9.9" {
t.Fatalf("Unexpected version %q", v)
}
}

func TestVersions(t *testing.T) {
t.Parallel()

repo := version.Repository{
Root: "./testdata/",
}

versions, err := repo.Entries()
if err != nil {
t.Fatalf("retrieving entries: %v", err)
}

expected := []string{
"v1.2.3",
"v2.0.0",
}

if len(versions) != len(expected) {
t.Fatalf("Expected to find 2 versions, got %d", len(versions))
}

for _, ev := range expected {
if !slices.ContainsFunc(versions, func(v version.Entry) bool { return ev == v.Version }) {
t.Fatalf("Expected version %q not found in %v", ev, versions)
}
}
}
Loading