Add initial containerd *Client

Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
This commit is contained in:
Michael Crosby 2017-05-23 15:50:06 -07:00
parent 17033dcaf2
commit d0e5732f0b
10 changed files with 482 additions and 0 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/bin/ /bin/
**/coverage.txt **/coverage.txt
**/coverage.out **/coverage.out
containerd.test

61
client.go Normal file
View File

@ -0,0 +1,61 @@
package containerd
import (
"context"
"io/ioutil"
"log"
"time"
"github.com/containerd/containerd/api/services/containers"
"github.com/pkg/errors"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
)
// New returns a new containerd client that is connected to the containerd
// instance provided by address
func New(address string) (*Client, error) {
// reset the grpc logger so that it does not output in the STDIO of the calling process
grpclog.SetLogger(log.New(ioutil.Discard, "", log.LstdFlags))
opts := []grpc.DialOption{
grpc.WithInsecure(),
grpc.WithTimeout(100 * time.Second),
grpc.WithDialer(dialer),
}
conn, err := grpc.Dial(dialAddress(address), opts...)
if err != nil {
return nil, errors.Wrapf(err, "failed to dial %q", address)
}
return &Client{
conn: conn,
}, nil
}
// Client is the client to interact with containerd and its various services
// using a uniform interface
type Client struct {
conn *grpc.ClientConn
}
// Containers returns all containers created in containerd
func (c *Client) Containers(ctx context.Context) ([]*Container, error) {
r, err := c.containers().List(ctx, &containers.ListContainersRequest{})
if err != nil {
return nil, err
}
var out []*Container
for _, container := range r.Containers {
out = append(out, containerFromProto(c, container))
}
return out, nil
}
// Close closes the clients connection to containerd
func (c *Client) Close() error {
return c.conn.Close()
}
func (c *Client) containers() containers.ContainersClient {
return containers.NewContainersClient(c.conn)
}

18
client_test.go Normal file
View File

@ -0,0 +1,18 @@
package containerd
import "testing"
const defaultAddress = "/run/containerd/containerd.sock"
func TestNewClient(t *testing.T) {
client, err := New(defaultAddress)
if err != nil {
t.Fatal(err)
}
if client == nil {
t.Fatal("New() returned nil client")
}
if err := client.Close(); err != nil {
t.Errorf("client closed returned errror %v", err)
}
}

17
client_unix.go Normal file
View File

@ -0,0 +1,17 @@
package containerd
import (
"fmt"
"net"
"strings"
"time"
)
func dialer(address string, timeout time.Duration) (net.Conn, error) {
address = strings.TrimPrefix(address, "unix://")
return net.DialTimeout("unix", address, timeout)
}
func dialAddress(address string) string {
return fmt.Sprintf("unix://%s", address)
}

16
client_windows.go Normal file
View File

@ -0,0 +1,16 @@
package containerd
import (
"net"
"time"
winio "github.com/Microsoft/go-winio"
)
func dialer(address string, timeout time.Duration) (net.Conn, error) {
return winio.DialPipe(bindAddress, &timeout)
}
func dialAddress(address string) string {
return address
}

21
container.go Normal file
View File

@ -0,0 +1,21 @@
package containerd
import "github.com/containerd/containerd/api/services/containers"
func containerFromProto(client *Client, c containers.Container) *Container {
return &Container{
client: client,
id: c.ID,
}
}
type Container struct {
client *Client
id string
}
// ID returns the container's unique id
func (c *Container) ID() string {
return c.id
}

23
container_test.go Normal file
View File

@ -0,0 +1,23 @@
package containerd
import (
"context"
"testing"
)
func TestContainerList(t *testing.T) {
client, err := New(defaultAddress)
if err != nil {
t.Fatal(err)
}
defer client.Close()
containers, err := client.Containers(context.Background())
if err != nil {
t.Errorf("container list returned error %v", err)
return
}
if len(containers) != 0 {
t.Errorf("expected 0 containers but received %d", len(containers))
}
}

44
spec.go Normal file
View File

@ -0,0 +1,44 @@
package containerd
import specs "github.com/opencontainers/runtime-spec/specs-go"
type SpecOpts func(s *specs.Spec) error
func WithImageRef(ref string) SpecOpts {
return func(s *specs.Spec) error {
if s.Annotations == nil {
s.Annotations = make(map[string]string)
}
s.Annotations["image"] = ref
return nil
}
}
func WithHostname(id string) SpecOpts {
return func(s *specs.Spec) error {
s.Hostname = id
return nil
}
}
func WithArgs(args ...string) SpecOpts {
return func(s *specs.Spec) error {
s.Process.Args = args
return nil
}
}
// GenerateSpec will generate a default spec from the provided image
// for use as a containerd container
func GenerateSpec(opts ...SpecOpts) (*specs.Spec, error) {
s, err := createDefaultSpec()
if err != nil {
return nil, err
}
for _, o := range opts {
if err := o(s); err != nil {
return nil, err
}
}
return s, nil
}

225
spec_unix.go Normal file
View File

