From e5771e283032e6101e2649bf6af107d2336c0319 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Tue, 5 Dec 2017 11:46:04 -0800 Subject: [PATCH 1/2] integration: support local registry Signed-off-by: Tonis Tiigi --- hack/dockerfiles/test.Dockerfile | 4 + util/testutil/integration/containerd.go | 2 +- util/testutil/integration/registry.go | 102 ++++++++++++++++++++++++ util/testutil/integration/run.go | 1 + util/testutil/integration/standalone.go | 15 +++- 5 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 util/testutil/integration/registry.go diff --git a/hack/dockerfiles/test.Dockerfile b/hack/dockerfiles/test.Dockerfile index ef35330fb038..b0d826c6cd11 100644 --- a/hack/dockerfiles/test.Dockerfile +++ b/hack/dockerfiles/test.Dockerfile @@ -1,6 +1,7 @@ ARG RUNC_VERSION=74a17296470088de3805e138d3d87c62e613dfc4 ARG CONTAINERD_VERSION=v1.0.0 ARG BUILDKIT_TARGET=standalone +ARG REGISTRY_VERSION=2.6 FROM golang:1.9-alpine AS gobuild-base RUN apk add --no-cache g++ linux-headers @@ -44,10 +45,13 @@ FROM buildkit-base AS buildd-containerd ENV CGO_ENABLED=0 RUN go build -ldflags '-d' -o /usr/bin/buildd-containerd -tags containerd ./cmd/buildd +FROM registry:$REGISTRY_VERSION AS registry + FROM unit-tests AS integration-tests COPY --from=buildctl /usr/bin/buildctl /usr/bin/ COPY --from=buildd-containerd /usr/bin/buildd-containerd /usr/bin COPY --from=buildd-standalone /usr/bin/buildd-standalone /usr/bin +COPY --from=registry /bin/registry /usr/bin FROM gobuild-base AS cross-windows ENV GOOS=windows diff --git a/util/testutil/integration/containerd.go b/util/testutil/integration/containerd.go index 95d8ed364014..c5980c134b75 100644 --- a/util/testutil/integration/containerd.go +++ b/util/testutil/integration/containerd.go @@ -70,7 +70,7 @@ func (c *containerd) New() (sb Sandbox, cl func() error, err error) { } deferF.append(stop) - return &cdsandbox{address: address, sandbox: sandbox{address: builddSock, logs: logs}}, cl, nil + return &cdsandbox{address: address, sandbox: sandbox{address: builddSock, logs: logs, cleanup: deferF}}, cl, nil } type cdsandbox struct { diff --git a/util/testutil/integration/registry.go b/util/testutil/integration/registry.go new file mode 100644 index 000000000000..fa772dd22436 --- /dev/null +++ b/util/testutil/integration/registry.go @@ -0,0 +1,102 @@ +package integration + +import ( + "bufio" + "context" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "regexp" + "time" + + "github.com/pkg/errors" +) + +func newRegistry() (url string, cl func() error, err error) { + if err := lookupBinary("registry"); err != nil { + return "", nil, err + } + + deferF := &multiCloser{} + cl = deferF.F() + + defer func() { + if err != nil { + deferF.F()() + cl = nil + } + }() + + tmpdir, err := ioutil.TempDir("", "test-registry") + if err != nil { + return "", nil, err + } + deferF.append(func() error { return os.RemoveAll(tmpdir) }) + + template := fmt.Sprintf(`version: 0.1 +loglevel: debug +storage: + filesystem: + rootdirectory: %s +http: + addr: 127.0.0.1:0 +`, filepath.Join(tmpdir, "data")) + + if err := ioutil.WriteFile(filepath.Join(tmpdir, "config.yaml"), []byte(template), 0600); err != nil { + return "", nil, err + } + + cmd := exec.Command("registry", "serve", filepath.Join(tmpdir, "config.yaml")) + rc, err := cmd.StdoutPipe() + if err != nil { + return "", nil, err + } + if stop, err := startCmd(cmd, nil); err != nil { + return "", nil, err + } else { + deferF.append(stop) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + url, err = detectPort(ctx, rc) + if err != nil { + return "", nil, err + } + + return +} + +func detectPort(ctx context.Context, rc io.ReadCloser) (string, error) { + r := regexp.MustCompile("listening on 127\\.0\\.0\\.1:(\\d+)") + s := bufio.NewScanner(rc) + found := make(chan struct{}) + defer func() { + close(found) + go io.Copy(ioutil.Discard, rc) + }() + + go func() { + select { + case <-ctx.Done(): + select { + case <-found: + return + default: + rc.Close() + } + case <-found: + } + }() + + for s.Scan() { + res := r.FindSubmatch(s.Bytes()) + if len(res) > 1 { + return "localhost:" + string(res[1]), nil + } + } + return "", errors.Errorf("no listening address found") +} diff --git a/util/testutil/integration/run.go b/util/testutil/integration/run.go index 5ed363c300da..b53a548d06cc 100644 --- a/util/testutil/integration/run.go +++ b/util/testutil/integration/run.go @@ -16,6 +16,7 @@ type Sandbox interface { Address() string PrintLogs(*testing.T) Cmd(...string) *exec.Cmd + NewRegistry() (string, error) } type Worker interface { diff --git a/util/testutil/integration/standalone.go b/util/testutil/integration/standalone.go index 913e96395556..cf49e8b32088 100644 --- a/util/testutil/integration/standalone.go +++ b/util/testutil/integration/standalone.go @@ -38,12 +38,16 @@ func (s *standalone) New() (Sandbox, func() error, error) { return nil, nil, err } - return &sandbox{address: builddSock, logs: logs}, stop, nil + deferF := &multiCloser{} + deferF.append(stop) + + return &sandbox{address: builddSock, logs: logs, cleanup: deferF}, deferF.F(), nil } type sandbox struct { address string logs map[string]*bytes.Buffer + cleanup *multiCloser } func (sb *sandbox) Address() string { @@ -60,6 +64,15 @@ func (sb *sandbox) PrintLogs(t *testing.T) { } } +func (sb *sandbox) NewRegistry() (string, error) { + url, cl, err := newRegistry() + if err != nil { + return "", err + } + sb.cleanup.append(cl) + return url, nil +} + func (sb *sandbox) Cmd(args ...string) *exec.Cmd { if len(args) == 1 { if split, err := shlex.Split(args[0]); err == nil { From f5f3b39228ce6f4f8ebd86e88444419dc629fdb7 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Tue, 5 Dec 2017 15:32:52 -0800 Subject: [PATCH 2/2] client: add tests for differ and pushed data Signed-off-by: Tonis Tiigi --- client/client_test.go | 206 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/client/client_test.go b/client/client_test.go index 410965dbefd5..bad733275191 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1,19 +1,31 @@ package client import ( + "archive/tar" + "bytes" + "compress/gzip" "context" + "encoding/json" + "io" "io/ioutil" "net/http" "os" "path/filepath" "runtime" + "strings" "testing" "time" + "github.com/containerd/containerd" + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/namespaces" + "github.com/docker/distribution/manifest/schema2" "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/identity" "github.com/moby/buildkit/util/testutil/httpserver" "github.com/moby/buildkit/util/testutil/integration" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" "github.com/stretchr/testify/require" ) @@ -22,6 +34,7 @@ func TestClientIntegration(t *testing.T) { testCallDiskUsage, testBuildMultiMount, testBuildHTTPSource, + testBuildPushAndValidate, }) } @@ -143,8 +156,201 @@ func testBuildHTTPSource(t *testing.T, sb integration.Sandbox) { // TODO: check that second request was marked as cached } +func testBuildPushAndValidate(t *testing.T, sb integration.Sandbox) { + requiresLinux(t) + t.Parallel() + c, err := New(sb.Address()) + require.NoError(t, err) + defer c.Close() + + busybox := llb.Image("busybox:latest") + st := llb.Scratch() + + run := func(cmd string) { + st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st) + } + + run(`sh -c "mkdir -p foo/sub; echo -n first > foo/sub/bar; chmod 0741 foo;"`) + run(`sh -c "echo -n second > foo/sub/baz"`) + + def, err := st.Marshal() + require.NoError(t, err) + + registry, err := sb.NewRegistry() + if errors.Cause(err) == integration.ErrorRequirements { + t.Skip(err.Error()) + } + require.NoError(t, err) + + target := registry + "/buildkit/testpush:latest" + + err = c.Solve(context.TODO(), def, SolveOpt{ + Exporter: ExporterImage, + ExporterAttrs: map[string]string{ + "name": target, + "push": "true", + }, + }, nil) + require.NoError(t, err) + + // test existence of the image with next build + firstBuild := llb.Image(target) + + def, err = firstBuild.Marshal() + require.NoError(t, err) + + destDir, err := ioutil.TempDir("", "buildkit") + require.NoError(t, err) + + err = c.Solve(context.TODO(), def, SolveOpt{ + Exporter: ExporterLocal, + ExporterAttrs: map[string]string{ + "output": destDir, + }, + }, nil) + require.NoError(t, err) + + dt, err := ioutil.ReadFile(filepath.Join(destDir, "foo/sub/bar")) + require.NoError(t, err) + require.Equal(t, dt, []byte("first")) + + dt, err = ioutil.ReadFile(filepath.Join(destDir, "foo/sub/baz")) + require.NoError(t, err) + require.Equal(t, dt, []byte("second")) + + fi, err := os.Stat(filepath.Join(destDir, "foo")) + require.NoError(t, err) + require.Equal(t, 0741, int(fi.Mode()&0777)) + + // examine contents of exported tars (requires containerd) + var cdAddress string + if cd, ok := sb.(interface { + ContainerdAddress() string + }); !ok { + return + } else { + cdAddress = cd.ContainerdAddress() + } + + // TODO: make public pull helper function so this can be checked for standalone as well + + client, err := containerd.New(cdAddress) + require.NoError(t, err) + defer client.Close() + + ctx := namespaces.WithNamespace(context.Background(), "buildkit") + + img, err := client.Pull(ctx, target) + require.NoError(t, err) + + desc, err := img.Config(ctx) + require.NoError(t, err) + + dt, err = content.ReadBlob(ctx, img.ContentStore(), desc.Digest) + require.NoError(t, err) + + var ociimg ocispec.Image + err = json.Unmarshal(dt, &ociimg) + require.NoError(t, err) + + require.NotEqual(t, "", ociimg.OS) + require.NotEqual(t, "", ociimg.Architecture) + require.NotEqual(t, "", ociimg.Config.WorkingDir) + require.Equal(t, "layers", ociimg.RootFS.Type) + require.Equal(t, 2, len(ociimg.RootFS.DiffIDs)) + require.Condition(t, func() bool { + for _, env := range ociimg.Config.Env { + if strings.HasPrefix(env, "PATH=") { + return true + } + } + return false + }) + + dt, err = content.ReadBlob(ctx, img.ContentStore(), img.Target().Digest) + require.NoError(t, err) + + var mfst schema2.Manifest + err = json.Unmarshal(dt, &mfst) + require.NoError(t, err) + + require.Equal(t, schema2.MediaTypeManifest, mfst.MediaType) + require.Equal(t, 2, len(mfst.Layers)) + + dt, err = content.ReadBlob(ctx, img.ContentStore(), mfst.Layers[0].Digest) + require.NoError(t, err) + + m, err := readTarToMap(dt) + require.NoError(t, err) + + item, ok := m["foo/"] + require.True(t, ok) + require.Equal(t, int32(item.header.Typeflag), tar.TypeDir) + require.Equal(t, 0741, int(item.header.Mode&0777)) + + item, ok = m["foo/sub/"] + require.True(t, ok) + require.Equal(t, int32(item.header.Typeflag), tar.TypeDir) + + item, ok = m["foo/sub/bar"] + require.True(t, ok) + require.Equal(t, int32(item.header.Typeflag), tar.TypeReg) + require.Equal(t, []byte("first"), item.data) + + _, ok = m["foo/sub/baz"] + require.False(t, ok) + + dt, err = content.ReadBlob(ctx, img.ContentStore(), mfst.Layers[1].Digest) + require.NoError(t, err) + + m, err = readTarToMap(dt) + require.NoError(t, err) + + item, ok = m["foo/sub/baz"] + require.True(t, ok) + require.Equal(t, int32(item.header.Typeflag), tar.TypeReg) + require.Equal(t, []byte("second"), item.data) + + // TODO: #154 check that the unmodified parents are still in tar + // item, ok = m["foo/"] + // require.True(t, ok) + // require.Equal(t, int32(item.header.Typeflag), tar.TypeDir) +} + func requiresLinux(t *testing.T) { if runtime.GOOS != "linux" { t.Skipf("unsupported GOOS: %s", runtime.GOOS) } } + +type tarItem struct { + header *tar.Header + data []byte +} + +func readTarToMap(dt []byte) (map[string]*tarItem, error) { + m := map[string]*tarItem{} + gz, err := gzip.NewReader(bytes.NewBuffer(dt)) + if err != nil { + return nil, err + } + defer gz.Close() + rdr := tar.NewReader(gz) + for { + h, err := rdr.Next() + if err != nil { + if err == io.EOF { + return m, nil + } + return nil, err + } + var dt []byte + if h.Typeflag == tar.TypeReg { + dt, err = ioutil.ReadAll(rdr) + if err != nil { + return nil, err + } + } + m[h.Name] = &tarItem{header: h, data: dt} + } +}