Skip to content

Commit a4939a8

Browse files
authored
Merge pull request #4514 from grafana/secretSource
Secret Source Implementation
2 parents ed02613 + fe26d76 commit a4939a8

File tree

21 files changed

+790
-2
lines changed

21 files changed

+790
-2
lines changed

cmd/state/state.go

+7
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import (
1414

1515
"go.k6.io/k6/internal/event"
1616
"go.k6.io/k6/internal/ui/console"
17+
"go.k6.io/k6/internal/usage"
1718
"go.k6.io/k6/lib/fsext"
19+
"go.k6.io/k6/secretsource"
1820
)
1921

2022
const defaultConfigFileName = "config.json"
@@ -55,6 +57,9 @@ type GlobalState struct {
5557

5658
Logger *logrus.Logger //nolint:forbidigo //TODO:change to FieldLogger
5759
FallbackLogger logrus.FieldLogger
60+
61+
SecretsManager *secretsource.Manager
62+
Usage *usage.Usage
5863
}
5964

6065
// NewGlobalState returns a new GlobalState with the given ctx.
@@ -131,6 +136,7 @@ func NewGlobalState(ctx context.Context) *GlobalState {
131136
Hooks: make(logrus.LevelHooks),
132137
Level: logrus.InfoLevel,
133138
},
139+
Usage: usage.New(),
134140
}
135141
}
136142

@@ -142,6 +148,7 @@ type GlobalFlags struct {
142148
Address string
143149
ProfilingEnabled bool
144150
LogOutput string
151+
SecretSource []string
145152
LogFormat string
146153
Verbose bool
147154
}

examples/secrets/file.secret

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
cool=some
2+
else=source

examples/secrets/secrets.test.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// k6 run --secret-source=file=file.secret secrets.test.js
2+
import secrets from "k6/secrets";
3+
4+
export default async () => {
5+
const my_secret = await secrets.get("cool"); // get secret from a source with the provided identifier
6+
console.log(my_secret);
7+
await secrets.get("else"); // get secret from a source with the provided identifier
8+
console.log(my_secret);
9+
}

ext/ext.go

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type ExtensionType uint8
2525
const (
2626
JSExtension ExtensionType = iota + 1
2727
OutputExtension
28+
SecretSourceExtension
2829
)
2930

