Skip to content
This repository has been archived by the owner on Feb 27, 2020. It is now read-only.

Commit

Permalink
Merge pull request #384 from taskcluster/docker-integration-test
Browse files Browse the repository at this point in the history
Docker integration test
  • Loading branch information
jonasfj authored May 23, 2018
2 parents 09e0dc2 + 0af310e commit 4d93412
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 10 deletions.
4 changes: 1 addition & 3 deletions engines/docker/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import (
)

// Image and tag used in test cases below
const (
dockerImageName = "alpine:3.6"
)
const dockerImageName = "alpine:3.6"

var provider = &enginetest.EngineProvider{
Engine: "docker",
Expand Down
33 changes: 26 additions & 7 deletions engines/docker/resultset.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (r *resultSet) ExtractFile(path string) (ioext.ReadSeekCloser, error) {

var result ioext.ReadSeekCloser
var m sync.Mutex
err := r.extractResource(path, func(p string, stream ioext.ReadSeekCloser) error {
err := r.extractResource(path, false, func(p string, stream ioext.ReadSeekCloser) error {
m.Lock()
defer m.Unlock()
if result != nil {
Expand Down Expand Up @@ -102,7 +102,7 @@ func (r *resultSet) ExtractFolder(path string, handler engines.FileHandler) erro
))
}
path += "/"
return r.extractResource(path, func(name string, stream ioext.ReadSeekCloser) error {
return r.extractResource(path, true, func(name string, stream ioext.ReadSeekCloser) error {
// Make the name relative to path
if !strings.HasPrefix(name, "/") {
name = "/" + name // Ensure we always have an absolute path
Expand All @@ -112,7 +112,7 @@ func (r *resultSet) ExtractFolder(path string, handler engines.FileHandler) erro
})
}

func (r *resultSet) extractResource(path string, handler engines.FileHandler) error {
func (r *resultSet) extractResource(path string, isFolder bool, handler engines.FileHandler) error {
// We force path to be absolute, this is the only sane thing
if !strings.HasPrefix(path, "/") {
debug("extractResource(%s) doesn't start with '/', hence it is a relative path", path)
Expand All @@ -125,9 +125,10 @@ func (r *resultSet) extractResource(path string, handler engines.FileHandler) er
stream, streamWriter := io.Pipe()

ctx, cancel := context.WithCancel(r.context)
notFound := false
var notFound atomics.Bool
var derr, rerr error
var interrupted atomics.Once
count := 0 // count entries in tar-stream
util.Parallel(func() {
var wg atomics.WaitGroup
defer wg.Wait() // Wait for all handlers to be done
Expand All @@ -147,7 +148,19 @@ func (r *resultSet) extractResource(path string, handler engines.FileHandler) er
rerr = errors.Wrap(rerr, "failed to read tar-stream from docker")
return
}
// HACK: docker seems to happily return a stream even if the file pointed to is a directory
// this is a problem. docker does require paths for directories to end in slash '/', so
// it could be a bug, it just bad design. To workaround this issue we count the number
// of entries and report file-not-found, if there is more than one entry in a tar-stream
// that is supposed to contain a single file.
count++
if count > 1 && !isFolder {
debug("extractResource(%s) found more than one entry when extracting a file", path)
notFound.Set(true)
return
}
if !ioext.IsPlainFileInfo(hdr.FileInfo()) {
debug("skipping non file at: %s", hdr.Name)
continue // skip entries that aren't files
}
// Sanity check on the file size mostly in case someone tries to export
Expand Down Expand Up @@ -199,7 +212,7 @@ func (r *resultSet) extractResource(path string, handler engines.FileHandler) er
}(tmpfile)
}
}, func() {
debug("extractResource(%s) extracting a folder", path)
debug("extractResource(%s) calling docker.DownloadFromContainer(%s, %s)", path, r.containerID, path)
derr = r.docker.DownloadFromContainer(r.containerID, docker.DownloadFromContainerOptions{
Context: ctx,
OutputStream: streamWriter,
Expand All @@ -215,7 +228,13 @@ func (r *resultSet) extractResource(path string, handler engines.FileHandler) er
if e, ok := derr.(*docker.Error); ok && (e.Status == 400 || e.Status == 404) {
// Note: this could also be container is missing, but that would be an internal
// error, as we haven't removed it yet. So we assume that can't happen.
notFound = true
notFound.Set(true)
derr = nil
} else if e, ok := derr.(*docker.Error); ok && e.Status == 500 && strings.Contains(e.Message, "not a directory") {
// HACK: docker returns 500 with a message explaining that path is "not a directory", if path is a file instead of
// a directory. Ideally, docker should return 4xx, it's plausibly a bug, or inconsistency, anyways this
// workarounds the issue by grepping the message for "not a directory"
notFound.Set(true)
derr = nil
} else {
derr = errors.Wrap(derr, "docker.DownloadFromContainer failed")
Expand All @@ -228,7 +247,7 @@ func (r *resultSet) extractResource(path string, handler engines.FileHandler) er
if interrupted.IsDone() {
return engines.ErrHandlerInterrupt
}
if notFound {
if notFound.Get() {
return engines.ErrResourceNotFound
}
if rerr != nil {
Expand Down
159 changes: 159 additions & 0 deletions test/dockertest/artifacts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// +build linux,docker

package dockertest

import (
"strings"
"testing"

"github.com/taskcluster/slugid-go/slugid"
"github.com/taskcluster/taskcluster-worker/worker/workertest"
)

func TestArtifacts(t *testing.T) {
debug("### Testing artifact plugin with docker engine")
filename := slugid.Nice()
debug("generated filename: %s", filename)

workertest.Case{
Engine: "docker",
Concurrency: 1,
EngineConfig: engineConfig,
PluginConfig: pluginConfig,
Tasks: workertest.Tasks([]workertest.Task{{
Title: "Artifact File",
Success: true,
Payload: `{
"image": "` + dockerImageName + `",
"command": ["sh", "-c", "echo 'hello-world' && echo 42 > /my-file-` + filename + `.txt"],
"env": {},
"maxRunTime": "10 minutes",
"artifacts": [
{
"name": "public/result.txt",
"type": "file",
"path": "/my-file-` + filename + `.txt"
}
]
}`,
Artifacts: workertest.ArtifactAssertions{
"public/logs/live.log": workertest.ReferenceArtifact(),
"public/logs/live_backing.log": workertest.GrepArtifact("hello-world"),
"public/result.txt": workertest.GrepArtifact("42"),
},
}, {
Title: "Artifact Directory",
Success: true,
Payload: `{
"image": "` + dockerImageName + `",
"command": ["sh", "-c", "` + strings.Join([]string{
"echo 'hello-world'",
"mkdir -p /sub/subsub/subsubsub",
"echo 42 > /sub/subsub/result.txt",
"echo -n '<html></html>' > /sub/subsub/subsubsub/result.html",
}, " && ") + `"],
"env": {},
"maxRunTime": "10 minutes",
"artifacts": [
{
"name": "public",
"type": "directory",
"path": "/sub"
}
]
}`,
Artifacts: workertest.ArtifactAssertions{
"public/logs/live.log": workertest.ReferenceArtifact(),
"public/logs/live_backing.log": workertest.GrepArtifact("hello-world"),
"public/subsub/result.txt": workertest.GrepArtifact("42"),
"public/subsub/subsubsub/result.html": workertest.MatchArtifact("<html></html>", "text/html; charset=utf-8"),
},
}, {
Title: "Artifact Directory Is File",
Success: false,
Payload: `{
"image": "` + dockerImageName + `",
"command": ["sh", "-c", "echo 42 > /notafolder"],
"env": {},
"maxRunTime": "10 minutes",
"artifacts": [
{
"name": "public/myfolder",
"type": "directory",
"path": "/notafolder"
}
]
}`,
Artifacts: workertest.ArtifactAssertions{
"public/logs/live.log": workertest.ReferenceArtifact(),
"public/logs/live_backing.log": workertest.GrepArtifact("/notafolder"),
"public/myfolder": workertest.ErrorArtifact(),
},
}, {
Title: "Artifact File Is Directory",
Success: false,
Payload: `{
"image": "` + dockerImageName + `",
"command": ["sh", "-c", "mkdir -p /sub/subsub/ && echo 42 > /sub/subsub/result.txt"],
"env": {},
"maxRunTime": "10 minutes",
"artifacts": [
{
"name": "public/subsub",
"type": "file",
"path": "/sub/subsub"
}
]
}`,
Artifacts: workertest.ArtifactAssertions{
// Expect some error message saying "sub/subsub"
"public/logs/live.log": workertest.ReferenceArtifact(),
"public/logs/live_backing.log": workertest.GrepArtifact("/sub/subsub"),
"public/subsub": workertest.ErrorArtifact(),
},
}, {
Title: "Artifact File Not Found",
Success: false,
Payload: `{
"image": "` + dockerImageName + `",
"command": ["true"],
"env": {},
"maxRunTime": "10 minutes",
"artifacts": [
{
"name": "public/result.txt",
"type": "file",
"path": "/no-such-file-` + filename + `.txt"
}
]
}`,
Artifacts: workertest.ArtifactAssertions{
"public/logs/live.log": workertest.ReferenceArtifact(),
"public/logs/live_backing.log": workertest.GrepArtifact("/no-such-file-" + filename + ".txt"),
"public/result.txt": workertest.ErrorArtifact(),
},
}, {
Title: "Artifact Directory Not Found",
Success: false,
Payload: `{
"image": "` + dockerImageName + `",
"command": ["sh", "-c", "true"],
"env": {},
"maxRunTime": "10 minutes",
"artifacts": [
{
"name": "public/myfolder",
"type": "directory",
"path": "/no-such-folder/no-sub-folder"
}
]
}`,
Artifacts: workertest.ArtifactAssertions{
"public/logs/live.log": workertest.ReferenceArtifact(),
"public/logs/live_backing.log": workertest.GrepArtifact("/no-such-folder/no-sub-folder"),
"public/myfolder": workertest.ErrorArtifact(),
},
// NOTE: If anyone can come up with an artifact path is illegal please add a test case
}}),
}.Test(t)
}
28 changes: 28 additions & 0 deletions test/dockertest/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Package dockertest provides integration tests for a few common configuration
// of docker engine and common plugins.
package dockertest

import (
"github.com/taskcluster/taskcluster-worker/runtime/util"
)

var debug = util.Debug("dockertest")

var engineConfig = `{
"privileged": "never"
}`

const pluginConfig = `{
"disabled": ["reboot"],
"artifacts": {},
"env": {},
"livelog": {},
"success": {},
"watchdog": {},
"maxruntime": {
"perTaskLimit": "require",
"maxRunTime": "3 hours"
}
}`

const dockerImageName = "alpine:3.6"
44 changes: 44 additions & 0 deletions test/dockertest/env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// +build linux,docker

package dockertest

import (
"testing"

"github.com/taskcluster/taskcluster-worker/worker/workertest"
)

func TestEnv(t *testing.T) {
workertest.Case{
Engine: "docker",
Concurrency: 1,
EngineConfig: engineConfig,
PluginConfig: pluginConfig,
Tasks: workertest.Tasks([]workertest.Task{{
Title: "Access Env Vars",
Success: true,
Payload: `{
"image": "` + dockerImageName + `",
"command": ["sh", "-c", "echo $MY_ENV_VAR"],
"env": {
"MY_ENV_VAR": "hello-world"
},
"maxRunTime": "10 minutes"
}`,
AllowAdditional: true, // Ignore additional artifacts
Artifacts: workertest.ArtifactAssertions{
"public/logs/live_backing.log": workertest.GrepArtifact("hello-world"),
},
}, {
Title: "TASK_ID and RUN_ID",
Success: true,
Payload: `{
"image": "` + dockerImageName + `",
"command": ["sh", "-c", "test -n \"$TASK_ID\" && test \"$RUN_ID\" = 0"],
"env": {},
"maxRunTime": "10 minutes"
}`,
AllowAdditional: true, // Ignore additional artifacts
}}),
}.Test(t)
}
13 changes: 13 additions & 0 deletions test/dockertest/imports_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dockertest

import (
_ "github.com/taskcluster/taskcluster-worker/engines/docker"
_ "github.com/taskcluster/taskcluster-worker/plugins/artifacts"
_ "github.com/taskcluster/taskcluster-worker/plugins/env"
_ "github.com/taskcluster/taskcluster-worker/plugins/livelog"
_ "github.com/taskcluster/taskcluster-worker/plugins/maxruntime"
_ "github.com/taskcluster/taskcluster-worker/plugins/reboot"
_ "github.com/taskcluster/taskcluster-worker/plugins/stoponerror"
_ "github.com/taskcluster/taskcluster-worker/plugins/success"
_ "github.com/taskcluster/taskcluster-worker/plugins/watchdog"
)

0 comments on commit 4d93412

Please sign in to comment.