This repository has been archived by the owner on Feb 27, 2020. It is now read-only.
/
mocksandbox.go
382 lines (354 loc) · 10 KB
/
mocksandbox.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
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
package mockengine
import (
"bytes"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync"
"time"
"github.com/taskcluster/taskcluster-worker/engines"
"github.com/taskcluster/taskcluster-worker/runtime"
"github.com/taskcluster/taskcluster-worker/runtime/atomics"
"github.com/taskcluster/taskcluster-worker/runtime/ioext"
)
type mount struct {
volume *volume
readOnly bool
}
// In this example it is easier to just implement with one object.
// This way we won't have to pass data between different instances.
// In larger more complex engines that downloads stuff, etc. it's probably not
// a good idea to implement everything in one structure.
type sandbox struct {
sync.Mutex
engines.SandboxBuilderBase
engines.SandboxBase
engines.ResultSetBase
environment runtime.Environment
payload payloadType
context *runtime.TaskContext
env map[string]string
mounts map[string]*mount
proxies map[string]http.Handler
files map[string][]byte
sessions atomics.WaitGroup
shells []engines.Shell
displays []io.ReadWriteCloser
resolve atomics.Once
result bool
resultErr error
abortErr error
}
///////////////////////////// Implementation of SandboxBuilder interface
func (s *sandbox) abortSessions() {
s.Lock()
defer s.Unlock()
s.sessions.Drain()
for _, shell := range s.shells {
shell.Abort()
}
for _, display := range s.displays {
display.Close()
}
}
func (s *sandbox) StartSandbox() (engines.Sandbox, error) {
s.Lock()
defer s.Unlock()
go func() {
// No need to lock access to payload, as it can't be mutated at this point
time.Sleep(time.Duration(s.payload.Delay) * time.Millisecond)
// No need to lock access mounts and proxies either
f := functions[s.payload.Function]
var err error
var result bool
if f == nil {
err = runtime.NewMalformedPayloadError("Unknown function")
} else {
result, err = f(s, s.payload.Argument)
}
s.sessions.WaitAndDrain()
s.resolve.Do(func() {
s.result = result
s.resultErr = err
s.abortErr = engines.ErrSandboxTerminated
})
}()
return s, nil
}
func (s *sandbox) AttachVolume(mountpoint string, v engines.Volume, readOnly bool) error {
// We can type cast Volume to our internal type as we know the volume was
// created by NewCacheFolder() or NewMemoryDisk(), this is a contract.
vol, valid := v.(*volume)
if !valid {
// TODO: Write to some sort of log if the type assertion fails
return fmt.Errorf("invalid volume type")
}
// Lock before we access mounts as this method may be called concurrently
s.Lock()
defer s.Unlock()
if strings.ContainsAny(mountpoint, " ") {
return runtime.NewMalformedPayloadError("MockEngine mountpoints cannot contain space")
}
if s.mounts[mountpoint] != nil {
return engines.ErrNamingConflict
}
s.mounts[mountpoint] = &mount{
volume: vol,
readOnly: readOnly,
}
return nil
}
func (s *sandbox) AttachProxy(name string, handler http.Handler) error {
// Lock before we access proxies as this method may be called concurrently
s.Lock()
defer s.Unlock()
if strings.ContainsAny(name, " ") {
return runtime.NewMalformedPayloadError(
"MockEngine proxy names cannot contain space.",
"Was given proxy name: '", name, "' which isn't allowed!",
)
}
if s.proxies[name] != nil {
return engines.ErrNamingConflict
}
s.proxies[name] = handler
return nil
}
func (s *sandbox) SetEnvironmentVariable(name string, value string) error {
s.Lock()
defer s.Unlock()
if strings.Contains(name, " ") {
return runtime.NewMalformedPayloadError(
"MockEngine environment variable names cannot contain space.",
"Was given environment variable name: '", name, "' which isn't allowed!",
)
}
if _, ok := s.env[name]; ok {
return engines.ErrNamingConflict
}
s.env[name] = value
return nil
}
///////////////////////////// Implementation of Sandbox interface
// List of functions implementing the task.payload.start.function functionality.
var functions = map[string]func(*sandbox, string) (bool, error){
"true": func(s *sandbox, arg string) (bool, error) { return true, nil },
"false": func(s *sandbox, arg string) (bool, error) { return false, nil },
"write-volume": func(s *sandbox, arg string) (bool, error) {
// Parse arg as: <mountPoint>/<file_name>:<fileData>
args := strings.SplitN(arg, "/", 2)
volumeName := args[0]
args = strings.SplitN(args[1], ":", 2)
fileName := args[0]
fileData := args[1]
mount := s.mounts[volumeName]
if mount == nil || mount.readOnly {
return false, nil
}
mount.volume.files[fileName] = fileData
return true, nil
},
"read-volume": func(s *sandbox, arg string) (bool, error) {
// Parse arg as: <mountPoint>/<fileName>
args := strings.SplitN(arg, "/", 2)
volumeName := args[0]
fileName := args[1]
mount := s.mounts[volumeName]
if mount == nil {
return false, nil
}
s.context.Log(mount.volume.files[fileName])
return mount.volume.files[fileName] != "", nil
},
"get-url": func(s *sandbox, arg string) (bool, error) {
res, err := http.Get(arg)
if err != nil {
s.context.Log("Failed to get url: ", arg, " err: ", err)
return false, nil
}
defer res.Body.Close()
io.Copy(s.context.LogDrain(), res.Body)
return res.StatusCode == http.StatusOK, nil
},
"ping-proxy": func(s *sandbox, arg string) (bool, error) {
u, err := url.Parse(arg)
if err != nil {
s.context.Log("Failed to parse url: ", arg, " got error: ", err)
return false, nil
}
handler := s.proxies[u.Host]
if handler == nil {
s.context.Log("No proxy for hostname: ", u.Host, " in: ", arg)
return false, nil
}
// Make a fake HTTP request and http response recorder
s.context.Log("Pinging")
req, err := http.NewRequest("GET", arg, nil)
if err != nil {
panic(err)
}
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
// Log response
s.context.Log(w.Body.String())
return w.Code == http.StatusOK, nil
},
"write-log": func(s *sandbox, arg string) (bool, error) {
s.context.Log(arg)
return true, nil
},
"write-error-log": func(s *sandbox, arg string) (bool, error) {
s.context.Log(arg)
return false, nil
},
"write-log-sleep": func(s *sandbox, arg string) (bool, error) {
s.context.Log(arg)
time.Sleep(500 * time.Millisecond)
return true, nil
},
"write-files": func(s *sandbox, arg string) (bool, error) {
for _, path := range strings.Split(arg, " ") {
s.files[path] = []byte("Hello World")
}
return true, nil
},
"print-env-var": func(s *sandbox, arg string) (bool, error) {
val, ok := s.env[arg]
s.context.Log(val)
return ok, nil
},
"fatal-internal-error": func(s *sandbox, arg string) (bool, error) {
// Should normally only be used if error is reported with Monitor
return false, runtime.ErrFatalInternalError
},
"nonfatal-internal-error": func(s *sandbox, arg string) (bool, error) {
// Should normally only be used if error is reported with Monitor
return false, runtime.ErrNonFatalInternalError
},
"malformed-payload-after-start": func(s *sandbox, arg string) (bool, error) {
return false, runtime.NewMalformedPayloadError(s.payload.Argument)
},
"stopNow-sleep": func(s *sandbox, arg string) (bool, error) {
// This is not really a reasonable thing for an engine to do. But it's
// useful for testing... StopNow causes all running tasks to be resolved
// 'exception' with reason: 'worker-shutdown'.
s.environment.Worker.StopNow()
time.Sleep(500 * time.Millisecond)
return true, nil
},
}
func (s *sandbox) WaitForResult() (engines.ResultSet, error) {
s.resolve.Wait()
if s.resultErr != nil {
return nil, s.resultErr
}
return s, nil
}
func (s *sandbox) Kill() error {
s.resolve.Do(func() {
s.abortSessions()
s.result = false
s.abortErr = engines.ErrSandboxTerminated
})
s.resolve.Wait()
return s.resultErr
}
func (s *sandbox) Abort() error {
s.resolve.Do(func() {
s.abortSessions()
s.result = false
s.resultErr = engines.ErrSandboxAborted
})
s.resolve.Wait()
return s.abortErr
}
func (s *sandbox) NewShell(command []string, tty bool) (engines.Shell, error) {
s.Lock()
defer s.Unlock()
if len(command) > 0 || tty {
return nil, engines.ErrFeatureNotSupported
}
if s.sessions.Add(1) != nil {
return nil, engines.ErrSandboxTerminated
}
shell := newShell()
s.shells = append(s.shells, shell)
go func() {
shell.Wait()
s.sessions.Done()
}()
return shell, nil
}
func (s *sandbox) ListDisplays() ([]engines.Display, error) {
return []engines.Display{
{
Name: "MockDisplay",
Description: "Simple mock VNC display rendering a static test image",
Width: mockDisplayWidth,
Height: mockDisplayHeight,
},
}, nil
}
func (s *sandbox) OpenDisplay(name string) (io.ReadWriteCloser, error) {
s.Lock()
defer s.Unlock()
if name != "MockDisplay" {
return nil, engines.ErrNoSuchDisplay
}
if s.sessions.Add(1) != nil {
return nil, engines.ErrSandboxTerminated
}
d := ioext.WatchPipe(newMockDisplay(), func(error) {
s.sessions.Done()
})
s.displays = append(s.displays, d)
return d, nil
}
///////////////////////////// Implementation of ResultSet interface
func (s *sandbox) ExtractFile(path string) (ioext.ReadSeekCloser, error) {
data := s.files[path]
if len(data) == 0 {
return nil, engines.ErrResourceNotFound
}
return ioext.NopCloser(bytes.NewReader(data)), nil
}
func (s *sandbox) ExtractFolder(folder string, handler engines.FileHandler) error {
if !strings.HasSuffix(folder, "/") {
folder += "/"
}
wg := sync.WaitGroup{}
m := sync.Mutex{}
handlerError := false
foundFolder := false
for p, data := range s.files {
if strings.HasPrefix(p, folder) {
foundFolder = true
wg.Add(1)
go func(p string, data []byte) {
p = p[len(folder):] // Note: folder always ends with slash
err := handler(p, ioext.NopCloser(bytes.NewReader(data)))
if err != nil {
m.Lock()
handlerError = true
m.Unlock()
}
wg.Done()
}(p, data)
}
}
wg.Wait()
if !foundFolder {
return engines.ErrResourceNotFound
}
if handlerError {
return engines.ErrHandlerInterrupt
}
return nil
}
func (s *sandbox) Success() bool {
// No need to lock access as result is immutable
return s.result
}