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