containerd/windows/hcs/hcs.go
Kenfe-Mickael Laventure c5843b7615 Initial windows runtime work
Signed-off-by: Kenfe-Mickael Laventure <mickael.laventure@gmail.com>
2017-04-07 09:20:44 -07:00

523 lines
14 KiB
Go

// +build windows
package hcs
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"github.com/Microsoft/hcsshim"
"github.com/Sirupsen/logrus"
"github.com/containerd/containerd"
"github.com/containerd/containerd/log"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
)
const (
layerFile = "layer"
defaultTerminateTimeout = 5 * time.Minute
)
func LoadAll(ctx context.Context, owner, rootDir string) (map[string]*HCS, error) {
ctrProps, err := hcsshim.GetContainers(hcsshim.ComputeSystemQuery{})
if err != nil {
return nil, errors.Wrap(err, "failed to retrieve running containers")
}
containers := make(map[string]*HCS)
for _, p := range ctrProps {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
if p.Owner != owner || p.SystemType != "Container" {
continue
}
// TODO: take context in account
container, err := hcsshim.OpenContainer(p.ID)
if err != nil {
return nil, errors.Wrapf(err, "failed open container %s", p.ID)
}
stateDir := filepath.Join(rootDir, p.ID)
b, err := ioutil.ReadFile(filepath.Join(stateDir, layerFile))
containers[p.ID] = &HCS{
id: p.ID,
container: container,
stateDir: stateDir,
layerFolderPath: string(b),
conf: Configuration{
TerminateDuration: defaultTerminateTimeout,
},
}
}
return containers, nil
}
// New creates a new container (but doesn't start) it.
func New(rootDir, owner, containerID string, spec specs.Spec, conf Configuration, cio containerd.IO) (*HCS, error) {
stateDir := filepath.Join(rootDir, containerID)
if err := os.MkdirAll(stateDir, 0755); err != nil {
return nil, errors.Wrapf(err, "unable to create container state dir %s", stateDir)
}
if conf.TerminateDuration == 0 {
conf.TerminateDuration = defaultTerminateTimeout
}
h := &HCS{
stateDir: stateDir,
owner: owner,
id: containerID,
spec: spec,
conf: conf,
}
sio, err := newSIO(cio)
if err != nil {
return nil, err
}
h.io = sio
runtime.SetFinalizer(sio, func(s *shimIO) {
s.Close()
})
hcsConf, err := h.newHCSConfiguration()
if err != nil {
return nil, err
}
ctr, err := hcsshim.CreateContainer(containerID, hcsConf)
if err != nil {
removeLayer(context.TODO(), hcsConf.LayerFolderPath)
return nil, err
}
h.container = ctr
h.layerFolderPath = hcsConf.LayerFolderPath
return h, nil
}
type HCS struct {
stateDir string
owner string
id string
spec specs.Spec
conf Configuration
io *shimIO
container hcsshim.Container
initProcess hcsshim.Process
layerFolderPath string
}
// Start starts the associated container and instantiate the init
// process within it.
func (s *HCS) Start(ctx context.Context, servicing bool) error {
if s.initProcess != nil {
return errors.New("init process already started")
}
if err := s.container.Start(); err != nil {
if err := s.Terminate(ctx); err != nil {
log.G(ctx).WithError(err).Errorf("failed to terminate container %s", s.id)
}
return err
}
proc, err := s.newProcess(ctx, s.io, s.spec.Process)
if err != nil {
s.Terminate(ctx)
return err
}
s.initProcess = proc
return nil
}
// Pid returns the pid of the container init process
func (s *HCS) Pid() int {
return s.initProcess.Pid()
}
// ExitCode waits for the container to exit and return the exit code
// of the init process
func (s *HCS) ExitCode(ctx context.Context) (uint32, error) {
// TODO: handle a context cancellation
if err := s.initProcess.Wait(); err != nil {
if herr, ok := err.(*hcsshim.ProcessError); ok && herr.Err != syscall.ERROR_BROKEN_PIPE {
return 255, errors.Wrapf(err, "failed to wait for container '%s' init process", s.id)
}
// container is probably dead, let's try to get its exit code
}
ec, err := s.initProcess.ExitCode()
if err != nil {
if herr, ok := err.(*hcsshim.ProcessError); ok && herr.Err != syscall.ERROR_BROKEN_PIPE {
return 255, errors.Wrapf(err, "failed to get container '%s' init process exit code", s.id)
}
// Well, unknown exit code it is
ec = 255
}
return uint32(ec), err
}
// Exec starts a new process within the container
func (s *HCS) Exec(ctx context.Context, procSpec specs.Process, io containerd.IO) (*Process, error) {
sio, err := newSIO(io)
if err != nil {
return nil, err
}
p, err := s.newProcess(ctx, sio, procSpec)
if err != nil {
return nil, err
}
return &Process{
containerID: s.id,
p: p,
status: containerd.RunningStatus,
}, nil
}
// newProcess create a new process within a running container. This is
// used to create both the init process and subsequent 'exec'
// processes.
func (s *HCS) newProcess(ctx context.Context, sio *shimIO, procSpec specs.Process) (hcsshim.Process, error) {
conf := hcsshim.ProcessConfig{
EmulateConsole: sio.terminal,
CreateStdInPipe: sio.stdin != nil,
CreateStdOutPipe: sio.stdout != nil,
CreateStdErrPipe: sio.stderr != nil,
User: procSpec.User.Username,
CommandLine: strings.Join(procSpec.Args, " "),
Environment: ociSpecEnvToHCSEnv(procSpec.Env),
WorkingDirectory: procSpec.Cwd,
}
conf.ConsoleSize[0] = procSpec.ConsoleSize.Height
conf.ConsoleSize[1] = procSpec.ConsoleSize.Width
if conf.WorkingDirectory == "" {
conf.WorkingDirectory = s.spec.Process.Cwd
}
proc, err := s.container.CreateProcess(&conf)
if err != nil {
return nil, errors.Wrapf(err, "failed to create process with conf %#v", conf)
}
pid := proc.Pid()
stdin, stdout, stderr, err := proc.Stdio()
if err != nil {
s.Terminate(ctx)
return nil, err
}
if sio.stdin != nil {
go func() {
log.G(ctx).WithField("pid", pid).Debug("stdin: copy started")
io.Copy(stdin, sio.stdin)
log.G(ctx).WithField("pid", pid).Debug("stdin: copy done")
stdin.Close()
sio.stdin.Close()
}()
} else {
proc.CloseStdin()
}
if sio.stdout != nil {
go func() {
log.G(ctx).WithField("pid", pid).Debug("stdout: copy started")
io.Copy(sio.stdout, stdout)
log.G(ctx).WithField("pid", pid).Debug("stdout: copy done")
stdout.Close()
sio.stdout.Close()
}()
}
if sio.stderr != nil {
go func() {
log.G(ctx).WithField("pid", pid).Debug("stderr: copy started")
io.Copy(sio.stderr, stderr)
log.G(ctx).WithField("pid", pid).Debug("stderr: copy done")
stderr.Close()
sio.stderr.Close()
}()
}
return proc, nil
}
// Terminate stop a running container.
func (s *HCS) Terminate(ctx context.Context) error {
err := s.container.Terminate()
switch {
case hcsshim.IsPending(err):
// TODO: take the context into account
err = s.container.WaitTimeout(s.conf.TerminateDuration)
case hcsshim.IsAlreadyStopped(err):
err = nil
}
return err
}
func (s *HCS) Shutdown(ctx context.Context) error {
err := s.container.Shutdown()
switch {
case hcsshim.IsPending(err):
// TODO: take the context into account
err = s.container.WaitTimeout(s.conf.TerminateDuration)
case hcsshim.IsAlreadyStopped(err):
err = nil
}
if err != nil {
log.G(ctx).WithError(err).Debugf("failed to shutdown container %s, calling terminate", s.id)
return s.Terminate(ctx)
}
return nil
}
// Remove start a servicing container if needed then cleanup the container
// resources
func (s *HCS) Remove(ctx context.Context) error {
defer func() {
if err := s.Shutdown(ctx); err != nil {
log.G(ctx).WithError(err).WithField("id", s.id).
Errorf("failed to shutdown/terminate container")
}
if s.initProcess != nil {
if err := s.initProcess.Close(); err != nil {
log.G(ctx).WithError(err).WithFields(logrus.Fields{"pid": s.Pid(), "id": s.id}).
Errorf("failed to clean init process resources")
}
}
if err := s.container.Close(); err != nil {
log.G(ctx).WithError(err).WithField("id", s.id).Errorf("failed to clean container resources")
}
// Cleanup folder layer
if err := removeLayer(ctx, s.layerFolderPath); err == nil {
os.RemoveAll(s.stateDir)
}
}()
if update, err := s.container.HasPendingUpdates(); err != nil || !update {
return nil
}
// TODO: take the context into account
serviceHCS, err := New(s.stateDir, s.owner, s.id+"_servicing", s.spec, s.conf, containerd.IO{})
if err != nil {
log.G(ctx).WithError(err).WithField("id", s.id).Warn("could not create servicing container")
return nil
}
defer serviceHCS.container.Close()
err = serviceHCS.Start(ctx, true)
if err != nil {
if err := serviceHCS.Terminate(ctx); err != nil {
log.G(ctx).WithError(err).WithField("id", s.id).Errorf("failed to terminate servicing container for %s")
}
log.G(ctx).WithError(err).WithField("id", s.id).Errorf("failed to start servicing container")
return nil
}
// wait for the container to exit
_, err = serviceHCS.ExitCode(ctx)
if err != nil {
if err := serviceHCS.Terminate(ctx); err != nil {
log.G(ctx).WithError(err).WithField("id", s.id).Errorf("failed to terminate servicing container for %s")
}
log.G(ctx).WithError(err).WithField("id", s.id).Errorf("failed to get servicing container exit code")
}
serviceHCS.container.WaitTimeout(s.conf.TerminateDuration)
return nil
}
// newHCSConfiguration generates a hcsshim configuration from the instance
// OCI Spec and hcs.Configuration.
func (s *HCS) newHCSConfiguration() (*hcsshim.ContainerConfig, error) {
configuration := &hcsshim.ContainerConfig{
SystemType: "Container",
Name: s.id,
Owner: s.owner,
HostName: s.spec.Hostname,
IgnoreFlushesDuringBoot: s.conf.IgnoreFlushesDuringBoot,
HvPartition: s.conf.UseHyperV,
AllowUnqualifiedDNSQuery: s.conf.AllowUnqualifiedDNSQuery,
EndpointList: s.conf.NetworkEndpoints,
NetworkSharedContainerName: s.conf.NetworkSharedContainerID,
Credentials: s.conf.Credentials,
}
// TODO: use the create request Mount for those
for _, layerPath := range s.conf.Layers {
_, filename := filepath.Split(layerPath)
guid, err := hcsshim.NameToGuid(filename)
if err != nil {
return nil, err
}
configuration.Layers = append(configuration.Layers, hcsshim.Layer{
ID: guid.ToString(),
Path: layerPath,
})
}
if len(s.spec.Mounts) > 0 {
mds := make([]hcsshim.MappedDir, len(s.spec.Mounts))
for i, mount := range s.spec.Mounts {
mds[i] = hcsshim.MappedDir{
HostPath: mount.Source,
ContainerPath: mount.Destination,
ReadOnly: false,
}
for _, o := range mount.Options {
if strings.ToLower(o) == "ro" {
mds[i].ReadOnly = true
}
}
}
configuration.MappedDirectories = mds
}
if s.conf.DNSSearchList != nil {
configuration.DNSSearchList = strings.Join(s.conf.DNSSearchList, ",")
}
if configuration.HvPartition {
for _, layerPath := range s.conf.Layers {
utilityVMPath := filepath.Join(layerPath, "UtilityVM")
_, err := os.Stat(utilityVMPath)
if err == nil {
configuration.HvRuntime = &hcsshim.HvRuntime{ImagePath: utilityVMPath}
break
} else if !os.IsNotExist(err) {
return nil, errors.Wrapf(err, "failed to access layer %s", layerPath)
}
}
}
if len(configuration.Layers) == 0 {
// TODO: support starting with 0 layers, this mean we need the "filter" directory as parameter
return nil, errors.New("at least one layers must be provided")
}
di := hcsshim.DriverInfo{
Flavour: 1, // filter driver
}
if len(configuration.Layers) > 0 {
di.HomeDir = filepath.Dir(s.conf.Layers[0])
}
// Windows doesn't support creating a container with a readonly
// filesystem, so always create a RW one
if err := hcsshim.CreateSandboxLayer(di, s.id, s.conf.Layers[0], s.conf.Layers); err != nil {
return nil, errors.Wrapf(err, "failed to create sandbox layer for %s: layers: %#v, driverInfo: %#v",
s.id, configuration.Layers, di)
}
configuration.LayerFolderPath = filepath.Join(di.HomeDir, s.id)
if err := ioutil.WriteFile(filepath.Join(s.stateDir, layerFile), []byte(configuration.LayerFolderPath), 0644); err != nil {
log.L.WithError(err).Warnf("failed to save active layer %s", configuration.LayerFolderPath)
}
err := hcsshim.ActivateLayer(di, s.id)
if err != nil {
removeLayer(context.TODO(), configuration.LayerFolderPath)
return nil, errors.Wrapf(err, "failed to active layer %s", configuration.LayerFolderPath)
}
err = hcsshim.PrepareLayer(di, s.id, s.conf.Layers)
if err != nil {
removeLayer(context.TODO(), configuration.LayerFolderPath)
return nil, errors.Wrapf(err, "failed to prepare layer %s", configuration.LayerFolderPath)
}
volumePath, err := hcsshim.GetLayerMountPath(di, s.id)
if err != nil {
if err := hcsshim.DestroyLayer(di, s.id); err != nil {
log.L.Warnf("failed to DestroyLayer %s: %s", s.id, err)
}
return nil, errors.Wrapf(err, "failed to getmount path for layer %s: driverInfo: %#v", s.id, di)
}
configuration.VolumePath = volumePath
f, err := os.OpenFile(fmt.Sprintf("%s-hcs.json", s.id), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 066)
if err != nil {
fmt.Println("failed to create file:", err)
} else {
defer f.Close()
enc := json.NewEncoder(f)
enc.Encode(configuration)
}
return configuration, nil
}
// removeLayer delete the given layer, all associated containers must have
// been shutdown for this to succeed.
func removeLayer(ctx context.Context, path string) error {
layerID := filepath.Base(path)
parentPath := filepath.Dir(path)
di := hcsshim.DriverInfo{
Flavour: 1, // filter driver
HomeDir: parentPath,
}
err := hcsshim.UnprepareLayer(di, layerID)
if err != nil {
log.G(ctx).WithError(err).Warnf("failed to unprepare layer %s for removal", path)
}
err = hcsshim.DeactivateLayer(di, layerID)
if err != nil {
log.G(ctx).WithError(err).Warnf("failed to deactivate layer %s for removal", path)
}
removePath := filepath.Join(parentPath, fmt.Sprintf("%s-removing", layerID))
err = os.Rename(path, removePath)
if err != nil {
log.G(ctx).WithError(err).Warnf("failed to rename container layer %s for removal", path)
removePath = path
}
if err := hcsshim.DestroyLayer(di, removePath); err != nil {
log.G(ctx).WithError(err).Errorf("failed to remove container layer %s", removePath)
return err
}
return nil
}
// ociSpecEnvToHCSEnv converts from the OCI Spec ENV format to the one
// expected by HCS.
func ociSpecEnvToHCSEnv(a []string) map[string]string {
env := make(map[string]string)
for _, s := range a {
arr := strings.SplitN(s, "=", 2)
if len(arr) == 2 {
env[arr[0]] = arr[1]
}
}
return env
}