mirror of
https://github.com/thegeeklab/drone-yaml.git
synced 2024-11-24 19:10:40 +00:00
initial commit
This commit is contained in:
commit
98eb77b4c5
20
.drone.yml
Normal file
20
.drone.yml
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
workspace:
|
||||
base: /go
|
||||
path: src/github.com/drone/drone-yaml
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: golang
|
||||
commands:
|
||||
- go get -t -v ./...
|
||||
- go test ./...
|
||||
|
||||
...
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
NOTES*
|
||||
*.out
|
||||
*.env
|
33
README.md
Normal file
33
README.md
Normal file
@ -0,0 +1,33 @@
|
||||
Package yaml provides a parser, linter, formatter and compiler for the [drone](https://github.com/drone/drone) configuration file format.
|
||||
|
||||
Lint the yaml file:
|
||||
|
||||
```text
|
||||
$ drone-yaml lint samples/simple.yml
|
||||
```
|
||||
|
||||
Format the yaml file:
|
||||
|
||||
```text
|
||||
$ drone-yaml fmt samples/simple.yml
|
||||
$ drone-yaml fmt samples/simple.yml --save
|
||||
```
|
||||
|
||||
Sign the yaml file using a 32-bit secret key:
|
||||
|
||||
```text
|
||||
$ drone-yaml sign 642909eb4c3d47e33999235c0598353c samples/simple.yml
|
||||
$ drone-yaml sign 642909eb4c3d47e33999235c0598353c samples/simple.yml --save
|
||||
```
|
||||
|
||||
Verify the yaml file signature:
|
||||
|
||||
```text
|
||||
$ drone-yaml verify 642909eb4c3d47e33999235c0598353c samples/simple.yml
|
||||
```
|
||||
|
||||
Compile the yaml file:
|
||||
|
||||
```text
|
||||
$ drone-yaml compile samples/simple.yml > samples/simple.json
|
||||
```
|
295
main.go
Normal file
295
main.go
Normal file
@ -0,0 +1,295 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/drone/drone-yaml/yaml"
|
||||
"github.com/drone/drone-yaml/yaml/compiler"
|
||||
"github.com/drone/drone-yaml/yaml/compiler/transform"
|
||||
"github.com/drone/drone-yaml/yaml/linter"
|
||||
"github.com/drone/drone-yaml/yaml/pretty"
|
||||
"github.com/drone/drone-yaml/yaml/signer"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
format = kingpin.Command("fmt", "format the yaml file")
|
||||
formatPriv = format.Flag("privileged", "privileged mode").Short('p').Bool()
|
||||
formatSave = format.Flag("save", "save result to source").Short('s').Bool()
|
||||
formatFile = format.Arg("source", "source file location").Default(".drone.yml").File()
|
||||
|
||||
lint = kingpin.Command("lint", "lint the yaml file")
|
||||
lintPriv = lint.Flag("privileged", "privileged mode").Short('p').Bool()
|
||||
lintFile = lint.Arg("source", "source file location").Default(".drone.yml").File()
|
||||
|
||||
sign = kingpin.Command("sign", "sign the yaml file")
|
||||
signKey = sign.Arg("key", "secret key").Required().String()
|
||||
signFile = sign.Arg("source", "source file location").Default(".drone.yml").File()
|
||||
signSave = sign.Flag("save", "save result to source").Short('s').Bool()
|
||||
|
||||
verify = kingpin.Command("verify", "verify the yaml signature")
|
||||
verifyKey = verify.Arg("key", "secret key").Required().String()
|
||||
verifyFile = verify.Arg("source", "source file location").Default(".drone.yml").File()
|
||||
|
||||
compile = kingpin.Command("compile", "compile the yaml file")
|
||||
compileIn = compile.Arg("source", "source file location").Default(".drone.yml").File()
|
||||
compileName = compile.Flag("name", "pipeline name").String()
|
||||
)
|
||||
|
||||
func main() {
|
||||
switch kingpin.Parse() {
|
||||
case format.FullCommand():
|
||||
kingpin.FatalIfError(runFormat(), "")
|
||||
case lint.FullCommand():
|
||||
kingpin.FatalIfError(runLint(), "")
|
||||
case sign.FullCommand():
|
||||
kingpin.FatalIfError(runSign(), "")
|
||||
case verify.FullCommand():
|
||||
kingpin.FatalIfError(runVerify(), "")
|
||||
case compile.FullCommand():
|
||||
kingpin.FatalIfError(runCompile(), "")
|
||||
}
|
||||
}
|
||||
|
||||
func runFormat() error {
|
||||
f := *formatFile
|
||||
m, err := yaml.Parse(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
pretty.Print(b, m)
|
||||
|
||||
if *formatSave {
|
||||
return ioutil.WriteFile(f.Name(), b.Bytes(), 0644)
|
||||
}
|
||||
_, err = io.Copy(os.Stderr, b)
|
||||
return err
|
||||
}
|
||||
|
||||
func runLint() error {
|
||||
f := *lintFile
|
||||
m, err := yaml.Parse(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range m.Resources {
|
||||
err := linter.Lint(r, *lintPriv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSign() error {
|
||||
f := *signFile
|
||||
d, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k := signer.KeyString(*signKey)
|
||||
|
||||
if *signSave {
|
||||
out, err := signer.SignUpdate(d, k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(f.Name(), out, 0644)
|
||||
}
|
||||
|
||||
hmac, err := signer.Sign(d, k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(hmac)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runVerify() error {
|
||||
f := *verifyFile
|
||||
d, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k := signer.KeyString(*verifyKey)
|
||||
ok, err := signer.Verify(d, k)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !ok {
|
||||
return errors.New("cannot verify yaml signature")
|
||||
}
|
||||
|
||||
fmt.Println("success: yaml signature verified")
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
trusted = compile.Flag("trusted", "trusted mode").Bool()
|
||||
labels = compile.Flag("label", "container labels").StringMap()
|
||||
clone = compile.Flag("clone", "clone step").Bool()
|
||||
volume = compile.Flag("volume", "attached volumes").StringMap()
|
||||
network = compile.Flag("network", "attached networks").Strings()
|
||||
environ = compile.Flag("env", "environment variable").StringMap()
|
||||
dind = compile.Flag("dind", "dind images").Default("plugins/docker").Strings()
|
||||
event = compile.Flag("event", "event type").PlaceHolder("<event>").Enum("push", "pull_request", "tag", "deployment")
|
||||
repo = compile.Flag("repo", "repository name").PlaceHolder("octocat/hello-world").String()
|
||||
remote = compile.Flag("git-remote", "git remote url").PlaceHolder("https://github.com/octocat/hello-world.git").String()
|
||||
branch = compile.Flag("git-branch", "git commit branch").PlaceHolder("master").String()
|
||||
ref = compile.Flag("git-ref", "git commit ref").PlaceHolder("refs/heads/master").String()
|
||||
sha = compile.Flag("git-sha", "git commit sha").String()
|
||||
creds = compile.Flag("git-creds", "git credentials").URLList()
|
||||
instance = compile.Flag("instance", "drone instance hostname").PlaceHolder("drone.company.com").String()
|
||||
deploy = compile.Flag("deploy-to", "target deployment").PlaceHolder("production").String()
|
||||
secrets = compile.Flag("secret", "secret variable").StringMap()
|
||||
registries = compile.Flag("registry", "registry credentials").URLList()
|
||||
username = compile.Flag("netrc-username", "netrc username").PlaceHolder("<token>").String()
|
||||
password = compile.Flag("netrc-password", "netrc password").PlaceHolder("x-oauth-basic").String()
|
||||
machine = compile.Flag("netrc-machine", "netrc machine").PlaceHolder("github.com").String()
|
||||
memlimit = compile.Flag("mem-limit", "memory limit").PlaceHolder("1GB").Bytes()
|
||||
cpulimit = compile.Flag("cpu-limit", "cpu limit").PlaceHolder("2").Int64()
|
||||
)
|
||||
|
||||
func runCompile() error {
|
||||
m, err := yaml.Parse(*compileIn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var p *yaml.Pipeline
|
||||
for _, r := range m.Resources {
|
||||
v, ok := r.(*yaml.Pipeline)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if *compileName == "" ||
|
||||
*compileName == v.Name {
|
||||
p = v
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if p == nil {
|
||||
return errors.New("cannot find pipeline resource")
|
||||
}
|
||||
|
||||
// the user has the option to disable the git clone
|
||||
// if the pipeline is being executed on the local
|
||||
// codebase.
|
||||
if *clone == false {
|
||||
p.Clone.Disable = true
|
||||
}
|
||||
|
||||
var auths []*engine.DockerAuth
|
||||
for _, uri := range *registries {
|
||||
if uri.User == nil {
|
||||
log.Fatalln("Expect registry format [user]:[password]@hostname")
|
||||
}
|
||||
password, ok := uri.User.Password()
|
||||
if !ok {
|
||||
log.Fatalln("Invalid or missing registry password")
|
||||
}
|
||||
auths = append(auths, &engine.DockerAuth{
|
||||
Address: uri.Host,
|
||||
Username: uri.User.Username(),
|
||||
Password: password,
|
||||
})
|
||||
}
|
||||
|
||||
comp := new(compiler.Compiler)
|
||||
comp.GitCredentialsFunc = defaultCreds // TODO create compiler.GitCredentialsFunc and compiler.GlobalGitCredentialsFunc
|
||||
comp.NetrcFunc = nil // TODO create compiler.NetrcFunc and compiler.GlobalNetrcFunc
|
||||
comp.PrivilegedFunc = compiler.DindFunc(*dind)
|
||||
comp.SkipFunc = compiler.SkipFunc(
|
||||
compiler.SkipData{
|
||||
Branch: *branch,
|
||||
Event: *event,
|
||||
Instance: *instance,
|
||||
Ref: *ref,
|
||||
Repo: *repo,
|
||||
Target: *deploy,
|
||||
},
|
||||
)
|
||||
comp.TransformFunc = transform.Combine(
|
||||
transform.WithAuths(auths),
|
||||
transform.WithEnviron(*environ),
|
||||
transform.WithEnviron(defaultEnvs()),
|
||||
transform.WithLables(*labels),
|
||||
transform.WithLimits(int64(*memlimit), int64(*cpulimit)),
|
||||
transform.WithNetrc(*machine, *username, *password),
|
||||
transform.WithNetworks(*network),
|
||||
transform.WithProxy(),
|
||||
transform.WithSecrets(*secrets),
|
||||
transform.WithVolumes(*volume),
|
||||
)
|
||||
compiled := comp.Compile(p)
|
||||
|
||||
// // for drone-exec we will need to change the workspace
|
||||
// // to a host volume mount, to the current working dir.
|
||||
// for _, volume := range compiled.Docker.Volumes {
|
||||
// if volume.Metadata.Name == "workspace" {
|
||||
// volume.EmptyDir = nil
|
||||
// volume.HostPath = &engine.VolumeHostPath{
|
||||
// Path: "", // pwd
|
||||
// }
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// // then we need to change the base mount for every container
|
||||
// // to use the workspace base + path.
|
||||
// for _, container := range compiled.Steps {
|
||||
// for _, volume := range container.Volumes {
|
||||
// if volume.Name == "workspace" {
|
||||
// volume.Path = container.Envs["DRONE_WORKSPACE"]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(compiled)
|
||||
}
|
||||
|
||||
// helper function returns the git credential function,
|
||||
// used to return a git credentials file.
|
||||
func defaultCreds() []byte {
|
||||
urls := *creds
|
||||
if len(urls) == 0 {
|
||||
return nil
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
for _, url := range urls {
|
||||
buf.WriteString(url.String())
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// helper function returns the minimum required environment
|
||||
// variables to clone a repository. All other environment
|
||||
// variables should be passed via the --env flag.
|
||||
func defaultEnvs() map[string]string {
|
||||
envs := map[string]string{}
|
||||
envs["DRONE_COMMIT_BRANCH"] = *branch
|
||||
envs["DRONE_COMMIT_SHA"] = *sha
|
||||
envs["DRONE_COMMIT_REF"] = *ref
|
||||
envs["DRONE_REMOTE_URL"] = *remote
|
||||
envs["DRONE_BUILD_EVENT"] = *event
|
||||
if strings.HasPrefix(*ref, "refs/tags/") {
|
||||
envs["DRONE_TAG"] = strings.TrimPrefix(*ref, "refs/tags/")
|
||||
}
|
||||
return envs
|
||||
}
|
71
samples/complex.yml
Normal file
71
samples/complex.yml
Normal file
@ -0,0 +1,71 @@
|
||||
kind: pipeline
|
||||
name: build
|
||||
|
||||
steps:
|
||||
- name: backend
|
||||
image: golang:1.11
|
||||
commands:
|
||||
- go build
|
||||
- go test -v
|
||||
|
||||
- name: frontend
|
||||
image: node
|
||||
commands:
|
||||
- npm install
|
||||
- npm run test
|
||||
- npm run lint
|
||||
|
||||
services:
|
||||
- name: redis
|
||||
image: redis:latest
|
||||
ports:
|
||||
- 6379
|
||||
volumes:
|
||||
- name: foo
|
||||
path: /bar
|
||||
|
||||
volumes:
|
||||
- name: foo
|
||||
temp: {}
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: notify
|
||||
|
||||
steps:
|
||||
- name: notify
|
||||
image: plugins/slack
|
||||
settings:
|
||||
room: general
|
||||
token:
|
||||
$secret: token
|
||||
|
||||
node:
|
||||
disk: ssd
|
||||
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
---
|
||||
kind: cron
|
||||
name: nightly
|
||||
spec:
|
||||
schedule: "1 * * * *"
|
||||
branch: master
|
||||
deployment:
|
||||
target: production
|
||||
|
||||
---
|
||||
kind: secret
|
||||
type: encrypted
|
||||
|
||||
data:
|
||||
token: N2NmYjA3ODQwNTY1ODFlY2E5MGJmOWI1NDk0NDFhMTEK
|
||||
|
||||
---
|
||||
kind: registry
|
||||
type: encrypted
|
||||
|
||||
data:
|
||||
index.drone.io: N2NmYjA3ODQwNTY1ODFlY2E5MGJmOWI1NDk0NDFhMTEK
|
||||
|
22
samples/simple.yml
Normal file
22
samples/simple.yml
Normal file
@ -0,0 +1,22 @@
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: backend
|
||||
image: golang:1.11
|
||||
commands:
|
||||
- go build
|
||||
- go test -v
|
||||
|
||||
- name: frontend
|
||||
image: node
|
||||
commands:
|
||||
- npm install
|
||||
- npm run test
|
||||
- npm run lint
|
||||
|
||||
services:
|
||||
- name: redis
|
||||
image: redis:latest
|
||||
ports:
|
||||
- 6379
|
40
yaml/build.go
Normal file
40
yaml/build.go
Normal file
@ -0,0 +1,40 @@
|
||||
package yaml
|
||||
|
||||
type (
|
||||
// Build configures a Docker build.
|
||||
Build struct {
|
||||
Args map[string]string `json:"args,omitempty"`
|
||||
CacheFrom []string `json:"cache_from,omitempty" yaml:"cache_from"`
|
||||
Context string `json:"context,omitempty"`
|
||||
Dockerfile string `json:"dockerfile,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
// build is a tempoary type used to unmarshal
|
||||
// the Build struct when long format is used.
|
||||
build struct {
|
||||
Args map[string]string
|
||||
CacheFrom []string `yaml:"cache_from"`
|
||||
Context string
|
||||
Dockerfile string
|
||||
Image string
|
||||
Labels map[string]string
|
||||
}
|
||||
)
|
||||
|
||||
// UnmarshalYAML implements yaml unmarshalling.
|
||||
func (b *Build) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
d := new(build)
|
||||
err := unmarshal(&d.Image)
|
||||
if err != nil {
|
||||
err = unmarshal(d)
|
||||
}
|
||||
b.Args = d.Args
|
||||
b.CacheFrom = d.CacheFrom
|
||||
b.Context = d.Context
|
||||
b.Dockerfile = d.Dockerfile
|
||||
b.Labels = d.Labels
|
||||
b.Image = d.Image
|
||||
return err
|
||||
}
|
44
yaml/build_test.go
Normal file
44
yaml/build_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
tests := []struct {
|
||||
yaml string
|
||||
image string
|
||||
}{
|
||||
{
|
||||
yaml: "bar",
|
||||
image: "bar",
|
||||
},
|
||||
{
|
||||
yaml: "{ image: foo }",
|
||||
image: "foo",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
in := []byte(test.yaml)
|
||||
out := new(Build)
|
||||
err := yaml.Unmarshal(in, out)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if got, want := out.Image, test.image; got != want {
|
||||
t.Errorf("Want image %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildError(t *testing.T) {
|
||||
in := []byte("[]")
|
||||
out := new(Build)
|
||||
err := yaml.Unmarshal(in, out)
|
||||
if err == nil {
|
||||
t.Errorf("Expect unmarshal error")
|
||||
}
|
||||
}
|
75
yaml/compiler/clone.go
Normal file
75
yaml/compiler/clone.go
Normal file
@ -0,0 +1,75 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/drone/drone-yaml/yaml"
|
||||
"github.com/drone/drone-yaml/yaml/compiler/internal/rand"
|
||||
)
|
||||
|
||||
// default name of the clone step.
|
||||
const cloneStepName = "clone"
|
||||
|
||||
// helper function returns the preferred clone image
|
||||
// based on the target architecture.
|
||||
func cloneImage(src *yaml.Pipeline) string {
|
||||
switch {
|
||||
case src.Platform.OS == "linux" && src.Platform.Arch == "arm":
|
||||
return "drone/git:linux-arm"
|
||||
case src.Platform.OS == "linux" && src.Platform.Arch == "arm64":
|
||||
return "drone/git:linux-arm64"
|
||||
case src.Platform.OS == "windows":
|
||||
return "drone/git:windows-1803"
|
||||
default:
|
||||
return "drone/git"
|
||||
}
|
||||
}
|
||||
|
||||
// helper function configures the clone depth parameter,
|
||||
// specific to the clone plugin.
|
||||
//
|
||||
// TODO(bradrydzewski) rename to setupCloneParams
|
||||
func setupCloneDepth(src *yaml.Pipeline, dst *engine.Step) {
|
||||
if depth := src.Clone.Depth; depth > 0 {
|
||||
dst.Envs["PLUGIN_DEPTH"] = strconv.Itoa(depth)
|
||||
}
|
||||
if skipVerify := src.Clone.SkipVerify; skipVerify {
|
||||
dst.Envs["GIT_SSL_NO_VERIFY"] = "true"
|
||||
dst.Envs["PLUGIN_SKIP_VERIFY"] = "true"
|
||||
}
|
||||
}
|
||||
|
||||
// helper function configures the .git-clone credentials
|
||||
// file. The file is mounted into the container, pointed
|
||||
// to by XDG_CONFIG_HOME
|
||||
// see https://git-scm.com/docs/git-credential-store
|
||||
func setupCloneCredentials(spec *engine.Spec, dst *engine.Step, data []byte) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
// TODO(bradrydzewski) we may need to update the git
|
||||
// clone plugin to configure the git credential store.
|
||||
dst.Files = append(dst.Files, &engine.FileMount{
|
||||
Name: ".git-credentials",
|
||||
Path: "/root/.git-credentials",
|
||||
})
|
||||
spec.Files = append(spec.Files, &engine.File{
|
||||
Metadata: engine.Metadata{
|
||||
UID: rand.String(),
|
||||
Namespace: spec.Metadata.Namespace,
|
||||
Name: ".git-credentials",
|
||||
},
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// helper function creates a default container configuration
|
||||
// for the clone stage. The clone stage is automatically
|
||||
// added to each pipeline.
|
||||
func createClone(src *yaml.Pipeline) *yaml.Container {
|
||||
return &yaml.Container{
|
||||
Name: cloneStepName,
|
||||
Image: cloneImage(src),
|
||||
}
|
||||
}
|
103
yaml/compiler/clone_test.go
Normal file
103
yaml/compiler/clone_test.go
Normal file
@ -0,0 +1,103 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/drone/drone-yaml/yaml"
|
||||
)
|
||||
|
||||
func TestCloneImage(t *testing.T) {
|
||||
tests := []struct {
|
||||
platform yaml.Platform
|
||||
image string
|
||||
}{
|
||||
{
|
||||
platform: yaml.Platform{OS: "linux", Arch: "amd64"},
|
||||
image: "drone/git",
|
||||
},
|
||||
{
|
||||
platform: yaml.Platform{OS: "linux", Arch: "arm"},
|
||||
image: "drone/git:linux-arm",
|
||||
},
|
||||
{
|
||||
platform: yaml.Platform{OS: "linux", Arch: "arm64"},
|
||||
image: "drone/git:linux-arm64",
|
||||
},
|
||||
{
|
||||
platform: yaml.Platform{OS: "windows", Arch: "amd64"},
|
||||
image: "drone/git:windows-1803",
|
||||
},
|
||||
{
|
||||
platform: yaml.Platform{},
|
||||
image: "drone/git",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
pipeline := &yaml.Pipeline{Platform: test.platform}
|
||||
image := cloneImage(pipeline)
|
||||
if got, want := image, test.image; got != want {
|
||||
t.Errorf("Want clone image %s, got %s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupCloneDepth(t *testing.T) {
|
||||
// test zero depth
|
||||
src := &yaml.Pipeline{
|
||||
Clone: yaml.Clone{
|
||||
Depth: 0,
|
||||
},
|
||||
}
|
||||
dst := &engine.Step{
|
||||
Envs: map[string]string{},
|
||||
}
|
||||
setupCloneDepth(src, dst)
|
||||
if _, ok := dst.Envs["PLUGIN_DEPTH"]; ok {
|
||||
t.Errorf("Expect depth ignored when zero value")
|
||||
}
|
||||
|
||||
// test non-zero depth
|
||||
src = &yaml.Pipeline{
|
||||
Clone: yaml.Clone{
|
||||
Depth: 50,
|
||||
},
|
||||
}
|
||||
dst = &engine.Step{
|
||||
Envs: map[string]string{},
|
||||
}
|
||||
setupCloneDepth(src, dst)
|
||||
if got, want := dst.Envs["PLUGIN_DEPTH"], "50"; got != want {
|
||||
t.Errorf("Expect depth %s, got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupCloneSkipVerify(t *testing.T) {
|
||||
// test zero depth
|
||||
src := &yaml.Pipeline{
|
||||
Clone: yaml.Clone{
|
||||
SkipVerify: false,
|
||||
},
|
||||
}
|
||||
dst := &engine.Step{
|
||||
Envs: map[string]string{},
|
||||
}
|
||||
setupCloneDepth(src, dst)
|
||||
if _, ok := dst.Envs["PLUGIN_SKIP_VERIFY"]; ok {
|
||||
t.Errorf("Expect skip verify not set")
|
||||
}
|
||||
|
||||
// test non-zero depth
|
||||
src = &yaml.Pipeline{
|
||||
Clone: yaml.Clone{
|
||||
SkipVerify: true,
|
||||
},
|
||||
}
|
||||
dst = &engine.Step{
|
||||
Envs: map[string]string{},
|
||||
}
|
||||
setupCloneDepth(src, dst)
|
||||
if got, want := dst.Envs["PLUGIN_SKIP_VERIFY"], "true"; got != want {
|
||||
t.Errorf("Expect skip verify %s, got %s", want, got)
|
||||
}
|
||||
}
|
303
yaml/compiler/compiler.go
Normal file
303
yaml/compiler/compiler.go
Normal file
@ -0,0 +1,303 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/drone/drone-yaml/yaml"
|
||||
"github.com/drone/drone-yaml/yaml/compiler/image"
|
||||
"github.com/drone/drone-yaml/yaml/compiler/internal/rand"
|
||||
)
|
||||
|
||||
// A Compiler compiles the pipeline configuration to an
|
||||
// intermediate representation that can be executed by
|
||||
// the Drone runtime engine.
|
||||
type Compiler struct {
|
||||
// GitCredentialsFunc returns a .git-credentials file
|
||||
// that can be used by the default clone step to
|
||||
// authenticate to the remote repository.
|
||||
GitCredentialsFunc func() []byte
|
||||
|
||||
// NetrcFunc returns a .netrc file that can be used by
|
||||
// the default clone step to authenticate to the remote
|
||||
// repository.
|
||||
NetrcFunc func() []byte
|
||||
|
||||
// PrivilegedFunc returns true if the container should
|
||||
// be started in privileged mode. The intended use is
|
||||
// for plugins that run Docker-in-Docker. This will be
|
||||
// deprecated in a future release.
|
||||
PrivilegedFunc func(*yaml.Container) bool
|
||||
|
||||
// SkipFunc returns true if the step should be skipped.
|
||||
// The skip function can be used to evaluate the when
|
||||
// clause of each step, and return true if it should
|
||||
// be skipped.
|
||||
SkipFunc func(*yaml.Container) bool
|
||||
|
||||
// TransformFunc can be used to modify the compiled
|
||||
// output prior to completion. This can be useful when
|
||||
// you need to programatically modify the output,
|
||||
// set defaults, etc.
|
||||
TransformFunc func(*engine.Spec)
|
||||
|
||||
// WorkspaceFunc can be used to set the workspace volume
|
||||
// that is created for the entire pipeline. The primary
|
||||
// use case for this function is running local builds,
|
||||
// where the workspace is mounted to the host machine
|
||||
// working directory.
|
||||
WorkspaceFunc func(*engine.Spec)
|
||||
|
||||
// WorkspaceMountFunc can be used to override the default
|
||||
// workspace volume mount.
|
||||
WorkspaceMountFunc func(step *engine.Step, base, path, full string)
|
||||
}
|
||||
|
||||
// Compile returns an intermediate representation of the
|
||||
// pipeline configuration that can be executed by the
|
||||
// Drone runtime engine.
|
||||
func (c *Compiler) Compile(from *yaml.Pipeline) *engine.Spec {
|
||||
namespace := rand.String()
|
||||
|
||||
isSerial := true
|
||||
for _, step := range from.Steps {
|
||||
if len(step.DependsOn) != 0 {
|
||||
isSerial = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
spec := &engine.Spec{
|
||||
Metadata: engine.Metadata{
|
||||
UID: namespace,
|
||||
Name: namespace,
|
||||
Namespace: namespace,
|
||||
Labels: map[string]string{
|
||||
"io.drone.pipeline.name": from.Name,
|
||||
"io.drone.pipeline.kind": from.Kind,
|
||||
"io.drone.pipeline.type": from.Type,
|
||||
},
|
||||
},
|
||||
Platform: engine.Platform{
|
||||
OS: from.Platform.OS,
|
||||
Arch: from.Platform.Arch,
|
||||
Version: from.Platform.Version,
|
||||
Variant: from.Platform.Variant,
|
||||
},
|
||||
Docker: &engine.DockerConfig{},
|
||||
Files: nil,
|
||||
Secrets: nil,
|
||||
}
|
||||
|
||||
// create the default workspace path. If a container
|
||||
// does not specify a working directory it defaults
|
||||
// to the workspace path.
|
||||
base, dir, workspace := createWorkspace(from)
|
||||
|
||||
// create the default workspace volume definition.
|
||||
// the volume will be mounted to each container in
|
||||
// the pipeline.
|
||||
c.setupWorkspace(spec)
|
||||
|
||||
// for each volume defined in the yaml configuration
|
||||
// file, convert to a runtime volume and append to the
|
||||
// specification.
|
||||
for _, from := range from.Volumes {
|
||||
to := &engine.Volume{
|
||||
Metadata: engine.Metadata{
|
||||
UID: rand.String(),
|
||||
Name: from.Name,
|
||||
Namespace: namespace,
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
}
|
||||
if from.EmptyDir != nil {
|
||||
// if the yaml configuration specifies an empty
|
||||
// directory volume (data volume) or an in-memory
|
||||
// file system.
|
||||
to.EmptyDir = &engine.VolumeEmptyDir{
|
||||
Medium: from.EmptyDir.Medium,
|
||||
SizeLimit: int64(from.EmptyDir.SizeLimit),
|
||||
}
|
||||
} else if from.HostPath != nil {
|
||||
// if the yaml configuration specifies a bind
|
||||
// mount to the host machine.
|
||||
to.HostPath = &engine.VolumeHostPath{
|
||||
Path: from.HostPath.Path,
|
||||
}
|
||||
}
|
||||
spec.Docker.Volumes = append(spec.Docker.Volumes, to)
|
||||
}
|
||||
|
||||
if !from.Clone.Disable {
|
||||
src := createClone(from)
|
||||
dst := createStep(spec, src)
|
||||
dst.Docker.PullPolicy = engine.PullIfNotExists
|
||||
setupCloneDepth(from, dst)
|
||||
setupCloneCredentials(spec, dst, c.gitCredentials())
|
||||
setupWorkingDir(src, dst, workspace)
|
||||
setupWorkspaceEnv(dst, base, dir, workspace)
|
||||
c.setupWorkspaceMount(dst, base, dir, workspace)
|
||||
spec.Steps = append(spec.Steps, dst)
|
||||
}
|
||||
|
||||
// for each pipeline service defined in the yaml
|
||||
// configuration file, convert to a runtime step
|
||||
// and append to the specification.
|
||||
for _, service := range from.Services {
|
||||
step := createStep(spec, service)
|
||||
// note that all services are automatically
|
||||
// set to run in detached mode.
|
||||
step.Detach = true
|
||||
setupWorkingDir(service, step, workspace)
|
||||
setupWorkspaceEnv(step, base, dir, workspace)
|
||||
c.setupWorkspaceMount(step, base, dir, workspace)
|
||||
// if the skip callback function returns true,
|
||||
// modify the runtime step to never execute.
|
||||
if c.skip(service) {
|
||||
step.RunPolicy = engine.RunNever
|
||||
}
|
||||
// if the step is a plugin and should be executed
|
||||
// in privileged mode, set the privileged flag.
|
||||
if c.privileged(service) {
|
||||
step.Docker.Privileged = true
|
||||
}
|
||||
// if the clone step is enabled, the service should
|
||||
// not start until the clone step is complete. Add
|
||||
// the clone step as a dependency in the graph.
|
||||
if isSerial == false && from.Clone.Disable == false {
|
||||
step.DependsOn = append(step.DependsOn, cloneStepName)
|
||||
}
|
||||
spec.Steps = append(spec.Steps, step)
|
||||
}
|
||||
|
||||
// rename will store a list of container names
|
||||
// that should be mapped to their temporary alias.
|
||||
rename := map[string]string{}
|
||||
|
||||
// for each pipeline step defined in the yaml
|
||||
// configuration file, convert to a runtime step
|
||||
// and append to the specification.
|
||||
for _, container := range from.Steps {
|
||||
var step *engine.Step
|
||||
switch {
|
||||
case container.Build != nil:
|
||||
step = createBuildStep(spec, container)
|
||||
rename[container.Build.Image] = step.Metadata.UID
|
||||
default:
|
||||
step = createStep(spec, container)
|
||||
}
|
||||
setupWorkingDir(container, step, workspace)
|
||||
setupWorkspaceEnv(step, base, dir, workspace)
|
||||
c.setupWorkspaceMount(step, base, dir, workspace)
|
||||
// if the skip callback function returns true,
|
||||
// modify the runtime step to never execute.
|
||||
if c.skip(container) {
|
||||
step.RunPolicy = engine.RunNever
|
||||
}
|
||||
// if the step is a plugin and should be executed
|
||||
// in privileged mode, set the privileged flag.
|
||||
if c.privileged(container) {
|
||||
step.Docker.Privileged = true
|
||||
}
|
||||
// if the clone step is enabled, the step should
|
||||
// not start until the clone step is complete. If
|
||||
// no dependencies are defined, at a minimum, the
|
||||
// step depends on the initial clone step completing.
|
||||
if isSerial == false && from.Clone.Disable == false && len(step.DependsOn) == 0 {
|
||||
step.DependsOn = append(step.DependsOn, cloneStepName)
|
||||
}
|
||||
spec.Steps = append(spec.Steps, step)
|
||||
}
|
||||
|
||||
// if the pipeline includes any build and publish
|
||||
// steps we should create an entry for the host
|
||||
// machine docker socket.
|
||||
if spec.Docker != nil && len(rename) > 0 {
|
||||
v := &engine.Volume{
|
||||
Metadata: engine.Metadata{
|
||||
UID: rand.String(),
|
||||
Name: "_docker_socket",
|
||||
Namespace: namespace,
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
HostPath: &engine.VolumeHostPath{
|
||||
Path: "/var/run/docker.sock",
|
||||
},
|
||||
}
|
||||
spec.Docker.Volumes = append(spec.Docker.Volumes, v)
|
||||
}
|
||||
|
||||
// images created during the pipeline are assigned a
|
||||
// random alias. All references to the origin image
|
||||
// name must be changed to the alias.
|
||||
for _, step := range spec.Steps {
|
||||
for k, v := range rename {
|
||||
if image.MatchTag(step.Docker.Image, k) {
|
||||
img := image.Trim(step.Docker.Image) + ":" + v
|
||||
step.Docker.Image = image.Expand(img)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executes user-defined transformations before the
|
||||
// final specification is returned.
|
||||
if c.TransformFunc != nil {
|
||||
c.TransformFunc(spec)
|
||||
}
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
// return a .git-credentials file. If the user-defined
|
||||
// function is nil, a nil credentials file is returned.
|
||||
func (c *Compiler) gitCredentials() []byte {
|
||||
if c.GitCredentialsFunc != nil {
|
||||
return c.GitCredentialsFunc()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// return a .netrc file. If the user-defined function is
|
||||
// nil, a nil netrc file is returned.
|
||||
func (c *Compiler) netrc() []byte {
|
||||
if c.NetrcFunc != nil {
|
||||
return c.NetrcFunc()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// return true if the step should be executed in privileged
|
||||
// mode. If the user-defined privileged function is nil,
|
||||
// a default value of false is returned.
|
||||
func (c *Compiler) privileged(container *yaml.Container) bool {
|
||||
if c.PrivilegedFunc != nil {
|
||||
return c.PrivilegedFunc(container)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// return true if the step should be skipped. If the
|
||||
// user-defined skip function is nil, a defalt skip
|
||||
// function is used that always returns true (i.e. do not skip).
|
||||
func (c *Compiler) skip(container *yaml.Container) bool {
|
||||
if c.SkipFunc != nil {
|
||||
return c.SkipFunc(container)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Compiler) setupWorkspace(spec *engine.Spec) {
|
||||
if c.WorkspaceFunc != nil {
|
||||
c.WorkspaceFunc(spec)
|
||||
return
|
||||
}
|
||||
CreateWorkspace(spec)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Compiler) setupWorkspaceMount(step *engine.Step, base, path, full string) {
|
||||
if c.WorkspaceMountFunc != nil {
|
||||
c.WorkspaceMountFunc(step, base, path, full)
|
||||
return
|
||||
}
|
||||
MountWorkspace(step, base, path, full)
|
||||
}
|
76
yaml/compiler/convert.go
Normal file
76
yaml/compiler/convert.go
Normal file
@ -0,0 +1,76 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/drone/drone-yaml/yaml"
|
||||
)
|
||||
|
||||
func toIgnoreErr(from *yaml.Container) bool {
|
||||
return strings.EqualFold(from.Failure, "ignore")
|
||||
}
|
||||
|
||||
func toPorts(from *yaml.Container) []*engine.Port {
|
||||
var ports []*engine.Port
|
||||
for _, port := range from.Ports {
|
||||
ports = append(ports, toPort(port))
|
||||
}
|
||||
return ports
|
||||
}
|
||||
|
||||
func toPort(from *yaml.Port) *engine.Port {
|
||||
return &engine.Port{
|
||||
Port: from.Port,
|
||||
Host: from.Host,
|
||||
Protocol: from.Protocol,
|
||||
}
|
||||
}
|
||||
|
||||
func toPullPolicy(from *yaml.Container) engine.PullPolicy {
|
||||
switch strings.ToLower(from.Pull) {
|
||||
case "always":
|
||||
return engine.PullAlways
|
||||
case "if-not-exists":
|
||||
return engine.PullIfNotExists
|
||||
case "never":
|
||||
return engine.PullNever
|
||||
default:
|
||||
return engine.PullDefault
|
||||
}
|
||||
}
|
||||
|
||||
func toRunPolicy(from *yaml.Container) engine.RunPolicy {
|
||||
onFailure := from.When.Status.Match("failure") && len(from.When.Status.Include) > 0
|
||||
onSuccess := from.When.Status.Match("success")
|
||||
switch {
|
||||
case onFailure && onSuccess:
|
||||
return engine.RunAlways
|
||||
case onFailure:
|
||||
return engine.RunOnFailure
|
||||
case onSuccess:
|
||||
return engine.RunOnSuccess
|
||||
default:
|
||||
return engine.RunNever
|
||||
}
|
||||
}
|
||||
|
||||
func toResources(from *yaml.Container) *engine.Resources {
|
||||
if from.Resources == nil {
|
||||
return nil
|
||||
}
|
||||
return &engine.Resources{
|
||||
Limits: toResourceObject(from.Resources.Limits),
|
||||
Requests: toResourceObject(from.Resources.Requests),
|
||||
}
|
||||
}
|
||||
|
||||
func toResourceObject(from *yaml.ResourceObject) *engine.ResourceObject {
|
||||
if from == nil {
|
||||
return nil
|
||||
}
|
||||
return &engine.ResourceObject{
|
||||
CPU: int64(from.CPU),
|
||||
Memory: int64(from.Memory),
|
||||
}
|
||||
}
|
154
yaml/compiler/convert_test.go
Normal file
154
yaml/compiler/convert_test.go
Normal file
@ -0,0 +1,154 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/drone/drone-yaml/yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func Test_toIgnoreErr(t *testing.T) {
|
||||
tests := []struct {
|
||||
mode string
|
||||
want bool
|
||||
}{
|
||||
{"Ignore", true},
|
||||
{"ignore", true},
|
||||
{"fail", false},
|
||||
}
|
||||
for _, test := range tests {
|
||||
from := &yaml.Container{Failure: test.mode}
|
||||
if toIgnoreErr(from) != test.want {
|
||||
t.Errorf("unexpected ignore error for %s", test.mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_toPullPolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
mode string
|
||||
want engine.PullPolicy
|
||||
}{
|
||||
{"", engine.PullDefault},
|
||||
{"always", engine.PullAlways},
|
||||
{"if-not-exists", engine.PullIfNotExists},
|
||||
{"never", engine.PullNever},
|
||||
}
|
||||
for _, test := range tests {
|
||||
from := &yaml.Container{Pull: test.mode}
|
||||
if toPullPolicy(from) != test.want {
|
||||
t.Errorf("unexpected pull policy for %s", test.mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_toRunPolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
cond yaml.Condition
|
||||
want engine.RunPolicy
|
||||
}{
|
||||
{yaml.Condition{}, engine.RunOnSuccess},
|
||||
{yaml.Condition{Include: []string{"success"}}, engine.RunOnSuccess},
|
||||
{yaml.Condition{Include: []string{"failure"}}, engine.RunOnFailure},
|
||||
{yaml.Condition{Include: []string{"success", "failure"}}, engine.RunAlways},
|
||||
{yaml.Condition{Exclude: []string{"success", "failure"}}, engine.RunNever},
|
||||
}
|
||||
for _, test := range tests {
|
||||
from := &yaml.Container{When: yaml.Conditions{Status: test.cond}}
|
||||
if toRunPolicy(from) != test.want {
|
||||
t.Errorf("unexpected pull policy for incude: %s, exclude: %s", test.cond.Include, test.cond.Exclude)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_toPorts(t *testing.T) {
|
||||
from := &yaml.Container{
|
||||
Ports: []*yaml.Port{
|
||||
{
|
||||
Port: 80,
|
||||
Host: 8080,
|
||||
Protocol: "TCP",
|
||||
},
|
||||
{
|
||||
Port: 80,
|
||||
Host: 0,
|
||||
Protocol: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
a := toPorts(from)
|
||||
b := []*engine.Port{
|
||||
{
|
||||
Port: 80,
|
||||
Host: 8080,
|
||||
Protocol: "TCP",
|
||||
},
|
||||
{
|
||||
Port: 80,
|
||||
Host: 0,
|
||||
Protocol: "",
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(a, b); diff != "" {
|
||||
t.Errorf("Unexpected port conversion")
|
||||
t.Log(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_toResources(t *testing.T) {
|
||||
from := &yaml.Container{
|
||||
Resources: nil,
|
||||
}
|
||||
if toResources(from) != nil {
|
||||
t.Errorf("Expected nil resources")
|
||||
}
|
||||
|
||||
// test what happens when limits are defined
|
||||
// but reservations are nil.
|
||||
|
||||
from = &yaml.Container{
|
||||
Resources: &yaml.Resources{
|
||||
Limits: &yaml.ResourceObject{
|
||||
Memory: yaml.BytesSize(1000),
|
||||
},
|
||||
},
|
||||
}
|
||||
a := toResources(from)
|
||||
b := &engine.Resources{
|
||||
Limits: &engine.ResourceObject{
|
||||
Memory: 1000,
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(a, b); diff != "" {
|
||||
t.Errorf("Unexpected resource conversion")
|
||||
t.Log(diff)
|
||||
}
|
||||
|
||||
// test what happens when reservation and limits
|
||||
// are both provided.
|
||||
|
||||
from = &yaml.Container{
|
||||
Resources: &yaml.Resources{
|
||||
Limits: &yaml.ResourceObject{
|
||||
Memory: yaml.BytesSize(1000),
|
||||
},
|
||||
Requests: &yaml.ResourceObject{
|
||||
Memory: yaml.BytesSize(2000),
|
||||
},
|
||||
},
|
||||
}
|
||||
a = toResources(from)
|
||||
b = &engine.Resources{
|
||||
Limits: &engine.ResourceObject{
|
||||
Memory: 1000,
|
||||
},
|
||||
Requests: &engine.ResourceObject{
|
||||
Memory: 2000,
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(a, b); diff != "" {
|
||||
t.Errorf("Unexpected resource conversion")
|
||||
t.Log(diff)
|
||||
}
|
||||
}
|
37
yaml/compiler/dind.go
Normal file
37
yaml/compiler/dind.go
Normal file
@ -0,0 +1,37 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"github.com/drone/drone-yaml/yaml"
|
||||
"github.com/drone/drone-yaml/yaml/compiler/image"
|
||||
)
|
||||
|
||||
// DindFunc is a helper function that returns true
|
||||
// if a container image (specifically a plugin) is
|
||||
// a whitelisted dind container and should be executed
|
||||
// in privileged mode.
|
||||
func DindFunc(images []string) func(*yaml.Container) bool {
|
||||
return func(container *yaml.Container) bool {
|
||||
// privileged-by-default containers are only
|
||||
// enabled for plugins steps that do not define
|
||||
// commands, command, or entrypoint.
|
||||
if len(container.Commands) > 0 {
|
||||
return false
|
||||
}
|
||||
if len(container.Command) > 0 {
|
||||
return false
|
||||
}
|
||||
if len(container.Entrypoint) > 0 {
|
||||
return false
|
||||
}
|
||||
// if the container image matches any image
|
||||
// in the whitelist, return true.
|
||||
for _, img := range images {
|
||||
a := img
|
||||
b := container.Image
|
||||
if image.Match(a, b) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
63
yaml/compiler/dind_test.go
Normal file
63
yaml/compiler/dind_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-yaml/yaml"
|
||||
)
|
||||
|
||||
func TestDind(t *testing.T) {
|
||||
tests := []struct {
|
||||
container *yaml.Container
|
||||
privileged bool
|
||||
}{
|
||||
{
|
||||
container: &yaml.Container{Image: "plugins/docker"},
|
||||
privileged: true,
|
||||
},
|
||||
{
|
||||
container: &yaml.Container{Image: "plugins/docker:latest"},
|
||||
privileged: true,
|
||||
},
|
||||
{
|
||||
container: &yaml.Container{Image: "plugins/docker:1"},
|
||||
privileged: true,
|
||||
},
|
||||
// no match
|
||||
{
|
||||
container: &yaml.Container{Image: "golang"},
|
||||
privileged: false,
|
||||
},
|
||||
// dind containers cannot set entrypoint or command
|
||||
{
|
||||
container: &yaml.Container{
|
||||
Image: "plugins/docker",
|
||||
Command: []string{"docker run ..."},
|
||||
},
|
||||
privileged: false,
|
||||
},
|
||||
{
|
||||
container: &yaml.Container{
|
||||
Image: "plugins/docker",
|
||||
Entrypoint: []string{"docker run ..."},
|
||||
},
|
||||
privileged: false,
|
||||
},
|
||||
// dind containers cannot set commands
|
||||
{
|
||||
container: &yaml.Container{
|
||||
Image: "plugins/docker",
|
||||
Commands: []string{"docker run ..."},
|
||||
},
|
||||
privileged: false,
|
||||
},
|
||||
}
|
||||
for i, test := range tests {
|
||||
images := []string{"plugins/docker", "plugins/ecr"}
|
||||
privileged := DindFunc(images)(test.container)
|
||||
if privileged != test.privileged {
|
||||
t.Errorf("Want privileged %v at index %d", test.privileged, i)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
53
yaml/compiler/encode.go
Normal file
53
yaml/compiler/encode.go
Normal file
@ -0,0 +1,53 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
json "github.com/ghodss/yaml"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// helper funciton 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)
|
||||
}
|
59
yaml/compiler/encode_test.go
Normal file
59
yaml/compiler/encode_test.go
Normal file
@ -0,0 +1,59 @@
|
||||
package compiler
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
67
yaml/compiler/image/image.go
Normal file
67
yaml/compiler/image/image.go
Normal file
@ -0,0 +1,67 @@
|
||||
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
|
||||
}
|
295
yaml/compiler/image/image_test.go
Normal file
295
yaml/compiler/image/image_test.go
Normal file
@ -0,0 +1,295 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
36
yaml/compiler/internal/rand/rand.go
Normal file
36
yaml/compiler/internal/rand/rand.go
Normal file
@ -0,0 +1,36 @@
|
||||
package rand
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
)
|
||||
|
||||
var chars = []byte("abcdefghijklmnopqrstuvwxyz0123456789")
|
||||
|
||||
// random string length
|
||||
const length = 32
|
||||
|
||||
// String returns a string value.
|
||||
func String() string {
|
||||
clen := len(chars)
|
||||
maxrb := 255 - (256 % clen)
|
||||
b := make([]byte, length)
|
||||
r := make([]byte, length+(length/4)) // storage for random bytes.
|
||||
i := 0
|
||||
for {
|
||||
if _, err := rand.Read(r); err != nil {
|
||||
panic("rand: error reading random bytes")
|
||||
}
|
||||
for _, rb := range r {
|
||||
c := int(rb)
|
||||
if c > maxrb {
|
||||
// Skip this number to avoid modulo bias.
|
||||
continue
|
||||
}
|
||||
b[i] = chars[c%clen]
|
||||
i++
|
||||
if i == length {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
70
yaml/compiler/script_posix.go
Normal file
70
yaml/compiler/script_posix.go
Normal file
@ -0,0 +1,70 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/drone/drone-yaml/yaml"
|
||||
"github.com/drone/drone-yaml/yaml/compiler/internal/rand"
|
||||
)
|
||||
|
||||
func setupScript(spec *engine.Spec, dst *engine.Step, src *yaml.Container) {
|
||||
var buf bytes.Buffer
|
||||
for _, command := range src.Commands {
|
||||
escaped := fmt.Sprintf("%q", command)
|
||||
escaped = strings.Replace(escaped, "$", `\$`, -1)
|
||||
buf.WriteString(fmt.Sprintf(
|
||||
traceScript,
|
||||
escaped,
|
||||
command,
|
||||
))
|
||||
}
|
||||
script := fmt.Sprintf(
|
||||
buildScript,
|
||||
buf.String(),
|
||||
)
|
||||
spec.Files = append(spec.Files, &engine.File{
|
||||
Metadata: engine.Metadata{
|
||||
UID: rand.String(),
|
||||
Namespace: spec.Metadata.Namespace,
|
||||
Name: src.Name,
|
||||
},
|
||||
Data: []byte(script),
|
||||
})
|
||||
dst.Files = append(dst.Files, &engine.FileMount{
|
||||
Name: src.Name,
|
||||
Path: "/usr/drone/bin/init",
|
||||
Mode: 0777,
|
||||
})
|
||||
|
||||
dst.Docker.Command = []string{"/bin/sh"}
|
||||
dst.Docker.Args = []string{"/usr/drone/bin/init"}
|
||||
}
|
||||
|
||||
// buildScript is a helper script this is added to the build
|
||||
// to prepare the environment and execute the build commands.
|
||||
const buildScript = `
|
||||
if [ -n "$CI_NETRC_MACHINE" ]; then
|
||||
cat <<EOF > $HOME/.netrc
|
||||
machine $CI_NETRC_MACHINE
|
||||
login $CI_NETRC_USERNAME
|
||||
password $CI_NETRC_PASSWORD
|
||||
EOF
|
||||
chmod 0600 $HOME/.netrc
|
||||
fi
|
||||
unset CI_NETRC_USERNAME
|
||||
unset CI_NETRC_PASSWORD
|
||||
unset DRONE_NETRC_USERNAME
|
||||
unset DRONE_NETRC_PASSWORD
|
||||
set -e
|
||||
%s
|
||||
`
|
||||
|
||||
// traceScript is a helper script that is added to
|
||||
// the build script to trace a command.
|
||||
const traceScript = `
|
||||
echo + %s
|
||||
%s
|
||||
`
|
1
yaml/compiler/script_posix_test.go
Normal file
1
yaml/compiler/script_posix_test.go
Normal file
@ -0,0 +1 @@
|
||||
package compiler
|
58
yaml/compiler/script_win.go
Normal file
58
yaml/compiler/script_win.go
Normal file
@ -0,0 +1,58 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/drone/drone-yaml/yaml"
|
||||
)
|
||||
|
||||
func setupScriptWin(spec *engine.Spec, dst *engine.Step, src *yaml.Container) {
|
||||
var buf bytes.Buffer
|
||||
for _, command := range src.Commands {
|
||||
escaped := fmt.Sprintf("%q", command)
|
||||
escaped = strings.Replace(escaped, "$", `\$`, -1)
|
||||
buf.WriteString(fmt.Sprintf(
|
||||
traceScriptWin,
|
||||
escaped,
|
||||
command,
|
||||
))
|
||||
}
|
||||
script := fmt.Sprintf(
|
||||
buildScriptWin,
|
||||
buf.String(),
|
||||
)
|
||||
dst.Docker.Command = []string{"powershell", "-noprofile", "-noninteractive", "-command"}
|
||||
dst.Docker.Args = []string{"[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Env:CI_SCRIPT)) | iex"}
|
||||
dst.Envs["CI_SCRIPT"] = base64.StdEncoding.EncodeToString([]byte(script))
|
||||
dst.Envs["SHELL"] = "powershell.exe"
|
||||
}
|
||||
|
||||
// buildScriptWin is a helper script this is added to the build
|
||||
// to prepare the environment and execute the build commands.
|
||||
const buildScriptWin = `
|
||||
if ($Env:CI_NETRC_MACHINE) {
|
||||
@"
|
||||
machine $Env:CI_NETRC_MACHINE
|
||||
login $Env:CI_NETRC_USERNAME
|
||||
password $Env:CI_NETRC_PASSWORD
|
||||
"@ > (Join-Path $Env:USERPROFILE '_netrc');
|
||||
}
|
||||
[Environment]::SetEnvironmentVariable("CI_NETRC_USERNAME", $null);
|
||||
[Environment]::SetEnvironmentVariable("CI_NETRC_PASSWORD", $null);
|
||||
[Environment]::SetEnvironmentVariable("DRONE_NETRC_USERNAME", $null);
|
||||
[Environment]::SetEnvironmentVariable("DRONE_NETRC_PASSWORD", $null);
|
||||
[Environment]::SetEnvironmentVariable("CI_SCRIPT", $null);
|
||||
$ErrorActionPreference = 'Stop';
|
||||
%s
|
||||
`
|
||||
|
||||
// traceScriptWin is a helper script that is added to
|
||||
// the build script to trace a command.
|
||||
const traceScriptWin = `
|
||||
Write-Output ('+ %s');
|
||||
& %s; if ($LASTEXITCODE -ne 0) {exit $LASTEXITCODE}
|
||||
`
|
1
yaml/compiler/script_win_test.go
Normal file
1
yaml/compiler/script_win_test.go
Normal file
@ -0,0 +1 @@
|
||||
package compiler
|
37
yaml/compiler/skip.go
Normal file
37
yaml/compiler/skip.go
Normal file
@ -0,0 +1,37 @@
|
||||
package compiler
|
||||
|
||||
import "github.com/drone/drone-yaml/yaml"
|
||||
|
||||
// SkipData provides build metadata use to determine if a
|
||||
// pipeline step should be skipped.
|
||||
type SkipData struct {
|
||||
Branch string
|
||||
Event string
|
||||
Instance string
|
||||
Ref string
|
||||
Repo string
|
||||
Target string
|
||||
}
|
||||
|
||||
// SkipFunc returns a function that can be used to skip
|
||||
// individual pipeline steps based on build metadata.
|
||||
func SkipFunc(data SkipData) func(*yaml.Container) bool {
|
||||
return func(container *yaml.Container) bool {
|
||||
switch {
|
||||
case !container.When.Branch.Match(data.Branch):
|
||||
return true
|
||||
case !container.When.Event.Match(data.Event):
|
||||
return true
|
||||
case !container.When.Instance.Match(data.Instance):
|
||||
return true
|
||||
case !container.When.Ref.Match(data.Ref):
|
||||
return true
|
||||
case !container.When.Repo.Match(data.Repo):
|
||||
return true
|
||||
case !container.When.Target.Match(data.Target):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
105
yaml/compiler/skip_test.go
Normal file
105
yaml/compiler/skip_test.go
Normal file
@ -0,0 +1,105 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-yaml/yaml"
|
||||
)
|
||||
|
||||
func TestSkipFunc(t *testing.T) {
|
||||
tests := []struct {
|
||||
data SkipData
|
||||
when yaml.Conditions
|
||||
want bool
|
||||
}{
|
||||
//
|
||||
// test branch conditions
|
||||
//
|
||||
{
|
||||
data: SkipData{Branch: "master"},
|
||||
when: yaml.Conditions{Branch: yaml.Condition{Include: []string{"master"}}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
data: SkipData{Branch: "master"},
|
||||
when: yaml.Conditions{Branch: yaml.Condition{Exclude: []string{"master"}}},
|
||||
want: true,
|
||||
},
|
||||
//
|
||||
// test event conditions
|
||||
//
|
||||
{
|
||||
data: SkipData{Event: "push"},
|
||||
when: yaml.Conditions{Event: yaml.Condition{Include: []string{"push"}}},
|
||||
want: false,
|
||||
},
|
||||
|
||||
{
|
||||
data: SkipData{Event: "push"},
|
||||
when: yaml.Conditions{Event: yaml.Condition{Exclude: []string{"push"}}},
|
||||
want: true,
|
||||
},
|
||||
//
|
||||
// test instance conditions
|
||||
//
|
||||
{
|
||||
data: SkipData{Instance: "drone.company.com"},
|
||||
when: yaml.Conditions{Instance: yaml.Condition{Include: []string{"drone.company.com"}}},
|
||||
want: false,
|
||||
},
|
||||
|
||||
{
|
||||
data: SkipData{Instance: "drone.company.com"},
|
||||
when: yaml.Conditions{Instance: yaml.Condition{Exclude: []string{"drone.company.com"}}},
|
||||
want: true,
|
||||
},
|
||||
//
|
||||
// test ref conditions
|
||||
//
|
||||
{
|
||||
data: SkipData{Ref: "refs/heads/master"},
|
||||
when: yaml.Conditions{Ref: yaml.Condition{Include: []string{"refs/heads/*"}}},
|
||||
want: false,
|
||||
},
|
||||
|
||||
{
|
||||
data: SkipData{Ref: "refs/heads/master"},
|
||||
when: yaml.Conditions{Ref: yaml.Condition{Exclude: []string{"refs/heads/*"}}},
|
||||
want: true,
|
||||
},
|
||||
//
|
||||
// test repo conditions
|
||||
//
|
||||
{
|
||||
data: SkipData{Repo: "octocat/hello-world"},
|
||||
when: yaml.Conditions{Repo: yaml.Condition{Include: []string{"octocat/hello-world"}}},
|
||||
want: false,
|
||||
},
|
||||
|
||||
{
|
||||
data: SkipData{Repo: "octocat/hello-world"},
|
||||
when: yaml.Conditions{Repo: yaml.Condition{Exclude: []string{"octocat/hello-world"}}},
|
||||
want: true,
|
||||
},
|
||||
//
|
||||
// test target conditions
|
||||
//
|
||||
{
|
||||
data: SkipData{Target: "prod"},
|
||||
when: yaml.Conditions{Target: yaml.Condition{Include: []string{"prod"}}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
data: SkipData{Target: "prod"},
|
||||
when: yaml.Conditions{Target: yaml.Condition{Exclude: []string{"prod"}}},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for i, test := range tests {
|
||||
container := &yaml.Container{When: test.when}
|
||||
got := SkipFunc(test.data)(container)
|
||||
if got != test.want {
|
||||
t.Errorf("Want skip %v at index %d", test.want, i)
|
||||
}
|
||||
}
|
||||
}
|
180
yaml/compiler/step.go
Normal file
180
yaml/compiler/step.go
Normal file
@ -0,0 +1,180 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/drone/drone-yaml/yaml"
|
||||
"github.com/drone/drone-yaml/yaml/compiler/image"
|
||||
"github.com/drone/drone-yaml/yaml/compiler/internal/rand"
|
||||
)
|
||||
|
||||
func createStep(spec *engine.Spec, src *yaml.Container) *engine.Step {
|
||||
dst := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: rand.String(),
|
||||
Name: src.Name,
|
||||
Namespace: spec.Metadata.Namespace,
|
||||
Labels: map[string]string{
|
||||
"io.drone.step.name": src.Name,
|
||||
},
|
||||
},
|
||||
Detach: src.Detach,
|
||||
DependsOn: src.DependsOn,
|
||||
Devices: nil,
|
||||
Docker: &engine.DockerStep{
|
||||
Args: src.Command,
|
||||
Command: src.Entrypoint,
|
||||
DNS: src.DNS,
|
||||
DNSSearch: src.DNSSearch,
|
||||
ExtraHosts: src.ExtraHosts,
|
||||
Image: image.Expand(src.Image),
|
||||
Networks: nil, // set in compiler.go
|
||||
Ports: toPorts(src),
|
||||
Privileged: src.Privileged,
|
||||
PullPolicy: toPullPolicy(src),
|
||||
},
|
||||
Envs: map[string]string{},
|
||||
Files: nil, // set below
|
||||
IgnoreErr: toIgnoreErr(src),
|
||||
IgnoreStderr: false,
|
||||
IgnoreStdout: false,
|
||||
Resources: toResources(src),
|
||||
RunPolicy: toRunPolicy(src),
|
||||
Secrets: nil, // set below
|
||||
Volumes: nil, // set below
|
||||
WorkingDir: "", // set in compiler.go
|
||||
}
|
||||
|
||||
// if the user is running a service container with
|
||||
// no custom commands, we should revert back to the
|
||||
// user-defined working directory, which may be empty.
|
||||
if dst.Detach && len(src.Commands) == 0 {
|
||||
dst.WorkingDir = src.WorkingDir
|
||||
}
|
||||
|
||||
// appends the volumes to the container def.
|
||||
for _, vol := range src.Volumes {
|
||||
// the user should never be able to directly
|
||||
// mount the docker socket. This should be
|
||||
// restricted by the linter, but we place this
|
||||
// check here just to be safe.
|
||||
if vol.Name == "_docker_socket" {
|
||||
continue
|
||||
}
|
||||
mount := &engine.VolumeMount{
|
||||
Name: vol.Name,
|
||||
Path: vol.MountPath,
|
||||
}
|
||||
dst.Volumes = append(dst.Volumes, mount)
|
||||
}
|
||||
|
||||
// appends the environment variables to the
|
||||
// container definition.
|
||||
for key, value := range src.Environment {
|
||||
if value.Secret != "" {
|
||||
sec := &engine.SecretVar{
|
||||
Name: value.Secret,
|
||||
Env: key,
|
||||
}
|
||||
dst.Secrets = append(dst.Secrets, sec)
|
||||
} else {
|
||||
dst.Envs[key] = value.Value
|
||||
}
|
||||
}
|
||||
|
||||
// appends the settings variables to the
|
||||
// container definition.
|
||||
for key, value := range src.Settings {
|
||||
// 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 != "" {
|
||||
sec := &engine.SecretVar{
|
||||
Name: value.Secret,
|
||||
Env: key,
|
||||
}
|
||||
dst.Secrets = append(dst.Secrets, sec)
|
||||
} else {
|
||||
// else if the setting parameter is opaque
|
||||
// we inject as a string-encoded environment
|
||||
// variable.
|
||||
dst.Envs[key] = 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)
|
||||
}
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
func createBuildStep(spec *engine.Spec, src *yaml.Container) *engine.Step {
|
||||
dst := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: rand.String(),
|
||||
Name: src.Name,
|
||||
Namespace: spec.Metadata.Namespace,
|
||||
Labels: map[string]string{
|
||||
"io.drone.step.name": src.Name,
|
||||
},
|
||||
},
|
||||
Detach: src.Detach,
|
||||
DependsOn: src.DependsOn,
|
||||
Devices: nil,
|
||||
Docker: &engine.DockerStep{
|
||||
Args: []string{"--build"},
|
||||
DNS: src.DNS,
|
||||
Image: "drone/docker",
|
||||
PullPolicy: engine.PullIfNotExists,
|
||||
},
|
||||
Envs: map[string]string{},
|
||||
IgnoreErr: toIgnoreErr(src),
|
||||
IgnoreStderr: false,
|
||||
IgnoreStdout: false,
|
||||
Resources: toResources(src),
|
||||
RunPolicy: toRunPolicy(src),
|
||||
}
|
||||
|
||||
// if v := src.Build.Args; len(v) > 0 {
|
||||
// dst.Envs["DOCKER_BUILD_ARGS"] = strings.Join(v, ",")
|
||||
// }
|
||||
if v := src.Build.CacheFrom; len(v) > 0 {
|
||||
dst.Envs["DOCKER_BUILD_CACHE_FROM"] = strings.Join(v, ",")
|
||||
}
|
||||
// if len(src.Build.Labels) > 0 {
|
||||
// dst.Envs["DOCKER_BUILD_LABELS"] = strings.Join(v, ",")
|
||||
// }
|
||||
if v := src.Build.Dockerfile; v != "" {
|
||||
dst.Envs["DOCKER_BUILD_DOCKERFILE"] = v
|
||||
|
||||
}
|
||||
if v := src.Build.Context; v != "" {
|
||||
dst.Envs["DOCKER_BUILD_CONTEXT"] = v
|
||||
}
|
||||
if v := src.Build.Image; v != "" {
|
||||
alias := image.Trim(v) + ":" + dst.Metadata.UID
|
||||
dst.Envs["DOCKER_BUILD_IMAGE"] = image.Expand(v)
|
||||
dst.Envs["DOCKER_BUILD_IMAGE_ALIAS"] = image.Expand(alias)
|
||||
}
|
||||
|
||||
dst.Volumes = append(dst.Volumes, &engine.VolumeMount{
|
||||
Name: "_docker_socket",
|
||||
Path: "/var/run/docker.sock",
|
||||
})
|
||||
|
||||
return dst
|
||||
}
|
29
yaml/compiler/transform/auths.go
Normal file
29
yaml/compiler/transform/auths.go
Normal file
@ -0,0 +1,29 @@
|
||||
package transform
|
||||
|
||||
import "github.com/drone/drone-runtime/engine"
|
||||
|
||||
// WithAuths is a transform function that adds a set
|
||||
// of global registry credentials to the container.
|
||||
func WithAuths(auths []*engine.DockerAuth) func(*engine.Spec) {
|
||||
return func(spec *engine.Spec) {
|
||||
for _, auth := range auths {
|
||||
spec.Docker.Auths = append(spec.Docker.Auths, auth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AuthsFunc is a callback function used to request
|
||||
// registry credentials to pull private images.
|
||||
type AuthsFunc func() []*engine.DockerAuth
|
||||
|
||||
// WithAuthsFunc is a transform function that provides
|
||||
// the sepcification with registry authentication
|
||||
// credentials via a callback function.
|
||||
func WithAuthsFunc(f AuthsFunc) func(*engine.Spec) {
|
||||
return func(spec *engine.Spec) {
|
||||
auths := f()
|
||||
if len(auths) != 0 {
|
||||
spec.Docker.Auths = append(spec.Docker.Auths, auths...)
|
||||
}
|
||||
}
|
||||
}
|
49
yaml/compiler/transform/auths_test.go
Normal file
49
yaml/compiler/transform/auths_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestWithAuths(t *testing.T) {
|
||||
spec := &engine.Spec{
|
||||
Steps: []*engine.Step{},
|
||||
Docker: &engine.DockerConfig{},
|
||||
}
|
||||
auths := []*engine.DockerAuth{
|
||||
{
|
||||
Address: "docker.io",
|
||||
Username: "octocat",
|
||||
Password: "correct-horse-battery-staple",
|
||||
},
|
||||
}
|
||||
WithAuths(auths)(spec)
|
||||
if diff := cmp.Diff(auths, spec.Docker.Auths); diff != "" {
|
||||
t.Errorf("Unexpected auths transform")
|
||||
t.Log(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithAuthsFunc(t *testing.T) {
|
||||
spec := &engine.Spec{
|
||||
Steps: []*engine.Step{},
|
||||
Docker: &engine.DockerConfig{},
|
||||
}
|
||||
auths := []*engine.DockerAuth{
|
||||
{
|
||||
Address: "docker.io",
|
||||
Username: "octocat",
|
||||
Password: "correct-horse-battery-staple",
|
||||
},
|
||||
}
|
||||
fn := func() []*engine.DockerAuth {
|
||||
return auths
|
||||
}
|
||||
WithAuthsFunc(fn)(spec)
|
||||
if diff := cmp.Diff(auths, spec.Docker.Auths); diff != "" {
|
||||
t.Errorf("Unexpected auths transform")
|
||||
t.Log(diff)
|
||||
}
|
||||
}
|
13
yaml/compiler/transform/combine.go
Normal file
13
yaml/compiler/transform/combine.go
Normal file
@ -0,0 +1,13 @@
|
||||
package transform
|
||||
|
||||
import "github.com/drone/drone-runtime/engine"
|
||||
|
||||
// Combine is a transform function that combines
|
||||
// one or many transform functions.
|
||||
func Combine(fns ...func(*engine.Spec)) func(*engine.Spec) {
|
||||
return func(spec *engine.Spec) {
|
||||
for _, fn := range fns {
|
||||
fn(spec)
|
||||
}
|
||||
}
|
||||
}
|
33
yaml/compiler/transform/combine_test.go
Normal file
33
yaml/compiler/transform/combine_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestCombine(t *testing.T) {
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "1",
|
||||
Name: "build",
|
||||
},
|
||||
Envs: map[string]string{},
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
Combine(
|
||||
WithEnviron(map[string]string{"GOOS": "linux"}),
|
||||
WithEnviron(map[string]string{"GOARCH": "amd64"}),
|
||||
)(spec)
|
||||
want := map[string]string{
|
||||
"GOOS": "linux",
|
||||
"GOARCH": "amd64",
|
||||
}
|
||||
if diff := cmp.Diff(want, step.Envs); diff != "" {
|
||||
t.Errorf("Unexpected transform")
|
||||
t.Log(diff)
|
||||
}
|
||||
}
|
15
yaml/compiler/transform/env.go
Normal file
15
yaml/compiler/transform/env.go
Normal file
@ -0,0 +1,15 @@
|
||||
package transform
|
||||
|
||||
import "github.com/drone/drone-runtime/engine"
|
||||
|
||||
// WithEnviron is a transform function that adds a set
|
||||
// of environment variables to each container.
|
||||
func WithEnviron(envs map[string]string) func(*engine.Spec) {
|
||||
return func(spec *engine.Spec) {
|
||||
for key, value := range envs {
|
||||
for _, step := range spec.Steps {
|
||||
step.Envs[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
yaml/compiler/transform/env_test.go
Normal file
30
yaml/compiler/transform/env_test.go
Normal file
@ -0,0 +1,30 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestWithEnviron(t *testing.T) {
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "1",
|
||||
Name: "build",
|
||||
},
|
||||
Envs: map[string]string{},
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
envs := map[string]string{
|
||||
"GOOS": "linux",
|
||||
"GOARCH": "amd64",
|
||||
}
|
||||
WithEnviron(envs)(spec)
|
||||
if diff := cmp.Diff(envs, step.Envs); diff != "" {
|
||||
t.Errorf("Unexpected transform")
|
||||
t.Log(diff)
|
||||
}
|
||||
}
|
72
yaml/compiler/transform/filter.go
Normal file
72
yaml/compiler/transform/filter.go
Normal file
@ -0,0 +1,72 @@
|
||||
package transform
|
||||
|
||||
import "github.com/drone/drone-runtime/engine"
|
||||
|
||||
// Include is a transform function that limits the
|
||||
// pipeline execution to a whitelist of named steps.
|
||||
func Include(names []string) func(*engine.Spec) {
|
||||
set := map[string]struct{}{}
|
||||
for _, name := range names {
|
||||
set[name] = struct{}{}
|
||||
}
|
||||
return func(spec *engine.Spec) {
|
||||
if len(names) == 0 {
|
||||
return
|
||||
}
|
||||
for _, step := range spec.Steps {
|
||||
if step.Metadata.Name == "clone" {
|
||||
continue
|
||||
}
|
||||
_, ok := set[step.Metadata.Name]
|
||||
if !ok {
|
||||
// if the step is not included in the
|
||||
// whitelist the run policy is set to never.
|
||||
step.RunPolicy = engine.RunNever
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exclude is a transform function that limits the
|
||||
// pipeline execution to a whitelist of named steps.
|
||||
func Exclude(names []string) func(*engine.Spec) {
|
||||
set := map[string]struct{}{}
|
||||
for _, name := range names {
|
||||
set[name] = struct{}{}
|
||||
}
|
||||
return func(spec *engine.Spec) {
|
||||
if len(names) == 0 {
|
||||
return
|
||||
}
|
||||
for _, step := range spec.Steps {
|
||||
if step.Metadata.Name == "clone" {
|
||||
continue
|
||||
}
|
||||
_, ok := set[step.Metadata.Name]
|
||||
if ok {
|
||||
// if the step is included in the blacklist
|
||||
// the run policy is set to never.
|
||||
step.RunPolicy = engine.RunNever
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ResumeAt is a transform function that modifies the
|
||||
// exuction to resume at a named step.
|
||||
func ResumeAt(name string) func(*engine.Spec) {
|
||||
return func(spec *engine.Spec) {
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
for _, step := range spec.Steps {
|
||||
if step.Metadata.Name == name {
|
||||
return
|
||||
}
|
||||
if step.Metadata.Name == "clone" {
|
||||
continue
|
||||
}
|
||||
step.RunPolicy = engine.RunNever
|
||||
}
|
||||
}
|
||||
}
|
139
yaml/compiler/transform/filter_test.go
Normal file
139
yaml/compiler/transform/filter_test.go
Normal file
@ -0,0 +1,139 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
)
|
||||
|
||||
func TestInclude(t *testing.T) {
|
||||
step1 := &engine.Step{
|
||||
Metadata: engine.Metadata{Name: "clone"},
|
||||
RunPolicy: engine.RunOnSuccess,
|
||||
}
|
||||
step2 := &engine.Step{
|
||||
Metadata: engine.Metadata{Name: "build"},
|
||||
RunPolicy: engine.RunOnSuccess,
|
||||
}
|
||||
step3 := &engine.Step{
|
||||
Metadata: engine.Metadata{Name: "test"},
|
||||
RunPolicy: engine.RunOnSuccess,
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Metadata: engine.Metadata{},
|
||||
Steps: []*engine.Step{step1, step2, step3},
|
||||
}
|
||||
Include([]string{"test"})(spec)
|
||||
if got, want := step1.RunPolicy, engine.RunOnSuccess; got != want {
|
||||
t.Errorf("Want run policy %s got %s", want, got)
|
||||
}
|
||||
if got, want := step2.RunPolicy, engine.RunNever; got != want {
|
||||
t.Errorf("Want run policy %s got %s", want, got)
|
||||
}
|
||||
if got, want := step3.RunPolicy, engine.RunOnSuccess; got != want {
|
||||
t.Errorf("Want run policy %s got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInclude_Empty(t *testing.T) {
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{},
|
||||
RunPolicy: engine.RunOnSuccess,
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Metadata: engine.Metadata{},
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
Include(nil)(spec)
|
||||
if got, want := step.RunPolicy, engine.RunOnSuccess; got != want {
|
||||
t.Errorf("Want run policy %s got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExclude(t *testing.T) {
|
||||
step1 := &engine.Step{
|
||||
Metadata: engine.Metadata{Name: "clone"},
|
||||
RunPolicy: engine.RunOnSuccess,
|
||||
}
|
||||
step2 := &engine.Step{
|
||||
Metadata: engine.Metadata{Name: "build"},
|
||||
RunPolicy: engine.RunOnSuccess,
|
||||
}
|
||||
step3 := &engine.Step{
|
||||
Metadata: engine.Metadata{Name: "test"},
|
||||
RunPolicy: engine.RunOnSuccess,
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Metadata: engine.Metadata{},
|
||||
Steps: []*engine.Step{step1, step2, step3},
|
||||
}
|
||||
Exclude([]string{"test"})(spec)
|
||||
if got, want := step1.RunPolicy, engine.RunOnSuccess; got != want {
|
||||
t.Errorf("Want run policy %s got %s", want, got)
|
||||
}
|
||||
if got, want := step2.RunPolicy, engine.RunOnSuccess; got != want {
|
||||
t.Errorf("Want run policy %s got %s", want, got)
|
||||
}
|
||||
if got, want := step3.RunPolicy, engine.RunNever; got != want {
|
||||
t.Errorf("Want run policy %s got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExclude_Empty(t *testing.T) {
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{},
|
||||
RunPolicy: engine.RunOnSuccess,
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Metadata: engine.Metadata{},
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
Exclude(nil)(spec)
|
||||
if got, want := step.RunPolicy, engine.RunOnSuccess; got != want {
|
||||
t.Errorf("Want run policy %s got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResumeAt(t *testing.T) {
|
||||
step1 := &engine.Step{
|
||||
Metadata: engine.Metadata{Name: "clone"},
|
||||
RunPolicy: engine.RunOnSuccess,
|
||||
}
|
||||
step2 := &engine.Step{
|
||||
Metadata: engine.Metadata{Name: "build"},
|
||||
RunPolicy: engine.RunOnSuccess,
|
||||
}
|
||||
step3 := &engine.Step{
|
||||
Metadata: engine.Metadata{Name: "test"},
|
||||
RunPolicy: engine.RunOnSuccess,
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Metadata: engine.Metadata{},
|
||||
Steps: []*engine.Step{step1, step2, step3},
|
||||
}
|
||||
ResumeAt("test")(spec)
|
||||
if got, want := step1.RunPolicy, engine.RunOnSuccess; got != want {
|
||||
t.Errorf("Want run policy %s got %s", want, got)
|
||||
}
|
||||
if got, want := step2.RunPolicy, engine.RunNever; got != want {
|
||||
t.Errorf("Want run policy %s got %s", want, got)
|
||||
}
|
||||
if got, want := step3.RunPolicy, engine.RunOnSuccess; got != want {
|
||||
t.Errorf("Want run policy %s got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResume_Empty(t *testing.T) {
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{},
|
||||
RunPolicy: engine.RunOnSuccess,
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Metadata: engine.Metadata{},
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
ResumeAt("")(spec)
|
||||
if got, want := step.RunPolicy, engine.RunOnSuccess; got != want {
|
||||
t.Errorf("Want run policy %s got %s", want, got)
|
||||
}
|
||||
}
|
23
yaml/compiler/transform/labels.go
Normal file
23
yaml/compiler/transform/labels.go
Normal file
@ -0,0 +1,23 @@
|
||||
package transform
|
||||
|
||||
import "github.com/drone/drone-runtime/engine"
|
||||
|
||||
// WithLables is a transform function that adds a set
|
||||
// of labels to each resource.
|
||||
func WithLables(labels map[string]string) func(*engine.Spec) {
|
||||
return func(spec *engine.Spec) {
|
||||
for k, v := range labels {
|
||||
spec.Metadata.Labels[k] = v
|
||||
}
|
||||
for _, resource := range spec.Docker.Volumes {
|
||||
for k, v := range labels {
|
||||
resource.Metadata.Labels[k] = v
|
||||
}
|
||||
}
|
||||
for _, resource := range spec.Steps {
|
||||
for k, v := range labels {
|
||||
resource.Metadata.Labels[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
yaml/compiler/transform/labels_test.go
Normal file
48
yaml/compiler/transform/labels_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestWithLabels(t *testing.T) {
|
||||
volume := &engine.Volume{
|
||||
Metadata: engine.Metadata{
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
}
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
Envs: map[string]string{},
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Metadata: engine.Metadata{
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
Steps: []*engine.Step{step},
|
||||
Docker: &engine.DockerConfig{
|
||||
Volumes: []*engine.Volume{volume},
|
||||
},
|
||||
}
|
||||
labels := map[string]string{
|
||||
"io.drone.build.number": "1",
|
||||
"io.drone.build.event": "push",
|
||||
}
|
||||
WithLables(labels)(spec)
|
||||
if diff := cmp.Diff(labels, spec.Metadata.Labels); diff != "" {
|
||||
t.Errorf("Unexpected spec labels")
|
||||
t.Log(diff)
|
||||
}
|
||||
if diff := cmp.Diff(labels, step.Metadata.Labels); diff != "" {
|
||||
t.Errorf("Unexpected step labels")
|
||||
t.Log(diff)
|
||||
}
|
||||
if diff := cmp.Diff(labels, volume.Metadata.Labels); diff != "" {
|
||||
t.Errorf("Unexpected volume labels")
|
||||
t.Log(diff)
|
||||
}
|
||||
}
|
29
yaml/compiler/transform/limits.go
Normal file
29
yaml/compiler/transform/limits.go
Normal file
@ -0,0 +1,29 @@
|
||||
package transform
|
||||
|
||||
import "github.com/drone/drone-runtime/engine"
|
||||
// WithLimits is a transform function that applies
|
||||
// resource limits to the container processes.
|
||||
func WithLimits(memlimit int64, cpulimit int64) func(*engine.Spec) {
|
||||
return func(spec *engine.Spec) {
|
||||
// if no limits are defined exit immediately.
|
||||
if memlimit == 0 && cpulimit == 0 {
|
||||
return
|
||||
}
|
||||
// otherwise apply the resource limits to every
|
||||
// step in the runtime spec.
|
||||
for _, step := range spec.Steps {
|
||||
if step.Resources == nil {
|
||||
step.Resources = &engine.Resources{}
|
||||
}
|
||||
if step.Resources.Limits == nil {
|
||||
step.Resources.Limits = &engine.ResourceObject{}
|
||||
}
|
||||
if memlimit != 0 {
|
||||
step.Resources.Limits.Memory = memlimit
|
||||
}
|
||||
if cpulimit != 0 {
|
||||
step.Resources.Limits.CPU = cpulimit * 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
84
yaml/compiler/transform/limits_test.go
Normal file
84
yaml/compiler/transform/limits_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
)
|
||||
|
||||
func TestWithLimits(t *testing.T) {
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "1",
|
||||
Name: "build",
|
||||
},
|
||||
Docker: &engine.DockerStep{},
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
WithLimits(1, 2)(spec)
|
||||
if got, want := step.Resources.Limits.Memory, int64(1); got != want {
|
||||
t.Errorf("Want memory limit %v, got %v", want, got)
|
||||
}
|
||||
if got, want := step.Resources.Limits.CPU, int64(2000); got != want {
|
||||
t.Errorf("Want cpu limit %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithMemory(t *testing.T) {
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "1",
|
||||
Name: "build",
|
||||
},
|
||||
Docker: &engine.DockerStep{},
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
WithLimits(1, 0)(spec)
|
||||
if got, want := step.Resources.Limits.Memory, int64(1); got != want {
|
||||
t.Errorf("Want memory limit %v, got %v", want, got)
|
||||
}
|
||||
if got, want := step.Resources.Limits.CPU, int64(0); got != want {
|
||||
t.Errorf("Want cpu limit %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithCPU(t *testing.T) {
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "1",
|
||||
Name: "build",
|
||||
},
|
||||
Docker: &engine.DockerStep{},
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
WithLimits(0, 3)(spec)
|
||||
if got, want := step.Resources.Limits.Memory, int64(0); got != want {
|
||||
t.Errorf("Want memory limit %v, got %v", want, got)
|
||||
}
|
||||
if got, want := step.Resources.Limits.CPU, int64(3000); got != want {
|
||||
t.Errorf("Want cpu limit %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithNoLimits(t *testing.T) {
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "1",
|
||||
Name: "build",
|
||||
},
|
||||
Docker: &engine.DockerStep{},
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
WithLimits(0, 0)(spec)
|
||||
if step.Resources != nil {
|
||||
t.Errorf("Expect no limits applied")
|
||||
}
|
||||
}
|
73
yaml/compiler/transform/netrc.go
Normal file
73
yaml/compiler/transform/netrc.go
Normal file
@ -0,0 +1,73 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/drone/drone-yaml/yaml/compiler/internal/rand"
|
||||
)
|
||||
|
||||
const (
|
||||
netrcName = ".netrc"
|
||||
netrcPath = "/var/run/drone/.netrc"
|
||||
netrcMode = 0777
|
||||
)
|
||||
|
||||
const disableNetrcMount = true
|
||||
|
||||
// WithNetrc is a helper function that creates a netrc file
|
||||
// and mounts the file to all container steps.
|
||||
func WithNetrc(machine, username, password string) func(*engine.Spec) {
|
||||
return func(spec *engine.Spec) {
|
||||
if username == "" || password == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(bradrydzewski) temporarily disable mounting
|
||||
// the netrc file due to issues with kubernetes
|
||||
// compatibility.
|
||||
if disableNetrcMount == false {
|
||||
// Currently file mounts don't seem to work in Windows so environment
|
||||
// variables are used instead
|
||||
// FIXME: https://github.com/drone/drone-yaml/issues/20
|
||||
if spec.Platform.OS != "windows" {
|
||||
netrc := generateNetrc(machine, username, password)
|
||||
spec.Files = append(spec.Files, &engine.File{
|
||||
Metadata: engine.Metadata{
|
||||
UID: rand.String(),
|
||||
Name: netrcName,
|
||||
Namespace: spec.Metadata.Namespace,
|
||||
},
|
||||
Data: []byte(netrc),
|
||||
})
|
||||
for _, step := range spec.Steps {
|
||||
step.Files = append(step.Files, &engine.FileMount{
|
||||
Name: netrcName,
|
||||
Path: netrcPath,
|
||||
Mode: netrcMode,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(bradrydzewski) these should only be injected
|
||||
// if the file is not mounted, if OS == Windows.
|
||||
for _, step := range spec.Steps {
|
||||
if step.Envs == nil {
|
||||
step.Envs = map[string]string{}
|
||||
}
|
||||
step.Envs["CI_NETRC_MACHINE"] = machine
|
||||
step.Envs["CI_NETRC_USERNAME"] = username
|
||||
step.Envs["CI_NETRC_PASSWORD"] = password
|
||||
|
||||
step.Envs["DRONE_NETRC_MACHINE"] = machine
|
||||
step.Envs["DRONE_NETRC_USERNAME"] = username
|
||||
step.Envs["DRONE_NETRC_PASSWORD"] = password
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateNetrc(machine, username, password string) string {
|
||||
return fmt.Sprintf("machine %s login %s password %s",
|
||||
machine, username, password)
|
||||
}
|
78
yaml/compiler/transform/netrc_test.go
Normal file
78
yaml/compiler/transform/netrc_test.go
Normal file
@ -0,0 +1,78 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
)
|
||||
|
||||
var ignoreMetadata = cmpopts.IgnoreFields(
|
||||
engine.Metadata{}, "UID")
|
||||
|
||||
func TestWithNetrc(t *testing.T) {
|
||||
if true {
|
||||
t.Skipf("mounting the netrc is temporarily disabled")
|
||||
return
|
||||
}
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "1",
|
||||
Name: "build",
|
||||
},
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "acdj0yjqv7uh5hidveg0ggr42x8oj67b",
|
||||
Namespace: "pivqfthg1c9hy83ylht1sxx4nygjc7tk",
|
||||
},
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
WithNetrc("@machine", "@username", "@password")(spec)
|
||||
if len(step.Files) == 0 {
|
||||
t.Errorf("File mount not added to step")
|
||||
return
|
||||
}
|
||||
if len(spec.Files) == 0 {
|
||||
t.Errorf("File not declared in spec")
|
||||
return
|
||||
}
|
||||
file := &engine.File{
|
||||
Metadata: engine.Metadata{
|
||||
Name: ".netrc",
|
||||
Namespace: "pivqfthg1c9hy83ylht1sxx4nygjc7tk",
|
||||
},
|
||||
Data: []byte("machine @machine login @username password @password"),
|
||||
}
|
||||
if diff := cmp.Diff(file, spec.Files[0], ignoreMetadata); diff != "" {
|
||||
t.Errorf("Unexpected file declaration")
|
||||
t.Log(diff)
|
||||
}
|
||||
|
||||
fileMount := &engine.FileMount{Name: ".netrc", Path: "/root/.netrc", Mode: 0600}
|
||||
if diff := cmp.Diff(fileMount, step.Files[0], ignoreMetadata); diff != "" {
|
||||
t.Errorf("Unexpected file mount")
|
||||
t.Log(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithEmptyNetrc(t *testing.T) {
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "1",
|
||||
Name: "build",
|
||||
},
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
WithNetrc("@machine", "", "")(spec)
|
||||
if len(spec.Files) != 0 {
|
||||
t.Errorf("Unexpected file declaration")
|
||||
}
|
||||
if len(step.Files) != 0 {
|
||||
t.Errorf("Unexpected file mount")
|
||||
}
|
||||
}
|
14
yaml/compiler/transform/network.go
Normal file
14
yaml/compiler/transform/network.go
Normal file
@ -0,0 +1,14 @@
|
||||
package transform
|
||||
|
||||
import "github.com/drone/drone-runtime/engine"
|
||||
|
||||
// WithNetworks is a transform function that attaches a
|
||||
// list of user-defined Docker networks to each step.
|
||||
func WithNetworks(networks []string) func(*engine.Spec) {
|
||||
return func(spec *engine.Spec) {
|
||||
for _, step := range spec.Steps {
|
||||
step.Docker.Networks = append(
|
||||
step.Docker.Networks, networks...)
|
||||
}
|
||||
}
|
||||
}
|
29
yaml/compiler/transform/network_test.go
Normal file
29
yaml/compiler/transform/network_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestWithNetwork(t *testing.T) {
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "1",
|
||||
Name: "build",
|
||||
},
|
||||
Docker: &engine.DockerStep{
|
||||
Networks: nil,
|
||||
},
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
nets := []string{"a", "b"}
|
||||
WithNetworks(nets)(spec)
|
||||
if diff := cmp.Diff(nets, step.Docker.Networks); diff != "" {
|
||||
t.Errorf("Unexpected transform")
|
||||
t.Log(diff)
|
||||
}
|
||||
}
|
39
yaml/compiler/transform/proxy.go
Normal file
39
yaml/compiler/transform/proxy.go
Normal file
@ -0,0 +1,39 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
)
|
||||
|
||||
// WithProxy is a transform function that adds the
|
||||
// http_proxy environment variables to every container.
|
||||
func WithProxy() func(*engine.Spec) {
|
||||
environ := map[string]string{}
|
||||
if value := getenv("no_proxy"); value != "" {
|
||||
environ["no_proxy"] = value
|
||||
environ["NO_PROXY"] = value
|
||||
}
|
||||
if value := getenv("http_proxy"); value != "" {
|
||||
environ["http_proxy"] = value
|
||||
environ["HTTP_PROXY"] = value
|
||||
}
|
||||
if value := getenv("https_proxy"); value != "" {
|
||||
environ["https_proxy"] = value
|
||||
environ["HTTPS_PROXY"] = value
|
||||
}
|
||||
return WithEnviron(environ)
|
||||
}
|
||||
|
||||
func getenv(name string) (value string) {
|
||||
name = strings.ToUpper(name)
|
||||
if value := os.Getenv(name); value != "" {
|
||||
return value
|
||||
}
|
||||
name = strings.ToLower(name)
|
||||
if value := os.Getenv(name); value != "" {
|
||||
return value
|
||||
}
|
||||
return
|
||||
}
|
52
yaml/compiler/transform/proxy_test.go
Normal file
52
yaml/compiler/transform/proxy_test.go
Normal file
@ -0,0 +1,52 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
)
|
||||
|
||||
func TestWithProxy(t *testing.T) {
|
||||
var (
|
||||
noProxy = getenv("no_proxy")
|
||||
httpProxy = getenv("https_proxy")
|
||||
httpsProxy = getenv("https_proxy")
|
||||
)
|
||||
defer func() {
|
||||
os.Setenv("no_proxy", noProxy)
|
||||
os.Setenv("NO_PROXY", noProxy)
|
||||
os.Setenv("http_proxy", httpProxy)
|
||||
os.Setenv("HTTP_PROXY", httpProxy)
|
||||
os.Setenv("HTTPS_PROXY", httpsProxy)
|
||||
os.Setenv("https_proxy", httpsProxy)
|
||||
}()
|
||||
|
||||
testdata := map[string]string{
|
||||
"NO_PROXY": "http://dummy.no.proxy",
|
||||
"http_proxy": "http://dummy.http.proxy",
|
||||
"https_proxy": "http://dummy.https.proxy",
|
||||
}
|
||||
|
||||
for k, v := range testdata {
|
||||
os.Setenv(k, v)
|
||||
}
|
||||
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "1",
|
||||
Name: "build",
|
||||
},
|
||||
Envs: map[string]string{},
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
WithProxy()(spec)
|
||||
for k, v := range testdata {
|
||||
step := spec.Steps[0]
|
||||
if step.Envs[k] != v {
|
||||
t.Errorf("Expect proxy varaible %s=%q, got %q", k, v, step.Envs[k])
|
||||
}
|
||||
}
|
||||
}
|
62
yaml/compiler/transform/secret.go
Normal file
62
yaml/compiler/transform/secret.go
Normal file
@ -0,0 +1,62 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/drone/drone-yaml/yaml/compiler/internal/rand"
|
||||
)
|
||||
|
||||
// WithSecrets is a transform function that adds a set
|
||||
// of global secrets to the container.
|
||||
func WithSecrets(secrets map[string]string) func(*engine.Spec) {
|
||||
return func(spec *engine.Spec) {
|
||||
for key, value := range secrets {
|
||||
spec.Secrets = append(spec.Secrets,
|
||||
&engine.Secret{
|
||||
Metadata: engine.Metadata{
|
||||
UID: rand.String(),
|
||||
Name: key,
|
||||
Namespace: spec.Metadata.Namespace,
|
||||
},
|
||||
Data: value,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SecretFunc is a callback function used to request
|
||||
// named secret, required by a pipeline step.
|
||||
type SecretFunc func(string) *engine.Secret
|
||||
|
||||
// WithSecretFunc is a transform function that resolves
|
||||
// all named secrets through a callback function, and
|
||||
// adds the secrets to the specification.
|
||||
func WithSecretFunc(f SecretFunc) func(*engine.Spec) {
|
||||
return func(spec *engine.Spec) {
|
||||
// first we get a unique list of all secrets
|
||||
// used by the specification.
|
||||
set := map[string]struct{}{}
|
||||
for _, step := range spec.Steps {
|
||||
// if we know the step is not going to run,
|
||||
// we can ignore any secrets that it requires.
|
||||
if step.RunPolicy == engine.RunNever {
|
||||
continue
|
||||
}
|
||||
for _, v := range step.Secrets {
|
||||
set[v.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// next we use the callback function to
|
||||
// get the value for each secret, and append
|
||||
// to the specification.
|
||||
for name := range set {
|
||||
secret := f(name)
|
||||
if secret != nil {
|
||||
secret.Metadata.UID = rand.String()
|
||||
secret.Metadata.Namespace = spec.Metadata.Namespace
|
||||
spec.Secrets = append(spec.Secrets, secret)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
107
yaml/compiler/transform/secret_test.go
Normal file
107
yaml/compiler/transform/secret_test.go
Normal file
@ -0,0 +1,107 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestWithSecret(t *testing.T) {
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "1",
|
||||
Name: "build",
|
||||
},
|
||||
Envs: map[string]string{},
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "acdj0yjqv7uh5hidveg0ggr42x8oj67b",
|
||||
Namespace: "pivqfthg1c9hy83ylht1sxx4nygjc7tk",
|
||||
},
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
secrets := map[string]string{
|
||||
"password": "correct-horse-battery-staple",
|
||||
}
|
||||
WithSecrets(secrets)(spec)
|
||||
|
||||
want := []*engine.Secret{
|
||||
{
|
||||
Metadata: engine.Metadata{
|
||||
Name: "password",
|
||||
Namespace: "pivqfthg1c9hy83ylht1sxx4nygjc7tk",
|
||||
},
|
||||
Data: "correct-horse-battery-staple",
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(want, spec.Secrets, ignoreMetadata); diff != "" {
|
||||
t.Errorf("Unexpected secret transform")
|
||||
t.Log(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithSecretFunc(t *testing.T) {
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "1",
|
||||
Name: "build",
|
||||
},
|
||||
Envs: map[string]string{},
|
||||
Secrets: []*engine.SecretVar{
|
||||
{
|
||||
Name: "password",
|
||||
Env: "PASSWORD",
|
||||
},
|
||||
},
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "acdj0yjqv7uh5hidveg0ggr42x8oj67b",
|
||||
Namespace: "pivqfthg1c9hy83ylht1sxx4nygjc7tk",
|
||||
},
|
||||
Steps: []*engine.Step{
|
||||
step,
|
||||
// this is a step that requests a secret
|
||||
// but should be skipped.
|
||||
{
|
||||
RunPolicy: engine.RunNever,
|
||||
Secrets: []*engine.SecretVar{
|
||||
{
|
||||
Name: "github_token",
|
||||
Env: "GITHUB_TOKEN",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fn := func(name string) *engine.Secret {
|
||||
if name == "github_token" {
|
||||
t.Errorf("Requested secret for skipped step")
|
||||
return nil
|
||||
}
|
||||
return &engine.Secret{
|
||||
Metadata: engine.Metadata{
|
||||
Name: "password",
|
||||
},
|
||||
Data: "correct-horse-battery-staple",
|
||||
}
|
||||
}
|
||||
WithSecretFunc(fn)(spec)
|
||||
|
||||
want := []*engine.Secret{
|
||||
{
|
||||
Metadata: engine.Metadata{
|
||||
Name: "password",
|
||||
Namespace: "pivqfthg1c9hy83ylht1sxx4nygjc7tk",
|
||||
},
|
||||
Data: "correct-horse-battery-staple",
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(want, spec.Secrets, ignoreMetadata); diff != "" {
|
||||
t.Errorf("Unexpected secret transform")
|
||||
t.Log(diff)
|
||||
}
|
||||
}
|
53
yaml/compiler/transform/volume.go
Normal file
53
yaml/compiler/transform/volume.go
Normal file
@ -0,0 +1,53 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/drone/drone-yaml/yaml/compiler/internal/rand"
|
||||
)
|
||||
|
||||
// WithVolumes is a transform function that adds a set
|
||||
// of global volumes to the container.
|
||||
func WithVolumes(volumes map[string]string) func(*engine.Spec) {
|
||||
return func(spec *engine.Spec) {
|
||||
for key, value := range volumes {
|
||||
volume := &engine.Volume{
|
||||
Metadata: engine.Metadata{
|
||||
UID: rand.String(),
|
||||
Name: rand.String(),
|
||||
Namespace: spec.Metadata.Name,
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
HostPath: &engine.VolumeHostPath{
|
||||
Path: key,
|
||||
},
|
||||
}
|
||||
spec.Docker.Volumes = append(spec.Docker.Volumes, volume)
|
||||
for _, step := range spec.Steps {
|
||||
mount := &engine.VolumeMount{
|
||||
Name: volume.Metadata.Name,
|
||||
Path: value,
|
||||
}
|
||||
step.Volumes = append(step.Volumes, mount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithVolumeSlice is a transform function that adds a set
|
||||
// of global volumes to the container that are defined in
|
||||
// --volume=host:container format.
|
||||
func WithVolumeSlice(volumes []string) func(*engine.Spec) {
|
||||
to := map[string]string{}
|
||||
for _, s := range volumes {
|
||||
parts := strings.Split(s, ":")
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := parts[0]
|
||||
val := parts[1]
|
||||
to[key] = val
|
||||
}
|
||||
return WithVolumes(to)
|
||||
}
|
69
yaml/compiler/transform/volume_test.go
Normal file
69
yaml/compiler/transform/volume_test.go
Normal file
@ -0,0 +1,69 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
)
|
||||
|
||||
func TestWithVolumes(t *testing.T) {
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "1",
|
||||
Name: "build",
|
||||
},
|
||||
Docker: &engine.DockerStep{
|
||||
Networks: nil,
|
||||
},
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Docker: &engine.DockerConfig{},
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
vols := map[string]string{"/path/on/host": "/path/in/container"}
|
||||
WithVolumes(vols)(spec)
|
||||
|
||||
if len(step.Volumes) == 0 {
|
||||
t.Error("Expected volume added to container")
|
||||
}
|
||||
if got, want := step.Volumes[0].Path, "/path/in/container"; got != want {
|
||||
t.Errorf("Want mount path %s, got %s", want, got)
|
||||
}
|
||||
if len(spec.Docker.Volumes) == 0 {
|
||||
t.Error("Expected volume added to spec")
|
||||
}
|
||||
if got, want := spec.Docker.Volumes[0].HostPath.Path, "/path/on/host"; got != want {
|
||||
t.Errorf("Want host mount path %s, got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithVolumeSlice(t *testing.T) {
|
||||
step := &engine.Step{
|
||||
Metadata: engine.Metadata{
|
||||
UID: "1",
|
||||
Name: "build",
|
||||
},
|
||||
Docker: &engine.DockerStep{
|
||||
Networks: nil,
|
||||
},
|
||||
}
|
||||
spec := &engine.Spec{
|
||||
Docker: &engine.DockerConfig{},
|
||||
Steps: []*engine.Step{step},
|
||||
}
|
||||
vols := []string{"/path/on/host:/path/in/container"}
|
||||
WithVolumeSlice(vols)(spec)
|
||||
|
||||
if len(step.Volumes) == 0 {
|
||||
t.Error("Expected volume added to container")
|
||||
}
|
||||
if got, want := step.Volumes[0].Path, "/path/in/container"; got != want {
|
||||
t.Errorf("Want mount path %s, got %s", want, got)
|
||||
}
|
||||
if len(spec.Docker.Volumes) == 0 {
|
||||
t.Error("Expected volume added to spec")
|
||||
}
|
||||
if got, want := spec.Docker.Volumes[0].HostPath.Path, "/path/on/host"; got != want {
|
||||
t.Errorf("Want host mount path %s, got %s", want, got)
|
||||
}
|
||||
}
|
144
yaml/compiler/workspace.go
Normal file
144
yaml/compiler/workspace.go
Normal file
@ -0,0 +1,144 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
unixpath "path"
|
||||
"strings"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/drone/drone-yaml/yaml"
|
||||
"github.com/drone/drone-yaml/yaml/compiler/internal/rand"
|
||||
)
|
||||
|
||||
const (
|
||||
workspacePath = "/drone/src"
|
||||
workspaceName = "workspace"
|
||||
workspaceHostName = "host"
|
||||
)
|
||||
|
||||
func setupWorkingDir(src *yaml.Container, 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)
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
func createWorkspace(from *yaml.Pipeline) (base, path, full string) {
|
||||
base = from.Workspace.Base
|
||||
path = from.Workspace.Path
|
||||
if base == "" {
|
||||
base = workspacePath
|
||||
}
|
||||
full = unixpath.Join(base, path)
|
||||
|
||||
if from.Platform.OS == "windows" {
|
||||
base = toWindowsDrive(base)
|
||||
path = toWindowsPath(path)
|
||||
full = toWindowsDrive(full)
|
||||
}
|
||||
return base, path, full
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
// CreateWorkspace creates the workspace volume as
|
||||
// an empty directory mount.
|
||||
func CreateWorkspace(spec *engine.Spec) {
|
||||
spec.Docker.Volumes = append(spec.Docker.Volumes,
|
||||
&engine.Volume{
|
||||
Metadata: engine.Metadata{
|
||||
UID: rand.String(),
|
||||
Name: workspaceName,
|
||||
Namespace: spec.Metadata.Namespace,
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
EmptyDir: &engine.VolumeEmptyDir{},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// CreateHostWorkspace returns a WorkspaceFunc that
|
||||
// mounts a host machine volume as the pipeline
|
||||
// workspace.
|
||||
func CreateHostWorkspace(workdir string) func(*engine.Spec) {
|
||||
return func(spec *engine.Spec) {
|
||||
CreateWorkspace(spec)
|
||||
spec.Docker.Volumes = append(
|
||||
spec.Docker.Volumes,
|
||||
&engine.Volume{
|
||||
Metadata: engine.Metadata{
|
||||
UID: rand.String(),
|
||||
Name: workspaceHostName,
|
||||
},
|
||||
HostPath: &engine.VolumeHostPath{
|
||||
Path: workdir,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
// MountWorkspace is a WorkspaceFunc that mounts the
|
||||
// default workspace volume to the pipeline step.
|
||||
func MountWorkspace(step *engine.Step, base, path, full string) {
|
||||
step.Volumes = append(step.Volumes, &engine.VolumeMount{
|
||||
Name: workspaceName,
|
||||
Path: base,
|
||||
})
|
||||
}
|
||||
|
||||
// MountHostWorkspace is a WorkspaceFunc that mounts
|
||||
// the default workspace and host volume to the pipeline.
|
||||
func MountHostWorkspace(step *engine.Step, base, path, full string) {
|
||||
step.Volumes = append(step.Volumes, &engine.VolumeMount{
|
||||
Name: workspaceHostName,
|
||||
Path: full,
|
||||
})
|
||||
if path != "" {
|
||||
step.Volumes = append(step.Volumes, &engine.VolumeMount{
|
||||
Name: workspaceName,
|
||||
Path: base,
|
||||
})
|
||||
}
|
||||
}
|
144
yaml/compiler/workspace_test.go
Normal file
144
yaml/compiler/workspace_test.go
Normal file
@ -0,0 +1,144 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-runtime/engine"
|
||||
"github.com/drone/drone-yaml/yaml"
|
||||
)
|
||||
|
||||
func TestSetupWorkspace(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
src *yaml.Container
|
||||
dst *engine.Step
|
||||
want string
|
||||
}{
|
||||
{
|
||||
path: "/drone/src",
|
||||
src: &yaml.Container{},
|
||||
dst: &engine.Step{},
|
||||
want: "/drone/src",
|
||||
},
|
||||
// do not override the user-defined working dir.
|
||||
{
|
||||
path: "/drone/src",
|
||||
src: &yaml.Container{},
|
||||
dst: &engine.Step{WorkingDir: "/foo"},
|
||||
want: "/foo",
|
||||
},
|
||||
// do not override the default working directory
|
||||
// for service containers with no commands.
|
||||
{
|
||||
path: "/drone/src",
|
||||
src: &yaml.Container{},
|
||||
dst: &engine.Step{Detach: true},
|
||||
want: "",
|
||||
},
|
||||
// overrides the default working directory
|
||||
// for service containers with commands.
|
||||
{
|
||||
path: "/drone/src",
|
||||
src: &yaml.Container{Commands: []string{"whoami"}},
|
||||
dst: &engine.Step{Detach: true},
|
||||
want: "/drone/src",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
setupWorkingDir(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 *yaml.Pipeline
|
||||
base string
|
||||
path string
|
||||
full string
|
||||
}{
|
||||
{
|
||||
from: &yaml.Pipeline{
|
||||
Workspace: yaml.Workspace{
|
||||
Base: "",
|
||||
Path: "",
|
||||
},
|
||||
},
|
||||
base: "/drone/src",
|
||||
path: "",
|
||||
full: "/drone/src",
|
||||
},
|
||||
{
|
||||
from: &yaml.Pipeline{
|
||||
Workspace: yaml.Workspace{
|
||||
Base: "",
|
||||
Path: "",
|
||||
},
|
||||
Platform: yaml.Platform{
|
||||
OS: "windows",
|
||||
},
|
||||
},
|
||||
base: "c:\\drone\\src",
|
||||
path: "",
|
||||
full: "c:\\drone\\src",
|
||||
},
|
||||
{
|
||||
from: &yaml.Pipeline{
|
||||
Workspace: yaml.Workspace{
|
||||
Base: "/drone",
|
||||
Path: "src",
|
||||
},
|
||||
},
|
||||
base: "/drone",
|
||||
path: "src",
|
||||
full: "/drone/src",
|
||||
},
|
||||
{
|
||||
from: &yaml.Pipeline{
|
||||
Workspace: yaml.Workspace{
|
||||
Base: "/drone",
|
||||
Path: "src",
|
||||
},
|
||||
Platform: yaml.Platform{
|
||||
OS: "windows",
|
||||
},
|
||||
},
|
||||
base: "c:\\drone",
|
||||
path: "src",
|
||||
full: "c:\\drone\\src",
|
||||
},
|
||||
{
|
||||
from: &yaml.Pipeline{
|
||||
Workspace: yaml.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)
|
||||
}
|
||||
}
|
||||
}
|
85
yaml/cond.go
Normal file
85
yaml/cond.go
Normal file
@ -0,0 +1,85 @@
|
||||
package yaml
|
||||
|
||||
import filepath "github.com/bmatcuk/doublestar"
|
||||
|
||||
// Conditions defines a group of conditions.
|
||||
type Conditions struct {
|
||||
Ref Condition `json:"ref,omitempty"`
|
||||
Repo Condition `json:"repo,omitempty"`
|
||||
Instance Condition `json:"instance,omitempty"`
|
||||
Target Condition `json:"target,omitempty"`
|
||||
Event Condition `json:"event,omitempty"`
|
||||
Branch Condition `json:"branch,omitempty"`
|
||||
Status Condition `json:"status,omitempty"`
|
||||
Paths Condition `json:"paths,omitempty"`
|
||||
}
|
||||
|
||||
// Condition defines a runtime condition.
|
||||
type Condition struct {
|
||||
Include []string `yaml:"include,omitempty" json:"include,omitempty"`
|
||||
Exclude []string `yaml:"exclude,omitempty" json:"exclude,omitempty"`
|
||||
}
|
||||
|
||||
// Match returns true if the string matches the include
|
||||
// patterns and does not match any of the exclude patterns.
|
||||
func (c *Condition) Match(v string) bool {
|
||||
if c.Excludes(v) {
|
||||
return false
|
||||
}
|
||||
if c.Includes(v) {
|
||||
return true
|
||||
}
|
||||
if len(c.Include) == 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Includes returns true if the string matches the include
|
||||
// patterns.
|
||||
func (c *Condition) Includes(v string) bool {
|
||||
for _, pattern := range c.Include {
|
||||
if ok, _ := filepath.Match(pattern, v); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Excludes returns true if the string matches the exclude
|
||||
// patterns.
|
||||
func (c *Condition) Excludes(v string) bool {
|
||||
for _, pattern := range c.Exclude {
|
||||
if ok, _ := filepath.Match(pattern, v); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yml unmarshalling.
|
||||
func (c *Condition) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var out1 string
|
||||
var out2 []string
|
||||
var out3 = struct {
|
||||
Include []string
|
||||
Exclude []string
|
||||
}{}
|
||||
|
||||
err := unmarshal(&out1)
|
||||
if err == nil {
|
||||
c.Include = []string{out1}
|
||||
return nil
|
||||
}
|
||||
|
||||
unmarshal(&out2)
|
||||
unmarshal(&out3)
|
||||
|
||||
c.Exclude = out3.Exclude
|
||||
c.Include = append(
|
||||
out3.Include,
|
||||
out2...,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
166
yaml/cond_test.go
Normal file
166
yaml/cond_test.go
Normal file
@ -0,0 +1,166 @@
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestConstraintMatch(t *testing.T) {
|
||||
testdata := []struct {
|
||||
conf string
|
||||
with string
|
||||
want bool
|
||||
}{
|
||||
// string value
|
||||
{
|
||||
conf: "master",
|
||||
with: "develop",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "master",
|
||||
with: "master",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "feature/*",
|
||||
with: "feature/foo",
|
||||
want: true,
|
||||
},
|
||||
// slice value
|
||||
{
|
||||
conf: "[ master, feature/* ]",
|
||||
with: "develop",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "[ master, feature/* ]",
|
||||
with: "master",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "[ master, feature/* ]",
|
||||
with: "feature/foo",
|
||||
want: true,
|
||||
},
|
||||
// includes block
|
||||
{
|
||||
conf: "include: [ master ]",
|
||||
with: "develop",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "include: [ master] ",
|
||||
with: "master",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "include: [ feature/* ]",
|
||||
with: "master",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "include: [ feature/* ]",
|
||||
with: "feature/foo",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "include: [ master, feature/* ]",
|
||||
with: "develop",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "include: [ master, feature/* ]",
|
||||
with: "master",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "include: [ master, feature/* ]",
|
||||
with: "feature/foo",
|
||||
want: true,
|
||||
},
|
||||
// excludes block
|
||||
{
|
||||
conf: "exclude: [ master ]",
|
||||
with: "develop",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "exclude: [ master ]",
|
||||
with: "master",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "exclude: [ feature/* ]",
|
||||
with: "master",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "exclude: [ feature/* ]",
|
||||
with: "feature/foo",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "exclude: [ master, develop ]",
|
||||
with: "master",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "exclude: [ feature/*, bar ]",
|
||||
with: "master",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "exclude: [ feature/*, bar ]",
|
||||
with: "feature/foo",
|
||||
want: false,
|
||||
},
|
||||
// include and exclude blocks
|
||||
{
|
||||
conf: "{ include: [ master, feature/* ], exclude: [ develop ] }",
|
||||
with: "master",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ master, feature/* ], exclude: [ feature/bar ] }",
|
||||
with: "feature/bar",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
conf: "{ include: [ master, feature/* ], exclude: [ master, develop ] }",
|
||||
with: "master",
|
||||
want: false,
|
||||
},
|
||||
// empty blocks
|
||||
{
|
||||
conf: "",
|
||||
with: "master",
|
||||
want: true,
|
||||
},
|
||||
// double star
|
||||
{
|
||||
conf: "foo/**",
|
||||
with: "foo/bar/baz/qux",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
conf: "foo/**/qux",
|
||||
with: "foo/bar/baz/qux",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, test := range testdata {
|
||||
c := parseCondition(test.conf)
|
||||
got, want := c.Match(test.with), test.want
|
||||
if got != want {
|
||||
t.Errorf("Expect %q matches %q is %v", test.with, test.conf, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseCondition(s string) *Condition {
|
||||
c := &Condition{}
|
||||
yaml.Unmarshal([]byte(s), c)
|
||||
return c
|
||||
}
|
104
yaml/converter/bitbucket/config.go
Normal file
104
yaml/converter/bitbucket/config.go
Normal file
@ -0,0 +1,104 @@
|
||||
package bitbucket
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
// Config defines the pipeline configuration.
|
||||
Config struct {
|
||||
// Image specifies the Docker image with
|
||||
// which we run your builds.
|
||||
Image string
|
||||
|
||||
// Clone defines the depth of Git clones
|
||||
// for all pipelines.
|
||||
Clone struct {
|
||||
Depth int
|
||||
}
|
||||
|
||||
// Pipeline defines the pipeline configuration
|
||||
// which includes a list of all steps for default,
|
||||
// tag, and branch-specific execution.
|
||||
Pipelines struct {
|
||||
Default Stage
|
||||
Tags map[string]Stage
|
||||
Branches map[string]Stage
|
||||
}
|
||||
|
||||
Definitions struct {
|
||||
Services map[string]*Step
|
||||
Caches map[string]string
|
||||
}
|
||||
}
|
||||
|
||||
// Stage contains a list of steps executed
|
||||
// for a specific branch or tag.
|
||||
Stage struct {
|
||||
Name string
|
||||
Steps []*Step
|
||||
}
|
||||
|
||||
// Step defines a build execution unit.
|
||||
Step struct {
|
||||
// Name of the pipeline step.
|
||||
Name string
|
||||
|
||||
// Image specifies the Docker image with
|
||||
// which we run your builds.
|
||||
Image string
|
||||
|
||||
// Script contains the list of bash commands
|
||||
// that are executed in sequence.
|
||||
Script []string
|
||||
|
||||
// Variables provides environment variables
|
||||
// passed to the container at runtime.
|
||||
Variables map[string]string
|
||||
|
||||
// Artifacts defines files that are to be
|
||||
// snapshotted and shared with the subsequent
|
||||
// step. This is not used, because Drone uses
|
||||
// a shared volume to share artifacts.
|
||||
Artifacts []string
|
||||
}
|
||||
)
|
||||
|
||||
// Pipeline returns the pipeline stage that best matches the branch
|
||||
// and ref. If there is no matching pipeline specific to the branch
|
||||
// or tag, the default pipeline is returned.
|
||||
func (c *Config) Pipeline(ref string) Stage {
|
||||
// match pipeline by tag name
|
||||
tag := strings.TrimPrefix(ref, "refs/tags/")
|
||||
for pattern, pipeline := range c.Pipelines.Tags {
|
||||
if ok, _ := path.Match(pattern, tag); ok {
|
||||
return pipeline
|
||||
}
|
||||
}
|
||||
// match pipeline by branch name
|
||||
branch := strings.TrimPrefix(ref, "refs/heads/")
|
||||
for pattern, pipeline := range c.Pipelines.Branches {
|
||||
if ok, _ := path.Match(pattern, branch); ok {
|
||||
return pipeline
|
||||
}
|
||||
}
|
||||
// use default
|
||||
return c.Pipelines.Default
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements custom parsing for the stage section of the yaml
|
||||
// to cleanup the structure a bit.
|
||||
func (s *Stage) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
in := []struct {
|
||||
Step *Step
|
||||
}{}
|
||||
err := unmarshal(&in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, step := range in {
|
||||
s.Steps = append(s.Steps, step.Step)
|
||||
}
|
||||
return nil
|
||||
}
|
1
yaml/converter/bitbucket/config_test.go
Normal file
1
yaml/converter/bitbucket/config_test.go
Normal file
@ -0,0 +1 @@
|
||||
package bitbucket
|
82
yaml/converter/bitbucket/convert.go
Normal file
82
yaml/converter/bitbucket/convert.go
Normal file
@ -0,0 +1,82 @@
|
||||
package bitbucket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
droneyaml "github.com/drone/drone-yaml/yaml"
|
||||
"github.com/drone/drone-yaml/yaml/pretty"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Convert converts the yaml configuration file from
|
||||
// the legacy format to the 1.0+ format.
|
||||
func Convert(b []byte, ref string) ([]byte, error) {
|
||||
config := new(Config)
|
||||
err := yaml.Unmarshal(b, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO (bradrydzewski) to correctly choose
|
||||
// the pipeline we need to pass the branch
|
||||
// and ref.
|
||||
stage := config.Pipeline(ref)
|
||||
|
||||
pipeline := &droneyaml.Pipeline{}
|
||||
pipeline.Name = "default"
|
||||
pipeline.Kind = "pipeline"
|
||||
|
||||
//
|
||||
// clone
|
||||
//
|
||||
|
||||
pipeline.Clone.Depth = config.Clone.Depth
|
||||
|
||||
//
|
||||
// steps
|
||||
//
|
||||
|
||||
for i, from := range stage.Steps {
|
||||
to := toContainer(from)
|
||||
// defaults to the global image if the
|
||||
// step does not define an image.
|
||||
if to.Image == "" {
|
||||
to.Image = config.Image
|
||||
}
|
||||
if to.Name == "" {
|
||||
to.Name = fmt.Sprintf("step_%d", i)
|
||||
}
|
||||
pipeline.Steps = append(pipeline.Steps, to)
|
||||
}
|
||||
|
||||
//
|
||||
// services
|
||||
//
|
||||
|
||||
for name, from := range config.Definitions.Services {
|
||||
to := toContainer(from)
|
||||
to.Name = name
|
||||
pipeline.Services = append(pipeline.Services, to)
|
||||
}
|
||||
|
||||
//
|
||||
// wrap the pipeline in the manifest
|
||||
//
|
||||
|
||||
manifest := &droneyaml.Manifest{}
|
||||
manifest.Resources = append(manifest.Resources, pipeline)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
pretty.Print(buf, manifest)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func toContainer(from *Step) *droneyaml.Container {
|
||||
return &droneyaml.Container{
|
||||
Name: from.Name,
|
||||
Image: from.Image,
|
||||
Commands: from.Script,
|
||||
}
|
||||
}
|
46
yaml/converter/bitbucket/convert_test.go
Normal file
46
yaml/converter/bitbucket/convert_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
package bitbucket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
tests := []struct {
|
||||
before, after, ref string
|
||||
}{
|
||||
{
|
||||
before: "testdata/sample1.yaml",
|
||||
after: "testdata/sample1.yaml.golden",
|
||||
ref: "refs/heads/master",
|
||||
},
|
||||
{
|
||||
before: "testdata/sample2.yaml",
|
||||
after: "testdata/sample2.yaml.golden",
|
||||
ref: "refs/heads/feature/foo",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
a, err := ioutil.ReadFile(test.before)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
b, err := ioutil.ReadFile(test.after)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
c, err := Convert([]byte(a), test.ref)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if bytes.Equal(b, c) == false {
|
||||
t.Errorf("Unexpected yaml conversion of %s", test.before)
|
||||
t.Log(string(c))
|
||||
}
|
||||
}
|
||||
}
|
32
yaml/converter/bitbucket/testdata/sample1.yaml
vendored
Normal file
32
yaml/converter/bitbucket/testdata/sample1.yaml
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
pipelines:
|
||||
default:
|
||||
- step:
|
||||
name: Build and test
|
||||
image: node:8.5.0
|
||||
caches:
|
||||
- node
|
||||
script:
|
||||
- npm install
|
||||
- npm test
|
||||
- npm build
|
||||
artifacts:
|
||||
- dist/**
|
||||
- step:
|
||||
name: Integration test
|
||||
image: node:8.5.0
|
||||
caches:
|
||||
- node
|
||||
services:
|
||||
- postgres
|
||||
script:
|
||||
- npm run integration-test
|
||||
- step:
|
||||
name: Deploy to beanstalk
|
||||
image: python:3.5.1
|
||||
script:
|
||||
- python deploy-to-beanstalk.py
|
||||
|
||||
definitions:
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:9.6.4
|
31
yaml/converter/bitbucket/testdata/sample1.yaml.golden
vendored
Normal file
31
yaml/converter/bitbucket/testdata/sample1.yaml.golden
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: Build and test
|
||||
image: node:8.5.0
|
||||
commands:
|
||||
- npm install
|
||||
- npm test
|
||||
- npm build
|
||||
|
||||
- name: Integration test
|
||||
image: node:8.5.0
|
||||
commands:
|
||||
- npm run integration-test
|
||||
|
||||
- name: Deploy to beanstalk
|
||||
image: python:3.5.1
|
||||
commands:
|
||||
- python deploy-to-beanstalk.py
|
||||
|
||||
services:
|
||||
- name: postgres
|
||||
image: postgres:9.6.4
|
||||
|
||||
...
|
40
yaml/converter/bitbucket/testdata/sample2.yaml
vendored
Normal file
40
yaml/converter/bitbucket/testdata/sample2.yaml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
pipelines:
|
||||
branches:
|
||||
feature/*:
|
||||
- step:
|
||||
name: Test
|
||||
image: node:latest
|
||||
script:
|
||||
- npm install
|
||||
- npm test
|
||||
default:
|
||||
- step:
|
||||
name: Build and test
|
||||
image: node:8.5.0
|
||||
caches:
|
||||
- node
|
||||
script:
|
||||
- npm install
|
||||
- npm test
|
||||
- npm build
|
||||
artifacts:
|
||||
- dist/**
|
||||
- step:
|
||||
name: Integration test
|
||||
image: node:8.5.0
|
||||
caches:
|
||||
- node
|
||||
services:
|
||||
- postgres
|
||||
script:
|
||||
- npm run integration-test
|
||||
- step:
|
||||
name: Deploy to beanstalk
|
||||
image: python:3.5.1
|
||||
script:
|
||||
- python deploy-to-beanstalk.py
|
||||
|
||||
definitions:
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:9.6.4
|
20
yaml/converter/bitbucket/testdata/sample2.yaml.golden
vendored
Normal file
20
yaml/converter/bitbucket/testdata/sample2.yaml.golden
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: Test
|
||||
image: node:latest
|
||||
commands:
|
||||
- npm install
|
||||
- npm test
|
||||
|
||||
services:
|
||||
- name: postgres
|
||||
image: postgres:9.6.4
|
||||
|
||||
...
|
76
yaml/converter/circleci/config.go
Normal file
76
yaml/converter/circleci/config.go
Normal file
@ -0,0 +1,76 @@
|
||||
package circleci
|
||||
|
||||
type (
|
||||
// Config defines the pipeline configuration.
|
||||
Config struct {
|
||||
// Version specifies the yaml configuration
|
||||
// file version.
|
||||
Version string
|
||||
|
||||
// Jobs defines a list of pipeline jobs.
|
||||
Jobs []*Job
|
||||
|
||||
// Workflows are used to orchestrate jobs.
|
||||
Workflows struct {
|
||||
Version string
|
||||
List map[string]*Workflow `yaml:",inline"`
|
||||
}
|
||||
}
|
||||
|
||||
// Workflow ochestrates one or more jobs.
|
||||
Workflow struct {
|
||||
Jobs []string
|
||||
}
|
||||
|
||||
// Job defines a pipeline job.
|
||||
Job struct {
|
||||
// Name of the stage.
|
||||
Name string
|
||||
|
||||
// Docker configures a Docker executor.
|
||||
Docker Docker
|
||||
|
||||
// Environment variables passed to the executor.
|
||||
Environment map[string]string
|
||||
|
||||
// Steps configures the Job steps.
|
||||
Steps map[string]Step
|
||||
|
||||
// Branches limits execution by branch.
|
||||
Branches []struct {
|
||||
Only []string
|
||||
Ignore []string
|
||||
}
|
||||
}
|
||||
|
||||
// Step defines a build execution unit.
|
||||
Step struct {
|
||||
Run Run
|
||||
AddSSHKeys map[string]interface{} `yaml:"add_ssh_keys"`
|
||||
AttachWorkspace map[string]interface{} `yaml:"attach_workspace"`
|
||||
Checkout map[string]interface{} `yaml:"checkout"`
|
||||
Deploy map[string]interface{} `yaml:"deploy"`
|
||||
PersistToWorkspace map[string]interface{} `yaml:"persist_to_workspace"`
|
||||
RestoreCache map[string]interface{} `yaml:"restore_cache"`
|
||||
SaveCache map[string]interface{} `yaml:"save_cache"`
|
||||
SetupRemoteDocker map[string]interface{} `yaml:"setup_remote_docker"`
|
||||
StoreArtifacts map[string]interface{} `yaml:"store_artifacts"`
|
||||
StoreTestResults map[string]interface{} `yaml:"store_test_results"`
|
||||
}
|
||||
)
|
||||
|
||||
// // UnmarshalYAML implements custom parsing for the stage section of the yaml
|
||||
// // to cleanup the structure a bit.
|
||||
// func (s *Stage) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
// in := []struct {
|
||||
// Step *Step
|
||||
// }{}
|
||||
// err := unmarshal(&in)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// for _, step := range in {
|
||||
// s.Steps = append(s.Steps, step.Step)
|
||||
// }
|
||||
// return nil
|
||||
// }
|
28
yaml/converter/circleci/docker.go
Normal file
28
yaml/converter/circleci/docker.go
Normal file
@ -0,0 +1,28 @@
|
||||
package circleci
|
||||
|
||||
// Docker configures a Docker executor.
|
||||
type Docker struct {
|
||||
// Image is the Docker image name.
|
||||
Image string
|
||||
|
||||
// Name is the Docker container hostname.
|
||||
Name string
|
||||
|
||||
// Entrypoint is the Docker container entrypoint.
|
||||
Entrypoint []string
|
||||
|
||||
// Command is the Docker container command.
|
||||
Command []string
|
||||
|
||||
// User is user that runs the Docker entrypoint.
|
||||
User string
|
||||
|
||||
// Environment variables passed to the container.
|
||||
Environment map[string]string
|
||||
|
||||
// Auth credentials to pull private images.
|
||||
Auth map[string]string
|
||||
|
||||
// Auth credentials to pull private ECR images.
|
||||
AWSAuth map[string]string `yaml:"aws_auth"`
|
||||
}
|
1
yaml/converter/circleci/docker_test.go
Normal file
1
yaml/converter/circleci/docker_test.go
Normal file
@ -0,0 +1 @@
|
||||
package circleci
|
34
yaml/converter/circleci/run.go
Normal file
34
yaml/converter/circleci/run.go
Normal file
@ -0,0 +1,34 @@
|
||||
package circleci
|
||||
|
||||
import "time"
|
||||
|
||||
// Run defines a command
|
||||
type Run struct {
|
||||
// Name of the command
|
||||
Name string
|
||||
|
||||
// Command run in the shell.
|
||||
Command string
|
||||
|
||||
// Shell to use to execute the command.
|
||||
Shell string
|
||||
|
||||
// Workiring Directory in which the command
|
||||
// is run.
|
||||
WorkingDir string `yaml:"working_directory"`
|
||||
|
||||
// Command is run in the background.
|
||||
Background bool `yaml:"background"`
|
||||
|
||||
// Amount of time the command can run with
|
||||
// no output before being canceled.
|
||||
NoOutputTimeout time.Duration `yaml:"no_output_timeout"`
|
||||
|
||||
// Environment variables set when running
|
||||
// the command in the shell.
|
||||
Environment map[string]string
|
||||
|
||||
// Defines when the command should be executed.
|
||||
// Values are always, on_success, and on_fail.
|
||||
When string
|
||||
}
|
11
yaml/converter/circleci/run_test.go
Normal file
11
yaml/converter/circleci/run_test.go
Normal file
@ -0,0 +1,11 @@
|
||||
package circleci
|
||||
|
||||
const testRun = `
|
||||
- run:
|
||||
name: test
|
||||
command: go test
|
||||
`
|
||||
|
||||
const testRunShort = `
|
||||
- run: go test
|
||||
`
|
22
yaml/converter/circleci/testdata/sample1.yml
vendored
Normal file
22
yaml/converter/circleci/testdata/sample1.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
version: 2
|
||||
jobs:
|
||||
backend:
|
||||
docker:
|
||||
- image: golang:1.8
|
||||
steps:
|
||||
- checkout
|
||||
- run: go build
|
||||
- run: go test
|
||||
frontend:
|
||||
docker:
|
||||
- image: node:latest
|
||||
steps:
|
||||
- checkout
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
workflows:
|
||||
version: 2
|
||||
default:
|
||||
jobs:
|
||||
- backend
|
||||
- frontend
|
0
yaml/converter/circleci/testdata/sample1.yml.golden
vendored
Normal file
0
yaml/converter/circleci/testdata/sample1.yml.golden
vendored
Normal file
51
yaml/converter/convert.go
Normal file
51
yaml/converter/convert.go
Normal file
@ -0,0 +1,51 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"github.com/drone/drone-yaml/yaml/converter/bitbucket"
|
||||
"github.com/drone/drone-yaml/yaml/converter/gitlab"
|
||||
"github.com/drone/drone-yaml/yaml/converter/legacy"
|
||||
)
|
||||
|
||||
// Metadata provides additional metadata used to
|
||||
// convert the configuration file format.
|
||||
type Metadata struct {
|
||||
// Filename of the configuration file, helps
|
||||
// determine the yaml configuration format.
|
||||
Filename string
|
||||
|
||||
// Ref of the commit use to choose the correct
|
||||
// pipeline if the configuration format defines
|
||||
// multiple pipelines (like Bitbucket)
|
||||
Ref string
|
||||
}
|
||||
|
||||
// Convert converts the yaml configuration file from
|
||||
// the legacy format to the 1.0+ format.
|
||||
func Convert(d []byte, m Metadata) ([]byte, error) {
|
||||
switch m.Filename {
|
||||
case "bitbucket-pipelines.yml":
|
||||
return bitbucket.Convert(d, m.Ref)
|
||||
case "circle.yml", ".circleci/config.yml":
|
||||
// TODO(bradrydzewski)
|
||||
case ".gitlab-ci.yml":
|
||||
return gitlab.Convert(d)
|
||||
case ".travis.yml":
|
||||
// TODO(bradrydzewski)
|
||||
}
|
||||
// if the filename does not match any external
|
||||
// systems we check to see if the configuration
|
||||
// file is a legacy (pre 1.0) .drone.yml format.
|
||||
if legacy.Match(d) {
|
||||
return legacy.Convert(d)
|
||||
}
|
||||
// else return the unmodified configuration
|
||||
// back to the caller.
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// ConvertString converts the yaml configuration file from
|
||||
// the legacy format to the 1.0+ format.
|
||||
func ConvertString(s string, m Metadata) (string, error) {
|
||||
b, err := Convert([]byte(s), m)
|
||||
return string(b), err
|
||||
}
|
1
yaml/converter/convert_test.go
Normal file
1
yaml/converter/convert_test.go
Normal file
@ -0,0 +1 @@
|
||||
package converter
|
125
yaml/converter/gitlab/config.go
Normal file
125
yaml/converter/gitlab/config.go
Normal file
@ -0,0 +1,125 @@
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"github.com/drone/drone-yaml/yaml/converter/internal"
|
||||
)
|
||||
|
||||
type (
|
||||
// Config defines the pipeline configuration.
|
||||
Config struct {
|
||||
// Image specifies the Docker image with
|
||||
// which we run your builds.
|
||||
Image Image
|
||||
|
||||
// Stages is used to group steps into stages,
|
||||
// where each stage is executed sequentially.
|
||||
Stages []string
|
||||
|
||||
// Services is used to define a set of services
|
||||
// that should be started and linked to each
|
||||
// step in the pipeline.
|
||||
Services []*Image
|
||||
|
||||
// Variables is used to customize execution,
|
||||
// such as the clone strategy.
|
||||
Variables map[string]string
|
||||
|
||||
// Before contains the list of bash commands
|
||||
// that are executed in sequence before the
|
||||
// first job.
|
||||
Before internal.StringSlice `yaml:"before_script"`
|
||||
|
||||
// After contains the list of bash commands
|
||||
// that are executed in sequence after the
|
||||
// last job.
|
||||
After internal.StringSlice `yaml:"after_script"`
|
||||
|
||||
// Jobs is used to define individual units
|
||||
// of execution that make up a stage.
|
||||
Jobs map[string]*Job `yaml:",inline"`
|
||||
}
|
||||
|
||||
// Job defines a build execution unit.
|
||||
Job struct {
|
||||
// Name of the pipeline step.
|
||||
Name string
|
||||
|
||||
// Stage is the name of the stage.
|
||||
Stage string
|
||||
|
||||
// Image specifies the Docker image with
|
||||
// which we run your builds.
|
||||
Image Image
|
||||
|
||||
// Script contains the list of bash commands
|
||||
// that are executed in sequence.
|
||||
Script internal.StringSlice
|
||||
|
||||
// Before contains the list of bash commands
|
||||
// that are executed in sequence before the
|
||||
// primary script.
|
||||
Before internal.StringSlice `yaml:"before_script"`
|
||||
|
||||
// After contains the list of bash commands
|
||||
// that are executed in sequence after the
|
||||
// primary script.
|
||||
After internal.StringSlice `yaml:"after_script"`
|
||||
|
||||
// Services defines a set of services linked
|
||||
// to the job.
|
||||
Services []*Image
|
||||
|
||||
// Only defines the names of branches and tags
|
||||
// for which the job will run.
|
||||
Only internal.StringSlice
|
||||
|
||||
// Except defines the names of branches and tags
|
||||
// for which the job will not run.
|
||||
Except internal.StringSlice
|
||||
|
||||
// Variables is used to customize execution,
|
||||
// such as the clone strategy.
|
||||
Variables map[string]string
|
||||
|
||||
// Allow job to fail. Failed job doesn’t contribute
|
||||
// to commit status
|
||||
AllowFailure bool
|
||||
|
||||
// Define when to run job. Can be on_success, on_failure,
|
||||
// always or manual
|
||||
When internal.StringSlice
|
||||
}
|
||||
|
||||
// Image defines a Docker image.
|
||||
Image struct {
|
||||
Name string
|
||||
Entrypoint []string
|
||||
Command []string
|
||||
Alias string
|
||||
}
|
||||
)
|
||||
|
||||
// UnmarshalYAML implements custom parsing for an Image.
|
||||
func (i *Image) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var name string
|
||||
err := unmarshal(&name)
|
||||
if err == nil {
|
||||
i.Name = name
|
||||
return nil
|
||||
}
|
||||
data := struct {
|
||||
Name string
|
||||
Entrypoint internal.StringSlice
|
||||
Command internal.StringSlice
|
||||
Alias string
|
||||
}{}
|
||||
err = unmarshal(&data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.Name = data.Name
|
||||
i.Entrypoint = data.Entrypoint
|
||||
i.Command = data.Command
|
||||
i.Alias = data.Alias
|
||||
return nil
|
||||
}
|
95
yaml/converter/gitlab/convert.go
Normal file
95
yaml/converter/gitlab/convert.go
Normal file
@ -0,0 +1,95 @@
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
droneyaml "github.com/drone/drone-yaml/yaml"
|
||||
"github.com/drone/drone-yaml/yaml/pretty"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Convert converts the yaml configuration file from
|
||||
// the legacy format to the 1.0+ format.
|
||||
func Convert(b []byte) ([]byte, error) {
|
||||
config := new(Config)
|
||||
err := yaml.Unmarshal(b, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manifest := &droneyaml.Manifest{}
|
||||
|
||||
// if no stages are defined, we create a single,
|
||||
// default stage that will be used for all jobs.
|
||||
if len(config.Stages) == 0 {
|
||||
for name, job := range config.Jobs {
|
||||
config.Stages = append(config.Stages, name)
|
||||
job.Stage = name
|
||||
}
|
||||
}
|
||||
|
||||
// create a new pipeline for each stage.
|
||||
var prevstage string
|
||||
for _, stage := range config.Stages {
|
||||
pipeline := &droneyaml.Pipeline{}
|
||||
pipeline.Name = stage
|
||||
pipeline.Kind = droneyaml.KindPipeline
|
||||
manifest.Resources = append(manifest.Resources, pipeline)
|
||||
for name, job := range config.Jobs {
|
||||
if job.Stage != stage {
|
||||
continue
|
||||
}
|
||||
cmds := []string(config.Before)
|
||||
cmds = append(cmds, []string(job.Before)...)
|
||||
cmds = append(cmds, []string(job.Script)...)
|
||||
cmds = append(cmds, []string(job.After)...)
|
||||
cmds = append(cmds, []string(config.After)...)
|
||||
|
||||
step := &droneyaml.Container{
|
||||
Name: name,
|
||||
Image: job.Image.Name,
|
||||
Command: job.Image.Command,
|
||||
Entrypoint: job.Image.Entrypoint,
|
||||
Commands: cmds,
|
||||
}
|
||||
|
||||
if job.AllowFailure {
|
||||
step.Failure = "ignore"
|
||||
}
|
||||
|
||||
if step.Image == "" {
|
||||
step.Image = config.Image.Name
|
||||
}
|
||||
// TODO: handle Services
|
||||
// TODO: handle Only
|
||||
// TODO: handle Except
|
||||
// TODO: handle Variables
|
||||
// TODO: handle When
|
||||
|
||||
pipeline.Steps = append(pipeline.Steps, step)
|
||||
}
|
||||
|
||||
for _, step := range config.Services {
|
||||
step := &droneyaml.Container{
|
||||
Name: step.Alias,
|
||||
Image: step.Name,
|
||||
Command: step.Command,
|
||||
Entrypoint: step.Entrypoint,
|
||||
}
|
||||
if step.Name == "" {
|
||||
step.Name = slug.Make(step.Image)
|
||||
}
|
||||
pipeline.Services = append(pipeline.Services, step)
|
||||
}
|
||||
|
||||
if prevstage != "" {
|
||||
pipeline.DependsOn = []string{prevstage}
|
||||
}
|
||||
prevstage = stage
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
pretty.Print(buf, manifest)
|
||||
return buf.Bytes(), nil
|
||||
}
|
57
yaml/converter/gitlab/convert_test.go
Normal file
57
yaml/converter/gitlab/convert_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
tests := []struct {
|
||||
before, after, ref string
|
||||
}{
|
||||
{
|
||||
before: "testdata/example1.yml",
|
||||
after: "testdata/example1.yml.golden",
|
||||
},
|
||||
{
|
||||
before: "testdata/example2.yml",
|
||||
after: "testdata/example2.yml.golden",
|
||||
},
|
||||
{
|
||||
before: "testdata/example3.yml",
|
||||
after: "testdata/example3.yml.golden",
|
||||
},
|
||||
{
|
||||
before: "testdata/example4.yml",
|
||||
after: "testdata/example4.yml.golden",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
a, err := ioutil.ReadFile(test.before)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
b, err := ioutil.ReadFile(test.after)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
c, err := Convert([]byte(a))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if bytes.Equal(b, c) == false {
|
||||
t.Errorf("Unexpected yaml conversion of %s", test.before)
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(b), string(c), false)
|
||||
t.Log(dmp.DiffCleanupSemantic(diffs))
|
||||
}
|
||||
}
|
||||
}
|
11
yaml/converter/gitlab/testdata/example1.yml
vendored
Normal file
11
yaml/converter/gitlab/testdata/example1.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
image: ruby:2.2
|
||||
|
||||
services:
|
||||
- postgres:9.3
|
||||
|
||||
before_script:
|
||||
- bundle install
|
||||
|
||||
test:
|
||||
script:
|
||||
- bundle exec rake spec
|
20
yaml/converter/gitlab/testdata/example1.yml.golden
vendored
Normal file
20
yaml/converter/gitlab/testdata/example1.yml.golden
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: test
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: ruby:2.2
|
||||
commands:
|
||||
- bundle install
|
||||
- bundle exec rake spec
|
||||
|
||||
services:
|
||||
- name: postgres-9-3
|
||||
image: postgres:9.3
|
||||
|
||||
...
|
16
yaml/converter/gitlab/testdata/example2.yml
vendored
Normal file
16
yaml/converter/gitlab/testdata/example2.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
before_script:
|
||||
- bundle install
|
||||
|
||||
test2.1:
|
||||
image: ruby:2.1
|
||||
services:
|
||||
- postgres:9.3
|
||||
script:
|
||||
- bundle exec rake spec
|
||||
|
||||
test2.2:
|
||||
image: ruby:2.2
|
||||
services:
|
||||
- postgres:9.4
|
||||
script:
|
||||
- bundle exec rake spec
|
34
yaml/converter/gitlab/testdata/example2.yml.golden
vendored
Normal file
34
yaml/converter/gitlab/testdata/example2.yml.golden
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: test2.1
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: test2.1
|
||||
image: ruby:2.1
|
||||
commands:
|
||||
- bundle install
|
||||
- bundle exec rake spec
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: test2.2
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: test2.2
|
||||
image: ruby:2.2
|
||||
commands:
|
||||
- bundle install
|
||||
- bundle exec rake spec
|
||||
|
||||
depends_on:
|
||||
- test2.1
|
||||
|
||||
...
|
16
yaml/converter/gitlab/testdata/example3.yml
vendored
Normal file
16
yaml/converter/gitlab/testdata/example3.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
image:
|
||||
name: ruby:2.2
|
||||
entrypoint: ["/bin/bash"]
|
||||
|
||||
services:
|
||||
- name: my-postgres:9.4
|
||||
alias: db-postgres
|
||||
entrypoint: ["/usr/local/bin/db-postgres"]
|
||||
command: ["start"]
|
||||
|
||||
before_script:
|
||||
- bundle install
|
||||
|
||||
test:
|
||||
script:
|
||||
- bundle exec rake spec
|
24
yaml/converter/gitlab/testdata/example3.yml.golden
vendored
Normal file
24
yaml/converter/gitlab/testdata/example3.yml.golden
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: test
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: ruby:2.2
|
||||
commands:
|
||||
- bundle install
|
||||
- bundle exec rake spec
|
||||
|
||||
services:
|
||||
- name: db-postgres
|
||||
image: my-postgres:9.4
|
||||
entrypoint:
|
||||
- /usr/local/bin/db-postgres
|
||||
command:
|
||||
- start
|
||||
|
||||
...
|
22
yaml/converter/gitlab/testdata/example4.yml
vendored
Normal file
22
yaml/converter/gitlab/testdata/example4.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- deploy
|
||||
|
||||
image: ruby:2.2
|
||||
|
||||
job 1:
|
||||
stage: build
|
||||
script: make build dependencies
|
||||
|
||||
job 2:
|
||||
stage: build
|
||||
script: make build artifacts
|
||||
|
||||
job 3:
|
||||
stage: test
|
||||
script: make test
|
||||
|
||||
job 4:
|
||||
stage: deploy
|
||||
script: make deploy
|
54
yaml/converter/gitlab/testdata/example4.yml.golden
vendored
Normal file
54
yaml/converter/gitlab/testdata/example4.yml.golden
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: build
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: job 1
|
||||
image: ruby:2.2
|
||||
commands:
|
||||
- make build dependencies
|
||||
|
||||
- name: job 2
|
||||
image: ruby:2.2
|
||||
commands:
|
||||
- make build artifacts
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: test
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: job 3
|
||||
image: ruby:2.2
|
||||
commands:
|
||||
- make test
|
||||
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: deploy
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: job 4
|
||||
image: ruby:2.2
|
||||
commands:
|
||||
- make deploy
|
||||
|
||||
depends_on:
|
||||
- test
|
||||
|
||||
...
|
20
yaml/converter/internal/string_slice.go
Normal file
20
yaml/converter/internal/string_slice.go
Normal file
@ -0,0 +1,20 @@
|
||||
package internal
|
||||
|
||||
// StringSlice represents a slice of strings or a string.
|
||||
type StringSlice []string
|
||||
|
||||
// UnmarshalYAML implements the Unmarshaller interface.
|
||||
func (s *StringSlice) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var stringType string
|
||||
if err := unmarshal(&stringType); err == nil {
|
||||
*s = []string{stringType}
|
||||
return nil
|
||||
}
|
||||
|
||||
var sliceType []string
|
||||
if err := unmarshal(&sliceType); err != nil {
|
||||
return err
|
||||
}
|
||||
*s = sliceType
|
||||
return nil
|
||||
}
|
45
yaml/converter/internal/string_slice_test.go
Normal file
45
yaml/converter/internal/string_slice_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestStringSlice(t *testing.T) {
|
||||
var tests = []struct {
|
||||
yaml string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
yaml: "hello world",
|
||||
want: []string{"hello world"},
|
||||
},
|
||||
{
|
||||
yaml: "[ hello, world ]",
|
||||
want: []string{"hello", "world"},
|
||||
},
|
||||
{
|
||||
yaml: "42",
|
||||
want: []string{"42"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
var got StringSlice
|
||||
|
||||
if err := yaml.Unmarshal([]byte(test.yaml), &got); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual([]string(got), test.want) {
|
||||
t.Errorf("Got slice %v want %v", got, test.want)
|
||||
}
|
||||
}
|
||||
|
||||
var got StringSlice
|
||||
if err := yaml.Unmarshal([]byte("{}"), &got); err == nil {
|
||||
t.Errorf("Want error unmarshaling invalid string or slice value.")
|
||||
}
|
||||
}
|
9
yaml/converter/legacy/convert.go
Normal file
9
yaml/converter/legacy/convert.go
Normal file
@ -0,0 +1,9 @@
|
||||
package legacy
|
||||
|
||||
import "github.com/drone/drone-yaml/yaml/converter/legacy/internal"
|
||||
|
||||
// Convert converts the yaml configuration file from
|
||||
// the legacy format to the 1.0+ format.
|
||||
func Convert(d []byte) ([]byte, error) {
|
||||
return yaml.Convert(d)
|
||||
}
|
247
yaml/converter/legacy/internal/config.go
Normal file
247
yaml/converter/legacy/internal/config.go
Normal file
@ -0,0 +1,247 @@
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
droneyaml "github.com/drone/drone-yaml/yaml"
|
||||
"github.com/drone/drone-yaml/yaml/pretty"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Config provides the high-level configuration.
|
||||
type Config struct {
|
||||
Workspace struct {
|
||||
Base string
|
||||
Path string
|
||||
}
|
||||
Clone Containers
|
||||
Pipeline Containers
|
||||
Services Containers
|
||||
Branches Constraint
|
||||
Secrets map[string]struct {
|
||||
Driver string
|
||||
DriverOpts map[string]string `yaml:"driver_opts"`
|
||||
Path string
|
||||
Vault string
|
||||
}
|
||||
}
|
||||
|
||||
// Convert converts the yaml configuration file from
|
||||
// the legacy format to the 1.0+ format.
|
||||
func Convert(d []byte) ([]byte, error) {
|
||||
from := new(Config)
|
||||
err := yaml.Unmarshal(d, from)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pipeline := &droneyaml.Pipeline{}
|
||||
pipeline.Name = "default"
|
||||
pipeline.Kind = "pipeline"
|
||||
|
||||
pipeline.Workspace.Base = from.Workspace.Base
|
||||
pipeline.Workspace.Path = from.Workspace.Path
|
||||
|
||||
if len(from.Clone.Containers) != 0 {
|
||||
pipeline.Clone.Disable = true
|
||||
for _, container := range from.Clone.Containers {
|
||||
pipeline.Steps = append(pipeline.Steps,
|
||||
toContainer(container),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for _, container := range from.Services.Containers {
|
||||
pipeline.Services = append(pipeline.Services,
|
||||
toContainer(container),
|
||||
)
|
||||
}
|
||||
|
||||
for _, container := range from.Pipeline.Containers {
|
||||
pipeline.Steps = append(pipeline.Steps,
|
||||
toContainer(container),
|
||||
)
|
||||
}
|
||||
pipeline.Volumes = toVolumes(from)
|
||||
pipeline.Trigger.Branch.Include = from.Branches.Include
|
||||
pipeline.Trigger.Branch.Exclude = from.Branches.Exclude
|
||||
|
||||
manifest := &droneyaml.Manifest{}
|
||||
manifest.Resources = append(manifest.Resources, pipeline)
|
||||
|
||||
secrets := toSecrets(from)
|
||||
if secrets != nil {
|
||||
manifest.Resources = append(manifest.Resources, secrets)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
pretty.Print(buf, manifest)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func toContainer(from *Container) *droneyaml.Container {
|
||||
return &droneyaml.Container{
|
||||
Name: from.Name,
|
||||
Image: from.Image,
|
||||
Detach: from.Detached,
|
||||
Command: from.Command,
|
||||
Commands: from.Commands,
|
||||
DNS: from.DNS,
|
||||
DNSSearch: from.DNSSearch,
|
||||
Entrypoint: from.Entrypoint,
|
||||
Environment: toEnvironment(from),
|
||||
ExtraHosts: from.ExtraHosts,
|
||||
Pull: toPullPolicy(from.Pull),
|
||||
Privileged: from.Privileged,
|
||||
Settings: toSettings(from.Vargs),
|
||||
Volumes: toVolumeMounts(from.Volumes),
|
||||
When: toConditions(from.Constraints),
|
||||
}
|
||||
}
|
||||
|
||||
// helper function converts the legacy constraint syntax
|
||||
// to the new condition syntax.
|
||||
func toConditions(from Constraints) droneyaml.Conditions {
|
||||
return droneyaml.Conditions{
|
||||
Ref: droneyaml.Condition{
|
||||
Include: from.Ref.Include,
|
||||
Exclude: from.Ref.Exclude,
|
||||
},
|
||||
Repo: droneyaml.Condition{
|
||||
Include: from.Repo.Include,
|
||||
Exclude: from.Repo.Exclude,
|
||||
},
|
||||
Instance: droneyaml.Condition{
|
||||
Include: from.Instance.Include,
|
||||
Exclude: from.Instance.Exclude,
|
||||
},
|
||||
Target: droneyaml.Condition{
|
||||
Include: from.Environment.Include,
|
||||
Exclude: from.Environment.Exclude,
|
||||
},
|
||||
Event: droneyaml.Condition{
|
||||
Include: from.Event.Include,
|
||||
Exclude: from.Event.Exclude,
|
||||
},
|
||||
Branch: droneyaml.Condition{
|
||||
Include: from.Branch.Include,
|
||||
Exclude: from.Branch.Exclude,
|
||||
},
|
||||
Status: droneyaml.Condition{
|
||||
Include: from.Status.Include,
|
||||
Exclude: from.Status.Exclude,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// helper function converts the legacy environment syntax
|
||||
// to the new environment syntax.
|
||||
func toEnvironment(from *Container) map[string]*droneyaml.Variable {
|
||||
envs := map[string]*droneyaml.Variable{}
|
||||
for key, val := range from.Environment.Map {
|
||||
envs[key] = &droneyaml.Variable{
|
||||
Value: val,
|
||||
}
|
||||
}
|
||||
for _, val := range from.Secrets.Secrets {
|
||||
name := strings.ToUpper(val.Target)
|
||||
envs[name] = &droneyaml.Variable{
|
||||
Secret: val.Source,
|
||||
}
|
||||
}
|
||||
return envs
|
||||
}
|
||||
|
||||
// helper function converts the legacy image pull syntax
|
||||
// to the new pull policy syntax.
|
||||
func toPullPolicy(pull bool) string {
|
||||
switch pull {
|
||||
case true:
|
||||
return "always"
|
||||
default:
|
||||
return "default"
|
||||
}
|
||||
}
|
||||
|
||||
// helper function converts the legacy secret syntax to the
|
||||
// new secret variable syntax.
|
||||
func toSecrets(from *Config) *droneyaml.Secret {
|
||||
secret := &droneyaml.Secret{}
|
||||
secret.Kind = "secret"
|
||||
secret.Type = "general"
|
||||
secret.External = map[string]droneyaml.ExternalData{}
|
||||
for key, val := range from.Secrets {
|
||||
external := droneyaml.ExternalData{}
|
||||
if val.Driver == "vault" {
|
||||
if val.DriverOpts != nil {
|
||||
external.Path = val.DriverOpts["path"]
|
||||
external.Name = val.DriverOpts["key"]
|
||||
}
|
||||
} else if val.Path != "" {
|
||||
external.Path = val.Path
|
||||
} else {
|
||||
external.Path = val.Vault
|
||||
}
|
||||
secret.External[key] = external
|
||||
}
|
||||
if len(secret.External) == 0 {
|
||||
return nil
|
||||
}
|
||||
return secret
|
||||
}
|
||||
|
||||
// helper function converts the legacy vargs syntax to the
|
||||
// new environment syntax.
|
||||
func toSettings(from map[string]interface{}) map[string]*droneyaml.Parameter {
|
||||
params := map[string]*droneyaml.Parameter{}
|
||||
for key, val := range from {
|
||||
params[key] = &droneyaml.Parameter{
|
||||
Value: val,
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// helper function converts the legacy volume syntax
|
||||
// to the new volume mount syntax.
|
||||
func toVolumeMounts(from []*Volume) []*droneyaml.VolumeMount {
|
||||
to := []*droneyaml.VolumeMount{}
|
||||
for _, v := range from {
|
||||
to = append(to, &droneyaml.VolumeMount{
|
||||
Name: fmt.Sprintf("%x", v.Source),
|
||||
MountPath: v.Destination,
|
||||
})
|
||||
}
|
||||
return to
|
||||
}
|
||||
|
||||
// helper function converts the legacy volume syntax
|
||||
// to the new volume mount syntax.
|
||||
func toVolumes(from *Config) []*droneyaml.Volume {
|
||||
set := map[string]struct{}{}
|
||||
to := []*droneyaml.Volume{}
|
||||
|
||||
containers := []*Container{}
|
||||
containers = append(containers, from.Pipeline.Containers...)
|
||||
containers = append(containers, from.Services.Containers...)
|
||||
|
||||
for _, container := range containers {
|
||||
for _, v := range container.Volumes {
|
||||
name := fmt.Sprintf("%x", v.Source)
|
||||
if _, ok := set[name]; ok {
|
||||
continue
|
||||
}
|
||||
set[name] = struct{}{}
|
||||
to = append(to, &droneyaml.Volume{
|
||||
Name: name,
|
||||
HostPath: &droneyaml.VolumeHostPath{
|
||||
Path: v.Source,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return to
|
||||
}
|
52
yaml/converter/legacy/internal/config_test.go
Normal file
52
yaml/converter/legacy/internal/config_test.go
Normal file
@ -0,0 +1,52 @@
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
tests := []struct {
|
||||
before, after string
|
||||
}{
|
||||
{
|
||||
before: "testdata/simple.yml",
|
||||
after: "testdata/simple.yml.golden",
|
||||
},
|
||||
{
|
||||
before: "testdata/vault_1.yml",
|
||||
after: "testdata/vault_1.yml.golden",
|
||||
},
|
||||
{
|
||||
before: "testdata/vault_2.yml",
|
||||
after: "testdata/vault_2.yml.golden",
|
||||
},
|
||||
{
|
||||
before: "testdata/vault_3.yml",
|
||||
after: "testdata/vault_3.yml.golden",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
a, err := ioutil.ReadFile(test.before)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
b, err := ioutil.ReadFile(test.after)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
c, err := Convert(a)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if bytes.Equal(b, c) == false {
|
||||
t.Errorf("Unexpected yaml conversion of %s", test.before)
|
||||
t.Log(string(c))
|
||||
}
|
||||
}
|
||||
}
|
69
yaml/converter/legacy/internal/constraint.go
Normal file
69
yaml/converter/legacy/internal/constraint.go
Normal file
@ -0,0 +1,69 @@
|
||||
package yaml
|
||||
|
||||
type (
|
||||
// Constraints defines a set of runtime constraints.
|
||||
Constraints struct {
|
||||
Ref Constraint
|
||||
Repo Constraint
|
||||
Instance Constraint
|
||||
Environment Constraint
|
||||
Event Constraint
|
||||
Branch Constraint
|
||||
Status Constraint
|
||||
}
|
||||
|
||||
// Constraint defines a runtime constraint.
|
||||
Constraint struct {
|
||||
Include []string
|
||||
Exclude []string
|
||||
}
|
||||
|
||||
// ConstraintMap defines a runtime constraint map.
|
||||
ConstraintMap struct {
|
||||
Include map[string]string
|
||||
Exclude map[string]string
|
||||
}
|
||||
)
|
||||
|
||||
// UnmarshalYAML unmarshals the constraint.
|
||||
func (c *Constraint) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var out1 = struct {
|
||||
Include StringSlice
|
||||
Exclude StringSlice
|
||||
}{}
|
||||
|
||||
var out2 StringSlice
|
||||
|
||||
unmarshal(&out1)
|
||||
unmarshal(&out2)
|
||||
|
||||
c.Exclude = out1.Exclude
|
||||
c.Include = append(
|
||||
out1.Include,
|
||||
out2...,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML unmarshals the constraint map.
|
||||
func (c *ConstraintMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
out1 := struct {
|
||||
Include map[string]string
|
||||
Exclude map[string]string
|
||||
}{
|
||||
Include: map[string]string{},
|
||||
Exclude: map[string]string{},
|
||||
}
|
||||
|
||||
out2 := map[string]string{}
|
||||
|
||||
unmarshal(&out1)
|
||||
unmarshal(&out2)
|
||||
|
||||
c.Include = out1.Include
|
||||
c.Exclude = out1.Exclude
|
||||
for k, v := range out2 {
|
||||
c.Include[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
58
yaml/converter/legacy/internal/container.go
Normal file
58
yaml/converter/legacy/internal/container.go
Normal file
@ -0,0 +1,58 @@
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type (
|
||||
// Containers represents an ordered list of containers.
|
||||
Containers struct {
|
||||
Containers []*Container
|
||||
}
|
||||
|
||||
// Container represents a Docker container.
|
||||
Container struct {
|
||||
Command StringSlice `yaml:"command,omitempty"`
|
||||
Commands StringSlice `yaml:"commands,omitempty"`
|
||||
Detached bool `yaml:"detach,omitempty"`
|
||||
Devices []string `yaml:"devices,omitempty"`
|
||||
ErrIgnore bool `yaml:"allow_failure,omitempty"`
|
||||
Tmpfs []string `yaml:"tmpfs,omitempty"`
|
||||
DNS StringSlice `yaml:"dns,omitempty"`
|
||||
DNSSearch StringSlice `yaml:"dns_search,omitempty"`
|
||||
Entrypoint StringSlice `yaml:"entrypoint,omitempty"`
|
||||
Environment SliceMap `yaml:"environment,omitempty"`
|
||||
ExtraHosts []string `yaml:"extra_hosts,omitempty"`
|
||||
Image string `yaml:"image,omitempty"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Privileged bool `yaml:"privileged,omitempty"`
|
||||
Pull bool `yaml:"pull,omitempty"`
|
||||
Shell string `yaml:"shell,omitempty"`
|
||||
Volumes []*Volume `yaml:"volumes,omitempty"`
|
||||
Secrets Secrets `yaml:"secrets,omitempty"`
|
||||
Constraints Constraints `yaml:"when,omitempty"`
|
||||
Vargs map[string]interface{} `yaml:",inline"`
|
||||
}
|
||||
)
|
||||
|
||||
// UnmarshalYAML implements the Unmarshaller interface.
|
||||
func (c *Containers) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
slice := yaml.MapSlice{}
|
||||
if err := unmarshal(&slice); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range slice {
|
||||
container := Container{}
|
||||
out, _ := yaml.Marshal(s.Value)
|
||||
|
||||
if err := yaml.Unmarshal(out, &container); err != nil {
|
||||
return err
|
||||
}
|
||||
container.Name = fmt.Sprintf("%v", s.Key)
|
||||
c.Containers = append(c.Containers, &container)
|
||||
}
|
||||
return nil
|
||||
}
|
1
yaml/converter/legacy/internal/container_test.go
Normal file
1
yaml/converter/legacy/internal/container_test.go
Normal file
@ -0,0 +1 @@
|
||||
package yaml
|
30
yaml/converter/legacy/internal/secret.go
Normal file
30
yaml/converter/legacy/internal/secret.go
Normal file
@ -0,0 +1,30 @@
|
||||
package yaml
|
||||
|
||||
type (
|
||||
// Secrets represents a list of container secrets.
|
||||
Secrets struct {
|
||||
Secrets []*Secret
|
||||
}
|
||||
|
||||
// Secret represents a container secret.
|
||||
Secret struct {
|
||||
Source string
|
||||
Target string
|
||||
}
|
||||
)
|
||||
|
||||
// UnmarshalYAML implements the Unmarshaller interface.
|
||||
func (s *Secrets) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var strslice []string
|
||||
err := unmarshal(&strslice)
|
||||
if err == nil {
|
||||
for _, str := range strslice {
|
||||
s.Secrets = append(s.Secrets, &Secret{
|
||||
Source: str,
|
||||
Target: str,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return unmarshal(&s.Secrets)
|
||||
}
|
62
yaml/converter/legacy/internal/secret_test.go
Normal file
62
yaml/converter/legacy/internal/secret_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestUnmarshalSecrets(t *testing.T) {
|
||||
testdata := []struct {
|
||||
from string
|
||||
want []*Secret
|
||||
}{
|
||||
{
|
||||
from: "[ mysql_username, mysql_password]",
|
||||
want: []*Secret{
|
||||
{
|
||||
Source: "mysql_username",
|
||||
Target: "mysql_username",
|
||||
},
|
||||
{
|
||||
Source: "mysql_password",
|
||||
Target: "mysql_password",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
from: "[ { source: mysql_prod_username, target: mysql_username } ]",
|
||||
want: []*Secret{
|
||||
{
|
||||
Source: "mysql_prod_username",
|
||||
Target: "mysql_username",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
from: "[ { source: mysql_prod_username, target: mysql_username }, { source: redis_username, target: redis_username } ]",
|
||||
want: []*Secret{
|
||||
{
|
||||
Source: "mysql_prod_username",
|
||||
Target: "mysql_username",
|
||||
},
|
||||
{
|
||||
Source: "redis_username",
|
||||
Target: "redis_username",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testdata {
|
||||
in := []byte(test.from)
|
||||
got := Secrets{}
|
||||
err := yaml.Unmarshal(in, &got)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else if !reflect.DeepEqual(test.want, got.Secrets) {
|
||||
t.Errorf("got secret %v want %v", got.Secrets, test.want)
|
||||
}
|
||||
}
|
||||
}
|
32
yaml/converter/legacy/internal/slice_map.go
Normal file
32
yaml/converter/legacy/internal/slice_map.go
Normal file
@ -0,0 +1,32 @@
|
||||
package yaml
|
||||
|
||||
import "strings"
|
||||
|
||||
// SliceMap represents a slice or map of key pairs.
|
||||
type SliceMap struct {
|
||||
Map map[string]string
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements custom Yaml unmarshaling.
|
||||
func (s *SliceMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
s.Map = map[string]string{}
|
||||
err := unmarshal(&s.Map)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var slice []string
|
||||
err = unmarshal(&slice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, v := range slice {
|
||||
parts := strings.SplitN(v, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
key := parts[0]
|
||||
val := parts[1]
|
||||
s.Map[key] = val
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
41
yaml/converter/legacy/internal/slice_map_test.go
Normal file
41
yaml/converter/legacy/internal/slice_map_test.go
Normal file
@ -0,0 +1,41 @@
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestMapSlice(t *testing.T) {
|
||||
var tests = []struct {
|
||||
yaml string
|
||||
want map[string]string
|
||||
}{
|
||||
{
|
||||
yaml: "[ foo=bar, baz=qux ]",
|
||||
want: map[string]string{"foo": "bar", "baz": "qux"},
|
||||
},
|
||||
{
|
||||
yaml: "{ foo: bar, baz: qux }",
|
||||
want: map[string]string{"foo": "bar", "baz": "qux"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
var got SliceMap
|
||||
|
||||
if err := yaml.Unmarshal([]byte(test.yaml), &got); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got.Map, test.want) {
|
||||
t.Errorf("Got map %v want %v", got, test.want)
|
||||
}
|
||||
}
|
||||
|
||||
var got SliceMap
|
||||
if err := yaml.Unmarshal([]byte("1"), &got); err == nil {
|
||||
t.Errorf("Want error unmarshaling invalid map value.")
|
||||
}
|
||||
}
|
20
yaml/converter/legacy/internal/string_slice.go
Normal file
20
yaml/converter/legacy/internal/string_slice.go
Normal file
@ -0,0 +1,20 @@
|
||||
package yaml
|
||||
|
||||
// StringSlice represents a slice of strings or a string.
|
||||
type StringSlice []string
|
||||
|
||||
// UnmarshalYAML implements the Unmarshaller interface.
|
||||
func (s *StringSlice) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var stringType string
|
||||
if err := unmarshal(&stringType); err == nil {
|
||||
*s = []string{stringType}
|
||||
return nil
|
||||
}
|
||||
|
||||
var sliceType []string
|
||||
if err := unmarshal(&sliceType); err != nil {
|
||||
return err
|
||||
}
|
||||
*s = sliceType
|
||||
return nil
|
||||
}
|
45
yaml/converter/legacy/internal/string_slice_test.go
Normal file
45
yaml/converter/legacy/internal/string_slice_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestStringSlice(t *testing.T) {
|
||||
var tests = []struct {
|
||||
yaml string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
yaml: "hello world",
|
||||
want: []string{"hello world"},
|
||||
},
|
||||
{
|
||||
yaml: "[ hello, world ]",
|
||||
want: []string{"hello", "world"},
|
||||
},
|
||||
{
|
||||
yaml: "42",
|
||||
want: []string{"42"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
var got StringSlice
|
||||
|
||||
if err := yaml.Unmarshal([]byte(test.yaml), &got); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual([]string(got), test.want) {
|
||||
t.Errorf("Got slice %v want %v", got, test.want)
|
||||
}
|
||||
}
|
||||
|
||||
var got StringSlice
|
||||
if err := yaml.Unmarshal([]byte("{}"), &got); err == nil {
|
||||
t.Errorf("Want error unmarshaling invalid string or slice value.")
|
||||
}
|
||||
}
|
48
yaml/converter/legacy/internal/testdata/simple.yml
vendored
Normal file
48
yaml/converter/legacy/internal/testdata/simple.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
workspace:
|
||||
base: /go
|
||||
path: src/github.com/octocat/hello-world
|
||||
|
||||
pipeline:
|
||||
build:
|
||||
image: golang
|
||||
commands:
|
||||
- go get
|
||||
- go build
|
||||
volumes:
|
||||
- /tmp/go:/go/bin
|
||||
environment:
|
||||
- GOOS=linux
|
||||
- GOARCH=amd64
|
||||
|
||||
test:
|
||||
image: golang:latest
|
||||
volumes:
|
||||
- /tmp/go:/go/bin
|
||||
commands:
|
||||
- go test -v
|
||||
|
||||
docker:
|
||||
image: plugins/docker
|
||||
secrets:
|
||||
- docker_username
|
||||
- docker_password
|
||||
repo: octocat/hello-world
|
||||
when:
|
||||
branch: master
|
||||
|
||||
slack:
|
||||
image: plugins/slack
|
||||
secrets:
|
||||
- source: token
|
||||
target: slack_token
|
||||
channel: general
|
||||
|
||||
services:
|
||||
database:
|
||||
image: mysql
|
||||
environment:
|
||||
MYSQL_USERNAME: foo
|
||||
MYSQL_PASSWORD: bar
|
||||
|
||||
branches:
|
||||
- master
|
76
yaml/converter/legacy/internal/testdata/simple.yml.golden
vendored
Normal file
76
yaml/converter/legacy/internal/testdata/simple.yml.golden
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
workspace:
|
||||
base: /go
|
||||
path: src/github.com/octocat/hello-world
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
pull: default
|
||||
image: golang
|
||||
commands:
|
||||
- go get
|
||||
- go build
|
||||
environment:
|
||||
GOARCH: amd64
|
||||
GOOS: linux
|
||||
volumes:
|
||||
- name: 2f746d702f676f
|
||||
path: /go/bin
|
||||
|
||||
- name: test
|
||||
pull: default
|
||||
image: golang:latest
|
||||
commands:
|
||||
- go test -v
|
||||
volumes:
|
||||
- name: 2f746d702f676f
|
||||
path: /go/bin
|
||||
|
||||
- name: docker
|
||||
pull: default
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: octocat/hello-world
|
||||
environment:
|
||||
DOCKER_PASSWORD:
|
||||
from_secret: docker_password
|
||||
DOCKER_USERNAME:
|
||||
from_secret: docker_username
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
|
||||
- name: slack
|
||||
pull: default
|
||||
image: plugins/slack
|
||||
settings:
|
||||
channel: general
|
||||
environment:
|
||||
SLACK_TOKEN:
|
||||
from_secret: token
|
||||
|
||||
services:
|
||||
- name: database
|
||||
pull: default
|
||||
image: mysql
|
||||
environment:
|
||||
MYSQL_PASSWORD: bar
|
||||
MYSQL_USERNAME: foo
|
||||
|
||||
volumes:
|
||||
- name: 2f746d702f676f
|
||||
host:
|
||||
path: /tmp/go
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
|
||||
...
|
16
yaml/converter/legacy/internal/testdata/vault_1.yml
vendored
Normal file
16
yaml/converter/legacy/internal/testdata/vault_1.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
pipeline:
|
||||
docker:
|
||||
image: plugins/docker
|
||||
secrets: [ docker_username, docker_password ]
|
||||
repo: octocat/hello-world
|
||||
|
||||
secrets:
|
||||
docker_username:
|
||||
driver: vault
|
||||
driver_opts:
|
||||
path: secret/docker/username
|
||||
docker_password:
|
||||
driver: vault
|
||||
driver_opts:
|
||||
path: secret/docker
|
||||
key: password
|
31
yaml/converter/legacy/internal/testdata/vault_1.yml.golden
vendored
Normal file
31
yaml/converter/legacy/internal/testdata/vault_1.yml.golden
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: docker
|
||||
pull: default
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: octocat/hello-world
|
||||
environment:
|
||||
DOCKER_PASSWORD:
|
||||
from_secret: docker_password
|
||||
DOCKER_USERNAME:
|
||||
from_secret: docker_username
|
||||
|
||||
---
|
||||
kind: secret
|
||||
type: general
|
||||
external_data:
|
||||
docker_password:
|
||||
path: secret/docker
|
||||
name: password
|
||||
docker_username:
|
||||
path: secret/docker/username
|
||||
|
||||
...
|
11
yaml/converter/legacy/internal/testdata/vault_2.yml
vendored
Normal file
11
yaml/converter/legacy/internal/testdata/vault_2.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
pipeline:
|
||||
docker:
|
||||
image: plugins/docker
|
||||
secrets: [ docker_username, docker_password ]
|
||||
repo: octocat/hello-world
|
||||
|
||||
secrets:
|
||||
docker_username:
|
||||
path: secret/docker/username
|
||||
docker_password:
|
||||
path: secret/docker/password
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user