-
Notifications
You must be signed in to change notification settings - Fork 5
/
sandbox.go
358 lines (307 loc) · 8.4 KB
/
sandbox.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
package xaqt
import (
"context"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
uuid "github.com/satori/go.uuid"
)
const (
TmpDirPrefix = "xaqt-"
)
// prepares for execution of user code by creating a temp directory for
// code and input.
//
type sandbox struct {
// sandbox id (uuidV4)
ID string
language ExecutionDetails
code string
stdin string
options options
// docker client connection
docker *client.Client
// wait channel for successful container exit
waitChan <-chan container.ContainerWaitOKBody
// error channel for container error
errChan <-chan error
}
// constructs a new sandbox given...
//
func newSandbox(l ExecutionDetails, code, stdin string, opts options) (*sandbox, error) {
var (
s *sandbox
err error
)
// set the API version to use in an environment variable
// TODO it would be nice to configure based on the docker version
// a user currently has.... not enough time right now so skipping that.
err = os.Setenv("DOCKER_API_VERSION", "1.35")
if err != nil {
return nil, err
}
// init a docker api client
dockerClient, err := client.NewEnvClient()
if err != nil {
// this could occur if docker has not been installed or started
return nil, err
}
// TODO (cw|4.29.2018) if we are spinning up this sandbox from within another docker
// container, we may want to define a bridge network between them (since they will be
// sibling containers). I don't know if this is entirely necessary though...
// THIS NETWORK SETUP SHOULD ACTUALLY GO IN A HIGHER SCOPE (within the struct which
// actually constructs sandboxes). this way we aren't creating and destroying docker
// networks all over the place. instead we should check to see if one has been created.
// define unique network name
// networkName := fmt.Sprintf("xaqt.%s", uuid.NewV4().String())
// setup container bridge network if one doesn't already exist.
// _, err = dockerClient.NetworkCreate(
// context.TODO(),
// networkName,
// types.NetworkCreate{},
// )
// if err != nil {
// return nil, err
// }
s = &sandbox{
ID: uuid.NewV4().String(),
language: l,
code: code,
stdin: stdin,
options: opts,
docker: dockerClient,
}
return s, nil
}
// runs user code within the sandbox after preparing the execution environment.
//
func (s *sandbox) run() (string, error) {
var (
output string
err error
)
err = s.prepare()
if err != nil {
return "", err
}
output, err = s.execute()
if err != nil {
return "", err
}
return output, nil
}
// prepares the execution environment and the sandbox docker container.
//
func (s *sandbox) prepare() error {
var err error
err = s.PrepareTmpDir()
if err != nil {
return err
}
err = s.PrepareContainer()
if err != nil {
return err
}
return nil
}
// prepares the execution environment by copying all resources (user code, input files,
// and execution payload) into a temporary directory.
//
func (s *sandbox) PrepareTmpDir() error {
// create tmp directory for keeping all code and inputs
tmpFolder, err := ioutil.TempDir(s.options.folder, TmpDirPrefix)
if err != nil {
return err
}
// modify perms on tmp dir
if err := os.Chmod(tmpFolder, 0777); err != nil {
return err
}
// record tmpdir for easy deletion
s.options.folder = tmpFolder
// copy the Payload dir into the tmp dir. for more details on what the
// Payload dir is, check out the TODO (cw|4.29.2018) fill this in...
err = s.copyPayload()
if err != nil {
return err
}
// write source file into tmp dir
// TODO (cw|4.29.2018) we should be able to write an arbitrary number of files
// to the tmp dir.
err = ioutil.WriteFile(tmpFolder+"/"+s.language.SourceFile, []byte(s.code), 0777)
if err != nil {
return err
}
// write a file for stdin
err = ioutil.WriteFile(tmpFolder+"/inputFile", []byte(s.stdin), 0777)
if err != nil {
return err
}
return nil
}
// create docker container for running code and stream container's stdout to our stdout.
//
func (s *sandbox) PrepareContainer() error {
var (
ctx = context.Background()
err error
)
// create docker container for executing user code
_, err = s.docker.ContainerCreate(
ctx,
&container.Config{
Image: s.options.image,
Cmd: []string{
"/usercode/script.sh",
s.language.Compiler,
s.language.SourceFile,
s.language.OptionalExecutable,
s.language.CompilerFlags,
},
// run the sandbox container as a specific user.
User: "mysql", // TODO (cw|4.29.2018) change this to a constant
// StopTimeout: s.options.timeout, // TODO this needs to be a *int ...
AttachStdout: true, // TODO (cw|8.21.2018) do we need this?
AttachStderr: true, // TODO (cw|8.21.2018) do we need this?
Tty: true, // TODO (cw|8.21.2018) do we need this?
},
&container.HostConfig{
// remove container from host once it exits
AutoRemove: true,
// specify the mount point(s) for the sandbox
Binds: []string{s.options.folder + ":/usercode"},
},
nil, // no network config currently
s.ID,
)
if err != nil {
return err
}
// setup stdout stream from container
// TODO (cw|8.21.2018) do we need this?
hijackedResp, err := s.docker.ContainerAttach(
ctx,
s.ID,
types.ContainerAttachOptions{
Stream: true,
Stdout: true,
Stderr: true,
},
)
if err != nil {
return err
}
// start hijacking stdout/stderr
// TODO (cw|8.21.2018) do we need this?
go func() {
defer hijackedResp.Close()
io.Copy(os.Stdout, hijackedResp.Reader)
}()
// setup channels to wait for container to stop and be removed.
// NOTE (cw|8.21.2018) we need WaitConditionRemoved since it is apparently
// not enough to just wait for the process to stop. Waiting for the process
// to merely stop resulted in race conditions between the stdout writer in the
// container and this parent process...
s.waitChan, s.errChan = s.docker.ContainerWait(
context.Background(),
s.ID,
container.WaitConditionRemoved,
)
return nil
}
// executes user code within the sandbox docker container.
//
// returns TODO (cw|4.29.2018) ???
//
func (s *sandbox) execute() (string, error) {
var (
ctx = context.Background()
err error
)
// delete temporary directory once we have finished execution
defer os.RemoveAll(s.options.folder)
// okay lets start the container...
err = s.docker.ContainerStart(
ctx,
s.ID,
types.ContainerStartOptions{},
)
if err != nil {
return "", err
}
// wait for the container to stop
select {
case <-s.waitChan:
// ok. the docker process has stopped and the container has been removed.
// get the errors file
errorBytes, err := ioutil.ReadFile(s.options.folder + "/errors")
if err != nil {
// there was an error reading the errors file, perhaps it is missing?
return "", err
}
// did the process error?
if len(errorBytes) > 0 {
// the user code which was run in the container errored.
err = fmt.Errorf("user code error: %s", errorBytes)
return "", err
}
outputBytes, err := ioutil.ReadFile(s.options.folder + "/completed")
if err != nil {
// there was an error reading the completed file, perhaps it is missing?
return "", err
}
// successfully completed
return string(outputBytes), nil
case err = <-s.errChan:
// the damn container errored
return "", err
case <-time.After(s.options.timeout):
// the damn container process timed out
log.Printf("%s timed out", s.language.Compiler)
return "", fmt.Errorf("Timed out")
}
}
func (s *sandbox) copyPayload() error {
source := filepath.Join(s.options.path, "Payload")
dest := filepath.Join(s.options.folder)
directory, err := os.Open(source)
if err != nil {
return err
}
files, err := directory.Readdir(-1)
if err != nil {
return err
}
for _, file := range files {
// read the file
destfile := dest + "/" + file.Name()
sourcefile := source + "/" + file.Name()
bytes, err := ioutil.ReadFile(sourcefile)
if err != nil {
return err
}
// write the file to tmp
err = ioutil.WriteFile(destfile, bytes, 0777)
if err != nil {
return err
}
}
return nil
}
// TODO (cw|4.29.2018) this cleanup should be in Context (which is initialized once in the
// calling code)
// func (s *sandbox) CleanUp() error {
// // remove the network
// err := s.docker.NetworkRemove(context.TODO(), executer.Network)
// if err != nil && !client.IsErrNotFound(err) {
// // something is very wrong here
// panic(err)
// }
// }