
In the course of setting out to add filters and address some cleanup, it was found that we had a few problems in the events subsystem that needed addressing before moving forward. The biggest change was to move to the more standard terminology of publish and subscribe. We make this terminology change across the Go interface and the GRPC API, making the behavior more familier. The previous system was very context-oriented, which is no longer required. With this, we've removed a large amount of dead and unneeded code. Event transactions, context storage and the concept of `Poster` is gone. This has been replaced in most places with a `Publisher`, which matches the actual usage throughout the codebase, removing the need for helpers. There are still some questions around the way events are handled in the shim. Right now, we've preserved some of the existing bugs which may require more extensive changes to resolve correctly. Signed-off-by: Stephen J Day <stephen.day@docker.com>
415 lines
9.6 KiB
Go
415 lines
9.6 KiB
Go
// +build windows
|
|
|
|
package windows
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/Microsoft/hcsshim"
|
|
"github.com/boltdb/bolt"
|
|
eventsapi "github.com/containerd/containerd/api/services/events/v1"
|
|
containerdtypes "github.com/containerd/containerd/api/types"
|
|
"github.com/containerd/containerd/errdefs"
|
|
"github.com/containerd/containerd/events"
|
|
"github.com/containerd/containerd/log"
|
|
"github.com/containerd/containerd/namespaces"
|
|
"github.com/containerd/containerd/plugin"
|
|
"github.com/containerd/containerd/runtime"
|
|
"github.com/containerd/containerd/typeurl"
|
|
"github.com/containerd/containerd/windows/hcsshimopts"
|
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
runtimeName = "windows"
|
|
hcsshimOwner = "containerd"
|
|
defaultTerminateDuration = 5 * time.Minute
|
|
)
|
|
|
|
var (
|
|
pluginID = fmt.Sprintf("%s.%s", plugin.RuntimePlugin, runtimeName)
|
|
)
|
|
|
|
var _ = (runtime.Runtime)(&windowsRuntime{})
|
|
|
|
func init() {
|
|
plugin.Register(&plugin.Registration{
|
|
ID: runtimeName,
|
|
Type: plugin.RuntimePlugin,
|
|
Init: New,
|
|
Requires: []plugin.PluginType{
|
|
plugin.MetadataPlugin,
|
|
},
|
|
})
|
|
}
|
|
|
|
func New(ic *plugin.InitContext) (interface{}, error) {
|
|
if err := os.MkdirAll(ic.Root, 0700); err != nil {
|
|
return nil, errors.Wrapf(err, "could not create state directory at %s", ic.Root)
|
|
}
|
|
|
|
m, err := ic.Get(plugin.MetadataPlugin)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r := &windowsRuntime{
|
|
root: ic.Root,
|
|
pidPool: newPidPool(),
|
|
|
|
events: make(chan interface{}, 4096),
|
|
publisher: ic.Events,
|
|
// TODO(mlaventure): windows needs a stat monitor
|
|
monitor: nil,
|
|
tasks: runtime.NewTaskList(),
|
|
db: m.(*bolt.DB),
|
|
}
|
|
|
|
// Load our existing containers and kill/delete them. We don't support
|
|
// reattaching to them
|
|
r.cleanup(ic.Context)
|
|
|
|
return r, nil
|
|
}
|
|
|
|
type windowsRuntime struct {
|
|
sync.Mutex
|
|
|
|
root string
|
|
pidPool *pidPool
|
|
|
|
publisher events.Publisher
|
|
events chan interface{}
|
|
|
|
monitor runtime.TaskMonitor
|
|
tasks *runtime.TaskList
|
|
db *bolt.DB
|
|
}
|
|
|
|
func (r *windowsRuntime) ID() string {
|
|
return pluginID
|
|
}
|
|
|
|
func (r *windowsRuntime) Create(ctx context.Context, id string, opts runtime.CreateOpts) (runtime.Task, error) {
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s, err := typeurl.UnmarshalAny(opts.Spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
spec := s.(*specs.Spec)
|
|
|
|
var createOpts *hcsshimopts.CreateOptions
|
|
if opts.Options != nil {
|
|
o, err := typeurl.UnmarshalAny(opts.Options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
createOpts = o.(*hcsshimopts.CreateOptions)
|
|
} else {
|
|
createOpts = &hcsshimopts.CreateOptions{}
|
|
}
|
|
|
|
if createOpts.TerminateDuration == 0 {
|
|
createOpts.TerminateDuration = defaultTerminateDuration
|
|
}
|
|
|
|
return r.newTask(ctx, namespace, id, spec, opts.IO, createOpts)
|
|
}
|
|
|
|
func (r *windowsRuntime) Get(ctx context.Context, id string) (runtime.Task, error) {
|
|
return r.tasks.Get(ctx, id)
|
|
}
|
|
|
|
func (r *windowsRuntime) Tasks(ctx context.Context) ([]runtime.Task, error) {
|
|
return r.tasks.GetAll(ctx)
|
|
}
|
|
|
|
func (r *windowsRuntime) Delete(ctx context.Context, t runtime.Task) (*runtime.Exit, error) {
|
|
wt, ok := t.(*task)
|
|
if !ok {
|
|
return nil, errors.Wrap(errdefs.ErrInvalidArgument, "no a windows task")
|
|
}
|
|
|
|
// TODO(mlaventure): stop monitor on this task
|
|
|
|
var (
|
|
err error
|
|
needServicing bool
|
|
state, _ = wt.State(ctx)
|
|
)
|
|
switch state.Status {
|
|
case runtime.StoppedStatus:
|
|
// Only try to service a container if it was started and it's not a
|
|
// servicing task itself
|
|
if wt.servicing == false {
|
|
needServicing, err = wt.hcsContainer.HasPendingUpdates()
|
|
if err != nil {
|
|
needServicing = false
|
|
log.G(ctx).WithError(err).
|
|
WithFields(logrus.Fields{"id": wt.id, "pid": wt.pid}).
|
|
Error("failed to check if container needs servicing")
|
|
}
|
|
}
|
|
fallthrough
|
|
case runtime.CreatedStatus:
|
|
// if it's stopped or in created state, we need to shutdown the
|
|
// container before removing it
|
|
if err = wt.stop(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
return nil, errors.Wrap(errdefs.ErrFailedPrecondition,
|
|
"cannot delete a non-stopped task")
|
|
}
|
|
|
|
var rtExit *runtime.Exit
|
|
if p := wt.getProcess(t.ID()); p != nil {
|
|
ec, ea, err := p.ExitCode()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rtExit = &runtime.Exit{
|
|
Pid: wt.pid,
|
|
Status: ec,
|
|
Timestamp: ea,
|
|
}
|
|
} else {
|
|
rtExit = &runtime.Exit{
|
|
Pid: wt.pid,
|
|
Status: 255,
|
|
Timestamp: time.Now(),
|
|
}
|
|
}
|
|
|
|
wt.cleanup()
|
|
r.tasks.Delete(ctx, t)
|
|
|
|
r.publisher.Publish(ctx,
|
|
runtime.TaskDeleteEventTopic,
|
|
&eventsapi.TaskDelete{
|
|
ContainerID: wt.id,
|
|
Pid: wt.pid,
|
|
ExitStatus: rtExit.Status,
|
|
ExitedAt: rtExit.Timestamp,
|
|
})
|
|
|
|
if needServicing {
|
|
ns, _ := namespaces.Namespace(ctx)
|
|
serviceCtx := log.WithLogger(context.Background(), log.GetLogger(ctx))
|
|
serviceCtx = namespaces.WithNamespace(serviceCtx, ns)
|
|
r.serviceTask(serviceCtx, ns, wt.id+"_servicing", wt.spec)
|
|
}
|
|
|
|
// We were never started, return failure
|
|
return rtExit, nil
|
|
}
|
|
|
|
func (r *windowsRuntime) newTask(ctx context.Context, namespace, id string, spec *specs.Spec, io runtime.IO, createOpts *hcsshimopts.CreateOptions) (*task, error) {
|
|
var (
|
|
err error
|
|
pset *pipeSet
|
|
)
|
|
|
|
if pset, err = newPipeSet(ctx, io); err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
pset.Close()
|
|
}
|
|
}()
|
|
|
|
var pid uint32
|
|
if pid, err = r.pidPool.Get(); err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
r.pidPool.Put(pid)
|
|
}
|
|
}()
|
|
|
|
var (
|
|
conf *hcsshim.ContainerConfig
|
|
nsid = namespace + "-" + id
|
|
)
|
|
if conf, err = newContainerConfig(ctx, hcsshimOwner, nsid, spec); err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
removeLayer(ctx, conf.LayerFolderPath)
|
|
}
|
|
}()
|
|
|
|
// TODO: remove this once we have a windows snapshotter
|
|
// Store the LayerFolder in the db so we can clean it if we die
|
|
if err = r.db.Update(func(tx *bolt.Tx) error {
|
|
s := newLayerFolderStore(tx)
|
|
return s.Create(nsid, conf.LayerFolderPath)
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
if dbErr := r.db.Update(func(tx *bolt.Tx) error {
|
|
s := newLayerFolderStore(tx)
|
|
return s.Delete(nsid)
|
|
}); dbErr != nil {
|
|
log.G(ctx).WithField("id", id).
|
|
Error("failed to remove key from metadata")
|
|
}
|
|
}
|
|
}()
|
|
|
|
ctr, err := hcsshim.CreateContainer(nsid, conf)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "hcsshim failed to create task")
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
ctr.Terminate()
|
|
ctr.Wait()
|
|
ctr.Close()
|
|
}
|
|
}()
|
|
|
|
if err = ctr.Start(); err != nil {
|
|
return nil, errors.Wrap(err, "hcsshim failed to spawn task")
|
|
}
|
|
|
|
t := &task{
|
|
id: id,
|
|
namespace: namespace,
|
|
pid: pid,
|
|
io: pset,
|
|
status: runtime.CreatedStatus,
|
|
spec: spec,
|
|
processes: make(map[string]*process),
|
|
hyperV: spec.Windows.HyperV != nil,
|
|
publisher: r.publisher,
|
|
rwLayer: conf.LayerFolderPath,
|
|
pidPool: r.pidPool,
|
|
hcsContainer: ctr,
|
|
terminateDuration: createOpts.TerminateDuration,
|
|
}
|
|
r.tasks.Add(ctx, t)
|
|
|
|
var rootfs []*containerdtypes.Mount
|
|
for _, l := range append([]string{t.rwLayer}, spec.Windows.LayerFolders...) {
|
|
rootfs = append(rootfs, &containerdtypes.Mount{
|
|
Type: "windows-layer",
|
|
Source: l,
|
|
})
|
|
}
|
|
|
|
r.publisher.Publish(ctx,
|
|
runtime.TaskCreateEventTopic,
|
|
&eventsapi.TaskCreate{
|
|
ContainerID: id,
|
|
IO: &eventsapi.TaskIO{
|
|
Stdin: io.Stdin,
|
|
Stdout: io.Stdout,
|
|
Stderr: io.Stderr,
|
|
Terminal: io.Terminal,
|
|
},
|
|
Pid: t.pid,
|
|
Rootfs: rootfs,
|
|
// TODO: what should be in Bundle for windows?
|
|
})
|
|
|
|
return t, nil
|
|
}
|
|
|
|
func (r *windowsRuntime) cleanup(ctx context.Context) {
|
|
cp, err := hcsshim.GetContainers(hcsshim.ComputeSystemQuery{
|
|
Types: []string{"Container"},
|
|
Owners: []string{hcsshimOwner},
|
|
})
|
|
if err != nil {
|
|
log.G(ctx).Warn("failed to retrieve running containers")
|
|
return
|
|
}
|
|
|
|
for _, p := range cp {
|
|
container, err := hcsshim.OpenContainer(p.ID)
|
|
if err != nil {
|
|
log.G(ctx).Warnf("failed open container %s", p.ID)
|
|
continue
|
|
}
|
|
|
|
err = container.Terminate()
|
|
if err == nil || hcsshim.IsPending(err) || hcsshim.IsAlreadyStopped(err) {
|
|
container.Wait()
|
|
}
|
|
container.Close()
|
|
|
|
// TODO: remove this once we have a windows snapshotter
|
|
var layerFolderPath string
|
|
if err := r.db.View(func(tx *bolt.Tx) error {
|
|
s := newLayerFolderStore(tx)
|
|
l, e := s.Get(p.ID)
|
|
if err == nil {
|
|
layerFolderPath = l
|
|
}
|
|
return e
|
|
}); err == nil && layerFolderPath != "" {
|
|
removeLayer(ctx, layerFolderPath)
|
|
if dbErr := r.db.Update(func(tx *bolt.Tx) error {
|
|
s := newLayerFolderStore(tx)
|
|
return s.Delete(p.ID)
|
|
}); dbErr != nil {
|
|
log.G(ctx).WithField("id", p.ID).
|
|
Error("failed to remove key from metadata")
|
|
}
|
|
} else {
|
|
log.G(ctx).WithField("id", p.ID).
|
|
Debug("key not found in metadata, R/W layer may be leaked")
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func (r *windowsRuntime) serviceTask(ctx context.Context, namespace, id string, spec *specs.Spec) {
|
|
var (
|
|
err error
|
|
t *task
|
|
io runtime.IO
|
|
createOpts = &hcsshimopts.CreateOptions{
|
|
TerminateDuration: defaultTerminateDuration,
|
|
}
|
|
)
|
|
|
|
t, err = r.newTask(ctx, namespace, id, spec, io, createOpts)
|
|
if err != nil {
|
|
log.G(ctx).WithError(err).WithField("id", id).
|
|
Warn("failed to created servicing task")
|
|
return
|
|
}
|
|
t.servicing = true
|
|
|
|
err = t.Start(ctx)
|
|
switch err {
|
|
case nil:
|
|
<-t.getProcess(id).exitCh
|
|
default:
|
|
log.G(ctx).WithError(err).WithField("id", id).
|
|
Warn("failed to start servicing task")
|
|
}
|
|
|
|
if _, err = r.Delete(ctx, t); err != nil {
|
|
log.G(ctx).WithError(err).WithField("id", id).
|
|
Warn("failed to stop servicing task")
|
|
}
|
|
}
|