-
Notifications
You must be signed in to change notification settings - Fork 18
Implement video feature in docker engine #383
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ import ( | |
"github.com/taskcluster/taskcluster-worker/runtime" | ||
"github.com/taskcluster/taskcluster-worker/runtime/atomics" | ||
"github.com/taskcluster/taskcluster-worker/runtime/ioext" | ||
funk "github.com/thoas/go-funk" | ||
) | ||
|
||
const dockerEngineKillTimeout = 5 * time.Second | ||
|
@@ -32,6 +33,7 @@ type sandbox struct { | |
taskCtx *runtime.TaskContext | ||
networkHandle *network.Handle | ||
imageHandle *imagecache.ImageHandle | ||
videoDev *device | ||
} | ||
|
||
func newSandbox(sb *sandboxBuilder) (*sandbox, error) { | ||
|
@@ -52,6 +54,21 @@ func newSandbox(sb *sandboxBuilder) (*sandbox, error) { | |
return nil, errors.Wrap(err, "docker.CreateNetwork failed") | ||
} | ||
|
||
devices := []docker.Device{} | ||
var dev *device | ||
if funk.InStrings(sb.payload.Devices, "video") { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for loop + a switch is likely better... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe when we add more options. |
||
dev = sb.e.video.claim() | ||
if dev == nil { | ||
return nil, errors.New("No video device available") | ||
} | ||
debug(fmt.Sprintf("Claimed %s", dev.path)) | ||
devices = append(devices, docker.Device{ | ||
PathOnHost: dev.path, | ||
PathInContainer: dev.path, | ||
CgroupPermissions: "rwm", | ||
}) | ||
} | ||
|
||
// Create the container | ||
container, err := sb.e.docker.CreateContainer(docker.CreateContainerOptions{ | ||
Config: &docker.Config{ | ||
|
@@ -70,6 +87,7 @@ func newSandbox(sb *sandboxBuilder) (*sandbox, error) { | |
// to the proxies added to proxyMux above.. | ||
ExtraHosts: []string{fmt.Sprintf("taskcluster:%s", networkHandle.Gateway())}, | ||
Mounts: sb.mounts, | ||
Devices: devices, | ||
}, | ||
NetworkingConfig: &docker.NetworkingConfig{ | ||
EndpointsConfig: map[string]*docker.EndpointConfig{ | ||
|
@@ -79,6 +97,7 @@ func newSandbox(sb *sandboxBuilder) (*sandbox, error) { | |
}) | ||
if err != nil { | ||
imageHandle.Release() | ||
sb.e.video.release(dev) | ||
return nil, runtime.NewMalformedPayloadError( | ||
"could not create container: " + err.Error()) | ||
} | ||
|
@@ -87,6 +106,7 @@ func newSandbox(sb *sandboxBuilder) (*sandbox, error) { | |
storage, err := sb.e.Environment.TemporaryStorage.NewFolder() | ||
if err != nil { | ||
imageHandle.Release() | ||
sb.e.video.release(dev) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is probably too soon to release, the container still exists... |
||
monitor.ReportError(err, "failed to create temporary folder") | ||
return nil, runtime.ErrFatalInternalError | ||
} | ||
|
@@ -101,6 +121,7 @@ func newSandbox(sb *sandboxBuilder) (*sandbox, error) { | |
"containerId": container.ID, | ||
"networkId": networkHandle.NetworkID(), | ||
}), | ||
videoDev: dev, | ||
} | ||
|
||
// attach to the container before starting so that we get all the logs | ||
|
@@ -277,5 +298,9 @@ func (s *sandbox) dispose() error { | |
if hasErr { | ||
return runtime.ErrNonFatalInternalError | ||
} | ||
|
||
if s.videoDev != nil { | ||
s.videoDev.claimed = false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's probably best to decide what is ecapsulated where... You have a method |
||
} | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package dockerengine | ||
|
||
import ( | ||
"path/filepath" | ||
"regexp" | ||
|
||
"github.com/pkg/errors" | ||
funk "github.com/thoas/go-funk" | ||
) | ||
|
||
type device struct { | ||
path string | ||
claimed bool | ||
} | ||
|
||
type videoDeviceManager struct { | ||
devices []device | ||
} | ||
|
||
func newVideoDeviceManager() (*videoDeviceManager, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest something like: package dockerengine
import (
"errors"
"sync"
"github.com/taskcluster/taskcluster-worker/runtime/atomics"
)
type deviceHandle interface {
Path() string
Release()
}
type videoDeviceManager struct {
m sync.Mutex
devices []string
}
// newVideoDeviceManager creates a new videoDeviceManager
func newVideoDeviceManager() (*videoDeviceManager, error) {
// TODO: Find or create devices
devices := []string{"/dev/video0"}
return &videoDeviceManager{devices: devices}, nil
}
// GetVideoDevice returns an idle video device
func (vdm *videoDeviceManager) GetVideoDevice() deviceHandle {
vdm.m.Lock()
defer vdm.m.Unlock()
// ensure we have enough devices
if len(vdm.devices) < 1 {
panic(errors.New("insufficent number of video devices available"))
}
// Create a device handle, ensuring we can't release twice
vdh := &videoDeviceHandle{owner: vdm}
vdh.path, vdm.devices = vdm.devices[0], vdm.devices[1:]
return vdh
}
// Dispose releases all resources assocated with the videoDeviceManager
func (vdm *videoDeviceManager) Dispose() error {
vdm.m.Lock()
defer vdm.m.Unlock()
return nil // TODO: remove devices if any was created
}
type videoDeviceHandle struct {
owner *videoDeviceManager
path string
released atomics.Once
}
func (vdh *videoDeviceHandle) Path() string {
if vdh.released.IsDone() {
panic(errors.New("videoDeviceHandle.Path() was called after release"))
}
return vdh.path
}
func (vdh *videoDeviceHandle) Release() string {
ok := vdh.released.Do(func() {
vdh.owner.m.Lock()
defer vdh.owner.m.Unlock()
vdh.owner.devices = append(vdh.owner.devices, vdh.path)
})
if !ok {
debug("WARNING: harmless double release() of videoDeviceHandle")
}
} In the sandbox and resultset code you just release() the While techincally all this is in the same package, I only use methods starting with capital outside this file... the only exception being: |
||
matches, err := filepath.Glob("/dev/video*") | ||
if err != nil { | ||
return nil, errors.Wrap(err, "Failed to call filepath.Glob function") | ||
} | ||
|
||
r := regexp.MustCompile("/dev/video[0-9]+") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't we want to make the devices in here? Also I think a That way we can hide some of the locking and internals away, and the engine just deals with a device which is something that has a path... and is created when the array from payload is passed to the device manager... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't know if I followed the idea, but let's keep things simple for now. |
||
matches = funk.FilterString(matches, r.MatchString) | ||
|
||
devices := make([]device, len(matches)) | ||
for i := range devices { | ||
devices[i].path = matches[i] | ||
} | ||
|
||
return &videoDeviceManager{ | ||
devices: devices, | ||
}, nil | ||
} | ||
|
||
func (d *videoDeviceManager) claim() *device { | ||
for i := range d.devices { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. locking? |
||
if !d.devices[i].claimed { | ||
return &d.devices[i] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. returning a reference to an object stored in a list... it's probably better to make a list pointers to devices.. Just so we don't have to think too much about happens when if we mutate the device... or something like that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At no point do you set, |
||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (d *videoDeviceManager) release(dev *device) { | ||
dev.claimed = false | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// +build dockervideo | ||
|
||
package dockerengine | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/taskcluster/taskcluster-worker/engines/enginetest" | ||
) | ||
|
||
// Image and tag used in test cases below | ||
const ( | ||
videoDockerImageName = "alpine:3.6" | ||
) | ||
|
||
var videoProvider = &enginetest.EngineProvider{ | ||
Engine: "docker", | ||
Config: `{ | ||
"privileged": "allow", | ||
"enableDevices": true | ||
}`, | ||
} | ||
|
||
func TestVideo(t *testing.T) { | ||
c := enginetest.LoggingTestCase{ | ||
EngineProvider: videoProvider, | ||
Target: "/dev/video0", | ||
TargetPayload: `{ | ||
"command": ["sh", "-c", "ls /dev/video0"], | ||
"devices": ["video"], | ||
"image": "` + videoDockerImageName + `" | ||
}`, | ||
} | ||
|
||
c.Test() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could just make the option of
video
be dependent on whether devices are enabled or not... anyways, nit... I'm still not sure what's ideal in either case.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I would suggest that we do:
That way, if in some future we decide we want additional options, we can add them to the object...