3031
func (e ExtensionType) String() string {
@@ -34,6 +35,8 @@ func (e ExtensionType) String() string {
3435
s = "js"
3536
case OutputExtension:
3637
s = "output"
38+
case SecretSourceExtension:
39+
s = "secret-source"
3740
}
3841
return s
3942
}
@@ -157,4 +160,5 @@ func extractModuleInfo(mod interface{}) (path, version string) {
157160
func init() {
158161
extensions[JSExtension] = make(map[string]*Extension)
159162
extensions[OutputExtension] = make(map[string]*Extension)
163+
extensions[SecretSourceExtension] = make(map[string]*Extension)
160164
}

internal/cmd/root.go

+92
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ import (
1919
"go.k6.io/k6/cmd/state"
2020
"go.k6.io/k6/errext"
2121
"go.k6.io/k6/errext/exitcodes"
22+
"go.k6.io/k6/ext"
2223
"go.k6.io/k6/internal/log"
24+
"go.k6.io/k6/secretsource"
25+
26+
_ "go.k6.io/k6/internal/secretsource" // import it to register internal secret sources
2327
)
2428

2529
const waitLoggerCloseTimeout = time.Second * 5
@@ -162,6 +166,10 @@ func rootCmdPersistentFlagSet(gs *state.GlobalState) *pflag.FlagSet {
162166
// `gs.DefaultFlags.<value>`, so that the `k6 --help` message is
163167
// not messed up...
164168

169+
// TODO(@mstoykov): likely needs work - no env variables and such. No config.json.
170+
flags.StringArrayVar(&gs.Flags.SecretSource, "secret-source", gs.Flags.SecretSource,
171+
"setting secret sources for k6 file[=./path.fileformat],")
172+
165173
flags.StringVar(&gs.Flags.LogOutput, "log-output", gs.Flags.LogOutput,
166174
"change the output for k6 logs, possible values are stderr,stdout,none,loki[=host:port],file[=./path.fileformat]")
167175
flags.Lookup("log-output").DefValue = gs.DefaultFlags.LogOutput
@@ -257,6 +265,22 @@ func (c *rootCommand) setupLoggers(stop <-chan struct{}) error {
257265
c.globalState.Logger.Debug("Logger format: TEXT")
258266
}
259267

268+
secretsources, err := createSecretSources(c.globalState)
269+
if err != nil {
270+
return err
271+
}
272+
// it is important that we add this hook first as hooks are executed in order of addition
273+
// and this means no other hook will get secrets
274+
var secretsHook logrus.Hook
275+
c.globalState.SecretsManager, secretsHook, err = secretsource.NewManager(secretsources)
276+
if err != nil {
277+
return err
278+
}
279+
if len(secretsources) != 0 {
280+
// don't actually filter anything if there will be no secrets
281+
c.globalState.Logger.AddHook(secretsHook)
282+
}
283+
260284
cancel := func() {} // noop as default
261285
if hook != nil {
262286
ctx := context.Background()
@@ -289,3 +313,71 @@ func (c *rootCommand) setLoggerHook(ctx context.Context, h log.AsyncHook) {
289313
c.globalState.Logger.AddHook(h)
290314
c.globalState.Logger.SetOutput(io.Discard) // don't output to anywhere else
291315
}
316+
317+
func createSecretSources(gs *state.GlobalState) (map[string]secretsource.Source, error) {
318+
baseParams := secretsource.Params{
319+
Logger: gs.Logger,
320+
Environment: gs.Env,
321+
FS: gs.FS,
322+
Usage: gs.Usage,
323+
}
324+
325+
result := make(map[string]secretsource.Source)
326+
for _, line := range gs.Flags.SecretSource {
327+
t, config, ok := strings.Cut(line, "=")
328+
if !ok {
329+
return nil, fmt.Errorf("couldn't parse secret source configuration %q", line)
330+
}
331+
secretSources := ext.Get(ext.SecretSourceExtension)
332+
found, ok := secretSources[t]
333+
if !ok {
334+
return nil, fmt.Errorf("no secret source for type %q for configuration %q", t, line)
335+
}
336+
c := found.Module.(secretsource.Constructor) //nolint:forcetypeassert
337+
params := baseParams
338+
name, isDefault, config := extractNameAndDefault(config)
339+
params.ConfigArgument = config
340+
341+
secretSource, err := c(params)
342+
if err != nil {
343+
return nil, err
344+
}
345+
_, alreadRegistered := result[name]
346+
if alreadRegistered {
347+
return nil, fmt.Errorf("secret source for name %q already registered before configuration %q", t, line)
348+
}
349+
result[name] = secretSource
350+
if isDefault {
351+
if _, ok := result["default"]; ok {
352+
return nil, fmt.Errorf("can't have two secret sources that are default ones, second one was %q", config)
353+
}
354+
result["default"] = secretSource
355+
}
356+
}
357+
358+
if len(result) == 1 {
359+
for _, l := range result {
360+
result["default"] = l
361+
}
362+
}
363+
364+
return result, nil
365+
}
366+
367+
func extractNameAndDefault(config string) (name string, isDefault bool, remaining string) {
368+
list := strings.Split(config, ",")
369+
remainingArray := make([]string, 0, len(list))
370+
for _, kv := range list {
371+
if kv == "default" {
372+
isDefault = true
373+
continue
374+
}
375+
k, v, _ := strings.Cut(kv, "=")
376+
if k == "name" {
377+
name = v
378+
continue
379+
}
380+
remainingArray = append(remainingArray, kv)
381+
}
382+
return name, isDefault, strings.Join(remainingArray, ",")
383+
}

internal/cmd/test_load.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"go.k6.io/k6/errext/exitcodes"
1919
"go.k6.io/k6/internal/js"
2020
"go.k6.io/k6/internal/loader"
21-
"go.k6.io/k6/internal/usage"
2221
"go.k6.io/k6/js/modules"
2322
"go.k6.io/k6/lib"
2423
"go.k6.io/k6/lib/fsext"
@@ -84,7 +83,8 @@ func loadLocalTest(gs *state.GlobalState, cmd *cobra.Command, args []string) (*l
8483
val, ok := gs.Env[key]
8584
return val, ok
8685
},
87-
Usage: usage.New(),
86+
Usage: gs.Usage,
87+
SecretsManager: gs.SecretsManager,
8888
}
8989

9090
test := &loadedTest{

internal/cmd/tests/cmd_run_test.go

+67
Original file line numberDiff line numberDiff line change
@@ -2402,3 +2402,70 @@ func TestTypeScriptSupport(t *testing.T) {
24022402
t.Log(stderr)
24032403
assert.Contains(t, stderr, `something 42`)
24042404
}
2405+
2406+
func TestBasicSecrets(t *testing.T) {
2407+
// This is the example it will be nice if we can just run it ,but in this case it needs extra arguments so ... maybe not such a great idea
2408+
t.Parallel()
2409+
mainScript := `
2410+
import secrets from "k6/secrets";
2411+
2412+
export default async () => {
2413+
const my_secret = await secrets.get("cool"); // get secret from a source with the provided identifier
2414+
console.log(my_secret);
2415+
await secrets.get("else"); // get secret from a source with the provided identifier
2416+
console.log(my_secret);
2417+
}
2418+
`
2419+
2420+
ts := NewGlobalTestState(t)
2421+
require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "secrets.js"), []byte(mainScript), 0o644))
2422+
2423+
ts.CmdArgs = []string{"k6", "run", "--secret-source=mock=cool=something,else=source", "secrets.js"}
2424+
2425+
cmd.ExecuteWithGlobalState(ts.GlobalState)
2426+
2427+
stderr := ts.Stderr.String()
2428+
t.Log(stderr)
2429+
2430+
assert.Contains(t, stderr, `level=info msg="***SECRET_REDACTED***" source=console`)
2431+
assert.Contains(t, stderr, `level=info msg="***SECRET_REDACTED***" ***SECRET_REDACTED***=console`)
2432+
}
2433+
2434+
func TestMultipleSecretSources(t *testing.T) {
2435+
// This is the example it will be nice if we can just run it ,but in this case it needs extra arguments so ... maybe not such a great idea
2436+
t.Parallel()
2437+
mainScript := `
2438+
import secrets from "k6/secrets";
2439+
2440+
export default async () => {
2441+
const my_secret = await secrets.source("first").get("cool");
2442+
console.log(my_secret);
2443+
await secrets.source("second").get("else");
2444+
console.log(my_secret);
2445+
try {
2446+
await secrets.source("second").get("unkwown");
2447+
} catch {
2448+
console.log("trigger exception on wrong key")
2449+
}
2450+
await secrets.get("else"); // testing default setting
2451+
}
2452+
`
2453+
2454+
ts := NewGlobalTestState(t)
2455+
require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "secrets.js"), []byte(mainScript), 0o644))
2456+
2457+
ts.CmdArgs = []string{
2458+
"k6", "run",
2459+
"--secret-source=mock=name=first,cool=something",
2460+
"--secret-source=mock=name=second,else=source,default", "secrets.js",
2461+
}
2462+
2463+
cmd.ExecuteWithGlobalState(ts.GlobalState)
2464+
2465+
stderr := ts.Stderr.String()
2466+
t.Log(stderr)
2467+
2468+
assert.Contains(t, stderr, `level=info msg="***SECRET_REDACTED***" source=console`)
2469+
assert.Contains(t, stderr, `level=info msg="***SECRET_REDACTED***" ***SECRET_REDACTED***=console`)
2470+
assert.Contains(t, stderr, `level=info msg="trigger exception on wrong key" ***SECRET_REDACTED***=console`)
2471+
}

