Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
15 changes: 14 additions & 1 deletion internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
stdlog "log"
"runtime/debug"
"slices"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -416,8 +417,20 @@ func createSecretSources(gs *state.GlobalState) (map[string]secretsource.Source,
Usage: gs.Usage,
}

// K6_SECRET_SOURCE is equivalent to a single extra --secret-source flag.
// It is treated as one complete source spec (e.g. "file=path.secret" or
// "mock=name=mysource,key=value") so commas inside the spec are handled
// correctly by extractNameAndDefault and the source's own config parser.
// Exact duplicates of an already-present flag value are skipped silently.
secretSourceArgs := gs.Flags.SecretSource
if envSrc := strings.TrimSpace(gs.Env["K6_SECRET_SOURCE"]); envSrc != "" {
if !slices.Contains(secretSourceArgs, envSrc) {
secretSourceArgs = append(secretSourceArgs, envSrc)
}
}

result := make(map[string]secretsource.Source)
for _, line := range gs.Flags.SecretSource {
for _, line := range secretSourceArgs {
t, config, ok := strings.Cut(line, "=")
if !ok {
// Special case: allow --secret-source=url without explicit config
Expand Down
204 changes: 204 additions & 0 deletions internal/cmd/tests/cmd_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2703,6 +2703,210 @@ func TestMultipleSecretSources(t *testing.T) {
assert.Contains(t, stderr, `level=info msg="trigger exception on wrong key" ***SECRET_REDACTED***=console`)
}

// TestK6SecretSourceEnvVar covers the K6_SECRET_SOURCE environment variable, which is
// treated as a single complete source spec equivalent to one --secret-source flag value.
func TestK6SecretSourceEnvVar(t *testing.T) {
t.Parallel()

// Script that reads two keys from the default secret source.
script := `
import secrets from "k6/secrets";
export default async () => {
const a = await secrets.get("cool");
const b = await secrets.get("else");
console.log(a);
console.log(b);
}
`

t.Run("basic mock source via env var", func(t *testing.T) {
t.Parallel()
ts := NewGlobalTestState(t)
require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "script.js"), []byte(script), 0o644))

ts.Env["K6_SECRET_SOURCE"] = "mock=cool=something,else=other"
ts.CmdArgs = []string{"k6", "run", "script.js"}

cmd.ExecuteWithGlobalState(ts.GlobalState)

stderr := ts.Stderr.String()
t.Log(stderr)
assert.NotContains(t, stderr, "level=error")
assert.Contains(t, stderr, `level=info msg="***SECRET_REDACTED***" source=console`)
})

t.Run("file source via env var", func(t *testing.T) {
t.Parallel()
ts := NewGlobalTestState(t)
require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "script.js"), []byte(script), 0o644))
secretFile := filepath.Join(ts.Cwd, "secrets.env")
require.NoError(t, fsext.WriteFile(ts.FS, secretFile, []byte("cool=val1\nelse=val2\n"), 0o644))

ts.Env["K6_SECRET_SOURCE"] = "file=" + secretFile
ts.CmdArgs = []string{"k6", "run", "script.js"}

cmd.ExecuteWithGlobalState(ts.GlobalState)

stderr := ts.Stderr.String()
t.Log(stderr)
assert.NotContains(t, stderr, "level=error")
assert.Contains(t, stderr, `level=info msg="***SECRET_REDACTED***" source=console`)
})

t.Run("commas inside spec are not treated as source separators", func(t *testing.T) {
// mock=name=mysource,cool=val has a comma, but it belongs to the source
// spec (name= suffix + key=val), not a second source definition.
t.Parallel()
namedScript := `
import secrets from "k6/secrets";
export default async () => {
const v = await secrets.source("mysource").get("cool");
console.log(v);
}
`
ts := NewGlobalTestState(t)
require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "script.js"), []byte(namedScript), 0o644))

ts.Env["K6_SECRET_SOURCE"] = "mock=name=mysource,cool=secretval"
ts.CmdArgs = []string{"k6", "run", "script.js"}

cmd.ExecuteWithGlobalState(ts.GlobalState)

stderr := ts.Stderr.String()
t.Log(stderr)
assert.NotContains(t, stderr, "level=error")
assert.Contains(t, stderr, `level=info msg="***SECRET_REDACTED***" source=console`)
})

