initial commit [ci skip]

This commit is contained in:
Brad Rydzewski
2019-10-10 19:01:58 -07:00
parent 56c135e4ae
commit 43bbf6e78c
95 changed files with 6579 additions and 1 deletions

53
engine/compiler/clone.go Normal file
View File

@@ -0,0 +1,53 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package compiler
import (
"strconv"
"github.com/drone-runners/drone-runner-docker/engine"
"github.com/drone-runners/drone-runner-docker/engine/resource"
"github.com/drone/runner-go/manifest"
)
// default name of the clone step.
const cloneStepName = "clone"
// helper function returns the clone image based on the
// target operating system.
func cloneImage(platform manifest.Platform) string {
switch platform.OS {
case "windows":
return "drone/git:latest"
default:
return "drone/git:1"
}
}
// helper function configures the clone depth parameter,
// specific to the clone plugin.
func cloneParams(src manifest.Clone) map[string]string {
dst := map[string]string{}
if depth := src.Depth; depth > 0 {
dst["PLUGIN_DEPTH"] = strconv.Itoa(depth)
}
if skipVerify := src.SkipVerify; skipVerify {
dst["GIT_SSL_NO_VERIFY"] = "true"
dst["PLUGIN_SKIP_VERIFY"] = "true"
}
return dst
}
// helper function creates a default container configuration
// for the clone stage. The clone stage is automatically
// added to each pipeline.
func createClone(src *resource.Pipeline) *engine.Step {
return &engine.Step{
Name: cloneStepName,
Image: cloneImage(src.Platform),
RunPolicy: engine.RunAlways,
Envs: cloneParams(src.Clone),
}
}

View File