internal/cmd/tests/test_state.go

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"go.k6.io/k6/internal/event"
1919
"go.k6.io/k6/internal/lib/testutils"
2020
"go.k6.io/k6/internal/ui/console"
21+
"go.k6.io/k6/internal/usage"
2122
"go.k6.io/k6/lib/fsext"
2223
)
2324

@@ -111,6 +112,7 @@ func NewGlobalTestState(tb testing.TB) *GlobalTestState {
111112
SignalStop: signal.Stop,
112113
Logger: logger,
113114
FallbackLogger: testutils.NewLogger(tb).WithField("fallback", true),
115+
Usage: usage.New(),
114116
}
115117

116118
return ts

internal/js/jsmodules.go

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
expws "go.k6.io/k6/internal/js/modules/k6/experimental/websockets"
1818
"go.k6.io/k6/internal/js/modules/k6/grpc"
1919
"go.k6.io/k6/internal/js/modules/k6/metrics"
20+
"go.k6.io/k6/internal/js/modules/k6/secrets"
2021
"go.k6.io/k6/internal/js/modules/k6/timers"
2122
"go.k6.io/k6/internal/js/modules/k6/webcrypto"
2223
"go.k6.io/k6/internal/js/modules/k6/ws"
@@ -62,6 +63,7 @@ func getInternalJSModules() map[string]interface{} {
6263
"k6/html": html.New(),
6364
"k6/http": http.New(),
6465
"k6/metrics": metrics.New(),
66+
"k6/secrets": secrets.New(),
6567
"k6/ws": ws.New(),
6668
"k6/experimental/grpc": newRemovedModule(
6769
"k6/experimental/grpc has been graduated, please use k6/net/grpc instead." +

0 commit comments

Comments
 (0)