Skip to content

Commit 6406644

Browse files
committed
runtime: implement building image in Docker runtime
1 parent b84534f commit 6406644

File tree

14 files changed

+212
-46
lines changed

14 files changed

+212
-46
lines changed

experimental/runme.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ project:
4747
runtime:
4848
# Optional Docker configuration to run code blocks in a container.
4949
docker:
50-
enabled: false
50+
enabled: true
5151
image: runme-runtime:latest
5252
build:
5353
context: ./experimental/docker

go.mod

+8
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ require (
6161
cloud.google.com/go/iam v1.4.0 // indirect
6262
cloud.google.com/go/longrunning v0.6.4 // indirect
6363
dario.cat/mergo v1.0.1 // indirect
64+
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
6465
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
6566
github.com/atombender/go-jsonschema v0.16.0 // indirect
6667
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
@@ -70,6 +71,7 @@ require (
7071
github.com/charmbracelet/x/term v0.2.1 // indirect
7172
github.com/chavacava/garif v0.1.0 // indirect
7273
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect
74+
github.com/containerd/log v0.1.0 // indirect
7375
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
7476
github.com/distribution/reference v0.6.0 // indirect
7577
github.com/docker/go-connections v0.5.0 // indirect
@@ -91,10 +93,15 @@ require (
9193
github.com/gookit/color v1.5.4 // indirect
9294
github.com/hashicorp/go-version v1.7.0 // indirect
9395
github.com/icholy/gomajor v0.13.1 // indirect
96+
github.com/klauspost/compress v1.17.11 // indirect
9497
github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 // indirect
9598
github.com/mgechev/revive v1.7.0 // indirect
9699
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
97100
github.com/moby/docker-image-spec v1.3.1 // indirect
101+
github.com/moby/patternmatcher v0.6.0 // indirect
102+
github.com/moby/sys/sequential v0.6.0 // indirect
103+
github.com/moby/sys/user v0.1.0 // indirect
104+
github.com/moby/sys/userns v0.1.0 // indirect
98105
github.com/moby/term v0.5.0 // indirect
99106
github.com/morikuni/aec v1.0.0 // indirect
100107
github.com/olekukonko/tablewriter v0.0.5 // indirect
@@ -103,6 +110,7 @@ require (
103110
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
104111
github.com/sanity-io/litter v1.5.5 // indirect
105112
github.com/securego/gosec/v2 v2.22.1 // indirect
113+
github.com/sirupsen/logrus v1.9.3 // indirect
106114
github.com/spf13/afero v1.12.0 // indirect
107115
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
108116
go.opentelemetry.io/auto/sdk v1.1.0 // indirect

go.sum

+12
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ cloud.google.com/go/secretmanager v1.14.5 h1:W++V0EL9iL6T2+ec24Dm++bIti0tI6Gx6sC
1616
cloud.google.com/go/secretmanager v1.14.5/go.mod h1:GXznZF3qqPZDGZQqETZwZqHw4R6KCaYVvcGiRBA+aqY=
1717
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
1818
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
19+
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
20+
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
1921
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
2022
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
2123
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
@@ -206,6 +208,8 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4
206208
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
207209
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
208210
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
211+
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
212+
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
209213
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
210214
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
211215
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -239,6 +243,14 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ
239243
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
240244
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
241245
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
246+
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
247+
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
248+
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
249+
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
250+
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
251+
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
252+
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
253+
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
242254
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
243255
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
244256
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=

internal/command/command.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,15 @@ func (c *base) Running() bool {
6363
}
6464

6565
func (c *base) Start(context.Context) error {
66-
return errors.New("not implemented")
66+
return errors.New("start: not implemented")
6767
}
6868

6969
func (c *base) Signal(os.Signal) error {
70-
return errors.New("not implemented")
70+
return errors.New("signal: not implemented")
7171
}
7272

7373
func (c *base) Wait(context.Context) error {
74-
return errors.New("not implemented")
74+
return errors.New("wait: not implemented")
7575
}
7676

7777
func (c *base) Env() []string {

internal/command/command_test.go

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"io"
7+
"os/exec"
78
"testing"
89

910
"github.com/stretchr/testify/assert"
@@ -39,6 +40,10 @@ func testExecuteCommandWithSession(
3940
) {
4041
t.Helper()
4142

43+
if _, err := exec.LookPath(cfg.ProgramName); err != nil {
44+
t.Skipf("command %q not found", cfg.ProgramName)
45+
}
46+
4247
stdout := bytes.NewBuffer(nil)
4348
stderr := bytes.NewBuffer(nil)
4449

internal/command/env_collector_fifo_unix.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func newEnvCollectorFifo(
4747
temp: temp,
4848
}
4949

50-
if c.init() != nil {
50+
if err := c.init(); err != nil {
5151
return nil, err
5252
}
5353

internal/command/factory.go

+25-14
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ func WithRuntime(r Runtime) FactoryOption {
7979
}
8080
}
8181

82+
func WithUseSystemEnv(val bool) FactoryOption {
83+
return func(f *commandFactory) {
84+
f.useSystemEnv = val
85+
}
86+
}
87+
8288
func NewFactory(opts ...FactoryOption) Factory {
8389
f := &commandFactory{}
8490
for _, opt := range opts {
@@ -91,11 +97,12 @@ func NewFactory(opts ...FactoryOption) Factory {
9197
}
9298

9399
type commandFactory struct {
94-
debug bool
95-
docker *dockerexec.Docker
96-
logger *zap.Logger
97-
project *project.Project
98-
runtime Runtime
100+
debug bool
101+
docker *dockerexec.Docker
102+
logger *zap.Logger
103+
project *project.Project
104+
runtime Runtime
105+
useSystemEnv bool
99106
}
100107

101108
// Build creates a new command based on the provided [ProgramConfig] and [CommandOptions].
@@ -214,19 +221,11 @@ func (f *commandFactory) Build(cfg *ProgramConfig, opts CommandOptions) (Command
214221
}
215222

216223
func (f *commandFactory) buildBase(cfg *ProgramConfig, opts CommandOptions) *base {
217-
runtime := f.runtime
218-
219-
if isNil(runtime) && f.docker != nil {
220-
runtime = &dockerRuntime{Docker: f.docker}
221-
} else if isNil(runtime) {
222-
runtime = &hostRuntime{useSystem: true}
223-
}
224-
225224
return &base{
226225
cfg: cfg,
227226
logger: f.getLogger("Base"),
228227
project: f.project,
229-
runtime: runtime,
228+
runtime: f.getRuntime(),
230229
session: opts.Session,
231230
stdin: opts.Stdin,
232231
stdout: opts.Stdout,
@@ -306,6 +305,18 @@ func (f *commandFactory) getLogger(name string) *zap.Logger {
306305
return f.logger.Named(name).With(zap.String("instanceID", id))
307306
}
308307

308+
func (f *commandFactory) getRuntime() Runtime {
309+
runtime := f.runtime
310+
311+
if isNil(runtime) && f.docker != nil {
312+
runtime = &dockerRuntime{Docker: f.docker}
313+
} else if isNil(runtime) {
314+
runtime = &hostRuntime{useSystem: f.useSystemEnv}
315+
}
316+
317+
return runtime
318+
}
319+
309320
func isNil(val any) bool {
310321
if val == nil {
311322
return true

internal/config/autoconfig/autoconfig.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,14 @@ func getClientFactory(cfg *config.Config, logger *zap.Logger) (ClientFactory, er
120120
}, nil
121121
}
122122

123-
func getCommandFactory(docker *dockerexec.Docker, logger *zap.Logger, proj *project.Project) command.Factory {
124-
return command.NewFactory(
123+
func getCommandFactory(cfg *config.Config, docker *dockerexec.Docker, logger *zap.Logger, proj *project.Project) command.Factory {
124+
opts := []command.FactoryOption{
125125
command.WithDocker(docker),
126126
command.WithLogger(logger),
127127
command.WithProject(proj),
128-
)
128+
command.WithUseSystemEnv(cfg.Project.Env.UseSystemEnv),
129+
}
130+
return command.NewFactory(opts...)
129131
}
130132

131133
func getConfigLoader() (*config.Loader, error) {

internal/dockerexec/docker.go

+70-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
package dockerexec
22

33
import (
4+
"bufio"
45
"context"
56
"encoding/hex"
7+
"encoding/json"
68
"io"
9+
"math/rand"
710
"time"
811

12+
"github.com/docker/docker/api/types"
913
"github.com/docker/docker/api/types/filters"
1014
"github.com/docker/docker/api/types/image"
1115
"github.com/docker/docker/client"
16+
"github.com/docker/docker/pkg/archive"
1217
"github.com/pkg/errors"
1318
"go.uber.org/zap"
14-
"golang.org/x/exp/rand"
1519
)
1620

1721
type Options struct {
@@ -34,7 +38,7 @@ func New(opts *Options) (*Docker, error) {
3438
logger = zap.NewNop()
3539
}
3640

37-
rnd := rand.New(rand.NewSource(uint64(time.Now().UnixNano())))
41+
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
3842

3943
d := &Docker{
4044
client: c,
@@ -77,6 +81,14 @@ func (d *Docker) CommandContext(ctx context.Context, program string, args ...str
7781
}
7882
}
7983

84+
func (d *Docker) RemoveImage(ctx context.Context) error {
85+
_, err := d.client.ImageRemove(ctx, d.image, image.RemoveOptions{Force: true, PruneChildren: true})
86+
if err != nil {
87+
return errors.WithStack(err)
88+
}
89+
return nil
90+
}
91+
8092
func (d *Docker) containerUniqueName() string {
8193
var hash [4]byte
8294
_, _ = d.rnd.Read(hash[:])
@@ -90,8 +102,62 @@ func (d *Docker) buildOrPullImage(ctx context.Context) error {
90102
return d.pullImage(ctx)
91103
}
92104

93-
func (d *Docker) buildImage(context.Context) error {
94-
return errors.New("not implemented")
105+
func (d *Docker) buildImage(ctx context.Context) error {
106+
tar, err := archive.TarWithOptions(d.buildContext, &archive.TarOptions{})
107+
if err != nil {
108+
return errors.WithMessage(err, "failed to create tar archive")
109+
}
110+
111+
resp, err := d.client.ImageBuild(
112+
ctx,
113+
tar,
114+
types.ImageBuildOptions{
115+
Dockerfile: d.dockerfile,
116+
Tags: []string{d.image},
117+
Remove: true,
118+
ForceRemove: true,
119+
NoCache: true,
120+
},
121+
)
122+
if err != nil {
123+
return errors.WithMessage(err, "failed to build image")
124+
}
125+
defer resp.Body.Close()
126+
127+
return errors.WithStack(
128+
printLogs(resp.Body, d.logger),
129+
)
130+
}
131+
132+
func printLogs(r io.Reader, logger *zap.Logger) error {
133+
type errorLine struct {
134+
Error string `json:"error"`
135+
ErrorDetail struct {
136+
Message string `json:"message"`
137+
} `json:"errorDetail"`
138+
}
139+
140+
var lastLine string
141+
142+
scanner := bufio.NewScanner(r)
143+
144+
for scanner.Scan() {
145+
lastLine = scanner.Text()
146+
logger.Debug("docker build", zap.String("log", lastLine))
147+
}
148+
149+
if err := scanner.Err(); err != nil {
150+
return errors.WithMessage(err, "docker build")
151+
}
152+
153+
errLine := errorLine{}
154+
if err := json.Unmarshal([]byte(lastLine), &errLine); err != nil {
155+
return errors.WithMessage(err, "docker build")
156+
}
157+
if errLine.Error != "" {
158+
return errors.Errorf("docker build: %s", errLine.Error)
159+
}
160+
return nil
95161
}
96162

97163
func (d *Docker) pullImage(ctx context.Context) error {

0 commit comments

Comments
 (0)