Skip to content

Add initial containerd client package#904

Merged
dmcgowan merged 14 commits intocontainerd:masterfrom
crosbymichael:client
May 25, 2017
Merged

Add initial containerd client package#904
dmcgowan merged 14 commits intocontainerd:masterfrom
crosbymichael:client

Conversation

@crosbymichael
Copy link
Copy Markdown
Member

@crosbymichael crosbymichael commented May 24, 2017

This adds the initial containerd client package that consumers will use when working with containerd. It also adds a start to the integration tests for the daemon based on the client.

You can see a full, working example of the client here:

package main

import (
	"context"
	"fmt"
	"log"
	"syscall"
	"time"

	"github.com/containerd/containerd"
)

const (
	address = "/run/containerd/containerd.sock"
	redis   = "docker.io/library/redis:alpine"
)

func main() {
	if err := runRedis(); err != nil {
		log.Fatal(err)
	}
}

func runRedis() error {
	ctx := context.Background()

        // WithNamespace is a placeholder while we get namespace support in the daemon
	client, err := containerd.New(address, containerd.WithNamespace("scripts"))
	if err != nil {
		return err
	}
	defer client.Close()

	log.Printf("pulling image %s...\n", redis)
	// make sure we unpack the image for use after the pull
	image, err := client.Pull(ctx, redis, containerd.WithPullUnpack)
	if err != nil {
		return err
	}
	log.Println("finished pulling image...")

	spec, err := containerd.GenerateSpec(containerd.WithImage(ctx, image))
	if err != nil {
		return err
	}
	log.Println("generated spec from image...")

	container, err := client.NewContainer(ctx, "redis-master", spec, containerd.WithNewRootFS("redis-master", image))
	if err != nil {
		return err
	}
	defer container.Delete(ctx)
	log.Println("created new container...")

	// lets get the container's stdio in our current process
	task, err := container.NewTask(ctx, containerd.Stdio)
	if err != nil {
		return err
	}
	defer task.Delete(ctx)
	fmt.Printf("container pid %d\n", task.Pid())

	// you can do network namespace setup here or whatever you want without hooks

	if err := task.Start(ctx); err != nil {
		return err
	}

	// let it run for 5 seconds and then kill it
	go func() {
		time.Sleep(5 * time.Second)
		log.Println("killing task...")
		if err := task.Kill(ctx, syscall.SIGTERM); err != nil {
			log.Println(err)
		}
	}()

	status, err := task.Wait(ctx)
	if err != nil {
		return err
	}
	fmt.Printf("task exited with status %d\n", status)

	// all done
	return nil
}

Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
client.go Outdated
conn *grpc.ClientConn

Runtime string
Namespace string
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider not having these exported. The With* pattern allows setting them and they should not be mutated by callers. Creating a new client to change these values seems reasonable.

@codecov-io
Copy link
Copy Markdown

codecov-io commented May 24, 2017

Codecov Report

Merging #904 into master will not change coverage.
The diff coverage is n/a.

Impacted file tree graph

@@           Coverage Diff           @@
##           master     #904   +/-   ##
=======================================
  Coverage   59.55%   59.55%           
=======================================
  Files           5        5           
  Lines         759      759           
=======================================
  Hits          452      452           
  Misses        199      199           
  Partials      108      108

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 26183b3...cebe099. Read the comment docs.

client.go Outdated
type NewContainerOpts func(ctx context.Context, client *Client, c *containers.Container) error

// WithContainerLables adds the provided labels to the container
func WithContainerLables(labels map[string]string) NewContainerOpts {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
type PullOpts func(*Client, *PullContext) error

type PullContext struct {
Resolver remotes.Resolver
Copy link
Copy Markdown
Member

@dmcgowan dmcgowan May 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing WithPullResolver

@crosbymichael
Copy link
Copy Markdown
Member Author

crosbymichael commented May 24, 2017

FYI: Not all of the functionality is implemented yet. This is opened to get early feedback on the overall design for the client and then to have it merged in where we all can work on it together.

I also have the integration tests turned off at the moment while we figure out how to seed the content and run the daemon inside travis. I'll try to get integration tests running on travis by the end of this week.

return nil, err
}
is := c.images()
if err := is.Put(ctx, name, desc); err != nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stevvooe Should we Put image metadata before or after pulling the image or doesn't matter? I'm a bit confused now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be before.

}
if err := images.Dispatch(ctx, images.Handlers(handlers...), desc); err != nil {
return nil, err
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add TODO to wait for image pulling?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, we will also be adding ways to hook into progress on the pull/push

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dispatch waits until all handlers are executed, so this effectively waits until the image is pulled.

Does CRI report status?

Copy link
Copy Markdown
Member

@Random-Liu Random-Liu May 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stevvooe Nope. We only log it in dockershim now.

If there are 2 concurrent Dispatch pulling the same resource (say pulling 2 images sharing layers), one will return error immediately. So we may still want to wait and ignore the error.

See #897.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, let me know what error is returned.

Copy link
Copy Markdown
Member

@Random-Liu Random-Liu May 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, we should probably have an inprogress error and then detect that in the handler.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM

return nil
}

