Merge pull request #388 from docker/api

Merge API Branch into Master
This commit is contained in:
Victor Marmol 2015-02-19 16:30:01 -08:00
commit 5b73860e65
150 changed files with 9214 additions and 5669 deletions

View File

@ -6,5 +6,4 @@ script:
- bash /go/src/github.com/docker/docker/hack/make/validate-dco
- bash /go/src/github.com/docker/docker/hack/make/validate-gofmt
- export GOPATH="$GOPATH:/go:$(pwd)/vendor" # Drone mucks with our GOPATH
- go get golang.org/x/tools/cmd/vet && go vet ./...
- make direct-test

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
nsinit/nsinit

View File

@ -22,3 +22,5 @@ direct-build:
direct-install:
go install -v $(GO_PACKAGES)
local:
go test -v

View File

@ -8,7 +8,7 @@ In the design and development of libcontainer we try to follow these principles:
* Less code is better.
* Fewer components are better. Do you really need to add one more class?
* 50 lines of straightforward, readable code is better than 10 lines of magic that nobody can understand.
* Don't do later what you can do now. "//FIXME: refactor" is not acceptable in new code.
* Don't do later what you can do now. "//TODO: refactor" is not acceptable in new code.
* When hesitating between two options, choose the one that is easier to reverse.
* "No" is temporary; "Yes" is forever. If you're not sure about a new feature, say no. You can change your mind later.
* Containers must be portable to the greatest possible number of machines. Be suspicious of any change which makes machines less interchangeable.

159
README.md
View File

@ -1,48 +1,169 @@
## libcontainer - reference implementation for containers [![Build Status](https://ci.dockerproject.com/github.com/docker/libcontainer/status.svg?branch=master)](https://ci.dockerproject.com/github.com/docker/libcontainer)
### Note on API changes:
Please bear with us while we work on making the libcontainer API stable and something that we can support long term. We are currently discussing the API with the community, therefore, if you currently depend on libcontainer please pin your dependency at a specific tag or commit id. Please join the discussion and help shape the API.
#### Background
libcontainer specifies configuration options for what a container is. It provides a native Go implementation for using Linux namespaces with no external dependencies. libcontainer provides many convenience functions for working with namespaces, networking, and management.
Libcontainer provides a native Go implementation for creating containers
with namespaces, cgroups, capabilities, and filesystem access controls.
It allows you to manage the lifecycle of the container performing additional operations
after the container is created.
#### Container
A container is a self contained execution environment that shares the kernel of the host system and which is (optionally) isolated from other containers in the system.
A container is a self contained execution environment that shares the kernel of the
host system and which is (optionally) isolated from other containers in the system.
libcontainer may be used to execute a process in a container. If a user tries to run a new process inside an existing container, the new process is added to the processes executing in the container.
#### Using libcontainer
To create a container you first have to initialize an instance of a factory
that will handle the creation and initialization for a container.
Because containers are spawned in a two step process you will need to provide
arguments to a binary that will be executed as the init process for the container.
To use the current binary that is spawning the containers and acting as the parent
you can use `os.Args[0]` and we have a command called `init` setup.
```go
initArgs := []string{os.Args[0], "init"}
root, err := libcontainer.New("/var/lib/container", initArgs)
if err != nil {
log.Fatal(err)
}
```
Once you have an instance of the factory created we can create a configuration
struct describing how the container is to be created. A sample would look similar to this:
```go
config := &configs.Config{
Rootfs: rootfs,
Capabilities: []string{
"CHOWN",
"DAC_OVERRIDE",
"FSETID",
"FOWNER",
"MKNOD",
"NET_RAW",
"SETGID",
"SETUID",
"SETFCAP",
"SETPCAP",
"NET_BIND_SERVICE",
"SYS_CHROOT",
"KILL",
"AUDIT_WRITE",
},
Namespaces: configs.Namespaces([]configs.Namespace{
{Type: configs.NEWNS},
{Type: configs.NEWUTS},
{Type: configs.NEWIPC},
{Type: configs.NEWPID},
{Type: configs.NEWNET},
}),
Cgroups: &configs.Cgroup{
Name: "test-container",
Parent: "system",
AllowAllDevices: false,
AllowedDevices: configs.DefaultAllowedDevices,
},
Devices: configs.DefaultAutoCreatedDevices,
Hostname: "testing",
Networks: []*configs.Network{
{
Type: "loopback",
Address: "127.0.0.1/0",
Gateway: "localhost",
},
},
Rlimits: []configs.Rlimit{
{
Type: syscall.RLIMIT_NOFILE,
Hard: uint64(1024),
Soft: uint64(1024),
},
},
}
```
Once you have the configuration populated you can create a container:
```go
container, err := root.Create("container-id", config)
```
To spawn bash as the initial process inside the container and have the
processes pid returned in order to wait, signal, or kill the process:
```go
process := &libcontainer.Process{
Args: []string{"/bin/bash"},
Env: []string{"PATH=/bin"},
User: "daemon",
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
}
pid, err := container.Start(process)
if err != nil {
log.Fatal(err)
}
#### Root file system
// wait for the process to finish.
wait(pid)
A container runs with a directory known as its *root file system*, or *rootfs*, mounted as the file system root. The rootfs is usually a full system tree.
// destroy the container.
container.Destroy()
```
Additional ways to interact with a running container are:
```go
// return all the pids for all processes running inside the container.
processes, err := container.Processes()
// get detailed cpu, memory, io, and network statistics for the container and
// it's processes.
stats, err := container.Stats()
#### Configuration
// pause all processes inside the container.
container.Pause()
A container is initially configured by supplying configuration data when the container is created.
// resume all paused processes.
container.Resume()
```
#### nsinit
`nsinit` is a cli application which demonstrates the use of libcontainer. It is able to spawn new containers or join existing containers, based on the current directory.
`nsinit` is a cli application which demonstrates the use of libcontainer.
It is able to spawn new containers or join existing containers. A root
filesystem must be provided for use along with a container configuration file.
To use `nsinit`, cd into a Linux rootfs and copy a `container.json` file into the directory with your specified configuration. Environment, networking, and different capabilities for the container are specified in this file. The configuration is used for each process executed inside the container.
To use `nsinit`, cd into a Linux rootfs and copy a `container.json` file into
the directory with your specified configuration. Environment, networking,
and different capabilities for the container are specified in this file.
The configuration is used for each process executed inside the container.
See the `sample_configs` folder for examples of what the container configuration should look like.
To execute `/bin/bash` in the current directory as a container just run the following **as root**:
```bash
nsinit exec /bin/bash
nsinit exec --tty /bin/bash
```
If you wish to spawn another process inside the container while your current bash session is running, run the same command again to get another bash shell (or change the command). If the original process (PID 1) dies, all other processes spawned inside the container will be killed and the namespace will be removed.
If you wish to spawn another process inside the container while your
current bash session is running, run the same command again to
get another bash shell (or change the command). If the original
process (PID 1) dies, all other processes spawned inside the container
will be killed and the namespace will be removed.
You can identify if a process is running in a container by looking to see if `state.json` is in the root of the directory.
You can identify if a process is running in a container by
looking to see if `state.json` is in the root of the directory.
You may also specify an alternate root place where the `container.json` file is read and where the `state.json` file will be saved.
You may also specify an alternate root place where
the `container.json` file is read and where the `state.json` file will be saved.
#### Future
See the [roadmap](ROADMAP.md).

View File

@ -1,21 +0,0 @@
/*
Temporary API endpoint for libcontainer while the full API is finalized (api.go).
*/
package libcontainer
import (
"github.com/docker/libcontainer/cgroups/fs"
"github.com/docker/libcontainer/network"
)
// TODO(vmarmol): Complete Stats() in final libcontainer API and move users to that.
// DEPRECATED: The below portions are only to be used during the transition to the official API.
// Returns all available stats for the given container.
func GetStats(container *Config, state *State) (stats *ContainerStats, err error) {
stats = &ContainerStats{}
if stats.CgroupStats, err = fs.GetStats(state.CgroupPaths); err != nil {
return stats, err
}
stats.NetworkStats, err = network.GetStats(&state.NetworkState)
return stats, err
}

View File

