Skip to content

Commit 446330d

Browse files
committed
feat: support container variables
1 parent e3e7712 commit 446330d

7 files changed

Lines changed: 157 additions & 6 deletions

File tree

internal/batches/executor/run_steps.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,16 @@ func RunSteps(ctx context.Context, opts *RunStepsOpts) (stepResults []execution.
165165
continue
166166
}
167167

168+
resolvedContainer, err := renderStepContainer(step.Container, &stepContext)
169+
if err != nil {
170+
return nil, errors.Wrapf(err, "failed to resolve image for step %d", i+1)
171+
}
172+
step.Container = resolvedContainer
173+
168174
// We need to grab the digest for the exact image we're using.
169175
img, err := opts.EnsureImage(ctx, step.Container)
170176
if err != nil {
171-
return nil, err
177+
return nil, errors.Wrapf(err, "failed to pull image for step %d: %s", i+1, step.Container)
172178
}
173179
digest, err := img.Digest(ctx)
174180
if err != nil {
@@ -241,6 +247,27 @@ func RunSteps(ctx context.Context, opts *RunStepsOpts) (stepResults []execution.
241247
return stepResults, err
242248
}
243249

250+
func renderStepContainer(container string, stepContext *template.StepContext) (string, error) {
251+
if container == "" {
252+
return "", nil
253+
}
254+
255+
var out bytes.Buffer
256+
if err := template.RenderStepTemplate("step-container", container, &out, stepContext); err != nil {
257+
return "", err
258+
}
259+
260+
resolved := out.String()
261+
if strings.TrimSpace(resolved) == "" {
262+
return "", errors.New("empty image")
263+
}
264+
if strings.Contains(resolved, "${{") {
265+
return "", errors.Errorf("unresolved template in image %q", resolved)
266+
}
267+
268+
return resolved, nil
269+
}
270+
244271
const workDir = "/work"
245272

246273
func executeSingleStep(
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package executor
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/sourcegraph/sourcegraph/lib/batches/template"
9+
)
10+
11+
func TestRenderStepContainer(t *testing.T) {
12+
t.Run("static image", func(t *testing.T) {
13+
got, err := renderStepContainer("alpine:3", &template.StepContext{})
14+
require.NoError(t, err)
15+
require.Equal(t, "alpine:3", got)
16+
})
17+
18+
t.Run("output image", func(t *testing.T) {
19+
got, err := renderStepContainer("${{ outputs.imageName }}", &template.StepContext{
20+
Outputs: map[string]any{"imageName": "alpine:3"},
21+
})
22+
require.NoError(t, err)
23+
require.Equal(t, "alpine:3", got)
24+
})
25+
26+
t.Run("missing output", func(t *testing.T) {
27+
_, err := renderStepContainer("${{ outputs.imageName }}", &template.StepContext{})
28+
require.Error(t, err)
29+
})
30+
}

internal/batches/service/service.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,10 +295,19 @@ func (svc *Service) EnsureDockerImages(
295295
parallelism int,
296296
progress func(done, total int),
297297
) (map[string]docker.Image, error) {
298-
// Figure out the image names used in the batch spec.
298+
// Figure out the concrete image names used in the batch spec. Images that
299+
// still depend on runtime values, such as outputs from earlier steps, are
300+
// resolved and pulled just-in-time by the executor.
299301
names := map[string]struct{}{}
300302
for i := range steps {
301-
names[steps[i].Container] = struct{}{}
303+
isStatic, name, err := templatelib.IsStaticString(steps[i].Container, &templatelib.StepContext{})
304+
if err != nil {
305+
return nil, err
306+
}
307+
if !isStatic {
308+
continue
309+
}
310+
names[name] = struct{}{}
302311
}
303312

304313
total := len(names)

internal/batches/service/service_test.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,9 @@ func TestEnsureDockerImages(t *testing.T) {
113113
}
114114

115115
for name, steps := range map[string][]batcheslib.Step{
116-
"single step": {{Container: "image"}},
117-
"multiple steps": {{Container: "image"}, {Container: "image"}},
116+
"single step": {{Container: "image"}},
117+
"multiple steps": {{Container: "image"}, {Container: "image"}},
118+
"dynamic deferred": {{Container: "${{ outputs.imageName }}"}, {Container: "image"}},
118119
} {
119120
t.Run(name, func(t *testing.T) {
120121
for _, parallelism := range parallelCases {
@@ -265,6 +266,35 @@ some-new-field: Foo bar
265266
`,
266267
expectedErr: errors.New("parsing batch spec: Additional property some-new-field is not allowed"),
267268
},
269+
{
270+
name: "step image alias",
271+
rawSpec: `
272+
name: test-spec
273+
description: A test spec
274+
steps:
275+
- run: echo hi
276+
image: alpine:3
277+
changesetTemplate:
278+
title: Test
279+
body: Test
280+
branch: test
281+
commit:
282+
message: Test
283+
`,
284+
expectedSpec: &batcheslib.BatchSpec{
285+
Name: "test-spec",
286+
Description: "A test spec",
287+
Steps: []batcheslib.Step{
288+
{Run: "echo hi", Container: "alpine:3", Image: "alpine:3"},
289+
},
290+
ChangesetTemplate: &batcheslib.ChangesetTemplate{
291+
Title: "Test",
292+
Body: "Test",
293+
Branch: "test",
294+
Commit: batcheslib.ExpandedGitCommitDescription{Message: "Test"},
295+
},
296+
},
297+
},
268298
{
269299
name: "supported version",
270300
rawSpec: `

lib/batches/batch_spec.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package batches
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"strings"
67

@@ -92,13 +93,42 @@ func (oqor *OnQueryOrRepository) GetBranches() ([]string, error) {
9293
type Step struct {
9394
Run string `json:"run,omitempty" yaml:"run"`
9495
Container string `json:"container,omitempty" yaml:"container"`
96+
Image string `json:"image,omitempty" yaml:"image,omitempty"`
9597
Env env.Environment `json:"env" yaml:"env"`
9698
Files map[string]string `json:"files,omitempty" yaml:"files,omitempty"`
9799
Outputs Outputs `json:"outputs,omitempty" yaml:"outputs,omitempty"`
98100
Mount []Mount `json:"mount,omitempty" yaml:"mount,omitempty"`
99101
If any `json:"if,omitempty" yaml:"if,omitempty"`
100102
}
101103

104+
func (s *Step) normalizeImageAlias() {
105+
if s.Container == "" && s.Image != "" {
106+
s.Container = s.Image
107+
}
108+
}
109+
110+
func (s *Step) UnmarshalJSON(data []byte) error {
111+
type step Step
112+
var decoded step
113+
if err := json.Unmarshal(data, &decoded); err != nil {
114+
return err
115+
}
116+
*s = Step(decoded)
117+
s.normalizeImageAlias()
118+
return nil
119+
}
120+
121+
func (s *Step) UnmarshalYAML(unmarshal func(any) error) error {
122+
type step Step
123+
var decoded step
124+
if err := unmarshal(&decoded); err != nil {
125+
return err
126+
}
127+
*s = Step(decoded)
128+
s.normalizeImageAlias()
129+
return nil
130+
}
131+
102132
func (s *Step) IfCondition() string {
103133
switch v := s.If.(type) {
104134
case bool:

lib/batches/schema/batch_spec_stringdata.go

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/batches/template/partial_eval.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,26 @@ func IsStaticBool(input string, ctx *StepContext) (isStatic bool, boolVal bool,
4242
return true, isTrueOutput(t.Tree.Root), nil
4343
}
4444

45+
// IsStaticString parses the input as a text/template and attempts to evaluate it
46+
// with only the information currently available in StepContext. If any template
47+
// actions remain after partial evaluation, the first return value is false.
48+
func IsStaticString(input string, ctx *StepContext) (isStatic bool, value string, err error) {
49+
t, err := parseAndPartialEval(input, ctx)
50+
if err != nil {
51+
return false, "", err
52+
}
53+
54+
var out bytes.Buffer
55+
for _, n := range t.Tree.Root.Nodes {
56+
if n.Type() != parse.NodeText {
57+
return false, "", nil
58+
}
59+
out.WriteString(n.String())
60+
}
61+
62+
return true, out.String(), nil
63+
}
64+
4565
// parseAndPartialEval parses input as a text/template and then attempts to
4666
// partially evaluate the parts of the template it can evaluate ahead of time
4767
// (meaning: before we've executed any batch spec steps and have a full

0 commit comments

Comments
 (0)