Skip to content

Commit 4985aec

Browse files
timofurrerGitLab
authored andcommitted
Merge branch 'no-job-inputs-no-interpolate' into 'main'
Avoid interpolation without defined job inputs See merge request https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/6374 Merged-by: Timo Furrer <tfurrer@gitlab.com> Approved-by: Axel von Bertoldi <avonbertoldi@gitlab.com>
2 parents b992157 + 70b43ad commit 4985aec

3 files changed

Lines changed: 124 additions & 0 deletions

File tree

common/build.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,8 @@ func (b *Build) expandInputs() error {
13021302
return nil
13031303
}
13041304

1305+
b.Inputs.SetLogger(b.Log())
1306+
13051307
return spec.ExpandInputs(&b.Inputs, b)
13061308
}
13071309

common/spec/inputs.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"reflect"
99
"slices"
1010

11+
"github.com/sirupsen/logrus"
1112
"gitlab.com/gitlab-org/moa"
1213
"gitlab.com/gitlab-org/moa/ast"
1314
"gitlab.com/gitlab-org/moa/value"
@@ -18,6 +19,7 @@ type Inputs struct {
1819
inputs []expression.Input
1920
evaluator *expression.Evaluator
2021
metricsCollector *JobInputsMetricsCollector
22+
logger *logrus.Entry
2123
}
2224

2325
type JobInput struct {
@@ -78,6 +80,8 @@ var (
7880
ErrSensitiveUnsupported = errors.New("sensitive inputs are unsupported in interpolations yet")
7981
// errInterpolationFound defines a sentinel error for when an interpolation was detected
8082
errInterpolationFound = errors.New("interpolation found")
83+
// errJobInputAccessFound defines a sentinel error for when a job input access pattern is detected
84+
errJobInputAccessFound = errors.New("job input access found")
8185
)
8286

8387
// interpolationDetector is a visitor that detects if the AST contains an Interpolation
@@ -95,6 +99,56 @@ func (v *interpolationDetector) Exit(expr ast.Expr) (ast.Expr, error) {
9599
return expr, nil
96100
}
97101

102+
// jobInputDetector is a visitor that detects if the AST contains access to job inputs.
103+
// It looks for the pattern job.inputs.<key> in the expression tree, where <key> is
104+
// accessed via static property access (dot notation).
105+
//
106+
// This detector only finds static access patterns like:
107+
// - job.inputs.username
108+
// - job.inputs.foo.bar (nested access on an input)
109+
// - str(job.inputs.age) (input used as function argument)
110+
//
111+
// It does NOT detect dynamic access patterns like:
112+
// - job.inputs[key] (bracket notation with variable)
113+
// - job["inputs"].foo (bracket notation for "inputs")
114+
// - job["in" + "puts"].foo (computed property access)
115+
//
116+
// The visitor returns a sentinel error as soon as it encounters the first match,
117+
// exiting traversal early.
118+
type jobInputDetector struct{}
119+
120+
func (v *jobInputDetector) Enter(expr ast.Expr) (ast.Visitor, error) {
121+
selector, ok := expr.(*ast.Selector)
122+
if !ok {
123+
return v, nil
124+
}
125+
126+
fromSelector, ok := selector.From.(*ast.Selector)
127+
if !ok {
128+
return v, nil
129+
}
130+
131+
ident, ok := fromSelector.From.(*ast.Ident)
132+
if !ok || ident.Name != "job" {
133+
return v, nil
134+
}
135+
136+
lit, ok := fromSelector.Select.(*ast.Literal)
137+
if !ok {
138+
return v, nil
139+
}
140+
141+
if lit.String() == "inputs" {
142+
return nil, errJobInputAccessFound
143+
}
144+
145+
return v, nil
146+
}
147+
148+
func (v *jobInputDetector) Exit(expr ast.Expr) (ast.Expr, error) {
149+
return expr, nil
150+
}
151+
98152
func (i *JobInput) UnmarshalJSON(data []byte) error {
99153
type alias JobInput
100154
if err := json.Unmarshal(data, (*alias)(i)); err != nil {
@@ -235,6 +289,11 @@ func (i *Inputs) SetMetricsCollector(collector *JobInputsMetricsCollector) {
235289
i.metricsCollector = collector
236290
}
237291

292+
// SetLogger injects the logger
293+
func (i *Inputs) SetLogger(logger *logrus.Entry) {
294+
i.logger = logger
295+
}
296+
238297
func (i *Inputs) Expand(text string) (string, error) {
239298
if i == nil || i.evaluator == nil {
240299
return text, nil
@@ -246,6 +305,25 @@ func (i *Inputs) Expand(text string) (string, error) {
246305
return "", &InputInterpolationError{err: err}
247306
}
248307

308+
// NOTE: check if we don't have any inputs defined to interpolate
309+
// We do this to avoid a breaking change when a user already uses
310+
// job input interpolation syntax (`${{ .. }}`) but doesn't actually
311+
// want to use them. This hides potential errors when a user forgets
312+
// to define inputs - but that's easier to debug and not a breaking
313+
// change once GitLab enables job inputs but rather at the point in
314+
// time when the user wants to use job inputs.
315+
// For context see:
316+
// https://gitlab.com/gitlab-org/step-runner/-/work_items/369
317+
if len(i.inputs) == 0 {
318+
_, walkErr := expr.Walk(&jobInputDetector{})
319+
if errors.Is(walkErr, errJobInputAccessFound) {
320+
if i.logger != nil {
321+
i.logger.Warn("job input interpolation syntax detected but no inputs are defined, therefore job interpolation is not performed")
322+
}
323+
}
324+
return text, nil
325+
}
326+
249327
result, err := i.evaluator.Eval(text, expr)
250328
if err != nil {
251329
i.metricsCollector.recordEvalError()

common/spec/inputs_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,3 +433,47 @@ func TestInputsTag(t *testing.T) {
433433
assert.Equal(t, customInputExpander("REDACTED"), jobResponse.CustomInputExpanderToExpand)
434434
assert.Equal(t, customInputExpander("${{ job.inputs.any }}"), jobResponse.CustomInputExpanderNotToExpand)
435435
}
436+
437+
func TestJobInputs_Expand_NoInputsDefined(t *testing.T) {
438+
t.Parallel()
439+
440+
tests := []struct {
441+
name string
442+
text string
443+
expected string
444+
}{
445+
{
446+
name: "no job input access",
447+
text: "Hello ${{ 1 + 2 }}",
448+
expected: "Hello ${{ 1 + 2 }}",
449+
},
450+
{
451+
name: "with job input access",
452+
text: "Hello ${{ job.inputs.username }}",
453+
expected: "Hello ${{ job.inputs.username }}",
454+
},
455+
{
456+
name: "plain text",
457+
text: "Hello world",
458+
expected: "Hello world",
459+
},
460+
{
461+
name: "other selector",
462+
text: "${{ foo.bar.baz }}",
463+
expected: "${{ foo.bar.baz }}",
464+
},
465+
}
466+
467+
for _, tt := range tests {
468+
t.Run(tt.name, func(t *testing.T) {
469+
t.Parallel()
470+
471+
inputs := Inputs{}
472+
inputs.SetMetricsCollector(NewJobInputsMetricsCollector())
473+
474+
expanded, err := inputs.Expand(tt.text)
475+
require.NoError(t, err)
476+
assert.Equal(t, tt.expected, expanded)
477+
})
478+
}
479+
}

0 commit comments

Comments
 (0)