@ -0,0 +1,225 @@
package containerd
import (
"fmt"
"runtime"
"strconv"
"strings"
"github.com/opencontainers/image-spec/specs-go/v1"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
const (
rwm = "rwm"
defaultRootfsPath = "rootfs"
)
func defaltCaps() []string {
return []string{
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_FSETID",
"CAP_FOWNER",
"CAP_MKNOD",
"CAP_NET_RAW",
"CAP_SETGID",
"CAP_SETUID",
"CAP_SETFCAP",
"CAP_SETPCAP",
"CAP_NET_BIND_SERVICE",
"CAP_SYS_CHROOT",
"CAP_KILL",
"CAP_AUDIT_WRITE",
}
}
func defaultNamespaces() []specs.LinuxNamespace {
return []specs.LinuxNamespace{
{
Type: specs.PIDNamespace,
},
{
Type: specs.IPCNamespace,
},
{
Type: specs.UTSNamespace,
},
{
Type: specs.MountNamespace,
},
{
Type: specs.NetworkNamespace,
},
}
}
func createDefaultSpec() (*specs.Spec, error) {
s := &specs.Spec{
Version: specs.Version,
Platform: specs.Platform{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
},
Root: specs.Root{
Path: defaultRootfsPath,
},
Process: specs.Process{
Cwd: "/",
NoNewPrivileges: true,
User: specs.User{
UID: 0,
GID: 0,
},
Capabilities: &specs.LinuxCapabilities{
Bounding: defaltCaps(),
Permitted: defaltCaps(),
Inheritable: defaltCaps(),
Effective: defaltCaps(),
Ambient: defaltCaps(),
},
Rlimits: []specs.LinuxRlimit{
{
Type: "RLIMIT_NOFILE",
Hard: uint64(1024),
Soft: uint64(1024),
},
},
},
Mounts: []specs.Mount{
{
Destination: "/proc",
Type: "proc",
Source: "proc",
},
{
Destination: "/dev",
Type: "tmpfs",
Source: "tmpfs",
Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
},
{
Destination: "/dev/pts",
Type: "devpts",
Source: "devpts",
Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620", "gid=5"},
},
{
Destination: "/dev/shm",
Type: "tmpfs",
Source: "shm",
Options: []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k"},
},
{
Destination: "/dev/mqueue",
Type: "mqueue",
Source: "mqueue",
Options: []string{"nosuid", "noexec", "nodev"},
},
{
Destination: "/sys",
Type: "sysfs",
Source: "sysfs",
Options: []string{"nosuid", "noexec", "nodev", "ro"},
},
{
Destination: "/run",
Type: "tmpfs",
Source: "tmpfs",
Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
},
{
Destination: "/etc/resolv.conf",
Type: "bind",
Source: "/etc/resolv.conf",
Options: []string{"rbind", "ro"},
},
{
Destination: "/etc/hosts",
Type: "bind",
Source: "/etc/hosts",
Options: []string{"rbind", "ro"},
},
{
Destination: "/etc/localtime",
Type: "bind",
Source: "/etc/localtime",
Options: []string{"rbind", "ro"},
},
},
Linux: &specs.Linux{
Resources: &specs.LinuxResources{
Devices: []specs.LinuxDeviceCgroup{
{
Allow: false,
Access: rwm,
},
},
},
Namespaces: defaultNamespaces(),
},
}
return s, nil
}
func WithTTY(s *specs.Spec) error {
s.Process.Terminal = true
s.Process.Env = append(s.Process.Env, "TERM=xterm")
return nil
}
func WithHostNamespace(ns specs.LinuxNamespaceType) SpecOpts {
return func(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:]...)
return nil
}
}
return nil
}
}
func WithImage(config *v1.ImageConfig) SpecOpts {
return func(s *specs.Spec) error {
env := []string{
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
}
s.Process.Env = append(env, config.Env...)
cmd := config.Cmd
var (
uid, gid uint32
)
s.Process.Args = append(config.Entrypoint, cmd...)
if config.User != "" {
parts := strings.Split(config.User, ":")
switch len(parts) {
case 1:
v, err := strconv.ParseUint(parts[0], 0, 10)
if err != nil {
return err
}
uid, gid = uint32(v), uint32(v)
case 2:
v, err := strconv.ParseUint(parts[0], 0, 10)
if err != nil {
return err
}
uid = uint32(v)
if v, err = strconv.ParseUint(parts[1], 0, 10); err != nil {
return err
}
gid = uint32(v)
default:
return fmt.Errorf("invalid USER value %s", config.User)
}
}
s.Process.User.UID, s.Process.User.GID = uid, gid
cwd := config.WorkingDir
if cwd == "" {
cwd = "/"
}
s.Process.Cwd = cwd
return nil
}
}

56
spec_unix_test.go Normal file
View File

@ -0,0 +1,56 @@
package containerd
import "testing"
func TestGenerateSpec(t *testing.T) {
s, err := GenerateSpec()
if err != nil {
t.Fatal(err)
}
if s == nil {
t.Fatal("GenerateSpec() returns a nil spec")
}
// check for matching caps
defaults := defaltCaps()
for _, cl := range [][]string{
s.Process.Capabilities.Ambient,
s.Process.Capabilities.Bounding,
s.Process.Capabilities.Permitted,
s.Process.Capabilities.Inheritable,
s.Process.Capabilities.Effective,
} {
for i := 0; i < len(defaults); i++ {
if cl[i] != defaults[i] {
t.Errorf("cap at %d does not match set %q != %q", i, defaults[i], cl[i])
}
}
}
// check default namespaces
defaultNS := defaultNamespaces()
for i, ns := range s.Linux.Namespaces {
if defaultNS[i] != ns {
t.Errorf("ns at %d does not match set %q != %q", i, defaultNS[i], ns)
}
}
// test that we don't have tty set
if s.Process.Terminal {
t.Error("terminal set on default process")
}
}
func TestSpecWithTTY(t *testing.T) {
s, err := GenerateSpec(WithTTY)
if err != nil {
t.Fatal(err)
}
if !s.Process.Terminal {
t.Error("terminal net set WithTTY()")
}
v := s.Process.Env[len(s.Process.Env)-1]
if v != "TERM=xterm" {
t.Errorf("xterm not set in env for TTY")
}
}