Add integration tests for running containers

Add travis support for running integration tests with the client package
and go test framework

Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
This commit is contained in:
Michael Crosby 2017-05-25 13:52:42 -07:00
parent 36f9605479
commit b3f891b09f
7 changed files with 446 additions and 155 deletions

View File

@ -1,5 +1,8 @@
dist: trusty
sudo: required
# setup travis so that we can run containers for integration tests
services:
- docker
language: go
@ -13,6 +16,8 @@ addons:
apt:
packages:
- btrfs-tools
- libseccomp-dev
- libapparmor-dev
env:
- TRAVIS_GOOS=windows TRAVIS_CGO_ENABLED=1
@ -25,6 +30,7 @@ install:
- unzip -o -d /tmp/protobuf /tmp/protoc-3.1.0-linux-x86_64.zip
- export PATH=$PATH:/tmp/protobuf/bin/
- go get -u github.com/vbatts/git-validation
- sudo wget https://github.com/crosbymichael/runc/releases/download/ctd-1/runc -O /bin/runc; sudo chmod +x /bin/runc
script:
- export GOOS=$TRAVIS_GOOS
@ -33,8 +39,10 @@ script:
- make fmt
- make vet
- make binaries
- if [ "$GOOS" = "linux" ]; then sudo make install ; fi
- if [ "$GOOS" = "linux" ]; then make coverage ; fi
- if [ "$GOOS" = "linux" ]; then sudo PATH=$PATH GOPATH=$GOPATH make root-coverage ; fi
- if [ "$GOOS" = "linux" ]; then sudo PATH=$PATH GOPATH=$GOPATH make integration ; fi
after_success:
- bash <(curl -s https://codecov.io/bash)

View File

@ -125,7 +125,7 @@ root-test: ## run tests, except integration tests
integration: ## run integration tests
@echo "$(WHALE) $@"
@go test ${TESTFLAGS} ${INTEGRATION_PACKAGE}
@go test ${TESTFLAGS}
FORCE:

View File

@ -33,6 +33,7 @@ import (
"github.com/pkg/errors"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/health/grpc_health_v1"
)
func init() {
@ -82,6 +83,14 @@ type Client struct {
namespace string
}
func (c *Client) IsServing(ctx context.Context) (bool, error) {
r, err := c.HealthService().Check(ctx, &grpc_health_v1.HealthCheckRequest{})
if err != nil {
return false, err
}
return r.Status == grpc_health_v1.HealthCheckResponse_SERVING, nil
}
// Containers returns all containers created in containerd
func (c *Client) Containers(ctx context.Context) ([]Container, error) {
r, err := c.ContainerService().List(ctx, &containers.ListContainersRequest{})
@ -309,6 +318,18 @@ func (c *Client) Pull(ctx context.Context, ref string, opts ...PullOpts) (Image,
}, nil
}
// GetImage returns an existing image
func (c *Client) GetImage(ctx context.Context, ref string) (Image, error) {
i, err := c.ImageService().Get(ctx, ref)
if err != nil {
return nil, err
}
return &image{
client: c,
i: i,
}, nil
}
// Close closes the clients connection to containerd
func (c *Client) Close() error {
return c.conn.Close()
@ -337,3 +358,7 @@ func (c *Client) ImageService() images.Store {
func (c *Client) DiffService() diff.DiffService {
return diffservice.NewDiffServiceFromClient(diffapi.NewDiffClient(c.conn))
}
func (c *Client) HealthService() grpc_health_v1.HealthClient {
return grpc_health_v1.NewHealthClient(c.conn)
}

View File

@ -1,17 +1,99 @@
package containerd
import (
"bytes"
"context"
"flag"
"fmt"
"os"
"os/exec"
"syscall"
"testing"
"time"
)
const (
defaultRoot = "/var/lib/containerd-test"
defaultState = "/run/containerd-test"
testImage = "docker.io/library/alpine:latest"
)
var address string
func init() {
flag.StringVar(&address, "address", "/run/containerd/containerd.sock", "The address to the containerd socket for use in the tests")
flag.Parse()
}
var address string
func TestMain(m *testing.M) {
if testing.Short() {
os.Exit(m.Run())
}
// setup a new containerd daemon if !testing.Short
cmd := exec.Command("containerd",
"--root", defaultRoot,
"--state", defaultState,
)
buf := bytes.NewBuffer(nil)
cmd.Stderr = buf
if err := cmd.Start(); err != nil {
fmt.Println(err)
os.Exit(1)
}
client, err := New(address)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if err := waitForDaemonStart(client); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// pull a seed image
if _, err = client.Pull(context.Background(), testImage, WithPullUnpack); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if err := client.Close(); err != nil {
fmt.Fprintln(os.Stderr, err)
}
// run the test
status := m.Run()
// tear down the daemon and resources created
if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
fmt.Fprintln(os.Stderr, err)
}
if _, err := cmd.Process.Wait(); err != nil {
fmt.Fprintln(os.Stderr, err)
}
if err := os.RemoveAll(defaultRoot); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// only print containerd logs if the test failed
if status != 0 {
fmt.Fprintln(os.Stderr, buf.String())
}
os.Exit(status)
}
func waitForDaemonStart(client *Client) error {
var (
serving bool
err error
)
for i := 0; i < 20; i++ {
serving, err = client.IsServing(context.Background())
if serving {
return nil
}
time.Sleep(100 * time.Millisecond)
}
return fmt.Errorf("containerd did not start within 2s: %v", err)
}
func TestNewClient(t *testing.T) {
if testing.Short() {
@ -39,8 +121,7 @@ func TestImagePull(t *testing.T) {
}
defer client.Close()
const ref = "docker.io/library/alpine:latest"
_, err = client.Pull(context.Background(), ref)
_, err = client.Pull(context.Background(), testImage)
if err != nil {
t.Error(err)
return

View File

@ -1,6 +1,7 @@
package containerd
import (
"bytes"
"context"
"testing"
)
@ -29,13 +30,13 @@ func TestNewContainer(t *testing.T) {
if testing.Short() {
t.Skip()
}
id := "NewContainer"
client, err := New(address)
if err != nil {
t.Fatal(err)
}
defer client.Close()
id := "test"
spec, err := GenerateSpec()
if err != nil {
t.Error(err)
@ -46,6 +47,7 @@ func TestNewContainer(t *testing.T) {
t.Error(err)
return
}
defer container.Delete(context.Background())
if container.ID() != id {
t.Errorf("expected container id %q but received %q", id, container.ID())
}
@ -58,3 +60,144 @@ func TestNewContainer(t *testing.T) {
return
}
}
func TestContainerStart(t *testing.T) {
if testing.Short() {
t.Skip()
return
}
client, err := New(address)
if err != nil {
t.Fatal(err)
}
defer client.Close()
var (
ctx = context.Background()
id = "ContainerStart"
)
image, err := client.GetImage(ctx, testImage)
if err != nil {
t.Error(err)
return
}
spec, err := GenerateSpec(WithImageConfig(ctx, image), WithProcessArgs("sh", "-c", "exit 7"))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, spec, WithImage(image), WithNewRootFS(id, image))
if err != nil {
t.Error(err)
return
}
defer container.Delete(ctx)
task, err := container.NewTask(ctx, Stdio)
if err != nil {
t.Error(err)
return
}
defer task.Delete(ctx)
statusC := make(chan uint32, 1)
go func() {
status, err := task.Wait(ctx)
if err != nil {
t.Error(err)
}
statusC <- status
}()
if pid := task.Pid(); pid <= 0 {
t.Errorf("invalid task pid %d", pid)
}
if err := task.Start(ctx); err != nil {
t.Error(err)
task.Delete(ctx)
return
}
status := <-statusC
if status != 7 {
t.Errorf("expected status 7 from wait but received %d", status)
}
if status, err = task.Delete(ctx); err != nil {
t.Error(err)
return
}
if status != 7 {
t.Errorf("expected status 7 from delete but received %d", status)
}
}
func TestContainerOutput(t *testing.T) {
if testing.Short() {
t.Skip()
return
}
client, err := New(address)
if err != nil {
t.Fatal(err)
}
defer client.Close()
var (
ctx = context.Background()
id = "ContainerOutput"
expected = "kingkoye"
)
image, err := client.GetImage(ctx, testImage)
if err != nil {
t.Error(err)
return
}
spec, err := GenerateSpec(WithImageConfig(ctx, image), WithProcessArgs("echo", expected))
if err != nil {
t.Error(err)
return
}
container, err := client.NewContainer(ctx, id, spec, WithImage(image), WithNewRootFS(id, image))
if err != nil {
t.Error(err)
return
}
defer container.Delete(ctx)
stdout := bytes.NewBuffer(nil)
task, err := container.NewTask(ctx, BufferedIO(bytes.NewBuffer(nil), stdout, bytes.NewBuffer(nil)))
if err != nil {
t.Error(err)
return
}
defer task.Delete(ctx)
statusC := make(chan uint32, 1)
go func() {
status, err := task.Wait(ctx)
if err != nil {
t.Error(err)
}
statusC <- status
}()
if err := task.Start(ctx); err != nil {
t.Error(err)
return
}
status := <-statusC
if status != 0 {
t.Errorf("expected status 0 but received %d", status)
}
if _, err := task.Delete(ctx); err != nil {
t.Error(err)
return
}
actual := stdout.String()
// echo adds a new line
expected = expected + "\n"
if actual != expected {
t.Errorf("expected output %q but received %q", expected, actual)
}
}

184
io.go Normal file
View File

@ -0,0 +1,184 @@
package containerd
import (
"bytes"
"context"
"io"
"io/ioutil"
"os"
"path/filepath"
"sync"
"syscall"
"github.com/containerd/fifo"
)
type IO struct {
Terminal bool
Stdin string
Stdout string
Stderr string
closer io.Closer
}
func (i *IO) Close() error {
if i.closer == nil {
return nil
}
return i.closer.Close()
}
type IOCreation func() (*IO, error)
// BufferedIO returns IO that will be logged to an in memory buffer
func BufferedIO(stdin, stdout, stderr *bytes.Buffer) IOCreation {
return func() (*IO, error) {
paths, err := fifoPaths()
if err != nil {
return nil, err
}
i := &IO{
Terminal: false,
Stdout: paths.out,
Stderr: paths.err,
Stdin: paths.in,
}
set := &ioSet{
in: stdin,
out: stdout,
err: stderr,
}
closer, err := copyIO(paths, set, false)
if err != nil {
return nil, err
}
i.closer = closer
return i, nil
}
}
// Stdio returns an IO implementation to be used for a task
// that outputs the container's IO as the current processes Stdio
func Stdio() (*IO, error) {
paths, err := fifoPaths()
if err != nil {
return nil, err
}
set := &ioSet{
in: os.Stdin,
out: os.Stdout,
err: os.Stderr,
}
closer, err := copyIO(paths, set, false)
if err != nil {
return nil, err
}
return &IO{
Terminal: false,
Stdin: paths.in,
Stdout: paths.out,
Stderr: paths.err,
closer: closer,
}, nil
}
func fifoPaths() (*fifoSet, error) {
root := filepath.Join(os.TempDir(), "containerd")
if err := os.MkdirAll(root, 0700); err != nil {
return nil, err
}
dir, err := ioutil.TempDir(root, "")
if err != nil {
return nil, err
}
return &fifoSet{
dir: dir,
in: filepath.Join(dir, "stdin"),
out: filepath.Join(dir, "stdout"),
err: filepath.Join(dir, "stderr"),
}, nil
}
type fifoSet struct {
// dir is the directory holding the task fifos
dir string
in, out, err string
}
type ioSet struct {
in io.Reader
out, err io.Writer
}
func copyIO(fifos *fifoSet, ioset *ioSet, tty bool) (closer io.Closer, err error) {
var (
f io.ReadWriteCloser
ctx = context.Background()
wg = &sync.WaitGroup{}
)
if f, err = fifo.OpenFifo(ctx, fifos.in, syscall.O_WRONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0700); err != nil {
return nil, err
}
defer func(c io.Closer) {
if err != nil {
c.Close()
}
}(f)
go func(w io.WriteCloser) {
io.Copy(w, ioset.in)
w.Close()
}(f)
if f, err = fifo.OpenFifo(ctx, fifos.out, syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0700); err != nil {
return nil, err
}
defer func(c io.Closer) {
if err != nil {
c.Close()
}
}(f)
wg.Add(1)
go func(r io.ReadCloser) {
io.Copy(ioset.out, r)
r.Close()
wg.Done()
}(f)
if f, err = fifo.OpenFifo(ctx, fifos.err, syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0700); err != nil {
return nil, err
}
defer func(c io.Closer) {
if err != nil {
c.Close()
}
}(f)
if !tty {
wg.Add(1)
go func(r io.ReadCloser) {
io.Copy(ioset.err, r)
r.Close()
wg.Done()
}(f)
}
return &wgCloser{
wg: wg,
dir: fifos.dir,
}, nil
}
type wgCloser struct {
wg *sync.WaitGroup
dir string
}
func (g *wgCloser) Close() error {
g.wg.Wait()
if g.dir != "" {
return os.RemoveAll(g.dir)
}
return nil
}

150
task.go
View File

@ -2,164 +2,14 @@ package containerd
import (
"context"
"io"
"io/ioutil"
"os"
"path/filepath"
"sync"
"syscall"
"github.com/containerd/containerd/api/services/execution"
taskapi "github.com/containerd/containerd/api/types/task"
"github.com/containerd/fifo"
)
const UnknownExitStatus = 255
type IO struct {
Terminal bool
Stdin string
Stdout string
Stderr string
closer io.Closer
}
func (i *IO) Close() error {
if i.closer == nil {
return nil
}
return i.closer.Close()
}
type IOCreation func() (*IO, error)
// Stdio returns an IO implementation to be used for a task
// that outputs the container's IO as the current processes Stdio
func Stdio() (*IO, error) {
paths, err := fifoPaths()
if err != nil {
return nil, err
}
set := &ioSet{
in: os.Stdin,
out: os.Stdout,
err: os.Stderr,
}
closer, err := copyIO(paths, set, false)
if err != nil {
return nil, err
}
return &IO{
Terminal: false,
Stdin: paths.in,
Stdout: paths.out,
Stderr: paths.err,
closer: closer,
}, nil
}
func fifoPaths() (*fifoSet, error) {
root := filepath.Join(os.TempDir(), "containerd")
if err := os.MkdirAll(root, 0700); err != nil {
return nil, err
}
dir, err := ioutil.TempDir(root, "")
if err != nil {
return nil, err
}
return &fifoSet{
dir: dir,
in: filepath.Join(dir, "stdin"),
out: filepath.Join(dir, "stdout"),
err: filepath.Join(dir, "stderr"),
}, nil
}
type fifoSet struct {
// dir is the directory holding the task fifos
dir string
in, out, err string
}
type ioSet struct {
in io.Reader
out, err io.Writer
}
func copyIO(fifos *fifoSet, ioset *ioSet, tty bool) (closer io.Closer, err error) {
var (
ctx = context.Background()
wg = &sync.WaitGroup{}
)
f, err := fifo.OpenFifo(ctx, fifos.in, syscall.O_WRONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0700)
if err != nil {
return nil, err
}
defer func(c io.Closer) {
if err != nil {
c.Close()
}
}(f)
go func(w io.WriteCloser) {
io.Copy(w, ioset.in)
w.Close()
}(f)
f, err = fifo.OpenFifo(ctx, fifos.out, syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0700)
if err != nil {
return nil, err
}
defer func(c io.Closer) {
if err != nil {
c.Close()
}
}(f)
wg.Add(1)
go func(r io.ReadCloser) {
io.Copy(ioset.out, r)
r.Close()
wg.Done()
}(f)
f, err = fifo.OpenFifo(ctx, fifos.err, syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0700)
if err != nil {
return nil, err
}
defer func(c io.Closer) {
if err != nil {
c.Close()
}
}(f)
if !tty {
wg.Add(1)
go func(r io.ReadCloser) {
io.Copy(ioset.err, r)
r.Close()
wg.Done()
}(f)
}
return &wgCloser{
wg: wg,
dir: fifos.dir,
}, nil
}
type wgCloser struct {
wg *sync.WaitGroup
dir string
}
func (g *wgCloser) Close() error {
g.wg.Wait()
if g.dir != "" {
return os.RemoveAll(g.dir)
}
return nil
}
type TaskStatus string
const (