@ -24,7 +24,6 @@ func ApplyProfile(name string) error {
if name == "" {
return nil
}
cName := C.CString(name)
defer C.free(unsafe.Pointer(cName))

View File

@ -3,16 +3,35 @@ package cgroups
import (
"fmt"
"github.com/docker/libcontainer/devices"
"github.com/docker/libcontainer/configs"
)
type FreezerState string
type Manager interface {
// Apply cgroup configuration to the process with the specified pid
Apply(pid int) error
const (
Undefined FreezerState = ""
Frozen FreezerState = "FROZEN"
Thawed FreezerState = "THAWED"
)
// Returns the PIDs inside the cgroup set
GetPids() ([]int, error)
// Returns statistics for the cgroup set
GetStats() (*Stats, error)
// Toggles the freezer cgroup according with specified state
Freeze(state configs.FreezerState) error
// Destroys the cgroup set
Destroy() error
// NewCgroupManager() and LoadCgroupManager() require following attributes:
// Paths map[string]string
// Cgroups *cgroups.Cgroup
// Paths maps cgroup subsystem to path at which it is mounted.
// Cgroups specifies specific cgroup settings for the various subsystems
// Returns cgroup paths to save in a state file and to be able to
// restore the object later.
GetPaths() map[string]string
}
type NotFoundError struct {
Subsystem string
@ -32,26 +51,6 @@ func IsNotFound(err error) bool {
if err == nil {
return false
}
_, ok := err.(*NotFoundError)
return ok
}
type Cgroup struct {
Name string `json:"name,omitempty"`
Parent string `json:"parent,omitempty"` // name of parent cgroup or slice
AllowAllDevices bool `json:"allow_all_devices,omitempty"` // If this is true allow access to any kind of device within the container. If false, allow access only to devices explicitly listed in the allowed_devices list.
AllowedDevices []*devices.Device `json:"allowed_devices,omitempty"`
Memory int64 `json:"memory,omitempty"` // Memory limit (in bytes)
MemoryReservation int64 `json:"memory_reservation,omitempty"` // Memory reservation or soft_limit (in bytes)
MemorySwap int64 `json:"memory_swap,omitempty"` // Total memory usage (memory + swap); set `-1' to disable swap
CpuShares int64 `json:"cpu_shares,omitempty"` // CPU shares (relative weight vs. other containers)
CpuQuota int64 `json:"cpu_quota,omitempty"` // CPU hardcap limit (in usecs). Allowed cpu time in a given period.
CpuPeriod int64 `json:"cpu_period,omitempty"` // CPU period to be used for hardcapping (in usecs). 0 to use system default.
CpusetCpus string `json:"cpuset_cpus,omitempty"` // CPU to use
CpusetMems string `json:"cpuset_mems,omitempty"` // MEM to use
BlkioWeight int64 `json:"blkio_weight,omitempty"` // Specifies per cgroup weight, range is from 10 to 1000.
Freezer FreezerState `json:"freezer,omitempty"` // set the freeze value for the process
Slice string `json:"slice,omitempty"` // Parent slice to use for systemd
}

View File

@ -8,6 +8,7 @@ import (
"sync"
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/configs"
)
var (
@ -24,6 +25,20 @@ var (
CgroupProcesses = "cgroup.procs"
)
type subsystem interface {
// Returns the stats, as 'stats', corresponding to the cgroup under 'path'.
GetStats(path string, stats *cgroups.Stats) error
// Removes the cgroup represented by 'data'.
Remove(*data) error
// Creates and joins the cgroup represented by data.
Set(*data) error
}
type Manager struct {
Cgroups *configs.Cgroup
Paths map[string]string
}
// The absolute path to the root of the cgroup hierarchies.
var cgroupRootLock sync.Mutex
var cgroupRoot string
@ -52,26 +67,21 @@ func getCgroupRoot() (string, error) {
return cgroupRoot, nil
}
type subsystem interface {
// Returns the stats, as 'stats', corresponding to the cgroup under 'path'.
GetStats(path string, stats *cgroups.Stats) error
// Removes the cgroup represented by 'data'.
Remove(*data) error
// Creates and joins the cgroup represented by data.
Set(*data) error
}
type data struct {
root string
cgroup string
c *cgroups.Cgroup
c *configs.Cgroup
pid int
}
func Apply(c *cgroups.Cgroup, pid int) (map[string]string, error) {
d, err := getCgroupData(c, pid)
func (m *Manager) Apply(pid int) error {
if m.Cgroups == nil {
return nil
}
d, err := getCgroupData(m.Cgroups, pid)
if err != nil {
return nil, err
return err
}
paths := make(map[string]string)
@ -82,9 +92,9 @@ func Apply(c *cgroups.Cgroup, pid int) (map[string]string, error) {
}()
for name, sys := range subsystems {
if err := sys.Set(d); err != nil {
return nil, err
return err
}
// FIXME: Apply should, ideally, be reentrant or be broken up into a separate
// TODO: Apply should, ideally, be reentrant or be broken up into a separate
// create and join phase so that the cgroup hierarchy for a container can be
// created then join consists of writing the process pids to cgroup.procs
p, err := d.path(name)
@ -92,16 +102,26 @@ func Apply(c *cgroups.Cgroup, pid int) (map[string]string, error) {
if cgroups.IsNotFound(err) {
continue
}
return nil, err
return err
}
paths[name] = p
}
return paths, nil
m.Paths = paths
return nil
}
func (m *Manager) Destroy() error {
return cgroups.RemovePaths(m.Paths)
}
func (m *Manager) GetPaths() map[string]string {
return m.Paths
}
// Symmetrical public function to update device based cgroups. Also available
// in the systemd implementation.
func ApplyDevices(c *cgroups.Cgroup, pid int) error {
func ApplyDevices(c *configs.Cgroup, pid int) error {
d, err := getCgroupData(c, pid)
if err != nil {
return err
@ -112,9 +132,9 @@ func ApplyDevices(c *cgroups.Cgroup, pid int) error {
return devices.Set(d)
}
func GetStats(systemPaths map[string]string) (*cgroups.Stats, error) {
func (m *Manager) GetStats() (*cgroups.Stats, error) {
stats := cgroups.NewStats()
for name, path := range systemPaths {
for name, path := range m.Paths {
sys, ok := subsystems[name]
if !ok || !cgroups.PathExists(path) {
continue
@ -129,27 +149,27 @@ func GetStats(systemPaths map[string]string) (*cgroups.Stats, error) {
// Freeze toggles the container's freezer cgroup depending on the state
// provided
func Freeze(c *cgroups.Cgroup, state cgroups.FreezerState) error {
d, err := getCgroupData(c, 0)
func (m *Manager) Freeze(state configs.FreezerState) error {
d, err := getCgroupData(m.Cgroups, 0)
if err != nil {
return err
}
prevState := c.Freezer
c.Freezer = state
prevState := m.Cgroups.Freezer
m.Cgroups.Freezer = state
freezer := subsystems["freezer"]
err = freezer.Set(d)
if err != nil {
c.Freezer = prevState
m.Cgroups.Freezer = prevState
return err
}
return nil
}
func GetPids(c *cgroups.Cgroup) ([]int, error) {
d, err := getCgroupData(c, 0)
func (m *Manager) GetPids() ([]int, error) {
d, err := getCgroupData(m.Cgroups, 0)
if err != nil {
return nil, err
}
@ -162,7 +182,7 @@ func GetPids(c *cgroups.Cgroup) ([]int, error) {
return cgroups.ReadProcsFile(dir)
}
func getCgroupData(c *cgroups.Cgroup, pid int) (*data, error) {
func getCgroupData(c *configs.Cgroup, pid int) (*data, error) {
root, err := getCgroupRoot()
if err != nil {
return nil, err

View File

@ -17,7 +17,7 @@ func (s *DevicesGroup) Set(d *data) error {
}
for _, dev := range d.c.AllowedDevices {
if err := writeFile(dir, "devices.allow", dev.GetCgroupAllowString()); err != nil {
if err := writeFile(dir, "devices.allow", dev.CgroupString()); err != nil {
return err
}
}

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/configs"
)
type FreezerGroup struct {
@ -12,7 +13,7 @@ type FreezerGroup struct {
func (s *FreezerGroup) Set(d *data) error {
switch d.c.Freezer {
case cgroups.Frozen, cgroups.Thawed:
case configs.Frozen, configs.Thawed:
dir, err := d.path("freezer")
if err != nil {
return err

View File

@ -6,24 +6,46 @@ import (
"fmt"
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/configs"
)
type Manager struct {
Cgroups *configs.Cgroup
Paths map[string]string
}
func UseSystemd() bool {
return false
}
func Apply(c *cgroups.Cgroup, pid int) (map[string]string, error) {
return nil, fmt.Errorf("Systemd not supported")
}
func GetPids(c *cgroups.Cgroup) ([]int, error) {
return nil, fmt.Errorf("Systemd not supported")
}
func ApplyDevices(c *cgroups.Cgroup, pid int) error {
func (m *Manager) Apply(pid int) error {
return fmt.Errorf("Systemd not supported")
}
func Freeze(c *cgroups.Cgroup, state cgroups.FreezerState) error {
func (m *Manager) GetPids() ([]int, error) {
return nil, fmt.Errorf("Systemd not supported")
}
func (m *Manager) Destroy() error {
return fmt.Errorf("Systemd not supported")
}
func (m *Manager) GetPaths() map[string]string {
return nil
}
func (m *Manager) GetStats() (*cgroups.Stats, error) {
return nil, fmt.Errorf("Systemd not supported")
}
func (m *Manager) Freeze(state configs.FreezerState) error {
return fmt.Errorf("Systemd not supported")
}
func ApplyDevices(c *configs.Cgroup, pid int) error {
return fmt.Errorf("Systemd not supported")
}
func Freeze(c *configs.Cgroup, state configs.FreezerState) error {
return fmt.Errorf("Systemd not supported")
}

View File

@ -16,11 +16,13 @@ import (
systemd "github.com/coreos/go-systemd/dbus"
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/cgroups/fs"
"github.com/docker/libcontainer/configs"
"github.com/godbus/dbus"
)
type systemdCgroup struct {
cgroup *cgroups.Cgroup
type Manager struct {
Cgroups *configs.Cgroup
Paths map[string]string
}
type subsystem interface {
@ -94,16 +96,14 @@ func getIfaceForUnit(unitName string) string {
return "Unit"
}
func Apply(c *cgroups.Cgroup, pid int) (map[string]string, error) {
func (m *Manager) Apply(pid int) error {
var (
c = m.Cgroups
unitName = getUnitName(c)
slice = "system.slice"
properties []systemd.Property
res = &systemdCgroup{}
)
res.cgroup = c
if c.Slice != "" {
slice = c.Slice
}
@ -143,23 +143,23 @@ func Apply(c *cgroups.Cgroup, pid int) (map[string]string, error) {
}
if _, err := theConn.StartTransientUnit(unitName, "replace", properties...); err != nil {
return nil, err
return err
}
if err := joinDevices(c, pid); err != nil {
return nil, err
return err
}
// TODO: CpuQuota and CpuPeriod not available in systemd
// we need to manually join the cpu.cfs_quota_us and cpu.cfs_period_us
if err := joinCpu(c, pid); err != nil {
return nil, err
return err
}
// -1 disables memorySwap
if c.MemorySwap >= 0 && c.Memory != 0 {
if err := joinMemory(c, pid); err != nil {
return nil, err
return err
}
}
@ -167,11 +167,11 @@ func Apply(c *cgroups.Cgroup, pid int) (map[string]string, error) {
// we need to manually join the freezer and cpuset cgroup in systemd
// because it does not currently support it via the dbus api.
if err := joinFreezer(c, pid); err != nil {
return nil, err
return err
}
if err := joinCpuset(c, pid); err != nil {
return nil, err
return err
}
paths := make(map[string]string)
@ -185,24 +185,35 @@ func Apply(c *cgroups.Cgroup, pid int) (map[string]string, error) {
"perf_event",
"freezer",
} {
subsystemPath, err := getSubsystemPath(res.cgroup, sysname)
subsystemPath, err := getSubsystemPath(m.Cgroups, sysname)
if err != nil {
// Don't fail if a cgroup hierarchy was not found, just skip this subsystem
if cgroups.IsNotFound(err) {
continue
}
return nil, err
return err
}
paths[sysname] = subsystemPath
}
return paths, nil
m.Paths = paths
return nil
}
func (m *Manager) Destroy() error {
return cgroups.RemovePaths(m.Paths)
}
func (m *Manager) GetPaths() map[string]string {
return m.Paths
}
func writeFile(dir, file, data string) error {
return ioutil.WriteFile(filepath.Join(dir, file), []byte(data), 0700)
}
func joinCpu(c *cgroups.Cgroup, pid int) error {
func joinCpu(c *configs.Cgroup, pid int) error {
path, err := getSubsystemPath(c, "cpu")
if err != nil {
return err
@ -220,7 +231,7 @@ func joinCpu(c *cgroups.Cgroup, pid int) error {
return nil
}
func joinFreezer(c *cgroups.Cgroup, pid int) error {
func joinFreezer(c *configs.Cgroup, pid int) error {
path, err := getSubsystemPath(c, "freezer")
if err != nil {
return err
@ -233,7 +244,7 @@ func joinFreezer(c *cgroups.Cgroup, pid int) error {
return ioutil.WriteFile(filepath.Join(path, "cgroup.procs"), []byte(strconv.Itoa(pid)), 0700)
}
func getSubsystemPath(c *cgroups.Cgroup, subsystem string) (string, error) {
func getSubsystemPath(c *configs.Cgroup, subsystem string) (string, error) {
mountpoint, err := cgroups.FindCgroupMountpoint(subsystem)
if err != nil {
return "", err
@ -252,8 +263,8 @@ func getSubsystemPath(c *cgroups.Cgroup, subsystem string) (string, error) {
return filepath.Join(mountpoint, initPath, slice, getUnitName(c)), nil
}
func Freeze(c *cgroups.Cgroup, state cgroups.FreezerState) error {
path, err := getSubsystemPath(c, "freezer")
func (m *Manager) Freeze(state configs.FreezerState) error {
path, err := getSubsystemPath(m.Cgroups, "freezer")
if err != nil {
return err
}
@ -271,11 +282,14 @@ func Freeze(c *cgroups.Cgroup, state cgroups.FreezerState) error {
}
time.Sleep(1 * time.Millisecond)
}
m.Cgroups.Freezer = state
return nil
}
func GetPids(c *cgroups.Cgroup) ([]int, error) {
path, err := getSubsystemPath(c, "cpu")
func (m *Manager) GetPids() ([]int, error) {
path, err := getSubsystemPath(m.Cgroups, "cpu")
if err != nil {
return nil, err
}
@ -283,7 +297,11 @@ func GetPids(c *cgroups.Cgroup) ([]int, error) {
return cgroups.ReadProcsFile(path)
}
func getUnitName(c *cgroups.Cgroup) string {
func (m *Manager) GetStats() (*cgroups.Stats, error) {
panic("not implemented")
}
func getUnitName(c *configs.Cgroup) string {
return fmt.Sprintf("%s-%s.scope", c.Parent, c.Name)
}
@ -298,7 +316,7 @@ func getUnitName(c *cgroups.Cgroup) string {
// Note: we can't use systemd to set up the initial limits, and then change the cgroup
// because systemd will re-write the device settings if it needs to re-apply the cgroup context.
// This happens at least for v208 when any sibling unit is started.
func joinDevices(c *cgroups.Cgroup, pid int) error {
func joinDevices(c *configs.Cgroup, pid int) error {
path, err := getSubsystemPath(c, "devices")
if err != nil {
return err
@ -316,24 +334,22 @@ func joinDevices(c *cgroups.Cgroup, pid int) error {
if err := writeFile(path, "devices.deny", "a"); err != nil {
return err
}
for _, dev := range c.AllowedDevices {
if err := writeFile(path, "devices.allow", dev.GetCgroupAllowString()); err != nil {
return err
}
}
for _, dev := range c.AllowedDevices {
if err := writeFile(path, "devices.allow", dev.CgroupString()); err != nil {
return err
}
}
return nil
}
// Symmetrical public function to update device based cgroups. Also available
// in the fs implementation.
func ApplyDevices(c *cgroups.Cgroup, pid int) error {
func ApplyDevices(c *configs.Cgroup, pid int) error {
return joinDevices(c, pid)
}
func joinMemory(c *cgroups.Cgroup, pid int) error {
func joinMemory(c *configs.Cgroup, pid int) error {
memorySwap := c.MemorySwap
if memorySwap == 0 {
@ -352,7 +368,7 @@ func joinMemory(c *cgroups.Cgroup, pid int) error {
// systemd does not atm set up the cpuset controller, so we must manually
// join it. Additionally that is a very finicky controller where each
// level must have a full setup as the default for a new directory is "no cpus"
func joinCpuset(c *cgroups.Cgroup, pid int) error {
func joinCpuset(c *configs.Cgroup, pid int) error {
path, err := getSubsystemPath(c, "cpuset")
if err != nil {
return err

166
config.go
View File

@ -1,166 +0,0 @@
package libcontainer
import (
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/mount"
"github.com/docker/libcontainer/network"
)
type MountConfig mount.MountConfig
type Network network.Network
type NamespaceType string
const (
NEWNET NamespaceType = "NEWNET"
NEWPID NamespaceType = "NEWPID"
NEWNS NamespaceType = "NEWNS"
NEWUTS NamespaceType = "NEWUTS"
NEWIPC NamespaceType = "NEWIPC"
NEWUSER NamespaceType = "NEWUSER"
)
// Namespace defines configuration for each namespace. It specifies an
// alternate path that is able to be joined via setns.
type Namespace struct {
Type NamespaceType `json:"type"`
Path string `json:"path,omitempty"`
}
type Namespaces []Namespace
func (n *Namespaces) Remove(t NamespaceType) bool {
i := n.index(t)
if i == -1 {
return false
}
*n = append((*n)[:i], (*n)[i+1:]...)
return true
}
func (n *Namespaces) Add(t NamespaceType, path string) {
i := n.index(t)
if i == -1 {
*n = append(*n, Namespace{Type: t, Path: path})
return
}
(*n)[i].Path = path
}
func (n *Namespaces) index(t NamespaceType) int {
for i, ns := range *n {
if ns.Type == t {
return i
}
}
return -1
}
func (n *Namespaces) Contains(t NamespaceType) bool {
return n.index(t) != -1
}
// Config defines configuration options for executing a process inside a contained environment.
type Config struct {
// Mount specific options.
MountConfig *MountConfig `json:"mount_config,omitempty"`
// Pathname to container's root filesystem
RootFs string `json:"root_fs,omitempty"`
// Hostname optionally sets the container's hostname if provided
Hostname string `json:"hostname,omitempty"`
// User will set the uid and gid of the executing process running inside the container
User string `json:"user,omitempty"`
// WorkingDir will change the processes current working directory inside the container's rootfs
WorkingDir string `json:"working_dir,omitempty"`
// Env will populate the processes environment with the provided values
// Any values from the parent processes will be cleared before the values
// provided in Env are provided to the process
Env []string `json:"environment,omitempty"`
// Tty when true will allocate a pty slave on the host for access by the container's process
// and ensure that it is mounted inside the container's rootfs
Tty bool `json:"tty,omitempty"`
// Namespaces specifies the container's namespaces that it should setup when cloning the init process
// If a namespace is not provided that namespace is shared from the container's parent process
Namespaces Namespaces `json:"namespaces,omitempty"`
// Capabilities specify the capabilities to keep when executing the process inside the container
// All capbilities not specified will be dropped from the processes capability mask
Capabilities []string `json:"capabilities,omitempty"`
// Networks specifies the container's network setup to be created
Networks []*Network `json:"networks,omitempty"`
// Routes can be specified to create entries in the route table as the container is started
Routes []*Route `json:"routes,omitempty"`
// Cgroups specifies specific cgroup settings for the various subsystems that the container is
// placed into to limit the resources the container has available
Cgroups *cgroups.Cgroup `json:"cgroups,omitempty"`
// AppArmorProfile specifies the profile to apply to the process running in the container and is
// change at the time the process is execed
AppArmorProfile string `json:"apparmor_profile,omitempty"`
// ProcessLabel specifies the label to apply to the process running in the container. It is
// commonly used by selinux
ProcessLabel string `json:"process_label,omitempty"`
// RestrictSys will remount /proc/sys, /sys, and mask over sysrq-trigger as well as /proc/irq and
// /proc/bus
RestrictSys bool `json:"restrict_sys,omitempty"`
// Rlimits specifies the resource limits, such as max open files, to set in the container
// If Rlimits are not set, the container will inherit rlimits from the parent process
Rlimits []Rlimit `json:"rlimits,omitempty"`
// AdditionalGroups specifies the gids that should be added to supplementary groups
// in addition to those that the user belongs to.
AdditionalGroups []int `json:"additional_groups,omitempty"`
// UidMappings is an array of User ID mappings for User Namespaces
UidMappings []IDMap `json:"uid_mappings,omitempty"`
// GidMappings is an array of Group ID mappings for User Namespaces
GidMappings []IDMap `json:"gid_mappings,omitempty"`
}
// Routes can be specified to create entries in the route table as the container is started
//
// All of destination, source, and gateway should be either IPv4 or IPv6.
// One of the three options must be present, and ommitted entries will use their
// IP family default for the route table. For IPv4 for example, setting the
// gateway to 1.2.3.4 and the interface to eth0 will set up a standard
// destination of 0.0.0.0(or *) when viewed in the route table.
type Route struct {
// Sets the destination and mask, should be a CIDR. Accepts IPv4 and IPv6
Destination string `json:"destination,omitempty"`
// Sets the source and mask, should be a CIDR. Accepts IPv4 and IPv6
Source string `json:"source,omitempty"`
// Sets the gateway. Accepts IPv4 and IPv6
Gateway string `json:"gateway,omitempty"`
// The device to set this route up for, for example: eth0
InterfaceName string `json:"interface_name,omitempty"`
}
type Rlimit struct {
Type int `json:"type,omitempty"`
Hard uint64 `json:"hard,omitempty"`
Soft uint64 `json:"soft,omitempty"`
}
// IDMap represents UID/GID Mappings for User Namespaces.
type IDMap struct {
ContainerID int `json:"container_id,omitempty"`
HostID int `json:"host_id,omitempty"`
Size int `json:"size,omitempty"`
}

54
configs/cgroup.go Normal file
View File

@ -0,0 +1,54 @@
package configs
type FreezerState string
const (
Undefined FreezerState = ""
Frozen FreezerState = "FROZEN"
Thawed FreezerState = "THAWED"
)
type Cgroup struct {
Name string `json:"name"`
// name of parent cgroup or slice
Parent string `json:"parent"`
// If this is true allow access to any kind of device within the container. If false, allow access only to devices explicitly listed in the allowed_devices list.
AllowAllDevices bool `json:"allow_all_devices"`
AllowedDevices []*Device `json:"allowed_devices"`
// Memory limit (in bytes)
Memory int64 `json:"memory"`
// Memory reservation or soft_limit (in bytes)
MemoryReservation int64 `json:"memory_reservation"`
// Total memory usage (memory + swap); set `-1' to disable swap
MemorySwap int64 `json:"memory_swap"`
// CPU shares (relative weight vs. other containers)
CpuShares int64 `json:"cpu_shares"`
// CPU hardcap limit (in usecs). Allowed cpu time in a given period.
CpuQuota int64 `json:"cpu_quota"`
// CPU period to be used for hardcapping (in usecs). 0 to use system default.
CpuPeriod int64 `json:"cpu_period"`
// CPU to use
CpusetCpus string `json:"cpuset_cpus"`
// MEM to use
CpusetMems string `json:"cpuset_mems"`
// Specifies per cgroup weight, range is from 10 to 1000.
BlkioWeight int64 `json:"blkio_weight"`
// set the freeze value for the process
Freezer FreezerState `json:"freezer"`
// Parent slice to use for systemd TODO: remove in favor or parent
Slice string `json:"slice"`
}

148
configs/config.go Normal file
View File

@ -0,0 +1,148 @@
package configs
import "fmt"
type Rlimit struct {
Type int `json:"type"`
Hard uint64 `json:"hard"`
Soft uint64 `json:"soft"`
}
// IDMap represents UID/GID Mappings for User Namespaces.
type IDMap struct {
ContainerID int `json:"container_id"`
HostID int `json:"host_id"`
Size int `json:"size"`
}
// Config defines configuration options for executing a process inside a contained environment.
type Config struct {
// NoPivotRoot will use MS_MOVE and a chroot to jail the process into the container's rootfs
// This is a common option when the container is running in ramdisk
NoPivotRoot bool `json:"no_pivot_root"`
// ParentDeathSignal specifies the signal that is sent to the container's process in the case
// that the parent process dies.
ParentDeathSignal int `json:"parent_death_signal"`
// PivotDir allows a custom directory inside the container's root filesystem to be used as pivot, when NoPivotRoot is not set.
// When a custom PivotDir not set, a temporary dir inside the root filesystem will be used. The pivot dir needs to be writeable.
// This is required when using read only root filesystems. In these cases, a read/writeable path can be (bind) mounted somewhere inside the root filesystem to act as pivot.
PivotDir string `json:"pivot_dir"`
// Path to a directory containing the container's root filesystem.
Rootfs string `json:"rootfs"`
// Readonlyfs will remount the container's rootfs as readonly where only externally mounted
// bind mounts are writtable.
Readonlyfs bool `json:"readonlyfs"`
// Mounts specify additional source and destination paths that will be mounted inside the container's
// rootfs and mount namespace if specified
Mounts []*Mount `json:"mounts"`
// The device nodes that should be automatically created within the container upon container start. Note, make sure that the node is marked as allowed in the cgroup as well!
Devices []*Device `json:"devices"`
MountLabel string `json:"mount_label"`
// Hostname optionally sets the container's hostname if provided
Hostname string `json:"hostname"`
// Console is the path to the console allocated to the container.
Console string `json:"console"`
// Namespaces specifies the container's namespaces that it should setup when cloning the init process
// If a namespace is not provided that namespace is shared from the container's parent process
Namespaces Namespaces `json:"namespaces"`
// Capabilities specify the capabilities to keep when executing the process inside the container
// All capbilities not specified will be dropped from the processes capability mask
Capabilities []string `json:"capabilities"`
// Networks specifies the container's network setup to be created
Networks []*Network `json:"networks"`
// Routes can be specified to create entries in the route table as the container is started
Routes []*Route `json:"routes"`
// Cgroups specifies specific cgroup settings for the various subsystems that the container is
// placed into to limit the resources the container has available
Cgroups *Cgroup `json:"cgroups"`
// AppArmorProfile specifies the profile to apply to the process running in the container and is
// change at the time the process is execed
AppArmorProfile string `json:"apparmor_profile"`
// ProcessLabel specifies the label to apply to the process running in the container. It is
// commonly used by selinux
ProcessLabel string `json:"process_label"`
// Rlimits specifies the resource limits, such as max open files, to set in the container
// If Rlimits are not set, the container will inherit rlimits from the parent process
Rlimits []Rlimit `json:"rlimits"`
// AdditionalGroups specifies the gids that should be added to supplementary groups
// in addition to those that the user belongs to.
AdditionalGroups []int `json:"additional_groups"`
// UidMappings is an array of User ID mappings for User Namespaces
UidMappings []IDMap `json:"uid_mappings"`
// GidMappings is an array of Group ID mappings for User Namespaces
GidMappings []IDMap `json:"gid_mappings"`
// MaskPaths specifies paths within the container's rootfs to mask over with a bind
// mount pointing to /dev/null as to prevent reads of the file.
MaskPaths []string `json:"mask_paths"`
// ReadonlyPaths specifies paths within the container's rootfs to remount as read-only
// so that these files prevent any writes.
ReadonlyPaths []string `json:"readonly_paths"`
}
// Gets the root uid for the process on host which could be non-zero
// when user namespaces are enabled.
func (c *Config) HostUID() (int, error) {
if c.Namespaces.Contains(NEWUSER) {
if c.UidMappings == nil {
return -1, fmt.Errorf("User namespaces enabled, but no user mappings found.")
}
id, found := c.hostIDFromMapping(0, c.UidMappings)
if !found {
return -1, fmt.Errorf("User namespaces enabled, but no root user mapping found.")
}
return id, nil
}
// Return default root uid 0
return 0, nil
}
// Gets the root uid for the process on host which could be non-zero
// when user namespaces are enabled.
func (c *Config) HostGID() (int, error) {
if c.Namespaces.Contains(NEWUSER) {
if c.GidMappings == nil {
return -1, fmt.Errorf("User namespaces enabled, but no gid mappings found.")
}
id, found := c.hostIDFromMapping(0, c.GidMappings)
if !found {
return -1, fmt.Errorf("User namespaces enabled, but no root user mapping found.")
}
return id, nil
}
// Return default root uid 0
return 0, nil
}
// Utility function that gets a host ID for a container ID from user namespace map
// if that ID is present in the map.
func (c *Config) hostIDFromMapping(containerID int, uMap []IDMap) (int, bool) {
for _, m := range uMap {
if (containerID >= m.ContainerID) && (containerID <= (m.ContainerID + m.Size - 1)) {
hostID := m.HostID + (containerID - m.ContainerID)
return hostID, true
}
}
return -1, false
}

View File

@ -1,12 +1,10 @@
package libcontainer
package configs
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/docker/libcontainer/devices"
)
// Checks whether the expected capability is specified in the capabilities.
@ -19,13 +17,13 @@ func contains(expected string, values []string) bool {
return false
}
func containsDevice(expected *devices.Device, values []*devices.Device) bool {
func containsDevice(expected *Device, values []*Device) bool {
for _, d := range values {
if d.Path == expected.Path &&
d.CgroupPermissions == expected.CgroupPermissions &&
d.Permissions == expected.Permissions &&
d.FileMode == expected.FileMode &&
d.MajorNumber == expected.MajorNumber &&
d.MinorNumber == expected.MinorNumber &&
d.Major == expected.Major &&
d.Minor == expected.Minor &&
d.Type == expected.Type {
return true
}
@ -34,7 +32,7 @@ func containsDevice(expected *devices.Device, values []*devices.Device) bool {
}
func loadConfig(name string) (*Config, error) {
f, err := os.Open(filepath.Join("sample_configs", name))
f, err := os.Open(filepath.Join("../sample_configs", name))
if err != nil {
return nil, err
}
@ -59,11 +57,6 @@ func TestConfigJsonFormat(t *testing.T) {
t.Fail()
}
if !container.Tty {
t.Log("tty should be set to true")
t.Fail()
}
if !container.Namespaces.Contains(NEWNET) {
t.Log("namespaces should contain NEWNET")
t.Fail()
@ -101,11 +94,6 @@ func TestConfigJsonFormat(t *testing.T) {
t.Fail()
}
if n.VethPrefix != "veth" {
t.Logf("veth prefix should be veth but received %q", n.VethPrefix)
t.Fail()
}
if n.Gateway != "172.17.42.1" {
t.Logf("veth gateway should be 172.17.42.1 but received %q", n.Gateway)
t.Fail()
@ -119,18 +107,12 @@ func TestConfigJsonFormat(t *testing.T) {
break
}
}
for _, d := range devices.DefaultSimpleDevices {
if !containsDevice(d, container.MountConfig.DeviceNodes) {
for _, d := range DefaultSimpleDevices {
if !containsDevice(d, container.Devices) {
t.Logf("expected device configuration for %s", d.Path)
t.Fail()
}
}
if !container.RestrictSys {
t.Log("expected restrict sys to be true")
t.Fail()
}
}
func TestApparmorProfile(t *testing.T) {
@ -154,8 +136,8 @@ func TestSelinuxLabels(t *testing.T) {
if container.ProcessLabel != label {
t.Fatalf("expected process label %q but received %q", label, container.ProcessLabel)
}
if container.MountConfig.MountLabel != label {
t.Fatalf("expected mount label %q but received %q", label, container.MountConfig.MountLabel)
if container.MountLabel != label {
t.Fatalf("expected mount label %q but received %q", label, container.MountLabel)
}
}
@ -170,3 +152,69 @@ func TestRemoveNamespace(t *testing.T) {
t.Fatalf("namespaces should have 0 items but reports %d", len(ns))
}
}
func TestHostUIDNoUSERNS(t *testing.T) {
config := &Config{
Namespaces: Namespaces{},
}
uid, err := config.HostUID()
if err != nil {
t.Fatal(err)
}
if uid != 0 {
t.Fatalf("expected uid 0 with no USERNS but received %d", uid)
}
}
func TestHostUIDWithUSERNS(t *testing.T) {
config := &Config{
Namespaces: Namespaces{{Type: NEWUSER}},
UidMappings: []IDMap{
{
ContainerID: 0,
HostID: 1000,
Size: 1,
},
},
}
uid, err := config.HostUID()
if err != nil {
t.Fatal(err)
}
if uid != 1000 {
t.Fatalf("expected uid 1000 with no USERNS but received %d", uid)
}
}
func TestHostGIDNoUSERNS(t *testing.T) {
config := &Config{
Namespaces: Namespaces{},
}
uid, err := config.HostGID()
if err != nil {
t.Fatal(err)
}
if uid != 0 {
t.Fatalf("expected gid 0 with no USERNS but received %d", uid)
}
}
func TestHostGIDWithUSERNS(t *testing.T) {
config := &Config{
Namespaces: Namespaces{{Type: NEWUSER}},
GidMappings: []IDMap{
{
ContainerID: 0,
HostID: 1000,
Size: 1,
},
},
}
uid, err := config.HostGID()
if err != nil {
t.Fatal(err)
}
if uid != 1000 {
t.Fatalf("expected gid 1000 with no USERNS but received %d", uid)
}
}

52
configs/device.go Normal file
View File

@ -0,0 +1,52 @@
package configs
import (
"fmt"
"os"
)
const (
Wildcard = -1
)
type Device struct {
// Device type, block, char, etc.
Type rune `json:"type"`
// Path to the device.
Path string `json:"path"`
// Major is the device's major number.
Major int64 `json:"major"`
// Minor is the device's minor number.
Minor int64 `json:"minor"`
// Cgroup permissions format, rwm.
Permissions string `json:"permissions"`
// FileMode permission bits for the device.
FileMode os.FileMode `json:"file_mode"`
// Uid of the device.
Uid uint32 `json:"uid"`
// Gid of the device.
Gid uint32 `json:"gid"`
}
func (d *Device) CgroupString() string {
return fmt.Sprintf("%c %s:%s %s", d.Type, deviceNumberString(d.Major), deviceNumberString(d.Minor), d.Permissions)
}
func (d *Device) Mkdev() int {
return int((d.Major << 8) | (d.Minor & 0xff) | ((d.Minor & 0xfff00) << 12))
}
// deviceNumberString converts the device number to a string return result.
func deviceNumberString(number int64) string {
if number == Wildcard {
return "*"
}
return fmt.Sprint(number)
}

137
configs/device_defaults.go Normal file
View File

@ -0,0 +1,137 @@
package configs
var (
// These are devices that are to be both allowed and created.
DefaultSimpleDevices = []*Device{
// /dev/null and zero
{
Path: "/dev/null",
Type: 'c',
Major: 1,
Minor: 3,
Permissions: "rwm",
FileMode: 0666,
},
{
Path: "/dev/zero",
Type: 'c',
Major: 1,
Minor: 5,
Permissions: "rwm",
FileMode: 0666,
},
{
Path: "/dev/full",
Type: 'c',
Major: 1,
Minor: 7,
Permissions: "rwm",
FileMode: 0666,
},
// consoles and ttys
{
Path: "/dev/tty",
Type: 'c',
Major: 5,
Minor: 0,
Permissions: "rwm",
FileMode: 0666,
},
// /dev/urandom,/dev/random
{
Path: "/dev/urandom",
Type: 'c',
Major: 1,
Minor: 9,
Permissions: "rwm",
FileMode: 0666,
},
{
Path: "/dev/random",
Type: 'c',
Major: 1,
Minor: 8,
Permissions: "rwm",
FileMode: 0666,
},
}
DefaultAllowedDevices = append([]*Device{
// allow mknod for any device
{
Type: 'c',
Major: Wildcard,
Minor: Wildcard,
Permissions: "m",
},
{
Type: 'b',
Major: Wildcard,
Minor: Wildcard,
Permissions: "m",
},
{
Path: "/dev/console",
Type: 'c',
Major: 5,
Minor: 1,
Permissions: "rwm",
},
{
Path: "/dev/tty0",
Type: 'c',
Major: 4,
Minor: 0,
Permissions: "rwm",
},
{
Path: "/dev/tty1",
Type: 'c',
Major: 4,
Minor: 1,
Permissions: "rwm",
},
// /dev/pts/ - pts namespaces are "coming soon"
{
Path: "",
Type: 'c',
Major: 136,
Minor: Wildcard,
Permissions: "rwm",
},
{
Path: "",
Type: 'c',
Major: 5,
Minor: 2,
Permissions: "rwm",
},
// tuntap
{
Path: "",
Type: 'c',
Major: 10,
Minor: 200,
Permissions: "rwm",
},
}, DefaultSimpleDevices...)
DefaultAutoCreatedDevices = append([]*Device{
{
// /dev/fuse is created but not allowed.
// This is to allow java to work. Because java
// Insists on there being a /dev/fuse
// https://github.com/docker/docker/issues/514
// https://github.com/docker/docker/issues/2393
//
Path: "/dev/fuse",
Type: 'c',
Major: 10,
Minor: 229,
Permissions: "rwm",
},
}, DefaultSimpleDevices...)
)

21
configs/mount.go Normal file
View File

@ -0,0 +1,21 @@
package configs
type Mount struct {
// Source path for the mount.
Source string `json:"source"`
// Destination path for the mount inside the container.
Destination string `json:"destination"`
// Device the mount is for.
Device string `json:"device"`
// Mount flags.
Flags int `json:"flags"`
// Mount data applied to the mount.
Data string `json:"data"`
// Relabel source if set, "z" indicates shared, "Z" indicates unshared.
Relabel string `json:"relabel"`
}

109
configs/namespaces.go Normal file
View File

@ -0,0 +1,109 @@
package configs
import (
"fmt"
"syscall"
)
type NamespaceType string
const (
NEWNET NamespaceType = "NEWNET"
NEWPID NamespaceType = "NEWPID"
NEWNS NamespaceType = "NEWNS"
NEWUTS NamespaceType = "NEWUTS"
NEWIPC NamespaceType = "NEWIPC"
NEWUSER NamespaceType = "NEWUSER"
)
// Namespace defines configuration for each namespace. It specifies an
// alternate path that is able to be joined via setns.
type Namespace struct {
Type NamespaceType `json:"type"`
Path string `json:"path"`
}
func (n *Namespace) Syscall() int {
return namespaceInfo[n.Type]
}
func (n *Namespace) GetPath(pid int) string {
if n.Path != "" {
return n.Path
}
return fmt.Sprintf("/proc/%d/ns/%s", pid, n.file())
}
func (n *Namespace) file() string {
file := ""
switch n.Type {
case NEWNET:
file = "net"
case NEWNS:
file = "mnt"
case NEWPID:
file = "pid"
case NEWIPC:
file = "ipc"
case NEWUSER:
file = "user"
case NEWUTS:
file = "uts"
}
return file
}
type Namespaces []Namespace
func (n *Namespaces) Remove(t NamespaceType) bool {
i := n.index(t)
if i == -1 {
return false
}
*n = append((*n)[:i], (*n)[i+1:]...)
return true
}
func (n *Namespaces) Add(t NamespaceType, path string) {
i := n.index(t)
if i == -1 {
*n = append(*n, Namespace{Type: t, Path: path})
return
}
(*n)[i].Path = path
}
func (n *Namespaces) index(t NamespaceType) int {
for i, ns := range *n {
if ns.Type == t {
return i
}
}
return -1
}
func (n *Namespaces) Contains(t NamespaceType) bool {
return n.index(t) != -1
}
var namespaceInfo = map[NamespaceType]int{
NEWNET: syscall.CLONE_NEWNET,
NEWNS: syscall.CLONE_NEWNS,
NEWUSER: syscall.CLONE_NEWUSER,
NEWIPC: syscall.CLONE_NEWIPC,
NEWUTS: syscall.CLONE_NEWUTS,
NEWPID: syscall.CLONE_NEWPID,
}
// CloneFlags parses the container's Namespaces options to set the correct
// flags on clone, unshare. This functions returns flags only for new namespaces.
func (n *Namespaces) CloneFlags() uintptr {
var flag int
for _, v := range *n {
if v.Path != "" {
continue
}
flag |= namespaceInfo[v.Type]
}
return uintptr(flag)
}

66
configs/network.go Normal file
View File

@ -0,0 +1,66 @@
package configs
// Network defines configuration for a container's networking stack
//
// The network configuration can be omited from a container causing the
// container to be setup with the host's networking stack
type Network struct {
// Type sets the networks type, commonly veth and loopback
Type string `json:"type"`
// Name of the network interface
Name string `json:"name"`
// The bridge to use.
Bridge string `json:"bridge"`
// MacAddress contains the MAC address to set on the network interface
MacAddress string `json:"mac_address"`
// Address contains the IPv4 and mask to set on the network interface
Address string `json:"address"`
// Gateway sets the gateway address that is used as the default for the interface
Gateway string `json:"gateway"`
// IPv6Address contains the IPv6 and mask to set on the network interface
IPv6Address string `json:"ipv6_address"`
// IPv6Gateway sets the ipv6 gateway address that is used as the default for the interface
IPv6Gateway string `json:"ipv6_gateway"`
// Mtu sets the mtu value for the interface and will be mirrored on both the host and
// container's interfaces if a pair is created, specifically in the case of type veth
// Note: This does not apply to loopback interfaces.
Mtu int `json:"mtu"`
// TxQueueLen sets the tx_queuelen value for the interface and will be mirrored on both the host and
// container's interfaces if a pair is created, specifically in the case of type veth
// Note: This does not apply to loopback interfaces.
TxQueueLen int `json:"txqueuelen"`
// HostInterfaceName is a unique name of a veth pair that resides on in the host interface of the
// container.
HostInterfaceName string `json:"host_interface_name"`
}
// Routes can be specified to create entries in the route table as the container is started
//
// All of destination, source, and gateway should be either IPv4 or IPv6.
// One of the three options must be present, and ommitted entries will use their
// IP family default for the route table. For IPv4 for example, setting the
// gateway to 1.2.3.4 and the interface to eth0 will set up a standard
// destination of 0.0.0.0(or *) when viewed in the route table.
type Route struct {
// Sets the destination and mask, should be a CIDR. Accepts IPv4 and IPv6
Destination string `json:"destination"`
// Sets the source and mask, should be a CIDR. Accepts IPv4 and IPv6
Source string `json:"source"`
// Sets the gateway. Accepts IPv4 and IPv6
Gateway string `json:"gateway"`
// The device to set this route up for, for example: eth0
InterfaceName string `json:"interface_name"`
}

View File

@ -0,0 +1,93 @@
package validate
import (
"fmt"
"os"
"path/filepath"
"github.com/docker/libcontainer/configs"
)
type Validator interface {
Validate(*configs.Config) error
}
func New() Validator {
return &ConfigValidator{}
}
type ConfigValidator struct {
}
func (v *ConfigValidator) Validate(config *configs.Config) error {
if err := v.rootfs(config); err != nil {
return err
}
if err := v.network(config); err != nil {
return err
}
if err := v.hostname(config); err != nil {
return err
}
if err := v.security(config); err != nil {
return err
}
if err := v.usernamespace(config); err != nil {
return err
}
return nil
}
// rootfs validates the the rootfs is an absolute path and is not a symlink
// to the container's root filesystem.
func (v *ConfigValidator) rootfs(config *configs.Config) error {
cleaned, err := filepath.Abs(config.Rootfs)
if err != nil {
return err
}
if cleaned, err = filepath.EvalSymlinks(cleaned); err != nil {
return err
}
if config.Rootfs != cleaned {
return fmt.Errorf("%s is not an absolute path or is a symlink", config.Rootfs)
}
return nil
}
func (v *ConfigValidator) network(config *configs.Config) error {
if !config.Namespaces.Contains(configs.NEWNET) {
if len(config.Networks) > 0 || len(config.Routes) > 0 {
return fmt.Errorf("unable to apply network settings without a private NET namespace")
}
}
return nil
}
func (v *ConfigValidator) hostname(config *configs.Config) error {
if config.Hostname != "" && !config.Namespaces.Contains(configs.NEWUTS) {
return fmt.Errorf("unable to set hostname without a private UTS namespace")
}
return nil
}
func (v *ConfigValidator) security(config *configs.Config) error {
// restrict sys without mount namespace
if (len(config.MaskPaths) > 0 || len(config.ReadonlyPaths) > 0) &&
!config.Namespaces.Contains(configs.NEWNS) {
return fmt.Errorf("unable to restrict sys entries without a private MNT namespace")
}
return nil
}
func (v *ConfigValidator) usernamespace(config *configs.Config) error {
if config.Namespaces.Contains(configs.NEWUSER) {
if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
return fmt.Errorf("USER namespaces aren't enabled in the kernel")
}
} else {
if config.UidMappings != nil || config.GidMappings != nil {
return fmt.Errorf("User namespace mappings specified, but USER namespace isn't enabled in the config")
}
}
return nil
}

15
console.go Normal file
View File

@ -0,0 +1,15 @@
package libcontainer
import "io"
// Console represents a pseudo TTY.
type Console interface {
io.ReadWriter
io.Closer
// Path returns the filesystem path to the slave side of the pty.
Path() string
// Fd returns the fd for the master of the pty.
Fd() uintptr
}

View File

@ -1,128 +0,0 @@
// +build linux
package console
import (
"fmt"
"os"
"path/filepath"
"syscall"
"unsafe"
"github.com/docker/libcontainer/label"
)
// Setup initializes the proper /dev/console inside the rootfs path
func Setup(rootfs, consolePath, mountLabel string, hostRootUid, hostRootGid int) error {
oldMask := syscall.Umask(0000)
defer syscall.Umask(oldMask)
if err := os.Chmod(consolePath, 0600); err != nil {
return err
}
if err := os.Chown(consolePath, hostRootUid, hostRootGid); err != nil {
return err
}
if err := label.SetFileLabel(consolePath, mountLabel); err != nil {
return fmt.Errorf("set file label %s %s", consolePath, err)
}
dest := filepath.Join(rootfs, "dev/console")
f, err := os.Create(dest)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("create %s %s", dest, err)
}
if f != nil {
f.Close()
}
if err := syscall.Mount(consolePath, dest, "bind", syscall.MS_BIND, ""); err != nil {
return fmt.Errorf("bind %s to %s %s", consolePath, dest, err)
}
return nil
}
func OpenAndDup(consolePath string) error {
slave, err := OpenTerminal(consolePath, syscall.O_RDWR)
if err != nil {
return fmt.Errorf("open terminal %s", err)
}
if err := syscall.Dup2(int(slave.Fd()), 0); err != nil {
return err
}
if err := syscall.Dup2(int(slave.Fd()), 1); err != nil {
return err
}
return syscall.Dup2(int(slave.Fd()), 2)
}
// Unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f.
// Unlockpt should be called before opening the slave side of a pseudoterminal.
func Unlockpt(f *os.File) error {
var u int32
return Ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))
}
// Ptsname retrieves the name of the first available pts for the given master.
func Ptsname(f *os.File) (string, error) {
var n int32
if err := Ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))); err != nil {
return "", err
}
return fmt.Sprintf("/dev/pts/%d", n), nil
}
// CreateMasterAndConsole will open /dev/ptmx on the host and retreive the
// pts name for use as the pty slave inside the container
func CreateMasterAndConsole() (*os.File, string, error) {
master, err := os.OpenFile("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0)
if err != nil {
return nil, "", err
}
console, err := Ptsname(master)
if err != nil {
return nil, "", err
}
if err := Unlockpt(master); err != nil {
return nil, "", err
}
return master, console, nil
}
// OpenPtmx opens /dev/ptmx, i.e. the PTY master.
func OpenPtmx() (*os.File, error) {
// O_NOCTTY and O_CLOEXEC are not present in os package so we use the syscall's one for all.
return os.OpenFile("/dev/ptmx", syscall.O_RDONLY|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0)
}
// OpenTerminal is a clone of os.OpenFile without the O_CLOEXEC
// used to open the pty slave inside the container namespace
func OpenTerminal(name string, flag int) (*os.File, error) {
r, e := syscall.Open(name, flag, 0)
if e != nil {
return nil, &os.PathError{Op: "open", Path: name, Err: e}
}
return os.NewFile(uintptr(r), name), nil
}
func Ioctl(fd uintptr, flag, data uintptr) error {
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, flag, data); err != 0 {
return err
}
return nil
}

View File

@ -1,8 +1,55 @@
/*
NOTE: The API is in flux and mainly not implemented. Proceed with caution until further notice.
*/
// Libcontainer provides a native Go implementation for creating containers
// with namespaces, cgroups, capabilities, and filesystem access controls.
// It allows you to manage the lifecycle of the container performing additional operations
// after the container is created.
package libcontainer
import (
"os"
"github.com/docker/libcontainer/configs"
)
// The status of a container.
type Status int
const (
// The container exists and is running.
Running Status = iota + 1
// The container exists, it is in the process of being paused.
Pausing
// The container exists, but all its processes are paused.
Paused
// The container does not exist.
Destroyed
)
// State represents a running container's state
type State struct {
// ID is the container ID.
ID string `json:"id"`
// InitProcessPid is the init process id in the parent namespace.
InitProcessPid int `json:"init_process_pid"`
// InitProcessStartTime is the init process start time.
InitProcessStartTime string `json:"init_process_start"`
// Path to all the cgroups setup for a container. Key is cgroup subsystem name
// with the value as the path.
CgroupPaths map[string]string `json:"cgroup_paths"`
// NamespacePaths are filepaths to the container's namespaces. Key is the namespace name
// with the value as the path.
NamespacePaths map[string]string `json:"namespace_paths"`
// Config is the container's configuration.
Config configs.Config `json:"config"`
}
// A libcontainer container object.
//
// Each container is thread-safe within the same process. Since a container can
@ -12,67 +59,87 @@ type Container interface {
// Returns the ID of the container
ID() string
// Returns the current run state of the container.
// Returns the current status of the container.
//
// Errors:
// errors:
// ContainerDestroyed - Container no longer exists,
// SystemError - System error.
RunState() (*RunState, Error)
// Systemerror - System error.
Status() (Status, error)
// State returns the current container's state information.
//
// errors:
// Systemerror - System erroor.
State() (*State, error)
// Returns the current config of the container.
Config() *Config
Config() configs.Config
// Returns the PIDs inside this container. The PIDs are in the namespace of the calling process.
//
// errors:
// ContainerDestroyed - Container no longer exists,
// Systemerror - System error.
//
// Some of the returned PIDs may no longer refer to processes in the Container, unless
// the Container state is PAUSED in which case every PID in the slice is valid.
Processes() ([]int, error)
// Returns statistics for the container.
//
// errors:
// ContainerDestroyed - Container no longer exists,
// Systemerror - System error.
Stats() (*Stats, error)
// Start a process inside the container. Returns the PID of the new process (in the caller process's namespace) and a channel that will return the exit status of the process whenever it dies.
//
// Errors:
// errors:
// ContainerDestroyed - Container no longer exists,
// ConfigInvalid - config is invalid,
// ContainerPaused - Container is paused,
// SystemError - System error.
Start(config *ProcessConfig) (pid int, exitChan chan int, err Error)
// Systemerror - System error.
Start(process *Process) (pid int, err error)
// Destroys the container after killing all running processes.
//
// Any event registrations are removed before the container is destroyed.
// No error is returned if the container is already destroyed.
//
// Errors:
// SystemError - System error.
Destroy() Error
// Returns the PIDs inside this container. The PIDs are in the namespace of the calling process.
//
// Errors:
// ContainerDestroyed - Container no longer exists,
// SystemError - System error.
//
// Some of the returned PIDs may no longer refer to processes in the Container, unless
// the Container state is PAUSED in which case every PID in the slice is valid.
Processes() ([]int, Error)
// Returns statistics for the container.
//
// Errors:
// ContainerDestroyed - Container no longer exists,
// SystemError - System error.
Stats() (*ContainerStats, Error)
// errors:
// Systemerror - System error.
Destroy() error
// If the Container state is RUNNING or PAUSING, sets the Container state to PAUSING and pauses
// the execution of any user processes. Asynchronously, when the container finished being paused the
// state is changed to PAUSED.
// If the Container state is PAUSED, do nothing.
//
// Errors:
// errors:
// ContainerDestroyed - Container no longer exists,
// SystemError - System error.
Pause() Error
// Systemerror - System error.
Pause() error
// If the Container state is PAUSED, resumes the execution of any user processes in the
// Container before setting the Container state to RUNNING.
// If the Container state is RUNNING, do nothing.
//
// Errors:
// errors:
// ContainerDestroyed - Container no longer exists,
// SystemError - System error.
Resume() Error
// Systemerror - System error.
Resume() error
// Signal sends the specified signal to the init process of the container.
//
// errors:
// ContainerDestroyed - Container no longer exists,
// ContainerPaused - Container is paused,
// Systemerror - System error.
Signal(signal os.Signal) error
// NotifyOOM returns a read-only channel signaling when the container receives an OOM notification.
//
// errors:
// Systemerror - System error.
NotifyOOM() (<-chan struct{}, error)
}

View File

@ -1,159 +0,0 @@
package devices
var (
// These are devices that are to be both allowed and created.
DefaultSimpleDevices = []*Device{
// /dev/null and zero
{
Path: "/dev/null",
Type: 'c',
MajorNumber: 1,
MinorNumber: 3,
CgroupPermissions: "rwm",
FileMode: 0666,
},
{
Path: "/dev/zero",
Type: 'c',
MajorNumber: 1,
MinorNumber: 5,
CgroupPermissions: "rwm",
FileMode: 0666,
},
{
Path: "/dev/full",
Type: 'c',
MajorNumber: 1,
MinorNumber: 7,
CgroupPermissions: "rwm",
FileMode: 0666,
},
// consoles and ttys
{
Path: "/dev/tty",
Type: 'c',
MajorNumber: 5,
MinorNumber: 0,
CgroupPermissions: "rwm",
FileMode: 0666,
},
// /dev/urandom,/dev/random
{
Path: "/dev/urandom",
Type: 'c',
MajorNumber: 1,
MinorNumber: 9,
CgroupPermissions: "rwm",
FileMode: 0666,
},
{
Path: "/dev/random",
Type: 'c',
MajorNumber: 1,
MinorNumber: 8,
CgroupPermissions: "rwm",
FileMode: 0666,
},
}
DefaultAllowedDevices = append([]*Device{
// allow mknod for any device
{
Type: 'c',
MajorNumber: Wildcard,
MinorNumber: Wildcard,
CgroupPermissions: "m",
},
{
Type: 'b',
MajorNumber: Wildcard,
MinorNumber: Wildcard,
CgroupPermissions: "m",
},
{
Path: "/dev/console",
Type: 'c',
MajorNumber: 5,
MinorNumber: 1,
CgroupPermissions: "rwm",
},
{
Path: "/dev/tty0",
Type: 'c',
MajorNumber: 4,
MinorNumber: 0,
CgroupPermissions: "rwm",
},
{
Path: "/dev/tty1",
Type: 'c',
MajorNumber: 4,
MinorNumber: 1,
CgroupPermissions: "rwm",
},
// /dev/pts/ - pts namespaces are "coming soon"
{
Path: "",
Type: 'c',
MajorNumber: 136,
MinorNumber: Wildcard,
CgroupPermissions: "rwm",
},
{
Path: "",
Type: 'c',
MajorNumber: 5,
MinorNumber: 2,
CgroupPermissions: "rwm",
},
// tuntap
{
Path: "",
Type: 'c',
MajorNumber: 10,
MinorNumber: 200,
CgroupPermissions: "rwm",
},
/*// fuse
{
Path: "",
Type: 'c',
MajorNumber: 10,
MinorNumber: 229,
CgroupPermissions: "rwm",
},
// rtc
{
Path: "",
Type: 'c',
MajorNumber: 254,
MinorNumber: 0,
CgroupPermissions: "rwm",
},
*/
}, DefaultSimpleDevices...)
DefaultAutoCreatedDevices = append([]*Device{
{
// /dev/fuse is created but not allowed.
// This is to allow java to work. Because java
// Insists on there being a /dev/fuse
// https://github.com/docker/docker/issues/514
// https://github.com/docker/docker/issues/2393
//
Path: "/dev/fuse",
Type: 'c',
MajorNumber: 10,
MinorNumber: 229,
CgroupPermissions: "rwm",
},
}, DefaultSimpleDevices...)
)

View File

@ -7,14 +7,12 @@ import (
"os"
"path/filepath"
"syscall"
)
const (
Wildcard = -1
"github.com/docker/libcontainer/configs"
)
var (
ErrNotADeviceNode = errors.New("not a device node")
ErrNotADevice = errors.New("not a device node")
)
// Testing dependencies
@ -23,45 +21,20 @@ var (
ioutilReadDir = ioutil.ReadDir
)
type Device struct {
Type rune `json:"type,omitempty"`
Path string `json:"path,omitempty"` // It is fine if this is an empty string in the case that you are using Wildcards
MajorNumber int64 `json:"major_number,omitempty"` // Use the wildcard constant for wildcards.
MinorNumber int64 `json:"minor_number,omitempty"` // Use the wildcard constant for wildcards.
CgroupPermissions string `json:"cgroup_permissions,omitempty"` // Typically just "rwm"
FileMode os.FileMode `json:"file_mode,omitempty"` // The permission bits of the file's mode
Uid uint32 `json:"uid,omitempty"`
Gid uint32 `json:"gid,omitempty"`
}
func GetDeviceNumberString(deviceNumber int64) string {
if deviceNumber == Wildcard {
return "*"
} else {
return fmt.Sprintf("%d", deviceNumber)
}
}
func (device *Device) GetCgroupAllowString() string {
return fmt.Sprintf("%c %s:%s %s", device.Type, GetDeviceNumberString(device.MajorNumber), GetDeviceNumberString(device.MinorNumber), device.CgroupPermissions)
}
// Given the path to a device and it's cgroup_permissions(which cannot be easilly queried) look up the information about a linux device and return that information as a Device struct.
func GetDevice(path, cgroupPermissions string) (*Device, error) {
func DeviceFromPath(path, permissions string) (*configs.Device, error) {
fileInfo, err := osLstat(path)
if err != nil {
return nil, err
}
var (
devType rune
mode = fileInfo.Mode()
fileModePermissionBits = os.FileMode.Perm(mode)
)
switch {
case mode&os.ModeDevice == 0:
return nil, ErrNotADeviceNode
return nil, ErrNotADevice
case mode&os.ModeCharDevice != 0:
fileModePermissionBits |= syscall.S_IFCHR
devType = 'c'
@ -69,36 +42,33 @@ func GetDevice(path, cgroupPermissions string) (*Device, error) {
fileModePermissionBits |= syscall.S_IFBLK
devType = 'b'
}
stat_t, ok := fileInfo.Sys().(*syscall.Stat_t)
if !ok {
return nil, fmt.Errorf("cannot determine the device number for device %s", path)
}
devNumber := int(stat_t.Rdev)
return &Device{
Type: devType,
Path: path,
MajorNumber: Major(devNumber),
MinorNumber: Minor(devNumber),
CgroupPermissions: cgroupPermissions,
FileMode: fileModePermissionBits,
Uid: stat_t.Uid,
Gid: stat_t.Gid,
return &configs.Device{
Type: devType,
Path: path,
Major: Major(devNumber),
Minor: Minor(devNumber),
Permissions: permissions,
FileMode: fileModePermissionBits,
Uid: stat_t.Uid,
Gid: stat_t.Gid,
}, nil
}
func GetHostDeviceNodes() ([]*Device, error) {
return getDeviceNodes("/dev")
func HostDevices() ([]*configs.Device, error) {
return getDevices("/dev")
}
func getDeviceNodes(path string) ([]*Device, error) {
func getDevices(path string) ([]*configs.Device, error) {
files, err := ioutilReadDir(path)
if err != nil {
return nil, err
}
out := []*Device{}
out := []*configs.Device{}
for _, f := range files {
switch {
case f.IsDir():
@ -106,7 +76,7 @@ func getDeviceNodes(path string) ([]*Device, error) {
case "pts", "shm", "fd", "mqueue":
continue
default:
sub, err := getDeviceNodes(filepath.Join(path, f.Name()))
sub, err := getDevices(filepath.Join(path, f.Name()))
if err != nil {
return nil, err
}
@ -117,16 +87,14 @@ func getDeviceNodes(path string) ([]*Device, error) {
case f.Name() == "console":
continue
}
device, err := GetDevice(filepath.Join(path, f.Name()), "rwm")
device, err := DeviceFromPath(filepath.Join(path, f.Name()), "rwm")
if err != nil {
if err == ErrNotADeviceNode {
if err == ErrNotADevice {
continue
}
return nil, err
}
out = append(out, device)
}
return out, nil
}

View File

@ -6,7 +6,7 @@ import (
"testing"
)
func TestGetDeviceLstatFailure(t *testing.T) {
func TestDeviceFromPathLstatFailure(t *testing.T) {
testError := errors.New("test error")
// Override os.Lstat to inject error.
@ -14,13 +14,13 @@ func TestGetDeviceLstatFailure(t *testing.T) {
return nil, testError
}
_, err := GetDevice("", "")
_, err := DeviceFromPath("", "")
if err != testError {
t.Fatalf("Unexpected error %v, expected %v", err, testError)
}
}
func TestGetHostDeviceNodesIoutilReadDirFailure(t *testing.T) {
func TestHostDevicesIoutilReadDirFailure(t *testing.T) {
testError := errors.New("test error")
// Override ioutil.ReadDir to inject error.
@ -28,13 +28,13 @@ func TestGetHostDeviceNodesIoutilReadDirFailure(t *testing.T) {
return nil, testError
}
_, err := GetHostDeviceNodes()
_, err := HostDevices()
if err != testError {
t.Fatalf("Unexpected error %v, expected %v", err, testError)
}
}
func TestGetHostDeviceNodesIoutilReadDirDeepFailure(t *testing.T) {
func TestHostDevicesIoutilReadDirDeepFailure(t *testing.T) {
testError := errors.New("test error")
called := false
@ -54,7 +54,7 @@ func TestGetHostDeviceNodesIoutilReadDirDeepFailure(t *testing.T) {
return []os.FileInfo{fi}, nil
}
_, err := GetHostDeviceNodes()
_, err := HostDevices()
if err != testError {
t.Fatalf("Unexpected error %v, expected %v", err, testError)
}

View File

@ -20,7 +20,3 @@ func Major(devNumber int) int64 {
func Minor(devNumber int) int64 {
return int64((devNumber & 0xff) | ((devNumber >> 12) & 0xfff00))
}
func Mkdev(majorNumber int64, minorNumber int64) int {
return int((majorNumber << 8) | (minorNumber & 0xff) | ((minorNumber & 0xfff00) << 12))
}

View File

@ -1,5 +1,7 @@
package libcontainer
import "io"
// API error code type.
type ErrorCode int
@ -8,29 +10,49 @@ const (
// Factory errors
IdInUse ErrorCode = iota
InvalidIdFormat
// TODO: add Load errors
// Container errors
ContainerDestroyed
ContainerNotExists
ContainerPaused
ContainerNotStopped
ContainerNotRunning
// Common errors
ConfigInvalid
SystemError
)
func (c ErrorCode) String() string {
switch c {
case IdInUse:
return "Id already in use"
case InvalidIdFormat:
return "Invalid format"
case ContainerPaused:
return "Container paused"
case ConfigInvalid:
return "Invalid configuration"
case SystemError:
return "System error"
case ContainerNotExists:
return "Container does not exist"
case ContainerNotStopped:
return "Container is not stopped"
case ContainerNotRunning:
return "Container is not running"
default:
return "Unknown error"
}
}
// API Error type.
type Error interface {
error
// Returns the stack trace, if any, which identifies the
// point at which the error occurred.
Stack() []byte
// Returns a verbose string including the error message
// and a representation of the stack trace suitable for
// printing.
Detail() string
Detail(w io.Writer) error
// Returns the error code for this error.
Code() ErrorCode

20
error_test.go Normal file
View File

@ -0,0 +1,20 @@
package libcontainer
import "testing"
func TestErrorCode(t *testing.T) {
codes := map[ErrorCode]string{
IdInUse: "Id already in use",
InvalidIdFormat: "Invalid format",
ContainerPaused: "Container paused",
ConfigInvalid: "Invalid configuration",
SystemError: "System error",
ContainerNotExists: "Container does not exist",
}
for code, expected := range codes {
if actual := code.String(); actual != expected {
t.Fatalf("expected string %q but received %q", expected, actual)
}
}
}

View File

@ -1,7 +1,10 @@
package libcontainer
type Factory interface {
import (
"github.com/docker/libcontainer/configs"
)
type Factory interface {
// Creates a new container with the given id and starts the initial process inside it.
// id must be a string containing only letters, digits and underscores and must contain
// between 1 and 1024 characters, inclusive.
@ -11,22 +14,31 @@ type Factory interface {
//
// Returns the new container with a running process.
//
// Errors:
// errors:
// IdInUse - id is already in use by a container
// InvalidIdFormat - id has incorrect format
// ConfigInvalid - config is invalid
// SystemError - System error
// Systemerror - System error
//
// On error, any partially created container parts are cleaned up (the operation is atomic).
Create(id string, config *Config) (Container, Error)
Create(id string, config *configs.Config) (Container, error)
// Load takes an ID for an existing container and reconstructs the container
// from the state.
// Load takes an ID for an existing container and returns the container information
// from the state. This presents a read only view of the container.
//
// Errors:
// errors:
// Path does not exist
// Container is stopped
// System error
// TODO: fix description
Load(id string) (Container, Error)
Load(id string) (Container, error)
// StartInitialization is an internal API to libcontainer used during the rexec of the
// container. pipefd is the fd to the child end of the pipe used to syncronize the
// parent and child process providing state and configuration to the child process and
// returning any errors during the init of the container
//
// Errors:
// pipe connection error
// system error
StartInitialization(pipefd uintptr) error
}

68
generic_error.go Normal file
View File

@ -0,0 +1,68 @@
package libcontainer
import (
"fmt"
"io"
"text/template"
"time"
"github.com/docker/libcontainer/stacktrace"
)
var errorTemplate = template.Must(template.New("error").Parse(`Timestamp: {{.Timestamp}}
Code: {{.ECode}}
{{if .Message }}
Message: {{.Message}}
{{end}}
Frames:{{range $i, $frame := .Stack.Frames}}
---
{{$i}}: {{$frame.Function}}
Package: {{$frame.Package}}
File: {{$frame.File}}@{{$frame.Line}}{{end}}
`))
func newGenericError(err error, c ErrorCode) Error {
if le, ok := err.(Error); ok {
return le
}
return &genericError{
Timestamp: time.Now(),
Err: err,
Message: err.Error(),
ECode: c,
Stack: stacktrace.Capture(1),
}
}
func newSystemError(err error) Error {
if le, ok := err.(Error); ok {
return le
}
return &genericError{
Timestamp: time.Now(),
Err: err,
ECode: SystemError,
Message: err.Error(),
Stack: stacktrace.Capture(1),
}
}
type genericError struct {
Timestamp time.Time
ECode ErrorCode
Err error `json:"-"`
Message string
Stack stacktrace.Stacktrace
}
func (e *genericError) Error() string {
return fmt.Sprintf("[%d] %s: %s", e.ECode, e.ECode, e.Message)
}
func (e *genericError) Code() ErrorCode {
return e.ECode
}
func (e *genericError) Detail(w io.Writer) error {
return errorTemplate.Execute(w, e)
}

14
generic_error_test.go Normal file
View File

@ -0,0 +1,14 @@
package libcontainer
import (
"fmt"
"io/ioutil"
"testing"
)
func TestErrorDetail(t *testing.T) {
err := newGenericError(fmt.Errorf("test error"), SystemError)
if derr := err.Detail(ioutil.Discard); derr != nil {
t.Fatal(derr)
}
}

View File

@ -1,34 +1,50 @@
package integration
import (
"bytes"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/docker/libcontainer"
"github.com/docker/libcontainer/configs"
)
func TestExecPS(t *testing.T) {
testExecPS(t, false)
}
func TestUsernsExecPS(t *testing.T) {
if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
t.Skip("userns is unsupported")
}
testExecPS(t, true)
}
func testExecPS(t *testing.T, userns bool) {
if testing.Short() {
return
}
rootfs, err := newRootFs()
rootfs, err := newRootfs()
if err != nil {
t.Fatal(err)
}
defer remove(rootfs)
config := newTemplateConfig(rootfs)
buffers, exitCode, err := runContainer(config, "", "ps")
if err != nil {
t.Fatal(err)
if userns {
config.UidMappings = []configs.IDMap{{0, 0, 1000}}
config.GidMappings = []configs.IDMap{{0, 0, 1000}}
config.Namespaces = append(config.Namespaces, configs.Namespace{Type: configs.NEWUSER})
}
buffers, exitCode, err := runContainer(config, "", "ps")
if err != nil {
t.Fatalf("%s: %s", buffers, err)
}
if exitCode != 0 {
t.Fatalf("exit code not 0. code %d stderr %q", exitCode, buffers.Stderr)
}
lines := strings.Split(buffers.Stdout.String(), "\n")
if len(lines) < 2 {
t.Fatalf("more than one process running for output %q", buffers.Stdout.String())
@ -45,7 +61,7 @@ func TestIPCPrivate(t *testing.T) {
return
}
rootfs, err := newRootFs()
rootfs, err := newRootfs()
if err != nil {
t.Fatal(err)
}
@ -76,7 +92,7 @@ func TestIPCHost(t *testing.T) {
return
}
rootfs, err := newRootFs()
rootfs, err := newRootfs()
if err != nil {
t.Fatal(err)
}
@ -88,7 +104,7 @@ func TestIPCHost(t *testing.T) {
}
config := newTemplateConfig(rootfs)
config.Namespaces.Remove(libcontainer.NEWIPC)
config.Namespaces.Remove(configs.NEWIPC)
buffers, exitCode, err := runContainer(config, "", "readlink", "/proc/self/ns/ipc")
if err != nil {
t.Fatal(err)
@ -108,7 +124,7 @@ func TestIPCJoinPath(t *testing.T) {
return
}
rootfs, err := newRootFs()
rootfs, err := newRootfs()
if err != nil {
t.Fatal(err)
}
@ -120,7 +136,7 @@ func TestIPCJoinPath(t *testing.T) {
}
config := newTemplateConfig(rootfs)
config.Namespaces.Add(libcontainer.NEWIPC, "/proc/1/ns/ipc")
config.Namespaces.Add(configs.NEWIPC, "/proc/1/ns/ipc")
buffers, exitCode, err := runContainer(config, "", "readlink", "/proc/self/ns/ipc")
if err != nil {
@ -141,14 +157,14 @@ func TestIPCBadPath(t *testing.T) {
return
}
rootfs, err := newRootFs()
rootfs, err := newRootfs()
if err != nil {
t.Fatal(err)
}
defer remove(rootfs)
config := newTemplateConfig(rootfs)
config.Namespaces.Add(libcontainer.NEWIPC, "/proc/1/ns/ipcc")
config.Namespaces.Add(configs.NEWIPC, "/proc/1/ns/ipcc")
_, _, err = runContainer(config, "", "true")
if err == nil {
@ -161,7 +177,7 @@ func TestRlimit(t *testing.T) {
return
}
rootfs, err := newRootFs()
rootfs, err := newRootfs()
if err != nil {
t.Fatal(err)
}
@ -177,33 +193,206 @@ func TestRlimit(t *testing.T) {
}
}
func TestPIDNSPrivate(t *testing.T) {
func newTestRoot() (string, error) {
dir, err := ioutil.TempDir("", "libcontainer")
if err != nil {
return "", err
}
if err := os.MkdirAll(dir, 0700); err != nil {
return "", err
}
return dir, nil
}
func waitProcess(pid int, t *testing.T) {
p, err := os.FindProcess(pid)
if err != nil {
t.Fatal(err)
}
status, err := p.Wait()
if err != nil {
t.Fatal(err)
}
if !status.Success() {
t.Fatal(status)
}
}
func TestEnter(t *testing.T) {
if testing.Short() {
return
}
root, err := newTestRoot()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
rootfs, err := newRootFs()
rootfs, err := newRootfs()
if err != nil {
t.Fatal(err)
}
defer remove(rootfs)
l, err := os.Readlink("/proc/1/ns/pid")
if err != nil {
t.Fatal(err)
}
config := newTemplateConfig(rootfs)
buffers, exitCode, err := runContainer(config, "", "readlink", "/proc/self/ns/pid")
factory, err := libcontainer.New(root, libcontainer.InitArgs(os.Args[0], "init", "--"), libcontainer.Cgroupfs)
if err != nil {
t.Fatal(err)
}
if exitCode != 0 {
t.Fatalf("exit code not 0. code %d stderr %q", exitCode, buffers.Stderr)
container, err := factory.Create("test", config)
if err != nil {
t.Fatal(err)
}
defer container.Destroy()
// Execute a first process in the container
stdinR, stdinW, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
if actual := strings.Trim(buffers.Stdout.String(), "\n"); actual == l {
t.Fatalf("pid link should be private to the container but equals host %q %q", actual, l)
var stdout, stdout2 bytes.Buffer
pconfig := libcontainer.Process{
Args: []string{"sh", "-c", "cat && readlink /proc/self/ns/pid"},
Env: standardEnvironment,
Stdin: stdinR,
Stdout: &stdout,
}
pid, err := container.Start(&pconfig)
stdinR.Close()
defer stdinW.Close()
if err != nil {
t.Fatal(err)
}
// Execute a first process in the container
stdinR2, stdinW2, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
pconfig.Args = []string{"sh", "-c", "cat && readlink /proc/self/ns/pid"}
pconfig.Stdin = stdinR2
pconfig.Stdout = &stdout2
pid2, err := container.Start(&pconfig)
stdinR2.Close()
defer stdinW2.Close()
if err != nil {
t.Fatal(err)
}
processes, err := container.Processes()
if err != nil {
t.Fatal(err)
}
n := 0
for i := range processes {
if processes[i] == pid || processes[i] == pid2 {
n++
}
}
if n != 2 {
t.Fatal("unexpected number of processes", processes, pid, pid2)
}
// Wait processes
stdinW2.Close()
waitProcess(pid2, t)
stdinW.Close()
waitProcess(pid, t)
// Check that both processes live in the same pidns
pidns := string(stdout.Bytes())
if err != nil {
t.Fatal(err)
}
pidns2 := string(stdout2.Bytes())
if err != nil {
t.Fatal(err)
}
if pidns != pidns2 {
t.Fatal("The second process isn't in the required pid namespace", pidns, pidns2)
}
}
func TestFreeze(t *testing.T) {
if testing.Short() {
return
}
root, err := newTestRoot()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
rootfs, err := newRootfs()
if err != nil {
t.Fatal(err)
}
defer remove(rootfs)
config := newTemplateConfig(rootfs)
factory, err := libcontainer.New(root, libcontainer.InitArgs(os.Args[0], "init", "--"), libcontainer.Cgroupfs)
if err != nil {
t.Fatal(err)
}
container, err := factory.Create("test", config)
if err != nil {
t.Fatal(err)
}
defer container.Destroy()
stdinR, stdinW, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
pconfig := libcontainer.Process{
Args: []string{"cat"},
Env: standardEnvironment,
Stdin: stdinR,
}
pid, err := container.Start(&pconfig)
stdinR.Close()
defer stdinW.Close()
if err != nil {
t.Fatal(err)
}
process, err := os.FindProcess(pid)
if err != nil {
t.Fatal(err)
}
if err := container.Pause(); err != nil {
t.Fatal(err)
}
state, err := container.Status()
if err != nil {
t.Fatal(err)
}
if err := container.Resume(); err != nil {
t.Fatal(err)
}
if state != libcontainer.Paused {
t.Fatal("Unexpected state: ", state)
}
stdinW.Close()
s, err := process.Wait()
if err != nil {
t.Fatal(err)
}
if !s.Success() {
t.Fatal(s.String())
}
}

View File

@ -2,59 +2,68 @@ package integration
import (
"os"
"os/exec"
"strings"
"sync"
"syscall"
"testing"
"github.com/docker/libcontainer"
"github.com/docker/libcontainer/namespaces"
)
func TestExecIn(t *testing.T) {
if testing.Short() {
return
}
rootfs, err := newRootFs()
rootfs, err := newRootfs()
if err != nil {
t.Fatal(err)
}
defer remove(rootfs)
config := newTemplateConfig(rootfs)
if err := writeConfig(config); err != nil {
t.Fatalf("failed to write config %s", err)
}
containerCmd, statePath, containerErr := startLongRunningContainer(config)
defer func() {
// kill the container
if containerCmd.Process != nil {
containerCmd.Process.Kill()
}
if err := <-containerErr; err != nil {
t.Fatal(err)
}
}()
// start the exec process
state, err := libcontainer.GetState(statePath)
container, err := newContainer(config)
if err != nil {
t.Fatalf("failed to get state %s", err)
t.Fatal(err)
}
defer container.Destroy()
buffers := newStdBuffers()
execErr := make(chan error, 1)
go func() {
_, err := namespaces.ExecIn(config, state, []string{"ps"},
os.Args[0], "exec", buffers.Stdin, buffers.Stdout, buffers.Stderr,
"", nil)
execErr <- err
}()
if err := <-execErr; err != nil {
t.Fatalf("exec finished with error %s", err)
process := &libcontainer.Process{
Args: []string{"sleep", "10"},
Env: standardEnvironment,
Stdin: buffers.Stdin,
Stdout: buffers.Stdout,
Stderr: buffers.Stderr,
}
pid1, err := container.Start(process)
if err != nil {
t.Fatal(err)
}
buffers = newStdBuffers()
psPid, err := container.Start(&libcontainer.Process{
Args: []string{"ps"},
Env: standardEnvironment,
Stdin: buffers.Stdin,
Stdout: buffers.Stdout,
Stderr: buffers.Stderr,
})
if err != nil {
t.Fatal(err)
}
ps, err := os.FindProcess(psPid)
if err != nil {
t.Fatal(err)
}
if _, err := ps.Wait(); err != nil {
t.Fatal(err)
}
p, err := os.FindProcess(pid1)
if err != nil {
t.Fatal(err)
}
if err := p.Signal(syscall.SIGKILL); err != nil {
t.Log(err)
}
if _, err := p.Wait(); err != nil {
t.Log(err)
}
out := buffers.Stdout.String()
if !strings.Contains(out, "sleep 10") || !strings.Contains(out, "ps") {
t.Fatalf("unexpected running process, output %q", out)
@ -65,83 +74,59 @@ func TestExecInRlimit(t *testing.T) {
if testing.Short() {
return
}
rootfs, err := newRootFs()
rootfs, err := newRootfs()
if err != nil {
t.Fatal(err)
}
defer remove(rootfs)
config := newTemplateConfig(rootfs)
if err := writeConfig(config); err != nil {
t.Fatalf("failed to write config %s", err)
}
containerCmd, statePath, containerErr := startLongRunningContainer(config)
defer func() {
// kill the container
if containerCmd.Process != nil {
containerCmd.Process.Kill()
}
if err := <-containerErr; err != nil {
t.Fatal(err)
}
}()
// start the exec process
state, err := libcontainer.GetState(statePath)
container, err := newContainer(config)
if err != nil {
t.Fatalf("failed to get state %s", err)
t.Fatal(err)
}
defer container.Destroy()
buffers := newStdBuffers()
execErr := make(chan error, 1)
go func() {
_, err := namespaces.ExecIn(config, state, []string{"/bin/sh", "-c", "ulimit -n"},
os.Args[0], "exec", buffers.Stdin, buffers.Stdout, buffers.Stderr,
"", nil)
execErr <- err
}()
if err := <-execErr; err != nil {
t.Fatalf("exec finished with error %s", err)
process := &libcontainer.Process{
Args: []string{"sleep", "10"},
Env: standardEnvironment,
Stdin: buffers.Stdin,
Stdout: buffers.Stdout,
Stderr: buffers.Stderr,
}
pid1, err := container.Start(process)
if err != nil {
t.Fatal(err)
}
buffers = newStdBuffers()
psPid, err := container.Start(&libcontainer.Process{
Args: []string{"/bin/sh", "-c", "ulimit -n"},
Env: standardEnvironment,
Stdin: buffers.Stdin,
Stdout: buffers.Stdout,
Stderr: buffers.Stderr,
})
if err != nil {
t.Fatal(err)
}
ps, err := os.FindProcess(psPid)
if err != nil {
t.Fatal(err)
}
if _, err := ps.Wait(); err != nil {
t.Fatal(err)
}
p, err := os.FindProcess(pid1)
if err != nil {
t.Fatal(err)
}
if err := p.Signal(syscall.SIGKILL); err != nil {
t.Log(err)
}
if _, err := p.Wait(); err != nil {
t.Log(err)
}
out := buffers.Stdout.String()
if limit := strings.TrimSpace(out); limit != "1024" {
t.Fatalf("expected rlimit to be 1024, got %s", limit)
}
}
// start a long-running container so we have time to inspect execin processes
func startLongRunningContainer(config *libcontainer.Config) (*exec.Cmd, string, chan error) {
containerErr := make(chan error, 1)
containerCmd := &exec.Cmd{}
setupContainerCmd := &exec.Cmd{}
var statePath string
createCmd := func(container *libcontainer.Config, console, dataPath, init string,
pipe *os.File, args []string) *exec.Cmd {
containerCmd = namespaces.DefaultCreateCommand(container, console, dataPath, init, pipe, args)
statePath = dataPath
return containerCmd
}
setupCmd := func(container *libcontainer.Config, console, dataPath, init string) *exec.Cmd {
setupContainerCmd = namespaces.DefaultSetupCommand(container, console, dataPath, init)
statePath = dataPath
return setupContainerCmd
}
var containerStart sync.WaitGroup
containerStart.Add(1)
go func() {
buffers := newStdBuffers()
_, err := namespaces.Exec(config,
buffers.Stdin, buffers.Stdout, buffers.Stderr,
"", config.RootFs, []string{"sleep", "10"},
createCmd, setupCmd, containerStart.Done)
containerErr <- err
}()
containerStart.Wait()
return containerCmd, statePath, containerErr
}

View File

@ -1,76 +1,27 @@
package integration
import (
"encoding/json"
"log"
"os"
"runtime"
"github.com/docker/libcontainer"
"github.com/docker/libcontainer/namespaces"
_ "github.com/docker/libcontainer/namespaces/nsenter"
_ "github.com/docker/libcontainer/nsenter"
)
// init runs the libcontainer initialization code because of the busybox style needs
// to work around the go runtime and the issues with forking
func init() {
if len(os.Args) < 2 {
if len(os.Args) < 2 || os.Args[1] != "init" {
return
}
// handle init
if len(os.Args) >= 2 && os.Args[1] == "init" {
runtime.LockOSThread()
container, err := loadConfig()
if err != nil {
log.Fatal(err)
}
rootfs, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
if err := namespaces.Init(container, rootfs, "", os.NewFile(3, "pipe"), os.Args[3:]); err != nil {
log.Fatalf("unable to initialize for container: %s", err)
}
os.Exit(1)
runtime.GOMAXPROCS(1)
runtime.LockOSThread()
factory, err := libcontainer.New("")
if err != nil {
log.Fatalf("unable to initialize for container: %s", err)
}
// handle execin
if len(os.Args) >= 2 && os.Args[0] == "nsenter-exec" {
runtime.LockOSThread()
// User args are passed after '--' in the command line.
userArgs := findUserArgs()
config, err := loadConfigFromFd()
if err != nil {
log.Fatalf("docker-exec: unable to receive config from sync pipe: %s", err)
}
if err := namespaces.FinalizeSetns(config, userArgs); err != nil {
log.Fatalf("docker-exec: failed to exec: %s", err)
}
os.Exit(1)
if err := factory.StartInitialization(3); err != nil {
log.Fatal(err)
}
}
func findUserArgs() []string {
for i, a := range os.Args {
if a == "--" {
return os.Args[i+1:]
}
}
return []string{}
}
// loadConfigFromFd loads a container's config from the sync pipe that is provided by
// fd 3 when running a process
func loadConfigFromFd() (*libcontainer.Config, error) {
var config *libcontainer.Config
if err := json.NewDecoder(os.NewFile(3, "child")).Decode(&config); err != nil {
return nil, err
}
return config, nil
}

View File

@ -3,19 +3,25 @@ package integration
import (
"syscall"
"github.com/docker/libcontainer"
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/devices"
"github.com/docker/libcontainer/configs"
)
var standardEnvironment = []string{
"HOME=/root",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HOSTNAME=integration",
"TERM=xterm",
}
const defaultMountFlags = syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
// newTemplateConfig returns a base template for running a container
//
// it uses a network strategy of just setting a loopback interface
// and the default setup for devices
func newTemplateConfig(rootfs string) *libcontainer.Config {
return &libcontainer.Config{
RootFs: rootfs,
Tty: false,
func newTemplateConfig(rootfs string) *configs.Config {
return &configs.Config{
Rootfs: rootfs,
Capabilities: []string{
"CHOWN",
"DAC_OVERRIDE",
@ -32,37 +38,56 @@ func newTemplateConfig(rootfs string) *libcontainer.Config {
"KILL",
"AUDIT_WRITE",
},
Namespaces: libcontainer.Namespaces([]libcontainer.Namespace{
{Type: libcontainer.NEWNS},
{Type: libcontainer.NEWUTS},
{Type: libcontainer.NEWIPC},
{Type: libcontainer.NEWPID},
{Type: libcontainer.NEWNET},
Namespaces: configs.Namespaces([]configs.Namespace{
{Type: configs.NEWNS},
{Type: configs.NEWUTS},
{Type: configs.NEWIPC},
{Type: configs.NEWPID},
{Type: configs.NEWNET},
}),
Cgroups: &cgroups.Cgroup{
Cgroups: &configs.Cgroup{
Name: "test",
Parent: "integration",
AllowAllDevices: false,
AllowedDevices: devices.DefaultAllowedDevices,
AllowedDevices: configs.DefaultAllowedDevices,
},
MountConfig: &libcontainer.MountConfig{
DeviceNodes: devices.DefaultAutoCreatedDevices,
MaskPaths: []string{
"/proc/kcore",
},
ReadonlyPaths: []string{
"/proc/sys", "/proc/sysrq-trigger", "/proc/irq", "/proc/bus",
},
Devices: configs.DefaultAutoCreatedDevices,
Hostname: "integration",
Env: []string{
"HOME=/root",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HOSTNAME=integration",
"TERM=xterm",
Mounts: []*configs.Mount{
{
Device: "tmpfs",
Source: "shm",
Destination: "/dev/shm",
Data: "mode=1777,size=65536k",
Flags: defaultMountFlags,
},
{
Source: "mqueue",
Destination: "/dev/mqueue",
Device: "mqueue",
Flags: defaultMountFlags,
},
{
Source: "sysfs",
Destination: "/sys",
Device: "sysfs",
Flags: defaultMountFlags | syscall.MS_RDONLY,
},
},
Networks: []*libcontainer.Network{
Networks: []*configs.Network{
{
Type: "loopback",
Address: "127.0.0.1/0",
Gateway: "localhost",
},
},
Rlimits: []libcontainer.Rlimit{
Rlimits: []configs.Rlimit{
{
Type: syscall.RLIMIT_NOFILE,
Hard: uint64(1024),

View File

@ -2,15 +2,15 @@ package integration
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"github.com/docker/libcontainer"
"github.com/docker/libcontainer/namespaces"
"github.com/docker/libcontainer/configs"
)
func newStdBuffers() *stdBuffers {
@ -27,31 +27,19 @@ type stdBuffers struct {
Stderr *bytes.Buffer
}
func writeConfig(config *libcontainer.Config) error {
f, err := os.OpenFile(filepath.Join(config.RootFs, "container.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0700)
if err != nil {
return err
func (b *stdBuffers) String() string {
s := []string{}
if b.Stderr != nil {
s = append(s, b.Stderr.String())
}
defer f.Close()
return json.NewEncoder(f).Encode(config)
if b.Stdout != nil {
s = append(s, b.Stdout.String())
}
return strings.Join(s, "|")
}
func loadConfig() (*libcontainer.Config, error) {
f, err := os.Open(filepath.Join(os.Getenv("data_path"), "container.json"))
if err != nil {
return nil, err
}
defer f.Close()
var container *libcontainer.Config
if err := json.NewDecoder(f).Decode(&container); err != nil {
return nil, err
}
return container, nil
}
// newRootFs creates a new tmp directory and copies the busybox root filesystem
func newRootFs() (string, error) {
// newRootfs creates a new tmp directory and copies the busybox root filesystem
func newRootfs() (string, error) {
dir, err := ioutil.TempDir("", "")
if err != nil {
return "", err
@ -79,17 +67,55 @@ func copyBusybox(dest string) error {
return nil
}
func newContainer(config *configs.Config) (libcontainer.Container, error) {
factory, err := libcontainer.New(".",
libcontainer.InitArgs(os.Args[0], "init", "--"),
libcontainer.Cgroupfs,
)
if err != nil {
return nil, err
}
return factory.Create("testCT", config)
}
// runContainer runs the container with the specific config and arguments
//
// buffers are returned containing the STDOUT and STDERR output for the run
// along with the exit code and any go error
func runContainer(config *libcontainer.Config, console string, args ...string) (buffers *stdBuffers, exitCode int, err error) {
if err := writeConfig(config); err != nil {
func runContainer(config *configs.Config, console string, args ...string) (buffers *stdBuffers, exitCode int, err error) {
container, err := newContainer(config)
if err != nil {
return nil, -1, err
}
defer container.Destroy()
buffers = newStdBuffers()
exitCode, err = namespaces.Exec(config, buffers.Stdin, buffers.Stdout, buffers.Stderr,
console, config.RootFs, args, namespaces.DefaultCreateCommand, namespaces.DefaultSetupCommand, nil)
process := &libcontainer.Process{
Args: args,
Env: standardEnvironment,
Stdin: buffers.Stdin,
Stdout: buffers.Stdout,
Stderr: buffers.Stderr,
}
pid, err := container.Start(process)
if err != nil {
return nil, -1, err
}
p, err := os.FindProcess(pid)
if err != nil {
return nil, -1, err
}
ps, err := p.Wait()
if err != nil {
return nil, -1, err
}
status := ps.Sys().(syscall.WaitStatus)
if status.Exited() {
exitCode = status.ExitStatus()
} else if status.Signaled() {
exitCode = -int(status.Signal())
} else {
return nil, -1, err
}
return
}

90
linux_capabilities.go Normal file
View File

@ -0,0 +1,90 @@
// +build linux
package libcontainer
import (
"fmt"
"os"
"github.com/syndtr/gocapability/capability"
)
const allCapabilityTypes = capability.CAPS | capability.BOUNDS
var capabilityList = map[string]capability.Cap{
"SETPCAP": capability.CAP_SETPCAP,
"SYS_MODULE": capability.CAP_SYS_MODULE,
"SYS_RAWIO": capability.CAP_SYS_RAWIO,
"SYS_PACCT": capability.CAP_SYS_PACCT,
"SYS_ADMIN": capability.CAP_SYS_ADMIN,
"SYS_NICE": capability.CAP_SYS_NICE,
"SYS_RESOURCE": capability.CAP_SYS_RESOURCE,
"SYS_TIME": capability.CAP_SYS_TIME,
"SYS_TTY_CONFIG": capability.CAP_SYS_TTY_CONFIG,
"MKNOD": capability.CAP_MKNOD,
"AUDIT_WRITE": capability.CAP_AUDIT_WRITE,
"AUDIT_CONTROL": capability.CAP_AUDIT_CONTROL,
"MAC_OVERRIDE": capability.CAP_MAC_OVERRIDE,
"MAC_ADMIN": capability.CAP_MAC_ADMIN,
"NET_ADMIN": capability.CAP_NET_ADMIN,
"SYSLOG": capability.CAP_SYSLOG,
"CHOWN": capability.CAP_CHOWN,
"NET_RAW": capability.CAP_NET_RAW,
"DAC_OVERRIDE": capability.CAP_DAC_OVERRIDE,
"FOWNER": capability.CAP_FOWNER,
"DAC_READ_SEARCH": capability.CAP_DAC_READ_SEARCH,
"FSETID": capability.CAP_FSETID,
"KILL": capability.CAP_KILL,
"SETGID": capability.CAP_SETGID,
"SETUID": capability.CAP_SETUID,
"LINUX_IMMUTABLE": capability.CAP_LINUX_IMMUTABLE,
"NET_BIND_SERVICE": capability.CAP_NET_BIND_SERVICE,
"NET_BROADCAST": capability.CAP_NET_BROADCAST,
"IPC_LOCK": capability.CAP_IPC_LOCK,
"IPC_OWNER": capability.CAP_IPC_OWNER,
"SYS_CHROOT": capability.CAP_SYS_CHROOT,
"SYS_PTRACE": capability.CAP_SYS_PTRACE,
"SYS_BOOT": capability.CAP_SYS_BOOT,
"LEASE": capability.CAP_LEASE,
"SETFCAP": capability.CAP_SETFCAP,
"WAKE_ALARM": capability.CAP_WAKE_ALARM,
"BLOCK_SUSPEND": capability.CAP_BLOCK_SUSPEND,
}
func newCapWhitelist(caps []string) (*whitelist, error) {
l := []capability.Cap{}
for _, c := range caps {
v, ok := capabilityList[c]
if !ok {
return nil, fmt.Errorf("unknown capability %q", c)
}
l = append(l, v)
}
pid, err := capability.NewPid(os.Getpid())
if err != nil {
return nil, err
}
return &whitelist{
keep: l,
pid: pid,
}, nil
}
type whitelist struct {
pid capability.Capabilities
keep []capability.Cap
}
// dropBoundingSet drops the capability bounding set to those specified in the whitelist.
func (w *whitelist) dropBoundingSet() error {
w.pid.Clear(capability.BOUNDS)
w.pid.Set(capability.BOUNDS, w.keep...)
return w.pid.Apply(capability.BOUNDS)
}
// drop drops all capabilities for the current process except those specified in the whitelist.
func (w *whitelist) drop() error {
w.pid.Clear(allCapabilityTypes)
w.pid.Set(allCapabilityTypes, w.keep...)
return w.pid.Apply(allCapabilityTypes)
}

147
linux_console.go Normal file
View File

@ -0,0 +1,147 @@
// +build linux
package libcontainer
import (
"fmt"
"os"
"path/filepath"
"syscall"
"unsafe"
"github.com/docker/libcontainer/label"
)
// NewConsole returns an initalized console that can be used within a container by copying bytes
// from the master side to the slave that is attached as the tty for the container's init process.
func NewConsole(uid, gid int) (Console, error) {
master, err := os.OpenFile("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0)
if err != nil {
return nil, err
}
console, err := ptsname(master)
if err != nil {
return nil, err
}
if err := unlockpt(master); err != nil {
return nil, err
}
if err := os.Chmod(console, 0600); err != nil {
return nil, err
}
if err := os.Chown(console, uid, gid); err != nil {
return nil, err
}
return &linuxConsole{
slavePath: console,
master: master,
}, nil
}
// newConsoleFromPath is an internal fucntion returning an initialzied console for use inside
// a container's MNT namespace.
func newConsoleFromPath(slavePath string) *linuxConsole {
return &linuxConsole{
slavePath: slavePath,
}
}
// linuxConsole is a linux psuedo TTY for use within a container.
type linuxConsole struct {
master *os.File
slavePath string
}
func (c *linuxConsole) Fd() uintptr {
return c.master.Fd()
}
func (c *linuxConsole) Path() string {
return c.slavePath
}
func (c *linuxConsole) Read(b []byte) (int, error) {
return c.master.Read(b)
}
func (c *linuxConsole) Write(b []byte) (int, error) {
return c.master.Write(b)
}
func (c *linuxConsole) Close() error {
if m := c.master; m != nil {
return m.Close()
}
return nil
}
// mount initializes the console inside the rootfs mounting with the specified mount label
// and applying the correct ownership of the console.
func (c *linuxConsole) mount(rootfs, mountLabel string, uid, gid int) error {
oldMask := syscall.Umask(0000)
defer syscall.Umask(oldMask)
if err := label.SetFileLabel(c.slavePath, mountLabel); err != nil {
return err
}
dest := filepath.Join(rootfs, "/dev/console")
f, err := os.Create(dest)
if err != nil && !os.IsExist(err) {
return err
}
if f != nil {
f.Close()
}
return syscall.Mount(c.slavePath, dest, "bind", syscall.MS_BIND, "")
}
// dupStdio opens the slavePath for the console and dup2s the fds to the current
// processes stdio, fd 0,1,2.
func (c *linuxConsole) dupStdio() error {
slave, err := c.open(syscall.O_RDWR)
if err != nil {
return err
}
fd := int(slave.Fd())
for _, i := range []int{0, 1, 2} {
if err := syscall.Dup2(fd, i); err != nil {
return err
}
}
return nil
}
// open is a clone of os.OpenFile without the O_CLOEXEC used to open the pty slave.
func (c *linuxConsole) open(flag int) (*os.File, error) {
r, e := syscall.Open(c.slavePath, flag, 0)
if e != nil {
return nil, &os.PathError{
Op: "open",
Path: c.slavePath,
Err: e,
}
}
return os.NewFile(uintptr(r), c.slavePath), nil
}
func ioctl(fd uintptr, flag, data uintptr) error {
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, flag, data); err != 0 {
return err
}
return nil
}
// unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f.
// unlockpt should be called before opening the slave side of a pty.
func unlockpt(f *os.File) error {
var u int32
return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))
}
// ptsname retrieves the name of the first available pts for the given master.
func ptsname(f *os.File) (string, error) {
var n int32
if err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))); err != nil {
return "", err
}
return fmt.Sprintf("/dev/pts/%d", n), nil
}

298
linux_container.go Normal file
View File

@ -0,0 +1,298 @@
// +build linux
package libcontainer
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"sync"
"syscall"
log "github.com/Sirupsen/logrus"
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/configs"
)
type linuxContainer struct {
id string
root string
config *configs.Config
cgroupManager cgroups.Manager
initArgs []string
initProcess parentProcess
m sync.Mutex
}
// ID returns the container's unique ID
func (c *linuxContainer) ID() string {
return c.id
}
// Config returns the container's configuration
func (c *linuxContainer) Config() configs.Config {
return *c.config
}
func (c *linuxContainer) Status() (Status, error) {
c.m.Lock()
defer c.m.Unlock()
return c.currentStatus()
}
func (c *linuxContainer) State() (*State, error) {
c.m.Lock()
defer c.m.Unlock()
return c.currentState()
}
func (c *linuxContainer) Processes() ([]int, error) {
pids, err := c.cgroupManager.GetPids()
if err != nil {
return nil, newSystemError(err)
}
return pids, nil
}
func (c *linuxContainer) Stats() (*Stats, error) {
var (
err error
stats = &Stats{}
)
if stats.CgroupStats, err = c.cgroupManager.GetStats(); err != nil {
return stats, newSystemError(err)
}
for _, iface := range c.config.Networks {
switch iface.Type {
case "veth":
istats, err := getNetworkInterfaceStats(iface.HostInterfaceName)
if err != nil {
return stats, newSystemError(err)
}
stats.Interfaces = append(stats.Interfaces, istats)
}
}
return stats, nil
}
func (c *linuxContainer) Start(process *Process) (int, error) {
c.m.Lock()
defer c.m.Unlock()
status, err := c.currentStatus()
if err != nil {
return -1, err
}
doInit := status == Destroyed
parent, err := c.newParentProcess(process, doInit)
if err != nil {
return -1, newSystemError(err)
}
if err := parent.start(); err != nil {
// terminate the process to ensure that it properly is reaped.
if err := parent.terminate(); err != nil {
log.Warn(err)
}
return -1, newSystemError(err)
}
if doInit {
c.updateState(parent)
}
return parent.pid(), nil
}
func (c *linuxContainer) newParentProcess(p *Process, doInit bool) (parentProcess, error) {
parentPipe, childPipe, err := newPipe()
if err != nil {
return nil, newSystemError(err)
}
cmd, err := c.commandTemplate(p, childPipe)
if err != nil {
return nil, newSystemError(err)
}
if !doInit {
return c.newSetnsProcess(p, cmd, parentPipe, childPipe), nil
}
return c.newInitProcess(p, cmd, parentPipe, childPipe)
}
func (c *linuxContainer) commandTemplate(p *Process, childPipe *os.File) (*exec.Cmd, error) {
cmd := exec.Command(c.initArgs[0], c.initArgs[1:]...)
cmd.Stdin = p.Stdin
cmd.Stdout = p.Stdout
cmd.Stderr = p.Stderr
cmd.Dir = c.config.Rootfs
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
cmd.ExtraFiles = []*os.File{childPipe}
cmd.SysProcAttr.Pdeathsig = syscall.SIGKILL
if c.config.ParentDeathSignal > 0 {
cmd.SysProcAttr.Pdeathsig = syscall.Signal(c.config.ParentDeathSignal)
}
return cmd, nil
}
func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, parentPipe, childPipe *os.File) (*initProcess, error) {
t := "_LIBCONTAINER_INITTYPE=standard"
cloneFlags := c.config.Namespaces.CloneFlags()
if cloneFlags&syscall.CLONE_NEWUSER != 0 {
if err := c.addUidGidMappings(cmd.SysProcAttr); err != nil {
// user mappings are not supported
return nil, err
}
// Default to root user when user namespaces are enabled.
if cmd.SysProcAttr.Credential == nil {
cmd.SysProcAttr.Credential = &syscall.Credential{}
}
}
cmd.Env = append(cmd.Env, t)
cmd.SysProcAttr.Cloneflags = cloneFlags
return &initProcess{
cmd: cmd,
childPipe: childPipe,
parentPipe: parentPipe,
manager: c.cgroupManager,
config: c.newInitConfig(p),
}, nil
}
func (c *linuxContainer) newSetnsProcess(p *Process, cmd *exec.Cmd, parentPipe, childPipe *os.File) *setnsProcess {
cmd.Env = append(cmd.Env,
fmt.Sprintf("_LIBCONTAINER_INITPID=%d", c.initProcess.pid()),
"_LIBCONTAINER_INITTYPE=setns",
)
// TODO: set on container for process management
return &setnsProcess{
cmd: cmd,
cgroupPaths: c.cgroupManager.GetPaths(),
childPipe: childPipe,
parentPipe: parentPipe,
config: c.newInitConfig(p),
}
}
func (c *linuxContainer) newInitConfig(process *Process) *initConfig {
return &initConfig{
Config: c.config,
Args: process.Args,
Env: process.Env,
User: process.User,
Cwd: process.Cwd,
}
}
func newPipe() (parent *os.File, child *os.File, err error) {
fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC, 0)
if err != nil {
return nil, nil, err
}
return os.NewFile(uintptr(fds[1]), "parent"), os.NewFile(uintptr(fds[0]), "child"), nil
}
func (c *linuxContainer) Destroy() error {
c.m.Lock()
defer c.m.Unlock()
status, err := c.currentStatus()
if err != nil {
return err
}
if status != Destroyed {
return newGenericError(nil, ContainerNotStopped)
}
if !c.config.Namespaces.Contains(configs.NEWPID) {
if err := killCgroupProcesses(c.cgroupManager); err != nil {
log.Warn(err)
}
}
err = c.cgroupManager.Destroy()
if rerr := os.RemoveAll(c.root); err == nil {
err = rerr
}
c.initProcess = nil
return err
}
func (c *linuxContainer) Pause() error {
c.m.Lock()
defer c.m.Unlock()
return c.cgroupManager.Freeze(configs.Frozen)
}
func (c *linuxContainer) Resume() error {
c.m.Lock()
defer c.m.Unlock()
return c.cgroupManager.Freeze(configs.Thawed)
}
func (c *linuxContainer) Signal(signal os.Signal) error {
c.m.Lock()
defer c.m.Unlock()
if c.initProcess == nil {
return newGenericError(nil, ContainerNotRunning)
}
return c.initProcess.signal(signal)
}
func (c *linuxContainer) NotifyOOM() (<-chan struct{}, error) {
return notifyOnOOM(c.cgroupManager.GetPaths())
}
func (c *linuxContainer) updateState(process parentProcess) error {
c.initProcess = process
state, err := c.currentState()
if err != nil {
return err
}
f, err := os.Create(filepath.Join(c.root, stateFilename))
if err != nil {
return err
}
defer f.Close()
return json.NewEncoder(f).Encode(state)
}
func (c *linuxContainer) currentStatus() (Status, error) {
if c.initProcess == nil {
return Destroyed, nil
}
// return Running if the init process is alive
if err := syscall.Kill(c.initProcess.pid(), 0); err != nil {
if err == syscall.ESRCH {
return Destroyed, nil
}
return 0, newSystemError(err)
}
if c.config.Cgroups != nil && c.config.Cgroups.Freezer == configs.Frozen {
return Paused, nil
}
return Running, nil
}
func (c *linuxContainer) currentState() (*State, error) {
status, err := c.currentStatus()
if err != nil {
return nil, err
}
if status == Destroyed {
return nil, newGenericError(fmt.Errorf("container destroyed"), ContainerNotExists)
}
startTime, err := c.initProcess.startTime()
if err != nil {
return nil, newSystemError(err)
}
state := &State{
ID: c.ID(),
Config: *c.config,
InitProcessPid: c.initProcess.pid(),
InitProcessStartTime: startTime,
CgroupPaths: c.cgroupManager.GetPaths(),
NamespacePaths: make(map[string]string),
}
for _, ns := range c.config.Namespaces {
state.NamespacePaths[string(ns.Type)] = ns.GetPath(c.initProcess.pid())
}
return state, nil
}

View File

@ -0,0 +1,13 @@
// +build !go1.4
package libcontainer
import (
"fmt"
"syscall"
)
// not available before go 1.4
func (c *linuxContainer) addUidGidMappings(sys *syscall.SysProcAttr) error {
return fmt.Errorf("User namespace is not supported in golang < 1.4")
}

196
linux_container_test.go Normal file
View File

@ -0,0 +1,196 @@
// +build linux
package libcontainer
import (
"fmt"
"os"
"testing"
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/configs"
)
type mockCgroupManager struct {
pids []int
stats *cgroups.Stats
paths map[string]string
}
func (m *mockCgroupManager) GetPids() ([]int, error) {
return m.pids, nil
}
func (m *mockCgroupManager) GetStats() (*cgroups.Stats, error) {
return m.stats, nil
}
func (m *mockCgroupManager) Apply(pid int) error {
return nil
}
func (m *mockCgroupManager) Destroy() error {
return nil
}
func (m *mockCgroupManager) GetPaths() map[string]string {
return m.paths
}
func (m *mockCgroupManager) Freeze(state configs.FreezerState) error {
return nil
}
type mockProcess struct {
_pid int
started string
}
func (m *mockProcess) terminate() error {
return nil
}
func (m *mockProcess) pid() int {
return m._pid
}
func (m *mockProcess) startTime() (string, error) {
return m.started, nil
}
func (m *mockProcess) start() error {
return nil
}
func (m *mockProcess) wait() (*os.ProcessState, error) {
return nil, nil
}
func (m *mockProcess) signal(_ os.Signal) error {
return nil
}
func TestGetContainerPids(t *testing.T) {
container := &linuxContainer{
id: "myid",
config: &configs.Config{},
cgroupManager: &mockCgroupManager{pids: []int{1, 2, 3}},
}
pids, err := container.Processes()
if err != nil {
t.Fatal(err)
}
for i, expected := range []int{1, 2, 3} {
if pids[i] != expected {
t.Fatalf("expected pid %d but received %d", expected, pids[i])
}
}
}
func TestGetContainerStats(t *testing.T) {
container := &linuxContainer{
id: "myid",
config: &configs.Config{},
cgroupManager: &mockCgroupManager{
pids: []int{1, 2, 3},
stats: &cgroups.Stats{
MemoryStats: cgroups.MemoryStats{
Usage: 1024,
},
},
},
}
stats, err := container.Stats()
if err != nil {
t.Fatal(err)
}
if stats.CgroupStats == nil {
t.Fatal("cgroup stats are nil")
}
if stats.CgroupStats.MemoryStats.Usage != 1024 {
t.Fatalf("expected memory usage 1024 but recevied %d", stats.CgroupStats.MemoryStats.Usage)
}
}
func TestGetContainerState(t *testing.T) {
var (
pid = os.Getpid()
expectedMemoryPath = "/sys/fs/cgroup/memory/myid"
expectedNetworkPath = "/networks/fd"
)
container := &linuxContainer{
id: "myid",
config: &configs.Config{
Namespaces: configs.Namespaces{
{Type: configs.NEWPID},
{Type: configs.NEWNS},
{Type: configs.NEWNET, Path: expectedNetworkPath},
{Type: configs.NEWUTS},
{Type: configs.NEWIPC},
},
},
initProcess: &mockProcess{
_pid: pid,
started: "010",
},
cgroupManager: &mockCgroupManager{
pids: []int{1, 2, 3},
stats: &cgroups.Stats{
MemoryStats: cgroups.MemoryStats{
Usage: 1024,
},
},
paths: map[string]string{
"memory": expectedMemoryPath,
},
},
}
state, err := container.State()
if err != nil {
t.Fatal(err)
}
if state.InitProcessPid != pid {
t.Fatalf("expected pid %d but received %d", pid, state.InitProcessPid)
}
if state.InitProcessStartTime != "010" {
t.Fatalf("expected process start time 010 but received %s", state.InitProcessStartTime)
}
paths := state.CgroupPaths
if paths == nil {
t.Fatal("cgroup paths should not be nil")
}
if memPath := paths["memory"]; memPath != expectedMemoryPath {
t.Fatalf("expected memory path %q but received %q", expectedMemoryPath, memPath)
}
for _, ns := range container.config.Namespaces {
path := state.NamespacePaths[string(ns.Type)]
if path == "" {
t.Fatalf("expected non nil namespace path for %s", ns.Type)
}
if ns.Type == configs.NEWNET {
if path != expectedNetworkPath {
t.Fatalf("expected path %q but received %q", expectedNetworkPath, path)
}
} else {
file := ""
switch ns.Type {
case configs.NEWNET:
file = "net"
case configs.NEWNS:
file = "mnt"
case configs.NEWPID:
file = "pid"
case configs.NEWIPC:
file = "ipc"
case configs.NEWUSER:
file = "user"
case configs.NEWUTS:
file = "uts"
}
expected := fmt.Sprintf("/proc/%d/ns/%s", pid, file)
if expected != path {
t.Fatalf("expected path %q but received %q", expected, path)
}
}
}
}

26
linux_container_userns.go Normal file
View File

@ -0,0 +1,26 @@
// +build go1.4
package libcontainer
import "syscall"
// Converts IDMap to SysProcIDMap array and adds it to SysProcAttr.
func (c *linuxContainer) addUidGidMappings(sys *syscall.SysProcAttr) error {
if c.config.UidMappings != nil {
sys.UidMappings = make([]syscall.SysProcIDMap, len(c.config.UidMappings))
for i, um := range c.config.UidMappings {
sys.UidMappings[i].ContainerID = um.ContainerID
sys.UidMappings[i].HostID = um.HostID
sys.UidMappings[i].Size = um.Size
}
}
if c.config.GidMappings != nil {
sys.GidMappings = make([]syscall.SysProcIDMap, len(c.config.GidMappings))
for i, gm := range c.config.GidMappings {
sys.GidMappings[i].ContainerID = gm.ContainerID
sys.GidMappings[i].HostID = gm.HostID
sys.GidMappings[i].Size = gm.Size
}
}
return nil
}

239
linux_factory.go Normal file
View File

@ -0,0 +1,239 @@
// +build linux
package libcontainer
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/cgroups/fs"
"github.com/docker/libcontainer/cgroups/systemd"
"github.com/docker/libcontainer/configs"
"github.com/docker/libcontainer/configs/validate"
)
const (
stateFilename = "state.json"
)
var (
idRegex = regexp.MustCompile(`^[\w_]+$`)
maxIdLen = 1024
)
// InitArgs returns an options func to configure a LinuxFactory with the
// provided init arguments.
func InitArgs(args ...string) func(*LinuxFactory) error {
return func(l *LinuxFactory) error {
l.InitArgs = args
return nil
}
}
// SystemdCgroups is an options func to configure a LinuxFactory to return
// containers that use systemd to create and manage cgroups.
func SystemdCgroups(l *LinuxFactory) error {
l.NewCgroupsManager = func(config *configs.Cgroup, paths map[string]string) cgroups.Manager {
return &systemd.Manager{
Cgroups: config,
Paths: paths,
}
}
return nil
}
// Cgroupfs is an options func to configure a LinuxFactory to return
// containers that use the native cgroups filesystem implementation to
// create and manage cgroups.
func Cgroupfs(l *LinuxFactory) error {
l.NewCgroupsManager = func(config *configs.Cgroup, paths map[string]string) cgroups.Manager {
return &fs.Manager{
Cgroups: config,
Paths: paths,
}
}
return nil
}
// New returns a linux based container factory based in the root directory and
// configures the factory with the provided option funcs.
func New(root string, options ...func(*LinuxFactory) error) (Factory, error) {
if root != "" {
if err := os.MkdirAll(root, 0700); err != nil {
return nil, newGenericError(err, SystemError)
}
}
l := &LinuxFactory{
Root: root,
InitArgs: []string{os.Args[0], "init"},
Validator: validate.New(),
}
Cgroupfs(l)
for _, opt := range options {
if err := opt(l); err != nil {
return nil, err
}
}
return l, nil
}
// LinuxFactory implements the default factory interface for linux based systems.
type LinuxFactory struct {
// Root directory for the factory to store state.
Root string
// InitArgs are arguments for calling the init responsibilities for spawning
// a container.
InitArgs []string
// Validator provides validation to container configurations.
Validator validate.Validator
// NewCgroupsManager returns an initialized cgroups manager for a single container.
NewCgroupsManager func(config *configs.Cgroup, paths map[string]string) cgroups.Manager
}
func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, error) {
if l.Root == "" {
return nil, newGenericError(fmt.Errorf("invalid root"), ConfigInvalid)
}
if err := l.validateID(id); err != nil {
return nil, err
}
if err := l.Validator.Validate(config); err != nil {
return nil, newGenericError(err, ConfigInvalid)
}
containerRoot := filepath.Join(l.Root, id)
if _, err := os.Stat(containerRoot); err == nil {
return nil, newGenericError(fmt.Errorf("Container with id exists: %v", id), IdInUse)
} else if !os.IsNotExist(err) {
return nil, newGenericError(err, SystemError)
}
if err := os.MkdirAll(containerRoot, 0700); err != nil {
return nil, newGenericError(err, SystemError)
}
return &linuxContainer{
id: id,
root: containerRoot,
config: config,
initArgs: l.InitArgs,
cgroupManager: l.NewCgroupsManager(config.Cgroups, nil),
}, nil
}
func (l *LinuxFactory) Load(id string) (Container, error) {
if l.Root == "" {
return nil, newGenericError(fmt.Errorf("invalid root"), ConfigInvalid)
}
containerRoot := filepath.Join(l.Root, id)
state, err := l.loadState(containerRoot)
if err != nil {
return nil, err
}
r := &restoredProcess{
processPid: state.InitProcessPid,
processStartTime: state.InitProcessStartTime,
}
return &linuxContainer{
initProcess: r,
id: id,
config: &state.Config,
initArgs: l.InitArgs,
cgroupManager: l.NewCgroupsManager(state.Config.Cgroups, state.CgroupPaths),
root: containerRoot,
}, nil
}
// StartInitialization loads a container by opening the pipe fd from the parent to read the configuration and state
// This is a low level implementation detail of the reexec and should not be consumed externally
func (l *LinuxFactory) StartInitialization(pipefd uintptr) (err error) {
var (
pipe = os.NewFile(uintptr(pipefd), "pipe")
it = initType(os.Getenv("_LIBCONTAINER_INITTYPE"))
)
// clear the current process's environment to clean any libcontainer
// specific env vars.
os.Clearenv()
defer func() {
// if we have an error during the initialization of the container's init then send it back to the
// parent process in the form of an initError.
if err != nil {
// ensure that any data sent from the parent is consumed so it doesn't
// receive ECONNRESET when the child writes to the pipe.
ioutil.ReadAll(pipe)
if err := json.NewEncoder(pipe).Encode(newSystemError(err)); err != nil {
panic(err)
}
}
// ensure that this pipe is always closed
pipe.Close()
}()
i, err := newContainerInit(it, pipe)
if err != nil {
return err
}
return i.Init()
}
func (l *LinuxFactory) loadState(root string) (*State, error) {
f, err := os.Open(filepath.Join(root, stateFilename))
if err != nil {
if os.IsNotExist(err) {
return nil, newGenericError(err, ContainerNotExists)
}
return nil, newGenericError(err, SystemError)
}
defer f.Close()
var state *State
if err := json.NewDecoder(f).Decode(&state); err != nil {
return nil, newGenericError(err, SystemError)
}
return state, nil
}
func (l *LinuxFactory) validateID(id string) error {
if !idRegex.MatchString(id) {
return newGenericError(fmt.Errorf("Invalid id format: %v", id), InvalidIdFormat)
}
if len(id) > maxIdLen {
return newGenericError(fmt.Errorf("Invalid id format: %v", id), InvalidIdFormat)
}
return nil
}
// restoredProcess represents a process where the calling process may or may not be
// the parent process. This process is created when a factory loads a container from
// a persisted state.
type restoredProcess struct {
processPid int
processStartTime string
}
func (p *restoredProcess) start() error {
return newGenericError(fmt.Errorf("restored process cannot be started"), SystemError)
}
func (p *restoredProcess) pid() int {
return p.processPid
}
func (p *restoredProcess) terminate() error {
return newGenericError(fmt.Errorf("restored process cannot be terminated"), SystemError)
}
func (p *restoredProcess) wait() (*os.ProcessState, error) {
return nil, newGenericError(fmt.Errorf("restored process cannot be waited on"), SystemError)
}
func (p *restoredProcess) startTime() (string, error) {
return p.processStartTime, nil
}
func (p *restoredProcess) signal(s os.Signal) error {
return newGenericError(fmt.Errorf("restored process cannot be signaled"), SystemError)
}

125
linux_factory_test.go Normal file
View File

@ -0,0 +1,125 @@
// +build linux
package libcontainer
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/docker/libcontainer/configs"
)
func newTestRoot() (string, error) {
dir, err := ioutil.TempDir("", "libcontainer")
if err != nil {
return "", err
}
if err := os.MkdirAll(dir, 0700); err != nil {
return "", err
}
return dir, nil
}
func TestFactoryNew(t *testing.T) {
root, rerr := newTestRoot()
if rerr != nil {
t.Fatal(rerr)
}
defer os.RemoveAll(root)
factory, err := New(root, Cgroupfs)
if err != nil {
t.Fatal(err)
}
if factory == nil {
t.Fatal("factory should not be nil")
}
lfactory, ok := factory.(*LinuxFactory)
if !ok {
t.Fatal("expected linux factory returned on linux based systems")
}
if lfactory.Root != root {
t.Fatalf("expected factory root to be %q but received %q", root, lfactory.Root)
}
}
func TestFactoryLoadNotExists(t *testing.T) {
root, rerr := newTestRoot()
if rerr != nil {
t.Fatal(rerr)
}
defer os.RemoveAll(root)
factory, err := New(root, Cgroupfs)
if err != nil {
t.Fatal(err)
}
_, err = factory.Load("nocontainer")
if err == nil {
t.Fatal("expected nil error loading non-existing container")
}
lerr, ok := err.(Error)
if !ok {
t.Fatal("expected libcontainer error type")
}
if lerr.Code() != ContainerNotExists {
t.Fatalf("expected error code %s but received %s", ContainerNotExists, lerr.Code())
}
}
func TestFactoryLoadContainer(t *testing.T) {
root, err := newTestRoot()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
// setup default container config and state for mocking
var (
id = "1"
expectedConfig = &configs.Config{
Rootfs: "/mycontainer/root",
}
expectedState = &State{
InitProcessPid: 1024,
Config: *expectedConfig,
}
)
if err := os.Mkdir(filepath.Join(root, id), 0700); err != nil {
t.Fatal(err)
}
if err := marshal(filepath.Join(root, id, stateFilename), expectedState); err != nil {
t.Fatal(err)
}
factory, err := New(root, Cgroupfs)
if err != nil {
t.Fatal(err)
}
container, err := factory.Load(id)
if err != nil {
t.Fatal(err)
}
if container.ID() != id {
t.Fatalf("expected container id %q but received %q", id, container.ID())
}
config := container.Config()
if config.Rootfs != expectedConfig.Rootfs {
t.Fatalf("expected rootfs %q but received %q", expectedConfig.Rootfs, config.Rootfs)
}
lcontainer, ok := container.(*linuxContainer)
if !ok {
t.Fatal("expected linux container on linux based systems")
}
if lcontainer.initProcess.pid() != expectedState.InitProcessPid {
t.Fatalf("expected init pid %d but received %d", expectedState.InitProcessPid, lcontainer.initProcess.pid())
}
}
func marshal(path string, v interface{}) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return json.NewEncoder(f).Encode(v)
}

252
linux_init.go Normal file
View File

@ -0,0 +1,252 @@
// +build linux
package libcontainer
import (
"encoding/json"
"fmt"
"os"
"strings"
"syscall"
log "github.com/Sirupsen/logrus"
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/configs"
"github.com/docker/libcontainer/netlink"
"github.com/docker/libcontainer/system"
"github.com/docker/libcontainer/user"
"github.com/docker/libcontainer/utils"
)
type initType string
const (
initSetns initType = "setns"
initStandard initType = "standard"
)
type pid struct {
Pid int `json:"pid"`
}
// network is an internal struct used to setup container networks.
type network struct {
configs.Network
// TempVethPeerName is a unique tempory veth peer name that was placed into
// the container's namespace.
TempVethPeerName string `json:"temp_veth_peer_name"`
}
// initConfig is used for transferring parameters from Exec() to Init()
type initConfig struct {
Args []string `json:"args"`
Env []string `json:"env"`
Cwd string `json:"cwd"`
User string `json:"user"`
Config *configs.Config `json:"config"`
Networks []*network `json:"network"`
}
type initer interface {
Init() error
}
func newContainerInit(t initType, pipe *os.File) (initer, error) {
var config *initConfig
if err := json.NewDecoder(pipe).Decode(&config); err != nil {
return nil, err
}
if err := populateProcessEnvironment(config.Env); err != nil {
return nil, err
}
switch t {
case initSetns:
return &linuxSetnsInit{
config: config,
}, nil
case initStandard:
return &linuxStandardInit{
config: config,
}, nil
}
return nil, fmt.Errorf("unknown init type %q", t)
}
// populateProcessEnvironment loads the provided environment variables into the
// current processes's environment.
func populateProcessEnvironment(env []string) error {
for _, pair := range env {
p := strings.SplitN(pair, "=", 2)
if len(p) < 2 {
return fmt.Errorf("invalid environment '%v'", pair)
}
if err := os.Setenv(p[0], p[1]); err != nil {
return err
}
}
return nil
}
// finalizeNamespace drops the caps, sets the correct user
// and working dir, and closes any leaked file descriptors
// before execing the command inside the namespace
func finalizeNamespace(config *initConfig) error {
// Ensure that all non-standard fds we may have accidentally
// inherited are marked close-on-exec so they stay out of the
// container
if err := utils.CloseExecFrom(3); err != nil {
return err
}
w, err := newCapWhitelist(config.Config.Capabilities)
if err != nil {
return err
}
// drop capabilities in bounding set before changing user
if err := w.dropBoundingSet(); err != nil {
return err
}
// preserve existing capabilities while we change users
if err := system.SetKeepCaps(); err != nil {
return err
}
if err := setupUser(config); err != nil {
return err
}
if err := system.ClearKeepCaps(); err != nil {
return err
}
// drop all other capabilities
if err := w.drop(); err != nil {
return err
}
if config.Cwd != "" {
if err := syscall.Chdir(config.Cwd); err != nil {
return err
}
}
return nil
}
// joinExistingNamespaces gets all the namespace paths specified for the container and
// does a setns on the namespace fd so that the current process joins the namespace.
func joinExistingNamespaces(namespaces []configs.Namespace) error {
for _, ns := range namespaces {
if ns.Path != "" {
f, err := os.OpenFile(ns.Path, os.O_RDONLY, 0)
if err != nil {
return err
}
err = system.Setns(f.Fd(), uintptr(ns.Syscall()))
f.Close()
if err != nil {
return err
}
}
}
return nil
}
// setupUser changes the groups, gid, and uid for the user inside the container
func setupUser(config *initConfig) error {
// Set up defaults.
defaultExecUser := user.ExecUser{
Uid: syscall.Getuid(),
Gid: syscall.Getgid(),
Home: "/",
}
passwdPath, err := user.GetPasswdPath()
if err != nil {
return err
}
groupPath, err := user.GetGroupPath()
if err != nil {
return err
}
execUser, err := user.GetExecUserPath(config.User, &defaultExecUser, passwdPath, groupPath)
if err != nil {
return err
}
suppGroups := append(execUser.Sgids, config.Config.AdditionalGroups...)
if err := syscall.Setgroups(suppGroups); err != nil {
return err
}
if err := system.Setgid(execUser.Gid); err != nil {
return err
}
if err := system.Setuid(execUser.Uid); err != nil {
return err
}
// if we didn't get HOME already, set it based on the user's HOME
if envHome := os.Getenv("HOME"); envHome == "" {
if err := os.Setenv("HOME", execUser.Home); err != nil {
return err
}
}
return nil
}
// setupNetwork sets up and initializes any network interface inside the container.
func setupNetwork(config *initConfig) error {
for _, config := range config.Networks {
strategy, err := getStrategy(config.Type)
if err != nil {
return err
}
if err := strategy.initialize(config); err != nil {
return err
}
}
return nil
}
func setupRoute(config *configs.Config) error {
for _, config := range config.Routes {
if err := netlink.AddRoute(config.Destination, config.Source, config.Gateway, config.InterfaceName); err != nil {
return err
}
}
return nil
}
func setupRlimits(config *configs.Config) error {
for _, rlimit := range config.Rlimits {
l := &syscall.Rlimit{Max: rlimit.Hard, Cur: rlimit.Soft}
if err := syscall.Setrlimit(rlimit.Type, l); err != nil {
return fmt.Errorf("error setting rlimit type %v: %v", rlimit.Type, err)
}
}
return nil
}
// killCgroupProcesses freezes then iterates over all the processes inside the
// manager's cgroups sending a SIGKILL to each process then waiting for them to
// exit.
func killCgroupProcesses(m cgroups.Manager) error {
var procs []*os.Process
if err := m.Freeze(configs.Frozen); err != nil {
log.Warn(err)
}
pids, err := m.GetPids()
if err != nil {
m.Freeze(configs.Thawed)
return err
}
for _, pid := range pids {
if p, err := os.FindProcess(pid); err == nil {
procs = append(procs, p)
if err := p.Kill(); err != nil {
log.Warn(err)
}
}
}
if err := m.Freeze(configs.Thawed); err != nil {
log.Warn(err)
}
for _, p := range procs {
if _, err := p.Wait(); err != nil {
log.Warn(err)
}
}
return nil
}

208
linux_network.go Normal file
View File

@ -0,0 +1,208 @@
// +build linux
package libcontainer
import (
"fmt"
"io/ioutil"
"net"
"path/filepath"
"strconv"
"strings"
"github.com/docker/libcontainer/netlink"
"github.com/docker/libcontainer/utils"
)
var strategies = map[string]networkStrategy{
"veth": &veth{},
"loopback": &loopback{},
}
// networkStrategy represents a specific network configuration for
// a container's networking stack
type networkStrategy interface {
create(*network, int) error
initialize(*network) error
}
// getStrategy returns the specific network strategy for the
// provided type.
func getStrategy(tpe string) (networkStrategy, error) {
s, exists := strategies[tpe]
if !exists {
return nil, fmt.Errorf("unknown strategy type %q", tpe)
}
return s, nil
}
// Returns the network statistics for the network interfaces represented by the NetworkRuntimeInfo.
func getNetworkInterfaceStats(interfaceName string) (*networkInterface, error) {
out := &networkInterface{Name: interfaceName}
// This can happen if the network runtime information is missing - possible if the
// container was created by an old version of libcontainer.
if interfaceName == "" {
return out, nil
}
type netStatsPair struct {
// Where to write the output.
Out *uint64
// The network stats file to read.
File string
}
// Ingress for host veth is from the container. Hence tx_bytes stat on the host veth is actually number of bytes received by the container.
netStats := []netStatsPair{
{Out: &out.RxBytes, File: "tx_bytes"},
{Out: &out.RxPackets, File: "tx_packets"},
{Out: &out.RxErrors, File: "tx_errors"},
{Out: &out.RxDropped, File: "tx_dropped"},
{Out: &out.TxBytes, File: "rx_bytes"},
{Out: &out.TxPackets, File: "rx_packets"},
{Out: &out.TxErrors, File: "rx_errors"},
{Out: &out.TxDropped, File: "rx_dropped"},
}
for _, netStat := range netStats {
data, err := readSysfsNetworkStats(interfaceName, netStat.File)
if err != nil {
return nil, err
}
*(netStat.Out) = data
}
return out, nil
}
// Reads the specified statistics available under /sys/class/net/<EthInterface>/statistics
func readSysfsNetworkStats(ethInterface, statsFile string) (uint64, error) {
data, err := ioutil.ReadFile(filepath.Join("/sys/class/net", ethInterface, "statistics", statsFile))
if err != nil {
return 0, err
}
return strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
}
// loopback is a network strategy that provides a basic loopback device
type loopback struct {
}
func (l *loopback) create(n *network, nspid int) error {
return nil
}
func (l *loopback) initialize(config *network) error {
iface, err := net.InterfaceByName("lo")
if err != nil {
return err
}
return netlink.NetworkLinkUp(iface)
}
// veth is a network strategy that uses a bridge and creates
// a veth pair, one that is attached to the bridge on the host and the other
// is placed inside the container's namespace
type veth struct {
}
func (v *veth) create(n *network, nspid int) (err error) {
tmpName, err := v.generateTempPeerName()
if err != nil {
return err
}
n.TempVethPeerName = tmpName
defer func() {
if err != nil {
netlink.NetworkLinkDel(n.HostInterfaceName)
netlink.NetworkLinkDel(n.TempVethPeerName)
}
}()
if n.Bridge == "" {
return fmt.Errorf("bridge is not specified")
}
bridge, err := net.InterfaceByName(n.Bridge)
if err != nil {
return err
}
if err := netlink.NetworkCreateVethPair(n.HostInterfaceName, n.TempVethPeerName, n.TxQueueLen); err != nil {
return err
}
host, err := net.InterfaceByName(n.HostInterfaceName)
if err != nil {
return err
}
if err := netlink.AddToBridge(host, bridge); err != nil {
return err
}
if err := netlink.NetworkSetMTU(host, n.Mtu); err != nil {
return err
}
if err := netlink.NetworkLinkUp(host); err != nil {
return err
}
child, err := net.InterfaceByName(n.TempVethPeerName)
if err != nil {
return err
}
return netlink.NetworkSetNsPid(child, nspid)
}
func (v *veth) generateTempPeerName() (string, error) {
return utils.GenerateRandomName("veth", 7)
}
func (v *veth) initialize(config *network) error {
peer := config.TempVethPeerName
if peer == "" {
return fmt.Errorf("peer is not specified")
}
child, err := net.InterfaceByName(peer)
if err != nil {
return err
}
if err := netlink.NetworkLinkDown(child); err != nil {
return err
}
if err := netlink.NetworkChangeName(child, config.Name); err != nil {
return err
}
// get the interface again after we changed the name as the index also changes.
if child, err = net.InterfaceByName(config.Name); err != nil {
return err
}
if config.MacAddress != "" {
if err := netlink.NetworkSetMacAddress(child, config.MacAddress); err != nil {
return err
}
}
ip, ipNet, err := net.ParseCIDR(config.Address)
if err != nil {
return err
}
if err := netlink.NetworkLinkAddIp(child, ip, ipNet); err != nil {
return err
}
if config.IPv6Address != "" {
if ip, ipNet, err = net.ParseCIDR(config.IPv6Address); err != nil {
return err
}
if err := netlink.NetworkLinkAddIp(child, ip, ipNet); err != nil {
return err
}
}
if err := netlink.NetworkSetMTU(child, config.Mtu); err != nil {
return err
}
if err := netlink.NetworkLinkUp(child); err != nil {
return err
}
if config.Gateway != "" {
if err := netlink.AddDefaultGw(config.Gateway, config.Name); err != nil {
return err
}
}
if config.IPv6Gateway != "" {
if err := netlink.AddDefaultGw(config.IPv6Gateway, config.Name); err != nil {
return err
}
}
return nil
}

View File

@ -12,11 +12,11 @@ import (
const oomCgroupName = "memory"
// NotifyOnOOM returns channel on which you can expect event about OOM,
// notifyOnOOM returns channel on which you can expect event about OOM,
// if process died without OOM this channel will be closed.
// s is current *libcontainer.State for container.
func NotifyOnOOM(s *State) (<-chan struct{}, error) {
dir := s.CgroupPaths[oomCgroupName]
func notifyOnOOM(paths map[string]string) (<-chan struct{}, error) {
dir := paths[oomCgroupName]
if dir == "" {
return nil, fmt.Errorf("There is no path for %q in state", oomCgroupName)
}

View File

@ -27,12 +27,10 @@ func TestNotifyOnOOM(t *testing.T) {
t.Fatal(err)
}
var eventFd, oomControlFd int
st := &State{
CgroupPaths: map[string]string{
"memory": memoryPath,
},
paths := map[string]string{
"memory": memoryPath,
}
ooms, err := NotifyOnOOM(st)
ooms, err := notifyOnOOM(paths)
if err != nil {
t.Fatal("expected no error, got:", err)
}

216
linux_process.go Normal file
View File

@ -0,0 +1,216 @@
// +build linux
package libcontainer
import (
"encoding/json"
"io"
"os"
"os/exec"
"syscall"
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/system"
)
type parentProcess interface {
// pid returns the pid for the running process.
pid() int
// start starts the process execution.
start() error
// send a SIGKILL to the process and wait for the exit.
terminate() error
// wait waits on the process returning the process state.
wait() (*os.ProcessState, error)
// startTime return's the process start time.
startTime() (string, error)
signal(os.Signal) error
}
type setnsProcess struct {
cmd *exec.Cmd
parentPipe *os.File
childPipe *os.File
forkedProcess *os.Process
cgroupPaths map[string]string
config *initConfig
}
func (p *setnsProcess) startTime() (string, error) {
return system.GetProcessStartTime(p.pid())
}
func (p *setnsProcess) signal(s os.Signal) error {
return p.forkedProcess.Signal(s)
}
func (p *setnsProcess) start() (err error) {
defer p.parentPipe.Close()
if p.forkedProcess, err = p.execSetns(); err != nil {
return newSystemError(err)
}
if len(p.cgroupPaths) > 0 {
if err := cgroups.EnterPid(p.cgroupPaths, p.forkedProcess.Pid); err != nil {
return newSystemError(err)
}
}
if err := json.NewEncoder(p.parentPipe).Encode(p.config); err != nil {
return newSystemError(err)
}
return nil
}
// execSetns runs the process that executes C code to perform the setns calls
// because setns support requires the C process to fork off a child and perform the setns
// before the go runtime boots, we wait on the process to die and receive the child's pid
// over the provided pipe.
func (p *setnsProcess) execSetns() (*os.Process, error) {
err := p.cmd.Start()
p.childPipe.Close()
if err != nil {
return nil, newSystemError(err)
}
status, err := p.cmd.Process.Wait()
if err != nil {
return nil, newSystemError(err)
}
if !status.Success() {
return nil, newSystemError(&exec.ExitError{ProcessState: status})
}
var pid *pid
if err := json.NewDecoder(p.parentPipe).Decode(&pid); err != nil {
return nil, newSystemError(err)
}
return os.FindProcess(pid.Pid)
}
// terminate sends a SIGKILL to the forked process for the setns routine then waits to
// avoid the process becomming a zombie.
func (p *setnsProcess) terminate() error {
if p.forkedProcess == nil {
return nil
}
err := p.forkedProcess.Kill()
if _, werr := p.wait(); err == nil {
err = werr
}
return err
}
func (p *setnsProcess) wait() (*os.ProcessState, error) {
return p.forkedProcess.Wait()
}
func (p *setnsProcess) pid() int {
return p.forkedProcess.Pid
}
type initProcess struct {
cmd *exec.Cmd
parentPipe *os.File
childPipe *os.File
config *initConfig
manager cgroups.Manager
}
func (p *initProcess) pid() int {
return p.cmd.Process.Pid
}
func (p *initProcess) start() error {
defer p.parentPipe.Close()
err := p.cmd.Start()
p.childPipe.Close()
if err != nil {
return newSystemError(err)
}
// Do this before syncing with child so that no children
// can escape the cgroup
if err := p.manager.Apply(p.pid()); err != nil {
return newSystemError(err)
}
defer func() {
if err != nil {
// TODO: should not be the responsibility to call here
p.manager.Destroy()
}
}()
if err := p.createNetworkInterfaces(); err != nil {
return newSystemError(err)
}
if err := p.sendConfig(); err != nil {
return newSystemError(err)
}
// wait for the child process to fully complete and receive an error message
// if one was encoutered
var ierr *genericError
if err := json.NewDecoder(p.parentPipe).Decode(&ierr); err != nil && err != io.EOF {
return newSystemError(err)
}
if ierr != nil {
return newSystemError(ierr)
}
return nil
}
func (p *initProcess) wait() (*os.ProcessState, error) {
state, err := p.cmd.Process.Wait()
if err != nil {
return nil, err
}
// we should kill all processes in cgroup when init is died if we use host PID namespace
if p.cmd.SysProcAttr.Cloneflags&syscall.CLONE_NEWPID == 0 {
killCgroupProcesses(p.manager)
}
return state, nil
}
func (p *initProcess) terminate() error {
if p.cmd.Process == nil {
return nil
}
err := p.cmd.Process.Kill()
if _, werr := p.wait(); err == nil {
err = werr
}
return err
}
func (p *initProcess) startTime() (string, error) {
return system.GetProcessStartTime(p.pid())
}
func (p *initProcess) sendConfig() error {
// send the state to the container's init process then shutdown writes for the parent
if err := json.NewEncoder(p.parentPipe).Encode(p.config); err != nil {
return err
}
// shutdown writes for the parent side of the pipe
return syscall.Shutdown(int(p.parentPipe.Fd()), syscall.SHUT_WR)
}
func (p *initProcess) createNetworkInterfaces() error {
for _, config := range p.config.Config.Networks {
strategy, err := getStrategy(config.Type)
if err != nil {
return err
}
n := &network{
Network: *config,
}
if err := strategy.create(n, p.pid()); err != nil {
return err
}
p.config.Networks = append(p.config.Networks, n)
}
return nil
}
func (p *initProcess) signal(s os.Signal) error {
return p.cmd.Process.Signal(s)
}

359
linux_rootfs.go Normal file
View File

@ -0,0 +1,359 @@
// +build linux
package libcontainer
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"syscall"
"time"
"github.com/docker/libcontainer/configs"
"github.com/docker/libcontainer/label"
)
const defaultMountFlags = syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
var baseMounts = []*configs.Mount{
{
Source: "proc",
Destination: "/proc",
Device: "proc",
Flags: defaultMountFlags,
},
{
Source: "tmpfs",
Destination: "/dev",
Device: "tmpfs",
Flags: syscall.MS_NOSUID | syscall.MS_STRICTATIME,
Data: "mode=755",
},
{
Source: "devpts",
Destination: "/dev/pts",
Device: "devpts",
Flags: syscall.MS_NOSUID | syscall.MS_NOEXEC,
Data: "newinstance,ptmxmode=0666,mode=0620,gid=5",
},
}
// setupRootfs sets up the devices, mount points, and filesystems for use inside a
// new mount namespace.
func setupRootfs(config *configs.Config) (err error) {
if err := prepareRoot(config); err != nil {
return newSystemError(err)
}
for _, m := range append(baseMounts, config.Mounts...) {
if err := mount(m, config.Rootfs, config.MountLabel); err != nil {
return newSystemError(err)
}
}
if err := createDevices(config); err != nil {
return newSystemError(err)
}
if err := setupPtmx(config); err != nil {
return newSystemError(err)
}
// stdin, stdout and stderr could be pointing to /dev/null from parent namespace.
// re-open them inside this namespace.
if err := reOpenDevNull(config.Rootfs); err != nil {
return newSystemError(err)
}
if err := setupDevSymlinks(config.Rootfs); err != nil {
return newSystemError(err)
}
if err := syscall.Chdir(config.Rootfs); err != nil {
return newSystemError(err)
}
if config.NoPivotRoot {
err = msMoveRoot(config.Rootfs)
} else {
err = pivotRoot(config.Rootfs, config.PivotDir)
}
if err != nil {
return newSystemError(err)
}
if config.Readonlyfs {
if err := setReadonly(); err != nil {
return newSystemError(err)
}
}
syscall.Umask(0022)
return nil
}
func mount(m *configs.Mount, rootfs, mountLabel string) error {
var (
dest = filepath.Join(rootfs, m.Destination)
data = label.FormatMountLabel(m.Data, mountLabel)
)
switch m.Device {
case "proc":
if err := os.MkdirAll(dest, 0755); err != nil && !os.IsExist(err) {
return err
}
return syscall.Mount(m.Source, dest, m.Device, uintptr(m.Flags), "")
case "tmpfs", "mqueue", "devpts", "sysfs":
if err := os.MkdirAll(dest, 0755); err != nil && !os.IsExist(err) {
return err
}
return syscall.Mount(m.Source, dest, m.Device, uintptr(m.Flags), data)
case "bind":
stat, err := os.Stat(m.Source)
if err != nil {
// error out if the source of a bind mount does not exist as we will be
// unable to bind anything to it.
return err
}
if err := createIfNotExists(dest, stat.IsDir()); err != nil {
return err
}
if err := syscall.Mount(m.Source, dest, m.Device, uintptr(m.Flags), data); err != nil {
return err
}
if m.Flags&syscall.MS_RDONLY != 0 {
if err := syscall.Mount(m.Source, dest, m.Device, uintptr(m.Flags|syscall.MS_REMOUNT), ""); err != nil {
return err
}
}
if m.Relabel != "" {
if err := label.Relabel(m.Source, mountLabel, m.Relabel); err != nil {
return err
}
}
if m.Flags&syscall.MS_PRIVATE != 0 {
if err := syscall.Mount("", dest, "none", uintptr(syscall.MS_PRIVATE), ""); err != nil {
return err
}
}
default:
return fmt.Errorf("unknown mount device %q to %q", m.Device, m.Destination)
}
return nil
}
func setupDevSymlinks(rootfs string) error {
var links = [][2]string{
{"/proc/self/fd", "/dev/fd"},
{"/proc/self/fd/0", "/dev/stdin"},
{"/proc/self/fd/1", "/dev/stdout"},
{"/proc/self/fd/2", "/dev/stderr"},
}
// kcore support can be toggled with CONFIG_PROC_KCORE; only create a symlink
// in /dev if it exists in /proc.
if _, err := os.Stat("/proc/kcore"); err == nil {
links = append(links, [2]string{"/proc/kcore", "/dev/kcore"})
}
for _, link := range links {
var (
src = link[0]
dst = filepath.Join(rootfs, link[1])
)
if err := os.Symlink(src, dst); err != nil && !os.IsExist(err) {
return fmt.Errorf("symlink %s %s %s", src, dst, err)
}
}
return nil
}
// If stdin, stdout or stderr are pointing to '/dev/null' in the global mount namespace,
// this method will make them point to '/dev/null' in this namespace.
func reOpenDevNull(rootfs string) error {
var stat, devNullStat syscall.Stat_t
file, err := os.Open(filepath.Join(rootfs, "/dev/null"))
if err != nil {
return fmt.Errorf("Failed to open /dev/null - %s", err)
}
defer file.Close()
if err := syscall.Fstat(int(file.Fd()), &devNullStat); err != nil {
return err
}
for fd := 0; fd < 3; fd++ {
if err := syscall.Fstat(fd, &stat); err != nil {
return err
}
if stat.Rdev == devNullStat.Rdev {
// Close and re-open the fd.
if err := syscall.Dup2(int(file.Fd()), fd); err != nil {
return err
}
}
}
return nil
}
// Create the device nodes in the container.
func createDevices(config *configs.Config) error {
oldMask := syscall.Umask(0000)
for _, node := range config.Devices {
if err := createDeviceNode(config.Rootfs, node); err != nil {
syscall.Umask(oldMask)
return err
}
}
syscall.Umask(oldMask)
return nil
}
// Creates the device node in the rootfs of the container.
func createDeviceNode(rootfs string, node *configs.Device) error {
dest := filepath.Join(rootfs, node.Path)
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
return err
}
if err := mknodDevice(dest, node); err != nil {
if os.IsExist(err) {
return nil
}
if err != syscall.EPERM {
return err
}
// containers running in a user namespace are not allowed to mknod
// devices so we can just bind mount it from the host.
f, err := os.Create(dest)
if err != nil && !os.IsExist(err) {
return err
}
if f != nil {
f.Close()
}
return syscall.Mount(node.Path, dest, "bind", syscall.MS_BIND, "")
}
return nil
}
func mknodDevice(dest string, node *configs.Device) error {
fileMode := node.FileMode
switch node.Type {
case 'c':
fileMode |= syscall.S_IFCHR
case 'b':
fileMode |= syscall.S_IFBLK
default:
return fmt.Errorf("%c is not a valid device type for device %s", node.Type, node.Path)
}
if err := syscall.Mknod(dest, uint32(fileMode), node.Mkdev()); err != nil {
return err
}
return syscall.Chown(dest, int(node.Uid), int(node.Gid))
}
func prepareRoot(config *configs.Config) error {
flag := syscall.MS_PRIVATE | syscall.MS_REC
if config.NoPivotRoot {
flag = syscall.MS_SLAVE | syscall.MS_REC
}
if err := syscall.Mount("", "/", "", uintptr(flag), ""); err != nil {
return err
}
return syscall.Mount(config.Rootfs, config.Rootfs, "bind", syscall.MS_BIND|syscall.MS_REC, "")
}
func setReadonly() error {
return syscall.Mount("/", "/", "bind", syscall.MS_BIND|syscall.MS_REMOUNT|syscall.MS_RDONLY|syscall.MS_REC, "")
}
func setupPtmx(config *configs.Config) error {
ptmx := filepath.Join(config.Rootfs, "dev/ptmx")
if err := os.Remove(ptmx); err != nil && !os.IsNotExist(err) {
return err
}
if err := os.Symlink("pts/ptmx", ptmx); err != nil {
return fmt.Errorf("symlink dev ptmx %s", err)
}
if config.Console != "" {
console := newConsoleFromPath(config.Console)
return console.mount(config.Rootfs, config.MountLabel, 0, 0)
}
return nil
}
func pivotRoot(rootfs, pivotBaseDir string) error {
if pivotBaseDir == "" {
pivotBaseDir = "/"
}
tmpDir := filepath.Join(rootfs, pivotBaseDir)
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("can't create tmp dir %s, error %v", tmpDir, err)
}
pivotDir, err := ioutil.TempDir(tmpDir, ".pivot_root")
if err != nil {
return fmt.Errorf("can't create pivot_root dir %s, error %v", pivotDir, err)
}
if err := syscall.PivotRoot(rootfs, pivotDir); err != nil {
return fmt.Errorf("pivot_root %s", err)
}
if err := syscall.Chdir("/"); err != nil {
return fmt.Errorf("chdir / %s", err)
}
// path to pivot dir now changed, update
pivotDir = filepath.Join(pivotBaseDir, filepath.Base(pivotDir))
if err := syscall.Unmount(pivotDir, syscall.MNT_DETACH); err != nil {
return fmt.Errorf("unmount pivot_root dir %s", err)
}
return os.Remove(pivotDir)
}
func msMoveRoot(rootfs string) error {
if err := syscall.Mount(rootfs, "/", "", syscall.MS_MOVE, ""); err != nil {
return err
}
if err := syscall.Chroot("."); err != nil {
return err
}
return syscall.Chdir("/")
}
// createIfNotExists creates a file or a directory only if it does not already exist.
func createIfNotExists(path string, isDir bool) error {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
if isDir {
return os.MkdirAll(path, 0755)
}
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
f, err := os.OpenFile(path, os.O_CREATE, 0755)
if err != nil {
return err
}
f.Close()
}
}
return nil
}
// remountReadonly will bind over the top of an existing path and ensure that it is read-only.
func remountReadonly(path string) error {
for i := 0; i < 5; i++ {
if err := syscall.Mount("", path, "", syscall.MS_REMOUNT|syscall.MS_RDONLY, ""); err != nil && !os.IsNotExist(err) {
switch err {
case syscall.EINVAL:
// Probably not a mountpoint, use bind-mount
if err := syscall.Mount(path, path, "", syscall.MS_BIND, ""); err != nil {
return err
}
return syscall.Mount(path, path, "", syscall.MS_BIND|syscall.MS_REMOUNT|syscall.MS_RDONLY|syscall.MS_REC|defaultMountFlags, "")
case syscall.EBUSY:
time.Sleep(100 * time.Millisecond)
continue
default:
return err
}
}
return nil
}
return fmt.Errorf("unable to mount %s as readonly max retries reached", path)
}
// maskFile bind mounts /dev/null over the top of the specified path inside a container
// to avoid security issues from processes reading information from non-namespace aware mounts ( proc/kcore ).
func maskFile(path string) error {
if err := syscall.Mount("/dev/null", path, "", syscall.MS_BIND, ""); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}

33
linux_setns_init.go Normal file
View File

@ -0,0 +1,33 @@
// +build linux
package libcontainer
import (
"github.com/docker/libcontainer/apparmor"
"github.com/docker/libcontainer/label"
"github.com/docker/libcontainer/system"
)
// linuxSetnsInit performs the container's initialization for running a new process
// inside an existing container.
type linuxSetnsInit struct {
config *initConfig
}
func (l *linuxSetnsInit) Init() error {
if err := setupRlimits(l.config.Config); err != nil {
return err
}
if err := finalizeNamespace(l.config); err != nil {
return err
}
if err := apparmor.ApplyProfile(l.config.Config.AppArmorProfile); err != nil {
return err
}
if l.config.Config.ProcessLabel != "" {
if err := label.SetProcessLabel(l.config.Config.ProcessLabel); err != nil {
return err
}
}
return system.Execv(l.config.Args[0], l.config.Args[0:], l.config.Env)
}

93
linux_standard_init.go Normal file
View File

@ -0,0 +1,93 @@
// +build linux
package libcontainer
import (
"syscall"
"github.com/docker/libcontainer/apparmor"
"github.com/docker/libcontainer/configs"
"github.com/docker/libcontainer/label"
"github.com/docker/libcontainer/system"
)
type linuxStandardInit struct {
config *initConfig
}
func (l *linuxStandardInit) Init() error {
// join any namespaces via a path to the namespace fd if provided
if err := joinExistingNamespaces(l.config.Config.Namespaces); err != nil {
return err
}
consolePath := l.config.Config.Console
if consolePath != "" {
console := newConsoleFromPath(consolePath)
if err := console.dupStdio(); err != nil {
return err
}
}
if _, err := syscall.Setsid(); err != nil {
return err
}
if consolePath != "" {
if err := system.Setctty(); err != nil {
return err
}
}
if err := setupNetwork(l.config); err != nil {
return err
}
if err := setupRoute(l.config.Config); err != nil {
return err
}
if err := setupRlimits(l.config.Config); err != nil {
return err
}
label.Init()
// InitializeMountNamespace() can be executed only for a new mount namespace
if l.config.Config.Namespaces.Contains(configs.NEWNS) {
if err := setupRootfs(l.config.Config); err != nil {
return err
}
}
if hostname := l.config.Config.Hostname; hostname != "" {
if err := syscall.Sethostname([]byte(hostname)); err != nil {
return err
}
}
if err := apparmor.ApplyProfile(l.config.Config.AppArmorProfile); err != nil {
return err
}
if err := label.SetProcessLabel(l.config.Config.ProcessLabel); err != nil {
return err
}
for _, path := range l.config.Config.ReadonlyPaths {
if err := remountReadonly(path); err != nil {
return err
}
}
for _, path := range l.config.Config.MaskPaths {
if err := maskFile(path); err != nil {
return err
}
}
pdeath, err := system.GetParentDeathSignal()
if err != nil {
return err
}
if err := finalizeNamespace(l.config); err != nil {
return err
}
// finalizeNamespace can change user/group which clears the parent death
// signal, so we restore it here.
if err := pdeath.Restore(); err != nil {
return err
}
// Signal self if parent is already dead. Does nothing if running in a new
// PID namespace, as Getppid will always return 0.
if syscall.Getppid() == 1 {
return syscall.Kill(syscall.Getpid(), syscall.SIGKILL)
}
return system.Execv(l.config.Args[0], l.config.Args[0:], l.config.Env)
}

View File

@ -1,212 +0,0 @@
// +build linux
package mount
import (
"fmt"
"os"
"path/filepath"
"syscall"
"github.com/docker/libcontainer/label"
"github.com/docker/libcontainer/mount/nodes"
)
// default mount point flags
const defaultMountFlags = syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
type mount struct {
source string
path string
device string
flags int
data string
}
// InitializeMountNamespace sets up the devices, mount points, and filesystems for use inside a
// new mount namespace.
func InitializeMountNamespace(rootfs, console string, sysReadonly bool, hostRootUid, hostRootGid int, mountConfig *MountConfig) error {
var (
err error
flag = syscall.MS_PRIVATE
)
if mountConfig.NoPivotRoot {
flag = syscall.MS_SLAVE
}
if err := syscall.Mount("", "/", "", uintptr(flag|syscall.MS_REC), ""); err != nil {
return fmt.Errorf("mounting / with flags %X %s", (flag | syscall.MS_REC), err)
}
if err := syscall.Mount(rootfs, rootfs, "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
return fmt.Errorf("mouting %s as bind %s", rootfs, err)
}
if err := mountSystem(rootfs, sysReadonly, mountConfig); err != nil {
return fmt.Errorf("mount system %s", err)
}
// apply any user specified mounts within the new mount namespace
for _, m := range mountConfig.Mounts {
if err := m.Mount(rootfs, mountConfig.MountLabel); err != nil {
return err
}
}
if err := nodes.CreateDeviceNodes(rootfs, mountConfig.DeviceNodes); err != nil {
return fmt.Errorf("create device nodes %s", err)
}
if err := SetupPtmx(rootfs, console, mountConfig.MountLabel, hostRootUid, hostRootGid); err != nil {
return err
}
// stdin, stdout and stderr could be pointing to /dev/null from parent namespace.
// Re-open them inside this namespace.
// FIXME: Need to fix this for user namespaces.
if hostRootUid == 0 {
if err := reOpenDevNull(rootfs); err != nil {
return fmt.Errorf("Failed to reopen /dev/null %s", err)
}
}
if err := setupDevSymlinks(rootfs); err != nil {
return fmt.Errorf("dev symlinks %s", err)
}
if err := syscall.Chdir(rootfs); err != nil {
return fmt.Errorf("chdir into %s %s", rootfs, err)
}
if mountConfig.NoPivotRoot {
err = MsMoveRoot(rootfs)
} else {
err = PivotRoot(rootfs, mountConfig.PivotDir)
}
if err != nil {
return err
}
if mountConfig.ReadonlyFs {
if err := SetReadonly(); err != nil {
return fmt.Errorf("set readonly %s", err)
}
}
syscall.Umask(0022)
return nil
}
// mountSystem sets up linux specific system mounts like mqueue, sys, proc, shm, and devpts
// inside the mount namespace
func mountSystem(rootfs string, sysReadonly bool, mountConfig *MountConfig) error {
for _, m := range newSystemMounts(rootfs, mountConfig.MountLabel, sysReadonly) {
if err := os.MkdirAll(m.path, 0755); err != nil && !os.IsExist(err) {
return fmt.Errorf("mkdirall %s %s", m.path, err)
}
if err := syscall.Mount(m.source, m.path, m.device, uintptr(m.flags), m.data); err != nil {
return fmt.Errorf("mounting %s into %s %s", m.source, m.path, err)
}
}
return nil
}
func createIfNotExists(path string, isDir bool) error {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
if isDir {
if err := os.MkdirAll(path, 0755); err != nil {
return err
}
} else {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
f, err := os.OpenFile(path, os.O_CREATE, 0755)
if err != nil {
return err
}
f.Close()
}
}
}
return nil
}
func setupDevSymlinks(rootfs string) error {
var links = [][2]string{
{"/proc/self/fd", "/dev/fd"},
{"/proc/self/fd/0", "/dev/stdin"},
{"/proc/self/fd/1", "/dev/stdout"},
{"/proc/self/fd/2", "/dev/stderr"},
}
// kcore support can be toggled with CONFIG_PROC_KCORE; only create a symlink
// in /dev if it exists in /proc.
if _, err := os.Stat("/proc/kcore"); err == nil {
links = append(links, [2]string{"/proc/kcore", "/dev/kcore"})
}
for _, link := range links {
var (
src = link[0]
dst = filepath.Join(rootfs, link[1])
)
if err := os.Symlink(src, dst); err != nil && !os.IsExist(err) {
return fmt.Errorf("symlink %s %s %s", src, dst, err)
}
}
return nil
}
// TODO: this is crappy right now and should be cleaned up with a better way of handling system and
// standard bind mounts allowing them to be more dynamic
func newSystemMounts(rootfs, mountLabel string, sysReadonly bool) []mount {
systemMounts := []mount{
{source: "proc", path: filepath.Join(rootfs, "proc"), device: "proc", flags: defaultMountFlags},
{source: "tmpfs", path: filepath.Join(rootfs, "dev"), device: "tmpfs", flags: syscall.MS_NOSUID | syscall.MS_STRICTATIME, data: label.FormatMountLabel("mode=755", mountLabel)},
{source: "shm", path: filepath.Join(rootfs, "dev", "shm"), device: "tmpfs", flags: defaultMountFlags, data: label.FormatMountLabel("mode=1777,size=65536k", mountLabel)},
{source: "mqueue", path: filepath.Join(rootfs, "dev", "mqueue"), device: "mqueue", flags: defaultMountFlags},
{source: "devpts", path: filepath.Join(rootfs, "dev", "pts"), device: "devpts", flags: syscall.MS_NOSUID | syscall.MS_NOEXEC, data: label.FormatMountLabel("newinstance,ptmxmode=0666,mode=620,gid=5", mountLabel)},
}
sysMountFlags := defaultMountFlags
if sysReadonly {
sysMountFlags |= syscall.MS_RDONLY
}
systemMounts = append(systemMounts, mount{source: "sysfs", path: filepath.Join(rootfs, "sys"), device: "sysfs", flags: sysMountFlags})
return systemMounts
}
// Is stdin, stdout or stderr were to be pointing to '/dev/null',
// this method will make them point to '/dev/null' from within this namespace.
func reOpenDevNull(rootfs string) error {
var stat, devNullStat syscall.Stat_t
file, err := os.Open(filepath.Join(rootfs, "/dev/null"))
if err != nil {
return fmt.Errorf("Failed to open /dev/null - %s", err)
}
defer file.Close()
if err = syscall.Fstat(int(file.Fd()), &devNullStat); err != nil {
return fmt.Errorf("Failed to stat /dev/null - %s", err)
}
for fd := 0; fd < 3; fd++ {
if err = syscall.Fstat(fd, &stat); err != nil {
return fmt.Errorf("Failed to stat fd %d - %s", fd, err)
}
if stat.Rdev == devNullStat.Rdev {
// Close and re-open the fd.
if err = syscall.Dup2(int(file.Fd()), fd); err != nil {
return fmt.Errorf("Failed to dup fd %d to fd %d - %s", file.Fd(), fd, err)
}
}
}
return nil
}

View File

@ -1,109 +0,0 @@
package mount
import (
"fmt"
"os"
"path/filepath"
"syscall"
"github.com/docker/docker/pkg/symlink"
"github.com/docker/libcontainer/label"
)
type Mount struct {
Type string `json:"type,omitempty"`
Source string `json:"source,omitempty"` // Source path, in the host namespace
Destination string `json:"destination,omitempty"` // Destination path, in the container
Writable bool `json:"writable,omitempty"`
Relabel string `json:"relabel,omitempty"` // Relabel source if set, "z" indicates shared, "Z" indicates unshared
Private bool `json:"private,omitempty"`
Slave bool `json:"slave,omitempty"`
}
func (m *Mount) Mount(rootfs, mountLabel string) error {
switch m.Type {
case "bind":
return m.bindMount(rootfs, mountLabel)
case "tmpfs":
return m.tmpfsMount(rootfs, mountLabel)
default:
return fmt.Errorf("unsupported mount type %s for %s", m.Type, m.Destination)
}
}
func (m *Mount) bindMount(rootfs, mountLabel string) error {
var (
flags = syscall.MS_BIND | syscall.MS_REC
dest = filepath.Join(rootfs, m.Destination)
)
if !m.Writable {
flags = flags | syscall.MS_RDONLY
}
if m.Slave {
flags = flags | syscall.MS_SLAVE
}
stat, err := os.Stat(m.Source)
if err != nil {
return err
}
// FIXME: (crosbymichael) This does not belong here and should be done a layer above
dest, err = symlink.FollowSymlinkInScope(dest, rootfs)
if err != nil {
return err
}
if err := createIfNotExists(dest, stat.IsDir()); err != nil {
return fmt.Errorf("creating new bind mount target %s", err)
}
if err := syscall.Mount(m.Source, dest, "bind", uintptr(flags), ""); err != nil {
return fmt.Errorf("mounting %s into %s %s", m.Source, dest, err)
}
if !m.Writable {
if err := syscall.Mount(m.Source, dest, "bind", uintptr(flags|syscall.MS_REMOUNT), ""); err != nil {
return fmt.Errorf("remounting %s into %s %s", m.Source, dest, err)
}
}
if m.Relabel != "" {
if err := label.Relabel(m.Source, mountLabel, m.Relabel); err != nil {
return fmt.Errorf("relabeling %s to %s %s", m.Source, mountLabel, err)
}
}
if m.Private {
if err := syscall.Mount("", dest, "none", uintptr(syscall.MS_PRIVATE), ""); err != nil {
return fmt.Errorf("mounting %s private %s", dest, err)
}
}
return nil
}
func (m *Mount) tmpfsMount(rootfs, mountLabel string) error {
var (
err error
l = label.FormatMountLabel("", mountLabel)
dest = filepath.Join(rootfs, m.Destination)
)
// FIXME: (crosbymichael) This does not belong here and should be done a layer above
if dest, err = symlink.FollowSymlinkInScope(dest, rootfs); err != nil {
return err
}
if err := createIfNotExists(dest, true); err != nil {
return fmt.Errorf("creating new tmpfs mount target %s", err)
}
if err := syscall.Mount("tmpfs", dest, "tmpfs", uintptr(defaultMountFlags), l); err != nil {
return fmt.Errorf("%s mounting %s in tmpfs", err, dest)
}
return nil
}

View File

@ -1,33 +0,0 @@
package mount
import (
"errors"
"github.com/docker/libcontainer/devices"
)
var ErrUnsupported = errors.New("Unsupported method")
type MountConfig struct {
// NoPivotRoot will use MS_MOVE and a chroot to jail the process into the container's rootfs
// This is a common option when the container is running in ramdisk
NoPivotRoot bool `json:"no_pivot_root,omitempty"`
// PivotDir allows a custom directory inside the container's root filesystem to be used as pivot, when NoPivotRoot is not set.
// When a custom PivotDir not set, a temporary dir inside the root filesystem will be used. The pivot dir needs to be writeable.
// This is required when using read only root filesystems. In these cases, a read/writeable path can be (bind) mounted somewhere inside the root filesystem to act as pivot.
PivotDir string `json:"pivot_dir,omitempty"`
// ReadonlyFs will remount the container's rootfs as readonly where only externally mounted
// bind mounts are writtable
ReadonlyFs bool `json:"readonly_fs,omitempty"`
// Mounts specify additional source and destination paths that will be mounted inside the container's
// rootfs and mount namespace if specified
Mounts []*Mount `json:"mounts,omitempty"`
// The device nodes that should be automatically created within the container upon container start. Note, make sure that the node is marked as allowed in the cgroup as well!
DeviceNodes []*devices.Device `json:"device_nodes,omitempty"`
MountLabel string `json:"mount_label,omitempty"`
}

View File

@ -1,20 +0,0 @@
// +build linux
package mount
import (
"fmt"
"syscall"
)
func MsMoveRoot(rootfs string) error {
if err := syscall.Mount(rootfs, "/", "", syscall.MS_MOVE, ""); err != nil {
return fmt.Errorf("mount move %s into / %s", rootfs, err)
}
if err := syscall.Chroot("."); err != nil {
return fmt.Errorf("chroot . %s", err)
}
return syscall.Chdir("/")
}

View File

@ -1,57 +0,0 @@
// +build linux
package nodes
import (
"fmt"
"os"
"path/filepath"
"syscall"
"github.com/docker/libcontainer/devices"
)
// Create the device nodes in the container.
func CreateDeviceNodes(rootfs string, nodesToCreate []*devices.Device) error {
oldMask := syscall.Umask(0000)
defer syscall.Umask(oldMask)
for _, node := range nodesToCreate {
if err := CreateDeviceNode(rootfs, node); err != nil {
return err
}
}
return nil
}
// Creates the device node in the rootfs of the container.
func CreateDeviceNode(rootfs string, node *devices.Device) error {
var (
dest = filepath.Join(rootfs, node.Path)
parent = filepath.Dir(dest)
)
if err := os.MkdirAll(parent, 0755); err != nil {
return err
}
fileMode := node.FileMode
switch node.Type {
case 'c':
fileMode |= syscall.S_IFCHR
case 'b':
fileMode |= syscall.S_IFBLK
default:
return fmt.Errorf("%c is not a valid device type for device %s", node.Type, node.Path)
}
if err := syscall.Mknod(dest, uint32(fileMode), devices.Mkdev(node.MajorNumber, node.MinorNumber)); err != nil && !os.IsExist(err) {
return fmt.Errorf("mknod %s %s", node.Path, err)
}
if err := syscall.Chown(dest, int(node.Uid), int(node.Gid)); err != nil {
return fmt.Errorf("chown %s to %d:%d", node.Path, node.Uid, node.Gid)
}
return nil
}

View File

@ -1,13 +0,0 @@
// +build !linux
package nodes
import (
"errors"
"github.com/docker/libcontainer/devices"
)
func CreateDeviceNodes(rootfs string, nodesToCreate []*devices.Device) error {
return errors.New("Unsupported method")
}

View File

@ -1,41 +0,0 @@
// +build linux
package mount
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"syscall"
)
func PivotRoot(rootfs, pivotBaseDir string) error {
if pivotBaseDir == "" {
pivotBaseDir = "/"
}
tmpDir := filepath.Join(rootfs, pivotBaseDir)
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("can't create tmp dir %s, error %v", tmpDir, err)
}
pivotDir, err := ioutil.TempDir(tmpDir, ".pivot_root")
if err != nil {
return fmt.Errorf("can't create pivot_root dir %s, error %v", pivotDir, err)
}
if err := syscall.PivotRoot(rootfs, pivotDir); err != nil {
return fmt.Errorf("pivot_root %s", err)
}
if err := syscall.Chdir("/"); err != nil {
return fmt.Errorf("chdir / %s", err)
}
// path to pivot dir now changed, update
pivotDir = filepath.Join(pivotBaseDir, filepath.Base(pivotDir))
if err := syscall.Unmount(pivotDir, syscall.MNT_DETACH); err != nil {
return fmt.Errorf("unmount pivot_root dir %s", err)
}
return os.Remove(pivotDir)
}

View File

@ -1,30 +0,0 @@
// +build linux
package mount
import (
"fmt"
"os"
"path/filepath"
"github.com/docker/libcontainer/console"
)
func SetupPtmx(rootfs, consolePath, mountLabel string, hostRootUid, hostRootGid int) error {
ptmx := filepath.Join(rootfs, "dev/ptmx")
if err := os.Remove(ptmx); err != nil && !os.IsNotExist(err) {
return err
}
if err := os.Symlink("pts/ptmx", ptmx); err != nil {
return fmt.Errorf("symlink dev ptmx %s", err)
}
if consolePath != "" {
if err := console.Setup(rootfs, consolePath, mountLabel, hostRootUid, hostRootGid); err != nil {
return err
}
}
return nil
}

View File

@ -1,11 +0,0 @@
// +build linux
package mount
import (
"syscall"
)
func SetReadonly() error {
return syscall.Mount("/", "/", "bind", syscall.MS_BIND|syscall.MS_REMOUNT|syscall.MS_RDONLY|syscall.MS_REC, "")
}

View File

@ -1,31 +0,0 @@
// +build linux
package mount
import "syscall"
func RemountProc() error {
if err := syscall.Unmount("/proc", syscall.MNT_DETACH); err != nil {
return err
}
if err := syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), ""); err != nil {
return err
}
return nil
}
func RemountSys() error {
if err := syscall.Unmount("/sys", syscall.MNT_DETACH); err != nil {
if err != syscall.EINVAL {
return err
}
} else {
if err := syscall.Mount("sysfs", "/sys", "sysfs", uintptr(defaultMountFlags), ""); err != nil {
return err
}
}
return nil
}

View File

@ -1,11 +0,0 @@
package namespaces
import (
"os"
"os/exec"
"github.com/docker/libcontainer"
)
type CreateCommand func(container *libcontainer.Config, console, dataPath, init string, childPipe *os.File, args []string) *exec.Cmd
type SetupCommand func(container *libcontainer.Config, console, dataPath, init string) *exec.Cmd

View File

@ -1,349 +0,0 @@
// +build linux
package namespaces
import (
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"syscall"
"github.com/docker/libcontainer"
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/cgroups/fs"
"github.com/docker/libcontainer/cgroups/systemd"
"github.com/docker/libcontainer/network"
"github.com/docker/libcontainer/system"
)
const (
EXIT_SIGNAL_OFFSET = 128
)
// TODO(vishh): This is part of the libcontainer API and it does much more than just namespaces related work.
// Move this to libcontainer package.
// Exec performs setup outside of a namespace so that a container can be
// executed. Exec is a high level function for working with container namespaces.
func Exec(container *libcontainer.Config, stdin io.Reader, stdout, stderr io.Writer, console, dataPath string, args []string, createCommand CreateCommand, setupCommand SetupCommand, startCallback func()) (int, error) {
var err error
// create a pipe so that we can syncronize with the namespaced process and
// pass the state and configuration to the child process
parent, child, err := newInitPipe()
if err != nil {
return -1, err
}
defer parent.Close()
command := createCommand(container, console, dataPath, os.Args[0], child, args)
// Note: these are only used in non-tty mode
// if there is a tty for the container it will be opened within the namespace and the
// fds will be duped to stdin, stdiout, and stderr
command.Stdin = stdin
command.Stdout = stdout
command.Stderr = stderr
if err := command.Start(); err != nil {
child.Close()
return -1, err
}
child.Close()
wait := func() (*os.ProcessState, error) {
ps, err := command.Process.Wait()
// we should kill all processes in cgroup when init is died if we use
// host PID namespace
if !container.Namespaces.Contains(libcontainer.NEWPID) {
killAllPids(container)
}
return ps, err
}
terminate := func(terr error) (int, error) {
// TODO: log the errors for kill and wait
command.Process.Kill()
wait()
return -1, terr
}
started, err := system.GetProcessStartTime(command.Process.Pid)
if err != nil {
return terminate(err)
}
// Do this before syncing with child so that no children
// can escape the cgroup
cgroupPaths, err := SetupCgroups(container, command.Process.Pid)
if err != nil {
return terminate(err)
}
defer cgroups.RemovePaths(cgroupPaths)
var networkState network.NetworkState
if err := InitializeNetworking(container, command.Process.Pid, &networkState); err != nil {
return terminate(err)
}
state := &libcontainer.State{
InitPid: command.Process.Pid,
InitStartTime: started,
NetworkState: networkState,
CgroupPaths: cgroupPaths,
}
if err := libcontainer.SaveState(dataPath, state); err != nil {
return terminate(err)
}
defer libcontainer.DeleteState(dataPath)
// Start the setup process to setup the init process
if container.Namespaces.Contains(libcontainer.NEWUSER) {
setupCmd := setupCommand(container, console, dataPath, os.Args[0])
output, err := setupCmd.CombinedOutput()
if err != nil || setupCmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() != 0 {
command.Process.Kill()
wait()
return -1, fmt.Errorf("setup failed: %s %s", err, output)
}
}
// send the state to the container's init process then shutdown writes for the parent
if err := json.NewEncoder(parent).Encode(networkState); err != nil {
return terminate(err)
}
// shutdown writes for the parent side of the pipe
if err := syscall.Shutdown(int(parent.Fd()), syscall.SHUT_WR); err != nil {
return terminate(err)
}
// wait for the child process to fully complete and receive an error message
// if one was encoutered
var ierr *initError
if err := json.NewDecoder(parent).Decode(&ierr); err != nil && err != io.EOF {
return terminate(err)
}
if ierr != nil {
return terminate(ierr)
}
if startCallback != nil {
startCallback()
}
ps, err := wait()
if err != nil {
if _, ok := err.(*exec.ExitError); !ok {
return -1, err
}
}
// waiting for pipe flushing
command.Wait()
waitStatus := ps.Sys().(syscall.WaitStatus)
if waitStatus.Signaled() {
return EXIT_SIGNAL_OFFSET + int(waitStatus.Signal()), nil
}
return waitStatus.ExitStatus(), nil
}
// killAllPids iterates over all of the container's processes
// sending a SIGKILL to each process.
func killAllPids(container *libcontainer.Config) error {
var (
procs []*os.Process
freeze = fs.Freeze
getPids = fs.GetPids
)
if systemd.UseSystemd() {
freeze = systemd.Freeze
getPids = systemd.GetPids
}
freeze(container.Cgroups, cgroups.Frozen)
pids, err := getPids(container.Cgroups)
if err != nil {
return err
}
for _, pid := range pids {
// TODO: log err without aborting if we are unable to find
// a single PID
if p, err := os.FindProcess(pid); err == nil {
procs = append(procs, p)
p.Kill()
}
}
freeze(container.Cgroups, cgroups.Thawed)
for _, p := range procs {
p.Wait()
}
return err
}
// Utility function that gets a host ID for a container ID from user namespace map
// if that ID is present in the map.
func hostIDFromMapping(containerID int, uMap []libcontainer.IDMap) (int, bool) {
for _, m := range uMap {
if (containerID >= m.ContainerID) && (containerID <= (m.ContainerID + m.Size - 1)) {
hostID := m.HostID + (containerID - m.ContainerID)
return hostID, true
}
}
return -1, false
}
// Gets the root gid for the process on host which could be non-zero
// when user namespaces are enabled.
func GetHostRootGid(container *libcontainer.Config) (int, error) {
if container.Namespaces.Contains(libcontainer.NEWUSER) {
if container.GidMappings == nil {
return -1, fmt.Errorf("User namespaces enabled, but no gid mappings found.")
}
hostRootGid, found := hostIDFromMapping(0, container.GidMappings)
if !found {
return -1, fmt.Errorf("User namespaces enabled, but no root user mapping found.")
}
return hostRootGid, nil
}
// Return default root gid 0
return 0, nil
}
// Gets the root uid for the process on host which could be non-zero
// when user namespaces are enabled.
func GetHostRootUid(container *libcontainer.Config) (int, error) {
if container.Namespaces.Contains(libcontainer.NEWUSER) {
if container.UidMappings == nil {
return -1, fmt.Errorf("User namespaces enabled, but no uid mappings found.")
}
hostRootUid, found := hostIDFromMapping(0, container.UidMappings)
if !found {
return -1, fmt.Errorf("User namespaces enabled, but no root user mapping found.")
}
return hostRootUid, nil
}
// Return default root uid 0
return 0, nil
}
// Converts IDMap to SysProcIDMap array and adds it to SysProcAttr.
func AddUidGidMappings(sys *syscall.SysProcAttr, container *libcontainer.Config) {
if container.UidMappings != nil {
sys.UidMappings = make([]syscall.SysProcIDMap, len(container.UidMappings))
for i, um := range container.UidMappings {
sys.UidMappings[i].ContainerID = um.ContainerID
sys.UidMappings[i].HostID = um.HostID
sys.UidMappings[i].Size = um.Size
}
}
if container.GidMappings != nil {
sys.GidMappings = make([]syscall.SysProcIDMap, len(container.GidMappings))
for i, gm := range container.GidMappings {
sys.GidMappings[i].ContainerID = gm.ContainerID
sys.GidMappings[i].HostID = gm.HostID
sys.GidMappings[i].Size = gm.Size
}
}
}
// DefaultCreateCommand will return an exec.Cmd with the Cloneflags set to the proper namespaces
// defined on the container's configuration and use the current binary as the init with the
// args provided
//
// console: the /dev/console to setup inside the container
// init: the program executed inside the namespaces
// dataPath: the path to the directory under which the container's state file is stored
// pipe: sync pipe to synchronize the parent and child processes
// args: the arguments to pass to the container to run as the user's program
func DefaultCreateCommand(container *libcontainer.Config, console, dataPath, init string, pipe *os.File, args []string) *exec.Cmd {
// get our binary name from arg0 so we can always reexec ourself
env := []string{
"console=" + console,
"pipe=3",
"data_path=" + dataPath,
}
command := exec.Command(init, append([]string{"init", "--"}, args...)...)
// make sure the process is executed inside the context of the rootfs
command.Dir = container.RootFs
command.Env = append(os.Environ(), env...)
if command.SysProcAttr == nil {
command.SysProcAttr = &syscall.SysProcAttr{}
}
command.SysProcAttr.Cloneflags = uintptr(GetNamespaceFlags(container.Namespaces))
command.SysProcAttr.Pdeathsig = syscall.SIGKILL
command.ExtraFiles = []*os.File{pipe}
if container.Namespaces.Contains(libcontainer.NEWUSER) {
AddUidGidMappings(command.SysProcAttr, container)
// Default to root user when user namespaces are enabled.
if command.SysProcAttr.Credential == nil {
command.SysProcAttr.Credential = &syscall.Credential{}
}
}
return command
}
// DefaultSetupCommand will return an exec.Cmd that joins the init process to set it up.
//
// console: the /dev/console to setup inside the container
// dataPath: the path to the directory under which the container's state file is stored
// init: the program executed inside the namespaces
func DefaultSetupCommand(container *libcontainer.Config, console, dataPath, init string) *exec.Cmd {
env := []string{
"console=" + console,
"data_path=" + dataPath,
}
if dataPath == "" {
dataPath, _ = os.Getwd()
}
if container.RootFs == "" {
container.RootFs, _ = os.Getwd()
}
args := []string{dataPath, container.RootFs, console}
command := exec.Command(init, append([]string{"exec", "--func", "setup", "--"}, args...)...)
// make sure the process is executed inside the context of the rootfs
command.Dir = container.RootFs
command.Env = append(os.Environ(), env...)
return command
}
// SetupCgroups applies the cgroup restrictions to the process running in the container based
// on the container's configuration
func SetupCgroups(container *libcontainer.Config, nspid int) (map[string]string, error) {
if container.Cgroups != nil {
c := container.Cgroups
if systemd.UseSystemd() {
return systemd.Apply(c, nspid)
}
return fs.Apply(c, nspid)
}
return map[string]string{}, nil
}
// InitializeNetworking creates the container's network stack outside of the namespace and moves
// interfaces into the container's net namespaces if necessary
func InitializeNetworking(container *libcontainer.Config, nspid int, networkState *network.NetworkState) error {
for _, config := range container.Networks {
strategy, err := network.GetStrategy(config.Type)
if err != nil {
return err
}
if err := strategy.Create((*network.Network)(config), nspid, networkState); err != nil {
return err
}
}
return nil
}

View File

@ -1,201 +0,0 @@
// +build linux
package namespaces
import (
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"syscall"
"github.com/docker/libcontainer"
"github.com/docker/libcontainer/apparmor"
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/label"
"github.com/docker/libcontainer/mount"
"github.com/docker/libcontainer/system"
"github.com/docker/libcontainer/utils"
)
// ExecIn reexec's the initPath with the argv 0 rewrite to "nsenter" so that it is able to run the
// setns code in a single threaded environment joining the existing containers' namespaces.
func ExecIn(container *libcontainer.Config, state *libcontainer.State, userArgs []string, initPath, action string,
stdin io.Reader, stdout, stderr io.Writer, console string, startCallback func(*exec.Cmd)) (int, error) {
args := []string{fmt.Sprintf("nsenter-%s", action), "--nspid", strconv.Itoa(state.InitPid)}
if console != "" {
args = append(args, "--console", console)
}
cmd := &exec.Cmd{
Path: initPath,
Args: append(args, append([]string{"--"}, userArgs...)...),
}
if filepath.Base(initPath) == initPath {
if lp, err := exec.LookPath(initPath); err == nil {
cmd.Path = lp
}
}
parent, child, err := newInitPipe()
if err != nil {
return -1, err
}
defer parent.Close()
// Note: these are only used in non-tty mode
// if there is a tty for the container it will be opened within the namespace and the
// fds will be duped to stdin, stdiout, and stderr
cmd.Stdin = stdin
cmd.Stdout = stdout
cmd.Stderr = stderr
cmd.ExtraFiles = []*os.File{child}
if err := cmd.Start(); err != nil {
child.Close()
return -1, err
}
child.Close()
terminate := func(terr error) (int, error) {
// TODO: log the errors for kill and wait
cmd.Process.Kill()
cmd.Wait()
return -1, terr
}
// Enter cgroups.
if err := EnterCgroups(state, cmd.Process.Pid); err != nil {
return terminate(err)
}
// finish cgroups' setup, unblock the child process.
if _, err := parent.WriteString("1"); err != nil {
return terminate(err)
}
if err := json.NewEncoder(parent).Encode(container); err != nil {
return terminate(err)
}
if startCallback != nil {
startCallback(cmd)
}
if err := cmd.Wait(); err != nil {
if _, ok := err.(*exec.ExitError); !ok {
return -1, err
}
}
return cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus(), nil
}
// Finalize expects that the setns calls have been setup and that is has joined an
// existing namespace
func FinalizeSetns(container *libcontainer.Config, args []string) error {
// clear the current processes env and replace it with the environment defined on the container
if err := LoadContainerEnvironment(container); err != nil {
return err
}
if err := setupRlimits(container); err != nil {
return fmt.Errorf("setup rlimits %s", err)
}
if err := FinalizeNamespace(container); err != nil {
return err
}
if err := apparmor.ApplyProfile(container.AppArmorProfile); err != nil {
return fmt.Errorf("set apparmor profile %s: %s", container.AppArmorProfile, err)
}
if container.ProcessLabel != "" {
if err := label.SetProcessLabel(container.ProcessLabel); err != nil {
return err
}
}
if err := system.Execv(args[0], args[0:], os.Environ()); err != nil {
return err
}
panic("unreachable")
}
// SetupContainer is run to setup mounts and networking related operations
// for a user namespace enabled process as a user namespace root doesn't
// have permissions to perform these operations.
// The setup process joins all the namespaces of user namespace enabled init
// except the user namespace, so it run as root in the root user namespace
// to perform these operations.
func SetupContainer(container *libcontainer.Config, dataPath, uncleanRootfs, consolePath string) error {
rootfs, err := utils.ResolveRootfs(uncleanRootfs)
if err != nil {
return err
}
// clear the current processes env and replace it with the environment
// defined on the container
if err := LoadContainerEnvironment(container); err != nil {
return err
}
state, err := libcontainer.GetState(dataPath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("unable to read state: %s", err)
}
cloneFlags := GetNamespaceFlags(container.Namespaces)
if (cloneFlags & syscall.CLONE_NEWNET) == 0 {
if len(container.Networks) != 0 || len(container.Routes) != 0 {
return fmt.Errorf("unable to apply network parameters without network namespace")
}
} else {
if err := setupNetwork(container, &state.NetworkState); err != nil {
return fmt.Errorf("setup networking %s", err)
}
if err := setupRoute(container); err != nil {
return fmt.Errorf("setup route %s", err)
}
}
label.Init()
hostRootUid, err := GetHostRootUid(container)
if err != nil {
return fmt.Errorf("failed to get hostRootUid %s", err)
}
hostRootGid, err := GetHostRootGid(container)
if err != nil {
return fmt.Errorf("failed to get hostRootGid %s", err)
}
// InitializeMountNamespace() can be executed only for a new mount namespace
if (cloneFlags & syscall.CLONE_NEWNS) == 0 {
if container.MountConfig != nil {
return fmt.Errorf("mount config is set without mount namespace")
}
} else if err := mount.InitializeMountNamespace(rootfs,
consolePath,
container.RestrictSys,
hostRootUid,
hostRootGid,
(*mount.MountConfig)(container.MountConfig)); err != nil {
return fmt.Errorf("setup mount namespace %s", err)
}
return nil
}
func EnterCgroups(state *libcontainer.State, pid int) error {
return cgroups.EnterPid(state.CgroupPaths, pid)
}

View File

@ -1,444 +0,0 @@
// +build linux
package namespaces
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"syscall"
"github.com/docker/libcontainer"
"github.com/docker/libcontainer/apparmor"
"github.com/docker/libcontainer/console"
"github.com/docker/libcontainer/label"
"github.com/docker/libcontainer/mount"
"github.com/docker/libcontainer/netlink"
"github.com/docker/libcontainer/network"
"github.com/docker/libcontainer/security/capabilities"
"github.com/docker/libcontainer/security/restrict"
"github.com/docker/libcontainer/system"
"github.com/docker/libcontainer/user"
"github.com/docker/libcontainer/utils"
)
// TODO(vishh): This is part of the libcontainer API and it does much more than just namespaces related work.
// Move this to libcontainer package.
// Init is the init process that first runs inside a new namespace to setup mounts, users, networking,
// and other options required for the new container.
// The caller of Init function has to ensure that the go runtime is locked to an OS thread
// (using runtime.LockOSThread) else system calls like setns called within Init may not work as intended.
func Init(container *libcontainer.Config, uncleanRootfs, consolePath string, pipe *os.File, args []string) (err error) {
defer func() {
// if we have an error during the initialization of the container's init then send it back to the
// parent process in the form of an initError.
if err != nil {
// ensure that any data sent from the parent is consumed so it doesn't
// receive ECONNRESET when the child writes to the pipe.
ioutil.ReadAll(pipe)
if err := json.NewEncoder(pipe).Encode(initError{
Message: err.Error(),
}); err != nil {
panic(err)
}
}
// ensure that this pipe is always closed
pipe.Close()
}()
if container.Namespaces.Contains(libcontainer.NEWUSER) {
return initUserNs(container, uncleanRootfs, consolePath, pipe, args)
} else {
return initDefault(container, uncleanRootfs, consolePath, pipe, args)
}
}
func initDefault(container *libcontainer.Config, uncleanRootfs, consolePath string, pipe *os.File, args []string) (err error) {
rootfs, err := utils.ResolveRootfs(uncleanRootfs)
if err != nil {
return err
}
// clear the current processes env and replace it with the environment
// defined on the container
if err := LoadContainerEnvironment(container); err != nil {
return err
}
// We always read this as it is a way to sync with the parent as well
var networkState *network.NetworkState
if err := json.NewDecoder(pipe).Decode(&networkState); err != nil {
return err
}
// join any namespaces via a path to the namespace fd if provided
if err := joinExistingNamespaces(container.Namespaces); err != nil {
return err
}
if consolePath != "" {
if err := console.OpenAndDup(consolePath); err != nil {
return err
}
}
if _, err := syscall.Setsid(); err != nil {
return fmt.Errorf("setsid %s", err)
}
if consolePath != "" {
if err := system.Setctty(); err != nil {
return fmt.Errorf("setctty %s", err)
}
}
cloneFlags := GetNamespaceFlags(container.Namespaces)
if (cloneFlags & syscall.CLONE_NEWNET) == 0 {
if len(container.Networks) != 0 || len(container.Routes) != 0 {
return fmt.Errorf("unable to apply network parameters without network namespace")
}
} else {
if err := setupNetwork(container, networkState); err != nil {
return fmt.Errorf("setup networking %s", err)
}
if err := setupRoute(container); err != nil {
return fmt.Errorf("setup route %s", err)
}
}
if err := setupRlimits(container); err != nil {
return fmt.Errorf("setup rlimits %s", err)
}
label.Init()
// InitializeMountNamespace() can be executed only for a new mount namespace
if (cloneFlags & syscall.CLONE_NEWNS) == 0 {
if container.MountConfig != nil {
return fmt.Errorf("mount config is set without mount namespace")
}
} else if err := mount.InitializeMountNamespace(rootfs,
consolePath,
container.RestrictSys,
0, // Default Root Uid
0, // Default Root Gid
(*mount.MountConfig)(container.MountConfig)); err != nil {
return fmt.Errorf("setup mount namespace %s", err)
}
if container.Hostname != "" {
if (cloneFlags & syscall.CLONE_NEWUTS) == 0 {
return fmt.Errorf("unable to set the hostname without UTS namespace")
}
if err := syscall.Sethostname([]byte(container.Hostname)); err != nil {
return fmt.Errorf("unable to sethostname %q: %s", container.Hostname, err)
}
}
if err := apparmor.ApplyProfile(container.AppArmorProfile); err != nil {
return fmt.Errorf("set apparmor profile %s: %s", container.AppArmorProfile, err)
}
if err := label.SetProcessLabel(container.ProcessLabel); err != nil {
return fmt.Errorf("set process label %s", err)
}
// TODO: (crosbymichael) make this configurable at the Config level
if container.RestrictSys {
if (cloneFlags & syscall.CLONE_NEWNS) == 0 {
return fmt.Errorf("unable to restrict access to kernel files without mount namespace")
}
if err := restrict.Restrict("proc/sys", "proc/sysrq-trigger", "proc/irq", "proc/bus"); err != nil {
return err
}
}
pdeathSignal, err := system.GetParentDeathSignal()
if err != nil {
return fmt.Errorf("get parent death signal %s", err)
}
if err := FinalizeNamespace(container); err != nil {
return fmt.Errorf("finalize namespace %s", err)
}
// FinalizeNamespace can change user/group which clears the parent death
// signal, so we restore it here.
if err := RestoreParentDeathSignal(pdeathSignal); err != nil {
return fmt.Errorf("restore parent death signal %s", err)
}
return system.Execv(args[0], args[0:], os.Environ())
}
func initUserNs(container *libcontainer.Config, uncleanRootfs, consolePath string, pipe *os.File, args []string) (err error) {
// clear the current processes env and replace it with the environment
// defined on the container
if err := LoadContainerEnvironment(container); err != nil {
return err
}
// We always read this as it is a way to sync with the parent as well
var networkState *network.NetworkState
if err := json.NewDecoder(pipe).Decode(&networkState); err != nil {
return err
}
// join any namespaces via a path to the namespace fd if provided
if err := joinExistingNamespaces(container.Namespaces); err != nil {
return err
}
if consolePath != "" {
if err := console.OpenAndDup("/dev/console"); err != nil {
return err
}
}
if _, err := syscall.Setsid(); err != nil {
return fmt.Errorf("setsid %s", err)
}
if consolePath != "" {
if err := system.Setctty(); err != nil {
return fmt.Errorf("setctty %s", err)
}
}
if container.WorkingDir == "" {
container.WorkingDir = "/"
}
if err := setupRlimits(container); err != nil {
return fmt.Errorf("setup rlimits %s", err)
}
cloneFlags := GetNamespaceFlags(container.Namespaces)
if container.Hostname != "" {
if (cloneFlags & syscall.CLONE_NEWUTS) == 0 {
return fmt.Errorf("unable to set the hostname without UTS namespace")
}
if err := syscall.Sethostname([]byte(container.Hostname)); err != nil {
return fmt.Errorf("unable to sethostname %q: %s", container.Hostname, err)
}
}
if err := apparmor.ApplyProfile(container.AppArmorProfile); err != nil {
return fmt.Errorf("set apparmor profile %s: %s", container.AppArmorProfile, err)
}
if err := label.SetProcessLabel(container.ProcessLabel); err != nil {
return fmt.Errorf("set process label %s", err)
}
if container.RestrictSys {
if (cloneFlags & syscall.CLONE_NEWNS) == 0 {
return fmt.Errorf("unable to restrict access to kernel files without mount namespace")
}
if err := restrict.Restrict("proc/sys", "proc/sysrq-trigger", "proc/irq", "proc/bus"); err != nil {
return err
}
}
pdeathSignal, err := system.GetParentDeathSignal()
if err != nil {
return fmt.Errorf("get parent death signal %s", err)
}
if err := FinalizeNamespace(container); err != nil {
return fmt.Errorf("finalize namespace %s", err)
}
// FinalizeNamespace can change user/group which clears the parent death
// signal, so we restore it here.
if err := RestoreParentDeathSignal(pdeathSignal); err != nil {
return fmt.Errorf("restore parent death signal %s", err)
}
return system.Execv(args[0], args[0:], os.Environ())
}
// RestoreParentDeathSignal sets the parent death signal to old.
func RestoreParentDeathSignal(old int) error {
if old == 0 {
return nil
}
current, err := system.GetParentDeathSignal()
if err != nil {
return fmt.Errorf("get parent death signal %s", err)
}
if old == current {
return nil
}
if err := system.ParentDeathSignal(uintptr(old)); err != nil {
return fmt.Errorf("set parent death signal %s", err)
}
// Signal self if parent is already dead. Does nothing if running in a new
// PID namespace, as Getppid will always return 0.
if syscall.Getppid() == 1 {
return syscall.Kill(syscall.Getpid(), syscall.SIGKILL)
}
return nil
}
// SetupUser changes the groups, gid, and uid for the user inside the container
func SetupUser(container *libcontainer.Config) error {
// Set up defaults.
defaultExecUser := user.ExecUser{
Uid: syscall.Getuid(),
Gid: syscall.Getgid(),
Home: "/",
}
passwdPath, err := user.GetPasswdPath()
if err != nil {
return err
}
groupPath, err := user.GetGroupPath()
if err != nil {
return err
}
execUser, err := user.GetExecUserPath(container.User, &defaultExecUser, passwdPath, groupPath)
if err != nil {
return fmt.Errorf("get supplementary groups %s", err)
}
suppGroups := append(execUser.Sgids, container.AdditionalGroups...)
if err := syscall.Setgroups(suppGroups); err != nil {
return fmt.Errorf("setgroups %s", err)
}
if err := system.Setgid(execUser.Gid); err != nil {
return fmt.Errorf("setgid %s", err)
}
if err := system.Setuid(execUser.Uid); err != nil {
return fmt.Errorf("setuid %s", err)
}
// if we didn't get HOME already, set it based on the user's HOME
if envHome := os.Getenv("HOME"); envHome == "" {
if err := os.Setenv("HOME", execUser.Home); err != nil {
return fmt.Errorf("set HOME %s", err)
}
}
return nil
}
// setupVethNetwork uses the Network config if it is not nil to initialize
// the new veth interface inside the container for use by changing the name to eth0
// setting the MTU and IP address along with the default gateway
func setupNetwork(container *libcontainer.Config, networkState *network.NetworkState) error {
for _, config := range container.Networks {
strategy, err := network.GetStrategy(config.Type)
if err != nil {
return err
}
err1 := strategy.Initialize((*network.Network)(config), networkState)
if err1 != nil {
return err1
}
}
return nil
}
func setupRoute(container *libcontainer.Config) error {
for _, config := range container.Routes {
if err := netlink.AddRoute(config.Destination, config.Source, config.Gateway, config.InterfaceName); err != nil {
return err
}
}
return nil
}
func setupRlimits(container *libcontainer.Config) error {
for _, rlimit := range container.Rlimits {
l := &syscall.Rlimit{Max: rlimit.Hard, Cur: rlimit.Soft}
if err := syscall.Setrlimit(rlimit.Type, l); err != nil {
return fmt.Errorf("error setting rlimit type %v: %v", rlimit.Type, err)
}
}
return nil
}
// FinalizeNamespace drops the caps, sets the correct user
// and working dir, and closes any leaky file descriptors
// before execing the command inside the namespace
func FinalizeNamespace(container *libcontainer.Config) error {
// Ensure that all non-standard fds we may have accidentally
// inherited are marked close-on-exec so they stay out of the
// container
if err := utils.CloseExecFrom(3); err != nil {
return fmt.Errorf("close open file descriptors %s", err)
}
// drop capabilities in bounding set before changing user
if err := capabilities.DropBoundingSet(container.Capabilities); err != nil {
return fmt.Errorf("drop bounding set %s", err)
}
// preserve existing capabilities while we change users
if err := system.SetKeepCaps(); err != nil {
return fmt.Errorf("set keep caps %s", err)
}
if err := SetupUser(container); err != nil {
return fmt.Errorf("setup user %s", err)
}
if err := system.ClearKeepCaps(); err != nil {
return fmt.Errorf("clear keep caps %s", err)
}
// drop all other capabilities
if err := capabilities.DropCapabilities(container.Capabilities); err != nil {
return fmt.Errorf("drop capabilities %s", err)
}
if container.WorkingDir != "" {
if err := syscall.Chdir(container.WorkingDir); err != nil {
return fmt.Errorf("chdir to %s %s", container.WorkingDir, err)
}
}
return nil
}
func LoadContainerEnvironment(container *libcontainer.Config) error {
os.Clearenv()
for _, pair := range container.Env {
p := strings.SplitN(pair, "=", 2)
if len(p) < 2 {
return fmt.Errorf("invalid environment '%v'", pair)
}
if err := os.Setenv(p[0], p[1]); err != nil {
return err
}
}
return nil
}
// joinExistingNamespaces gets all the namespace paths specified for the container and
// does a setns on the namespace fd so that the current process joins the namespace.
func joinExistingNamespaces(namespaces []libcontainer.Namespace) error {
for _, ns := range namespaces {
if ns.Path != "" {
f, err := os.OpenFile(ns.Path, os.O_RDONLY, 0)
if err != nil {
return err
}
err = system.Setns(f.Fd(), uintptr(namespaceInfo[ns.Type]))
f.Close()
if err != nil {
return err
}
}
}
return nil
}

View File

@ -1,265 +0,0 @@
// +build cgo
//
// formated with indent -linux nsenter.c
#define _GNU_SOURCE
#include <sched.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/limits.h>
#include <linux/sched.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <getopt.h>
#define pr_perror(fmt, ...) fprintf(stderr, "nsenter: " fmt ": %m\n", ##__VA_ARGS__)
static const int kBufSize = 256;
static const char *kNsEnter = "nsenter";
void get_args(int *argc, char ***argv)
{
// Read argv
int fd = open("/proc/self/cmdline", O_RDONLY);
if (fd < 0) {
pr_perror("Unable to open /proc/self/cmdline");
exit(1);
}
// Read the whole commandline.
ssize_t contents_size = 0;
ssize_t contents_offset = 0;
char *contents = NULL;
ssize_t bytes_read = 0;
do {
contents_size += kBufSize;
contents = (char *)realloc(contents, contents_size);
bytes_read =
read(fd, contents + contents_offset,
contents_size - contents_offset);
if (bytes_read < 0) {
pr_perror("Unable to read from /proc/self/cmdline");
exit(1);
}
contents_offset += bytes_read;
}
while (bytes_read > 0);
close(fd);
// Parse the commandline into an argv. /proc/self/cmdline has \0 delimited args.
ssize_t i;
*argc = 0;
for (i = 0; i < contents_offset; i++) {
if (contents[i] == '\0') {
(*argc)++;
}
}
*argv = (char **)malloc(sizeof(char *) * ((*argc) + 1));
int idx;
for (idx = 0; idx < (*argc); idx++) {
(*argv)[idx] = contents;
contents += strlen(contents) + 1;
}
(*argv)[*argc] = NULL;
}
// Use raw setns syscall for versions of glibc that don't include it (namely glibc-2.12)
#if __GLIBC__ == 2 && __GLIBC_MINOR__ < 14
#define _GNU_SOURCE
#include <sched.h>
#include "syscall.h"
#ifdef SYS_setns
int setns(int fd, int nstype)
{
return syscall(SYS_setns, fd, nstype);
}
#endif
#endif
void print_usage()
{
fprintf(stderr,
"nsenter --nspid <pid> --console <console> -- cmd1 arg1 arg2...\n");
}
void child_handler(int sig)
{
while (waitpid(-1, NULL, WNOHANG) > 0) {
}
}
void nsenter()
{
int argc, c;
char **argv;
get_args(&argc, &argv);
// check argv 0 to ensure that we are supposed to setns
// we use strncmp to test for a value of "nsenter" but also allows alternate implmentations
// after the setns code path to continue to use the argv 0 to determine actions to be run
// resulting in the ability to specify "nsenter-mknod", "nsenter-exec", etc...
if (strncmp(argv[0], kNsEnter, strlen(kNsEnter)) != 0) {
return;
}
#ifdef PR_SET_CHILD_SUBREAPER
if (prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) == -1) {
pr_perror("Failed to set child subreaper");
exit(1);
}
// setup SIGCHLD handler to reap zombie processes
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_handler = &child_handler;
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
pr_perror("Failed to set SIGCHLD handler");
exit(1);
}
#endif
static const struct option longopts[] = {
{"nspid", required_argument, NULL, 'n'},
{"console", required_argument, NULL, 't'},
{NULL, 0, NULL, 0}
};
pid_t init_pid = -1;
char *init_pid_str = NULL;
char *console = NULL;
while ((c = getopt_long_only(argc, argv, "n:c:", longopts, NULL)) != -1) {
switch (c) {
case 'n':
init_pid_str = optarg;
break;
case 't':
console = optarg;
break;
}
}
if (init_pid_str == NULL) {
print_usage();
exit(1);
}
init_pid = strtol(init_pid_str, NULL, 10);
if ((init_pid == 0 && errno == EINVAL) || errno == ERANGE) {
pr_perror("Failed to parse PID from \"%s\" with output \"%d\"",
init_pid_str, init_pid);
print_usage();
exit(1);
}
argc -= 3;
argv += 3;
if (setsid() == -1) {
pr_perror("setsid failed");
exit(1);
}
// before we setns we need to dup the console
int consolefd = -1;
if (console != NULL) {
consolefd = open(console, O_RDWR);
if (consolefd < 0) {
pr_perror("Failed to open console %s", console);
exit(1);
}
}
// blocking until the parent placed the process inside correct cgroups.
unsigned char s;
if (read(3, &s, 1) != 1 || s != '1') {
pr_perror("failed to receive synchronization data from parent");
exit(1);
}
// Setns on all supported namespaces.
char ns_dir[PATH_MAX];
memset(ns_dir, 0, PATH_MAX);
snprintf(ns_dir, PATH_MAX - 1, "/proc/%d/ns/", init_pid);
int ns_dir_fd;
ns_dir_fd = open(ns_dir, O_RDONLY | O_DIRECTORY);
if (ns_dir_fd < 0) {
pr_perror("Unable to open %s", ns_dir);
exit(1);
}
char *namespaces[] = { "ipc", "uts", "net", "pid", "mnt" };
const int num = sizeof(namespaces) / sizeof(char *);
int i;
for (i = 0; i < num; i++) {
// A zombie process has links on namespaces, but they can't be opened
struct stat st;
if (fstatat(ns_dir_fd, namespaces[i], &st, AT_SYMLINK_NOFOLLOW)
== -1) {
if (errno == ENOENT)
continue;
pr_perror("Failed to stat ns file %s for ns %s",
ns_dir, namespaces[i]);
exit(1);
}
int fd = openat(ns_dir_fd, namespaces[i], O_RDONLY);
if (fd == -1) {
pr_perror("Failed to open ns file %s for ns %s",
ns_dir, namespaces[i]);
exit(1);
}
// Set the namespace.
if (setns(fd, 0) == -1) {
pr_perror("Failed to setns for %s", namespaces[i]);
exit(1);
}
close(fd);
}
close(ns_dir_fd);
// We must fork to actually enter the PID namespace.
int child = fork();
if (child == -1) {
pr_perror("Unable to fork a process");
exit(1);
}
if (child == 0) {
if (consolefd != -1) {
if (dup2(consolefd, STDIN_FILENO) != 0) {
pr_perror("Failed to dup 0");
exit(1);
}
if (dup2(consolefd, STDOUT_FILENO) != STDOUT_FILENO) {
pr_perror("Failed to dup 1");
exit(1);
}
if (dup2(consolefd, STDERR_FILENO) != STDERR_FILENO) {
pr_perror("Failed to dup 2\n");
exit(1);
}
}
// Finish executing, let the Go runtime take over.
return;
} else {
// Parent, wait for the child.
int status = 0;
if (waitpid(child, &status, 0) == -1) {
pr_perror("nsenter: Failed to waitpid with error");
exit(1);
}
// Forward the child's exit code or re-send its death signal.
if (WIFEXITED(status)) {
exit(WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
kill(getpid(), WTERMSIG(status));
}
exit(1);
}
return;
}

View File

@ -1,11 +0,0 @@
// +build linux
package nsenter
/*
void nsenter();
__attribute__((constructor)) int init() {
nsenter();
}
*/
import "C"

View File

@ -1,48 +0,0 @@
// +build linux
package namespaces
import (
"os"
"syscall"
"github.com/docker/libcontainer"
)
type initError struct {
Message string `json:"message,omitempty"`
}
func (i initError) Error() string {
return i.Message
}
var namespaceInfo = map[libcontainer.NamespaceType]int{
libcontainer.NEWNET: syscall.CLONE_NEWNET,
libcontainer.NEWNS: syscall.CLONE_NEWNS,
libcontainer.NEWUSER: syscall.CLONE_NEWUSER,
libcontainer.NEWIPC: syscall.CLONE_NEWIPC,
libcontainer.NEWUTS: syscall.CLONE_NEWUTS,
libcontainer.NEWPID: syscall.CLONE_NEWPID,
}
// New returns a newly initialized Pipe for communication between processes
func newInitPipe() (parent *os.File, child *os.File, err error) {
fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC, 0)
if err != nil {
return nil, nil, err
}
return os.NewFile(uintptr(fds[1]), "parent"), os.NewFile(uintptr(fds[0]), "child"), nil
}
// GetNamespaceFlags parses the container's Namespaces options to set the correct
// flags on clone, unshare. This functions returns flags only for new namespaces.
func GetNamespaceFlags(namespaces libcontainer.Namespaces) (flag int) {
for _, v := range namespaces {
if v.Path != "" {
continue
}
flag |= namespaceInfo[v.Type]
}
return flag
}

View File

@ -1,23 +0,0 @@
// +build linux
package network
import (
"fmt"
)
// Loopback is a network strategy that provides a basic loopback device
type Loopback struct {
}
func (l *Loopback) Create(n *Network, nspid int, networkState *NetworkState) error {
return nil
}
func (l *Loopback) Initialize(config *Network, networkState *NetworkState) error {
// Do not set the MTU on the loopback interface - use the default.
if err := InterfaceUp("lo"); err != nil {
return fmt.Errorf("lo up %s", err)
}
return nil
}

View File

@ -1,117 +0,0 @@
// +build linux
package network
import (
"net"
"github.com/docker/libcontainer/netlink"
)
func InterfaceUp(name string) error {
iface, err := net.InterfaceByName(name)
if err != nil {
return err
}
return netlink.NetworkLinkUp(iface)
}
func InterfaceDown(name string) error {
iface, err := net.InterfaceByName(name)
if err != nil {
return err
}
return netlink.NetworkLinkDown(iface)
}
func ChangeInterfaceName(old, newName string) error {
iface, err := net.InterfaceByName(old)
if err != nil {
return err
}
return netlink.NetworkChangeName(iface, newName)
}
func CreateVethPair(name1, name2 string, txQueueLen int) error {
return netlink.NetworkCreateVethPair(name1, name2, txQueueLen)
}
func SetInterfaceInNamespacePid(name string, nsPid int) error {
iface, err := net.InterfaceByName(name)
if err != nil {
return err
}
return netlink.NetworkSetNsPid(iface, nsPid)
}
func SetInterfaceInNamespaceFd(name string, fd uintptr) error {
iface, err := net.InterfaceByName(name)
if err != nil {
return err
}
return netlink.NetworkSetNsFd(iface, int(fd))
}
func SetInterfaceMaster(name, master string) error {
iface, err := net.InterfaceByName(name)
if err != nil {
return err
}
masterIface, err := net.InterfaceByName(master)
if err != nil {
return err
}
return netlink.AddToBridge(iface, masterIface)
}
func SetDefaultGateway(ip, ifaceName string) error {
return netlink.AddDefaultGw(ip, ifaceName)
}
func SetInterfaceMac(name string, macaddr string) error {
iface, err := net.InterfaceByName(name)
if err != nil {
return err
}
return netlink.NetworkSetMacAddress(iface, macaddr)
}
func SetInterfaceIp(name string, rawIp string) error {
iface, err := net.InterfaceByName(name)
if err != nil {
return err
}
ip, ipNet, err := net.ParseCIDR(rawIp)
if err != nil {
return err
}
return netlink.NetworkLinkAddIp(iface, ip, ipNet)
}
func DeleteInterfaceIp(name string, rawIp string) error {
iface, err := net.InterfaceByName(name)
if err != nil {
return err
}
ip, ipNet, err := net.ParseCIDR(rawIp)
if err != nil {
return err
}
return netlink.NetworkLinkDelIp(iface, ip, ipNet)
}
func SetMtu(name string, mtu int) error {
iface, err := net.InterfaceByName(name)
if err != nil {
return err
}
return netlink.NetworkSetMTU(iface, mtu)
}
func SetHairpinMode(name string, enabled bool) error {
iface, err := net.InterfaceByName(name)
if err != nil {
return err
}
return netlink.SetHairpinMode(iface, enabled)
}

View File

@ -1,74 +0,0 @@
package network
import (
"io/ioutil"
"path/filepath"
"strconv"
"strings"
)
type NetworkStats struct {
RxBytes uint64 `json:"rx_bytes"`
RxPackets uint64 `json:"rx_packets"`
RxErrors uint64 `json:"rx_errors"`
RxDropped uint64 `json:"rx_dropped"`
TxBytes uint64 `json:"tx_bytes"`
TxPackets uint64 `json:"tx_packets"`
TxErrors uint64 `json:"tx_errors"`
TxDropped uint64 `json:"tx_dropped"`
}
// Returns the network statistics for the network interfaces represented by the NetworkRuntimeInfo.
func GetStats(networkState *NetworkState) (*NetworkStats, error) {
// This can happen if the network runtime information is missing - possible if the container was created by an old version of libcontainer.
if networkState.VethHost == "" {
return &NetworkStats{}, nil
}
out := &NetworkStats{}
type netStatsPair struct {
// Where to write the output.
Out *uint64
// The network stats file to read.
File string
}
// Ingress for host veth is from the container. Hence tx_bytes stat on the host veth is actually number of bytes received by the container.
netStats := []netStatsPair{
{Out: &out.RxBytes, File: "tx_bytes"},
{Out: &out.RxPackets, File: "tx_packets"},
{Out: &out.RxErrors, File: "tx_errors"},
{Out: &out.RxDropped, File: "tx_dropped"},
{Out: &out.TxBytes, File: "rx_bytes"},
{Out: &out.TxPackets, File: "rx_packets"},
{Out: &out.TxErrors, File: "rx_errors"},
{Out: &out.TxDropped, File: "rx_dropped"},
}
for _, netStat := range netStats {
data, err := readSysfsNetworkStats(networkState.VethHost, netStat.File)
if err != nil {
return nil, err
}
*(netStat.Out) = data
}
return out, nil
}
// Reads the specified statistics available under /sys/class/net/<EthInterface>/statistics
func readSysfsNetworkStats(ethInterface, statsFile string) (uint64, error) {
fullPath := filepath.Join("/sys/class/net", ethInterface, "statistics", statsFile)
data, err := ioutil.ReadFile(fullPath)
if err != nil {
return 0, err
}
value, err := strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
if err != nil {
return 0, err
}
return value, err
}

View File

@ -1,34 +0,0 @@
// +build linux
package network
import (
"errors"
)
var (
ErrNotValidStrategyType = errors.New("not a valid network strategy type")
)
var strategies = map[string]NetworkStrategy{
"veth": &Veth{},
"loopback": &Loopback{},
}
// NetworkStrategy represents a specific network configuration for
// a container's networking stack
type NetworkStrategy interface {
Create(*Network, int, *NetworkState) error
Initialize(*Network, *NetworkState) error
}
// GetStrategy returns the specific network strategy for the
// provided type. If no strategy is registered for the type an
// ErrNotValidStrategyType is returned.
func GetStrategy(tpe string) (NetworkStrategy, error) {
s, exists := strategies[tpe]
if !exists {
return nil, ErrNotValidStrategyType
}
return s, nil
}

View File

@ -1,56 +0,0 @@
package network
// Network defines configuration for a container's networking stack
//
// The network configuration can be omited from a container causing the
// container to be setup with the host's networking stack
type Network struct {
// Type sets the networks type, commonly veth and loopback
Type string `json:"type,omitempty"`
// The bridge to use.
Bridge string `json:"bridge,omitempty"`
// Prefix for the veth interfaces.
VethPrefix string `json:"veth_prefix,omitempty"`
// MacAddress contains the MAC address to set on the network interface
MacAddress string `json:"mac_address,omitempty"`
// Address contains the IPv4 and mask to set on the network interface
Address string `json:"address,omitempty"`
// IPv6Address contains the IPv6 and mask to set on the network interface
IPv6Address string `json:"ipv6_address,omitempty"`
// Gateway sets the gateway address that is used as the default for the interface
Gateway string `json:"gateway,omitempty"`
// IPv6Gateway sets the ipv6 gateway address that is used as the default for the interface
IPv6Gateway string `json:"ipv6_gateway,omitempty"`
// Mtu sets the mtu value for the interface and will be mirrored on both the host and
// container's interfaces if a pair is created, specifically in the case of type veth
// Note: This does not apply to loopback interfaces.
Mtu int `json:"mtu,omitempty"`
// TxQueueLen sets the tx_queuelen value for the interface and will be mirrored on both the host and
// container's interfaces if a pair is created, specifically in the case of type veth
// Note: This does not apply to loopback interfaces.
TxQueueLen int `json:"txqueuelen,omitempty"`
// HairpinMode specifies if hairpin NAT should be enabled on the virtual interface
// bridge port in the case of type veth
// Note: This is unsupported on some systems.
// Note: This does not apply to loopback interfaces.
HairpinMode bool `json:"hairpin_mode"`
}
// Struct describing the network specific runtime state that will be maintained by libcontainer for all running containers
// Do not depend on it outside of libcontainer.
type NetworkState struct {
// The name of the veth interface on the Host.
VethHost string `json:"veth_host,omitempty"`
// The name of the veth interface created inside the container for the child.
VethChild string `json:"veth_child,omitempty"`
}

View File

@ -1,127 +0,0 @@
// +build linux
package network
import (
"fmt"
"github.com/docker/libcontainer/netlink"
"github.com/docker/libcontainer/utils"
)
// Veth is a network strategy that uses a bridge and creates
// a veth pair, one that stays outside on the host and the other
// is placed inside the container's namespace
type Veth struct {
}
const defaultDevice = "eth0"
func (v *Veth) Create(n *Network, nspid int, networkState *NetworkState) error {
var (
bridge = n.Bridge
prefix = n.VethPrefix
txQueueLen = n.TxQueueLen
)
if bridge == "" {
return fmt.Errorf("bridge is not specified")
}
if prefix == "" {
return fmt.Errorf("veth prefix is not specified")
}
name1, name2, err := createVethPair(prefix, txQueueLen)
if err != nil {
return err
}
if err := SetInterfaceMaster(name1, bridge); err != nil {
return err
}
if err := SetMtu(name1, n.Mtu); err != nil {
return err
}
if n.HairpinMode {
if err := SetHairpinMode(name1, true); err != nil {
return err
}
}
if err := InterfaceUp(name1); err != nil {
return err
}
if err := SetInterfaceInNamespacePid(name2, nspid); err != nil {
return err
}
networkState.VethHost = name1
networkState.VethChild = name2
return nil
}
func (v *Veth) Initialize(config *Network, networkState *NetworkState) error {
var vethChild = networkState.VethChild
if vethChild == "" {
return fmt.Errorf("vethChild is not specified")
}
if err := InterfaceDown(vethChild); err != nil {
return fmt.Errorf("interface down %s %s", vethChild, err)
}
if err := ChangeInterfaceName(vethChild, defaultDevice); err != nil {
return fmt.Errorf("change %s to %s %s", vethChild, defaultDevice, err)
}
if config.MacAddress != "" {
if err := SetInterfaceMac(defaultDevice, config.MacAddress); err != nil {
return fmt.Errorf("set %s mac %s", defaultDevice, err)
}
}
if err := SetInterfaceIp(defaultDevice, config.Address); err != nil {
return fmt.Errorf("set %s ip %s", defaultDevice, err)
}
if config.IPv6Address != "" {
if err := SetInterfaceIp(defaultDevice, config.IPv6Address); err != nil {
return fmt.Errorf("set %s ipv6 %s", defaultDevice, err)
}
}
if err := SetMtu(defaultDevice, config.Mtu); err != nil {
return fmt.Errorf("set %s mtu to %d %s", defaultDevice, config.Mtu, err)
}
if err := InterfaceUp(defaultDevice); err != nil {
return fmt.Errorf("%s up %s", defaultDevice, err)
}
if config.Gateway != "" {
if err := SetDefaultGateway(config.Gateway, defaultDevice); err != nil {
return fmt.Errorf("set gateway to %s on device %s failed with %s", config.Gateway, defaultDevice, err)
}
}
if config.IPv6Gateway != "" {
if err := SetDefaultGateway(config.IPv6Gateway, defaultDevice); err != nil {
return fmt.Errorf("set gateway for ipv6 to %s on device %s failed with %s", config.IPv6Gateway, defaultDevice, err)
}
}
return nil
}
// createVethPair will automatically generage two random names for
// the veth pair and ensure that they have been created
func createVethPair(prefix string, txQueueLen int) (name1 string, name2 string, err error) {
for i := 0; i < 10; i++ {
if name1, err = utils.GenerateRandomName(prefix, 7); err != nil {
return
}
if name2, err = utils.GenerateRandomName(prefix, 7); err != nil {
return
}
if err = CreateVethPair(name1, name2, txQueueLen); err != nil {
if err == netlink.ErrInterfaceExists {
continue
}
return
}
break
}
return
}

View File

@ -1,53 +0,0 @@
// +build linux
package network
import (
"testing"
"github.com/docker/libcontainer/netlink"
)
func TestGenerateVethNames(t *testing.T) {
if testing.Short() {
return
}
prefix := "veth"
name1, name2, err := createVethPair(prefix, 0)
if err != nil {
t.Fatal(err)
}
if name1 == "" {
t.Fatal("name1 should not be empty")
}
if name2 == "" {
t.Fatal("name2 should not be empty")
}
}
func TestCreateDuplicateVethPair(t *testing.T) {
if testing.Short() {
return
}
prefix := "veth"
name1, name2, err := createVethPair(prefix, 0)
if err != nil {
t.Fatal(err)
}
// retry to create the name interfaces and make sure that we get the correct error
err = CreateVethPair(name1, name2, 0)
if err == nil {
t.Fatal("expected error to not be nil with duplicate interface")
}
if err != netlink.ErrInterfaceExists {
t.Fatalf("expected error to be ErrInterfaceExists but received %q", err)
}
}

12
nsenter/nsenter.go Normal file
View File

@ -0,0 +1,12 @@
// +build linux
package nsenter
/*
#cgo CFLAGS: -Wall
extern void nsexec();
void __attribute__((constructor)) init() {
nsexec();
}
*/
import "C"

View File

@ -1,6 +1,7 @@
package nsenter
import (
"encoding/json"
"fmt"
"os"
"os/exec"
@ -10,8 +11,12 @@ import (
"testing"
)
type pid struct {
Pid int `json:"Pid"`
}
func TestNsenterAlivePid(t *testing.T) {
args := []string{"nsenter-exec", "--nspid", fmt.Sprintf("%d", os.Getpid())}
args := []string{"nsenter-exec"}
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("failed to create pipe %v", err)
@ -20,30 +25,39 @@ func TestNsenterAlivePid(t *testing.T) {
cmd := &exec.Cmd{
Path: os.Args[0],
Args: args,
ExtraFiles: []*os.File{r},
ExtraFiles: []*os.File{w},
Env: []string{fmt.Sprintf("_LIBCONTAINER_INITPID=%d", os.Getpid())},
}
if err := cmd.Start(); err != nil {
t.Fatalf("nsenter failed to start %v", err)
}
r.Close()
w.Close()
// unblock the child process
if _, err := w.WriteString("1"); err != nil {
t.Fatalf("parent failed to write synchronization data %v", err)
decoder := json.NewDecoder(r)
var pid *pid
if err := decoder.Decode(&pid); err != nil {
t.Fatalf("%v", err)
}
if err := cmd.Wait(); err != nil {
t.Fatalf("nsenter exits with a non-zero exit status")
}
p, err := os.FindProcess(pid.Pid)
if err != nil {
t.Fatalf("%v", err)
}
p.Wait()
}
func TestNsenterInvalidPid(t *testing.T) {
args := []string{"nsenter-exec", "--nspid", "-1"}
args := []string{"nsenter-exec"}
cmd := &exec.Cmd{
Path: os.Args[0],
Args: args,
Env: []string{"_LIBCONTAINER_INITPID=-1"},
}
err := cmd.Run()
@ -63,11 +77,12 @@ func TestNsenterDeadPid(t *testing.T) {
defer dead_cmd.Wait()
<-c // dead_cmd is zombie
args := []string{"nsenter-exec", "--nspid", fmt.Sprintf("%d", dead_cmd.Process.Pid)}
args := []string{"nsenter-exec"}
cmd := &exec.Cmd{
Path: os.Args[0],
Args: args,
Env: []string{fmt.Sprintf("_LIBCONTAINER_INITPID=%d", dead_cmd.Process.Pid)},
}
err := cmd.Run()

135
nsenter/nsexec.c Normal file
View File

@ -0,0 +1,135 @@
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <linux/limits.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <setjmp.h>
#include <sched.h>
#include <signal.h>
/* All arguments should be above stack, because it grows down */
struct clone_arg {
/*
* Reserve some space for clone() to locate arguments
* and retcode in this place
*/
char stack[4096] __attribute__ ((aligned(8)));
char stack_ptr[0];
jmp_buf *env;
};
static int child_func(void *_arg)
{
struct clone_arg *arg = (struct clone_arg *)_arg;
longjmp(*arg->env, 1);
}
#define pr_perror(fmt, ...) fprintf(stderr, "nsenter: " fmt ": %m\n", ##__VA_ARGS__)
// Use raw setns syscall for versions of glibc that don't include it (namely glibc-2.12)
#if __GLIBC__ == 2 && __GLIBC_MINOR__ < 14
#define _GNU_SOURCE
#include "syscall.h"
#ifdef SYS_setns
int setns(int fd, int nstype)
{
return syscall(SYS_setns, fd, nstype);
}
#endif
#endif
static int clone_parent(jmp_buf * env) __attribute__ ((noinline));
static int clone_parent(jmp_buf * env)
{
struct clone_arg ca;
int child;
ca.env = env;
child = clone(child_func, ca.stack_ptr, CLONE_PARENT | SIGCHLD, &ca);
return child;
}
void nsexec()
{
char *namespaces[] = { "ipc", "uts", "net", "pid", "mnt" };
const int num = sizeof(namespaces) / sizeof(char *);
jmp_buf env;
char buf[PATH_MAX], *val;
int i, tfd, child, len;
pid_t pid;
val = getenv("_LIBCONTAINER_INITPID");
if (val == NULL)
return;
pid = atoi(val);
snprintf(buf, sizeof(buf), "%d", pid);
if (strcmp(val, buf)) {
pr_perror("Unable to parse _LIBCONTAINER_INITPID");
exit(1);
}
/* Check that the specified process exists */
snprintf(buf, PATH_MAX - 1, "/proc/%d/ns", pid);
tfd = open(buf, O_DIRECTORY | O_RDONLY);
if (tfd == -1) {
pr_perror("Failed to open \"%s\"", buf);
exit(1);
}
for (i = 0; i < num; i++) {
struct stat st;
int fd;
/* Symlinks on all namespaces exist for dead processes, but they can't be opened */
if (fstatat(tfd, namespaces[i], &st, AT_SYMLINK_NOFOLLOW) == -1) {
// Ignore nonexistent namespaces.
if (errno == ENOENT)
continue;
}
fd = openat(tfd, namespaces[i], O_RDONLY);
if (fd == -1) {
pr_perror("Failed to open ns file %s for ns %s", buf,
namespaces[i]);
exit(1);
}
// Set the namespace.
if (setns(fd, 0) == -1) {
pr_perror("Failed to setns for %s", namespaces[i]);
exit(1);
}
close(fd);
}
if (setjmp(env) == 1) {
// Finish executing, let the Go runtime take over.
return;
}
child = clone_parent(&env);
if (child < 0) {
pr_perror("Unable to fork");
exit(1);
}
len = snprintf(buf, sizeof(buf), "{ \"pid\" : %d }\n", child);
if (write(3, buf, len) != len) {
pr_perror("Unable to send a child pid");
kill(child, SIGKILL);
exit(1);
}
exit(0);
}

2
nsinit/Makefile Normal file
View File

@ -0,0 +1,2 @@
all:
go build -o nsinit .

View File

@ -1,29 +1,266 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"io"
"math"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"github.com/docker/libcontainer/configs"
"github.com/docker/libcontainer/utils"
)
const defaultMountFlags = syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
var createFlags = []cli.Flag{
cli.IntFlag{Name: "parent-death-signal", Usage: "set the signal that will be delivered to the process in case the parent dies"},
cli.BoolFlag{Name: "read-only", Usage: "set the container's rootfs as read-only"},
cli.StringSliceFlag{Name: "bind", Value: &cli.StringSlice{}, Usage: "add bind mounts to the container"},
cli.StringSliceFlag{Name: "tmpfs", Value: &cli.StringSlice{}, Usage: "add tmpfs mounts to the container"},
cli.IntFlag{Name: "cpushares", Usage: "set the cpushares for the container"},
cli.IntFlag{Name: "memory-limit", Usage: "set the memory limit for the container"},
cli.IntFlag{Name: "memory-swap", Usage: "set the memory swap limit for the container"},
cli.StringFlag{Name: "cpuset-cpus", Usage: "set the cpuset cpus"},
cli.StringFlag{Name: "cpuset-mems", Usage: "set the cpuset mems"},
cli.StringFlag{Name: "apparmor-profile", Usage: "set the apparmor profile"},
cli.StringFlag{Name: "process-label", Usage: "set the process label"},
cli.StringFlag{Name: "mount-label", Usage: "set the mount label"},
cli.StringFlag{Name: "rootfs", Usage: "set the rootfs"},
cli.IntFlag{Name: "userns-root-uid", Usage: "set the user namespace root uid"},
cli.StringFlag{Name: "hostname", Value: "nsinit", Usage: "hostname value for the container"},
cli.StringFlag{Name: "net", Value: "", Usage: "network namespace"},
cli.StringFlag{Name: "ipc", Value: "", Usage: "ipc namespace"},
cli.StringFlag{Name: "pid", Value: "", Usage: "pid namespace"},
cli.StringFlag{Name: "uts", Value: "", Usage: "uts namespace"},
cli.StringFlag{Name: "mnt", Value: "", Usage: "mount namespace"},
cli.StringFlag{Name: "veth-bridge", Usage: "veth bridge"},
cli.StringFlag{Name: "veth-address", Usage: "veth ip address"},
cli.StringFlag{Name: "veth-gateway", Usage: "veth gateway address"},
cli.IntFlag{Name: "veth-mtu", Usage: "veth mtu"},
}
var configCommand = cli.Command{
Name: "config",
Usage: "display the container configuration",
Action: configAction,
Name: "config",
Usage: "generate a standard configuration file for a container",
Flags: append([]cli.Flag{
cli.StringFlag{Name: "file,f", Value: "stdout", Usage: "write the configuration to the specified file"},
}, createFlags...),
Action: func(context *cli.Context) {
template := getTemplate()
modify(template, context)
data, err := json.MarshalIndent(template, "", "\t")
if err != nil {
fatal(err)
}
var f *os.File
filePath := context.String("file")
switch filePath {
case "stdout", "":
f = os.Stdout
default:
if f, err = os.Create(filePath); err != nil {
fatal(err)
}
defer f.Close()
}
if _, err := io.Copy(f, bytes.NewBuffer(data)); err != nil {
fatal(err)
}
},
}
func configAction(context *cli.Context) {
container, err := loadConfig()
if err != nil {
log.Fatal(err)
func modify(config *configs.Config, context *cli.Context) {
config.ParentDeathSignal = context.Int("parent-death-signal")
config.Readonlyfs = context.Bool("read-only")
config.Cgroups.CpusetCpus = context.String("cpuset-cpus")
config.Cgroups.CpusetMems = context.String("cpuset-mems")
config.Cgroups.CpuShares = int64(context.Int("cpushares"))
config.Cgroups.Memory = int64(context.Int("memory-limit"))
config.Cgroups.MemorySwap = int64(context.Int("memory-swap"))
config.AppArmorProfile = context.String("apparmor-profile")
config.ProcessLabel = context.String("process-label")
config.MountLabel = context.String("mount-label")
rootfs := context.String("rootfs")
if rootfs != "" {
config.Rootfs = rootfs
}
data, err := json.MarshalIndent(container, "", "\t")
if err != nil {
log.Fatal(err)
userns_uid := context.Int("userns-root-uid")
if userns_uid != 0 {
config.Namespaces.Add(configs.NEWUSER, "")
config.UidMappings = []configs.IDMap{
{ContainerID: 0, HostID: userns_uid, Size: 1},
{ContainerID: 1, HostID: 1, Size: userns_uid - 1},
{ContainerID: userns_uid + 1, HostID: userns_uid + 1, Size: math.MaxInt32 - userns_uid},
}
config.GidMappings = []configs.IDMap{
{ContainerID: 0, HostID: userns_uid, Size: 1},
{ContainerID: 1, HostID: 1, Size: userns_uid - 1},
{ContainerID: userns_uid + 1, HostID: userns_uid + 1, Size: math.MaxInt32 - userns_uid},
}
for _, node := range config.Devices {
node.Uid = uint32(userns_uid)
node.Gid = uint32(userns_uid)
}
}
for _, rawBind := range context.StringSlice("bind") {
mount := &configs.Mount{
Device: "bind",
Flags: syscall.MS_BIND | syscall.MS_REC,
}
parts := strings.SplitN(rawBind, ":", 3)
switch len(parts) {
default:
logrus.Fatalf("invalid bind mount %s", rawBind)
case 2:
mount.Source, mount.Destination = parts[0], parts[1]
case 3:
mount.Source, mount.Destination = parts[0], parts[1]
switch parts[2] {
case "ro":
mount.Flags |= syscall.MS_RDONLY
case "rw":
default:
logrus.Fatalf("invalid bind mount mode %s", parts[2])
}
}
config.Mounts = append(config.Mounts, mount)
}
for _, tmpfs := range context.StringSlice("tmpfs") {
config.Mounts = append(config.Mounts, &configs.Mount{
Device: "tmpfs",
Destination: tmpfs,
Flags: syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV,
})
}
for flag, value := range map[string]configs.NamespaceType{
"net": configs.NEWNET,
"mnt": configs.NEWNS,
"pid": configs.NEWPID,
"ipc": configs.NEWIPC,
"uts": configs.NEWUTS,
} {
switch v := context.String(flag); v {
case "host":
config.Namespaces.Remove(value)
case "", "private":
if !config.Namespaces.Contains(value) {
config.Namespaces.Add(value, "")
}
if flag == "net" {
config.Networks = []*configs.Network{
{
Type: "loopback",
Address: "127.0.0.1/0",
Gateway: "localhost",
},
}
}
if flag == "uts" {
config.Hostname = context.String("hostname")
}
default:
config.Namespaces.Remove(value)
config.Namespaces.Add(value, v)
}
}
if bridge := context.String("veth-bridge"); bridge != "" {
hostName, err := utils.GenerateRandomName("veth", 7)
if err != nil {
logrus.Fatal(err)
}
network := &configs.Network{
Type: "veth",
Name: "eth0",
Bridge: bridge,
Address: context.String("veth-address"),
Gateway: context.String("veth-gateway"),
Mtu: context.Int("veth-mtu"),
HostInterfaceName: hostName,
}
config.Networks = append(config.Networks, network)
}
fmt.Printf("%s", data)
}
func getTemplate() *configs.Config {
cwd, err := os.Getwd()
if err != nil {
panic(err)
}
return &configs.Config{
Rootfs: cwd,
ParentDeathSignal: int(syscall.SIGKILL),
Capabilities: []string{
"CHOWN",
"DAC_OVERRIDE",
"FSETID",
"FOWNER",
"MKNOD",
"NET_RAW",
"SETGID",
"SETUID",
"SETFCAP",
"SETPCAP",
"NET_BIND_SERVICE",
"SYS_CHROOT",
"KILL",
"AUDIT_WRITE",
},
Namespaces: configs.Namespaces([]configs.Namespace{
{Type: configs.NEWNS},
{Type: configs.NEWUTS},
{Type: configs.NEWIPC},
{Type: configs.NEWPID},
{Type: configs.NEWNET},
}),
Cgroups: &configs.Cgroup{
Name: filepath.Base(cwd),
Parent: "nsinit",
AllowAllDevices: false,
AllowedDevices: configs.DefaultAllowedDevices,
},
Devices: configs.DefaultAutoCreatedDevices,
MaskPaths: []string{
"/proc/kcore",
},
ReadonlyPaths: []string{
"/proc/sys", "/proc/sysrq-trigger", "/proc/irq", "/proc/bus",
},
Mounts: []*configs.Mount{
{
Device: "tmpfs",
Source: "shm",
Destination: "/dev/shm",
Data: "mode=1777,size=65536k",
Flags: defaultMountFlags,
},
{
Source: "mqueue",
Destination: "/dev/mqueue",
Device: "mqueue",
Flags: defaultMountFlags,
},
{
Source: "sysfs",
Destination: "/sys",
Device: "sysfs",
Flags: defaultMountFlags | syscall.MS_RDONLY,
},
},
Rlimits: []configs.Rlimit{
{
Type: syscall.RLIMIT_NOFILE,
Hard: 1024,
Soft: 1024,
},
},
}
}

View File

@ -1,212 +1,122 @@
package main
import (
"fmt"
"io"
"log"
"os"
"os/exec"
"os/signal"
"syscall"
"text/tabwriter"
"github.com/codegangsta/cli"
"github.com/docker/docker/pkg/term"
"github.com/docker/libcontainer"
consolepkg "github.com/docker/libcontainer/console"
"github.com/docker/libcontainer/namespaces"
"github.com/docker/libcontainer/utils"
)
var standardEnvironment = &cli.StringSlice{
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HOSTNAME=nsinit",
"TERM=xterm",
}
var execCommand = cli.Command{
Name: "exec",
Usage: "execute a new command inside a container",
Action: execAction,
Flags: []cli.Flag{
cli.BoolFlag{Name: "list", Usage: "list all registered exec functions"},
cli.StringFlag{Name: "func", Value: "exec", Usage: "function name to exec inside a container"},
},
Flags: append([]cli.Flag{
cli.BoolFlag{Name: "tty,t", Usage: "allocate a TTY to the container"},
cli.StringFlag{Name: "id", Value: "nsinit", Usage: "specify the ID for a container"},
cli.StringFlag{Name: "config", Value: "container.json", Usage: "path to the configuration file"},
cli.BoolFlag{Name: "create", Usage: "create the container's configuration on the fly with arguments"},
cli.StringFlag{Name: "user,u", Value: "root", Usage: "set the user, uid, and/or gid for the process"},
cli.StringFlag{Name: "cwd", Value: "", Usage: "set the current working dir"},
cli.StringSliceFlag{Name: "env", Value: standardEnvironment, Usage: "set environment variables for the process"},
}, createFlags...),
}
func execAction(context *cli.Context) {
if context.Bool("list") {
w := tabwriter.NewWriter(os.Stdout, 10, 1, 3, ' ', 0)
fmt.Fprint(w, "NAME\tUSAGE\n")
for k, f := range argvs {
fmt.Fprintf(w, "%s\t%s\n", k, f.Usage)
factory, err := loadFactory(context)
if err != nil {
fatal(err)
}
config, err := loadConfig(context)
if err != nil {
fatal(err)
}
rootuid, err := config.HostUID()
if err != nil {
fatal(err)
}
tty, err := newTty(context, rootuid)
if err != nil {
fatal(err)
}
created := false
container, err := factory.Load(context.String("id"))
if err != nil {
if tty.console != nil {
config.Console = tty.console.Path()
}
created = true
if container, err = factory.Create(context.String("id"), config); err != nil {
tty.Close()
fatal(err)
}
w.Flush()
return
}
var exitCode int
container, err := loadConfig()
go handleSignals(container, tty)
process := &libcontainer.Process{
Args: context.Args(),
Env: context.StringSlice("env"),
User: context.String("user"),
Cwd: context.String("cwd"),
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
}
if err := tty.attach(process); err != nil {
fatal(err)
}
pid, err := container.Start(process)
if err != nil {
log.Fatal(err)
tty.Close()
if created {
container.Destroy()
}
fatal(err)
}
state, err := libcontainer.GetState(dataPath)
if err != nil && !os.IsNotExist(err) {
log.Fatalf("unable to read state.json: %s", err)
}
if state != nil {
exitCode, err = startInExistingContainer(container, state, context.String("func"), context)
} else {
exitCode, err = startContainer(container, dataPath, []string(context.Args()))
}
proc, err := os.FindProcess(pid)
if err != nil {
log.Fatalf("failed to exec: %s", err)
tty.Close()
if created {
container.Destroy()
}
fatal(err)
}
os.Exit(exitCode)
status, err := proc.Wait()
if err != nil {
tty.Close()
if created {
container.Destroy()
}
fatal(err)
}
if created {
if err := container.Destroy(); err != nil {
tty.Close()
fatal(err)
}
}
tty.Close()
os.Exit(utils.ExitStatus(status.Sys().(syscall.WaitStatus)))
}
// the process for execing a new process inside an existing container is that we have to exec ourself
// with the nsenter argument so that the C code can setns an the namespaces that we require. Then that
// code path will drop us into the path that we can do the final setup of the namespace and exec the users
// application.
func startInExistingContainer(config *libcontainer.Config, state *libcontainer.State, action string, context *cli.Context) (int, error) {
var (
master *os.File
console string
err error
sigc = make(chan os.Signal, 10)
stdin = os.Stdin
stdout = os.Stdout
stderr = os.Stderr
)
func handleSignals(container libcontainer.Container, tty *tty) {
sigc := make(chan os.Signal, 10)
signal.Notify(sigc)
if config.Tty && action != "setup" {
stdin = nil
stdout = nil
stderr = nil
master, console, err = consolepkg.CreateMasterAndConsole()
if err != nil {
return -1, err
tty.resize()
for sig := range sigc {
switch sig {
case syscall.SIGWINCH:
tty.resize()
default:
container.Signal(sig)
}
go io.Copy(master, os.Stdin)
go io.Copy(os.Stdout, master)
state, err := term.SetRawTerminal(os.Stdin.Fd())
if err != nil {
return -1, err
}
defer term.RestoreTerminal(os.Stdin.Fd(), state)
}
startCallback := func(cmd *exec.Cmd) {
go func() {
resizeTty(master)
for sig := range sigc {
switch sig {
case syscall.SIGWINCH:
resizeTty(master)
default:
cmd.Process.Signal(sig)
}
}
}()
}
return namespaces.ExecIn(config, state, context.Args(), os.Args[0], action, stdin, stdout, stderr, console, startCallback)
}
// startContainer starts the container. Returns the exit status or -1 and an
// error.
//
// Signals sent to the current process will be forwarded to container.
func startContainer(container *libcontainer.Config, dataPath string, args []string) (int, error) {
var (
cmd *exec.Cmd
sigc = make(chan os.Signal, 10)
)
signal.Notify(sigc)
createCommand := func(container *libcontainer.Config, console, dataPath, init string, pipe *os.File, args []string) *exec.Cmd {
cmd = namespaces.DefaultCreateCommand(container, console, dataPath, init, pipe, args)
if logPath != "" {
cmd.Env = append(cmd.Env, fmt.Sprintf("log=%s", logPath))
}
return cmd
}
setupCommand := func(container *libcontainer.Config, console, dataPath, init string) *exec.Cmd {
return namespaces.DefaultSetupCommand(container, console, dataPath, init)
}
var (
master *os.File
console string
err error
stdin = os.Stdin
stdout = os.Stdout
stderr = os.Stderr
)
if container.Tty {
stdin = nil
stdout = nil
stderr = nil
master, console, err = consolepkg.CreateMasterAndConsole()
if err != nil {
return -1, err
}
go io.Copy(master, os.Stdin)
go io.Copy(os.Stdout, master)
state, err := term.SetRawTerminal(os.Stdin.Fd())
if err != nil {
return -1, err
}
defer term.RestoreTerminal(os.Stdin.Fd(), state)
}
startCallback := func() {
go func() {
resizeTty(master)
for sig := range sigc {
switch sig {
case syscall.SIGWINCH:
resizeTty(master)
default:
cmd.Process.Signal(sig)
}
}
}()
}
return namespaces.Exec(container, stdin, stdout, stderr, console, dataPath, args, createCommand, setupCommand, startCallback)
}
func resizeTty(master *os.File) {
if master == nil {
return
}
ws, err := term.GetWinsize(os.Stdin.Fd())
if err != nil {
return
}
if err := term.SetWinsize(master.Fd(), ws); err != nil {
return
}
}

View File

@ -1,47 +1,28 @@
package main
import (
"log"
"os"
"runtime"
"strconv"
log "github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"github.com/docker/libcontainer/namespaces"
"github.com/docker/libcontainer"
_ "github.com/docker/libcontainer/nsenter"
)
var (
dataPath = os.Getenv("data_path")
console = os.Getenv("console")
rawPipeFd = os.Getenv("pipe")
initCommand = cli.Command{
Name: "init",
Usage: "runs the init process inside the namespace",
Action: initAction,
}
)
func initAction(context *cli.Context) {
runtime.LockOSThread()
container, err := loadConfig()
if err != nil {
log.Fatal(err)
}
rootfs, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
pipeFd, err := strconv.Atoi(rawPipeFd)
if err != nil {
log.Fatal(err)
}
pipe := os.NewFile(uintptr(pipeFd), "pipe")
if err := namespaces.Init(container, rootfs, console, pipe, []string(context.Args())); err != nil {
log.Fatalf("unable to initialize for container: %s", err)
}
var initCommand = cli.Command{
Name: "init",
Usage: "runs the init process inside the namespace",
Action: func(context *cli.Context) {
log.SetLevel(log.DebugLevel)
runtime.GOMAXPROCS(1)
runtime.LockOSThread()
factory, err := libcontainer.New("")
if err != nil {
fatal(err)
}
if err := factory.StartInitialization(3); err != nil {
fatal(err)
}
panic("This line should never been executed")
},
}

View File

@ -1,62 +1,22 @@
package main
import (
"log"
"os"
"strings"
log "github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
)
var (
logPath = os.Getenv("log")
argvs = make(map[string]*rFunc)
)
func init() {
argvs["exec"] = &rFunc{
Usage: "execute a process inside an existing container",
Action: nsenterExec,
}
argvs["mknod"] = &rFunc{
Usage: "mknod a device inside an existing container",
Action: nsenterMknod,
}
argvs["ip"] = &rFunc{
Usage: "display the container's network interfaces",
Action: nsenterIp,
}
argvs["setup"] = &rFunc{
Usage: "finish setting up init before it is ready to exec",
Action: nsenterSetup,
}
}
func main() {
// we need to check our argv 0 for any registred functions to run instead of the
// normal cli code path
f, exists := argvs[strings.TrimPrefix(os.Args[0], "nsenter-")]
if exists {
runFunc(f)
return
}
app := cli.NewApp()
app.Name = "nsinit"
app.Version = "0.1"
app.Version = "2"
app.Author = "libcontainer maintainers"
app.Flags = []cli.Flag{
cli.StringFlag{Name: "nspid"},
cli.StringFlag{Name: "console"},
cli.StringFlag{Name: "root", Value: ".", Usage: "root directory for containers"},
cli.StringFlag{Name: "log-file", Value: "nsinit-debug.log", Usage: "set the log file to output logs to"},
cli.BoolFlag{Name: "debug", Usage: "enable debug output in the logs"},
}
app.Before = preload
app.Commands = []cli.Command{
configCommand,
execCommand,
@ -65,8 +25,21 @@ func main() {
pauseCommand,
statsCommand,
unpauseCommand,
stateCommand,
}
app.Before = func(context *cli.Context) error {
if context.GlobalBool("debug") {
log.SetLevel(log.DebugLevel)
}
if path := context.GlobalString("log-file"); path != "" {
f, err := os.Create(path)
if err != nil {
return err
}
log.SetOutput(f)
}
return nil
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}

View File

@ -1,102 +0,0 @@
package main
import (
"fmt"
"log"
"net"
"os"
"strconv"
"strings"
"text/tabwriter"
"github.com/docker/libcontainer"
"github.com/docker/libcontainer/devices"
"github.com/docker/libcontainer/mount/nodes"
"github.com/docker/libcontainer/namespaces"
_ "github.com/docker/libcontainer/namespaces/nsenter"
)
// nsenterExec exec's a process inside an existing container
func nsenterExec(config *libcontainer.Config, args []string) {
if err := namespaces.FinalizeSetns(config, args); err != nil {
log.Fatalf("failed to nsenter: %s", err)
}
}
// nsenterMknod runs mknod inside an existing container
//
// mknod <path> <type> <major> <minor>
func nsenterMknod(config *libcontainer.Config, args []string) {
if len(args) != 4 {
log.Fatalf("expected mknod to have 4 arguments not %d", len(args))
}
t := rune(args[1][0])
major, err := strconv.Atoi(args[2])
if err != nil {
log.Fatal(err)
}
minor, err := strconv.Atoi(args[3])
if err != nil {
log.Fatal(err)
}
n := &devices.Device{
Path: args[0],
Type: t,
MajorNumber: int64(major),
MinorNumber: int64(minor),
}
if err := nodes.CreateDeviceNode("/", n); err != nil {
log.Fatal(err)
}
}
// nsenterIp displays the network interfaces inside a container's net namespace
func nsenterIp(config *libcontainer.Config, args []string) {
interfaces, err := net.Interfaces()
if err != nil {
log.Fatal(err)
}
w := tabwriter.NewWriter(os.Stdout, 10, 1, 3, ' ', 0)
fmt.Fprint(w, "NAME\tMTU\tMAC\tFLAG\tADDRS\n")
for _, iface := range interfaces {
addrs, err := iface.Addrs()
if err != nil {
log.Fatal(err)
}
o := []string{}
for _, a := range addrs {
o = append(o, a.String())
}
fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%s\n", iface.Name, iface.MTU, iface.HardwareAddr, iface.Flags, strings.Join(o, ","))
}
w.Flush()
}
func nsenterSetup(config *libcontainer.Config, args []string) {
if len(args) < 2 || len(args) > 3 {
log.Fatalf("expected setup to have 2 or 3 arguments not %d", len(args))
}
dataPath := args[0]
uncleanRootfs := args[1]
consolePath := ""
if len(args) == 3 {
consolePath = args[2]
}
if err := namespaces.SetupContainer(config, dataPath, uncleanRootfs, consolePath); err != nil {
log.Fatalf("failed to nsenter setup: %s", err)
}
}

View File

@ -4,25 +4,27 @@ import (
"log"
"github.com/codegangsta/cli"
"github.com/docker/libcontainer"
)
var oomCommand = cli.Command{
Name: "oom",
Usage: "display oom notifications for a container",
Action: oomAction,
}
func oomAction(context *cli.Context) {
state, err := libcontainer.GetState(dataPath)
if err != nil {
log.Fatal(err)
}
n, err := libcontainer.NotifyOnOOM(state)
if err != nil {
log.Fatal(err)
}
for _ = range n {
log.Printf("OOM notification received")
}
Name: "oom",
Usage: "display oom notifications for a container",
Flags: []cli.Flag{
cli.StringFlag{Name: "id", Value: "nsinit", Usage: "specify the ID for a container"},
},
Action: func(context *cli.Context) {
container, err := getContainer(context)
if err != nil {
log.Fatal(err)
}
n, err := container.NotifyOOM()
if err != nil {
log.Fatal(err)
}
for x := range n {
// hack for calm down go1.4 gofmt
_ = x
log.Printf("OOM notification received")
}
},
}

View File

@ -4,46 +4,38 @@ import (
"log"
"github.com/codegangsta/cli"
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/cgroups/fs"
"github.com/docker/libcontainer/cgroups/systemd"
)
var pauseCommand = cli.Command{
Name: "pause",
Usage: "pause the container's processes",
Action: pauseAction,
Name: "pause",
Usage: "pause the container's processes",
Flags: []cli.Flag{
cli.StringFlag{Name: "id", Value: "nsinit", Usage: "specify the ID for a container"},
},
Action: func(context *cli.Context) {
container, err := getContainer(context)
if err != nil {
log.Fatal(err)
}
if err = container.Pause(); err != nil {
log.Fatal(err)
}
},
}
var unpauseCommand = cli.Command{
Name: "unpause",
Usage: "unpause the container's processes",
Action: unpauseAction,
}
func pauseAction(context *cli.Context) {
if err := toggle(cgroups.Frozen); err != nil {
log.Fatal(err)
}
}
func unpauseAction(context *cli.Context) {
if err := toggle(cgroups.Thawed); err != nil {
log.Fatal(err)
}
}
func toggle(state cgroups.FreezerState) error {
container, err := loadConfig()
if err != nil {
return err
}
if systemd.UseSystemd() {
err = systemd.Freeze(container.Cgroups, state)
} else {
err = fs.Freeze(container.Cgroups, state)
}
return err
Name: "unpause",
Usage: "unpause the container's processes",
Flags: []cli.Flag{
cli.StringFlag{Name: "id", Value: "nsinit", Usage: "specify the ID for a container"},
},
Action: func(context *cli.Context) {
container, err := getContainer(context)
if err != nil {
log.Fatal(err)
}
if err = container.Resume(); err != nil {
log.Fatal(err)
}
},
}

31
nsinit/state.go Normal file
View File

@ -0,0 +1,31 @@
package main
import (
"encoding/json"
"fmt"
"github.com/codegangsta/cli"
)
var stateCommand = cli.Command{
Name: "state",
Usage: "get the container's current state",
Flags: []cli.Flag{
cli.StringFlag{Name: "id", Value: "nsinit", Usage: "specify the ID for a container"},
},
Action: func(context *cli.Context) {
container, err := getContainer(context)
if err != nil {
fatal(err)
}
state, err := container.State()
if err != nil {
fatal(err)
}
data, err := json.MarshalIndent(state, "", "\t")
if err != nil {
fatal(err)
}
fmt.Printf("%s", data)
},
}

View File

@ -3,37 +3,29 @@ package main
import (
"encoding/json"
"fmt"
"log"
"github.com/codegangsta/cli"
"github.com/docker/libcontainer"
)
var statsCommand = cli.Command{
Name: "stats",
Usage: "display statistics for the container",
Action: statsAction,
}
func statsAction(context *cli.Context) {
container, err := loadConfig()
if err != nil {
log.Fatal(err)
}
state, err := libcontainer.GetState(dataPath)
if err != nil {
log.Fatal(err)
}
stats, err := libcontainer.GetStats(container, state)
if err != nil {
log.Fatal(err)
}
data, err := json.MarshalIndent(stats, "", "\t")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", data)
Name: "stats",
Usage: "display statistics for the container",
Flags: []cli.Flag{
cli.StringFlag{Name: "id", Value: "nsinit", Usage: "specify the ID for a container"},
},
Action: func(context *cli.Context) {
container, err := getContainer(context)
if err != nil {
fatal(err)
}
stats, err := container.Stats()
if err != nil {
fatal(err)
}
data, err := json.MarshalIndent(stats, "", "\t")
if err != nil {
fatal(err)
}
fmt.Printf("%s", data)
},
}

65
nsinit/tty.go Normal file
View File

@ -0,0 +1,65 @@
package main
import (
"io"
"os"
"github.com/codegangsta/cli"
"github.com/docker/docker/pkg/term"
"github.com/docker/libcontainer"
)
func newTty(context *cli.Context, rootuid int) (*tty, error) {
if context.Bool("tty") {
console, err := libcontainer.NewConsole(rootuid, rootuid)
if err != nil {
return nil, err
}
return &tty{
console: console,
}, nil
}
return &tty{}, nil
}
type tty struct {
console libcontainer.Console
state *term.State
}
func (t *tty) Close() error {
if t.console != nil {
t.console.Close()
}
if t.state != nil {
term.RestoreTerminal(os.Stdin.Fd(), t.state)
}
return nil
}
func (t *tty) attach(process *libcontainer.Process) error {
if t.console != nil {
go io.Copy(t.console, os.Stdin)
go io.Copy(os.Stdout, t.console)
state, err := term.SetRawTerminal(os.Stdin.Fd())
if err != nil {
return err
}
t.state = state
process.Stderr = nil
process.Stdout = nil
process.Stdin = nil
}
return nil
}
func (t *tty) resize() error {
if t.console == nil {
return nil
}
ws, err := term.GetWinsize(os.Stdin.Fd())
if err != nil {
return err
}
return term.SetWinsize(t.console.Fd(), ws)
}

View File

@ -2,89 +2,57 @@ package main
import (
"encoding/json"
"log"
"fmt"
"os"
"path/filepath"
"github.com/codegangsta/cli"
"github.com/docker/libcontainer"
"github.com/docker/libcontainer/configs"
)
// rFunc is a function registration for calling after an execin
type rFunc struct {
Usage string
Action func(*libcontainer.Config, []string)
}
func loadConfig() (*libcontainer.Config, error) {
f, err := os.Open(filepath.Join(dataPath, "container.json"))
func loadConfig(context *cli.Context) (*configs.Config, error) {
if context.Bool("create") {
config := getTemplate()
modify(config, context)
return config, nil
}
f, err := os.Open(context.String("config"))
if err != nil {
return nil, err
}
defer f.Close()
var container *libcontainer.Config
if err := json.NewDecoder(f).Decode(&container); err != nil {
return nil, err
}
return container, nil
}
func openLog(name string) error {
f, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0755)
if err != nil {
return err
}
log.SetOutput(f)
return nil
}
func findUserArgs() []string {
i := 0
for _, a := range os.Args {
i++
if a == "--" {
break
}
}
return os.Args[i:]
}
// loadConfigFromFd loads a container's config from the sync pipe that is provided by
// fd 3 when running a process
func loadConfigFromFd() (*libcontainer.Config, error) {
pipe := os.NewFile(3, "pipe")
defer pipe.Close()
var config *libcontainer.Config
if err := json.NewDecoder(pipe).Decode(&config); err != nil {
var config *configs.Config
if err := json.NewDecoder(f).Decode(&config); err != nil {
return nil, err
}
return config, nil
}
func preload(context *cli.Context) error {
if logPath != "" {
if err := openLog(logPath); err != nil {
return err
}
}
return nil
func loadFactory(context *cli.Context) (libcontainer.Factory, error) {
return libcontainer.New(context.GlobalString("root"), libcontainer.Cgroupfs)
}
func runFunc(f *rFunc) {
userArgs := findUserArgs()
config, err := loadConfigFromFd()
func getContainer(context *cli.Context) (libcontainer.Container, error) {
factory, err := loadFactory(context)
if err != nil {
log.Fatalf("unable to receive config from sync pipe: %s", err)
return nil, err
}
f.Action(config, userArgs)
container, err := factory.Load(context.String("id"))
if err != nil {
return nil, err
}
return container, nil
}
func fatal(err error) {
if lerr, ok := err.(libcontainer.Error); ok {
lerr.Detail(os.Stderr)
os.Exit(1)
}
fmt.Fprintln(os.Stderr, err)
}
func fatalf(t string, v ...interface{}) {
fmt.Fprintf(os.Stderr, t, v...)
os.Exit(1)
}

View File

@ -2,26 +2,28 @@ package libcontainer
import "io"
// Configuration for a process to be run inside a container.
type ProcessConfig struct {
// Process specifies the configuration and IO for a process inside
// a container.
type Process struct {
// The command to be run followed by any arguments.
Args []string
// Map of environment variables to their values.
// Env specifies the environment variables for the process.
Env []string
// User will set the uid and gid of the executing process running inside the container
// local to the contaienr's user and group configuration.
User string
// Cwd will change the processes current working directory inside the container's rootfs.
Cwd string
// Stdin is a pointer to a reader which provides the standard input stream.
Stdin io.Reader
// Stdout is a pointer to a writer which receives the standard output stream.
Stdout io.Writer
// Stderr is a pointer to a writer which receives the standard error stream.
//
// If a reader or writer is nil, the input stream is assumed to be empty and the output is
// discarded.
//
// The readers and writers, if supplied, are closed when the process terminates. Their Close
// methods should be idempotent.
//
// Stdout and Stderr may refer to the same writer in which case the output is interspersed.
Stdin io.ReadCloser
Stdout io.WriteCloser
Stderr io.WriteCloser
Stderr io.Writer
}

View File

@ -1,196 +1,341 @@
{
"capabilities": [
"CHOWN",
"DAC_OVERRIDE",
"FOWNER",
"MKNOD",
"NET_RAW",
"SETGID",
"SETUID",
"SETFCAP",
"SETPCAP",
"NET_BIND_SERVICE",
"SYS_CHROOT",
"KILL"
],
"cgroups": {
"allowed_devices": [
{
"cgroup_permissions": "m",
"major_number": -1,
"minor_number": -1,
"type": 99
},
{
"cgroup_permissions": "m",
"major_number": -1,
"minor_number": -1,
"type": 98
},
{
"cgroup_permissions": "rwm",
"major_number": 5,
"minor_number": 1,
"path": "/dev/console",
"type": 99
},
{
"cgroup_permissions": "rwm",
"major_number": 4,
"path": "/dev/tty0",
"type": 99
},
{
"cgroup_permissions": "rwm",
"major_number": 4,
"minor_number": 1,
"path": "/dev/tty1",
"type": 99
},
{
"cgroup_permissions": "rwm",
"major_number": 136,
"minor_number": -1,
"type": 99
},
{
"cgroup_permissions": "rwm",
"major_number": 5,
"minor_number": 2,
"type": 99
},
{
"cgroup_permissions": "rwm",
"major_number": 10,
"minor_number": 200,
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 3,
"path": "/dev/null",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 5,
"path": "/dev/zero",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 7,
"path": "/dev/full",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 5,
"path": "/dev/tty",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 9,
"path": "/dev/urandom",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 8,
"path": "/dev/random",
"type": 99
}
],
"name": "docker-koye",
"parent": "docker"
},
"restrict_sys": true,
"apparmor_profile": "docker-default",
"mount_config": {
"device_nodes": [
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 3,
"path": "/dev/null",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 5,
"path": "/dev/zero",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 7,
"path": "/dev/full",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 5,
"path": "/dev/tty",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 9,
"path": "/dev/urandom",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 8,
"path": "/dev/random",
"type": 99
}
]
},
"environment": [
"HOME=/",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HOSTNAME=koye",
"TERM=xterm"
],
"hostname": "koye",
"namespaces": [
{"type":"NEWIPC"},
{"type": "NEWNET"},
{"type": "NEWNS"},
{"type": "NEWPID"},
{"type": "NEWUTS"}
],
"networks": [
{
"address": "127.0.0.1/0",
"gateway": "localhost",
"mtu": 1500,
"type": "loopback"
}
],
"tty": true,
"user": "daemon"
"no_pivot_root": false,
"parent_death_signal": 0,
"pivot_dir": "",
"rootfs": "/rootfs/jessie",
"readonlyfs": false,
"mounts": [
{
"source": "shm",
"destination": "/dev/shm",
"device": "tmpfs",
"flags": 14,
"data": "mode=1777,size=65536k",
"relabel": ""
},
{
"source": "mqueue",
"destination": "/dev/mqueue",
"device": "mqueue",
"flags": 14,
"data": "",
"relabel": ""
},
{
"source": "sysfs",
"destination": "/sys",
"device": "sysfs",
"flags": 15,
"data": "",
"relabel": ""
}
],
"devices": [
{
"type": 99,
"path": "/dev/fuse",
"major": 10,
"minor": 229,
"permissions": "rwm",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/null",
"major": 1,
"minor": 3,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/zero",
"major": 1,
"minor": 5,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/full",
"major": 1,
"minor": 7,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/tty",
"major": 5,
"minor": 0,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/urandom",
"major": 1,
"minor": 9,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/random",
"major": 1,
"minor": 8,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
}
],
"mount_label": "",
"hostname": "nsinit",
"console": "",
"namespaces": [
{
"type": "NEWNS",
"path": ""
},
{
"type": "NEWUTS",
"path": ""
},
{
"type": "NEWIPC",
"path": ""
},
{
"type": "NEWPID",
"path": ""
},
{
"type": "NEWNET",
"path": ""
}
],
"capabilities": [
"CHOWN",
"DAC_OVERRIDE",
"FSETID",
"FOWNER",
"MKNOD",
"NET_RAW",
"SETGID",
"SETUID",
"SETFCAP",
"SETPCAP",
"NET_BIND_SERVICE",
"SYS_CHROOT",
"KILL",
"AUDIT_WRITE"
],
"networks": [
{
"type": "loopback",
"name": "",
"bridge": "",
"mac_address": "",
"address": "127.0.0.1/0",
"gateway": "localhost",
"ipv6_address": "",
"ipv6_gateway": "",
"mtu": 0,
"txqueuelen": 0,
"host_interface_name": ""
}
],
"routes": null,
"cgroups": {
"name": "libcontainer",
"parent": "nsinit",
"allow_all_devices": false,
"allowed_devices": [
{
"type": 99,
"path": "",
"major": -1,
"minor": -1,
"permissions": "m",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 98,
"path": "",
"major": -1,
"minor": -1,
"permissions": "m",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/console",
"major": 5,
"minor": 1,
"permissions": "rwm",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/tty0",
"major": 4,
"minor": 0,
"permissions": "rwm",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/tty1",
"major": 4,
"minor": 1,
"permissions": "rwm",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "",
"major": 136,
"minor": -1,
"permissions": "rwm",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "",
"major": 5,
"minor": 2,
"permissions": "rwm",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "",
"major": 10,
"minor": 200,
"permissions": "rwm",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/null",
"major": 1,
"minor": 3,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/zero",
"major": 1,
"minor": 5,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/full",
"major": 1,
"minor": 7,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/tty",
"major": 5,
"minor": 0,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/urandom",
"major": 1,
"minor": 9,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/random",
"major": 1,
"minor": 8,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
}
],
"memory": 0,
"memory_reservation": 0,
"memory_swap": 0,
"cpu_shares": 0,
"cpu_quota": 0,
"cpu_period": 0,
"cpuset_cpus": "",
"cpuset_mems": "",
"blkio_weight": 0,
"freezer": "",
"slice": ""
},
"apparmor_profile": "docker-default",
"process_label": "",
"rlimits": [
{
"type": 7,
"hard": 1024,
"soft": 1024
}
],
"additional_groups": null,
"uid_mappings": null,
"gid_mappings": null,
"mask_paths": [
"/proc/kcore"
],
"readonly_paths": [
"/proc/sys",
"/proc/sysrq-trigger",
"/proc/irq",
"/proc/bus"
]
}

View File

@ -1,202 +1,354 @@
{
"capabilities": [
"CHOWN",
"DAC_OVERRIDE",
"FOWNER",
"MKNOD",
"NET_RAW",
"SETGID",
"SETUID",
"SETFCAP",
"SETPCAP",
"NET_BIND_SERVICE",
"SYS_CHROOT",
"KILL"
],
"cgroups": {
"allowed_devices": [
{
"cgroup_permissions": "m",
"major_number": -1,
"minor_number": -1,
"type": 99
},
{
"cgroup_permissions": "m",
"major_number": -1,
"minor_number": -1,
"type": 98
},
{
"cgroup_permissions": "rwm",
"major_number": 5,
"minor_number": 1,
"path": "/dev/console",
"type": 99
},
{
"cgroup_permissions": "rwm",
"major_number": 4,
"path": "/dev/tty0",
"type": 99
},
{
"cgroup_permissions": "rwm",
"major_number": 4,
"minor_number": 1,
"path": "/dev/tty1",
"type": 99
},
{
"cgroup_permissions": "rwm",
"major_number": 136,
"minor_number": -1,
"type": 99
},
{
"cgroup_permissions": "rwm",
"major_number": 5,
"minor_number": 2,
"type": 99
},
{
"cgroup_permissions": "rwm",
"major_number": 10,
"minor_number": 200,
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 3,
"path": "/dev/null",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 5,
"path": "/dev/zero",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 7,
"path": "/dev/full",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 5,
"path": "/dev/tty",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 9,
"path": "/dev/urandom",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 8,
"path": "/dev/random",
"type": 99
}
],
"name": "docker-koye",
"parent": "docker"
},
"restrict_sys": true,
"mount_config": {
"device_nodes": [
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 3,
"path": "/dev/null",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 5,
"path": "/dev/zero",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 7,
"path": "/dev/full",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 5,
"path": "/dev/tty",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 9,
"path": "/dev/urandom",
"type": 99
},
{
"cgroup_permissions": "rwm",
"file_mode": 438,
"major_number": 1,
"minor_number": 8,
"path": "/dev/random",
"type": 99
}
]
},
"environment": [
"HOME=/",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HOSTNAME=koye",
"TERM=xterm"
],
"hostname": "koye",
"namespaces": [
{"type": "NEWIPC"},
{"type": "NEWNET"},
{"type": "NEWNS"},
{"type": "NEWPID"},
{"type": "NEWUTS"}
],
"networks": [
"no_pivot_root": false,
"parent_death_signal": 0,
"pivot_dir": "",
"rootfs": "/rootfs/jessie",
"readonlyfs": false,
"mounts": [
{
"source": "shm",
"destination": "/dev/shm",
"device": "tmpfs",
"flags": 14,
"data": "mode=1777,size=65536k",
"relabel": ""
},
{
"source": "mqueue",
"destination": "/dev/mqueue",
"device": "mqueue",
"flags": 14,
"data": "",
"relabel": ""
},
{
"source": "sysfs",
"destination": "/sys",
"device": "sysfs",
"flags": 15,
"data": "",
"relabel": ""
}
],
"devices": [
{
"type": 99,
"path": "/dev/fuse",
"major": 10,
"minor": 229,
"permissions": "rwm",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/null",
"major": 1,
"minor": 3,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/zero",
"major": 1,
"minor": 5,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/full",
"major": 1,
"minor": 7,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/tty",
"major": 5,
"minor": 0,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/urandom",
"major": 1,
"minor": 9,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/random",
"major": 1,
"minor": 8,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
}
],
"mount_label": "",
"hostname": "koye",
"console": "",
"namespaces": [
{
"type": "NEWNS",
"path": ""
},
{
"type": "NEWUTS",
"path": ""
},
{
"type": "NEWIPC",
"path": ""
},
{
"type": "NEWPID",
"path": ""
},
{
"type": "NEWNET",
"path": ""
}
],
"capabilities": [
"CHOWN",
"DAC_OVERRIDE",
"FSETID",
"FOWNER",
"MKNOD",
"NET_RAW",
"SETGID",
"SETUID",
"SETFCAP",
"SETPCAP",
"NET_BIND_SERVICE",
"SYS_CHROOT",
"KILL",
"AUDIT_WRITE"
],
"networks": [
{
"type": "loopback",
"name": "",
"bridge": "",
"mac_address": "",
"address": "127.0.0.1/0",
"gateway": "localhost",
"ipv6_address": "",
"ipv6_gateway": "",
"mtu": 0,
"txqueuelen": 0,
"host_interface_name": ""
},
{
"address": "127.0.0.1/0",
"gateway": "localhost",
"mtu": 1500,
"type": "loopback"
},
{
"address": "172.17.0.101/16",
"bridge": "docker0",
"veth_prefix": "veth",
"gateway": "172.17.42.1",
"mtu": 1500,
"type": "veth"
}
],
"tty": true
"type": "veth",
"name": "eth0",
"bridge": "docker0",
"mac_address": "",
"address": "172.17.0.101/16",
"gateway": "172.17.42.1",
"ipv6_address": "",
"ipv6_gateway": "",
"mtu": 1500,
"txqueuelen": 0,
"host_interface_name": "vethnsinit"
}
],
"routes": null,
"cgroups": {
"name": "libcontainer",
"parent": "nsinit",
"allow_all_devices": false,
"allowed_devices": [
{
"type": 99,
"path": "",
"major": -1,
"minor": -1,
"permissions": "m",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 98,
"path": "",
"major": -1,
"minor": -1,
"permissions": "m",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/console",
"major": 5,
"minor": 1,
"permissions": "rwm",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/tty0",
"major": 4,
"minor": 0,
"permissions": "rwm",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/tty1",
"major": 4,
"minor": 1,
"permissions": "rwm",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "",
"major": 136,
"minor": -1,
"permissions": "rwm",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "",
"major": 5,
"minor": 2,
"permissions": "rwm",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "",
"major": 10,
"minor": 200,
"permissions": "rwm",
"file_mode": 0,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/null",
"major": 1,
"minor": 3,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/zero",
"major": 1,
"minor": 5,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/full",
"major": 1,
"minor": 7,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/tty",
"major": 5,
"minor": 0,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/urandom",
"major": 1,
"minor": 9,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
},
{
"type": 99,
"path": "/dev/random",
"major": 1,
"minor": 8,
"permissions": "rwm",
"file_mode": 438,
"uid": 0,
"gid": 0
}
],
"memory": 0,
"memory_reservation": 0,
"memory_swap": 0,
"cpu_shares": 0,
"cpu_quota": 0,
"cpu_period": 0,
"cpuset_cpus": "",
"cpuset_mems": "",
"blkio_weight": 0,
"freezer": "",
"slice": ""
},
"apparmor_profile": "",
"process_label": "",
"rlimits": [
{
"type": 7,
"hard": 1024,
"soft": 1024
}
],
"additional_groups": null,
"uid_mappings": null,
"gid_mappings": null,
"mask_paths": [
"/proc/kcore"
],
"readonly_paths": [
"/proc/sys",
"/proc/sysrq-trigger",
"/proc/irq",
"/proc/bus"
]
}

Some files were not shown because too many files have changed in this diff Show More