Merge pull request #1413 from crosbymichael/user-opts

Implement WithUsername for /etc/passwd lookup
This commit is contained in:
Kenfe-Mickaël Laventure 2017-08-24 11:49:23 -07:00 committed by GitHub
commit a6ce1ef2a1
23 changed files with 946 additions and 307 deletions

View File

@ -60,25 +60,25 @@ image, err := client.Pull(context, "docker.io/library/redis:latest")
err := client.Push(context, "docker.io/library/redis:latest", image.Target())
```
### OCI Runtime Specification
containerd fully supports the OCI runtime specification for running containers. We have built in functions to help you generate runtime specifications based on images as well as custom parameters.
```go
spec, err := containerd.GenerateSpec(containerd.WithImageConfig(context, image))
```
### Containers
In containerd, a container is a metadata object. Resources such as an OCI runtime specification, image, root filesystem, and other metadata can be attached to a container.
```go
redis, err := client.NewContainer(context, "redis-master",
containerd.WithSpec(spec),
)
redis, err := client.NewContainer(context, "redis-master")
defer redis.Delete(context)
```
### OCI Runtime Specification
containerd fully supports the OCI runtime specification for running containers. We have built in functions to help you generate runtime specifications based on images as well as custom parameters.
You can specify options when creating a container about how to modify the specification.
```go
redis, err := client.NewContainer(context, "redis-master", containerd.WithNewSpec(containerd.WithImageConfig(image)))
```
## Root Filesystems
containerd allows you to use overlay or snapshot filesystems with your containers. It comes with builtin support for overlayfs and btrfs.
@ -89,16 +89,17 @@ image, err := client.Pull(context, "docker.io/library/redis:latest", containerd.
// allocate a new RW root filesystem for a container based on the image
redis, err := client.NewContainer(context, "redis-master",
containerd.WithSpec(spec),
containerd.WithNewSnapshot("redis-rootfs", image),
containerd.WithNewSpec(containerd.WithImageConfig(image)),
)
// use a readonly filesystem with multiple containers
for i := 0; i < 10; i++ {
id := fmt.Sprintf("id-%s", i)
container, err := client.NewContainer(ctx, id,
containerd.WithSpec(spec),
containerd.WithNewSnapshotView(id, image),
containerd.WithNewSpec(containerd.WithImageConfig(image)),
)
}
```

View File

@ -2,11 +2,16 @@
package containerd
import specs "github.com/opencontainers/runtime-spec/specs-go"
import (
"context"
"github.com/containerd/containerd/containers"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
// WithApparmor sets the provided apparmor profile to the spec
func WithApparmorProfile(profile string) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Process.ApparmorProfile = profile
return nil
}

View File

