Skip to content
Merged
29 changes: 28 additions & 1 deletion internal/batches/executor/run_steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,16 @@ func RunSteps(ctx context.Context, opts *RunStepsOpts) (stepResults []execution.
continue
}

resolvedContainer, err := renderStepContainer(step.Container, &stepContext)
if err != nil {
return nil, errors.Wrapf(err, "failed to resolve image for step %d", i+1)
}
step.Container = resolvedContainer

// We need to grab the digest for the exact image we're using.
img, err := opts.EnsureImage(ctx, step.Container)
if err != nil {
return nil, err
return nil, errors.Wrapf(err, "failed to pull image for step %d: %s", i+1, step.Container)
}
digest, err := img.Digest(ctx)
if err != nil {
Expand Down Expand Up @@ -241,6 +247,27 @@ func RunSteps(ctx context.Context, opts *RunStepsOpts) (stepResults []execution.
return stepResults, err
}

func renderStepContainer(container string, stepContext *template.StepContext) (string, error) {
if container == "" {
return "", nil
}

var out bytes.Buffer
if err := template.RenderStepTemplate("step-container", container, &out, stepContext); err != nil {
return "", err
}

resolved := out.String()
if strings.TrimSpace(resolved) == "" {
return "", errors.New("empty image")
}
if strings.Contains(resolved, "${{") {
return "", errors.Errorf("unresolved template in image %q", resolved)
}

return resolved, nil
}

const workDir = "/work"

func executeSingleStep(
Expand Down
30 changes: 30 additions & 0 deletions internal/batches/executor/run_steps_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package executor

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/sourcegraph/sourcegraph/lib/batches/template"
)

func TestRenderStepContainer(t *testing.T) {
t.Run("static image", func(t *testing.T) {
got, err := renderStepContainer("alpine:3", &template.StepContext{})
require.NoError(t, err)
require.Equal(t, "alpine:3", got)
})

t.Run("output image", func(t *testing.T) {
got, err := renderStepContainer("${{ outputs.imageName }}", &template.StepContext{
Outputs: map[string]any{"imageName": "alpine:3"},
})
require.NoError(t, err)
require.Equal(t, "alpine:3", got)
})

t.Run("missing output", func(t *testing.T) {
_, err := renderStepContainer("${{ outputs.imageName }}", &template.StepContext{})
require.Error(t, err)
})
}
13 changes: 11 additions & 2 deletions internal/batches/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,10 +295,19 @@ func (svc *Service) EnsureDockerImages(
parallelism int,
progress func(done, total int),
) (map[string]docker.Image, error) {
// Figure out the image names used in the batch spec.
// Figure out the concrete image names used in the batch spec. Images that
// still depend on runtime values, such as outputs from earlier steps, are
// resolved and pulled just-in-time by the executor.
names := map[string]struct{}{}
for i := range steps {
names[steps[i].Container] = struct{}{}
isStatic, name, err := templatelib.IsStaticString(steps[i].Container, &templatelib.StepContext{})
if err != nil {
return nil, err
}
if !isStatic {
continue
}
names[name] = struct{}{}
}

total := len(names)
Expand Down
5 changes: 3 additions & 2 deletions internal/batches/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,9 @@ func TestEnsureDockerImages(t *testing.T) {
}

for name, steps := range map[string][]batcheslib.Step{
"single step": {{Container: "image"}},
"multiple steps": {{Container: "image"}, {Container: "image"}},
"single step": {{Container: "image"}},
"multiple steps": {{Container: "image"}, {Container: "image"}},
"dynamic deferred": {{Container: "${{ outputs.imageName }}"}, {Container: "image"}},
} {
t.Run(name, func(t *testing.T) {
for _, parallelism := range parallelCases {
Expand Down
20 changes: 20 additions & 0 deletions lib/batches/template/partial_eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ func IsStaticBool(input string, ctx *StepContext) (isStatic bool, boolVal bool,
return true, isTrueOutput(t.Tree.Root), nil
}

// IsStaticString parses the input as a text/template and attempts to evaluate it
// with only the information currently available in StepContext. If any template
// actions remain after partial evaluation, the first return value is false.
func IsStaticString(input string, ctx *StepContext) (isStatic bool, value string, err error) {
t, err := parseAndPartialEval(input, ctx)
if err != nil {
return false, "", err
}

var out bytes.Buffer
for _, n := range t.Tree.Root.Nodes {
if n.Type() != parse.NodeText {
return false, "", nil
}
out.WriteString(n.String())
}

return true, out.String(), nil
}

// parseAndPartialEval parses input as a text/template and then attempts to
// partially evaluate the parts of the template it can evaluate ahead of time
// (meaning: before we've executed any batch spec steps and have a full
Expand Down
Loading