@@ -0,0 +1,126 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package compiler
import (
"testing"
"github.com/dchest/uniuri"
"github.com/drone-runners/drone-runner-docker/engine"
"github.com/drone-runners/drone-runner-docker/engine/resource"
"github.com/drone/drone-go/drone"
"github.com/drone/runner-go/manifest"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestClone(t *testing.T) {
random = notRandom
defer func() {
random = uniuri.New
}()
c := &Compiler{
Repo: &drone.Repo{},
Build: &drone.Build{},
Stage: &drone.Stage{},
System: &drone.System{},
Netrc: &drone.Netrc{},
Manifest: &manifest.Manifest{},
Pipeline: &resource.Pipeline{},
}
want := []*engine.Step{
{
ID: "random",
Image: "drone/git:1",
Name: "clone",
RunPolicy: engine.RunAlways,
WorkingDir: "/drone/src",
},
}
got := c.Compile(nil)
ignore := cmpopts.IgnoreFields(engine.Step{}, "Envs")
if diff := cmp.Diff(got.Steps, want, ignore); len(diff) != 0 {
t.Errorf(diff)
}
}
func TestCloneDisable(t *testing.T) {
c := &Compiler{
Repo: &drone.Repo{},
Build: &drone.Build{},
Stage: &drone.Stage{},
System: &drone.System{},
Netrc: &drone.Netrc{},
Manifest: &manifest.Manifest{},
Pipeline: &resource.Pipeline{Clone: manifest.Clone{Disable: true}},
}
got := c.Compile(nil)
if len(got.Steps) != 0 {
t.Errorf("Expect no clone step added when disabled")
}
}
func TestCloneCreate(t *testing.T) {
want := &engine.Step{
Name: "clone",
Image: "drone/git:1",
RunPolicy: engine.RunAlways,
Envs: map[string]string{"PLUGIN_DEPTH": "50"},
}
src := &resource.Pipeline{Clone: manifest.Clone{Depth: 50}}
got := createClone(src)
if diff := cmp.Diff(got, want); len(diff) != 0 {
t.Errorf(diff)
}
}
func TestCloneImage(t *testing.T) {
tests := []struct {
in manifest.Platform
out string
}{
{
in: manifest.Platform{},
out: "drone/git:1",
},
{
in: manifest.Platform{OS: "linux"},
out: "drone/git:1",
},
{
in: manifest.Platform{OS: "windows"},
out: "drone/git:latest",
},
}
for _, test := range tests {
got, want := cloneImage(test.in), test.out
if got != want {
t.Errorf("Want clone image %q, got %q", want, got)
}
}
}
func TestCloneParams(t *testing.T) {
params := cloneParams(manifest.Clone{})
if len(params) != 0 {
t.Errorf("Expect empty clone parameters")
}
params = cloneParams(manifest.Clone{Depth: 0})
if len(params) != 0 {
t.Errorf("Expect zero depth ignored")
}
params = cloneParams(manifest.Clone{Depth: 50, SkipVerify: true})
if params["PLUGIN_DEPTH"] != "50" {
t.Errorf("Expect clone depth 50")
}
if params["GIT_SSL_NO_VERIFY"] != "true" {
t.Errorf("Expect GIT_SSL_NO_VERIFY is true")
}
if params["PLUGIN_SKIP_VERIFY"] != "true" {
t.Errorf("Expect PLUGIN_SKIP_VERIFY is true")
}
}

238
engine/compiler/compiler.go Normal file
View File

@@ -0,0 +1,238 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package compiler
import (
"context"
"fmt"
"github.com/drone-runners/drone-runner-docker/engine"
"github.com/drone-runners/drone-runner-docker/engine/resource"
"github.com/drone/drone-go/drone"
"github.com/drone/runner-go/clone"
"github.com/drone/runner-go/environ"
"github.com/drone/runner-go/manifest"
"github.com/drone/runner-go/secret"
"github.com/dchest/uniuri"
)
// random generator function
var random = uniuri.New
// Compiler compiles the Yaml configuration file to an
// intermediate representation optimized for simple execution.
type Compiler struct {
// Manifest provides the parsed manifest.
Manifest *manifest.Manifest
// Pipeline provides the parsed pipeline. This pipeline is
// the compiler source and is converted to the intermediate
// representation by the Compile method.
Pipeline *resource.Pipeline
// Build provides the compiler with stage information that
// is converted to environment variable format and passed to
// each pipeline step. It is also used to clone the commit.
Build *drone.Build
// Stage provides the compiler with stage information that
// is converted to environment variable format and passed to
// each pipeline step.
Stage *drone.Stage
// Repo provides the compiler with repo information. This
// repo information is converted to environment variable
// format and passed to each pipeline step. It is also used
// to clone the repository.
Repo *drone.Repo
// System provides the compiler with system information that
// is converted to environment variable format and passed to
// each pipeline step.
System *drone.System
// Environ provides a set of environment variables that
// should be added to each pipeline step by default.
Environ map[string]string
// Netrc provides netrc parameters that can be used by the
// default clone step to authenticate to the remote
// repository.
Netrc *drone.Netrc
// Secret returns a named secret value that can be injected
// into the pipeline step.
Secret secret.Provider
}
// Compile compiles the configuration file.
func (c *Compiler) Compile(ctx context.Context) *engine.Spec {
os := c.Pipeline.Platform.OS
// create the workspace paths
base, path, full := createWorkspace(c.Pipeline)
// create the workspace mount
mount := &engine.VolumeMount{
Name: "_workspace",
Path: base,
}
// create the workspace volume
volume := &engine.VolumeEmptyDir{
ID: random(),
Name: mount.Name,
}
spec := &engine.Spec{
Network: engine.Network{
ID: random(),
},
Platform: engine.Platform{
OS: c.Pipeline.Platform.OS,
Arch: c.Pipeline.Platform.Arch,
Variant: c.Pipeline.Platform.Variant,
Version: c.Pipeline.Platform.Version,
},
Volumes: []*engine.Volume{
{EmptyDir: volume},
},
}
// create the default environment variables.
envs := environ.Combine(
c.Environ,
c.Build.Params,
environ.Proxy(),
environ.System(c.System),
environ.Repo(c.Repo),
environ.Build(c.Build),
environ.Stage(c.Stage),
environ.Link(c.Repo, c.Build, c.System),
clone.Environ(clone.Config{
SkipVerify: c.Pipeline.Clone.SkipVerify,
Trace: c.Pipeline.Clone.Trace,
User: clone.User{
Name: c.Build.AuthorName,
Email: c.Build.AuthorEmail,
},
}),
)
// create docker reference variables
envs["DRONE_DOCKER_VOLUME_ID"] = volume.ID
envs["DRONE_DOCKER_NETWORK_ID"] = spec.Network.ID
// create the workspace variables
envs["DRONE_WORKSPACE"] = full
envs["DRONE_WORKSPACE_BASE"] = base
envs["DRONE_WORKSPACE_PATH"] = path
// create the netrc environment variables
if c.Netrc != nil && c.Netrc.Machine != "" {
envs["DRONE_NETRC_MACHINE"] = c.Netrc.Machine
envs["DRONE_NETRC_USERNAME"] = c.Netrc.Login
envs["DRONE_NETRC_PASSWORD"] = c.Netrc.Password
envs["DRONE_NETRC_FILE"] = fmt.Sprintf(
"machine %s login %s password %s",
c.Netrc.Machine,
c.Netrc.Login,
c.Netrc.Password,
)
}
match := manifest.Match{
Action: c.Build.Action,
Cron: c.Build.Cron,
Ref: c.Build.Ref,
Repo: c.Repo.Slug,
Instance: c.System.Host,
Target: c.Build.Deploy,
Event: c.Build.Event,
Branch: c.Build.Target,
}
// create the clone step
if c.Pipeline.Clone.Disable == false {
step := createClone(c.Pipeline)
step.ID = random()
step.Envs = environ.Combine(envs, step.Envs)
step.WorkingDir = full
step.Volumes = append(step.Volumes, mount)
spec.Steps = append(spec.Steps, step)
}
// create steps
for _, src := range c.Pipeline.Services {
dst := createStep(c.Pipeline, src)
dst.Detach = true
dst.Envs = environ.Combine(envs, dst.Envs)
dst.Volumes = append(dst.Volumes, mount)
setupScript(src, dst, os)
setupWorkdir(src, dst, full)
spec.Steps = append(spec.Steps, dst)
// if the pipeline step has unmet conditions the step is
// automatically skipped.
if !src.When.Match(match) {
dst.RunPolicy = engine.RunNever
}
}
// create steps
for _, src := range c.Pipeline.Steps {
dst := createStep(c.Pipeline, src)
dst.Envs = environ.Combine(envs, dst.Envs)
dst.Volumes = append(dst.Volumes, mount)
setupScript(src, dst, full)
setupWorkdir(src, dst, full)
spec.Steps = append(spec.Steps, dst)
// if the pipeline step has unmet conditions the step is
// automatically skipped.
if !src.When.Match(match) {
dst.RunPolicy = engine.RunNever
}
}
if isGraph(spec) == false {
configureSerial(spec)
} else if c.Pipeline.Clone.Disable == false {
configureCloneDeps(spec)
} else if c.Pipeline.Clone.Disable == true {
removeCloneDeps(spec)
}
for _, step := range spec.Steps {
for _, s := range step.Secrets {
secret, ok := c.findSecret(ctx, s.Name)
if ok {
s.Data = []byte(secret)
}
}
}
return spec
}
// helper function attempts to find and return the named secret.
// from the secret provider.
func (c *Compiler) findSecret(ctx context.Context, name string) (s string, ok bool) {
if name == "" {
return
}
found, _ := c.Secret.Find(ctx, &secret.Request{
Name: name,
Build: c.Build,
Repo: c.Repo,
Conf: c.Manifest,
})
if found == nil {
return
}
return found.Data, true
}

View File

@@ -0,0 +1,186 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
// +build !windows
package compiler
import (
"context"
"encoding/json"
"io/ioutil"
"os"
"testing"
"github.com/dchest/uniuri"
"github.com/drone-runners/drone-runner-docker/engine"
"github.com/drone-runners/drone-runner-docker/engine/resource"
"github.com/drone/drone-go/drone"
"github.com/drone/runner-go/manifest"
"github.com/drone/runner-go/secret"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
var nocontext = context.Background()
// dummy function that returns a non-random string for testing.
// it is used in place of the random function.
func notRandom() string {
return "random"
}
// This test verifies the pipeline dependency graph. When no
// dependency graph is defined, a default dependency graph is
// automatically defined to run steps serially.
func TestCompile_Serial(t *testing.T) {
testCompile(t, "testdata/serial.yml", "testdata/serial.json")
}
// This test verifies the pipeline dependency graph. It also
// verifies that pipeline steps with no dependencies depend on
// the initial clone step.
func TestCompile_Graph(t *testing.T) {
testCompile(t, "testdata/graph.yml", "testdata/graph.json")
}
// This test verifies no clone step exists in the pipeline if
// cloning is disabled.
func TestCompile_CloneDisabled_Serial(t *testing.T) {
testCompile(t, "testdata/noclone_serial.yml", "testdata/noclone_serial.json")
}
// This test verifies no clone step exists in the pipeline if
// cloning is disabled. It also verifies no pipeline steps
// depend on a clone step.
func TestCompile_CloneDisabled_Graph(t *testing.T) {
testCompile(t, "testdata/noclone_graph.yml", "testdata/noclone_graph.json")
}
// This test verifies that steps are disabled if conditions
// defined in the when block are not satisfied.
func TestCompile_Match(t *testing.T) {
ir := testCompile(t, "testdata/match.yml", "testdata/match.json")
if ir.Steps[0].RunPolicy != engine.RunOnSuccess {
t.Errorf("Expect run on success")
}
if ir.Steps[1].RunPolicy != engine.RunNever {
t.Errorf("Expect run never")
}
}
// This test verifies that steps configured to run on both
// success or failure are configured to always run.
func TestCompile_RunAlways(t *testing.T) {
ir := testCompile(t, "testdata/run_always.yml", "testdata/run_always.json")
if ir.Steps[0].RunPolicy != engine.RunAlways {
t.Errorf("Expect run always")
}
}
// This test verifies that steps configured to run on failure
// are configured to run on failure.
func TestCompile_RunFaiure(t *testing.T) {
ir := testCompile(t, "testdata/run_failure.yml", "testdata/run_failure.json")
if ir.Steps[0].RunPolicy != engine.RunOnFailure {
t.Errorf("Expect run on failure")
}
}
// This test verifies that secrets defined in the yaml are
// requested and stored in the intermediate representation
// at compile time.
func TestCompile_Secrets(t *testing.T) {
manifest, _ := manifest.ParseFile("testdata/secret.yml")
compiler := Compiler{}
compiler.Build = &drone.Build{}
compiler.Repo = &drone.Repo{}
compiler.Stage = &drone.Stage{}
compiler.System = &drone.System{}
compiler.Netrc = &drone.Netrc{}
compiler.Manifest = manifest
compiler.Pipeline = manifest.Resources[0].(*resource.Pipeline)
compiler.Secret = secret.StaticVars(map[string]string{
"token": "3DA541559918A808C2402BBA5012F6C60B27661C",
"password": "password",
"my_username": "octocat",
})
ir := compiler.Compile(nocontext)
got := ir.Steps[0].Secrets
want := []*engine.Secret{
{
Name: "my_password",
Env: "PASSWORD",
Data: nil, // secret not found, data nil
Mask: true,
},
{
Name: "my_username",
Env: "USERNAME",
Data: []byte("octocat"), // secret found
Mask: true,
},
}
if diff := cmp.Diff(got, want); len(diff) != 0 {
// TODO(bradrydzewski) ordering is not guaranteed. this
// unit tests needs to be adjusted accordingly.
t.Skipf(diff)
}
}
// helper function parses and compiles the source file and then
// compares to a golden json file.
func testCompile(t *testing.T, source, golden string) *engine.Spec {
// replace the default random function with one that
// is deterministic, for testing purposes.
random = notRandom
// restore the default random function and the previously
// specified temporary directory
defer func() {
random = uniuri.New
}()
manifest, err := manifest.ParseFile(source)
if err != nil {
t.Error(err)
return nil
}
compiler := Compiler{}
compiler.Build = &drone.Build{Target: "master"}
compiler.Repo = &drone.Repo{}
compiler.Stage = &drone.Stage{}
compiler.System = &drone.System{}
compiler.Netrc = &drone.Netrc{Machine: "github.com", Login: "octocat", Password: "correct-horse-battery-staple"}
compiler.Manifest = manifest
compiler.Pipeline = manifest.Resources[0].(*resource.Pipeline)
got := compiler.Compile(nocontext)
raw, err := ioutil.ReadFile(golden)
if err != nil {
t.Error(err)
}
want := new(engine.Spec)
err = json.Unmarshal(raw, want)
if err != nil {
t.Error(err)
}
ignore := cmpopts.IgnoreFields(engine.Step{}, "Envs", "Secrets")
unexported := cmpopts.IgnoreUnexported(engine.Spec{})
if diff := cmp.Diff(got, want, ignore, unexported); len(diff) != 0 {
t.Errorf(diff)
}
return got
}
func dump(v interface{}) {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
enc.Encode(v)
}

View File

@@ -0,0 +1,56 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package encoder
import (
"encoding/base64"
"strconv"
"strings"
"github.com/buildkite/yaml"
json "github.com/ghodss/yaml"
)
// Encode encodes an interface value as a string. This function
// assumes all types were unmarshaled by the yaml.v2 library.
// The yaml.v2 package only supports a subset of primative types.
func Encode(v interface{}) string {
switch v := v.(type) {
case string:
return v
case bool:
return strconv.FormatBool(v)
case int:
return strconv.Itoa(v)
case float64:
return strconv.FormatFloat(v, 'g', -1, 64)
case []byte:
return base64.StdEncoding.EncodeToString(v)
case []interface{}:
return encodeSlice(v)
default:
return encodeMap(v)
}
}
// helper function encodes a parameter in map format.
func encodeMap(v interface{}) string {
yml, _ := yaml.Marshal(v)
out, _ := json.YAMLToJSON(yml)
return string(out)
}
// helper function encodes a parameter in slice format.
func encodeSlice(v interface{}) string {
out, _ := yaml.Marshal(v)
in := []string{}
err := yaml.Unmarshal(out, &in)
if err == nil {
return strings.Join(in, ",")
}
out, _ = json.YAMLToJSON(out)
return string(out)
}

View File

@@ -0,0 +1,63 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package encoder
import "testing"
func TestEncode(t *testing.T) {
testdatum := []struct {
data interface{}
text string
}{
{
data: "foo",
text: "foo",
},
{
data: true,
text: "true",
},
{
data: 42,
text: "42",
},
{
data: float64(42.424242),
text: "42.424242",
},
{
data: []interface{}{"foo", "bar", "baz"},
text: "foo,bar,baz",
},
{
data: []interface{}{1, 1, 2, 3, 5, 8},
text: "1,1,2,3,5,8",
},
{
data: []byte("foo"),
text: "Zm9v",
},
{
data: []interface{}{
struct {
Name string `json:"name"`
}{
Name: "john",
},
},
text: `[{"name":"john"}]`,
},
{
data: map[interface{}]interface{}{"foo": "bar"},
text: `{"foo":"bar"}`,
},
}
for _, testdata := range testdatum {
if got, want := Encode(testdata.data), testdata.text; got != want {
t.Errorf("Want interface{} encoded to %q, got %q", want, got)
}
}
}

View File

@@ -0,0 +1,71 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package image
import "github.com/docker/distribution/reference"
// Trim returns the short image name without tag.
func Trim(name string) string {
ref, err := reference.ParseAnyReference(name)
if err != nil {
return name
}
named, err := reference.ParseNamed(ref.String())
if err != nil {
return name
}
named = reference.TrimNamed(named)
return reference.FamiliarName(named)
}
// Expand returns the fully qualified image name.
func Expand(name string) string {
ref, err := reference.ParseAnyReference(name)
if err != nil {
return name
}
named, err := reference.ParseNamed(ref.String())
if err != nil {
return name
}
named = reference.TagNameOnly(named)
return named.String()
}
// Match returns true if the image name matches
// an image in the list. Note the image tag is not used
// in the matching logic.
func Match(from string, to ...string) bool {
from = Trim(from)
for _, match := range to {
if from == Trim(match) {
return true
}
}
return false
}
// MatchTag returns true if the image name matches
// an image in the list, including the tag.
func MatchTag(a, b string) bool {
return Expand(a) == Expand(b)
}
// MatchHostname returns true if the image hostname
// matches the specified hostname.
func MatchHostname(image, hostname string) bool {
ref, err := reference.ParseAnyReference(image)
if err != nil {
return false
}
named, err := reference.ParseNamed(ref.String())
if err != nil {
return false
}
if hostname == "index.docker.io" {
hostname = "docker.io"
}
return reference.Domain(named) == hostname
}

View File

@@ -0,0 +1,299 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package image
import "testing"
func Test_trimImage(t *testing.T) {
testdata := []struct {
from string
want string
}{
{
from: "golang",
want: "golang",
},
{
from: "golang:latest",
want: "golang",
},
{
from: "golang:1.0.0",
want: "golang",
},
{
from: "library/golang",
want: "golang",
},
{
from: "library/golang:latest",
want: "golang",
},
{
from: "library/golang:1.0.0",
want: "golang",
},
{
from: "index.docker.io/library/golang:1.0.0",
want: "golang",
},
{
from: "docker.io/library/golang:1.0.0",
want: "golang",
},
{
from: "gcr.io/library/golang:1.0.0",
want: "gcr.io/library/golang",
},
// error cases, return input unmodified
{
from: "foo/bar?baz:boo",
want: "foo/bar?baz:boo",
},
}
for _, test := range testdata {
got, want := Trim(test.from), test.want
if got != want {
t.Errorf("Want image %q trimmed to %q, got %q", test.from, want, got)
}
}
}
func Test_expandImage(t *testing.T) {
testdata := []struct {
from string
want string
}{
{
from: "golang",
want: "docker.io/library/golang:latest",
},
{
from: "golang:latest",
want: "docker.io/library/golang:latest",
},
{
from: "golang:1.0.0",
want: "docker.io/library/golang:1.0.0",
},
{
from: "library/golang",
want: "docker.io/library/golang:latest",
},
{
from: "library/golang:latest",
want: "docker.io/library/golang:latest",
},
{
from: "library/golang:1.0.0",
want: "docker.io/library/golang:1.0.0",
},
{
from: "index.docker.io/library/golang:1.0.0",
want: "docker.io/library/golang:1.0.0",
},
{
from: "gcr.io/golang",
want: "gcr.io/golang:latest",
},
{
from: "gcr.io/golang:1.0.0",
want: "gcr.io/golang:1.0.0",
},
// error cases, return input unmodified
{
from: "foo/bar?baz:boo",
want: "foo/bar?baz:boo",
},
}
for _, test := range testdata {
got, want := Expand(test.from), test.want
if got != want {
t.Errorf("Want image %q expanded to %q, got %q", test.from, want, got)
}
}
}
func Test_matchImage(t *testing.T) {
testdata := []struct {
from, to string
want bool
}{
{
from: "golang",
to: "golang",
want: true,
},
{
from: "golang:latest",
to: "golang",
want: true,
},
{
from: "library/golang:latest",
to: "golang",
want: true,
},
{
from: "index.docker.io/library/golang:1.0.0",
to: "golang",
want: true,
},
{
from: "golang",
to: "golang:latest",
want: true,
},
{
from: "library/golang:latest",
to: "library/golang",
want: true,
},
{
from: "gcr.io/golang",
to: "gcr.io/golang",
want: true,
},
{
from: "gcr.io/golang:1.0.0",
to: "gcr.io/golang",
want: true,
},
{
from: "gcr.io/golang:latest",
to: "gcr.io/golang",
want: true,
},
{
from: "gcr.io/golang",
to: "gcr.io/golang:latest",
want: true,
},
{
from: "golang",
to: "library/golang",
want: true,
},
{
from: "golang",
to: "gcr.io/project/golang",
want: false,
},
{
from: "golang",
to: "gcr.io/library/golang",
want: false,
},
{
from: "golang",
to: "gcr.io/golang",
want: false,
},
}
for _, test := range testdata {
got, want := Match(test.from, test.to), test.want
if got != want {
t.Errorf("Want image %q matching %q is %v", test.from, test.to, want)
}
}
}
func Test_matchHostname(t *testing.T) {
testdata := []struct {
image, hostname string
want bool
}{
{
image: "golang",
hostname: "docker.io",
want: true,
},
{
image: "golang:latest",
hostname: "docker.io",
want: true,
},
{
image: "golang:latest",
hostname: "index.docker.io",
want: true,
},
{
image: "library/golang:latest",
hostname: "docker.io",
want: true,
},
{
image: "docker.io/library/golang:1.0.0",
hostname: "docker.io",
want: true,
},
{
image: "gcr.io/golang",
hostname: "docker.io",
want: false,
},
{
image: "gcr.io/golang:1.0.0",
hostname: "gcr.io",
want: true,
},
{
image: "1.2.3.4:8000/golang:1.0.0",
hostname: "1.2.3.4:8000",
want: true,
},
{
image: "*&^%",
hostname: "1.2.3.4:8000",
want: false,
},
}
for _, test := range testdata {
got, want := MatchHostname(test.image, test.hostname), test.want
if got != want {
t.Errorf("Want image %q matching hostname %q is %v", test.image, test.hostname, want)
}
}
}
func Test_matchTag(t *testing.T) {
testdata := []struct {
a, b string
want bool
}{
{
a: "golang:1.0",
b: "golang:1.0",
want: true,
},
{
a: "golang",
b: "golang:latest",
want: true,
},
{
a: "docker.io/library/golang",
b: "golang:latest",
want: true,
},
{
a: "golang",
b: "golang:1.0",
want: false,
},
{
a: "golang:1.0",
b: "golang:2.0",
want: false,
},
}
for _, test := range testdata {
got, want := MatchTag(test.a, test.b), test.want
if got != want {
t.Errorf("Want image %q matching image tag %q is %v", test.a, test.b, want)
}
}
}

85
engine/compiler/os.go Normal file
View File

@@ -0,0 +1,85 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package compiler
import (
"fmt"
"strings"
"github.com/drone/runner-go/shell/bash"
"github.com/drone/runner-go/shell/powershell"
)
// helper function returns the base temporary directory based
// on the target platform.
func tempdir(os string) string {
dir := fmt.Sprintf("drone-%s", random())
switch os {
case "windows":
return join(os, "C:\\Windows\\Temp", dir)
default:
return join(os, "/tmp", dir)
}
}
// helper function joins the file paths.
func join(os string, paths ...string) string {
switch os {
case "windows":
return strings.Join(paths, "\\")
default:
return strings.Join(paths, "/")
}
}
// helper function returns the shell extension based on the
// target platform.
func getExt(os, file string) (s string) {
switch os {
case "windows":
return file + ".ps1"
default:
return file
}
}
//
// TODO(bradrydzewski) can we remove the below functions?
//
// helper function returns the shell command and arguments
// based on the target platform to invoke the script
func getCommand(os, script string) (string, []string) {
cmd, args := bash.Command()
switch os {
case "windows":
cmd, args = powershell.Command()
}
return cmd, append(args, script)
}
// helper function returns the netrc file name based on the
// target platform.
func getNetrc(os string) string {
switch os {
case "windows":
return "_netrc"
default:
return ".netrc"
}
}
// helper function generates and returns a shell script to
// execute the provided shell commands. The shell scripting
// language (bash vs pwoershell) is determined by the operating
// system.
func genScript(os string, commands []string) string {
switch os {
case "windows":
return powershell.Script(commands)
default:
return bash.Script(commands)
}
}

128
engine/compiler/os_test.go Normal file
View File

@@ -0,0 +1,128 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package compiler
import (
"reflect"
"testing"
"github.com/drone/runner-go/shell/bash"
"github.com/drone/runner-go/shell/powershell"
"github.com/dchest/uniuri"
)
func Test_tempdir(t *testing.T) {
// replace the default random function with one that
// is deterministic, for testing purposes.
random = notRandom
// restore the default random function and the previously
// specified temporary directory
defer func() {
random = uniuri.New
}()
tests := []struct {
os string
path string
}{
{os: "windows", path: "C:\\Windows\\Temp\\drone-random"},
{os: "linux", path: "/tmp/drone-random"},
{os: "openbsd", path: "/tmp/drone-random"},
{os: "netbsd", path: "/tmp/drone-random"},
{os: "freebsd", path: "/tmp/drone-random"},
}
for _, test := range tests {
if got, want := tempdir(test.os), test.path; got != want {
t.Errorf("Want tempdir %s, got %s", want, got)
}
}
}
func Test_join(t *testing.T) {
tests := []struct {
os string
a []string
b string
}{
{os: "windows", a: []string{"C:", "Windows", "Temp"}, b: "C:\\Windows\\Temp"},
{os: "linux", a: []string{"/tmp", "foo", "bar"}, b: "/tmp/foo/bar"},
}
for _, test := range tests {
if got, want := join(test.os, test.a...), test.b; got != want {
t.Errorf("Want %s, got %s", want, got)
}
}
}
func Test_getExt(t *testing.T) {
tests := []struct {
os string
a string
b string
}{
{os: "windows", a: "clone", b: "clone.ps1"},
{os: "linux", a: "clone", b: "clone"},
}
for _, test := range tests {
if got, want := getExt(test.os, test.a), test.b; got != want {
t.Errorf("Want %s, got %s", want, got)
}
}
}
func Test_getCommand(t *testing.T) {
cmd, args := getCommand("linux", "clone.sh")
if got, want := cmd, "/bin/sh"; got != want {
t.Errorf("Want command %s, got %s", want, got)
}
if !reflect.DeepEqual(args, []string{"-e", "clone.sh"}) {
t.Errorf("Unexpected args %v", args)
}
cmd, args = getCommand("windows", "clone.ps1")
if got, want := cmd, "powershell"; got != want {
t.Errorf("Want command %s, got %s", want, got)
}
if !reflect.DeepEqual(args, []string{"-noprofile", "-noninteractive", "-command", "clone.ps1"}) {
t.Errorf("Unexpected args %v", args)
}
}
func Test_getNetrc(t *testing.T) {
tests := []struct {
os string
name string
}{
{os: "windows", name: "_netrc"},
{os: "linux", name: ".netrc"},
{os: "openbsd", name: ".netrc"},
{os: "netbsd", name: ".netrc"},
{os: "freebsd", name: ".netrc"},
}
for _, test := range tests {
if got, want := getNetrc(test.os), test.name; got != want {
t.Errorf("Want %s on %s, got %s", want, test.os, got)
}
}
}
func Test_getScript(t *testing.T) {
commands := []string{"go build"}
a := genScript("windows", commands)
b := powershell.Script(commands)
if !reflect.DeepEqual(a, b) {
t.Errorf("Generated windows linux script")
}
a = genScript("linux", commands)
b = bash.Script(commands)
if !reflect.DeepEqual(a, b) {
t.Errorf("Generated invalid linux script")
}
}

35
engine/compiler/script.go Normal file
View File

@@ -0,0 +1,35 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package compiler
import (
"github.com/drone-runners/drone-runner-docker/engine"
"github.com/drone-runners/drone-runner-docker/engine/compiler/shell"
"github.com/drone-runners/drone-runner-docker/engine/compiler/shell/powershell"
"github.com/drone-runners/drone-runner-docker/engine/resource"
)
func setupScript(src *resource.Step, dst *engine.Step, os string) {
if len(src.Commands) > 0 {
switch os {
case "windows":
setupScriptWindows(src, dst)
default:
setupScriptPosix(src, dst)
}
}
}
func setupScriptWindows(src *resource.Step, dst *engine.Step) {
dst.Entrypoint = []string{"powershell", "-noprofile", "-noninteractive", "-command"}
dst.Command = []string{"echo $DRONE_SCRIPT | iex"}
dst.Envs["DRONE_SCRIPT"] = powershell.Script(src.Commands)
}
func setupScriptPosix(src *resource.Step, dst *engine.Step) {
dst.Entrypoint = []string{"/bin/sh", "-c"}
dst.Command = []string{"echo $DRONE_SCRIPT | /bin/sh -e"}
dst.Envs["DRONE_SCRIPT"] = shell.Script(src.Commands)
}

View File

@@ -0,0 +1,5 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package compiler

View File

@@ -0,0 +1,59 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
// Package powershell provides functions for converting shell
// commands to powershell scripts.
package powershell
import (
"bytes"
"fmt"
"strings"
)
// Script converts a slice of individual shell commands to
// a powershell script.
func Script(commands []string) string {
buf := new(bytes.Buffer)
fmt.Fprintln(buf)
fmt.Fprintf(buf, optionScript)
fmt.Fprintln(buf)
for _, command := range commands {
escaped := fmt.Sprintf("%q", "+ "+command)
escaped = strings.Replace(escaped, "$", "`$", -1)
buf.WriteString(fmt.Sprintf(
traceScript,
escaped,
command,
))
}
return buf.String()
}
// optionScript is a helper script this is added to the build
// to set shell options, in this case, to exit on error.
const optionScript = `
if ($Env:DRONE_NETRC_MACHINE) {
@"
machine $Env:DRONE_NETRC_MACHINE
login $Env:DRONE_NETRC_USERNAME
password $Env:DRONE_NETRC_PASSWORD
"@ > (Join-Path $Env:USERPROFILE '_netrc');
}
[Environment]::SetEnvironmentVariable("DRONE_NETRC_USERNAME", $null);
[Environment]::SetEnvironmentVariable("DRONE_NETRC_PASSWORD", $null);
[Environment]::SetEnvironmentVariable("DRONE_NETRC_USERNAME", $null);
[Environment]::SetEnvironmentVariable("DRONE_NETRC_PASSWORD", $null);
[Environment]::SetEnvironmentVariable("DRONE_SCRIPT", $null);
$erroractionpreference = "stop"
`
// traceScript is a helper script that is added to
// the build script to trace a command.
const traceScript = `
echo %s
%s
if ($LastExitCode -ne 0) { exit $LastExitCode }
`

View File

@@ -0,0 +1,5 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package powershell

View File

@@ -0,0 +1,56 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
// Package shell provides functions for converting shell commands
// to posix shell scripts.
package shell
import (
"bytes"
"fmt"
"strings"
)
// Script converts a slice of individual shell commands to
// a posix-compliant shell script.
func Script(commands []string) string {
buf := new(bytes.Buffer)
fmt.Fprintln(buf)
fmt.Fprintf(buf, optionScript)
fmt.Fprintln(buf)
for _, command := range commands {
escaped := fmt.Sprintf("%q", command)
escaped = strings.Replace(escaped, "$", `\$`, -1)
buf.WriteString(fmt.Sprintf(
traceScript,
escaped,
command,
))
}
return buf.String()
}
// optionScript is a helper script this is added to the build
// to set shell options, in this case, to exit on error.
const optionScript = `
if [[ ! -z "${DRONE_NETRC_FILE}" ]]; then
echo $DRONE_NETRC_FILE > $HOME/.netrc
EOF
fi
unset DRONE_SCRIPT
unset DRONE_NETRC_MACHINE
unset DRONE_NETRC_USERNAME
unset DRONE_NETRC_PASSWORD
unset DRONE_NETRC_FILE
set -e
`
// traceScript is a helper script that is added to
// the build script to trace a command.
const traceScript = `
echo + %s
%s
`

View File

@@ -0,0 +1,5 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package shell

108
engine/compiler/step.go Normal file
View File

@@ -0,0 +1,108 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package compiler
import (
"strings"
"github.com/drone-runners/drone-runner-docker/engine"
"github.com/drone-runners/drone-runner-docker/engine/compiler/encoder"
"github.com/drone-runners/drone-runner-docker/engine/compiler/image"
"github.com/drone-runners/drone-runner-docker/engine/resource"
)
func createStep(spec *resource.Pipeline, src *resource.Step) *engine.Step {
dst := &engine.Step{
ID: random(),
Name: src.Name,
Image: image.Expand(src.Image),
Command: src.Command,
Entrypoint: src.Entrypoint,
Detach: src.Detach,
DependsOn: src.DependsOn,
DNS: src.DNS,
DNSSearch: src.DNSSearch,
Envs: convertStaticEnv(src.Environment),
ExtraHosts: src.ExtraHosts,
IgnoreErr: strings.EqualFold(src.Failure, "ignore"),
IgnoreStderr: false,
IgnoreStdout: false,
Network: src.Network,
Privileged: src.Privileged,
Pull: convertPullPolicy(src.Pull),
User: src.User,
Secrets: convertSecretEnv(src.Environment),
WorkingDir: src.WorkingDir,
//
//
//
Networks: nil, // set in compiler.go
Files: nil, // set below
Volumes: nil, // set below
// Devices: nil, // TODO
// Resources: toResources(src), // TODO
}
// appends the volumes to the container def.
for _, vol := range src.Volumes {
dst.Volumes = append(dst.Volumes, &engine.VolumeMount{
Name: vol.Name,
Path: vol.MountPath,
})
}
// appends the settings variables to the
// container definition.
for key, value := range src.Settings {
// fix https://github.com/drone/drone-yaml/issues/13
if value == nil {
continue
}
// all settings are passed to the plugin env
// variables, prefixed with PLUGIN_
key = "PLUGIN_" + strings.ToUpper(key)
// if the setting parameter is sources from the
// secret we create a secret enviornment variable.
if value.Secret != "" {
dst.Secrets = append(dst.Secrets, &engine.Secret{
Name: value.Secret,
Mask: true,
Env: key,
})
} else {
// else if the setting parameter is opaque
// we inject as a string-encoded environment
// variable.
dst.Envs[key] = encoder.Encode(value.Value)
}
}
// // if the step specifies shell commands we generate a
// // script. The script is copied to the container at
// // runtime (or mounted as a config map) and then executed
// // as the entrypoint.
// if len(src.Commands) > 0 {
// switch spec.Platform.OS {
// case "windows":
// setupScriptWin(spec, dst, src)
// default:
// setupScript(spec, dst, src)
// }
// }
// set the pipeline step run policy. steps run on
// success by default, but may be optionally configured
// to run on failure.
if isRunAlways(src) {
dst.RunPolicy = engine.RunAlways
} else if isRunOnFailure(src) {
dst.RunPolicy = engine.RunOnFailure
}
return dst
}

104
engine/compiler/testdata/graph.json vendored Normal file
View File

@@ -0,0 +1,104 @@
{
"platform": {},
"token": "3DA541559918A808C2402BBA5012F6C60B27661C",
"server": {
"name": "drone-temp-random",
"image": "docker-18-04",
"region": "nyc1",
"size": "s-1vcpu-1gb",
"user": "root"
},
"root": "/tmp/drone-random",
"files": [
{
"path": "/tmp/drone-random/home",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/home/drone",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/drone",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/drone/src",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/opt",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/home/drone/.netrc",
"mode": 384,
"data": "bWFjaGluZSBnaXRodWIuY29tIGxvZ2luIG9jdG9jYXQgcGFzc3dvcmQgY29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ=="
}
],
"steps": [
{
"args": [
"-e",
"/tmp/drone-random/opt/clone"
],
"command": "/bin/sh",
"files": [
{
"path": "/tmp/drone-random/opt/clone",
"mode": 448,
"data": "CnNldCAtZQoKZWNobyArICJnaXQgaW5pdCIKZ2l0IGluaXQKCmVjaG8gKyAiZ2l0IHJlbW90ZSBhZGQgb3JpZ2luICIKZ2l0IHJlbW90ZSBhZGQgb3JpZ2luIAoKZWNobyArICJnaXQgZmV0Y2ggIG9yaWdpbiArcmVmcy9oZWFkcy9tYXN0ZXI6IgpnaXQgZmV0Y2ggIG9yaWdpbiArcmVmcy9oZWFkcy9tYXN0ZXI6CgplY2hvICsgImdpdCBjaGVja291dCAgLWIgbWFzdGVyIgpnaXQgY2hlY2tvdXQgIC1iIG1hc3Rlcgo="
}
],
"secrets": [],
"name": "clone",
"run_policy": 2,
"working_dir": "/tmp/drone-random/drone/src"
},
{
"args": [
"-e",
"/tmp/drone-random/opt/build"
],
"command": "/bin/sh",
"depends_on": [
"clone"
],
"files": [
{
"path": "/tmp/drone-random/opt/build",
"mode": 448,
"data": "CnNldCAtZQoKZWNobyArICJnbyBidWlsZCIKZ28gYnVpbGQK"
}
],
"secrets": [],
"name": "build",
"working_dir": "/tmp/drone-random/drone/src"
},
{
"args": [
"-e",
"/tmp/drone-random/opt/test"
],
"command": "/bin/sh",
"depends_on": [
"build"
],
"files": [
{
"path": "/tmp/drone-random/opt/test",
"mode": 448,
"data": "CnNldCAtZQoKZWNobyArICJnbyB0ZXN0IgpnbyB0ZXN0Cg=="
}
],
"secrets": [],
"name": "test",
"working_dir": "/tmp/drone-random/drone/src"
}
]
}

20
engine/compiler/testdata/graph.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
kind: pipeline
type: docker
name: default
server:
image: docker-18-04
region: nyc1
size: s-1vcpu-1gb
token: 3DA541559918A808C2402BBA5012F6C60B27661C
steps:
- name: build
commands:
- go build
- name: test
commands:
- go test
depends_on: [ build ]

82
engine/compiler/testdata/match.json vendored Normal file
View File

@@ -0,0 +1,82 @@
{
"platform": {},
"token": "3DA541559918A808C2402BBA5012F6C60B27661C",
"server": {
"name": "drone-temp-random",
"image": "docker-18-04",
"region": "nyc1",
"size": "s-1vcpu-1gb",
"user": "root"
},
"root": "/tmp/drone-random",
"files": [
{
"path": "/tmp/drone-random/home",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/home/drone",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/drone",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/drone/src",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/opt",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/home/drone/.netrc",
"mode": 384,
"data": "bWFjaGluZSBnaXRodWIuY29tIGxvZ2luIG9jdG9jYXQgcGFzc3dvcmQgY29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ=="
}
],
"steps": [
{
"args": [
"-e",
"/tmp/drone-random/opt/build"
],
"command": "/bin/sh",
"files": [
{
"path": "/tmp/drone-random/opt/build",
"mode": 448,
"data": "CnNldCAtZQoKZWNobyArICJnbyBidWlsZCIKZ28gYnVpbGQK"
}
],
"name": "build",
"working_dir": "/tmp/drone-random/drone/src"
},
{
"args": [
"-e",
"/tmp/drone-random/opt/test"
],
"command": "/bin/sh",
"depends_on": [
"build"
],
"files": [
{
"path": "/tmp/drone-random/opt/test",
"mode": 448,
"data": "CnNldCAtZQoKZWNobyArICJnbyB0ZXN0IgpnbyB0ZXN0Cg=="
}
],
"name": "test",
"run_policy": 3,
"working_dir": "/tmp/drone-random/drone/src"
}
]
}

26
engine/compiler/testdata/match.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
kind: pipeline
type: docker
name: default
clone:
disable: true
server:
image: docker-18-04
region: nyc1
size: s-1vcpu-1gb
token: 3DA541559918A808C2402BBA5012F6C60B27661C
steps:
- name: build
commands:
- go build
when:
branch: [ master ]
- name: test
commands:
- go test
when:
branch: [ develop ]

View File

@@ -0,0 +1,83 @@
{
"platform": {},
"token": "3DA541559918A808C2402BBA5012F6C60B27661C",
"server": {
"name": "drone-temp-random",
"image": "docker-18-04",
"region": "nyc1",
"size": "s-1vcpu-1gb",
"user": "root"
},
"root": "/tmp/drone-random",
"files": [
{
"path": "/tmp/drone-random/home",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/home/drone",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/drone",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/drone/src",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/opt",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/home/drone/.netrc",
"mode": 384,
"data": "bWFjaGluZSBnaXRodWIuY29tIGxvZ2luIG9jdG9jYXQgcGFzc3dvcmQgY29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ=="
}
],
"steps": [
{
"args": [
"-e",
"/tmp/drone-random/opt/build"
],
"command": "/bin/sh",
"files": [
{
"path": "/tmp/drone-random/opt/build",
"mode": 448,
"data": "CnNldCAtZQoKZWNobyArICJnbyBidWlsZCIKZ28gYnVpbGQK"
}
],
"name": "build",
"secrets": [],
"working_dir": "/tmp/drone-random/drone/src"
},
{
"args": [
"-e",
"/tmp/drone-random/opt/test"
],
"command": "/bin/sh",
"depends_on": [
"build"
],
"files": [
{
"path": "/tmp/drone-random/opt/test",
"mode": 448,
"data": "CnNldCAtZQoKZWNobyArICJnbyB0ZXN0IgpnbyB0ZXN0Cg=="
}
],
"name": "test",
"secrets": [],
"working_dir": "/tmp/drone-random/drone/src"
}
]
}

View File

@@ -0,0 +1,23 @@
kind: pipeline
type: docker
name: default
clone:
disable: true
server:
image: docker-18-04
region: nyc1
size: s-1vcpu-1gb
token: 3DA541559918A808C2402BBA5012F6C60B27661C
steps:
- name: build
commands:
- go build
- name: test
commands:
- go test
depends_on: [ build ]

View File

@@ -0,0 +1,62 @@
{
"platform": {},
"token": "3DA541559918A808C2402BBA5012F6C60B27661C",
"server": {
"name": "drone-temp-random",
"image": "docker-18-04",
"region": "nyc1",
"size": "s-1vcpu-1gb",
"user": "root"
},
"root": "/tmp/drone-random",
"files": [
{
"path": "/tmp/drone-random/home",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/home/drone",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/drone",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/drone/src",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/opt",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/home/drone/.netrc",
"mode": 384,
"data": "bWFjaGluZSBnaXRodWIuY29tIGxvZ2luIG9jdG9jYXQgcGFzc3dvcmQgY29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ=="
}
],
"steps": [
{
"args": [
"-e",
"/tmp/drone-random/opt/build"
],
"command": "/bin/sh",
"files": [
{
"path": "/tmp/drone-random/opt/build",
"mode": 448,
"data": "CnNldCAtZQoKZWNobyArICJnbyBidWlsZCIKZ28gYnVpbGQKCmVjaG8gKyAiZ28gdGVzdCIKZ28gdGVzdAo="
}
],
"name": "build",
"working_dir": "/tmp/drone-random/drone/src"
}
]
}

View File

@@ -0,0 +1,19 @@
kind: pipeline
type: docker
name: default
clone:
disable: true
server:
image: docker-18-04
region: nyc1
size: s-1vcpu-1gb
token: 3DA541559918A808C2402BBA5012F6C60B27661C
steps:
- name: build
commands:
- go build
- go test

View File

@@ -0,0 +1,63 @@
{
"platform": {},
"token": "3DA541559918A808C2402BBA5012F6C60B27661C",
"server": {
"name": "drone-temp-random",
"image": "docker-18-04",
"region": "nyc1",
"size": "s-1vcpu-1gb",
"user": "root"
},
"root": "/tmp/drone-random",
"files": [
{
"path": "/tmp/drone-random/home",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/home/drone",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/drone",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/drone/src",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/opt",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/home/drone/.netrc",
"mode": 384,
"data": "bWFjaGluZSBnaXRodWIuY29tIGxvZ2luIG9jdG9jYXQgcGFzc3dvcmQgY29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ=="
}
],
"steps": [
{
"args": [
"-e",
"/tmp/drone-random/opt/build"
],
"command": "/bin/sh",
"files": [
{
"path": "/tmp/drone-random/opt/build",
"mode": 448,
"data": "CnNldCAtZQoKZWNobyArICJnbyBidWlsZCIKZ28gYnVpbGQK"
}
],
"name": "build",
"run_policy": 2,
"working_dir": "/tmp/drone-random/drone/src"
}
]
}

20
engine/compiler/testdata/run_always.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
kind: pipeline
type: docker
name: default
clone:
disable: true
server:
image: docker-18-04
region: nyc1
size: s-1vcpu-1gb
token: 3DA541559918A808C2402BBA5012F6C60B27661C
steps:
- name: build
commands:
- go build
when:
status: [ success, failure ]

View File

@@ -0,0 +1,63 @@
{
"platform": {},
"token": "3DA541559918A808C2402BBA5012F6C60B27661C",
"server": {
"name": "drone-temp-random",
"image": "docker-18-04",
"region": "nyc1",
"size": "s-1vcpu-1gb",
"user": "root"
},
"root": "/tmp/drone-random",
"files": [
{
"path": "/tmp/drone-random/home",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/home/drone",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/drone",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/drone/src",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/opt",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/home/drone/.netrc",
"mode": 384,
"data": "bWFjaGluZSBnaXRodWIuY29tIGxvZ2luIG9jdG9jYXQgcGFzc3dvcmQgY29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ=="
}
],
"steps": [
{
"args": [
"-e",
"/tmp/drone-random/opt/build"
],
"command": "/bin/sh",
"files": [
{
"path": "/tmp/drone-random/opt/build",
"mode": 448,
"data": "CnNldCAtZQoKZWNobyArICJnbyBidWlsZCIKZ28gYnVpbGQK"
}
],
"name": "build",
"run_policy": 1,
"working_dir": "/tmp/drone-random/drone/src"
}
]
}

View File

@@ -0,0 +1,20 @@
kind: pipeline
type: docker
name: default
clone:
disable: true
server:
image: docker-18-04
region: nyc1
size: s-1vcpu-1gb
token: 3DA541559918A808C2402BBA5012F6C60B27661C
steps:
- name: build
commands:
- go build
when:
status: [ failure ]

25
engine/compiler/testdata/secret.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
kind: pipeline
type: docker
name: default
clone:
disable: true
server:
image: docker-18-04
region: nyc1
size: s-1vcpu-1gb
token:
from_secret: token
steps:
- name: build
environment:
PASSWORD:
from_secret: my_password
USERNAME:
from_secret: my_username
commands:
- go build
- go test

104
engine/compiler/testdata/serial.json vendored Normal file
View File

@@ -0,0 +1,104 @@
{
"platform": {},
"token": "3DA541559918A808C2402BBA5012F6C60B27661C",
"server": {
"name": "drone-temp-random",
"image": "docker-18-04",
"region": "nyc1",
"size": "s-1vcpu-1gb",
"user": "root"
},
"root": "/tmp/drone-random",
"files": [
{
"path": "/tmp/drone-random/home",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/home/drone",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/drone",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/drone/src",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/opt",
"mode": 448,
"is_dir": true
},
{
"path": "/tmp/drone-random/home/drone/.netrc",
"mode": 384,
"data": "bWFjaGluZSBnaXRodWIuY29tIGxvZ2luIG9jdG9jYXQgcGFzc3dvcmQgY29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ=="
}
],
"steps": [
{
"args": [
"-e",
"/tmp/drone-random/opt/clone"
],
"command": "/bin/sh",
"files": [
{
"path": "/tmp/drone-random/opt/clone",
"mode": 448,
"data": "CnNldCAtZQoKZWNobyArICJnaXQgaW5pdCIKZ2l0IGluaXQKCmVjaG8gKyAiZ2l0IHJlbW90ZSBhZGQgb3JpZ2luICIKZ2l0IHJlbW90ZSBhZGQgb3JpZ2luIAoKZWNobyArICJnaXQgZmV0Y2ggIG9yaWdpbiArcmVmcy9oZWFkcy9tYXN0ZXI6IgpnaXQgZmV0Y2ggIG9yaWdpbiArcmVmcy9oZWFkcy9tYXN0ZXI6CgplY2hvICsgImdpdCBjaGVja291dCAgLWIgbWFzdGVyIgpnaXQgY2hlY2tvdXQgIC1iIG1hc3Rlcgo="
}
],
"secrets": [],
"name": "clone",
"run_policy": 2,
"working_dir": "/tmp/drone-random/drone/src"
},
{
"args": [
"-e",
"/tmp/drone-random/opt/build"
],
"command": "/bin/sh",
"depends_on": [
"clone"
],
"files": [
{
"path": "/tmp/drone-random/opt/build",
"mode": 448,
"data": "CnNldCAtZQoKZWNobyArICJnbyBidWlsZCIKZ28gYnVpbGQK"
}
],
"secrets": [],
"name": "build",
"working_dir": "/tmp/drone-random/drone/src"
},
{
"args": [
"-e",
"/tmp/drone-random/opt/test"
],
"command": "/bin/sh",
"depends_on": [
"build"
],
"files": [
{
"path": "/tmp/drone-random/opt/test",
"mode": 448,
"data": "CnNldCAtZQoKZWNobyArICJnbyB0ZXN0IgpnbyB0ZXN0Cg=="
}
],
"secrets": [],
"name": "test",
"working_dir": "/tmp/drone-random/drone/src"
}
]
}

19
engine/compiler/testdata/serial.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
kind: pipeline
type: docker
name: default
server:
image: docker-18-04
region: nyc1
size: s-1vcpu-1gb
token: 3DA541559918A808C2402BBA5012F6C60B27661C
steps:
- name: build
commands:
- go build
- name: test
commands:
- go test

132
engine/compiler/util.go Normal file
View File

@@ -0,0 +1,132 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package compiler
import (
"strings"
"github.com/drone-runners/drone-runner-docker/engine"
"github.com/drone-runners/drone-runner-docker/engine/resource"
"github.com/drone/drone-go/drone"
"github.com/drone/runner-go/manifest"
)
// helper function returns true if the step is configured to
// always run regardless of status.
func isRunAlways(step *resource.Step) bool {
if len(step.When.Status.Include) == 0 &&
len(step.When.Status.Exclude) == 0 {
return false
}
return step.When.Status.Match(drone.StatusFailing) &&
step.When.Status.Match(drone.StatusPassing)
}
// helper function returns true if the step is configured to
// only run on failure.
func isRunOnFailure(step *resource.Step) bool {
if len(step.When.Status.Include) == 0 &&
len(step.When.Status.Exclude) == 0 {
return false
}
return step.When.Status.Match(drone.StatusFailing)
}
// helper function returns true if the pipeline specification
// manually defines an execution graph.
func isGraph(spec *engine.Spec) bool {
for _, step := range spec.Steps {
if len(step.DependsOn) > 0 {
return true
}
}
return false
}
// helper function creates the dependency graph for serial
// pipeline execution.
func configureSerial(spec *engine.Spec) {
var prev *engine.Step
for _, step := range spec.Steps {
if prev != nil {
step.DependsOn = []string{prev.Name}
}
prev = step
}
}
// helper function converts the environment variables to a map,
// returning only inline environment variables not derived from
// a secret.
func convertStaticEnv(src map[string]*manifest.Variable) map[string]string {
dst := map[string]string{}
for k, v := range src {
if strings.TrimSpace(v.Secret) == "" {
dst[k] = v.Value
}
}
return dst
}
// helper function converts the environment variables to a map,
// returning only inline environment variables not derived from
// a secret.
func convertSecretEnv(src map[string]*manifest.Variable) []*engine.Secret {
dst := []*engine.Secret{}
for k, v := range src {
if strings.TrimSpace(v.Secret) != "" {
dst = append(dst, &engine.Secret{
Name: v.Secret,
Mask: true,
Env: k,
})
}
}
return dst
}
// helper function modifies the pipeline dependency graph to
// account for the clone step.
func configureCloneDeps(spec *engine.Spec) {
for _, step := range spec.Steps {
if step.Name == "clone" {
continue
}
if len(step.DependsOn) == 0 {
step.DependsOn = []string{"clone"}
}
}
}
// helper function modifies the pipeline dependency graph to
// account for a disabled clone step.
func removeCloneDeps(spec *engine.Spec) {
for _, step := range spec.Steps {
if step.Name == "clone" {
return
}
}
for _, step := range spec.Steps {
if len(step.DependsOn) == 1 &&
step.DependsOn[0] == "clone" {
step.DependsOn = []string{}
}
}
}
// helper function modifies the pipeline dependency graph to
// account for the clone step.
func convertPullPolicy(s string) engine.PullPolicy {
switch strings.ToLower(s) {
case "always":
return engine.PullAlways
case "if-not-exists":
return engine.PullIfNotExists
case "never":
return engine.PullNever
default:
return engine.PullDefault
}
}

View File

@@ -0,0 +1,200 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package compiler
import (
"testing"
"github.com/drone-runners/drone-runner-docker/engine"
"github.com/drone-runners/drone-runner-docker/engine/resource"
"github.com/drone/runner-go/manifest"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func Test_isRunAlways(t *testing.T) {
step := new(resource.Step)
if isRunAlways(step) == true {
t.Errorf("Want always run false if empty when clause")
}
step.When.Status.Include = []string{"success"}
if isRunAlways(step) == true {
t.Errorf("Want always run false if when success")
}
step.When.Status.Include = []string{"failure"}
if isRunAlways(step) == true {
t.Errorf("Want always run false if when faiure")
}
step.When.Status.Include = []string{"success", "failure"}
if isRunAlways(step) == false {
t.Errorf("Want always run true if when success, failure")
}
}
func Test_isRunOnFailure(t *testing.T) {
step := new(resource.Step)
if isRunOnFailure(step) == true {
t.Errorf("Want run on failure false if empty when clause")
}
step.When.Status.Include = []string{"success"}
if isRunOnFailure(step) == true {
t.Errorf("Want run on failure false if when success")
}
step.When.Status.Include = []string{"failure"}
if isRunOnFailure(step) == false {
t.Errorf("Want run on failure true if when faiure")
}
step.When.Status.Include = []string{"success", "failure"}
if isRunOnFailure(step) == false {
t.Errorf("Want run on failure true if when success, failure")
}
}
func Test_isGraph(t *testing.T) {
spec := new(engine.Spec)
spec.Steps = []*engine.Step{
{DependsOn: []string{}},
}
if isGraph(spec) == true {
t.Errorf("Expect is graph false if deps not exist")
}
spec.Steps[0].DependsOn = []string{"clone"}
if isGraph(spec) == false {
t.Errorf("Expect is graph true if deps exist")
}
}
func Test_configureSerial(t *testing.T) {
before := new(engine.Spec)
before.Steps = []*engine.Step{
{Name: "build"},
{Name: "test"},
{Name: "deploy"},
}
after := new(engine.Spec)
after.Steps = []*engine.Step{
{Name: "build"},
{Name: "test", DependsOn: []string{"build"}},
{Name: "deploy", DependsOn: []string{"test"}},
}
configureSerial(before)
opts := cmpopts.IgnoreUnexported(engine.Spec{})
if diff := cmp.Diff(before, after, opts); diff != "" {
t.Errorf("Unexpected serial configuration")
t.Log(diff)
}
}
func Test_convertStaticEnv(t *testing.T) {
vars := map[string]*manifest.Variable{
"username": &manifest.Variable{Value: "octocat"},
"password": &manifest.Variable{Secret: "password"},
}
envs := convertStaticEnv(vars)
want := map[string]string{"username": "octocat"}
if diff := cmp.Diff(envs, want); diff != "" {
t.Errorf("Unexpected environment variable set")
t.Log(diff)
}
}
func Test_convertSecretEnv(t *testing.T) {
vars := map[string]*manifest.Variable{
"USERNAME": &manifest.Variable{Value: "octocat"},
"PASSWORD": &manifest.Variable{Secret: "password"},
}
envs := convertSecretEnv(vars)
want := []*engine.Secret{
{
Name: "password",
Env: "PASSWORD",
Mask: true,
},
}
if diff := cmp.Diff(envs, want); diff != "" {
t.Errorf("Unexpected secret list")
t.Log(diff)
}
}
func Test_configureCloneDeps(t *testing.T) {
before := new(engine.Spec)
before.Steps = []*engine.Step{
{Name: "clone"},
{Name: "backend"},
{Name: "frontend"},
{Name: "deploy", DependsOn: []string{
"backend", "frontend",
}},
}
after := new(engine.Spec)
after.Steps = []*engine.Step{
{Name: "clone"},
{Name: "backend", DependsOn: []string{"clone"}},
{Name: "frontend", DependsOn: []string{"clone"}},
{Name: "deploy", DependsOn: []string{
"backend", "frontend",
}},
}
configureCloneDeps(before)
opts := cmpopts.IgnoreUnexported(engine.Spec{})
if diff := cmp.Diff(before, after, opts); diff != "" {
t.Errorf("Unexpected dependency adjustment")
t.Log(diff)
}
}
func Test_removeCloneDeps(t *testing.T) {
before := new(engine.Spec)
before.Steps = []*engine.Step{
{Name: "backend", DependsOn: []string{"clone"}},
{Name: "frontend", DependsOn: []string{"clone"}},
{Name: "deploy", DependsOn: []string{
"backend", "frontend",
}},
}
after := new(engine.Spec)
after.Steps = []*engine.Step{
{Name: "backend", DependsOn: []string{}},
{Name: "frontend", DependsOn: []string{}},
{Name: "deploy", DependsOn: []string{
"backend", "frontend",
}},
}
removeCloneDeps(before)
opts := cmpopts.IgnoreUnexported(engine.Spec{})
if diff := cmp.Diff(before, after, opts); diff != "" {
t.Errorf("Unexpected result after removing clone deps")
t.Log(diff)
}
}
func Test_removeCloneDeps_CloneEnabled(t *testing.T) {
before := new(engine.Spec)
before.Steps = []*engine.Step{
{Name: "clone"},
{Name: "test", DependsOn: []string{"clone"}},
}
after := new(engine.Spec)
after.Steps = []*engine.Step{
{Name: "clone"},
{Name: "test", DependsOn: []string{"clone"}},
}
removeCloneDeps(before)
opts := cmpopts.IgnoreUnexported(engine.Spec{})
if diff := cmp.Diff(before, after, opts); diff != "" {
t.Errorf("Expect clone dependencies not removed")
t.Log(diff)
}
}

View File

@@ -0,0 +1,74 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package compiler
import (
stdpath "path"
"strings"
"github.com/drone-runners/drone-runner-docker/engine"
"github.com/drone-runners/drone-runner-docker/engine/resource"
)
const (
workspacePath = "/drone/src"
workspaceName = "workspace"
workspaceHostName = "host"
)
func createWorkspace(from *resource.Pipeline) (base, path, full string) {
base = from.Workspace.Base
path = from.Workspace.Path
if base == "" {
base = workspacePath
}
full = stdpath.Join(base, path)
if from.Platform.OS == "windows" {
base = toWindowsDrive(base)
path = toWindowsPath(path)
full = toWindowsDrive(full)
}
return base, path, full
}
func setupWorkdir(src *resource.Step, dst *engine.Step, path string) {
// if the working directory is already set
// do not alter.
if dst.WorkingDir != "" {
return
}
// if the user is running the container as a
// service (detached mode) with no commands, we
// should use the default working directory.
if dst.Detach && len(src.Commands) == 0 {
return
}
// else set the working directory.
dst.WorkingDir = path
}
// helper function appends the workspace base and
// path to the step's list of environment variables.
func setupWorkspaceEnv(step *engine.Step, base, path, full string) {
step.Envs["DRONE_WORKSPACE_BASE"] = base
step.Envs["DRONE_WORKSPACE_PATH"] = path
step.Envs["DRONE_WORKSPACE"] = full
step.Envs["CI_WORKSPACE_BASE"] = base
step.Envs["CI_WORKSPACE_PATH"] = path
step.Envs["CI_WORKSPACE"] = full
}
// helper function converts the path to a valid windows
// path, including the default C drive.
func toWindowsDrive(s string) string {
return "c:" + toWindowsPath(s)
}
// helper function converts the path to a valid windows
// path, replacing backslashes with forward slashes.
func toWindowsPath(s string) string {
return strings.Replace(s, "/", "\\", -1)
}

View File

@@ -0,0 +1,149 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Polyform License
// that can be found in the LICENSE file.
package compiler
import (
"testing"
"github.com/drone-runners/drone-runner-docker/engine"
"github.com/drone-runners/drone-runner-docker/engine/resource"
"github.com/drone/runner-go/manifest"
)
func TestSetupWorkspace(t *testing.T) {
tests := []struct {
path string
src *resource.Step
dst *engine.Step
want string
}{
{
path: "/drone/src",
src: &resource.Step{},
dst: &engine.Step{},
want: "/drone/src",
},
// do not override the user-defined working dir.
{
path: "/drone/src",
src: &resource.Step{},
dst: &engine.Step{WorkingDir: "/foo"},
want: "/foo",
},
// do not override the default working directory
// for service containers with no commands.
{
path: "/drone/src",
src: &resource.Step{},
dst: &engine.Step{Detach: true},
want: "",
},
// overrides the default working directory
// for service containers with commands.
{
path: "/drone/src",
src: &resource.Step{Commands: []string{"whoami"}},
dst: &engine.Step{Detach: true},
want: "/drone/src",
},
}
for _, test := range tests {
setupWorkdir(test.src, test.dst, test.path)
if got, want := test.dst.WorkingDir, test.want; got != want {
t.Errorf("Want working_dir %s, got %s", want, got)
}
}
}
func TestToWindows(t *testing.T) {
got := toWindowsDrive("/go/src/github.com/octocat/hello-world")
want := "c:\\go\\src\\github.com\\octocat\\hello-world"
if got != want {
t.Errorf("Want windows drive %q, got %q", want, got)
}
}
func TestCreateWorkspace(t *testing.T) {
tests := []struct {
from *resource.Pipeline
base string
path string
full string
}{
{
from: &resource.Pipeline{
Workspace: resource.Workspace{
Base: "",
Path: "",
},
},
base: "/drone/src",
path: "",
full: "/drone/src",
},
{
from: &resource.Pipeline{
Workspace: resource.Workspace{
Base: "",
Path: "",
},
Platform: manifest.Platform{
OS: "windows",
},
},
base: "c:\\drone\\src",
path: "",
full: "c:\\drone\\src",
},
{
from: &resource.Pipeline{
Workspace: resource.Workspace{
Base: "/drone",
Path: "src",
},
},
base: "/drone",
path: "src",
full: "/drone/src",
},
{
from: &resource.Pipeline{
Workspace: resource.Workspace{
Base: "/drone",
Path: "src",
},
Platform: manifest.Platform{
OS: "windows",
},
},
base: "c:\\drone",
path: "src",
full: "c:\\drone\\src",
},
{
from: &resource.Pipeline{
Workspace: resource.Workspace{
Base: "/foo",
Path: "bar",
},
},
base: "/foo",
path: "bar",
full: "/foo/bar",
},
}
for _, test := range tests {
base, path, full := createWorkspace(test.from)
if got, want := test.base, base; got != want {
t.Errorf("Want workspace base %s, got %s", want, got)
}
if got, want := test.path, path; got != want {
t.Errorf("Want workspace path %s, got %s", want, got)
}
if got, want := test.full, full; got != want {
t.Errorf("Want workspace %s, got %s", want, got)
}
}
}