t.Run("name= and default suffixes work via env var", func(t *testing.T) {
t.Parallel()
namedScript := `
import secrets from "k6/secrets";
export default async () => {
// access by explicit name
const a = await secrets.source("named").get("cool");
// access via default
const b = await secrets.get("cool");
console.log(a);
console.log(b);
}
`
ts := NewGlobalTestState(t)
require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "script.js"), []byte(namedScript), 0o644))

ts.Env["K6_SECRET_SOURCE"] = "mock=name=named,cool=secretval,default"
ts.CmdArgs = []string{"k6", "run", "script.js"}

cmd.ExecuteWithGlobalState(ts.GlobalState)

stderr := ts.Stderr.String()
t.Log(stderr)
assert.NotContains(t, stderr, "level=error")
assert.Contains(t, stderr, `level=info msg="***SECRET_REDACTED***" source=console`)
})

t.Run("env var merged with flag provides two separate sources", func(t *testing.T) {
t.Parallel()
multiScript := `
import secrets from "k6/secrets";
export default async () => {
const a = await secrets.source("first").get("cool");
const b = await secrets.source("second").get("cool");
console.log(a);
console.log(b);
console.log(a === b);
}
`
ts := NewGlobalTestState(t)
require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "script.js"), []byte(multiScript), 0o644))

ts.Env["K6_SECRET_SOURCE"] = "mock=name=second,cool=second-secret"
ts.CmdArgs = []string{"k6", "run", "--secret-source=mock=name=first,cool=first-secret", "script.js"}

cmd.ExecuteWithGlobalState(ts.GlobalState)

stderr := ts.Stderr.String()
t.Log(stderr)
assert.NotContains(t, stderr, "level=error")
// Both secrets are redacted in the output.
assert.Contains(t, stderr, `level=info msg="***SECRET_REDACTED***" source=console`)
// The boolean false is not a secret — it is logged as-is.
assert.Contains(t, stderr, `level=info msg=false source=console`)
})

t.Run("exact duplicate of flag value is silently deduplicated", func(t *testing.T) {
// When K6_SECRET_SOURCE equals a --secret-source value exactly, only one
// source is registered (no "already registered" error).
t.Parallel()
singleScript := `
import secrets from "k6/secrets";
export default async () => {
const v = await secrets.get("cool");
console.log(v);
}
`
ts := NewGlobalTestState(t)
require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "script.js"), []byte(singleScript), 0o644))

ts.Env["K6_SECRET_SOURCE"] = "mock=cool=secretval"
ts.CmdArgs = []string{"k6", "run", "--secret-source=mock=cool=secretval", "script.js"}

cmd.ExecuteWithGlobalState(ts.GlobalState)

stderr := ts.Stderr.String()
t.Log(stderr)
assert.NotContains(t, stderr, "level=error")
assert.Contains(t, stderr, `level=info msg="***SECRET_REDACTED***" source=console`)
})

t.Run("empty env var is ignored, no source configured error", func(t *testing.T) {
t.Parallel()
ts := NewGlobalTestState(t)
require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "script.js"), []byte(script), 0o644))

ts.Env["K6_SECRET_SOURCE"] = ""
ts.CmdArgs = []string{"k6", "run", "script.js"}

cmd.ExecuteWithGlobalState(ts.GlobalState)

stderr := ts.Stderr.String()
t.Log(stderr)
assert.Contains(t, stderr, "no secret sources are configured")
})

t.Run("whitespace-only env var is ignored", func(t *testing.T) {
t.Parallel()
ts := NewGlobalTestState(t)
require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "script.js"), []byte(script), 0o644))

ts.Env["K6_SECRET_SOURCE"] = " "
ts.CmdArgs = []string{"k6", "run", "script.js"}

cmd.ExecuteWithGlobalState(ts.GlobalState)

stderr := ts.Stderr.String()
t.Log(stderr)
assert.Contains(t, stderr, "no secret sources are configured")
})

t.Run("invalid source type gives clear error", func(t *testing.T) {
t.Parallel()
ts := NewGlobalTestState(t)
require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "script.js"), []byte(script), 0o644))

ts.ExpectedExitCode = -1
ts.Env["K6_SECRET_SOURCE"] = "nonexistent=config"
ts.CmdArgs = []string{"k6", "run", "script.js"}

cmd.ExecuteWithGlobalState(ts.GlobalState)

stderr := ts.Stderr.String()
t.Log(stderr)
assert.Contains(t, stderr, "no secret source for type")
assert.Contains(t, stderr, "nonexistent")
})
}

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

Expand Down
Loading