From ba69f5d4884e0a076ce682e7b6ef6de30c5df065 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Tue, 22 Aug 2017 11:41:17 -0400 Subject: [PATCH 1/7] Add WithUserIDs SpecOpt Signed-off-by: Michael Crosby --- spec_opts_unix.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec_opts_unix.go b/spec_opts_unix.go index 3e35c18c8..a2a040a0e 100644 --- a/spec_opts_unix.go +++ b/spec_opts_unix.go @@ -289,3 +289,12 @@ func WithNamespacedCgroup(ctx context.Context, id string) SpecOpts { return nil } } + +// WithUserIDs allows the UID and GID for the Process to be set +func WithUserIDs(uid, gid uint32) SpecOpts { + return func(s *specs.Spec) error { + s.Process.User.UID = uid + s.Process.User.GID = gid + return nil + } +} From fa14f2ef3ac53035b57661e332b690b1ef5f1fac Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Tue, 22 Aug 2017 12:03:21 -0400 Subject: [PATCH 2/7] Add context and client to SpecOpts In order to do more advanced spec generation with images, snapshots, etc, we need to inject the context and client into the spec generation code. Signed-off-by: Michael Crosby --- apparmor.go | 8 ++++-- benchmark_test.go | 4 +-- cmd/containerd-stress/main.go | 2 +- cmd/ctr/run.go | 4 +-- cmd/ctr/run_unix.go | 4 +-- container_checkpoint_test.go | 6 ++--- container_linux_test.go | 6 ++--- container_test.go | 46 +++++++++++++++++------------------ helpers_unix_test.go | 23 +++++++----------- helpers_windows_test.go | 20 +++++++-------- spec.go | 10 +++++--- spec_opts.go | 12 ++++++--- spec_opts_unix.go | 32 ++++++++++++------------ spec_opts_windows.go | 8 +++--- spec_unix_test.go | 7 +++--- 15 files changed, 100 insertions(+), 92 deletions(-) diff --git a/apparmor.go b/apparmor.go index 117b52bb1..49f21484d 100644 --- a/apparmor.go +++ b/apparmor.go @@ -2,11 +2,15 @@ package containerd -import specs "github.com/opencontainers/runtime-spec/specs-go" +import ( + "context" + + 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, s *specs.Spec) error { s.Process.ApparmorProfile = profile return nil } diff --git a/benchmark_test.go b/benchmark_test.go index 1b154de87..4d98c6088 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -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, 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, WithImageConfig(image), withTrue()) if err != nil { b.Error(err) return diff --git a/cmd/containerd-stress/main.go b/cmd/containerd-stress/main.go index fa2c167e4..7a9ff18f0 100644 --- a/cmd/containerd-stress/main.go +++ b/cmd/containerd-stress/main.go @@ -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, containerd.WithImageConfig(image), containerd.WithProcessArgs("true")) if err != nil { return err } diff --git a/cmd/ctr/run.go b/cmd/ctr/run.go index da7a4412c..8a9f91c63 100644 --- a/cmd/ctr/run.go +++ b/cmd/ctr/run.go @@ -24,7 +24,7 @@ type killer interface { } func withEnv(context *cli.Context) containerd.SpecOpts { - return func(s *specs.Spec) error { + return func(_ gocontext.Context, _ *containerd.Client, s *specs.Spec) error { env := context.StringSlice("env") if len(env) > 0 { s.Process.Env = replaceOrAppendEnvValues(s.Process.Env, env) @@ -34,7 +34,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, s *specs.Spec) error { for _, mount := range context.StringSlice("mount") { m, err := parseMountFlag(mount) if err != nil { diff --git a/cmd/ctr/run_unix.go b/cmd/ctr/run_unix.go index 18680bfd8..46a3e8aa0 100644 --- a/cmd/ctr/run_unix.go +++ b/cmd/ctr/run_unix.go @@ -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,7 +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...) + spec, err := containerd.GenerateSpec(ctx, client, opts...) if err != nil { return nil, err } diff --git a/container_checkpoint_test.go b/container_checkpoint_test.go index ebd762370..55d9c672f 100644 --- a/container_checkpoint_test.go +++ b/container_checkpoint_test.go @@ -28,7 +28,7 @@ func TestCheckpointRestore(t *testing.T) { t.Error(err) return } - spec, err := GenerateSpec(WithImageConfig(ctx, image), WithProcessArgs("sleep", "100")) + spec, err := GenerateSpec(ctx, client, WithImageConfig(image), WithProcessArgs("sleep", "100")) if err != nil { t.Error(err) return @@ -113,7 +113,7 @@ func TestCheckpointRestoreNewContainer(t *testing.T) { t.Error(err) return } - spec, err := GenerateSpec(WithImageConfig(ctx, image), WithProcessArgs("sleep", "100")) + spec, err := GenerateSpec(ctx, client, WithImageConfig(image), WithProcessArgs("sleep", "100")) if err != nil { t.Error(err) return @@ -211,7 +211,7 @@ func TestCheckpointLeaveRunning(t *testing.T) { t.Error(err) return } - spec, err := GenerateSpec(WithImageConfig(ctx, image), WithProcessArgs("sleep", "100")) + spec, err := GenerateSpec(ctx, client, WithImageConfig(image), WithProcessArgs("sleep", "100")) if err != nil { t.Error(err) return diff --git a/container_linux_test.go b/container_linux_test.go index ab6ff9fa5..4dc7f0899 100644 --- a/container_linux_test.go +++ b/container_linux_test.go @@ -39,7 +39,7 @@ func TestContainerUpdate(t *testing.T) { t.Error(err) return } - spec, err := generateSpec(WithImageConfig(ctx, image), withProcessArgs("sleep", "30")) + spec, err := generateSpec(ctx, client, WithImageConfig(image), withProcessArgs("sleep", "30")) if err != nil { t.Error(err) return @@ -127,7 +127,7 @@ func TestShimInCgroup(t *testing.T) { t.Error(err) return } - spec, err := GenerateSpec(WithImageConfig(ctx, image), WithProcessArgs("sleep", "30")) + spec, err := GenerateSpec(ctx, client, WithImageConfig(image), WithProcessArgs("sleep", "30")) if err != nil { t.Error(err) return @@ -202,7 +202,7 @@ func TestDaemonRestart(t *testing.T) { return } - spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "30")) + spec, err := generateSpec(ctx, client, withImageConfig(image), withProcessArgs("sleep", "30")) if err != nil { t.Error(err) return diff --git a/container_test.go b/container_test.go index ffd7583ae..3f589af4d 100644 --- a/container_test.go +++ b/container_test.go @@ -55,15 +55,15 @@ func TestNewContainer(t *testing.T) { } defer client.Close() - spec, err := generateSpec() + ctx, cancel := testContext() + defer cancel() + + spec, err := generateSpec(ctx, client) if err != nil { t.Error(err) return } - ctx, cancel := testContext() - defer cancel() - container, err := client.NewContainer(ctx, id, WithSpec(spec)) if err != nil { t.Error(err) @@ -107,7 +107,7 @@ func TestContainerStart(t *testing.T) { } } - spec, err := generateSpec(withImageConfig(ctx, image), withExitStatus(7)) + spec, err := generateSpec(ctx, client, withImageConfig(image), withExitStatus(7)) if err != nil { t.Error(err) return @@ -185,7 +185,7 @@ func TestContainerOutput(t *testing.T) { } } - spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("echo", expected)) + spec, err := generateSpec(ctx, client, withImageConfig(image), withProcessArgs("echo", expected)) if err != nil { t.Error(err) return @@ -258,7 +258,7 @@ func TestContainerExec(t *testing.T) { } } - spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "100")) + spec, err := generateSpec(ctx, client, withImageConfig(image), withProcessArgs("sleep", "100")) if err != nil { t.Error(err) return @@ -357,7 +357,7 @@ func TestContainerPids(t *testing.T) { } } - spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "100")) + spec, err := generateSpec(ctx, client, withImageConfig(image), withProcessArgs("sleep", "100")) if err != nil { t.Error(err) return @@ -435,7 +435,7 @@ func TestContainerCloseIO(t *testing.T) { } } - spec, err := generateSpec(withImageConfig(ctx, image), withCat()) + spec, err := generateSpec(ctx, client, withImageConfig(image), withCat()) if err != nil { t.Error(err) return @@ -505,7 +505,7 @@ func TestDeleteRunningContainer(t *testing.T) { } } - spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "100")) + spec, err := generateSpec(ctx, client, withImageConfig(image), withProcessArgs("sleep", "100")) if err != nil { t.Error(err) return @@ -573,7 +573,7 @@ func TestContainerKill(t *testing.T) { } } - spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "10")) + spec, err := generateSpec(ctx, client, withImageConfig(image), withProcessArgs("sleep", "10")) if err != nil { t.Error(err) return @@ -642,7 +642,7 @@ func TestContainerNoBinaryExists(t *testing.T) { } } - spec, err := generateSpec(withImageConfig(ctx, image), WithProcessArgs("nothing")) + spec, err := generateSpec(ctx, client, withImageConfig(image), WithProcessArgs("nothing")) if err != nil { t.Error(err) return @@ -696,7 +696,7 @@ func TestContainerExecNoBinaryExists(t *testing.T) { } } - spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "100")) + spec, err := generateSpec(ctx, client, withImageConfig(image), withProcessArgs("sleep", "100")) if err != nil { t.Error(err) return @@ -769,8 +769,8 @@ func TestUserNamespaces(t *testing.T) { } } - spec, err := generateSpec( - withImageConfig(ctx, image), + spec, err := generateSpec(ctx, client, + withImageConfig(image), withExitStatus(7), withUserNamespace(0, 1000, 10000), ) @@ -852,7 +852,7 @@ func TestWaitStoppedTask(t *testing.T) { } } - spec, err := generateSpec(withImageConfig(ctx, image), withExitStatus(7)) + spec, err := generateSpec(ctx, client, withImageConfig(image), withExitStatus(7)) if err != nil { t.Error(err) return @@ -928,7 +928,7 @@ func TestWaitStoppedProcess(t *testing.T) { } } - spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "100")) + spec, err := generateSpec(ctx, client, withImageConfig(image), withProcessArgs("sleep", "100")) if err != nil { t.Error(err) return @@ -1027,7 +1027,7 @@ func TestTaskForceDelete(t *testing.T) { return } } - spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "30")) + spec, err := generateSpec(ctx, client, withImageConfig(image), withProcessArgs("sleep", "30")) if err != nil { t.Error(err) return @@ -1080,7 +1080,7 @@ func TestProcessForceDelete(t *testing.T) { return } } - spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "30")) + spec, err := generateSpec(ctx, client, withImageConfig(image), withProcessArgs("sleep", "30")) if err != nil { t.Error(err) return @@ -1160,8 +1160,8 @@ func TestContainerHostname(t *testing.T) { } } - spec, err := generateSpec( - withImageConfig(ctx, image), + spec, err := generateSpec(ctx, client, + withImageConfig(image), withProcessArgs("hostname"), WithHostname(expected), ) @@ -1243,7 +1243,7 @@ func TestContainerExitedAtSet(t *testing.T) { } } - spec, err := generateSpec(withImageConfig(ctx, image), withTrue()) + spec, err := generateSpec(ctx, client, withImageConfig(image), withTrue()) if err != nil { t.Error(err) return @@ -1315,7 +1315,7 @@ func TestDeleteContainerExecCreated(t *testing.T) { } } - spec, err := generateSpec(withImageConfig(ctx, image), withProcessArgs("sleep", "100")) + spec, err := generateSpec(ctx, client, withImageConfig(image), withProcessArgs("sleep", "100")) if err != nil { t.Error(err) return diff --git a/helpers_unix_test.go b/helpers_unix_test.go index ea93953ef..bb50ad4da 100644 --- a/helpers_unix_test.go +++ b/helpers_unix_test.go @@ -11,12 +11,12 @@ import ( const newLine = "\n" -func generateSpec(opts ...SpecOpts) (*specs.Spec, error) { - return GenerateSpec(opts...) +func generateSpec(ctx context.Context, client *Client, opts ...SpecOpts) (*specs.Spec, error) { + return GenerateSpec(ctx, client, opts...) } func withExitStatus(es int) SpecOpts { - return func(s *specs.Spec) error { + return func(_ context.Context, _ *Client, s *specs.Spec) error { s.Process.Args = []string{"sh", "-c", fmt.Sprintf("exit %d", es)} return nil } @@ -42,14 +42,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 +) diff --git a/helpers_windows_test.go b/helpers_windows_test.go index 7a148d90c..b93c06a16 100644 --- a/helpers_windows_test.go +++ b/helpers_windows_test.go @@ -12,8 +12,8 @@ import ( const newLine = "\r\n" -func generateSpec(opts ...SpecOpts) (*specs.Spec, error) { - spec, err := GenerateSpec(opts...) +func generateSpec(ctx context.Context, client *Client, opts ...SpecOpts) (*specs.Spec, error) { + spec, err := GenerateSpec(ctx, client, opts...) if err != nil { return nil, err } @@ -24,7 +24,7 @@ func generateSpec(opts ...SpecOpts) (*specs.Spec, error) { } func withExitStatus(es int) SpecOpts { - return func(s *specs.Spec) error { + return func(_ context.Context, _ *Client, s *specs.Spec) error { s.Process.Args = []string{"powershell", "-noprofile", "exit", strconv.Itoa(es)} return nil } @@ -50,11 +50,9 @@ func withExecArgs(s *specs.Process, args ...string) { s.Args = append([]string{"powershell", "-noprofile"}, args...) } -func withImageConfig(ctx context.Context, i Image) SpecOpts { +func withImageConfig(i Image) SpecOpts { // TODO: when windows has a snapshotter remove the withImageConfig helper - return func(s *specs.Spec) error { - return nil - } + return withNoop } func withNewSnapshot(id string, i Image) NewContainerOpts { @@ -65,9 +63,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 +71,7 @@ func withRemappedSnapshot(id string, i Image, u, g uint32) NewContainerOpts { return nil } } + +func withNoop(_ context.Context, _ *Client, _ *specs.Spec) error { + return nil +} diff --git a/spec.go b/spec.go index 8567ace67..d139069bc 100644 --- a/spec.go +++ b/spec.go @@ -1,16 +1,20 @@ package containerd -import specs "github.com/opencontainers/runtime-spec/specs-go" +import ( + "context" + + 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, 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, s); err != nil { return nil, err } } diff --git a/spec_opts.go b/spec_opts.go index 66cf5b3d7..a1abc1b3e 100644 --- a/spec_opts.go +++ b/spec_opts.go @@ -1,13 +1,17 @@ package containerd -import specs "github.com/opencontainers/runtime-spec/specs-go" +import ( + "context" + + 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, *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, s *specs.Spec) error { s.Process.Args = args return nil } @@ -15,7 +19,7 @@ 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, s *specs.Spec) error { s.Hostname = name return nil } diff --git a/spec_opts_unix.go b/spec_opts_unix.go index a2a040a0e..3c80736e9 100644 --- a/spec_opts_unix.go +++ b/spec_opts_unix.go @@ -22,7 +22,7 @@ import ( // 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, s *specs.Spec) error { s.Process.Terminal = true s.Process.Env = append(s.Process.Env, "TERM=xterm") return nil @@ -30,7 +30,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, 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 +44,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, s *specs.Spec) error { for i, n := range s.Linux.Namespaces { if n.Type == ns.Type { before := s.Linux.Namespaces[:i] @@ -60,11 +60,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, 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 { @@ -129,7 +129,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, s *specs.Spec) error { s.Root = &specs.Root{ Path: path, Readonly: readonly, @@ -160,13 +160,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, 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, s *specs.Spec) error { s.Mounts = append(s.Mounts, specs.Mount{ Destination: "/etc/hosts", Type: "bind", @@ -177,7 +177,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, s *specs.Spec) error { s.Mounts = append(s.Mounts, specs.Mount{ Destination: "/etc/resolv.conf", Type: "bind", @@ -188,7 +188,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, s *specs.Spec) error { s.Mounts = append(s.Mounts, specs.Mount{ Destination: "/etc/localtime", Type: "bind", @@ -201,7 +201,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, s *specs.Spec) error { var hasUserns bool for _, ns := range s.Linux.Namespaces { if ns.Type == specs.UserNamespace { @@ -271,7 +271,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, s *specs.Spec) error { s.Linux.CgroupsPath = path return nil } @@ -279,8 +279,8 @@ 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(id string) SpecOpts { + return func(ctx context.Context, _ *Client, s *specs.Spec) error { namespace, err := namespaces.NamespaceRequired(ctx) if err != nil { return err @@ -292,7 +292,7 @@ func WithNamespacedCgroup(ctx context.Context, id string) SpecOpts { // WithUserIDs allows the UID and GID for the Process to be set func WithUserIDs(uid, gid uint32) SpecOpts { - return func(s *specs.Spec) error { + return func(_ context.Context, _ *Client, s *specs.Spec) error { s.Process.User.UID = uid s.Process.User.GID = gid return nil diff --git a/spec_opts_windows.go b/spec_opts_windows.go index f4dc8b25b..498607766 100644 --- a/spec_opts_windows.go +++ b/spec_opts_windows.go @@ -15,11 +15,11 @@ import ( 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, 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 +52,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, s *specs.Spec) error { s.Process.Terminal = true s.Process.ConsoleSize.Width = uint(width) s.Process.ConsoleSize.Height = uint(height) diff --git a/spec_unix_test.go b/spec_unix_test.go index e7451fe22..fa3e5685d 100644 --- a/spec_unix_test.go +++ b/spec_unix_test.go @@ -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) 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, 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, WithLinuxNamespace(replacedNS)) if err != nil { t.Fatal(err) } From c601606f8432c4b9fa44e25bf91ebc26485e8b47 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Tue, 22 Aug 2017 16:56:25 -0400 Subject: [PATCH 3/7] Move spec generation to Container Create Signed-off-by: Michael Crosby --- README.md | 6 +- apparmor.go | 3 +- benchmark_test.go | 105 --------------------- cmd/containerd-stress/main.go | 2 +- cmd/ctr/run.go | 5 +- cmd/ctr/run_unix.go | 6 +- cmd/ctr/run_windows.go | 2 +- container_checkpoint_test.go | 21 +---- container_linux_test.go | 36 ++------ container_test.go | 166 ++++++++++------------------------ docs/client-opts.md | 2 +- docs/getting-started.md | 4 +- helpers_unix_test.go | 7 +- spec.go | 5 +- spec_opts.go | 7 +- spec_opts_unix.go | 57 ++++++++---- spec_opts_windows.go | 2 +- spec_unix_test.go | 6 +- 18 files changed, 128 insertions(+), 314 deletions(-) delete mode 100644 benchmark_test.go diff --git a/README.md b/README.md index 6b9692dce..145e2e5dd 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ In containerd, a container is a metadata object. Resources such as an OCI runti ```go redis, err := client.NewContainer(context, "redis-master", - containerd.WithSpec(spec), + containerd.WithNewSpec(spec), ) defer redis.Delete(context) ``` @@ -89,7 +89,7 @@ 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.WithNewSpec(spec), containerd.WithNewSnapshot("redis-rootfs", image), ) @@ -97,7 +97,7 @@ redis, err := client.NewContainer(context, "redis-master", for i := 0; i < 10; i++ { id := fmt.Sprintf("id-%s", i) container, err := client.NewContainer(ctx, id, - containerd.WithSpec(spec), + containerd.WithNewSpec(spec), containerd.WithNewSnapshotView(id, image), ) } diff --git a/apparmor.go b/apparmor.go index 49f21484d..e8485c3f5 100644 --- a/apparmor.go +++ b/apparmor.go @@ -5,12 +5,13 @@ package containerd 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(_ context.Context, _ *Client, s *specs.Spec) error { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Process.ApparmorProfile = profile return nil } diff --git a/benchmark_test.go b/benchmark_test.go deleted file mode 100644 index 4d98c6088..000000000 --- a/benchmark_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package containerd - -import ( - "fmt" - "testing" -) - -func BenchmarkContainerCreate(b *testing.B) { - client, err := newClient(b, address) - if err != nil { - b.Fatal(err) - } - defer client.Close() - - ctx, cancel := testContext() - defer cancel() - - image, err := client.GetImage(ctx, testImage) - if err != nil { - b.Error(err) - return - } - spec, err := GenerateSpec(ctx, client, WithImageConfig(image), withTrue()) - if err != nil { - b.Error(err) - return - } - var containers []Container - defer func() { - for _, c := range containers { - if err := c.Delete(ctx, WithSnapshotCleanup); err != nil { - b.Error(err) - } - } - }() - - // reset the timer before creating containers - b.ResetTimer() - for i := 0; i < b.N; i++ { - id := fmt.Sprintf("%s-%d", b.Name(), i) - container, err := client.NewContainer(ctx, id, WithSpec(spec), WithNewSnapshot(id, image)) - if err != nil { - b.Error(err) - return - } - containers = append(containers, container) - } - b.StopTimer() -} - -func BenchmarkContainerStart(b *testing.B) { - client, err := newClient(b, address) - if err != nil { - b.Fatal(err) - } - defer client.Close() - - ctx, cancel := testContext() - defer cancel() - - image, err := client.GetImage(ctx, testImage) - if err != nil { - b.Error(err) - return - } - spec, err := GenerateSpec(ctx, client, WithImageConfig(image), withTrue()) - if err != nil { - b.Error(err) - return - } - var containers []Container - defer func() { - for _, c := range containers { - if err := c.Delete(ctx, WithSnapshotCleanup); err != nil { - b.Error(err) - } - } - }() - - for i := 0; i < b.N; i++ { - id := fmt.Sprintf("%s-%d", b.Name(), i) - container, err := client.NewContainer(ctx, id, WithSpec(spec), WithNewSnapshot(id, image)) - if err != nil { - b.Error(err) - return - } - containers = append(containers, container) - - } - // reset the timer before starting tasks - b.ResetTimer() - for _, c := range containers { - task, err := c.NewTask(ctx, empty()) - if err != nil { - b.Error(err) - return - } - defer task.Delete(ctx) - if err := task.Start(ctx); err != nil { - b.Error(err) - return - } - } - b.StopTimer() -} diff --git a/cmd/containerd-stress/main.go b/cmd/containerd-stress/main.go index 7a9ff18f0..a62c03285 100644 --- a/cmd/containerd-stress/main.go +++ b/cmd/containerd-stress/main.go @@ -98,7 +98,7 @@ func test(c config) error { return err } logrus.Info("generating spec from image") - spec, err := containerd.GenerateSpec(ctx, client, containerd.WithImageConfig(image), containerd.WithProcessArgs("true")) + spec, err := containerd.GenerateSpec(ctx, client, nil, containerd.WithImageConfig(image), containerd.WithProcessArgs("true")) if err != nil { return err } diff --git a/cmd/ctr/run.go b/cmd/ctr/run.go index 8a9f91c63..880000876 100644 --- a/cmd/ctr/run.go +++ b/cmd/ctr/run.go @@ -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(_ gocontext.Context, _ *containerd.Client, 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(_ gocontext.Context, _ *containerd.Client, 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 { diff --git a/cmd/ctr/run_unix.go b/cmd/ctr/run_unix.go index 46a3e8aa0..659400ada 100644 --- a/cmd/ctr/run_unix.go +++ b/cmd/ctr/run_unix.go @@ -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(ctx, client, 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...) } diff --git a/cmd/ctr/run_windows.go b/cmd/ctr/run_windows.go index c679fe03b..f48b50542 100644 --- a/cmd/ctr/run_windows.go +++ b/cmd/ctr/run_windows.go @@ -115,7 +115,7 @@ func newContainer(ctx gocontext.Context, client *containerd.Client, context *cli } return client.NewContainer(ctx, id, - containerd.WithSpec(spec), + containerd.WithNewSpec(spec), containerd.WithContainerLabels(labels), containerd.WithRuntime(context.String("runtime")), // TODO(mlaventure): containerd.WithImage(image), diff --git a/container_checkpoint_test.go b/container_checkpoint_test.go index 55d9c672f..d7636b2fb 100644 --- a/container_checkpoint_test.go +++ b/container_checkpoint_test.go @@ -28,12 +28,7 @@ func TestCheckpointRestore(t *testing.T) { t.Error(err) return } - spec, err := GenerateSpec(ctx, client, WithImageConfig(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(ctx, client, WithImageConfig(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(ctx, client, WithImageConfig(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 diff --git a/container_linux_test.go b/container_linux_test.go index 4dc7f0899..6e53187ad 100644 --- a/container_linux_test.go +++ b/container_linux_test.go @@ -14,6 +14,7 @@ import ( "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 +40,14 @@ func TestContainerUpdate(t *testing.T) { t.Error(err) return } - spec, err := generateSpec(ctx, client, WithImageConfig(image), withProcessArgs("sleep", "30")) - if err != nil { - t.Error(err) - return - } limit := int64(32 * 1024 * 1024) - spec.Linux.Resources.Memory = &specs.LinuxMemory{ - Limit: &limit, + memory := func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { + s.Linux.Resources.Memory = &specs.LinuxMemory{ + Limit: &limit, + } + return nil } - container, err := client.NewContainer(ctx, id, WithSpec(spec), WithNewSnapshot(id, image)) + 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 +126,7 @@ func TestShimInCgroup(t *testing.T) { t.Error(err) return } - spec, err := GenerateSpec(ctx, client, WithImageConfig(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 +196,7 @@ func TestDaemonRestart(t *testing.T) { return } - spec, err := generateSpec(ctx, client, withImageConfig(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 +282,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 diff --git a/container_test.go b/container_test.go index 3f589af4d..3bd901956 100644 --- a/container_test.go +++ b/container_test.go @@ -58,13 +58,7 @@ func TestNewContainer(t *testing.T) { ctx, cancel := testContext() defer cancel() - spec, err := generateSpec(ctx, client) - if err != nil { - t.Error(err) - return - } - - 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(ctx, client, withImageConfig(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(ctx, client, withImageConfig(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(ctx, client, withImageConfig(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(ctx, client, withImageConfig(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(ctx, client, withImageConfig(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(ctx, client, withImageConfig(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(ctx, client, withImageConfig(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(ctx, client, withImageConfig(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(ctx, client, withImageConfig(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(ctx, client, - withImageConfig(image), - withExitStatus(7), - withUserNamespace(0, 1000, 10000), - ) - if err != nil { - t.Error(err) - return - } container, err := client.NewContainer(ctx, id, - WithSpec(spec), + WithNewSpec(withImageConfig(image), + withExitStatus(7), + withUserNamespace(0, 1000, 10000), + ), withRemappedSnapshot(id, image, 1000, 1000), ) if err != nil { @@ -852,12 +803,7 @@ func TestWaitStoppedTask(t *testing.T) { } } - spec, err := generateSpec(ctx, client, withImageConfig(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(ctx, client, withImageConfig(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(ctx, client, withImageConfig(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(ctx, client, withImageConfig(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(ctx, client, - withImageConfig(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(ctx, client, withImageConfig(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(ctx, client, withImageConfig(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 diff --git a/docs/client-opts.md b/docs/client-opts.md index cd6cff426..4810aa2b7 100644 --- a/docs/client-opts.md +++ b/docs/client-opts.md @@ -10,7 +10,7 @@ For many functions and methods within the client package you will generally see If we look at the `NewContainer` method on the client we can see that it has a required argument of `id` and then additional `NewContainerOpts`. -There are a few built in options that allow the container to be created with an existing spec, `WithSpec`, and snapshot opts for creating or using an existing snapshot. +There are a few built in options that allow the container to be created with an existing spec, `WithNewSpec`, and snapshot opts for creating or using an existing snapshot. ```go func (c *Client) NewContainer(ctx context.Context, id string, opts ...NewContainerOpts) (Container, error) { diff --git a/docs/getting-started.md b/docs/getting-started.md index ab96f76db..f50b13e2f 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -109,7 +109,7 @@ The container will be based off of the image, use the runtime information in the container, err := client.NewContainer( ctx, "redis-server", - containerd.WithSpec(spec), + containerd.WithNewSpec(spec), containerd.WithImage(image), containerd.WithNewSnapshot("redis-server-snapshot", image), ) @@ -254,7 +254,7 @@ func redisExample() error { container, err := client.NewContainer( ctx, "redis-server", - containerd.WithSpec(spec), + containerd.WithNewSpec(spec), containerd.WithImage(image), containerd.WithNewSnapshot("redis-server-snapshot", image), ) diff --git a/helpers_unix_test.go b/helpers_unix_test.go index bb50ad4da..7b2aa3385 100644 --- a/helpers_unix_test.go +++ b/helpers_unix_test.go @@ -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(ctx context.Context, client *Client, opts ...SpecOpts) (*specs.Spec, error) { - return GenerateSpec(ctx, client, opts...) -} - func withExitStatus(es int) SpecOpts { - return func(_ context.Context, _ *Client, 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 } diff --git a/spec.go b/spec.go index d139069bc..fc1201467 100644 --- a/spec.go +++ b/spec.go @@ -3,18 +3,19 @@ package containerd 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(ctx context.Context, client *Client, 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(ctx, client, s); err != nil { + if err := o(ctx, client, c, s); err != nil { return nil, err } } diff --git a/spec_opts.go b/spec_opts.go index a1abc1b3e..608d122ae 100644 --- a/spec_opts.go +++ b/spec_opts.go @@ -3,15 +3,16 @@ package containerd import ( "context" + "github.com/containerd/containerd/containers" specs "github.com/opencontainers/runtime-spec/specs-go" ) // SpecOpts sets spec specific information to a newly generated OCI spec -type SpecOpts func(context.Context, *Client, *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(_ context.Context, _ *Client, s *specs.Spec) error { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Process.Args = args return nil } @@ -19,7 +20,7 @@ func WithProcessArgs(args ...string) SpecOpts { // WithHostname sets the container's hostname func WithHostname(name string) SpecOpts { - return func(_ context.Context, _ *Client, s *specs.Spec) error { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Hostname = name return nil } diff --git a/spec_opts_unix.go b/spec_opts_unix.go index 3c80736e9..86f3420c8 100644 --- a/spec_opts_unix.go +++ b/spec_opts_unix.go @@ -22,7 +22,7 @@ import ( // WithTTY sets the information on the spec as well as the environment variables for // using a TTY -func WithTTY(_ context.Context, _ *Client, 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 +30,7 @@ func WithTTY(_ context.Context, _ *Client, s *specs.Spec) error { // WithHostNamespace allows a task to run inside the host's linux namespace func WithHostNamespace(ns specs.LinuxNamespaceType) SpecOpts { - return func(_ context.Context, _ *Client, 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 +44,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(_ context.Context, _ *Client, 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] @@ -61,7 +61,7 @@ func WithLinuxNamespace(ns specs.LinuxNamespace) SpecOpts { // WithImageConfig configures the spec to from the configuration of an Image func WithImageConfig(i Image) SpecOpts { - return func(ctx context.Context, client *Client, s *specs.Spec) error { + return func(ctx context.Context, client *Client, c *containers.Container, s *specs.Spec) error { var ( image = i.(*image) store = client.ContentStore() @@ -129,7 +129,7 @@ func WithImageConfig(i Image) SpecOpts { // WithRootFSPath specifies unmanaged rootfs path. func WithRootFSPath(path string, readonly bool) SpecOpts { - return func(_ context.Context, _ *Client, s *specs.Spec) error { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Root = &specs.Root{ Path: path, Readonly: readonly, @@ -139,10 +139,31 @@ func WithRootFSPath(path string, readonly bool) SpecOpts { } } -// WithSpec sets the provided spec for a new container -func WithSpec(spec *specs.Spec) NewContainerOpts { +// 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 { - any, err := typeurl.MarshalAny(spec) + 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) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + any, err := typeurl.MarshalAny(s) if err != nil { return err } @@ -160,13 +181,13 @@ func WithResources(resources *specs.LinuxResources) UpdateTaskOpts { } // WithNoNewPrivileges sets no_new_privileges on the process for the container -func WithNoNewPrivileges(_ context.Context, _ *Client, 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(_ context.Context, _ *Client, 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 +198,7 @@ func WithHostHostsFile(_ context.Context, _ *Client, s *specs.Spec) error { } // WithHostResolvconf bind-mounts the host's /etc/resolv.conf into the container as readonly -func WithHostResolvconf(_ context.Context, _ *Client, 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 +209,7 @@ func WithHostResolvconf(_ context.Context, _ *Client, s *specs.Spec) error { } // WithHostLocaltime bind-mounts the host's /etc/localtime into the container as readonly -func WithHostLocaltime(_ context.Context, _ *Client, 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 +222,7 @@ func WithHostLocaltime(_ context.Context, _ *Client, 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(_ context.Context, _ *Client, 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 +292,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(_ context.Context, _ *Client, s *specs.Spec) error { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Linux.CgroupsPath = path return nil } @@ -279,20 +300,20 @@ 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(id string) SpecOpts { - return func(ctx context.Context, _ *Client, 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, s *specs.Spec) error { + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { s.Process.User.UID = uid s.Process.User.GID = gid return nil diff --git a/spec_opts_windows.go b/spec_opts_windows.go index 498607766..8b7eff8e9 100644 --- a/spec_opts_windows.go +++ b/spec_opts_windows.go @@ -60,7 +60,7 @@ func WithTTY(width, height int) SpecOpts { } } -func WithSpec(spec *specs.Spec) NewContainerOpts { +func WithNewSpec(spec *specs.Spec) NewContainerOpts { return func(ctx context.Context, client *Client, c *containers.Container) error { any, err := typeurl.MarshalAny(spec) if err != nil { diff --git a/spec_unix_test.go b/spec_unix_test.go index fa3e5685d..41a6471f5 100644 --- a/spec_unix_test.go +++ b/spec_unix_test.go @@ -12,7 +12,7 @@ import ( func TestGenerateSpec(t *testing.T) { t.Parallel() - s, err := GenerateSpec(context.Background(), nil) + s, err := GenerateSpec(context.Background(), nil, nil) if err != nil { t.Fatal(err) } @@ -52,7 +52,7 @@ func TestGenerateSpec(t *testing.T) { func TestSpecWithTTY(t *testing.T) { t.Parallel() - s, err := GenerateSpec(context.Background(), nil, WithTTY) + s, err := GenerateSpec(context.Background(), nil, nil, WithTTY) if err != nil { t.Fatal(err) } @@ -69,7 +69,7 @@ func TestWithLinuxNamespace(t *testing.T) { t.Parallel() replacedNS := specs.LinuxNamespace{Type: specs.NetworkNamespace, Path: "/var/run/netns/test"} - s, err := GenerateSpec(context.Background(), nil, WithLinuxNamespace(replacedNS)) + s, err := GenerateSpec(context.Background(), nil, nil, WithLinuxNamespace(replacedNS)) if err != nil { t.Fatal(err) } From f436f4c828f61f9b10ae1f5e7c47e8d4c1af7b2e Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Wed, 23 Aug 2017 14:01:58 -0400 Subject: [PATCH 4/7] Add WithUsername spec opt This option will mount and inspect the /etc/passwd file of an image to get the uid/gid of a user. Signed-off-by: Michael Crosby --- container_linux_test.go | 78 +++++++++++++++++++++++++++++++++++++++++ spec_opts_unix.go | 56 +++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/container_linux_test.go b/container_linux_test.go index 6e53187ad..10e79f04e 100644 --- a/container_linux_test.go +++ b/container_linux_test.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "runtime" + "strings" "sync" "syscall" "testing" @@ -364,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) + } +} diff --git a/spec_opts_unix.go b/spec_opts_unix.go index 86f3420c8..7e0cef518 100644 --- a/spec_opts_unix.go +++ b/spec_opts_unix.go @@ -6,10 +6,14 @@ 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" @@ -17,6 +21,7 @@ import ( "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" ) @@ -100,6 +105,10 @@ func WithImageConfig(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) @@ -319,3 +328,50 @@ func WithUserIDs(uid, gid uint32) SpecOpts { 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 fmt.Errorf("no snapshotter set for container") + } + if c.RootFS == "" { + return fmt.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 fmt.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 + } +} From f66f0fb7a0671f0ad9d18cecac3d04ad38b05841 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Wed, 23 Aug 2017 14:11:41 -0400 Subject: [PATCH 5/7] Update windows SpecOpts in tests Signed-off-by: Michael Crosby --- cmd/ctr/run_windows.go | 14 ++++---------- helpers_windows_test.go | 21 ++++++--------------- spec_opts_windows.go | 26 +++++++++++++++++++++++--- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/cmd/ctr/run_windows.go b/cmd/ctr/run_windows.go index f48b50542..2bfda8f8a 100644 --- a/cmd/ctr/run_windows.go +++ b/cmd/ctr/run_windows.go @@ -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.WithNewSpec(spec), + containerd.WithNewSpec(opts...), containerd.WithContainerLabels(labels), containerd.WithRuntime(context.String("runtime")), // TODO(mlaventure): containerd.WithImage(image), diff --git a/helpers_windows_test.go b/helpers_windows_test.go index b93c06a16..e5f1ee4e7 100644 --- a/helpers_windows_test.go +++ b/helpers_windows_test.go @@ -12,19 +12,8 @@ import ( const newLine = "\r\n" -func generateSpec(ctx context.Context, client *Client, opts ...SpecOpts) (*specs.Spec, error) { - spec, err := GenerateSpec(ctx, client, opts...) - if err != nil { - return nil, err - } - - spec.Windows.LayerFolders = dockerLayerFolders - - return spec, nil -} - func withExitStatus(es int) SpecOpts { - return func(_ context.Context, _ *Client, 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 } @@ -51,8 +40,10 @@ func withExecArgs(s *specs.Process, args ...string) { } func withImageConfig(i Image) SpecOpts { - // TODO: when windows has a snapshotter remove the withImageConfig helper - return withNoop + return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { + s.Windows.LayerFolders = dockerLayerFolders + return nil + } } func withNewSnapshot(id string, i Image) NewContainerOpts { @@ -72,6 +63,6 @@ func withRemappedSnapshot(id string, i Image, u, g uint32) NewContainerOpts { } } -func withNoop(_ context.Context, _ *Client, _ *specs.Spec) error { +func withNoop(_ context.Context, _ *Client, _ *containers.Container, _ *specs.Spec) error { return nil } diff --git a/spec_opts_windows.go b/spec_opts_windows.go index 8b7eff8e9..7881ad28c 100644 --- a/spec_opts_windows.go +++ b/spec_opts_windows.go @@ -16,7 +16,7 @@ import ( ) func WithImageConfig(i Image) SpecOpts { - return func(ctx context.Context, client *Client, s *specs.Spec) error { + return func(ctx context.Context, client *Client, _ *containers.Container, s *specs.Spec) error { var ( image = i.(*image) store = client.ContentStore() @@ -52,7 +52,7 @@ func WithImageConfig(i Image) SpecOpts { } func WithTTY(width, height int) SpecOpts { - return func(_ context.Context, _ *Client, 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,7 +60,27 @@ func WithTTY(width, height int) SpecOpts { } } -func WithNewSpec(spec *specs.Spec) NewContainerOpts { +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 + } +} + +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 { From 5c7f67186a7460a89e20e7b3998259499cb2b610 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Wed, 23 Aug 2017 14:20:09 -0400 Subject: [PATCH 6/7] Add libcontainer/user package for passwd parsing Signed-off-by: Michael Crosby --- .../runc/libcontainer/user/lookup.go | 111 +++++ .../runc/libcontainer/user/lookup_unix.go | 30 ++ .../libcontainer/user/lookup_unsupported.go | 21 + .../runc/libcontainer/user/user.go | 441 ++++++++++++++++++ 4 files changed, 603 insertions(+) create mode 100644 vendor/github.com/opencontainers/runc/libcontainer/user/lookup.go create mode 100644 vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unix.go create mode 100644 vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unsupported.go create mode 100644 vendor/github.com/opencontainers/runc/libcontainer/user/user.go diff --git a/vendor/github.com/opencontainers/runc/libcontainer/user/lookup.go b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup.go new file mode 100644 index 000000000..bf491c89c --- /dev/null +++ b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup.go @@ -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 + }) +} diff --git a/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unix.go b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unix.go new file mode 100644 index 000000000..758b734c2 --- /dev/null +++ b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unix.go @@ -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) +} diff --git a/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unsupported.go b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unsupported.go new file mode 100644 index 000000000..721794887 --- /dev/null +++ b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unsupported.go @@ -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 +} diff --git a/vendor/github.com/opencontainers/runc/libcontainer/user/user.go b/vendor/github.com/opencontainers/runc/libcontainer/user/user.go new file mode 100644 index 000000000..2471535a7 --- /dev/null +++ b/vendor/github.com/opencontainers/runc/libcontainer/user/user.go @@ -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) +} From 2052b76fa7199c1f9d7863713ae23d6df337457b Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Wed, 23 Aug 2017 15:05:42 -0400 Subject: [PATCH 7/7] Change WithSpec to take SpecOpts Signed-off-by: Michael Crosby --- README.md | 27 ++++++----- benchmark_test.go | 105 ++++++++++++++++++++++++++++++++++++++++ docs/client-opts.md | 7 ++- docs/getting-started.md | 23 ++------- spec_opts.go | 39 +++++++++++++++ spec_opts_unix.go | 41 ++-------------- spec_opts_windows.go | 32 ------------ 7 files changed, 170 insertions(+), 104 deletions(-) create mode 100644 benchmark_test.go diff --git a/README.md b/README.md index 145e2e5dd..08a68145d 100644 --- a/README.md +++ b/README.md @@ -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.WithNewSpec(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.WithNewSpec(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.WithNewSpec(spec), containerd.WithNewSnapshotView(id, image), + containerd.WithNewSpec(containerd.WithImageConfig(image)), ) } ``` diff --git a/benchmark_test.go b/benchmark_test.go new file mode 100644 index 000000000..844bed2fc --- /dev/null +++ b/benchmark_test.go @@ -0,0 +1,105 @@ +package containerd + +import ( + "fmt" + "testing" +) + +func BenchmarkContainerCreate(b *testing.B) { + client, err := newClient(b, address) + if err != nil { + b.Fatal(err) + } + defer client.Close() + + ctx, cancel := testContext() + defer cancel() + + image, err := client.GetImage(ctx, testImage) + if err != nil { + b.Error(err) + return + } + spec, err := GenerateSpec(ctx, client, nil, WithImageConfig(image), withTrue()) + if err != nil { + b.Error(err) + return + } + var containers []Container + defer func() { + for _, c := range containers { + if err := c.Delete(ctx, WithSnapshotCleanup); err != nil { + b.Error(err) + } + } + }() + + // reset the timer before creating containers + b.ResetTimer() + for i := 0; i < b.N; i++ { + id := fmt.Sprintf("%s-%d", b.Name(), i) + container, err := client.NewContainer(ctx, id, WithSpec(spec), WithNewSnapshot(id, image)) + if err != nil { + b.Error(err) + return + } + containers = append(containers, container) + } + b.StopTimer() +} + +func BenchmarkContainerStart(b *testing.B) { + client, err := newClient(b, address) + if err != nil { + b.Fatal(err) + } + defer client.Close() + + ctx, cancel := testContext() + defer cancel() + + image, err := client.GetImage(ctx, testImage) + if err != nil { + b.Error(err) + return + } + spec, err := GenerateSpec(ctx, client, nil, WithImageConfig(image), withTrue()) + if err != nil { + b.Error(err) + return + } + var containers []Container + defer func() { + for _, c := range containers { + if err := c.Delete(ctx, WithSnapshotCleanup); err != nil { + b.Error(err) + } + } + }() + + for i := 0; i < b.N; i++ { + id := fmt.Sprintf("%s-%d", b.Name(), i) + container, err := client.NewContainer(ctx, id, WithSpec(spec), WithNewSnapshot(id, image)) + if err != nil { + b.Error(err) + return + } + containers = append(containers, container) + + } + // reset the timer before starting tasks + b.ResetTimer() + for _, c := range containers { + task, err := c.NewTask(ctx, empty()) + if err != nil { + b.Error(err) + return + } + defer task.Delete(ctx) + if err := task.Start(ctx); err != nil { + b.Error(err) + return + } + } + b.StopTimer() +} diff --git a/docs/client-opts.md b/docs/client-opts.md index 4810aa2b7..bd790e44b 100644 --- a/docs/client-opts.md +++ b/docs/client-opts.md @@ -10,7 +10,7 @@ For many functions and methods within the client package you will generally see If we look at the `NewContainer` method on the client we can see that it has a required argument of `id` and then additional `NewContainerOpts`. -There are a few built in options that allow the container to be created with an existing spec, `WithNewSpec`, and snapshot opts for creating or using an existing snapshot. +There are a few built in options that allow the container to be created with an existing spec, `WithSpec`, and snapshot opts for creating or using an existing snapshot. ```go func (c *Client) NewContainer(ctx context.Context, id string, opts ...NewContainerOpts) (Container, error) { @@ -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), ) ``` diff --git a/docs/getting-started.md b/docs/getting-started.md index f50b13e2f..0c0d7fb35 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -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.WithNewSpec(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.WithNewSpec(spec), containerd.WithImage(image), containerd.WithNewSnapshot("redis-server-snapshot", image), + containerd.WithNewSpec(containerd.WithImageConfig(image)), ) if err != nil { return err diff --git a/spec_opts.go b/spec_opts.go index 608d122ae..811e1eeba 100644 --- a/spec_opts.go +++ b/spec_opts.go @@ -4,6 +4,7 @@ import ( "context" "github.com/containerd/containerd/containers" + "github.com/containerd/containerd/typeurl" specs "github.com/opencontainers/runtime-spec/specs-go" ) @@ -25,3 +26,41 @@ func WithHostname(name string) SpecOpts { 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 + } +} diff --git a/spec_opts_unix.go b/spec_opts_unix.go index 7e0cef518..8aec1f22b 100644 --- a/spec_opts_unix.go +++ b/spec_opts_unix.go @@ -18,11 +18,11 @@ import ( "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 @@ -148,39 +148,6 @@ func WithRootFSPath(path string, readonly bool) SpecOpts { } } -// 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) NewContainerOpts { - return func(ctx context.Context, client *Client, c *containers.Container) error { - any, err := typeurl.MarshalAny(s) - 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 { @@ -335,10 +302,10 @@ func WithUserIDs(uid, gid uint32) SpecOpts { func WithUsername(username string) SpecOpts { return func(ctx context.Context, client *Client, c *containers.Container, s *specs.Spec) error { if c.Snapshotter == "" { - return fmt.Errorf("no snapshotter set for container") + return errors.Errorf("no snapshotter set for container") } if c.RootFS == "" { - return fmt.Errorf("rootfs not created for container") + return errors.Errorf("rootfs not created for container") } snapshotter := client.SnapshotService(c.Snapshotter) mounts, err := snapshotter.Mounts(ctx, c.RootFS) @@ -368,7 +335,7 @@ func WithUsername(username string) SpecOpts { return err } if len(users) == 0 { - return fmt.Errorf("no users found for %s", username) + 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) diff --git a/spec_opts_windows.go b/spec_opts_windows.go index 7881ad28c..8f0fa6139 100644 --- a/spec_opts_windows.go +++ b/spec_opts_windows.go @@ -10,7 +10,6 @@ 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" ) @@ -60,37 +59,6 @@ func WithTTY(width, height int) SpecOpts { } } -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 - } -} - -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