type Unpacker interface {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only Unpacker is an interface?

It would help us write unit test if we could make all clients an interface. :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can always depend on a Client interface in your code. I think we talked about this some and @stevvooe may have some more context on this subject

Copy link
Copy Markdown
Member

@Random-Liu Random-Liu May 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@crosbymichael The problem is that, we expect Image and Container also to be an interface. It will be harder to wrap them all on our side, e.g. https://play.golang.org/p/JvlZhLONen will fail.

parts := strings.Split(config.User, ":")
switch len(parts) {
case 1:
v, err := strconv.ParseUint(parts[0], 0, 10)
Copy link
Copy Markdown
Member

@Random-Liu Random-Liu May 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we handle user name and group name? We don't have a rootfs before creating the container.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to add some helpers for this because its complex when you have to parse a username

client *Client

i images.Image
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be very helpful, if we could add some frequently used function here, GetManifest, GetConfig, GetSize etc.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, will be adding those soon.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No getters in Go. Manifest, Config, Size.

Although, we should be very careful what we consider size. This will be the packed size.

Copy link
Copy Markdown
Member

@Random-Liu Random-Liu May 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stevvooe Based on the discussion, would it help to introduce a Ready function? Or else Config or Manifest may return weird error during the image is being downloaded or unpacked.

/bin/
**/coverage.txt
**/coverage.out
containerd.test
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*.test?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This output from compiling a test executable. go test -c, I believe.

client_test.go Outdated

func TestNewClient(t *testing.T) {
if testing.Short() {
return
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

t.Skip?

)

const defaultAddress = "/run/containerd/containerd.sock"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does tester need to launch the daemon manually?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

@AkihiroSuda
Copy link
Copy Markdown
Member

I'd expect containerd.New to instantiate a daemon. How about containerd/client.New?

task.go Outdated
func (t *Task) Wait(ctx context.Context) (uint32, error) {
events, err := t.client.tasks().Events(ctx, &execution.EventsRequest{})
if err != nil {
return 255, err
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand why this is 255, but it still looks like a magic number. Can you put this in a constant so it has a name? Something like failureExitStatus?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm more for UnknownExitStatus, but yes, probably better to have it defined (and exported)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just put a comment:

// this number is magic
return 255, err

Copy link
Copy Markdown
Contributor

@mlaventure mlaventure left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the principles, but I had a few comments obviously 😉

)

func dialer(address string, timeout time.Duration) (net.Conn, error) {
return winio.DialPipe(bindAddress, &timeout)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/bindAddress/address/

)

func dialer(address string, timeout time.Duration) (net.Conn, error) {
address = strings.TrimPrefix(address, "unix://")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validate that the prefix is actually present?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why validate first? Just trim it. same reason you don't stat a file before opening, just open it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough


// Client is the client to interact with containerd and its various services
// using a uniform interface
type Client struct {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would prefer for Client to be an interface and have client internally as the type. Since the struct doesn't have any exported data, I don't think it would be an issue and it will definitely help when writing unit tests.


// New returns a new containerd client that is connected to the containerd
// instance provided by address
func New(address string, opts ...NewClientOpts) (*Client, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take a context here and use grc.DialContext()

func New(address string, opts ...NewClientOpts) (*Client, error) {
gopts := []grpc.DialOption{
grpc.WithInsecure(),
grpc.WithTimeout(100 * time.Second),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a WithTimeout NewClientOpts

container.go Outdated
Stderr: i.Stderr,
}
// get the rootfs from the snapshotter and add it to the request
mounts, err := c.client.snapshotter().Mounts(ctx, c.c.RootFS)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use of the snapshotter should be optional,. If c.c.RootFS is not set, this should just be skipped

spec.go Outdated
}
}

func WithArgs(args ...string) SpecOpts {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WithProcessArgs ?

task.go Outdated
return nil
}

type Task struct {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idem, make it an interface.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Easier to mock it in test that way

task.go Outdated
func (t *Task) Wait(ctx context.Context) (uint32, error) {
events, err := t.client.tasks().Events(ctx, &execution.EventsRequest{})
if err != nil {
return 255, err
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm more for UnknownExitStatus, but yes, probably better to have it defined (and exported)

@@ -0,0 +1,252 @@
package containerd
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cough, cough

windows? 😇

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec_linux.go?

Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
client_test.go Outdated
"testing"
)

const defaultAddress = "/run/containerd/containerd.sock"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a test flag here?

Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
@crosbymichael
Copy link
Copy Markdown
Member Author

This should be good for another round of review. We can get it merged in and continue filling out the missing functionality and getting integration tests running in a separate PR for travis.


// NewContainer will create a new container in container with the provided id
// the id must be unique within the namespace
func (c *Client) NewContainer(ctx context.Context, id string, spec *specs.Spec, opts ...NewContainerOpts) (Container, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To fully support runtimes that need more than the OCI spec (e.g. windows atm), we're either going to have to introduce an interface for the spec, or have the spec specified as an option (i.e. WithSpec)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mlaventure We could wrap it in an interface and then type assert?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think the WithSpec would do just fine and nicely fit into the current definition, wdyt?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really need a way to test windows stuff because its just guessing for most of us

client_test.go Outdated
func TestNewClient(t *testing.T) {
if testing.Short() {
t.Skip()
return
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to return, t.Skip calls os.Exit

_, err = client.Pull(context.Background(), ref)
if err != nil {
t.Error(err)
return
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

t.Fail

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I want the defers to run

@@ -0,0 +1,252 @@
package containerd
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec_linux.go?

Delete(context.Context) error
NewTask(context.Context, IOCreation) (Task, error)
Spec() (*specs.Spec, error)
Task() Task
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can we add labels to this container?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NewContainer(WithLabels)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if I want to change the labels on an existing container?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have an update method yet cuz this is the initial client PR

task.go Outdated
Resume(context.Context) error
Pid() uint32
Start(context.Context) error
Status(context.Context) (string, error)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to have a type for status? This is hard to discover in docker today because it is just a string field.

Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
@dmcgowan
Copy link
Copy Markdown
Member

LGTM

1 similar comment
@stevvooe
Copy link
Copy Markdown
Member

LGTM

@dmcgowan dmcgowan merged commit 199544e into containerd:master May 25, 2017
@crosbymichael crosbymichael deleted the client branch May 25, 2017 23:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants