Skip to content

Commit d9f769e

Browse files
committed
feat: implement secrets in multihttp
1 parent 680aebe commit d9f769e

File tree

5 files changed

+1041
-62
lines changed

5 files changed

+1041
-62
lines changed

internal/prober/interpolation/interpolation.go

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
var VariableRegex = regexp.MustCompile(`\$\{([a-zA-Z_][a-zA-Z0-9_-]*)\}`)
1515

1616
// SecretRegex matches ${secrets.secret_name} patterns
17-
var SecretRegex = regexp.MustCompile(`\$\{secrets\.([^}]*)\}`)
17+
var SecretRegex = regexp.MustCompile(`\$\{secrets\.([a-zA-Z0-9_][a-zA-Z0-9_\.\-]*)\}`)
1818

1919
// VariableProvider defines the interface for resolving variables
2020
type VariableProvider interface {
@@ -219,6 +219,100 @@ func ToJavaScript(value string) string {
219219
return s.String()
220220
}
221221

222+
// ToJavaScriptWithSecrets converts a string with both variable and secret interpolation to JavaScript code
223+
// This is used by multihttp to generate JavaScript that references both variables and secrets
224+
func ToJavaScriptWithSecrets(value string) string {
225+
if len(value) == 0 {
226+
return `''`
227+
}
228+
229+
var s strings.Builder
230+
buf := []byte(value)
231+
232+
// First handle secret variables
233+
locs := SecretRegex.FindAllSubmatchIndex(buf, -1)
234+
p := 0
235+
236+
for _, loc := range locs {
237+
if len(loc) < 4 {
238+
panic("unexpected result while building JavaScript")
239+
}
240+
241+
if s.Len() > 0 {
242+
s.WriteRune('+')
243+
}
244+
245+
if pre := buf[p:loc[0]]; len(pre) > 0 {
246+
s.WriteRune('\'')
247+
escapeJavaScript(&s, pre)
248+
s.WriteRune('\'')
249+
s.WriteRune('+')
250+
}
251+
252+
// Generate async secret lookup
253+
s.WriteString(`await secrets.get('`)
254+
s.Write(buf[loc[2]:loc[3]])
255+
s.WriteString(`')`)
256+
257+
p = loc[1]
258+
}
259+
260+
// Then handle regular variables in the remaining text
261+
remainingText := buf[p:]
262+
if len(remainingText) > 0 {
263+
regularLocs := VariableRegex.FindAllSubmatchIndex(remainingText, -1)
264+
265+
if len(regularLocs) > 0 {
266+
if s.Len() > 0 {
267+
s.WriteRune('+')
268+
}
269+
270+
p2 := 0
271+
for _, loc := range regularLocs {
272+
if len(loc) < 4 {
273+
panic("unexpected result while building JavaScript")
274+
}
275+
276+
if s.Len() > 0 {
277+
s.WriteRune('+')
278+
}
279+
280+
if pre := remainingText[p2:loc[0]]; len(pre) > 0 {
281+
s.WriteRune('\'')
282+
escapeJavaScript(&s, pre)
283+
s.WriteRune('\'')
284+
s.WriteRune('+')
285+
}
286+
287+
s.WriteString(`vars['`)
288+
s.Write(remainingText[loc[2]:loc[3]])
289+
s.WriteString(`']`)
290+
291+
p2 = loc[1]
292+
}
293+
294+
if len(remainingText[p2:]) > 0 {
295+
if s.Len() > 0 {
296+
s.WriteRune('+')
297+
}
298+
s.WriteRune('\'')
299+
escapeJavaScript(&s, remainingText[p2:])
300+
s.WriteRune('\'')
301+
}
302+
} else {
303+
// No regular variables, just append the remaining text
304+
if s.Len() > 0 {
305+
s.WriteRune('+')
306+
}
307+
s.WriteRune('\'')
308+
escapeJavaScript(&s, remainingText)
309+
s.WriteRune('\'')
310+
}
311+
}
312+
313+
return s.String()
314+
}
315+
222316
// escapeJavaScript escapes a byte slice for use in JavaScript strings
223317
func escapeJavaScript(s *strings.Builder, buf []byte) {
224318
for _, b := range buf {
@@ -235,10 +329,18 @@ func escapeJavaScript(s *strings.Builder, buf []byte) {
235329
s.WriteString(`\r`)
236330
case '\t':
237331
s.WriteString(`\t`)
332+
case '=':
333+
s.WriteString(`\u003D`)
334+
case '>':
335+
s.WriteString(`\u003E`)
336+
case '<':
337+
s.WriteString(`\u003C`)
338+
case '&':
339+
s.WriteString(`\u0026`)
238340
default:
239-
if b < 32 || b > 126 {
240-
// Escape non-printable characters
241-
fmt.Fprintf(s, `\x%02x`, b)
341+
if b < 32 {
342+
// Escape control characters using Unicode escape
343+
fmt.Fprintf(s, `\u%04X`, b)
242344
} else {
243345
s.WriteByte(b)
244346
}

internal/prober/interpolation/interpolation_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ func TestResolver_Resolve(t *testing.T) {
116116
"empty secret name": {
117117
input: "${secrets.}",
118118
secretEnabled: true,
119-
expectedOutput: "",
120-
expectError: true,
119+
expectedOutput: "${secrets.}",
120+
expectError: false,
121121
},
122122
"invalid secret name": {
123123
input: "${secrets.invalid-name}",

internal/prober/multihttp/script.go

Lines changed: 78 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010
"text/template"
1111

12+
"github.com/grafana/synthetic-monitoring-agent/internal/prober/interpolation"
1213
sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring"
1314
)
1415

@@ -20,52 +21,7 @@ var templateFS embed.FS
2021
var userVariables = regexp.MustCompile(`\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}`)
2122

2223
func performVariableExpansion(in string) string {
23-
if len(in) == 0 {
24-
return `''`
25-
}
26-
27-
var s strings.Builder
28-
buf := []byte(in)
29-
locs := userVariables.FindAllSubmatchIndex(buf, -1)
30-
31-
p := 0
32-
for _, loc := range locs {
33-
if len(loc) < 4 { // put the bounds checker at ease
34-
panic("unexpected result while building URL")
35-
}
36-
37-
if s.Len() > 0 {
38-
s.WriteRune('+')
39-
}
40-
41-
if pre := buf[p:loc[0]]; len(pre) > 0 {
42-
s.WriteRune('\'')
43-
template.JSEscape(&s, pre)
44-
s.WriteRune('\'')
45-
s.WriteRune('+')
46-
}
47-
48-
s.WriteString(`vars['`)
49-
// Because of the capture in the regular expression, the result
50-
// has two indices that represent the matched substring, and
51-
// two more indices that represent the capture group.
52-
s.Write(buf[loc[2]:loc[3]])
53-
s.WriteString(`']`)
54-
55-
p = loc[1]
56-
}
57-
58-
if len(buf[p:]) > 0 {
59-
if s.Len() > 0 {
60-
s.WriteRune('+')
61-
}
62-
63-
s.WriteRune('\'')
64-
template.JSEscape(&s, buf[p:])
65-
s.WriteRune('\'')
66-
}
67-
68-
return s.String()
24+
return interpolation.ToJavaScriptWithSecrets(in)
6925
}
7026

7127
// Query params must be appended to a URL that has already been created.
@@ -113,13 +69,19 @@ func interpolateBodyVariables(bodyVarName string, body *sm.HttpRequestBody) []st
11369
default:
11470
var buf strings.Builder
11571

116-
matches := userVariables.FindAllString(string(body.Payload), -1)
72+
// Find both regular and secret variables using submatch indices
73+
regularMatches := userVariables.FindAllSubmatchIndex(body.Payload, -1)
74+
secretMatches := interpolation.SecretRegex.FindAllSubmatchIndex(body.Payload, -1)
75+
11776
parsedMatches := make(map[string]struct{})
118-
out := make([]string, 0, len(matches))
77+
out := make([]string, 0, len(regularMatches)+len(secretMatches))
11978

120-
// For every instance of ${variable} in the body,
121-
// this block returns {bodyVarName} = {bodyVarName}.replaceAll('${variable}', vars['variable'])
122-
for _, m := range matches {
79+
// Handle regular variables
80+
for _, match := range regularMatches {
81+
if len(match) < 4 {
82+
continue
83+
}
84+
m := string(body.Payload[match[0]:match[1]])
12385
if _, found := parsedMatches[m]; found {
12486
continue
12587
}
@@ -132,15 +94,38 @@ func interpolateBodyVariables(bodyVarName string, body *sm.HttpRequestBody) []st
13294
buf.WriteString(m)
13395
buf.WriteString("', vars['")
13496
// writing the variable name from between ${ and }
135-
for i := 2; i < len(m)-1; i++ {
136-
buf.WriteByte(m[i])
137-
}
97+
buf.Write(body.Payload[match[2]:match[3]])
13898
buf.WriteString("'])")
13999
out = append(out, buf.String())
140100

141101
parsedMatches[m] = struct{}{}
142102
}
143103

104+
// Handle secret variables
105+
for _, match := range secretMatches {
106+
if len(match) < 4 {
107+
continue
108+
}
109+
m := string(body.Payload[match[0]:match[1]])
110+
if _, found := parsedMatches[m]; found {
111+
continue
112+
}
113+
114+
buf.Reset()
115+
buf.WriteString(bodyVarName)
116+
buf.WriteString("=")
117+
buf.WriteString(bodyVarName)
118+
buf.WriteString(".replaceAll('")
119+
buf.WriteString(m)
120+
buf.WriteString("', await secrets.get('")
121+
// writing the secret name from the capture group
122+
buf.Write(body.Payload[match[2]:match[3]])
123+
buf.WriteString("'))")
124+
out = append(out, buf.String())
125+
126+
parsedMatches[m] = struct{}{}
127+
}
128+
144129
return out
145130
}
146131
}
@@ -422,6 +407,35 @@ func buildVars(variable *sm.MultiHttpEntryVariable) string {
422407
return b.String()
423408
}
424409

410+
func hasSecretVariables(settings *sm.MultiHttpSettings) bool {
411+
for _, entry := range settings.Entries {
412+
// Check URL
413+
if interpolation.SecretRegex.MatchString(entry.Request.Url) {
414+
return true
415+
}
416+
417+
// Check headers
418+
for _, header := range entry.Request.Headers {
419+
if interpolation.SecretRegex.MatchString(header.Value) {
420+
return true
421+
}
422+
}
423+
424+
// Check query fields
425+
for _, field := range entry.Request.QueryFields {
426+
if interpolation.SecretRegex.MatchString(field.Name) || interpolation.SecretRegex.MatchString(field.Value) {
427+
return true
428+
}
429+
}
430+
431+
// Check body
432+
if entry.Request.Body != nil && interpolation.SecretRegex.MatchString(string(entry.Request.Body.Payload)) {
433+
return true
434+
}
435+
}
436+
return false
437+
}
438+
425439
func settingsToScript(settings *sm.MultiHttpSettings) ([]byte, error) {
426440
// Convert settings to script using a Go template
427441
tmpl, err := template.
@@ -442,9 +456,18 @@ func settingsToScript(settings *sm.MultiHttpSettings) ([]byte, error) {
442456

443457
var buf bytes.Buffer
444458

459+
// Create template data with secret variable detection
460+
templateData := struct {
461+
*sm.MultiHttpSettings
462+
HasSecretVariables bool
463+
}{
464+
MultiHttpSettings: settings,
465+
HasSecretVariables: hasSecretVariables(settings),
466+
}
467+
445468
// TODO(mem): figure out if we need to transform the data in some way
446469
// before executing the template
447-
if err := tmpl.ExecuteTemplate(&buf, "script.tmpl", settings); err != nil {
470+
if err := tmpl.ExecuteTemplate(&buf, "script.tmpl", templateData); err != nil {
448471
return nil, fmt.Errorf("executing script template: %w", err)
449472
}
450473

internal/prober/multihttp/script.tmpl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { test } from 'k6/execution';
77
import encoding from 'k6/encoding';
88
import jsonpath from 'https://jslib.k6.io/jsonpath/1.0.2/index.js';
99
import { URL } from 'https://jslib.k6.io/url/1.0.0/index.js';
10+
{{ if .HasSecretVariables }}import secrets from 'k6/secrets';{{ end }}
1011

1112
export const options = {
1213
scenarios: {
@@ -65,7 +66,7 @@ function assertHeader(headers, name, matcher) {
6566
return false;
6667
}
6768

68-
export default function() {
69+
export default {{ if .HasSecretVariables }}async {{ end }}function() {
6970
let response;
7071
let body;
7172
let url;

0 commit comments

Comments
 (0)