@ -20,7 +20,7 @@ func BenchmarkContainerCreate(b *testing.B) {
b.Error(err)
return
}
spec, err := GenerateSpec(WithImageConfig(ctx, image), withTrue())
spec, err := GenerateSpec(ctx, client, nil, WithImageConfig(image), withTrue())
if err != nil {
b.Error(err)
return
@ -63,7 +63,7 @@ func BenchmarkContainerStart(b *testing.B) {
b.Error(err)
return
}
spec, err := GenerateSpec(WithImageConfig(ctx, image), withTrue())
spec, err := GenerateSpec(ctx, client, nil, WithImageConfig(image), withTrue())
if err != nil {
b.Error(err)
return

View File

@ -98,7 +98,7 @@ func test(c config) error {
return err
}
logrus.Info("generating spec from image")
spec, err := containerd.GenerateSpec(containerd.WithImageConfig(ctx, image), containerd.WithProcessArgs("true"))
spec, err := containerd.GenerateSpec(ctx, client, nil, containerd.WithImageConfig(image), containerd.WithProcessArgs("true"))
if err != nil {
return err
}

View File

@ -8,6 +8,7 @@ import (
"github.com/containerd/console"
"github.com/containerd/containerd"
"github.com/containerd/containerd/containers"
digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
@ -24,7 +25,7 @@ type killer interface {
}
func withEnv(context *cli.Context) containerd.SpecOpts {
return func(s *specs.Spec) error {
return func(_ gocontext.Context, _ *containerd.Client, _ *containers.Container, s *specs.Spec) error {
env := context.StringSlice("env")
if len(env) > 0 {
s.Process.Env = replaceOrAppendEnvValues(s.Process.Env, env)
@ -34,7 +35,7 @@ func withEnv(context *cli.Context) containerd.SpecOpts {
}
func withMounts(context *cli.Context) containerd.SpecOpts {
return func(s *specs.Spec) error {
return func(_ gocontext.Context, _ *containerd.Client, _ *containers.Container, s *specs.Spec) error {
for _, mount := range context.StringSlice("mount") {
m, err := parseMountFlag(mount)
if err != nil {

View File

@ -90,7 +90,7 @@ func newContainer(ctx gocontext.Context, client *containerd.Client, context *cli
if err != nil {
return nil, err
}
opts = append(opts, containerd.WithImageConfig(ctx, image))
opts = append(opts, containerd.WithImageConfig(image))
cOpts = append(cOpts, containerd.WithImage(image))
cOpts = append(cOpts, containerd.WithSnapshotter(context.String("snapshotter")))
if context.Bool("readonly") {
@ -111,11 +111,7 @@ func newContainer(ctx gocontext.Context, client *containerd.Client, context *cli
if context.Bool("net-host") {
opts = append(opts, setHostNetworking())
}
spec, err := containerd.GenerateSpec(opts...)
if err != nil {
return nil, err
}
cOpts = append([]containerd.NewContainerOpts{containerd.WithSpec(spec)}, cOpts...)
cOpts = append([]containerd.NewContainerOpts{containerd.WithNewSpec(opts...)}, cOpts...)
return client.NewContainer(ctx, id, cOpts...)
}

View File

@ -6,6 +6,7 @@ import (
"github.com/containerd/console"
"github.com/containerd/containerd"
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/log"
digest "github.com/opencontainers/go-digest"
@ -25,7 +26,7 @@ func init() {
}
func withLayers(context *cli.Context) containerd.SpecOpts {
return func(s *specs.Spec) error {
return func(ctx gocontext.Context, client *containerd.Client, c *containers.Container, s *specs.Spec) error {
l := context.StringSlice("layer")
if l == nil {
return errors.Wrap(errdefs.ErrInvalidArgument, "base layers must be specified with `--layer`")
@ -65,7 +66,7 @@ func handleConsoleResize(ctx gocontext.Context, task resizer, con console.Consol
func withTTY(terminal bool) containerd.SpecOpts {
if !terminal {
return func(s *specs.Spec) error {
return func(ctx gocontext.Context, client *containerd.Client, c *containers.Container, s *specs.Spec) error {
s.Process.Terminal = false
return nil
}
@ -85,8 +86,6 @@ func setHostNetworking() containerd.SpecOpts {
func newContainer(ctx gocontext.Context, client *containerd.Client, context *cli.Context) (containerd.Container, error) {
var (
err error
// ref = context.Args().First()
id = context.Args().Get(1)
args = context.Args()[2:]
@ -109,13 +108,8 @@ func newContainer(ctx gocontext.Context, client *containerd.Client, context *cli
opts = append(opts, containerd.WithProcessArgs(args...))
}
spec, err := containerd.GenerateSpec(opts...)
if err != nil {
return nil, err
}
return client.NewContainer(ctx, id,
containerd.WithSpec(spec),
containerd.WithNewSpec(opts...),
containerd.WithContainerLabels(labels),
containerd.WithRuntime(context.String("runtime")),
// TODO(mlaventure): containerd.WithImage(image),

View File

@ -28,12 +28,7 @@ func TestCheckpointRestore(t *testing.T) {
t.Error(err)
return
}
spec, err := GenerateSpec(WithImageConfig(ctx, image), WithProcessArgs("sleep", "100"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), WithNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(WithImageConfig(image), WithProcessArgs("sleep", "100")), WithNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -113,12 +108,7 @@ func TestCheckpointRestoreNewContainer(t *testing.T) {
t.Error(err)
return
}
spec, err := GenerateSpec(WithImageConfig(ctx, image), WithProcessArgs("sleep", "100"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), WithNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(WithImageConfig(image), WithProcessArgs("sleep", "100")), WithNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -211,12 +201,7 @@ func TestCheckpointLeaveRunning(t *testing.T) {
t.Error(err)
return
}
spec, err := GenerateSpec(WithImageConfig(ctx, image), WithProcessArgs("sleep", "100"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), WithNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(WithImageConfig(image), WithProcessArgs("sleep", "100")), WithNewSnapshot(id, image))
if err != nil {
t.Error(err)
return

View File

@ -8,12 +8,14 @@ import (
"fmt"
"io"
"runtime"
"strings"
"sync"
"syscall"
"testing"
"time"
"github.com/containerd/cgroups"
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/linux/runcopts"
specs "github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/sys/unix"
@ -39,16 +41,14 @@ func TestContainerUpdate(t *testing.T) {
t.Error(err)
return
}
spec, err := generateSpec(WithImageConfig(ctx, image), withProcessArgs("sleep", "30"))
if err != nil {
t.Error(err)
return
}
limit := int64(32 * 1024 * 1024)
spec.Linux.Resources.Memory = &specs.LinuxMemory{
memory := func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Linux.Resources.Memory = &specs.LinuxMemory{
Limit: &limit,
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), WithNewSnapshot(id, image))
return nil
}
container, err := client.NewContainer(ctx, id, WithNewSpec(WithImageConfig(image), withProcessArgs("sleep", "30"), memory), WithNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -127,12 +127,7 @@ func TestShimInCgroup(t *testing.T) {
t.Error(err)
return
}
spec, err := GenerateSpec(WithImageConfig(ctx, image), WithProcessArgs("sleep", "30"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), WithNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(WithImageConfig(image), WithProcessArgs("sleep", "30")), WithNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -202,12 +197,7 @@ func TestDaemonRestart(t *testing.T) {
return
}
spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "30"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "30")), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -293,12 +283,7 @@ func TestContainerAttach(t *testing.T) {
}
}
spec, err := generateSpec(withImageConfig(ctx, image), withCat())
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withCat()), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -380,3 +365,80 @@ func TestContainerAttach(t *testing.T) {
t.Errorf("expected output %q but received %q", expected, output)
}
}
func TestContainerUsername(t *testing.T) {
t.Parallel()
client, err := newClient(t, address)
if err != nil {
t.Fatal(err)
}
defer client.Close()
var (
image Image
ctx, cancel = testContext()
id = t.Name()
)
defer cancel()
if runtime.GOOS != "windows" {
image, err = client.GetImage(ctx, testImage)
if err != nil {
t.Error(err)
return
}
}
direct, err := NewDirectIO(ctx, false)
if err != nil {
t.Error(err)
return
}
defer direct.Delete()
var (
wg sync.WaitGroup
buf = bytes.NewBuffer(nil)
)
wg.Add(1)
go func() {
defer wg.Done()
io.Copy(buf, direct.Stdout)
}()
// squid user in the alpine image has a uid of 31
container, err := client.NewContainer(ctx, id,
withNewSnapshot(id, image),
WithNewSpec(withImageConfig(image), WithUsername("squid"), WithProcessArgs("id", "-u")),
)
if err != nil {
t.Error(err)
return
}
defer container.Delete(ctx, WithSnapshotCleanup)
task, err := container.NewTask(ctx, direct.IOCreate)
if err != nil {
t.Error(err)
return
}
defer task.Delete(ctx)
statusC, err := task.Wait(ctx)
if err != nil {
t.Error(err)
return
}
if err := task.Start(ctx); err != nil {
t.Error(err)
return
}
<-statusC
wg.Wait()
output := strings.TrimSuffix(buf.String(), "\n")
if output != "31" {
t.Errorf("expected squid uid to be 31 but received %q", output)
}
}

View File

@ -55,16 +55,10 @@ func TestNewContainer(t *testing.T) {
}
defer client.Close()
spec, err := generateSpec()
if err != nil {
t.Error(err)
return
}
ctx, cancel := testContext()
defer cancel()
container, err := client.NewContainer(ctx, id, WithSpec(spec))
container, err := client.NewContainer(ctx, id, WithNewSpec())
if err != nil {
t.Error(err)
return
@ -106,13 +100,7 @@ func TestContainerStart(t *testing.T) {
return
}
}
spec, err := generateSpec(withImageConfig(ctx, image), withExitStatus(7))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withExitStatus(7)), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -184,13 +172,7 @@ func TestContainerOutput(t *testing.T) {
return
}
}
spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("echo", expected))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("echo", expected)), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -258,12 +240,7 @@ func TestContainerExec(t *testing.T) {
}
}
spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "100"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "100")), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -287,6 +264,11 @@ func TestContainerExec(t *testing.T) {
t.Error(err)
return
}
spec, err := container.Spec()
if err != nil {
t.Error(err)
return
}
// start an exec process without running the original container process info
processSpec := spec.Process
@ -357,12 +339,7 @@ func TestContainerPids(t *testing.T) {
}
}
spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "100"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "100")), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -435,12 +412,7 @@ func TestContainerCloseIO(t *testing.T) {
}
}
spec, err := generateSpec(withImageConfig(ctx, image), withCat())
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withCat()), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -505,12 +477,7 @@ func TestDeleteRunningContainer(t *testing.T) {
}
}
spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "100"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "100")), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -573,12 +540,7 @@ func TestContainerKill(t *testing.T) {
}
}
spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "10"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "10")), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -642,12 +604,7 @@ func TestContainerNoBinaryExists(t *testing.T) {
}
}
spec, err := generateSpec(withImageConfig(ctx, image), WithProcessArgs("nothing"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), WithProcessArgs("nothing")), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -696,12 +653,7 @@ func TestContainerExecNoBinaryExists(t *testing.T) {
}
}
spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "100"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "100")), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -723,6 +675,11 @@ func TestContainerExecNoBinaryExists(t *testing.T) {
t.Error(err)
return
}
spec, err := container.Spec()
if err != nil {
t.Error(err)
return
}
// start an exec process without running the original container process
processSpec := spec.Process
@ -769,17 +726,11 @@ func TestUserNamespaces(t *testing.T) {
}
}
spec, err := generateSpec(
withImageConfig(ctx, image),
container, err := client.NewContainer(ctx, id,
WithNewSpec(withImageConfig(image),
withExitStatus(7),
withUserNamespace(0, 1000, 10000),
)
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id,
WithSpec(spec),
),
withRemappedSnapshot(id, image, 1000, 1000),
)
if err != nil {
@ -852,12 +803,7 @@ func TestWaitStoppedTask(t *testing.T) {
}
}
spec, err := generateSpec(withImageConfig(ctx, image), withExitStatus(7))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withExitStatus(7)), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -928,12 +874,7 @@ func TestWaitStoppedProcess(t *testing.T) {
}
}
spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "100"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "100")), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -956,6 +897,11 @@ func TestWaitStoppedProcess(t *testing.T) {
t.Error(err)
return
}
spec, err := container.Spec()
if err != nil {
t.Error(err)
return
}
// start an exec process without running the original container process info
processSpec := spec.Process
@ -1027,12 +973,7 @@ func TestTaskForceDelete(t *testing.T) {
return
}
}
spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "30"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "30")), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -1080,12 +1021,7 @@ func TestProcessForceDelete(t *testing.T) {
return
}
}
spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "30"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "30")), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -1110,6 +1046,12 @@ func TestProcessForceDelete(t *testing.T) {
t.Error(err)
return
}
spec, err := container.Spec()
if err != nil {
t.Error(err)
return
}
processSpec := spec.Process
withExecArgs(processSpec, "sleep", "20")
execID := t.Name() + "_exec"
@ -1160,16 +1102,11 @@ func TestContainerHostname(t *testing.T) {
}
}
spec, err := generateSpec(
withImageConfig(ctx, image),
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image),
withProcessArgs("hostname"),
WithHostname(expected),
)
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
),
withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -1243,12 +1180,7 @@ func TestContainerExitedAtSet(t *testing.T) {
}
}
spec, err := generateSpec(withImageConfig(ctx, image), withTrue())
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withTrue()), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -1315,12 +1247,7 @@ func TestDeleteContainerExecCreated(t *testing.T) {
}
}
spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "100"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, WithSpec(spec), withNewSnapshot(id, image))
container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "100")), withNewSnapshot(id, image))
if err != nil {
t.Error(err)
return
@ -1343,6 +1270,11 @@ func TestDeleteContainerExecCreated(t *testing.T) {
t.Error(err)
return
}
spec, err := container.Spec()
if err != nil {
t.Error(err)
return
}
// start an exec process without running the original container process info
processSpec := spec.Process

View File

@ -90,9 +90,8 @@ Adding your new option to spec generation is as easy as importing your new packa
```go
import "github.com/crosbymichael/monitor"
spec, err := containerd.GenerateSpec(
containerd.WithImageConfig(ctx, image),
monitor.WithHtop,
container, err := client.NewContainer(ctx, id,
containerd.WithNewSpec(containerd.WithImageConfig(image), monitor.WithHtop),
)
```

View File

@ -90,28 +90,19 @@ We use the `containerd.WithPullUnpack` so that we not only fetch and download th
## Creating an OCI Spec and Container
Now that we have an image to base our container off of, we need to generate an OCI runtime specification that the container can be based off of.
Now that we have an image to base our container off of, we need to generate an OCI runtime specification that the container can be based off of as well as the new container.
containerd provides reasonable defaults for generating OCI runtime specs.
There is also an `Opt` for modifying the default config based on the image that we pulled.
```go
spec, err := containerd.GenerateSpec(containerd.WithImageConfig(ctx, image))
if err != nil {
return err
}
```
After we have a spec generated we need to create a container.
The container will be based off of the image, use the runtime information in the spec that was just created, and we will allocate a new read-write snapshot so the container can store any persistent information.
```go
container, err := client.NewContainer(
ctx,
"redis-server",
containerd.WithSpec(spec),
containerd.WithImage(image),
containerd.WithNewSnapshot("redis-server-snapshot", image),
containerd.WithNewSpec(containerd.WithImageConfig(image)),
)
if err != nil {
return err
@ -119,6 +110,8 @@ The container will be based off of the image, use the runtime information in the
defer container.Delete(ctx, containerd.WithSnapshotCleanup)
```
If you have an existing OCI specification created you can use `containerd.WithSpec(spec)` to set it on the container.
When creating a new snapshot for the container we need to provide a snapshot ID as well as the Image that the container will be based on.
By providing a separate snapshot ID than the container ID we can easily reuse, existing snapshots across different containers.
@ -244,19 +237,13 @@ func redisExample() error {
return err
}
// generate an OCI runtime spec using the Args, Env, etc from the redis image that we pulled
spec, err := containerd.GenerateSpec(containerd.WithImageConfig(ctx, image))
if err != nil {
return err
}
// create a container
container, err := client.NewContainer(
ctx,
"redis-server",
containerd.WithSpec(spec),
containerd.WithImage(image),
containerd.WithNewSnapshot("redis-server-snapshot", image),
containerd.WithNewSpec(containerd.WithImageConfig(image)),
)
if err != nil {
return err

View File

@ -6,17 +6,14 @@ import (
"context"
"fmt"
"github.com/containerd/containerd/containers"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
const newLine = "\n"
func generateSpec(opts ...SpecOpts) (*specs.Spec, error) {
return GenerateSpec(opts...)
}
func withExitStatus(es int) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Process.Args = []string{"sh", "-c", fmt.Sprintf("exit %d", es)}
return nil
}
@ -42,14 +39,9 @@ func withExecArgs(s *specs.Process, args ...string) {
s.Args = args
}
func withImageConfig(ctx context.Context, i Image) SpecOpts {
return WithImageConfig(ctx, i)
}
func withNewSnapshot(id string, i Image) NewContainerOpts {
return WithNewSnapshot(id, i)
}
var withUserNamespace = WithUserNamespace
var withRemappedSnapshot = WithRemappedSnapshot
var (
withUserNamespace = WithUserNamespace
withRemappedSnapshot = WithRemappedSnapshot
withNewSnapshot = WithNewSnapshot
withImageConfig = WithImageConfig
)

View File

@ -12,19 +12,8 @@ import (
const newLine = "\r\n"
func generateSpec(opts ...SpecOpts) (*specs.Spec, error) {
spec, err := GenerateSpec(opts...)
if err != nil {
return nil, err
}
spec.Windows.LayerFolders = dockerLayerFolders
return spec, nil
}
func withExitStatus(es int) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Process.Args = []string{"powershell", "-noprofile", "exit", strconv.Itoa(es)}
return nil
}
@ -50,9 +39,9 @@ func withExecArgs(s *specs.Process, args ...string) {
s.Args = append([]string{"powershell", "-noprofile"}, args...)
}
func withImageConfig(ctx context.Context, i Image) SpecOpts {
// TODO: when windows has a snapshotter remove the withImageConfig helper
return func(s *specs.Spec) error {
func withImageConfig(i Image) SpecOpts {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Windows.LayerFolders = dockerLayerFolders
return nil
}
}
@ -65,9 +54,7 @@ func withNewSnapshot(id string, i Image) NewContainerOpts {
}
func withUserNamespace(u, g, s uint32) SpecOpts {
return func(s *specs.Spec) error {
return nil
}
return withNoop
}
func withRemappedSnapshot(id string, i Image, u, g uint32) NewContainerOpts {
@ -75,3 +62,7 @@ func withRemappedSnapshot(id string, i Image, u, g uint32) NewContainerOpts {
return nil
}
}
func withNoop(_ context.Context, _ *Client, _ *containers.Container, _ *specs.Spec) error {
return nil
}

11
spec.go
View File

@ -1,16 +1,21 @@
package containerd
import specs "github.com/opencontainers/runtime-spec/specs-go"
import (
"context"
"github.com/containerd/containerd/containers"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
// GenerateSpec will generate a default spec from the provided image
// for use as a containerd container
func GenerateSpec(opts ...SpecOpts) (*specs.Spec, error) {
func GenerateSpec(ctx context.Context, client *Client, c *containers.Container, opts ...SpecOpts) (*specs.Spec, error) {
s, err := createDefaultSpec()
if err != nil {
return nil, err
}
for _, o := range opts {
if err := o(s); err != nil {
if err := o(ctx, client, c, s); err != nil {
return nil, err
}
}

View File

@ -1,13 +1,19 @@
package containerd
import specs "github.com/opencontainers/runtime-spec/specs-go"
import (
"context"
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/typeurl"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
// SpecOpts sets spec specific information to a newly generated OCI spec
type SpecOpts func(s *specs.Spec) error
type SpecOpts func(context.Context, *Client, *containers.Container, *specs.Spec) error
// WithProcessArgs replaces the args on the generated spec
func WithProcessArgs(args ...string) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Process.Args = args
return nil
}
@ -15,8 +21,46 @@ func WithProcessArgs(args ...string) SpecOpts {
// WithHostname sets the container's hostname
func WithHostname(name string) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Hostname = name
return nil
}
}
// WithNewSpec generates a new spec for a new container
func WithNewSpec(opts ...SpecOpts) NewContainerOpts {
return func(ctx context.Context, client *Client, c *containers.Container) error {
s, err := createDefaultSpec()
if err != nil {
return err
}
for _, o := range opts {
if err := o(ctx, client, c, s); err != nil {
return err
}
}
any, err := typeurl.MarshalAny(s)
if err != nil {
return err
}
c.Spec = any
return nil
}
}
// WithSpec sets the provided spec on the container
func WithSpec(s *specs.Spec, opts ...SpecOpts) NewContainerOpts {
return func(ctx context.Context, client *Client, c *containers.Container) error {
for _, o := range opts {
if err := o(ctx, client, c, s); err != nil {
return err
}
}
any, err := typeurl.MarshalAny(s)
if err != nil {
return err
}
c.Spec = any
return nil
}
}

View File

@ -6,23 +6,28 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"golang.org/x/sys/unix"
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/typeurl"
"github.com/opencontainers/image-spec/identity"
"github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runc/libcontainer/user"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
)
// WithTTY sets the information on the spec as well as the environment variables for
// using a TTY
func WithTTY(s *specs.Spec) error {
func WithTTY(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Process.Terminal = true
s.Process.Env = append(s.Process.Env, "TERM=xterm")
return nil
@ -30,7 +35,7 @@ func WithTTY(s *specs.Spec) error {
// WithHostNamespace allows a task to run inside the host's linux namespace
func WithHostNamespace(ns specs.LinuxNamespaceType) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
for i, n := range s.Linux.Namespaces {
if n.Type == ns {
s.Linux.Namespaces = append(s.Linux.Namespaces[:i], s.Linux.Namespaces[i+1:]...)
@ -44,7 +49,7 @@ func WithHostNamespace(ns specs.LinuxNamespaceType) SpecOpts {
// WithLinuxNamespace uses the passed in namespace for the spec. If a namespace of the same type already exists in the
// spec, the existing namespace is replaced by the one provided.
func WithLinuxNamespace(ns specs.LinuxNamespace) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
for i, n := range s.Linux.Namespaces {
if n.Type == ns.Type {
before := s.Linux.Namespaces[:i]
@ -60,11 +65,11 @@ func WithLinuxNamespace(ns specs.LinuxNamespace) SpecOpts {
}
// WithImageConfig configures the spec to from the configuration of an Image
func WithImageConfig(ctx context.Context, i Image) SpecOpts {
return func(s *specs.Spec) error {
func WithImageConfig(i Image) SpecOpts {
return func(ctx context.Context, client *Client, c *containers.Container, s *specs.Spec) error {
var (
image = i.(*image)
store = image.client.ContentStore()
store = client.ContentStore()
)
ic, err := image.i.Config(ctx, store)
if err != nil {
@ -100,6 +105,10 @@ func WithImageConfig(ctx context.Context, i Image) SpecOpts {
case 1:
v, err := strconv.ParseUint(parts[0], 0, 10)
if err != nil {
// if we cannot parse as a uint they try to see if it is a username
if err := WithUsername(config.User)(ctx, client, c, s); err != nil {
return err
}
return err
}
uid, gid = uint32(v), uint32(v)
@ -129,7 +138,7 @@ func WithImageConfig(ctx context.Context, i Image) SpecOpts {
// WithRootFSPath specifies unmanaged rootfs path.
func WithRootFSPath(path string, readonly bool) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Root = &specs.Root{
Path: path,
Readonly: readonly,
@ -139,18 +148,6 @@ func WithRootFSPath(path string, readonly bool) SpecOpts {
}
}
// WithSpec sets the provided spec for a new container
func WithSpec(spec *specs.Spec) NewContainerOpts {
return func(ctx context.Context, client *Client, c *containers.Container) error {
any, err := typeurl.MarshalAny(spec)
if err != nil {
return err
}
c.Spec = any
return nil
}
}
// WithResources sets the provided resources on the spec for task updates
func WithResources(resources *specs.LinuxResources) UpdateTaskOpts {
return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error {
@ -160,13 +157,13 @@ func WithResources(resources *specs.LinuxResources) UpdateTaskOpts {
}
// WithNoNewPrivileges sets no_new_privileges on the process for the container
func WithNoNewPrivileges(s *specs.Spec) error {
func WithNoNewPrivileges(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Process.NoNewPrivileges = true
return nil
}
// WithHostHostsFile bind-mounts the host's /etc/hosts into the container as readonly
func WithHostHostsFile(s *specs.Spec) error {
func WithHostHostsFile(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Mounts = append(s.Mounts, specs.Mount{
Destination: "/etc/hosts",
Type: "bind",
@ -177,7 +174,7 @@ func WithHostHostsFile(s *specs.Spec) error {
}
// WithHostResolvconf bind-mounts the host's /etc/resolv.conf into the container as readonly
func WithHostResolvconf(s *specs.Spec) error {
func WithHostResolvconf(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Mounts = append(s.Mounts, specs.Mount{
Destination: "/etc/resolv.conf",
Type: "bind",
@ -188,7 +185,7 @@ func WithHostResolvconf(s *specs.Spec) error {
}
// WithHostLocaltime bind-mounts the host's /etc/localtime into the container as readonly
func WithHostLocaltime(s *specs.Spec) error {
func WithHostLocaltime(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Mounts = append(s.Mounts, specs.Mount{
Destination: "/etc/localtime",
Type: "bind",
@ -201,7 +198,7 @@ func WithHostLocaltime(s *specs.Spec) error {
// WithUserNamespace sets the uid and gid mappings for the task
// this can be called multiple times to add more mappings to the generated spec
func WithUserNamespace(container, host, size uint32) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
var hasUserns bool
for _, ns := range s.Linux.Namespaces {
if ns.Type == specs.UserNamespace {
@ -271,7 +268,7 @@ func WithRemappedSnapshot(id string, i Image, uid, gid uint32) NewContainerOpts
// WithCgroup sets the container's cgroup path
func WithCgroup(path string) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Linux.CgroupsPath = path
return nil
}
@ -279,13 +276,69 @@ func WithCgroup(path string) SpecOpts {
// WithNamespacedCgroup uses the namespace set on the context to create a
// root directory for containers in the cgroup with the id as the subcgroup
func WithNamespacedCgroup(ctx context.Context, id string) SpecOpts {
return func(s *specs.Spec) error {
func WithNamespacedCgroup() SpecOpts {
return func(ctx context.Context, _ *Client, c *containers.Container, s *specs.Spec) error {
namespace, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return err
}
s.Linux.CgroupsPath = filepath.Join("/", namespace, id)
s.Linux.CgroupsPath = filepath.Join("/", namespace, c.ID)
return nil
}
}
// WithUserIDs allows the UID and GID for the Process to be set
func WithUserIDs(uid, gid uint32) SpecOpts {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Process.User.UID = uid
s.Process.User.GID = gid
return nil
}
}
// WithUsername sets the correct UID and GID for the container
// based on the the image's /etc/passwd contents.
// id is the snapshot id that is used
func WithUsername(username string) SpecOpts {
return func(ctx context.Context, client *Client, c *containers.Container, s *specs.Spec) error {
if c.Snapshotter == "" {
return errors.Errorf("no snapshotter set for container")
}
if c.RootFS == "" {
return errors.Errorf("rootfs not created for container")
}
snapshotter := client.SnapshotService(c.Snapshotter)
mounts, err := snapshotter.Mounts(ctx, c.RootFS)
if err != nil {
return err
}
root, err := ioutil.TempDir("", "ctd-username")
if err != nil {
return err
}
defer os.RemoveAll(root)
for _, m := range mounts {
if err := m.Mount(root); err != nil {
return err
}
}
defer unix.Unmount(root, 0)
f, err := os.Open(filepath.Join(root, "/etc/passwd"))
if err != nil {
return err
}
defer f.Close()
users, err := user.ParsePasswdFilter(f, func(u user.User) bool {
return u.Name == username
})
if err != nil {
return err
}
if len(users) == 0 {
return errors.Errorf("no users found for %s", username)
}
u := users[0]
s.Process.User.UID, s.Process.User.GID = uint32(u.Uid), uint32(u.Gid)
return nil
}
}

View File

@ -10,16 +10,15 @@ import (
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/typeurl"
"github.com/opencontainers/image-spec/specs-go/v1"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
func WithImageConfig(ctx context.Context, i Image) SpecOpts {
return func(s *specs.Spec) error {
func WithImageConfig(i Image) SpecOpts {
return func(ctx context.Context, client *Client, _ *containers.Container, s *specs.Spec) error {
var (
image = i.(*image)
store = image.client.ContentStore()
store = client.ContentStore()
)
ic, err := image.i.Config(ctx, store)
if err != nil {
@ -52,7 +51,7 @@ func WithImageConfig(ctx context.Context, i Image) SpecOpts {
}
func WithTTY(width, height int) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Process.Terminal = true
s.Process.ConsoleSize.Width = uint(width)
s.Process.ConsoleSize.Height = uint(height)
@ -60,17 +59,6 @@ func WithTTY(width, height int) SpecOpts {
}
}
func WithSpec(spec *specs.Spec) NewContainerOpts {
return func(ctx context.Context, client *Client, c *containers.Container) error {
any, err := typeurl.MarshalAny(spec)
if err != nil {
return err
}
c.Spec = any
return nil
}
}
func WithResources(resources *specs.WindowsResources) UpdateTaskOpts {
return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error {
r.Resources = resources

View File

@ -3,6 +3,7 @@
package containerd
import (
"context"
"testing"
specs "github.com/opencontainers/runtime-spec/specs-go"
@ -11,7 +12,7 @@ import (
func TestGenerateSpec(t *testing.T) {
t.Parallel()
s, err := GenerateSpec()
s, err := GenerateSpec(context.Background(), nil, nil)
if err != nil {
t.Fatal(err)
}
@ -51,7 +52,7 @@ func TestGenerateSpec(t *testing.T) {
func TestSpecWithTTY(t *testing.T) {
t.Parallel()
s, err := GenerateSpec(WithTTY)
s, err := GenerateSpec(context.Background(), nil, nil, WithTTY)
if err != nil {
t.Fatal(err)
}
@ -68,7 +69,7 @@ func TestWithLinuxNamespace(t *testing.T) {
t.Parallel()
replacedNS := specs.LinuxNamespace{Type: specs.NetworkNamespace, Path: "/var/run/netns/test"}
s, err := GenerateSpec(WithLinuxNamespace(replacedNS))
s, err := GenerateSpec(context.Background(), nil, nil, WithLinuxNamespace(replacedNS))
if err != nil {
t.Fatal(err)
}

View File

@ -0,0 +1,111 @@
package user
import (
"errors"
"golang.org/x/sys/unix"
)
var (
// The current operating system does not provide the required data for user lookups.
ErrUnsupported = errors.New("user lookup: operating system does not provide passwd-formatted data")
// No matching entries found in file.
ErrNoPasswdEntries = errors.New("no matching entries in passwd file")
ErrNoGroupEntries = errors.New("no matching entries in group file")
)
func lookupUser(filter func(u User) bool) (User, error) {
// Get operating system-specific passwd reader-closer.
passwd, err := GetPasswd()
if err != nil {
return User{}, err
}
defer passwd.Close()
// Get the users.
users, err := ParsePasswdFilter(passwd, filter)
if err != nil {
return User{}, err
}
// No user entries found.
if len(users) == 0 {
return User{}, ErrNoPasswdEntries
}
// Assume the first entry is the "correct" one.
return users[0], nil
}
// CurrentUser looks up the current user by their user id in /etc/passwd. If the
// user cannot be found (or there is no /etc/passwd file on the filesystem),
// then CurrentUser returns an error.
func CurrentUser() (User, error) {
return LookupUid(unix.Getuid())
}
// LookupUser looks up a user by their username in /etc/passwd. If the user
// cannot be found (or there is no /etc/passwd file on the filesystem), then
// LookupUser returns an error.
func LookupUser(username string) (User, error) {
return lookupUser(func(u User) bool {
return u.Name == username
})
}
// LookupUid looks up a user by their user id in /etc/passwd. If the user cannot
// be found (or there is no /etc/passwd file on the filesystem), then LookupId
// returns an error.
func LookupUid(uid int) (User, error) {
return lookupUser(func(u User) bool {
return u.Uid == uid
})
}
func lookupGroup(filter func(g Group) bool) (Group, error) {
// Get operating system-specific group reader-closer.
group, err := GetGroup()
if err != nil {
return Group{}, err
}
defer group.Close()
// Get the users.
groups, err := ParseGroupFilter(group, filter)
if err != nil {
return Group{}, err
}
// No user entries found.
if len(groups) == 0 {
return Group{}, ErrNoGroupEntries
}
// Assume the first entry is the "correct" one.
return groups[0], nil
}
// CurrentGroup looks up the current user's group by their primary group id's
// entry in /etc/passwd. If the group cannot be found (or there is no
// /etc/group file on the filesystem), then CurrentGroup returns an error.
func CurrentGroup() (Group, error) {
return LookupGid(unix.Getgid())
}
// LookupGroup looks up a group by its name in /etc/group. If the group cannot
// be found (or there is no /etc/group file on the filesystem), then LookupGroup
// returns an error.
func LookupGroup(groupname string) (Group, error) {
return lookupGroup(func(g Group) bool {
return g.Name == groupname
})
}
// LookupGid looks up a group by its group id in /etc/group. If the group cannot
// be found (or there is no /etc/group file on the filesystem), then LookupGid
// returns an error.
func LookupGid(gid int) (Group, error) {
return lookupGroup(func(g Group) bool {
return g.Gid == gid
})
}

View File

@ -0,0 +1,30 @@
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
package user
import (
"io"
"os"
)
// Unix-specific path to the passwd and group formatted files.
const (
unixPasswdPath = "/etc/passwd"
unixGroupPath = "/etc/group"
)
func GetPasswdPath() (string, error) {
return unixPasswdPath, nil
}
func GetPasswd() (io.ReadCloser, error) {
return os.Open(unixPasswdPath)
}
func GetGroupPath() (string, error) {
return unixGroupPath, nil
}
func GetGroup() (io.ReadCloser, error) {
return os.Open(unixGroupPath)
}

View File

@ -0,0 +1,21 @@
// +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris
package user
import "io"
func GetPasswdPath() (string, error) {
return "", ErrUnsupported
}
func GetPasswd() (io.ReadCloser, error) {
return nil, ErrUnsupported
}
func GetGroupPath() (string, error) {
return "", ErrUnsupported
}
func GetGroup() (io.ReadCloser, error) {
return nil, ErrUnsupported
}

View File

@ -0,0 +1,441 @@
package user
import (
"bufio"
"fmt"
"io"
"os"
"strconv"
"strings"
)
const (
minId = 0
maxId = 1<<31 - 1 //for 32-bit systems compatibility
)
var (
ErrRange = fmt.Errorf("uids and gids must be in range %d-%d", minId, maxId)
)
type User struct {
Name string
Pass string
Uid int
Gid int
Gecos string
Home string
Shell string
}
type Group struct {
Name string
Pass string
Gid int
List []string
}
func parseLine(line string, v ...interface{}) {
if line == "" {
return
}
parts := strings.Split(line, ":")
for i, p := range parts {
// Ignore cases where we don't have enough fields to populate the arguments.
// Some configuration files like to misbehave.
if len(v) <= i {
break
}
// Use the type of the argument to figure out how to parse it, scanf() style.
// This is legit.
switch e := v[i].(type) {
case *string:
*e = p
case *int:
// "numbers", with conversion errors ignored because of some misbehaving configuration files.
*e, _ = strconv.Atoi(p)
case *[]string:
// Comma-separated lists.
if p != "" {
*e = strings.Split(p, ",")
} else {
*e = []string{}
}
default:
// Someone goof'd when writing code using this function. Scream so they can hear us.
panic(fmt.Sprintf("parseLine only accepts {*string, *int, *[]string} as arguments! %#v is not a pointer!", e))
}
}
}
func ParsePasswdFile(path string) ([]User, error) {
passwd, err := os.Open(path)
if err != nil {
return nil, err
}
defer passwd.Close()
return ParsePasswd(passwd)
}
func ParsePasswd(passwd io.Reader) ([]User, error) {
return ParsePasswdFilter(passwd, nil)
}
func ParsePasswdFileFilter(path string, filter func(User) bool) ([]User, error) {
passwd, err := os.Open(path)
if err != nil {
return nil, err
}
defer passwd.Close()
return ParsePasswdFilter(passwd, filter)
}
func ParsePasswdFilter(r io.Reader, filter func(User) bool) ([]User, error) {
if r == nil {
return nil, fmt.Errorf("nil source for passwd-formatted data")
}
var (
s = bufio.NewScanner(r)
out = []User{}
)
for s.Scan() {
if err := s.Err(); err != nil {
return nil, err
}
line := strings.TrimSpace(s.Text())
if line == "" {
continue
}
// see: man 5 passwd
// name:password:UID:GID:GECOS:directory:shell
// Name:Pass:Uid:Gid:Gecos:Home:Shell
// root:x:0:0:root:/root:/bin/bash
// adm:x:3:4:adm:/var/adm:/bin/false
p := User{}
parseLine(line, &p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell)
if filter == nil || filter(p) {
out = append(out, p)
}
}
return out, nil
}
func ParseGroupFile(path string) ([]Group, error) {
group, err := os.Open(path)
if err != nil {
return nil, err
}
defer group.Close()
return ParseGroup(group)
}
func ParseGroup(group io.Reader) ([]Group, error) {
return ParseGroupFilter(group, nil)
}
func ParseGroupFileFilter(path string, filter func(Group) bool) ([]Group, error) {
group, err := os.Open(path)
if err != nil {
return nil, err
}
defer group.Close()
return ParseGroupFilter(group, filter)
}
func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) {
if r == nil {
return nil, fmt.Errorf("nil source for group-formatted data")
}
var (
s = bufio.NewScanner(r)
out = []Group{}
)
for s.Scan() {
if err := s.Err(); err != nil {
return nil, err
}
text := s.Text()
if text == "" {
continue
}
// see: man 5 group
// group_name:password:GID:user_list
// Name:Pass:Gid:List
// root:x:0:root
// adm:x:4:root,adm,daemon
p := Group{}
parseLine(text, &p.Name, &p.Pass, &p.Gid, &p.List)
if filter == nil || filter(p) {
out = append(out, p)
}
}
return out, nil
}
type ExecUser struct {
Uid int
Gid int
Sgids []int
Home string
}
// GetExecUserPath is a wrapper for GetExecUser. It reads data from each of the
// given file paths and uses that data as the arguments to GetExecUser. If the
// files cannot be opened for any reason, the error is ignored and a nil
// io.Reader is passed instead.
func GetExecUserPath(userSpec string, defaults *ExecUser, passwdPath, groupPath string) (*ExecUser, error) {
var passwd, group io.Reader
if passwdFile, err := os.Open(passwdPath); err == nil {
passwd = passwdFile
defer passwdFile.Close()
}
if groupFile, err := os.Open(groupPath); err == nil {
group = groupFile
defer groupFile.Close()
}
return GetExecUser(userSpec, defaults, passwd, group)
}
// GetExecUser parses a user specification string (using the passwd and group
// readers as sources for /etc/passwd and /etc/group data, respectively). In
// the case of blank fields or missing data from the sources, the values in
// defaults is used.
//
// GetExecUser will return an error if a user or group literal could not be
// found in any entry in passwd and group respectively.
//
// Examples of valid user specifications are:
// * ""
// * "user"
// * "uid"
// * "user:group"
// * "uid:gid
// * "user:gid"
// * "uid:group"
//
// It should be noted that if you specify a numeric user or group id, they will
// not be evaluated as usernames (only the metadata will be filled). So attempting
// to parse a user with user.Name = "1337" will produce the user with a UID of
// 1337.
func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) (*ExecUser, error) {
if defaults == nil {
defaults = new(ExecUser)
}
// Copy over defaults.
user := &ExecUser{
Uid: defaults.Uid,
Gid: defaults.Gid,
Sgids: defaults.Sgids,
Home: defaults.Home,
}
// Sgids slice *cannot* be nil.
if user.Sgids == nil {
user.Sgids = []int{}
}
// Allow for userArg to have either "user" syntax, or optionally "user:group" syntax
var userArg, groupArg string
parseLine(userSpec, &userArg, &groupArg)
// Convert userArg and groupArg to be numeric, so we don't have to execute
// Atoi *twice* for each iteration over lines.
uidArg, uidErr := strconv.Atoi(userArg)
gidArg, gidErr := strconv.Atoi(groupArg)
// Find the matching user.
users, err := ParsePasswdFilter(passwd, func(u User) bool {
if userArg == "" {
// Default to current state of the user.
return u.Uid == user.Uid
}
if uidErr == nil {
// If the userArg is numeric, always treat it as a UID.
return uidArg == u.Uid
}
return u.Name == userArg
})
// If we can't find the user, we have to bail.
if err != nil && passwd != nil {
if userArg == "" {
userArg = strconv.Itoa(user.Uid)
}
return nil, fmt.Errorf("unable to find user %s: %v", userArg, err)
}
var matchedUserName string
if len(users) > 0 {
// First match wins, even if there's more than one matching entry.
matchedUserName = users[0].Name
user.Uid = users[0].Uid
user.Gid = users[0].Gid
user.Home = users[0].Home
} else if userArg != "" {
// If we can't find a user with the given username, the only other valid
// option is if it's a numeric username with no associated entry in passwd.
if uidErr != nil {
// Not numeric.
return nil, fmt.Errorf("unable to find user %s: %v", userArg, ErrNoPasswdEntries)
}
user.Uid = uidArg
// Must be inside valid uid range.
if user.Uid < minId || user.Uid > maxId {
return nil, ErrRange
}
// Okay, so it's numeric. We can just roll with this.
}
// On to the groups. If we matched a username, we need to do this because of
// the supplementary group IDs.
if groupArg != "" || matchedUserName != "" {
groups, err := ParseGroupFilter(group, func(g Group) bool {
// If the group argument isn't explicit, we'll just search for it.
if groupArg == "" {
// Check if user is a member of this group.
for _, u := range g.List {
if u == matchedUserName {
return true
}
}
return false
}
if gidErr == nil {
// If the groupArg is numeric, always treat it as a GID.
return gidArg == g.Gid
}
return g.Name == groupArg
})
if err != nil && group != nil {
return nil, fmt.Errorf("unable to find groups for spec %v: %v", matchedUserName, err)
}
// Only start modifying user.Gid if it is in explicit form.
if groupArg != "" {
if len(groups) > 0 {
// First match wins, even if there's more than one matching entry.
user.Gid = groups[0].Gid
} else {
// If we can't find a group with the given name, the only other valid
// option is if it's a numeric group name with no associated entry in group.
if gidErr != nil {
// Not numeric.
return nil, fmt.Errorf("unable to find group %s: %v", groupArg, ErrNoGroupEntries)
}
user.Gid = gidArg
// Must be inside valid gid range.
if user.Gid < minId || user.Gid > maxId {
return nil, ErrRange
}
// Okay, so it's numeric. We can just roll with this.
}
} else if len(groups) > 0 && uidErr != nil {
// Supplementary group ids only make sense if in the implicit form for non-numeric users.
user.Sgids = make([]int, len(groups))
for i, group := range groups {
user.Sgids[i] = group.Gid
}
}
}
return user, nil
}
// GetAdditionalGroups looks up a list of groups by name or group id
// against the given /etc/group formatted data. If a group name cannot
// be found, an error will be returned. If a group id cannot be found,
// or the given group data is nil, the id will be returned as-is
// provided it is in the legal range.
func GetAdditionalGroups(additionalGroups []string, group io.Reader) ([]int, error) {
var groups = []Group{}
if group != nil {
var err error
groups, err = ParseGroupFilter(group, func(g Group) bool {
for _, ag := range additionalGroups {
if g.Name == ag || strconv.Itoa(g.Gid) == ag {
return true
}
}
return false
})
if err != nil {
return nil, fmt.Errorf("Unable to find additional groups %v: %v", additionalGroups, err)
}
}
gidMap := make(map[int]struct{})
for _, ag := range additionalGroups {
var found bool
for _, g := range groups {
// if we found a matched group either by name or gid, take the
// first matched as correct
if g.Name == ag || strconv.Itoa(g.Gid) == ag {
if _, ok := gidMap[g.Gid]; !ok {
gidMap[g.Gid] = struct{}{}
found = true
break
}
}
}
// we asked for a group but didn't find it. let's check to see
// if we wanted a numeric group
if !found {
gid, err := strconv.Atoi(ag)
if err != nil {
return nil, fmt.Errorf("Unable to find group %s", ag)
}
// Ensure gid is inside gid range.
if gid < minId || gid > maxId {
return nil, ErrRange
}
gidMap[gid] = struct{}{}
}
}
gids := []int{}
for gid := range gidMap {
gids = append(gids, gid)
}
return gids, nil
}
// GetAdditionalGroupsPath is a wrapper around GetAdditionalGroups
// that opens the groupPath given and gives it as an argument to
// GetAdditionalGroups.
func GetAdditionalGroupsPath(additionalGroups []string, groupPath string) ([]int, error) {
var group io.Reader
if groupFile, err := os.Open(groupPath); err == nil {
group = groupFile
defer groupFile.Close()
}
return GetAdditionalGroups(additionalGroups, group)
}