| package libcontainerd |
| |
| import ( |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "strings" |
| "syscall" |
| |
| "golang.org/x/net/context" |
| |
| "github.com/Microsoft/hcsshim" |
| "github.com/Sirupsen/logrus" |
| "github.com/docker/docker/pkg/sysinfo" |
| "github.com/opencontainers/runtime-spec/specs-go" |
| ) |
| |
| type client struct { |
| clientCommon |
| |
| // Platform specific properties below here (none presently on Windows) |
| } |
| |
| // Win32 error codes that are used for various workarounds |
| // These really should be ALL_CAPS to match golangs syscall library and standard |
| // Win32 error conventions, but golint insists on CamelCase. |
| const ( |
| CoEClassstring = syscall.Errno(0x800401F3) // Invalid class string |
| ErrorNoNetwork = syscall.Errno(1222) // The network is not present or not started |
| ErrorBadPathname = syscall.Errno(161) // The specified path is invalid |
| ErrorInvalidObject = syscall.Errno(0x800710D8) // The object identifier does not represent a valid object |
| ) |
| |
| // defaultOwner is a tag passed to HCS to allow it to differentiate between |
| // container creator management stacks. We hard code "docker" in the case |
| // of docker. |
| const defaultOwner = "docker" |
| |
| // Create is the entrypoint to create a container from a spec, and if successfully |
| // created, start it too. Table below shows the fields required for HCS JSON calling parameters, |
| // where if not populated, is omitted. |
| // +-----------------+--------------------------------------------+---------------------------------------------------+ |
| // | | Isolation=Process | Isolation=Hyper-V | |
| // +-----------------+--------------------------------------------+---------------------------------------------------+ |
| // | VolumePath | \\?\\Volume{GUIDa} | | |
| // | LayerFolderPath | %root%\windowsfilter\containerID | %root%\windowsfilter\containerID (servicing only) | |
| // | Layers[] | ID=GUIDb;Path=%root%\windowsfilter\layerID | ID=GUIDb;Path=%root%\windowsfilter\layerID | |
| // | SandboxPath | | %root%\windowsfilter | |
| // | HvRuntime | | ImagePath=%root%\BaseLayerID\UtilityVM | |
| // +-----------------+--------------------------------------------+---------------------------------------------------+ |
| // |
| // Isolation=Process example: |
| // |
| // { |
| // "SystemType": "Container", |
| // "Name": "5e0055c814a6005b8e57ac59f9a522066e0af12b48b3c26a9416e23907698776", |
| // "Owner": "docker", |
| // "IsDummy": false, |
| // "VolumePath": "\\\\\\\\?\\\\Volume{66d1ef4c-7a00-11e6-8948-00155ddbef9d}", |
| // "IgnoreFlushesDuringBoot": true, |
| // "LayerFolderPath": "C:\\\\control\\\\windowsfilter\\\\5e0055c814a6005b8e57ac59f9a522066e0af12b48b3c26a9416e23907698776", |
| // "Layers": [{ |
| // "ID": "18955d65-d45a-557b-bf1c-49d6dfefc526", |
| // "Path": "C:\\\\control\\\\windowsfilter\\\\65bf96e5760a09edf1790cb229e2dfb2dbd0fcdc0bf7451bae099106bfbfea0c" |
| // }], |
| // "HostName": "5e0055c814a6", |
| // "MappedDirectories": [], |
| // "HvPartition": false, |
| // "EndpointList": ["eef2649d-bb17-4d53-9937-295a8efe6f2c"], |
| // "Servicing": false |
| //} |
| // |
| // Isolation=Hyper-V example: |
| // |
| //{ |
| // "SystemType": "Container", |
| // "Name": "475c2c58933b72687a88a441e7e0ca4bd72d76413c5f9d5031fee83b98f6045d", |
| // "Owner": "docker", |
| // "IsDummy": false, |
| // "IgnoreFlushesDuringBoot": true, |
| // "Layers": [{ |
| // "ID": "18955d65-d45a-557b-bf1c-49d6dfefc526", |
| // "Path": "C:\\\\control\\\\windowsfilter\\\\65bf96e5760a09edf1790cb229e2dfb2dbd0fcdc0bf7451bae099106bfbfea0c" |
| // }], |
| // "HostName": "475c2c58933b", |
| // "MappedDirectories": [], |
| // "SandboxPath": "C:\\\\control\\\\windowsfilter", |
| // "HvPartition": true, |
| // "EndpointList": ["e1bb1e61-d56f-405e-b75d-fd520cefa0cb"], |
| // "HvRuntime": { |
| // "ImagePath": "C:\\\\control\\\\windowsfilter\\\\65bf96e5760a09edf1790cb229e2dfb2dbd0fcdc0bf7451bae099106bfbfea0c\\\\UtilityVM" |
| // }, |
| // "Servicing": false |
| //} |
| func (clnt *client) Create(containerID string, checkpoint string, checkpointDir string, spec specs.Spec, attachStdio StdioCallback, options ...CreateOption) error { |
| clnt.lock(containerID) |
| defer clnt.unlock(containerID) |
| logrus.Debugln("libcontainerd: client.Create() with spec", spec) |
| |
| configuration := &hcsshim.ContainerConfig{ |
| SystemType: "Container", |
| Name: containerID, |
| Owner: defaultOwner, |
| IgnoreFlushesDuringBoot: false, |
| HostName: spec.Hostname, |
| HvPartition: false, |
| } |
| |
| if spec.Windows.Resources != nil { |
| if spec.Windows.Resources.CPU != nil { |
| if spec.Windows.Resources.CPU.Count != nil { |
| // This check is being done here rather than in adaptContainerSettings |
| // because we don't want to update the HostConfig in case this container |
| // is moved to a host with more CPUs than this one. |
| cpuCount := *spec.Windows.Resources.CPU.Count |
| hostCPUCount := uint64(sysinfo.NumCPU()) |
| if cpuCount > hostCPUCount { |
| logrus.Warnf("Changing requested CPUCount of %d to current number of processors, %d", cpuCount, hostCPUCount) |
| cpuCount = hostCPUCount |
| } |
| configuration.ProcessorCount = uint32(cpuCount) |
| } |
| if spec.Windows.Resources.CPU.Shares != nil { |
| configuration.ProcessorWeight = uint64(*spec.Windows.Resources.CPU.Shares) |
| } |
| if spec.Windows.Resources.CPU.Percent != nil { |
| configuration.ProcessorMaximum = int64(*spec.Windows.Resources.CPU.Percent) * 100 // ProcessorMaximum is a value between 1 and 10000 |
| } |
| } |
| if spec.Windows.Resources.Memory != nil { |
| if spec.Windows.Resources.Memory.Limit != nil { |
| configuration.MemoryMaximumInMB = int64(*spec.Windows.Resources.Memory.Limit) / 1024 / 1024 |
| } |
| } |
| if spec.Windows.Resources.Storage != nil { |
| if spec.Windows.Resources.Storage.Bps != nil { |
| configuration.StorageBandwidthMaximum = *spec.Windows.Resources.Storage.Bps |
| } |
| if spec.Windows.Resources.Storage.Iops != nil { |
| configuration.StorageIOPSMaximum = *spec.Windows.Resources.Storage.Iops |
| } |
| } |
| } |
| |
| var layerOpt *LayerOption |
| for _, option := range options { |
| if s, ok := option.(*ServicingOption); ok { |
| configuration.Servicing = s.IsServicing |
| continue |
| } |
| if f, ok := option.(*FlushOption); ok { |
| configuration.IgnoreFlushesDuringBoot = f.IgnoreFlushesDuringBoot |
| continue |
| } |
| if h, ok := option.(*HyperVIsolationOption); ok { |
| configuration.HvPartition = h.IsHyperV |
| configuration.SandboxPath = h.SandboxPath |
| continue |
| } |
| if l, ok := option.(*LayerOption); ok { |
| layerOpt = l |
| } |
| if n, ok := option.(*NetworkEndpointsOption); ok { |
| configuration.EndpointList = n.Endpoints |
| configuration.AllowUnqualifiedDNSQuery = n.AllowUnqualifiedDNSQuery |
| continue |
| } |
| if c, ok := option.(*CredentialsOption); ok { |
| configuration.Credentials = c.Credentials |
| continue |
| } |
| } |
| |
| // We must have a layer option with at least one path |
| if layerOpt == nil || layerOpt.LayerPaths == nil { |
| return fmt.Errorf("no layer option or paths were supplied to the runtime") |
| } |
| |
| if configuration.HvPartition { |
| // Find the upper-most utility VM image, since the utility VM does not |
| // use layering in RS1. |
| // TODO @swernli/jhowardmsft at some point post RS1 this may be re-locatable. |
| var uvmImagePath string |
| for _, path := range layerOpt.LayerPaths { |
| fullPath := filepath.Join(path, "UtilityVM") |
| _, err := os.Stat(fullPath) |
| if err == nil { |
| uvmImagePath = fullPath |
| break |
| } |
| if !os.IsNotExist(err) { |
| return err |
| } |
| } |
| if uvmImagePath == "" { |
| return errors.New("utility VM image could not be found") |
| } |
| configuration.HvRuntime = &hcsshim.HvRuntime{ImagePath: uvmImagePath} |
| } else { |
| configuration.VolumePath = spec.Root.Path |
| } |
| |
| configuration.LayerFolderPath = layerOpt.LayerFolderPath |
| |
| for _, layerPath := range layerOpt.LayerPaths { |
| _, filename := filepath.Split(layerPath) |
| g, err := hcsshim.NameToGuid(filename) |
| if err != nil { |
| return err |
| } |
| configuration.Layers = append(configuration.Layers, hcsshim.Layer{ |
| ID: g.ToString(), |
| Path: layerPath, |
| }) |
| } |
| |
| // Add the mounts (volumes, bind mounts etc) to the structure |
| mds := make([]hcsshim.MappedDir, len(spec.Mounts)) |
| for i, mount := range 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 |
| |
| hcsContainer, err := hcsshim.CreateContainer(containerID, configuration) |
| if err != nil { |
| return err |
| } |
| |
| // Construct a container object for calling start on it. |
| container := &container{ |
| containerCommon: containerCommon{ |
| process: process{ |
| processCommon: processCommon{ |
| containerID: containerID, |
| client: clnt, |
| friendlyName: InitFriendlyName, |
| }, |
| commandLine: strings.Join(spec.Process.Args, " "), |
| }, |
| processes: make(map[string]*process), |
| }, |
| ociSpec: spec, |
| hcsContainer: hcsContainer, |
| } |
| |
| container.options = options |
| for _, option := range options { |
| if err := option.Apply(container); err != nil { |
| logrus.Errorf("libcontainerd: %v", err) |
| } |
| } |
| |
| // Call start, and if it fails, delete the container from our |
| // internal structure, start will keep HCS in sync by deleting the |
| // container there. |
| logrus.Debugf("libcontainerd: Create() id=%s, Calling start()", containerID) |
| if err := container.start(attachStdio); err != nil { |
| clnt.deleteContainer(containerID) |
| return err |
| } |
| |
| logrus.Debugf("libcontainerd: Create() id=%s completed successfully", containerID) |
| return nil |
| |
| } |
| |
| // AddProcess is the handler for adding a process to an already running |
| // container. It's called through docker exec. It returns the system pid of the |
| // exec'd process. |
| func (clnt *client) AddProcess(ctx context.Context, containerID, processFriendlyName string, procToAdd Process, attachStdio StdioCallback) (int, error) { |
| clnt.lock(containerID) |
| defer clnt.unlock(containerID) |
| container, err := clnt.getContainer(containerID) |
| if err != nil { |
| return -1, err |
| } |
| // Note we always tell HCS to |
| // create stdout as it's required regardless of '-i' or '-t' options, so that |
| // docker can always grab the output through logs. We also tell HCS to always |
| // create stdin, even if it's not used - it will be closed shortly. Stderr |
| // is only created if it we're not -t. |
| createProcessParms := hcsshim.ProcessConfig{ |
| EmulateConsole: procToAdd.Terminal, |
| CreateStdInPipe: true, |
| CreateStdOutPipe: true, |
| CreateStdErrPipe: !procToAdd.Terminal, |
| } |
| createProcessParms.ConsoleSize[0] = uint(procToAdd.ConsoleSize.Height) |
| createProcessParms.ConsoleSize[1] = uint(procToAdd.ConsoleSize.Width) |
| |
| // Take working directory from the process to add if it is defined, |
| // otherwise take from the first process. |
| if procToAdd.Cwd != "" { |
| createProcessParms.WorkingDirectory = procToAdd.Cwd |
| } else { |
| createProcessParms.WorkingDirectory = container.ociSpec.Process.Cwd |
| } |
| |
| // Configure the environment for the process |
| createProcessParms.Environment = setupEnvironmentVariables(procToAdd.Env) |
| createProcessParms.CommandLine = strings.Join(procToAdd.Args, " ") |
| createProcessParms.User = procToAdd.User.Username |
| |
| logrus.Debugf("libcontainerd: commandLine: %s", createProcessParms.CommandLine) |
| |
| // Start the command running in the container. |
| var stdout, stderr io.ReadCloser |
| var stdin io.WriteCloser |
| newProcess, err := container.hcsContainer.CreateProcess(&createProcessParms) |
| if err != nil { |
| logrus.Errorf("libcontainerd: AddProcess(%s) CreateProcess() failed %s", containerID, err) |
| return -1, err |
| } |
| |
| pid := newProcess.Pid() |
| |
| stdin, stdout, stderr, err = newProcess.Stdio() |
| if err != nil { |
| logrus.Errorf("libcontainerd: %s getting std pipes failed %s", containerID, err) |
| return -1, err |
| } |
| |
| iopipe := &IOPipe{Terminal: procToAdd.Terminal} |
| iopipe.Stdin = createStdInCloser(stdin, newProcess) |
| |
| // Convert io.ReadClosers to io.Readers |
| if stdout != nil { |
| iopipe.Stdout = ioutil.NopCloser(&autoClosingReader{ReadCloser: stdout}) |
| } |
| if stderr != nil { |
| iopipe.Stderr = ioutil.NopCloser(&autoClosingReader{ReadCloser: stderr}) |
| } |
| |
| proc := &process{ |
| processCommon: processCommon{ |
| containerID: containerID, |
| friendlyName: processFriendlyName, |
| client: clnt, |
| systemPid: uint32(pid), |
| }, |
| commandLine: createProcessParms.CommandLine, |
| hcsProcess: newProcess, |
| } |
| |
| // Add the process to the container's list of processes |
| container.processes[processFriendlyName] = proc |
| |
| // Tell the engine to attach streams back to the client |
| if err := attachStdio(*iopipe); err != nil { |
| return -1, err |
| } |
| |
| // Spin up a go routine waiting for exit to handle cleanup |
| go container.waitExit(proc, false) |
| |
| return pid, nil |
| } |
| |
| // Signal handles `docker stop` on Windows. While Linux has support for |
| // the full range of signals, signals aren't really implemented on Windows. |
| // We fake supporting regular stop and -9 to force kill. |
| func (clnt *client) Signal(containerID string, sig int) error { |
| var ( |
| cont *container |
| err error |
| ) |
| |
| // Get the container as we need it to get the container handle. |
| clnt.lock(containerID) |
| defer clnt.unlock(containerID) |
| if cont, err = clnt.getContainer(containerID); err != nil { |
| return err |
| } |
| |
| cont.manualStopRequested = true |
| |
| logrus.Debugf("libcontainerd: Signal() containerID=%s sig=%d pid=%d", containerID, sig, cont.systemPid) |
| |
| if syscall.Signal(sig) == syscall.SIGKILL { |
| // Terminate the compute system |
| if err := cont.hcsContainer.Terminate(); err != nil { |
| if !hcsshim.IsPending(err) { |
| logrus.Errorf("libcontainerd: failed to terminate %s - %q", containerID, err) |
| } |
| } |
| } else { |
| // Terminate Process |
| if err := cont.hcsProcess.Kill(); err != nil && !hcsshim.IsAlreadyStopped(err) { |
| // ignore errors |
| logrus.Warnf("libcontainerd: failed to terminate pid %d in %s: %q", cont.systemPid, containerID, err) |
| } |
| } |
| |
| return nil |
| } |
| |
| // While Linux has support for the full range of signals, signals aren't really implemented on Windows. |
| // We try to terminate the specified process whatever signal is requested. |
| func (clnt *client) SignalProcess(containerID string, processFriendlyName string, sig int) error { |
| clnt.lock(containerID) |
| defer clnt.unlock(containerID) |
| cont, err := clnt.getContainer(containerID) |
| if err != nil { |
| return err |
| } |
| |
| for _, p := range cont.processes { |
| if p.friendlyName == processFriendlyName { |
| return p.hcsProcess.Kill() |
| } |
| } |
| |
| return fmt.Errorf("SignalProcess could not find process %s in %s", processFriendlyName, containerID) |
| } |
| |
| // Resize handles a CLI event to resize an interactive docker run or docker exec |
| // window. |
| func (clnt *client) Resize(containerID, processFriendlyName string, width, height int) error { |
| // Get the libcontainerd container object |
| clnt.lock(containerID) |
| defer clnt.unlock(containerID) |
| cont, err := clnt.getContainer(containerID) |
| if err != nil { |
| return err |
| } |
| |
| h, w := uint16(height), uint16(width) |
| |
| if processFriendlyName == InitFriendlyName { |
| logrus.Debugln("libcontainerd: resizing systemPID in", containerID, cont.process.systemPid) |
| return cont.process.hcsProcess.ResizeConsole(w, h) |
| } |
| |
| for _, p := range cont.processes { |
| if p.friendlyName == processFriendlyName { |
| logrus.Debugln("libcontainerd: resizing exec'd process", containerID, p.systemPid) |
| return p.hcsProcess.ResizeConsole(w, h) |
| } |
| } |
| |
| return fmt.Errorf("Resize could not find containerID %s to resize", containerID) |
| |
| } |
| |
| // Pause handles pause requests for containers |
| func (clnt *client) Pause(containerID string) error { |
| unlockContainer := true |
| // Get the libcontainerd container object |
| clnt.lock(containerID) |
| defer func() { |
| if unlockContainer { |
| clnt.unlock(containerID) |
| } |
| }() |
| container, err := clnt.getContainer(containerID) |
| if err != nil { |
| return err |
| } |
| |
| for _, option := range container.options { |
| if h, ok := option.(*HyperVIsolationOption); ok { |
| if !h.IsHyperV { |
| return errors.New("cannot pause Windows Server Containers") |
| } |
| break |
| } |
| } |
| |
| err = container.hcsContainer.Pause() |
| if err != nil { |
| return err |
| } |
| |
| // Unlock container before calling back into the daemon |
| unlockContainer = false |
| clnt.unlock(containerID) |
| |
| return clnt.backend.StateChanged(containerID, StateInfo{ |
| CommonStateInfo: CommonStateInfo{ |
| State: StatePause, |
| }}) |
| } |
| |
| // Resume handles resume requests for containers |
| func (clnt *client) Resume(containerID string) error { |
| unlockContainer := true |
| // Get the libcontainerd container object |
| clnt.lock(containerID) |
| defer func() { |
| if unlockContainer { |
| clnt.unlock(containerID) |
| } |
| }() |
| container, err := clnt.getContainer(containerID) |
| if err != nil { |
| return err |
| } |
| |
| // This should never happen, since Windows Server Containers cannot be paused |
| for _, option := range container.options { |
| if h, ok := option.(*HyperVIsolationOption); ok { |
| if !h.IsHyperV { |
| return errors.New("cannot resume Windows Server Containers") |
| } |
| break |
| } |
| } |
| |
| err = container.hcsContainer.Resume() |
| if err != nil { |
| return err |
| } |
| |
| // Unlock container before calling back into the daemon |
| unlockContainer = false |
| clnt.unlock(containerID) |
| |
| return clnt.backend.StateChanged(containerID, StateInfo{ |
| CommonStateInfo: CommonStateInfo{ |
| State: StateResume, |
| }}) |
| } |
| |
| // Stats handles stats requests for containers |
| func (clnt *client) Stats(containerID string) (*Stats, error) { |
| // Get the libcontainerd container object |
| clnt.lock(containerID) |
| defer clnt.unlock(containerID) |
| container, err := clnt.getContainer(containerID) |
| if err != nil { |
| return nil, err |
| } |
| s, err := container.hcsContainer.Statistics() |
| if err != nil { |
| return nil, err |
| } |
| st := Stats(s) |
| return &st, nil |
| } |
| |
| // Restore is the handler for restoring a container |
| func (clnt *client) Restore(containerID string, _ StdioCallback, unusedOnWindows ...CreateOption) error { |
| logrus.Debugf("libcontainerd: Restore(%s)", containerID) |
| |
| // TODO Windows: On RS1, a re-attach isn't possible. |
| // However, there is a scenario in which there is an issue. |
| // Consider a background container. The daemon dies unexpectedly. |
| // HCS will still have the compute service alive and running. |
| // For consistence, we call in to shoot it regardless if HCS knows about it |
| // We explicitly just log a warning if the terminate fails. |
| // Then we tell the backend the container exited. |
| if hc, err := hcsshim.OpenContainer(containerID); err == nil { |
| if err := hc.Terminate(); err != nil { |
| if !hcsshim.IsPending(err) { |
| logrus.Warnf("libcontainerd: failed to terminate %s on restore - %q", containerID, err) |
| } |
| } |
| } |
| return clnt.backend.StateChanged(containerID, StateInfo{ |
| CommonStateInfo: CommonStateInfo{ |
| State: StateExit, |
| ExitCode: 1 << 31, |
| }}) |
| } |
| |
| // GetPidsForContainer returns a list of process IDs running in a container. |
| // Although implemented, this is not used in Windows. |
| func (clnt *client) GetPidsForContainer(containerID string) ([]int, error) { |
| var pids []int |
| clnt.lock(containerID) |
| defer clnt.unlock(containerID) |
| cont, err := clnt.getContainer(containerID) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Add the first process |
| pids = append(pids, int(cont.containerCommon.systemPid)) |
| // And add all the exec'd processes |
| for _, p := range cont.processes { |
| pids = append(pids, int(p.processCommon.systemPid)) |
| } |
| return pids, nil |
| } |
| |
| // Summary returns a summary of the processes running in a container. |
| // This is present in Windows to support docker top. In linux, the |
| // engine shells out to ps to get process information. On Windows, as |
| // the containers could be Hyper-V containers, they would not be |
| // visible on the container host. However, libcontainerd does have |
| // that information. |
| func (clnt *client) Summary(containerID string) ([]Summary, error) { |
| |
| // Get the libcontainerd container object |
| clnt.lock(containerID) |
| defer clnt.unlock(containerID) |
| container, err := clnt.getContainer(containerID) |
| if err != nil { |
| return nil, err |
| } |
| p, err := container.hcsContainer.ProcessList() |
| if err != nil { |
| return nil, err |
| } |
| pl := make([]Summary, len(p)) |
| for i := range p { |
| pl[i] = Summary(p[i]) |
| } |
| return pl, nil |
| } |
| |
| // UpdateResources updates resources for a running container. |
| func (clnt *client) UpdateResources(containerID string, resources Resources) error { |
| // Updating resource isn't supported on Windows |
| // but we should return nil for enabling updating container |
| return nil |
| } |
| |
| func (clnt *client) CreateCheckpoint(containerID string, checkpointID string, checkpointDir string, exit bool) error { |
| return errors.New("Windows: Containers do not support checkpoints") |
| } |
| |
| func (clnt *client) DeleteCheckpoint(containerID string, checkpointID string, checkpointDir string) error { |
| return errors.New("Windows: Containers do not support checkpoints") |
| } |
| |
| func (clnt *client) ListCheckpoints(containerID string, checkpointDir string) (*Checkpoints, error) { |
| return nil, errors.New("Windows: Containers do not support checkpoints") |
| } |
| |
| func (clnt *client) GetServerVersion(ctx context.Context) (*ServerVersion, error) { |
| return &ServerVersion{}, nil |
| } |