├── .drone.yml ├── .github_changelog_generator ├── .gitignore ├── CHANGELOG.md ├── HISTORY.md ├── LICENSE.md ├── README.md ├── client ├── client.go ├── http.go ├── http_test.go ├── single.go └── single_test.go ├── clone ├── clone.go ├── clone_test.go ├── environ.go ├── environ_test.go ├── utils.go └── utils_test.go ├── container ├── volume.go └── volume_test.go ├── environ ├── calver.go ├── calver_test.go ├── environ.go ├── environ_test.go ├── expand.go ├── expand_test.go ├── provider │ ├── combine.go │ ├── combine_test.go │ ├── external.go │ ├── external_test.go │ ├── provider.go │ ├── provider_test.go │ ├── static.go │ ├── static_test.go │ ├── util.go │ └── util_test.go ├── proxy.go ├── proxy_test.go ├── semver.go └── semver_test.go ├── go.mod ├── go.sum ├── handler ├── handler.go ├── handler_test.go ├── nocache.go ├── nocache_test.go ├── render.go ├── render_test.go ├── router │ ├── router.go │ └── router_test.go ├── static │ ├── files │ │ ├── favicon.png │ │ ├── icons │ │ │ ├── arrow-right.svg │ │ │ ├── failure.svg │ │ │ ├── pending.svg │ │ │ ├── running.svg │ │ │ ├── skipped.svg │ │ │ ├── sleeping.svg │ │ │ └── success.svg │ │ ├── index.html │ │ ├── logs.html │ │ ├── reset.css │ │ ├── style.css │ │ └── timeago.js │ ├── static.go │ └── static_gen.go └── template │ ├── files │ ├── index.tmpl │ ├── logs.tmpl │ └── stage.tmpl │ ├── server.go │ ├── template.go │ ├── template_gen.go │ └── testdata │ ├── logs.json │ ├── logs_empty.json │ ├── stage.json │ ├── stage_cron.json │ ├── stage_promote.json │ ├── stage_pull_request.json │ ├── stage_rollback.json │ ├── stage_tag.json │ ├── stages.json │ ├── stages_empty.json │ └── stages_idle.json ├── internal ├── clone.go ├── clone_test.go ├── merge.go └── merge_test.go ├── labels ├── labels.go └── labels_test.go ├── licenses ├── Polyform-Free-Trial.md ├── Polyform-Noncommercial.md └── Polyform-Small-Business.md ├── livelog ├── copy.go ├── copy_test.go ├── extractor │ └── writer.go ├── livelog.go └── livelog_test.go ├── logger ├── context.go ├── context_test.go ├── dumper.go ├── dumper_test.go ├── history │ ├── history.go │ └── history_test.go ├── logger.go ├── logger_test.go ├── logrus.go └── logrus_test.go ├── manifest ├── clone.go ├── clone_test.go ├── concur.go ├── concur_test.go ├── cond.go ├── cond_test.go ├── driver.go ├── driver_test.go ├── env.go ├── env_test.go ├── lookup.go ├── lookup_test.go ├── manifest.go ├── param.go ├── param_test.go ├── parse.go ├── parse_test.go ├── platform.go ├── secret.go ├── secret_test.go ├── signature.go ├── signature_test.go ├── unit.go ├── unit_test.go ├── workspace.go └── workspace_test.go ├── pipeline ├── pipeline.go ├── reporter.go ├── reporter │ ├── doc.go │ ├── history │ │ ├── entry.go │ │ ├── entry_test.go │ │ ├── history.go │ │ └── history_test.go │ └── remote │ │ ├── remote.go │ │ └── remote_test.go ├── reporter_test.go ├── runtime │ ├── const.go │ ├── const_test.go │ ├── execer.go │ ├── execer_test.go │ ├── replacer.go │ ├── replacer_test.go │ ├── runner.go │ ├── runner_test.go │ └── type.go ├── state.go ├── state_test.go ├── streamer.go ├── streamer │ ├── console │ │ ├── console.go │ │ ├── plain.go │ │ ├── plain_test.go │ │ ├── pretty.go │ │ ├── pretty_test.go │ │ ├── sequence.go │ │ └── sequence_test.go │ └── doc.go ├── streamer_test.go ├── uploader.go ├── uploader │ ├── upload.go │ └── upload_test.go └── uploader_test.go ├── poller ├── poller.go └── poller_test.go ├── registry ├── auths │ ├── auth.go │ ├── auth_test.go │ ├── encode.go │ ├── encode_test.go │ └── testdata │ │ ├── config.json │ │ └── config2.json ├── combine.go ├── combine_test.go ├── external.go ├── external_test.go ├── file.go ├── file_test.go ├── registry.go ├── registry_test.go ├── static.go └── static_test.go ├── secret ├── combine.go ├── combine_test.go ├── encrypted.go ├── encrypted_test.go ├── external.go ├── external_test.go ├── secret.go ├── secret_test.go ├── static.go └── static_test.go ├── server └── server.go └── shell ├── bash ├── bash.go └── bash_test.go ├── powershell ├── powershell.go └── powershell_test.go ├── shell.go ├── shell_test.go └── shell_windows.go /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: default 3 | 4 | steps: 5 | - name: test 6 | image: golang:1.12 7 | commands: 8 | - go test ./... 9 | 10 | -------------------------------------------------------------------------------- /.github_changelog_generator: -------------------------------------------------------------------------------- 1 | since-tag=v1.8.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | NOTES* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # runner-go 2 | 3 | A collection of helper packages to extend Drone with customer runners. 4 | 5 | ## Release procedure 6 | 7 | Run the changelog generator. 8 | 9 | ```BASH 10 | docker run -it --rm -v "$(pwd)":/usr/local/src/your-app githubchangeloggenerator/github-changelog-generator -u drone -p runner-go -t 11 | ``` 12 | 13 | You can generate a token by logging into your GitHub account and going to Settings -> Personal access tokens. 14 | 15 | Next we tag the PR's with the fixes or enhancements labels. If the PR does not fufil the requirements, do not add a label. 16 | 17 | Run the changelog generator again with the future version according to semver. 18 | 19 | ```BASH 20 | docker run -it --rm -v "$(pwd)":/usr/local/src/your-app githubchangeloggenerator/github-changelog-generator -u drone -p runner-go --token --future-release v1.0.0 21 | ``` 22 | 23 | Create your pull request for the release. Get it merged then tag the release. 24 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package client provides a client for using the runner API. 6 | package client 7 | 8 | import ( 9 | "context" 10 | "errors" 11 | 12 | "github.com/drone/drone-go/drone" 13 | ) 14 | 15 | // V1 is version 1 of the runner API 16 | const V1 = "application/vnd.drone.runner.v1+json" 17 | 18 | // ErrOptimisticLock is returned by if the struct being 19 | // modified has a Version field and the value is not equal 20 | // to the current value in the database 21 | var ErrOptimisticLock = errors.New("Optimistic Lock Error") 22 | 23 | type ( 24 | // Filter is used to filter the builds that are pulled 25 | // from the queue. 26 | Filter struct { 27 | Kind string `json:"kind"` 28 | Type string `json:"type"` 29 | OS string `json:"os"` 30 | Arch string `json:"arch"` 31 | Variant string `json:"variant"` 32 | Kernel string `json:"kernel"` 33 | Labels map[string]string `json:"labels,omitempty"` 34 | } 35 | 36 | // File represents a file from the version control 37 | // repository. It is used by the runner to provide the 38 | // yaml configuration file to the runner. 39 | File struct { 40 | Data []byte 41 | Hash []byte 42 | } 43 | 44 | // Context provides the runner with the build context and 45 | // includes all environment data required to execute the 46 | // build. 47 | Context struct { 48 | Build *drone.Build `json:"build"` 49 | Stage *drone.Stage `json:"stage"` 50 | Config *File `json:"config"` 51 | Netrc *drone.Netrc `json:"netrc"` 52 | Repo *drone.Repo `json:"repository"` 53 | Secrets []*drone.Secret `json:"secrets"` 54 | System *drone.System `json:"system"` 55 | } 56 | ) 57 | 58 | // A Client manages communication with the runner. 59 | type Client interface { 60 | // Join notifies the server the runner is joining the cluster. 61 | Join(ctx context.Context, machine string) error 62 | 63 | // Leave notifies the server the runner is leaving the cluster. 64 | Leave(ctx context.Context, machine string) error 65 | 66 | // Ping sends a ping message to the server to test connectivity. 67 | Ping(ctx context.Context, machine string) error 68 | 69 | // Request requests the next available build stage for execution. 70 | Request(ctx context.Context, args *Filter) (*drone.Stage, error) 71 | 72 | // Accept accepts the build stage for execution. 73 | Accept(ctx context.Context, stage *drone.Stage) error 74 | 75 | // Detail gets the build stage details for execution. 76 | Detail(ctx context.Context, stage *drone.Stage) (*Context, error) 77 | 78 | // Update updates the build stage. 79 | Update(ctxt context.Context, step *drone.Stage) error 80 | 81 | // UpdateStep updates the build step. 82 | UpdateStep(ctx context.Context, stage *drone.Step) error 83 | 84 | // Watch watches for build cancellation requests. 85 | Watch(ctx context.Context, stage int64) (bool, error) 86 | 87 | // Batch batch writes logs to the build logs. 88 | Batch(ctx context.Context, step int64, lines []*drone.Line) error 89 | 90 | // Upload uploads the full logs to the server. 91 | Upload(ctx context.Context, step int64, lines []*drone.Line) error 92 | 93 | // UploadCard uploads a card to drone server. 94 | UploadCard(ctx context.Context, step int64, card *drone.CardInput) error 95 | } 96 | -------------------------------------------------------------------------------- /client/http_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package client 6 | -------------------------------------------------------------------------------- /client/single.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "context" 9 | "runtime/debug" 10 | "sync" 11 | 12 | "github.com/drone/drone-go/drone" 13 | ) 14 | 15 | var _ Client = (*SingleFlight)(nil) 16 | 17 | // SingleFlight wraps a Client and limits to a single in-flight 18 | // request to pull items from the queue. 19 | type SingleFlight struct { 20 | Client 21 | mu sync.Mutex 22 | } 23 | 24 | // NewSingleFlight returns a Client that is limited to a single in-flight 25 | // request to pull items from the queue. 26 | func NewSingleFlight(endpoint, secret string, skipverify bool) *SingleFlight { 27 | return &SingleFlight{Client: New(endpoint, secret, skipverify)} 28 | } 29 | 30 | // Request requests the next available build stage for execution. 31 | func (t *SingleFlight) Request(ctx context.Context, args *Filter) (*drone.Stage, error) { 32 | // if the context is canceled there is no need to make 33 | // the request and we can exit early. 34 | select { 35 | case <-ctx.Done(): 36 | return nil, ctx.Err() 37 | default: 38 | } 39 | // if is critical to unlock the mutex when the function 40 | // exits. although a panic is unlikely it is critical that 41 | // we recover from the panic to avoid deadlock. 42 | defer func() { 43 | if r := recover(); r != nil { 44 | debug.PrintStack() 45 | } 46 | t.mu.Unlock() 47 | }() 48 | // lock the mutex to ensure only a single in-flight 49 | // request to request a resource from the server queue. 50 | t.mu.Lock() 51 | return t.Client.Request(ctx, args) 52 | } 53 | -------------------------------------------------------------------------------- /client/single_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package client 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "testing" 11 | 12 | "github.com/drone/drone-go/drone" 13 | ) 14 | 15 | var noContext = context.Background() 16 | 17 | func TestSingleFlight(t *testing.T) { 18 | mock := &mockRequestClient{ 19 | out: &drone.Stage{}, 20 | err: errors.New("some random error"), 21 | } 22 | client := NewSingleFlight("", "", false) 23 | client.Client = mock 24 | out, err := client.Request(noContext, nil) 25 | if got, want := out, mock.out; got != want { 26 | t.Errorf("Expect stage returned from request") 27 | } 28 | if got, want := err, mock.err; got != want { 29 | t.Errorf("Expect error returned from request") 30 | } 31 | } 32 | 33 | func TestSingleFlightPanic(t *testing.T) { 34 | mock := &mockRequestClientPanic{} 35 | client := NewSingleFlight("", "", false) 36 | client.Client = mock 37 | 38 | defer func() { 39 | if recover() != nil { 40 | t.Errorf("Expect Request to recover from panic") 41 | } 42 | client.mu.Lock() 43 | client.mu.Unlock() 44 | }() 45 | 46 | client.Request(noContext, nil) 47 | } 48 | 49 | func TestSingleFlightCancel(t *testing.T) { 50 | ctx, cancel := context.WithCancel(noContext) 51 | cancel() 52 | client := NewSingleFlight("", "", false) 53 | client.Request(ctx, nil) 54 | } 55 | 56 | // mock client that returns a static stage and error 57 | // from the request method. 58 | type mockRequestClient struct { 59 | Client 60 | 61 | out *drone.Stage 62 | err error 63 | } 64 | 65 | func (m *mockRequestClient) Request(ctx context.Context, args *Filter) (*drone.Stage, error) { 66 | return m.out, m.err 67 | } 68 | 69 | // mock client that returns panics when the request 70 | // method is invoked. 71 | type mockRequestClientPanic struct { 72 | Client 73 | } 74 | 75 | func (m *mockRequestClientPanic) Request(ctx context.Context, args *Filter) (*drone.Stage, error) { 76 | panic("method not implemented") 77 | } 78 | -------------------------------------------------------------------------------- /clone/clone.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package clone provides utilities for cloning commits. 6 | package clone 7 | 8 | import "fmt" 9 | 10 | // 11 | // IMPORTANT: DO NOT MODIFY THIS FILE 12 | // 13 | // this file must not be changed unless the changes have been 14 | // discussed and approved by the project maintainers in the 15 | // GitHub issue tracker. 16 | // 17 | 18 | // Args provide arguments to clone a repository. 19 | type Args struct { 20 | Branch string 21 | Commit string 22 | Ref string 23 | Remote string 24 | Depth int 25 | Tags bool 26 | NoFF bool 27 | } 28 | 29 | // Commands returns posix-compliant commands to clone a 30 | // repository and checkout a commit. 31 | func Commands(args Args) []string { 32 | switch { 33 | case isTag(args.Ref): 34 | return tag(args) 35 | case isPullRequest(args.Ref): 36 | return pull(args) 37 | default: 38 | return branch(args) 39 | } 40 | } 41 | 42 | // branch returns posix-compliant commands to clone a repository 43 | // and checkout the named branch. 44 | func branch(args Args) []string { 45 | return []string{ 46 | "git init", 47 | fmt.Sprintf("git remote add origin %s", args.Remote), 48 | fmt.Sprintf("git fetch %s origin +refs/heads/%s:", fetchFlags(args), args.Branch), 49 | fmt.Sprintf("git checkout %s -b %s", args.Commit, args.Branch), 50 | } 51 | } 52 | 53 | // tag returns posix-compliant commands to clone a repository 54 | // and checkout the tag by reference path. 55 | func tag(args Args) []string { 56 | return []string{ 57 | "git init", 58 | fmt.Sprintf("git remote add origin %s", args.Remote), 59 | fmt.Sprintf("git fetch %s origin +%s:", fetchFlags(args), args.Ref), 60 | "git checkout -qf FETCH_HEAD", 61 | } 62 | } 63 | 64 | // pull returns posix-compliant commands to clone a repository 65 | // and checkout the pull request by reference path. 66 | func pull(args Args) []string { 67 | return []string{ 68 | "git init", 69 | fmt.Sprintf("git remote add origin %s", args.Remote), 70 | fmt.Sprintf("git fetch %s origin +refs/heads/%s:", fetchFlags(args), args.Branch), 71 | fmt.Sprintf("git checkout %s", args.Branch), 72 | fmt.Sprintf("git fetch origin %s:", args.Ref), 73 | fmt.Sprintf("git merge %s %s", mergeFlags(args), args.Commit), 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /clone/clone_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package clone 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | ) 12 | 13 | func TestCommandsTag(t *testing.T) { 14 | args := Args{ 15 | Depth: 50, 16 | Remote: "https://github.com/octocat/hello-world.git", 17 | Ref: "refs/tags/v1.0.0", 18 | } 19 | got := Commands(args) 20 | want := []string{ 21 | "git init", 22 | "git remote add origin https://github.com/octocat/hello-world.git", 23 | "git fetch --depth=50 origin +refs/tags/v1.0.0:", 24 | "git checkout -qf FETCH_HEAD", 25 | } 26 | if diff := cmp.Diff(got, want); diff != "" { 27 | t.Fail() 28 | t.Log(diff) 29 | } 30 | } 31 | 32 | func TestCommandsBranch(t *testing.T) { 33 | args := Args{ 34 | Branch: "develop", 35 | Commit: "3650a5d21bbf086fa8d2f16b0067ddeecfa604df", 36 | Depth: 50, 37 | NoFF: true, 38 | Remote: "https://github.com/octocat/hello-world.git", 39 | Ref: "refs/heads/develop", 40 | Tags: true, 41 | } 42 | got := Commands(args) 43 | want := []string{ 44 | "git init", 45 | "git remote add origin https://github.com/octocat/hello-world.git", 46 | "git fetch --depth=50 --tags origin +refs/heads/develop:", 47 | "git checkout 3650a5d21bbf086fa8d2f16b0067ddeecfa604df -b develop", 48 | } 49 | if diff := cmp.Diff(got, want); diff != "" { 50 | t.Log(want) 51 | t.Fail() 52 | t.Log(diff) 53 | } 54 | } 55 | 56 | func TestCommandsPullRequest(t *testing.T) { 57 | args := Args{ 58 | Branch: "master", 59 | Commit: "3650a5d21bbf086fa8d2f16b0067ddeecfa604df", 60 | Depth: 50, 61 | NoFF: true, 62 | Remote: "https://github.com/octocat/hello-world.git", 63 | Ref: "refs/pull/42/head", 64 | Tags: true, 65 | } 66 | got := Commands(args) 67 | want := []string{ 68 | "git init", 69 | "git remote add origin https://github.com/octocat/hello-world.git", 70 | "git fetch --depth=50 --tags origin +refs/heads/master:", 71 | "git checkout master", 72 | "git fetch origin refs/pull/42/head:", 73 | "git merge --no-ff 3650a5d21bbf086fa8d2f16b0067ddeecfa604df", 74 | } 75 | if diff := cmp.Diff(got, want); diff != "" { 76 | t.Log(want) 77 | t.Fail() 78 | t.Log(diff) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /clone/environ.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package clone 6 | 7 | // Config provides the Git Configuration parameters. 8 | type Config struct { 9 | User User 10 | Trace bool 11 | SkipVerify bool 12 | } 13 | 14 | // User provides the Git user parameters. 15 | type User struct { 16 | Name string 17 | Email string 18 | } 19 | 20 | // Environ returns a set of global Git environment variables, 21 | // from the configuration input. 22 | func Environ(config Config) map[string]string { 23 | environ := map[string]string{ 24 | "GIT_AUTHOR_NAME": "drone", 25 | "GIT_AUTHOR_EMAIL": "noreply@drone", 26 | "GIT_COMMITTER_NAME": "drone", 27 | "GIT_COMMITTER_EMAIL": "noreply@drone", 28 | "GIT_TERMINAL_PROMPT": "0", 29 | } 30 | if s := config.User.Name; s != "" { 31 | environ["GIT_AUTHOR_NAME"] = s 32 | environ["GIT_COMMITTER_NAME"] = s 33 | } 34 | if s := config.User.Email; s != "" { 35 | environ["GIT_AUTHOR_EMAIL"] = s 36 | environ["GIT_COMMITTER_EMAIL"] = s 37 | } 38 | if config.Trace { 39 | environ["GIT_TRACE"] = "true" 40 | } 41 | if config.SkipVerify { 42 | environ["GIT_SSL_NO_VERIFY"] = "true" 43 | } 44 | return environ 45 | } 46 | -------------------------------------------------------------------------------- /clone/environ_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package clone 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | ) 12 | 13 | func TestEnvironDefault(t *testing.T) { 14 | c := Config{} 15 | a := Environ(c) 16 | b := map[string]string{ 17 | "GIT_AUTHOR_NAME": "drone", 18 | "GIT_AUTHOR_EMAIL": "noreply@drone", 19 | "GIT_COMMITTER_NAME": "drone", 20 | "GIT_COMMITTER_EMAIL": "noreply@drone", 21 | "GIT_TERMINAL_PROMPT": "0", 22 | } 23 | if diff := cmp.Diff(a, b); diff != "" { 24 | t.Fail() 25 | t.Log(diff) 26 | } 27 | } 28 | 29 | func TestEnviron(t *testing.T) { 30 | c := Config{ 31 | User: User{ 32 | Name: "The Octocat", 33 | Email: "octocat@github.com", 34 | }, 35 | Trace: true, 36 | SkipVerify: true, 37 | } 38 | a := Environ(c) 39 | b := map[string]string{ 40 | "GIT_AUTHOR_NAME": "The Octocat", 41 | "GIT_AUTHOR_EMAIL": "octocat@github.com", 42 | "GIT_COMMITTER_NAME": "The Octocat", 43 | "GIT_COMMITTER_EMAIL": "octocat@github.com", 44 | "GIT_TERMINAL_PROMPT": "0", 45 | "GIT_TRACE": "true", 46 | "GIT_SSL_NO_VERIFY": "true", 47 | } 48 | if diff := cmp.Diff(a, b); diff != "" { 49 | t.Fail() 50 | t.Log(diff) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /clone/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package clone 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | // fetchFlags is a helper function that returns a string of 13 | // optional git-fetch command line flags. 14 | func fetchFlags(args Args) string { 15 | var flags []string 16 | if depth := args.Depth; depth > 0 { 17 | flag := fmt.Sprintf("--depth=%d", depth) 18 | flags = append(flags, flag) 19 | } 20 | if args.Tags { 21 | flags = append(flags, "--tags") 22 | } 23 | return strings.Join(flags, " ") 24 | } 25 | 26 | // mergeFlags is a helper function that returns a string of 27 | // optional git-merge command line flags. 28 | func mergeFlags(args Args) string { 29 | var flags []string 30 | if args.NoFF { 31 | flags = append(flags, "--no-ff") 32 | } 33 | return strings.Join(flags, " ") 34 | } 35 | 36 | // isTag returns true if the reference path points to 37 | // a tag object. 38 | func isTag(ref string) bool { 39 | return strings.HasPrefix(ref, "refs/tags/") 40 | } 41 | 42 | // isPullRequest returns true if the reference path points to 43 | // a pull request object. 44 | func isPullRequest(ref string) bool { 45 | return strings.HasPrefix(ref, "refs/pull/") || 46 | strings.HasPrefix(ref, "refs/pull-requests/") || 47 | strings.HasPrefix(ref, "refs/merge-requests/") 48 | } 49 | -------------------------------------------------------------------------------- /clone/utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package clone 6 | 7 | import "testing" 8 | 9 | func TestFetchFlags(t *testing.T) { 10 | var args Args 11 | if got, want := fetchFlags(args), ""; got != want { 12 | t.Errorf("Want %q, got %q", want, got) 13 | } 14 | args.Tags = true 15 | if got, want := fetchFlags(args), "--tags"; got != want { 16 | t.Errorf("Want %q, got %q", want, got) 17 | } 18 | args.Tags = false 19 | args.Depth = 50 20 | if got, want := fetchFlags(args), "--depth=50"; got != want { 21 | t.Errorf("Want %q, got %q", want, got) 22 | } 23 | } 24 | 25 | func TestMergeFlags(t *testing.T) { 26 | var args Args 27 | if got, want := mergeFlags(args), ""; got != want { 28 | t.Errorf("Want %q, got %q", want, got) 29 | } 30 | args.NoFF = true 31 | if got, want := mergeFlags(args), "--no-ff"; got != want { 32 | t.Errorf("Want %q, got %q", want, got) 33 | } 34 | } 35 | 36 | func TestIsTag(t *testing.T) { 37 | tests := []struct { 38 | s string 39 | v bool 40 | }{ 41 | { 42 | s: "refs/heads/master", 43 | v: false, 44 | }, 45 | { 46 | s: "refs/pull/1/head", 47 | v: false, 48 | }, 49 | { 50 | s: "refs/tags/v1.0.0", 51 | v: true, 52 | }, 53 | } 54 | 55 | for _, test := range tests { 56 | if got, want := isTag(test.s), test.v; got != want { 57 | t.Errorf("Want tag %v for %s", want, test.s) 58 | } 59 | } 60 | } 61 | 62 | func TestIsPullRequst(t *testing.T) { 63 | tests := []struct { 64 | s string 65 | v bool 66 | }{ 67 | { 68 | s: "refs/heads/master", 69 | v: false, 70 | }, 71 | { 72 | s: "refs/pull/1/head", 73 | v: true, 74 | }, 75 | { 76 | s: "refs/pull/2/merge", 77 | v: true, 78 | }, 79 | } 80 | 81 | for _, test := range tests { 82 | if got, want := isPullRequest(test.s), test.v; got != want { 83 | t.Errorf("Want pull request %v for %s", want, test.s) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /container/volume.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package container 6 | 7 | import ( 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | // IsRestrictedVolume is helper function that 13 | // returns true if mounting the volume is restricted for un-trusted containers. 14 | func IsRestrictedVolume(path string) bool { 15 | path, err := filepath.Abs(path) 16 | if err != nil { 17 | return true 18 | } 19 | 20 | path = strings.ToLower(path) 21 | 22 | switch { 23 | case path == "/": 24 | case path == "/etc": 25 | case path == "/etc/docker" || strings.HasPrefix(path, "/etc/docker/"): 26 | case path == "/var": 27 | case path == "/var/run" || strings.HasPrefix(path, "/var/run/"): 28 | case path == "/proc" || strings.HasPrefix(path, "/proc/"): 29 | case path == "/usr/local/bin" || strings.HasPrefix(path, "/usr/local/bin/"): 30 | case path == "/usr/local/sbin" || strings.HasPrefix(path, "/usr/local/sbin/"): 31 | case path == "/usr/bin" || strings.HasPrefix(path, "/usr/bin/"): 32 | case path == "/bin" || strings.HasPrefix(path, "/bin/"): 33 | case path == "/mnt" || strings.HasPrefix(path, "/mnt/"): 34 | case path == "/mount" || strings.HasPrefix(path, "/mount/"): 35 | case path == "/media" || strings.HasPrefix(path, "/media/"): 36 | case path == "/sys" || strings.HasPrefix(path, "/sys/"): 37 | case path == "/dev" || strings.HasPrefix(path, "/dev/"): 38 | default: 39 | return false 40 | } 41 | 42 | return true 43 | } 44 | -------------------------------------------------------------------------------- /container/volume_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package container 6 | 7 | import "testing" 8 | 9 | func TestIsRestrictedVolume(t *testing.T) { 10 | restrictedPaths := []string{ 11 | "/", 12 | "../../../../../../../../../../../../var/run", 13 | "/var/run", 14 | "//var/run", 15 | "/var/run/", 16 | "/var/run/.", 17 | "/var//run/", 18 | "/var/run//", 19 | "/var/run/test/..", 20 | "/./var/run", 21 | "/var/./run", 22 | } 23 | 24 | allowedPaths := []string{ 25 | "/drone", 26 | "/drone/var/run", 27 | "/development", 28 | "/var/lib", 29 | "/etc/ssh", 30 | } 31 | 32 | for _, path := range restrictedPaths { 33 | if result := IsRestrictedVolume(path); result != true { 34 | t.Errorf("Test failed for restricted path %q", path) 35 | } 36 | } 37 | 38 | for _, path := range allowedPaths { 39 | if result := IsRestrictedVolume(path); result != false { 40 | t.Errorf("Test failed for allowed path %q", path) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /environ/calver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package environ 6 | 7 | import ( 8 | "bytes" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | func calversions(s string) map[string]string { 14 | env := map[string]string{} 15 | 16 | version := parseCalver(s) 17 | if version == nil { 18 | return nil 19 | } 20 | 21 | // we try to determine if the major and minor 22 | // versions are valid years. 23 | if !isYear(version.Major) { 24 | return env 25 | } 26 | 27 | env["DRONE_CALVER"] = version.String() 28 | env["DRONE_CALVER_MAJOR_MINOR"] = version.Major + "." + version.Minor 29 | env["DRONE_CALVER_MAJOR"] = version.Major 30 | env["DRONE_CALVER_MINOR"] = version.Minor 31 | env["DRONE_CALVER_MICRO"] = version.Micro 32 | if version.Modifier != "" { 33 | env["DRONE_CALVER_MODIFIER"] = version.Modifier 34 | } 35 | 36 | version.Modifier = "" 37 | env["DRONE_CALVER_SHORT"] = version.String() 38 | return env 39 | } 40 | 41 | type calver struct { 42 | Major string 43 | Minor string 44 | Micro string 45 | Modifier string 46 | } 47 | 48 | // helper function that parses tags in the calendar version 49 | // format. note this is not a robust parser implementation 50 | // and mat fail to properly parse all strings. 51 | func parseCalver(s string) *calver { 52 | s = strings.TrimPrefix(s, "v") 53 | p := strings.SplitN(s, ".", 3) 54 | if len(p) < 2 { 55 | return nil 56 | } 57 | 58 | c := new(calver) 59 | c.Major = p[0] 60 | c.Minor = p[1] 61 | if len(p) > 2 { 62 | c.Micro = p[2] 63 | } 64 | 65 | switch { 66 | case strings.Contains(c.Micro, "-"): 67 | p := strings.SplitN(c.Micro, "-", 2) 68 | c.Micro = p[0] 69 | c.Modifier = p[1] 70 | } 71 | 72 | // the major and minor segments must be numbers to 73 | // conform to the calendar version spec. 74 | if !isNumber(c.Major) || 75 | !isNumber(c.Minor) { 76 | return nil 77 | } 78 | 79 | return c 80 | } 81 | 82 | // String returns the calendar version string. 83 | func (c *calver) String() string { 84 | var buf bytes.Buffer 85 | buf.WriteString(c.Major) 86 | buf.WriteString(".") 87 | buf.WriteString(c.Minor) 88 | if c.Micro != "" { 89 | buf.WriteString(".") 90 | buf.WriteString(c.Micro) 91 | } 92 | if c.Modifier != "" { 93 | buf.WriteString("-") 94 | buf.WriteString(c.Modifier) 95 | } 96 | return buf.String() 97 | } 98 | 99 | // helper function returns true if the string is a 100 | // valid number. 101 | func isNumber(s string) bool { 102 | _, err := strconv.Atoi(s) 103 | return err == nil 104 | } 105 | 106 | // helper function returns true if the string is a 107 | // valid year. This assumes a minimum year of 2019 108 | // for YYYY format and a minimum year of 19 for YY 109 | // format. 110 | // 111 | // TODO(bradrydzewski) if people are still using this 112 | // code in 2099 we need to adjust the minimum YY value. 113 | func isYear(s string) bool { 114 | i, _ := strconv.Atoi(s) 115 | return (i > 18 && i < 100) || (i > 2018 && i < 9999) 116 | } 117 | -------------------------------------------------------------------------------- /environ/calver_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package environ 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | ) 12 | 13 | func TestCalver(t *testing.T) { 14 | a := calversions("v19.1.0-beta.20190318") 15 | b := map[string]string{ 16 | "DRONE_CALVER": "19.1.0-beta.20190318", 17 | "DRONE_CALVER_MAJOR": "19", 18 | "DRONE_CALVER_MAJOR_MINOR": "19.1", 19 | "DRONE_CALVER_MINOR": "1", 20 | "DRONE_CALVER_MICRO": "0", 21 | "DRONE_CALVER_SHORT": "19.1.0", 22 | "DRONE_CALVER_MODIFIER": "beta.20190318", 23 | } 24 | if diff := cmp.Diff(a, b); diff != "" { 25 | t.Errorf("Unexpected calver variables") 26 | t.Log(diff) 27 | } 28 | } 29 | 30 | func TestCalverAlternate(t *testing.T) { 31 | a := calversions("2019.01.0002") 32 | b := map[string]string{ 33 | "DRONE_CALVER": "2019.01.0002", 34 | "DRONE_CALVER_MAJOR_MINOR": "2019.01", 35 | "DRONE_CALVER_MAJOR": "2019", 36 | "DRONE_CALVER_MINOR": "01", 37 | "DRONE_CALVER_MICRO": "0002", 38 | "DRONE_CALVER_SHORT": "2019.01.0002", 39 | } 40 | if diff := cmp.Diff(a, b); diff != "" { 41 | t.Errorf("Unexpected calver variables") 42 | t.Log(diff) 43 | } 44 | } 45 | 46 | func TestCalver_Invalid(t *testing.T) { 47 | tests := []string{ 48 | "1.2.3", 49 | "1.2", 50 | "1", 51 | "0.12", 52 | "0.12.1", 53 | } 54 | for _, s := range tests { 55 | envs := calversions(s) 56 | if len(envs) != 0 { 57 | t.Errorf("Expect invalid calversion: %s", s) 58 | } 59 | } 60 | } 61 | 62 | func TestCalverParser(t *testing.T) { 63 | tests := []struct { 64 | s string 65 | v *calver 66 | }{ 67 | {"09.01.02", &calver{"09", "01", "02", ""}}, 68 | {"2009.01.02", &calver{"2009", "01", "02", ""}}, 69 | {"2009.1.2", &calver{"2009", "1", "2", ""}}, 70 | {"09.1.2", &calver{"09", "1", "2", ""}}, 71 | {"9.1.2", &calver{"9", "1", "2", ""}}, 72 | {"v19.1.0-beta.20190318", &calver{"19", "1", "0", "beta.20190318"}}, 73 | 74 | // invalid values 75 | {"foo.bar.baz", nil}, 76 | {"foo.bar", nil}, 77 | {"foo.1", nil}, 78 | {"foo", nil}, 79 | {"1", nil}, 80 | {"1.foo", nil}, 81 | } 82 | 83 | for _, test := range tests { 84 | got, want := parseCalver(test.s), test.v 85 | if diff := cmp.Diff(got, want); diff != "" { 86 | t.Errorf("Unexpected calver %s", test.s) 87 | t.Log(diff) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /environ/expand.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package environ 6 | 7 | import "os" 8 | 9 | // function used to expand environment variables. 10 | var getenv = os.Getenv 11 | 12 | // Expand is a helper function to expand the PATH parameter in 13 | // the pipeline environment. 14 | func Expand(env map[string]string) map[string]string { 15 | c := map[string]string{} 16 | for k, v := range env { 17 | c[k] = v 18 | } 19 | if path := c["PATH"]; path != "" { 20 | c["PATH"] = os.Expand(path, getenv) 21 | } 22 | return c 23 | } 24 | -------------------------------------------------------------------------------- /environ/expand_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package environ 6 | 7 | import ( 8 | "os" 9 | "testing" 10 | ) 11 | 12 | func TestExpand(t *testing.T) { 13 | defer func() { 14 | getenv = os.Getenv 15 | }() 16 | 17 | getenv = func(string) string { 18 | return "/bin:/usr/local/bin" 19 | } 20 | 21 | before := map[string]string{ 22 | "USER": "root", 23 | "HOME": "/home/$USER", // does not expect 24 | "PATH": "/go/bin:$PATH", 25 | } 26 | 27 | after := Expand(before) 28 | if got, want := after["PATH"], "/go/bin:/bin:/usr/local/bin"; got != want { 29 | t.Errorf("Got PATH %q, want %q", got, want) 30 | } 31 | if got, want := after["USER"], "root"; got != want { 32 | t.Errorf("Got USER %q, want %q", got, want) 33 | } 34 | // only the PATH variable should expand. No other variables 35 | // should be expanded. 36 | if got, want := after["HOME"], "/home/$USER"; got != want { 37 | t.Errorf("Got HOME %q, want %q", got, want) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /environ/provider/combine.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package provider 6 | 7 | import "context" 8 | 9 | // Combine returns a new combined environment provider, 10 | // capable of sourcing environment variables from multiple 11 | // providers. 12 | func Combine(sources ...Provider) Provider { 13 | return &combined{sources} 14 | } 15 | 16 | type combined struct { 17 | sources []Provider 18 | } 19 | 20 | func (p *combined) List(ctx context.Context, in *Request) ([]*Variable, error) { 21 | var out []*Variable 22 | for _, source := range p.sources { 23 | got, err := source.List(ctx, in) 24 | if err != nil { 25 | return nil, err 26 | } 27 | out = append(out, got...) 28 | } 29 | return out, nil 30 | } 31 | -------------------------------------------------------------------------------- /environ/provider/combine_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package provider 6 | 7 | import ( 8 | "errors" 9 | "testing" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | func TestCombine(t *testing.T) { 15 | a := map[string]string{"a": "b"} 16 | b := map[string]string{"c": "d"} 17 | aa := Static(a) 18 | bb := Static(b) 19 | p := Combine(aa, bb) 20 | got, err := p.List(noContext, nil) 21 | if err != nil { 22 | t.Error(err) 23 | return 24 | } 25 | if len(got) != 2 { 26 | t.Errorf("Expect combined variable output") 27 | return 28 | } 29 | want := []*Variable{ 30 | { 31 | Name: "a", 32 | Data: "b", 33 | Mask: false, 34 | }, 35 | { 36 | Name: "c", 37 | Data: "d", 38 | Mask: false, 39 | }, 40 | } 41 | if diff := cmp.Diff(got, want); diff != "" { 42 | t.Errorf(diff) 43 | } 44 | } 45 | 46 | func TestCombineError(t *testing.T) { 47 | e := errors.New("not found") 48 | m := mockProvider{err: e} 49 | p := Combine(&m) 50 | _, err := p.List(noContext, nil) 51 | if err != e { 52 | t.Errorf("Expect error") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /environ/provider/external.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package provider 6 | 7 | import ( 8 | "context" 9 | "time" 10 | 11 | "github.com/drone/drone-go/plugin/environ" 12 | "github.com/drone/runner-go/logger" 13 | ) 14 | 15 | // MultiExternal returns a new environment provider that 16 | // is comprised of multiple external providers, and 17 | // aggregates their results. 18 | func MultiExternal(endpoints []string, token string, insecure bool) Provider { 19 | var sources []Provider 20 | for _, endpoint := range endpoints { 21 | sources = append(sources, External( 22 | endpoint, token, insecure, 23 | )) 24 | } 25 | return Combine(sources...) 26 | } 27 | 28 | // External returns a new external environment variable 29 | // provider. This provider makes an external API call to 30 | // list and return environment variables. 31 | func External(endpoint, token string, insecure bool) Provider { 32 | provider := &external{} 33 | if endpoint != "" { 34 | provider.client = environ.Client(endpoint, token, insecure) 35 | } 36 | return provider 37 | } 38 | 39 | type external struct { 40 | client environ.Plugin 41 | } 42 | 43 | func (p *external) List(ctx context.Context, in *Request) ([]*Variable, error) { 44 | if p.client == nil { 45 | return nil, nil 46 | } 47 | 48 | logger := logger.FromContext(ctx) 49 | 50 | // include a timeout to prevent an API call from 51 | // hanging the build process indefinitely. The 52 | // external service must return a request within 53 | // one minute. 54 | ctx, cancel := context.WithTimeout(ctx, time.Minute) 55 | defer cancel() 56 | 57 | req := &environ.Request{ 58 | Repo: *in.Repo, 59 | Build: *in.Build, 60 | } 61 | res, err := p.client.List(ctx, req) 62 | if err != nil { 63 | logger.WithError(err).Debug("environment: external: cannot get environment variable list") 64 | return nil, err 65 | } 66 | 67 | // if no error is returned and the list is empty, 68 | // this indicates the client returned No Content, 69 | // and we should exit with no credentials, but no error. 70 | if len(res) == 0 { 71 | logger.Trace("environment: external: environment variable list is empty") 72 | return nil, nil 73 | } 74 | 75 | logger.Trace("environment: external: environment variable list returned") 76 | 77 | var out []*Variable 78 | for _, v := range res { 79 | out = append(out, &Variable{ 80 | Name: v.Name, 81 | Data: v.Data, 82 | Mask: v.Mask, 83 | }) 84 | } 85 | return out, nil 86 | } 87 | -------------------------------------------------------------------------------- /environ/provider/external_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package provider 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "testing" 11 | 12 | "github.com/drone/drone-go/drone" 13 | "github.com/drone/drone-go/plugin/environ" 14 | "github.com/google/go-cmp/cmp" 15 | ) 16 | 17 | func TestExternal(t *testing.T) { 18 | req := &Request{ 19 | Build: &drone.Build{Event: drone.EventPush}, 20 | Repo: &drone.Repo{Private: false}, 21 | } 22 | res := []*environ.Variable{ 23 | { 24 | Name: "a", 25 | Data: "b", 26 | Mask: true, 27 | }, 28 | } 29 | 30 | want := []*Variable{{Name: "a", Data: "b", Mask: true}} 31 | provider := External("http://localhost", "secret", false) 32 | provider.(*external).client = &mockPlugin{out: res} 33 | got, err := provider.List(noContext, req) 34 | if err != nil { 35 | t.Error(err) 36 | } 37 | if diff := cmp.Diff(got, want); diff != "" { 38 | t.Errorf(diff) 39 | } 40 | } 41 | 42 | // This test verifies that if the remote API call to the 43 | // external plugin returns an error, the provider returns the 44 | // error to the caller. 45 | func TestExternal_ClientError(t *testing.T) { 46 | req := &Request{ 47 | Build: &drone.Build{Event: drone.EventPush}, 48 | Repo: &drone.Repo{Private: false}, 49 | } 50 | want := errors.New("Not Found") 51 | provider := External("http://localhost", "secret", false) 52 | provider.(*external).client = &mockPlugin{err: want} 53 | _, got := provider.List(noContext, req) 54 | if got != want { 55 | t.Errorf("Want error %s, got %s", want, got) 56 | } 57 | } 58 | 59 | // This test verifies that if no endpoint is configured the 60 | // provider exits immediately and returns a nil slice and nil 61 | // error. 62 | func TestExternal_NoEndpoint(t *testing.T) { 63 | provider := External("", "", false) 64 | res, err := provider.List(noContext, nil) 65 | if err != nil { 66 | t.Errorf("Expect nil error, provider disabled") 67 | } 68 | if res != nil { 69 | t.Errorf("Expect nil secret, provider disabled") 70 | } 71 | } 72 | 73 | // This test verifies that nil credentials and a nil error 74 | // are returned if the registry endpoint returns no content. 75 | func TestExternal_NotFound(t *testing.T) { 76 | req := &Request{ 77 | Repo: &drone.Repo{}, 78 | Build: &drone.Build{}, 79 | } 80 | provider := External("http://localhost", "secret", false) 81 | provider.(*external).client = &mockPlugin{} 82 | res, err := provider.List(noContext, req) 83 | if err != nil { 84 | t.Errorf("Expect nil error, registry list empty") 85 | } 86 | if res != nil { 87 | t.Errorf("Expect nil registry credentials") 88 | } 89 | } 90 | 91 | // This test verifies that multiple external providers 92 | // are combined into a single provider that concatenates 93 | // the results. 94 | func TestMultiExternal(t *testing.T) { 95 | provider := MultiExternal([]string{"https://foo", "https://bar"}, "correct-horse-batter-staple", true).(*combined) 96 | if len(provider.sources) != 2 { 97 | t.Errorf("Expect two provider sources") 98 | } 99 | } 100 | 101 | type mockPlugin struct { 102 | out []*environ.Variable 103 | err error 104 | } 105 | 106 | func (m *mockPlugin) List(context.Context, *environ.Request) ([]*environ.Variable, error) { 107 | return m.out, m.err 108 | } 109 | -------------------------------------------------------------------------------- /environ/provider/provider.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package provider provides environment variables to 6 | // a pipeline. 7 | package provider 8 | 9 | import ( 10 | "context" 11 | 12 | "github.com/drone/drone-go/drone" 13 | ) 14 | 15 | // Request provides arguments for requesting a environment 16 | // variables from an environment Provider. 17 | type Request struct { 18 | Repo *drone.Repo 19 | Build *drone.Build 20 | } 21 | 22 | // Variable defines an environment variable. 23 | type Variable struct { 24 | Name string 25 | Data string 26 | Mask bool 27 | } 28 | 29 | // Provider is the interface that must be implemented by an 30 | // environment provider. 31 | type Provider interface { 32 | // List returns a list of environment variables. 33 | List(context.Context, *Request) ([]*Variable, error) 34 | } 35 | -------------------------------------------------------------------------------- /environ/provider/provider_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package provider 6 | 7 | import ( 8 | "context" 9 | ) 10 | 11 | var noContext = context.Background() 12 | 13 | type mockProvider struct { 14 | out []*Variable 15 | err error 16 | } 17 | 18 | func (p *mockProvider) List(context.Context, *Request) ([]*Variable, error) { 19 | return p.out, p.err 20 | } 21 | -------------------------------------------------------------------------------- /environ/provider/static.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package provider 6 | 7 | import "context" 8 | 9 | // Static returns a new static environment variable provider. 10 | // The static provider finds and returns the static list 11 | // of static environment variables. 12 | func Static(params map[string]string) Provider { 13 | return &static{params} 14 | } 15 | 16 | type static struct { 17 | params map[string]string 18 | } 19 | 20 | func (p *static) List(context.Context, *Request) ([]*Variable, error) { 21 | return ToSlice(p.params), nil 22 | } 23 | -------------------------------------------------------------------------------- /environ/provider/static_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package provider 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | ) 12 | 13 | func TestStatic(t *testing.T) { 14 | in := map[string]string{"a": "b"} 15 | 16 | got, err := Static(in).List(noContext, nil) 17 | if err != nil { 18 | t.Error(err) 19 | return 20 | } 21 | 22 | want := []*Variable{ 23 | { 24 | Name: "a", 25 | Data: "b", 26 | }, 27 | } 28 | 29 | if diff := cmp.Diff(got, want); diff != "" { 30 | t.Errorf(diff) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /environ/provider/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package provider 6 | 7 | // ToMap is a helper function that converts a list of 8 | // variables to a map. 9 | func ToMap(src []*Variable) map[string]string { 10 | dst := map[string]string{} 11 | for _, v := range src { 12 | dst[v.Name] = v.Data 13 | } 14 | return dst 15 | } 16 | 17 | // ToSlice is a helper function that converts a map of 18 | // environment variables to a slice. 19 | func ToSlice(src map[string]string) []*Variable { 20 | var dst []*Variable 21 | for k, v := range src { 22 | dst = append(dst, &Variable{ 23 | Name: k, 24 | Data: v, 25 | }) 26 | } 27 | return dst 28 | } 29 | 30 | // FilterMasked is a helper function that filters a list of 31 | // variable to return a list of masked variables only. 32 | func FilterMasked(v []*Variable) []*Variable { 33 | var filtered []*Variable 34 | for _, vv := range v { 35 | if vv.Mask { 36 | filtered = append(filtered, vv) 37 | } 38 | } 39 | return filtered 40 | } 41 | 42 | // FilterUnmasked is a helper function that filters a list of 43 | // variable to return a list of masked variables only. 44 | func FilterUnmasked(v []*Variable) []*Variable { 45 | var filtered []*Variable 46 | for _, vv := range v { 47 | if vv.Mask == false { 48 | filtered = append(filtered, vv) 49 | } 50 | } 51 | return filtered 52 | } 53 | -------------------------------------------------------------------------------- /environ/provider/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package provider 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | ) 12 | 13 | func TestToMap(t *testing.T) { 14 | in := []*Variable{ 15 | { 16 | Name: "foo", 17 | Data: "bar", 18 | }, 19 | } 20 | want := map[string]string{ 21 | "foo": "bar", 22 | } 23 | got := ToMap(in) 24 | if diff := cmp.Diff(want, got); diff != "" { 25 | t.Log(diff) 26 | t.Errorf("Unexpected map value") 27 | } 28 | } 29 | 30 | func TestFromMap(t *testing.T) { 31 | in := map[string]string{ 32 | "foo": "bar", 33 | } 34 | want := []*Variable{ 35 | { 36 | Name: "foo", 37 | Data: "bar", 38 | }, 39 | } 40 | got := ToSlice(in) 41 | if diff := cmp.Diff(want, got); diff != "" { 42 | t.Log(diff) 43 | t.Errorf("Unexpected variable list") 44 | } 45 | } 46 | 47 | func TestFilterMasked(t *testing.T) { 48 | in := []*Variable{ 49 | { 50 | Name: "foo", 51 | Data: "bar", 52 | Mask: false, 53 | }, 54 | { 55 | Name: "baz", 56 | Data: "qux", 57 | Mask: true, 58 | }, 59 | } 60 | want := in[1:] 61 | got := FilterMasked(in) 62 | if diff := cmp.Diff(want, got); diff != "" { 63 | t.Log(diff) 64 | t.Errorf("Unexpected variable list") 65 | } 66 | } 67 | 68 | func TestFilterUnmasked(t *testing.T) { 69 | in := []*Variable{ 70 | { 71 | Name: "foo", 72 | Data: "bar", 73 | Mask: true, 74 | }, 75 | { 76 | Name: "baz", 77 | Data: "qux", 78 | Mask: false, 79 | }, 80 | } 81 | want := in[1:] 82 | got := FilterUnmasked(in) 83 | if diff := cmp.Diff(want, got); diff != "" { 84 | t.Log(diff) 85 | t.Errorf("Unexpected variable list") 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /environ/proxy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package environ 6 | 7 | import ( 8 | "strings" 9 | ) 10 | 11 | // Proxy returns the http_proxy variables. 12 | func Proxy() map[string]string { 13 | environ := map[string]string{} 14 | if value := envAnyCase("no_proxy"); value != "" { 15 | environ["no_proxy"] = value 16 | environ["NO_PROXY"] = value 17 | } 18 | if value := envAnyCase("http_proxy"); value != "" { 19 | environ["http_proxy"] = value 20 | environ["HTTP_PROXY"] = value 21 | } 22 | if value := envAnyCase("https_proxy"); value != "" { 23 | environ["https_proxy"] = value 24 | environ["HTTPS_PROXY"] = value 25 | } 26 | if value := envAnyCase("all_proxy"); value != "" { 27 | environ["all_proxy"] = value 28 | environ["ALL_PROXY"] = value 29 | } 30 | return environ 31 | } 32 | 33 | // helper function returns the environment variable value 34 | // using a case-insenstive environment name. 35 | func envAnyCase(name string) (value string) { 36 | name = strings.ToUpper(name) 37 | if value := getenv(name); value != "" { 38 | return value 39 | } 40 | name = strings.ToLower(name) 41 | if value := getenv(name); value != "" { 42 | return value 43 | } 44 | return 45 | } 46 | -------------------------------------------------------------------------------- /environ/proxy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package environ 6 | 7 | import ( 8 | "os" 9 | "testing" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | func TestProxy(t *testing.T) { 15 | defer func() { 16 | getenv = os.Getenv 17 | }() 18 | 19 | getenv = func(s string) string { 20 | switch s { 21 | case "no_proxy": 22 | return "http://dummy.no.proxy" 23 | case "http_proxy": 24 | return "http://dummy.http.proxy" 25 | case "https_proxy": 26 | return "http://dummy.https.proxy" 27 | case "all_proxy": 28 | return "http://dummy.https.proxy" 29 | default: 30 | return "" 31 | } 32 | } 33 | 34 | a := map[string]string{ 35 | "no_proxy": "http://dummy.no.proxy", 36 | "NO_PROXY": "http://dummy.no.proxy", 37 | "http_proxy": "http://dummy.http.proxy", 38 | "HTTP_PROXY": "http://dummy.http.proxy", 39 | "https_proxy": "http://dummy.https.proxy", 40 | "HTTPS_PROXY": "http://dummy.https.proxy", 41 | "all_proxy": "http://dummy.https.proxy", 42 | "ALL_PROXY": "http://dummy.https.proxy", 43 | } 44 | b := Proxy() 45 | if diff := cmp.Diff(a, b); diff != "" { 46 | t.Fail() 47 | t.Log(diff) 48 | } 49 | } 50 | 51 | func Test_envAnyCase(t *testing.T) { 52 | defer func() { 53 | getenv = os.Getenv 54 | }() 55 | 56 | getenv = func(s string) string { 57 | switch s { 58 | case "foo": 59 | return "bar" 60 | default: 61 | return "" 62 | } 63 | } 64 | 65 | if envAnyCase("FOO") != "bar" { 66 | t.Errorf("Expect environment variable sourced from lowercase variant") 67 | } 68 | 69 | getenv = func(s string) string { 70 | switch s { 71 | case "FOO": 72 | return "bar" 73 | default: 74 | return "" 75 | } 76 | } 77 | 78 | if envAnyCase("foo") != "bar" { 79 | t.Errorf("Expect environment variable sourced from uppercase variant") 80 | } 81 | 82 | if envAnyCase("bar") != "" { 83 | t.Errorf("Expect zero value string when environment variable does not exit") 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /environ/semver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package environ 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/coreos/go-semver/semver" 12 | ) 13 | 14 | // helper function returns a list of environment variables 15 | // that represent the semantic version. 16 | func versions(s string) map[string]string { 17 | env := map[string]string{} 18 | 19 | s = strings.TrimPrefix(s, "v") 20 | version, err := semver.NewVersion(s) 21 | if err != nil { 22 | env["DRONE_SEMVER_ERROR"] = err.Error() 23 | return env 24 | } 25 | 26 | env["DRONE_SEMVER"] = version.String() 27 | env["DRONE_SEMVER_MAJOR"] = fmt.Sprint(version.Major) 28 | env["DRONE_SEMVER_MINOR"] = fmt.Sprint(version.Minor) 29 | env["DRONE_SEMVER_PATCH"] = fmt.Sprint(version.Patch) 30 | if s := string(version.PreRelease); s != "" { 31 | env["DRONE_SEMVER_PRERELEASE"] = s 32 | } 33 | if version.Metadata != "" { 34 | env["DRONE_SEMVER_BUILD"] = version.Metadata 35 | } 36 | version.Metadata = "" 37 | version.PreRelease = "" 38 | env["DRONE_SEMVER_SHORT"] = version.String() 39 | return env 40 | } 41 | -------------------------------------------------------------------------------- /environ/semver_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package environ 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | ) 12 | 13 | func TestInvalidSemver(t *testing.T) { 14 | a := versions("this is an invalid version") 15 | b := map[string]string{"DRONE_SEMVER_ERROR": "this is an invalid version is not in dotted-tri format"} 16 | if diff := cmp.Diff(a, b); diff != "" { 17 | t.Errorf("Unexpected semver variables") 18 | t.Log(diff) 19 | } 20 | } 21 | 22 | func TestSemver(t *testing.T) { 23 | a := versions("v1.2.3-alpha+001") 24 | b := map[string]string{ 25 | "DRONE_SEMVER": "1.2.3-alpha+001", 26 | "DRONE_SEMVER_MAJOR": "1", 27 | "DRONE_SEMVER_MINOR": "2", 28 | "DRONE_SEMVER_PATCH": "3", 29 | "DRONE_SEMVER_SHORT": "1.2.3", 30 | "DRONE_SEMVER_PRERELEASE": "alpha", 31 | "DRONE_SEMVER_BUILD": "001", 32 | } 33 | if diff := cmp.Diff(a, b); diff != "" { 34 | t.Errorf("Unexpected semver variables") 35 | t.Log(diff) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/drone/runner-go 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d 7 | github.com/bmatcuk/doublestar v1.1.1 8 | github.com/buildkite/yaml v2.1.0+incompatible 9 | github.com/coreos/go-semver v0.3.0 10 | github.com/docker/go-units v0.4.0 11 | github.com/drone/drone-go v1.7.1 12 | github.com/drone/envsubst v1.0.2 13 | github.com/google/go-cmp v0.3.0 14 | github.com/hashicorp/go-multierror v1.0.0 15 | github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 16 | github.com/sirupsen/logrus v1.4.2 17 | golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 18 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 19 | ) 20 | -------------------------------------------------------------------------------- /handler/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package handler provides HTTP handlers that expose pipeline 6 | // state and status. 7 | package handler 8 | 9 | import ( 10 | "net/http" 11 | "sort" 12 | "strconv" 13 | 14 | "github.com/drone/drone-go/drone" 15 | hook "github.com/drone/runner-go/logger/history" 16 | "github.com/drone/runner-go/pipeline/reporter/history" 17 | ) 18 | 19 | // HandleHealth returns a http.HandlerFunc that returns a 200 20 | // if the service is healthly. 21 | func HandleHealth(t *history.History) http.HandlerFunc { 22 | return func(w http.ResponseWriter, r *http.Request) { 23 | // TODO(bradrydzewski) iterate through the list of 24 | // pending or running stages and write an error message 25 | // if the timeout is exceeded. 26 | nocache(w) 27 | w.WriteHeader(200) 28 | } 29 | } 30 | 31 | // HandleIndex returns a http.HandlerFunc that displays a list 32 | // of currently and previously executed builds. 33 | func HandleIndex(t *history.History) http.HandlerFunc { 34 | return func(w http.ResponseWriter, r *http.Request) { 35 | d := t.Entries() 36 | 37 | s1 := history.ByTimestamp(d) 38 | s2 := history.ByStatus(d) 39 | sort.Sort(s1) 40 | sort.Sort(s2) 41 | 42 | if r.Header.Get("Accept") == "application/json" { 43 | nocache(w) 44 | renderJSON(w, d) 45 | } else { 46 | nocache(w) 47 | render(w, "index.tmpl", &data{Items: d}) 48 | } 49 | } 50 | } 51 | 52 | // HandleStage returns a http.HandlerFunc that displays the 53 | // stage details. 54 | func HandleStage(hist *history.History, logger *hook.Hook) http.HandlerFunc { 55 | return func(w http.ResponseWriter, r *http.Request) { 56 | id, _ := strconv.ParseInt(r.FormValue("id"), 10, 64) 57 | 58 | // filter logs by stage id. 59 | logs := logger.Filter(func(entry *hook.Entry) bool { 60 | return entry.Data["stage.id"] == id 61 | }) 62 | 63 | // find pipeline by stage id 64 | entry := hist.Entry(id) 65 | if entry == nil { 66 | w.WriteHeader(404) 67 | return 68 | } 69 | 70 | nocache(w) 71 | render(w, "stage.tmpl", struct { 72 | *history.Entry 73 | Logs []*hook.Entry 74 | }{ 75 | Entry: entry, 76 | Logs: logs, 77 | }) 78 | } 79 | } 80 | 81 | // HandleLogHistory returns a http.HandlerFunc that displays a 82 | // list recent log entries. 83 | func HandleLogHistory(t *hook.Hook) http.HandlerFunc { 84 | return func(w http.ResponseWriter, r *http.Request) { 85 | nocache(w) 86 | render(w, "logs.tmpl", struct { 87 | Entries []*hook.Entry 88 | }{t.Entries()}) 89 | } 90 | } 91 | 92 | // data is a template data structure that provides helper 93 | // functions for calculating the system state. 94 | type data struct { 95 | Items []*history.Entry 96 | } 97 | 98 | // helper function returns true if no running builds exists. 99 | func (d *data) Idle() bool { 100 | for _, item := range d.Items { 101 | switch item.Stage.Status { 102 | case drone.StatusPending, drone.StatusRunning: 103 | return false 104 | } 105 | } 106 | return true 107 | } 108 | -------------------------------------------------------------------------------- /handler/handler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package handler 6 | -------------------------------------------------------------------------------- /handler/nocache.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package handler 6 | 7 | import ( 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | // unix epoch time 13 | var epoch = time.Unix(0, 0).Format(time.RFC1123) 14 | 15 | // http headers to disable caching. 16 | var noCacheHeaders = map[string]string{ 17 | "Expires": epoch, 18 | "Cache-Control": "no-cache, private, max-age=0", 19 | "Pragma": "no-cache", 20 | "X-Accel-Expires": "0", 21 | } 22 | 23 | // helper function to prevent http response caching. 24 | func nocache(w http.ResponseWriter) { 25 | for k, v := range noCacheHeaders { 26 | w.Header().Set(k, v) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /handler/nocache_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package handler 6 | -------------------------------------------------------------------------------- /handler/render.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package handler 6 | 7 | import ( 8 | "encoding/json" 9 | "net/http" 10 | 11 | "github.com/drone/runner-go/handler/template" 12 | ) 13 | 14 | // renderJSON writes the json-encoded representation of v to 15 | // the response body. 16 | func renderJSON(w http.ResponseWriter, v interface{}) { 17 | for k, v := range noCacheHeaders { 18 | w.Header().Set(k, v) 19 | } 20 | w.Header().Set("Content-Type", "application/json") 21 | enc := json.NewEncoder(w) 22 | enc.SetIndent("", " ") 23 | enc.Encode(v) 24 | } 25 | 26 | // render writes the template to the response body. 27 | func render(w http.ResponseWriter, t string, v interface{}) { 28 | w.Header().Set("Content-Type", "text/html") 29 | template.T.ExecuteTemplate(w, t, v) 30 | } 31 | -------------------------------------------------------------------------------- /handler/render_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package handler 6 | -------------------------------------------------------------------------------- /handler/router/router.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package router 6 | 7 | import ( 8 | "net/http" 9 | 10 | "github.com/drone/runner-go/handler" 11 | "github.com/drone/runner-go/handler/static" 12 | hook "github.com/drone/runner-go/logger/history" 13 | "github.com/drone/runner-go/pipeline/reporter/history" 14 | 15 | "github.com/99designs/basicauth-go" 16 | ) 17 | 18 | // Config provides router configuration. 19 | type Config struct { 20 | Username string 21 | Password string 22 | Realm string 23 | } 24 | 25 | // New returns a new route handler. 26 | func New(tracer *history.History, history *hook.Hook, config Config) http.Handler { 27 | mux := http.NewServeMux() 28 | mux.HandleFunc("/healthz", handler.HandleHealth(tracer)) 29 | 30 | // omit dashboard handlers when no password configured. 31 | if config.Password == "" { 32 | return mux 33 | } 34 | 35 | // middleware to require basic authentication. 36 | auth := basicauth.New(config.Realm, map[string][]string{ 37 | config.Username: {config.Password}, 38 | }) 39 | 40 | // handler to serve static assets for the dashboard. 41 | fs := http.FileServer(static.New()) 42 | 43 | // dashboard handles. 44 | mux.Handle("/static/", http.StripPrefix("/static/", fs)) 45 | mux.Handle("/logs", auth(handler.HandleLogHistory(history))) 46 | mux.Handle("/view", auth(handler.HandleStage(tracer, history))) 47 | mux.Handle("/", auth(handler.HandleIndex(tracer))) 48 | return mux 49 | } 50 | -------------------------------------------------------------------------------- /handler/router/router_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package router 6 | -------------------------------------------------------------------------------- /handler/static/files/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drone/runner-go/d12a95813c3667b036fc6f574c74a3b5ba5e804c/handler/static/files/favicon.png -------------------------------------------------------------------------------- /handler/static/files/icons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /handler/static/files/icons/failure.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /handler/static/files/icons/pending.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /handler/static/files/icons/running.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /handler/static/files/icons/skipped.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /handler/static/files/icons/sleeping.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 27 | 28 | 30 | 31 | 32 | 33 | 36 | 37 | -------------------------------------------------------------------------------- /handler/static/files/icons/success.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /handler/static/files/reset.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-size: 100%; 18 | font: inherit; 19 | vertical-align: baseline; 20 | } 21 | /* HTML5 display-role reset for older browsers */ 22 | article, aside, details, figcaption, figure, 23 | footer, header, hgroup, menu, nav, section { 24 | display: block; 25 | } 26 | body { 27 | line-height: 1; 28 | } 29 | ol, ul { 30 | list-style: none; 31 | } 32 | blockquote, q { 33 | quotes: none; 34 | } 35 | blockquote:before, blockquote:after, 36 | q:before, q:after { 37 | content: ''; 38 | content: none; 39 | } 40 | table { 41 | border-collapse: collapse; 42 | border-spacing: 0; 43 | } -------------------------------------------------------------------------------- /handler/static/static.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package static 6 | 7 | //go:generate togo http -package static -output static_gen.go 8 | -------------------------------------------------------------------------------- /handler/template/files/index.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dashboard 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 25 | 26 |
27 |
28 |
29 |

Dashboard

30 |
31 | 52 |
53 |
54 | 55 |
56 | 57 | 60 | 61 | -------------------------------------------------------------------------------- /handler/template/files/logs.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dashboard 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 24 | 25 |
26 |
27 |
28 |

Recent Logs

29 |
30 | {{ if .Entries }} 31 |
32 | {{ range .Entries }} 33 |
34 | {{ .Level }} 35 | {{ .Message }} 36 | 37 | {{ range $key, $val := .Data }} 38 | {{ $key }}{{ $val }} 39 | {{ end }} 40 | 41 | 42 |
43 | {{ end }} 44 |
45 | {{ else }} 46 |
47 |

There is no recent log activity to display.

48 |
49 | {{ end }} 50 |
51 |
52 | 53 |
54 | 55 | 58 | 59 | -------------------------------------------------------------------------------- /handler/template/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // +build ignore 6 | 7 | package main 8 | 9 | import ( 10 | "encoding/json" 11 | "html/template" 12 | "io/ioutil" 13 | "log" 14 | "net/http" 15 | "path/filepath" 16 | "regexp" 17 | "strings" 18 | "time" 19 | ) 20 | 21 | func main() { 22 | addr := ":3333" 23 | 24 | // serve templates with dummy data 25 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 26 | path := r.FormValue("data") 27 | if path == "" { 28 | http.Error(w, "missing data parameter", 500) 29 | return 30 | } 31 | 32 | tmpl := r.FormValue("template") 33 | if path == "" { 34 | http.Error(w, "missing template parameter", 500) 35 | return 36 | } 37 | 38 | // read the json data from file. 39 | rawjson, err := ioutil.ReadFile(filepath.Join("testdata", path)) 40 | if err != nil { 41 | http.Error(w, "cannot open json file", 500) 42 | return 43 | } 44 | 45 | // unmarshal the json data 46 | data := map[string]interface{}{} 47 | err = json.Unmarshal(rawjson, &data) 48 | if err != nil { 49 | http.Error(w, err.Error(), 500) 50 | return 51 | } 52 | 53 | // load the templates 54 | T := template.New("_").Funcs(funcMap) 55 | matches, _ := filepath.Glob("files/*.tmpl") 56 | for _, match := range matches { 57 | raw, _ := ioutil.ReadFile(match) 58 | base := filepath.Base(match) 59 | T = template.Must( 60 | T.New(base).Parse(string(raw)), 61 | ) 62 | } 63 | 64 | // render the template 65 | w.Header().Set("Content-Type", "text/html") 66 | err = T.ExecuteTemplate(w, tmpl, data) 67 | if err != nil { 68 | log.Println(err) 69 | } 70 | }) 71 | 72 | // serve static content. 73 | http.Handle("/static/", 74 | http.StripPrefix("/static/", 75 | http.FileServer( 76 | http.Dir("../static/files"), 77 | ), 78 | ), 79 | ) 80 | 81 | log.Printf("listening at %s", addr) 82 | log.Fatalln(http.ListenAndServe(addr, nil)) 83 | } 84 | 85 | // regular expression to extract the pull request number 86 | // from the git ref (e.g. refs/pulls/{d}/head) 87 | var re = regexp.MustCompile("\\d+") 88 | 89 | // mirros the func map in template.go 90 | var funcMap = map[string]interface{}{ 91 | "timestamp": func(v float64) string { 92 | return time.Unix(int64(v), 0).UTC().Format("2006-01-02T15:04:05Z") 93 | }, 94 | "pr": func(s string) string { 95 | return re.FindString(s) 96 | }, 97 | "sha": func(s string) string { 98 | if len(s) > 8 { 99 | s = s[:8] 100 | } 101 | return s 102 | }, 103 | "tag": func(s string) string { 104 | return strings.TrimPrefix(s, "refs/tags/") 105 | }, 106 | "done": func(s string) bool { 107 | return s != "pending" && s != "running" 108 | }, 109 | } 110 | -------------------------------------------------------------------------------- /handler/template/template.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package template 6 | 7 | import ( 8 | "regexp" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | //go:generate togo tmpl -func funcMap -format html 14 | 15 | // regular expression to extract the pull request number 16 | // from the git ref (e.g. refs/pulls/{d}/head) 17 | var re = regexp.MustCompile("\\d+") 18 | 19 | // mirros the func map in template.go 20 | var funcMap = map[string]interface{}{ 21 | "timestamp": func(v int64) string { 22 | return time.Unix(v, 0).UTC().Format("2006-01-02T15:04:05Z") 23 | }, 24 | "pr": func(s string) string { 25 | return re.FindString(s) 26 | }, 27 | "sha": func(s string) string { 28 | if len(s) > 8 { 29 | s = s[:8] 30 | } 31 | return s 32 | }, 33 | "tag": func(s string) string { 34 | return strings.TrimPrefix(s, "refs/tags/") 35 | }, 36 | "done": func(s string) bool { 37 | return s != "pending" && s != "running" 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /handler/template/testdata/logs.json: -------------------------------------------------------------------------------- 1 | { 2 | "Entries": [ 3 | { 4 | "Level": "trace", 5 | "Message": "this is a test trace message", 6 | "Data": { "foo": "bar", "baz": "boo" }, 7 | "Unix": 1563058875 8 | }, 9 | { 10 | "Level": "debug", 11 | "Message": "this is a test debug message", 12 | "Data": { "foo": "bar", "baz": "boo" }, 13 | "Unix": 1563058875 14 | }, 15 | { 16 | "Level": "info", 17 | "Message": "this is an info trace message", 18 | "Data": { "foo": "bar", "baz": "boo" }, 19 | "Unix": 1563058975 20 | }, 21 | { 22 | "Level": "warn", 23 | "Message": "this is a test warning message", 24 | "Data": { "foo": "bar", "baz": "boo" }, 25 | "Unix": 1563058977 26 | }, 27 | { 28 | "Level": "error", 29 | "Message": "this is a test error message", 30 | "Data": { "foo": "bar", "baz": "boo" }, 31 | "Unix": 1563059000 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /handler/template/testdata/logs_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "Entries": [] 3 | } -------------------------------------------------------------------------------- /handler/template/testdata/stage.json: -------------------------------------------------------------------------------- 1 | { 2 | "Repo": { 3 | "Slug": "octocat/hello-world", 4 | "Name": "hello-world" 5 | }, 6 | "Build": { 7 | "After": "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", 8 | "Event": "push", 9 | "Source": "master", 10 | "Target": "master", 11 | "Message": "Merge pull request #6 from Spaceghost/patch-1", 12 | "Author": "octocat", 13 | "AuthorAvatar": "https://avatars0.githubusercontent.com/u/583231?s=460&v=4", 14 | "Number": 42 15 | }, 16 | "Stage": { 17 | "Name": "test", 18 | "Status": "running", 19 | "Started": 1563059000, 20 | "Created": 1563059000, 21 | "Steps": [ 22 | { "Name": "clone", "Status": "success" }, 23 | { "Name": "build", "Status": "running" }, 24 | { "Name": "test", "Status": "success" }, 25 | { "Name": "deploy", "Status": "success" } 26 | ] 27 | }, 28 | "Logs": [ 29 | { 30 | "Level": "debug", 31 | "Message": "updated stage to running", 32 | "Data": { 33 | "build.id": 110, 34 | "build.number": 110, 35 | "repo.id": 48, 36 | "repo.name": "hello-world", 37 | "repo.namespace": "octocat", 38 | "stage.id": 110, 39 | "stage.name": "test", 40 | "stage.number": 1, 41 | "thread": 1 42 | }, 43 | "Unix": 1563058875 44 | }, 45 | { 46 | "Level": "debug", 47 | "Message": "process started", 48 | "Data": { 49 | "build.id": 110, 50 | "build.number": 110, 51 | "repo.id": 48, 52 | "repo.name": "hello-world", 53 | "repo.namespace": "octocat", 54 | "stage.id": 110, 55 | "stage.name": "test", 56 | "stage.number": 1, 57 | "thread": 1 58 | }, 59 | "Unix": 1563058875 60 | }, 61 | { 62 | "Level": "debug", 63 | "Message": "process finished", 64 | "Data": { 65 | "build.id": 110, 66 | "build.number": 110, 67 | "repo.id": 48, 68 | "repo.name": "hello-world", 69 | "repo.namespace": "octocat", 70 | "stage.id": 110, 71 | "stage.name": "test", 72 | "stage.number": 1, 73 | "thread": 1 74 | }, 75 | "Unix": 1563058975 76 | }, 77 | { 78 | "Level": "debug", 79 | "Message": "updated stage to complete", 80 | "Data": { 81 | "build.id": 110, 82 | "build.number": 110, 83 | "repo.id": 48, 84 | "repo.name": "hello-world", 85 | "repo.namespace": "octocat", 86 | "stage.id": 110, 87 | "stage.name": "test", 88 | "stage.number": 1, 89 | "thread": 1 90 | }, 91 | "Unix": 1563058977 92 | } 93 | ] 94 | } 95 | -------------------------------------------------------------------------------- /handler/template/testdata/stage_cron.json: -------------------------------------------------------------------------------- 1 | { 2 | "Repo": { 3 | "Slug": "octocat/hello-world", 4 | "Name": "hello-world" 5 | }, 6 | "Build": { 7 | "After": "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", 8 | "Event": "cron", 9 | "Cron": "nightly", 10 | "Source": "master", 11 | "Target": "master", 12 | "Deploy": "production", 13 | "Message": "Merge pull request #6 from Spaceghost/patch-1", 14 | "Author": "octocat", 15 | "AuthorAvatar": "https://avatars0.githubusercontent.com/u/583231?s=460&v=4", 16 | "Parent": 41, 17 | "Number": 42 18 | }, 19 | "Stage": { 20 | "Name": "test", 21 | "Status": "success", 22 | "Started": 1563059000, 23 | "Created": 1563059000, 24 | "Steps": [ 25 | { "Name": "clone", "Status": "success" }, 26 | { "Name": "build", "Status": "success" }, 27 | { "Name": "test", "Status": "success" }, 28 | { "Name": "deploy", "Status": "success" } 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /handler/template/testdata/stage_promote.json: -------------------------------------------------------------------------------- 1 | { 2 | "Repo": { 3 | "Slug": "octocat/hello-world", 4 | "Name": "hello-world" 5 | }, 6 | "Build": { 7 | "After": "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", 8 | "Event": "promote", 9 | "Source": "master", 10 | "Target": "master", 11 | "Deploy": "production", 12 | "Message": "Merge pull request #6 from Spaceghost/patch-1", 13 | "Author": "octocat", 14 | "AuthorAvatar": "https://avatars0.githubusercontent.com/u/583231?s=460&v=4", 15 | "Parent": 41, 16 | "Number": 42 17 | }, 18 | "Stage": { 19 | "Name": "test", 20 | "Status": "success", 21 | "Started": 1563059000, 22 | "Created": 1563059000, 23 | "Steps": [ 24 | { "Name": "clone", "Status": "success" }, 25 | { "Name": "build", "Status": "success" }, 26 | { "Name": "test", "Status": "success" }, 27 | { "Name": "deploy", "Status": "success" } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /handler/template/testdata/stage_pull_request.json: -------------------------------------------------------------------------------- 1 | { 2 | "Repo": { 3 | "Slug": "octocat/hello-world", 4 | "Name": "hello-world" 5 | }, 6 | "Build": { 7 | "Ref": "refs/pull/42/head", 8 | "After": "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", 9 | "Event": "pull_request", 10 | "Action": "opened", 11 | "Source": "master", 12 | "Target": "master", 13 | "Message": "Merge pull request #6 from Spaceghost/patch-1", 14 | "Author": "octocat", 15 | "AuthorAvatar": "https://avatars0.githubusercontent.com/u/583231?s=460&v=4", 16 | "Number": 42 17 | }, 18 | "Stage": { 19 | "Name": "test", 20 | "Status": "success", 21 | "Started": 1563059000, 22 | "Created": 1563059000, 23 | "Steps": [ 24 | { "Name": "clone", "Status": "success" }, 25 | { "Name": "build", "Status": "success" }, 26 | { "Name": "test", "Status": "success" }, 27 | { "Name": "deploy", "Status": "success" } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /handler/template/testdata/stage_rollback.json: -------------------------------------------------------------------------------- 1 | { 2 | "Repo": { 3 | "Slug": "octocat/hello-world", 4 | "Name": "hello-world" 5 | }, 6 | "Build": { 7 | "After": "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", 8 | "Event": "rollback", 9 | "Source": "master", 10 | "Target": "master", 11 | "Deploy": "production", 12 | "Message": "Merge pull request #6 from Spaceghost/patch-1", 13 | "Author": "octocat", 14 | "AuthorAvatar": "https://avatars0.githubusercontent.com/u/583231?s=460&v=4", 15 | "Parent": 41, 16 | "Number": 42 17 | }, 18 | "Stage": { 19 | "Name": "test", 20 | "Status": "success", 21 | "Started": 1563059000, 22 | "Created": 1563059000, 23 | "Steps": [ 24 | { "Name": "clone", "Status": "success" }, 25 | { "Name": "build", "Status": "success" }, 26 | { "Name": "test", "Status": "success" }, 27 | { "Name": "deploy", "Status": "success" } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /handler/template/testdata/stage_tag.json: -------------------------------------------------------------------------------- 1 | { 2 | "Repo": { 3 | "Slug": "octocat/hello-world", 4 | "Name": "hello-world" 5 | }, 6 | "Build": { 7 | "After": "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", 8 | "Event": "tag", 9 | "Source": "master", 10 | "Target": "master", 11 | "Ref": "refs/tags/v1.0.0", 12 | "Message": "Merge pull request #6 from Spaceghost/patch-1", 13 | "Author": "octocat", 14 | "AuthorAvatar": "https://avatars0.githubusercontent.com/u/583231?s=460&v=4", 15 | "Number": 42 16 | }, 17 | "Stage": { 18 | "Name": "test", 19 | "Status": "success", 20 | "Started": 1563059000, 21 | "Created": 1563059000, 22 | "Steps": [ 23 | { "Name": "clone", "Status": "success" }, 24 | { "Name": "build", "Status": "success" }, 25 | { "Name": "test", "Status": "success" }, 26 | { "Name": "deploy", "Status": "success" } 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /handler/template/testdata/stages.json: -------------------------------------------------------------------------------- 1 | { 2 | "Items": [ 3 | { 4 | "Idle": false, 5 | "Repo": { 6 | "Slug": "octocat/hello-world" 7 | }, 8 | "Build": { 9 | "AuthorAvatar": "https://avatars0.githubusercontent.com/u/583231?s=460&v=4", 10 | "Number": 42 11 | }, 12 | "Stage": { 13 | "ID": 1, 14 | "Name": "test", 15 | "Status": "running", 16 | "Started": 1563059000, 17 | "Created": 1563059000 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /handler/template/testdata/stages_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "Idle": true, 3 | "Items": [] 4 | } -------------------------------------------------------------------------------- /handler/template/testdata/stages_idle.json: -------------------------------------------------------------------------------- 1 | { 2 | "Idle": true, 3 | "Items": [ 4 | { 5 | "Repo": { 6 | "Slug": "octocat/hello-world" 7 | }, 8 | "Build": { 9 | "AuthorAvatar": "https://avatars0.githubusercontent.com/u/583231?s=460&v=4", 10 | "Number": 42 11 | }, 12 | "Stage": { 13 | "ID": 1, 14 | "Name": "test", 15 | "Status": "success", 16 | "Started": 1563059000, 17 | "Created": 1563059000 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /internal/clone.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package internal 6 | 7 | import "github.com/drone/drone-go/drone" 8 | 9 | // CloneRepo returns a copy of the Repository. 10 | func CloneRepo(src *drone.Repo) *drone.Repo { 11 | dst := new(drone.Repo) 12 | *dst = *src 13 | return dst 14 | } 15 | 16 | // CloneBuild returns a copy of the Build. 17 | func CloneBuild(src *drone.Build) *drone.Build { 18 | dst := new(drone.Build) 19 | *dst = *src 20 | dst.Stages = append(src.Stages[:0:0], src.Stages...) 21 | dst.Params = map[string]string{} 22 | for k, v := range src.Params { 23 | dst.Params[k] = v 24 | } 25 | for i, v := range src.Stages { 26 | dst.Stages[i] = CloneStage(v) 27 | } 28 | return dst 29 | } 30 | 31 | // CloneStage returns a copy of the Stage. 32 | func CloneStage(src *drone.Stage) *drone.Stage { 33 | dst := new(drone.Stage) 34 | *dst = *src 35 | dst.DependsOn = append(src.DependsOn[:0:0], src.DependsOn...) 36 | dst.Steps = append(src.Steps[:0:0], src.Steps...) 37 | dst.Labels = map[string]string{} 38 | for k, v := range src.Labels { 39 | dst.Labels[k] = v 40 | } 41 | for i, v := range src.Steps { 42 | dst.Steps[i] = CloneStep(v) 43 | } 44 | return dst 45 | } 46 | 47 | // CloneStep returns a copy of the Step. 48 | func CloneStep(src *drone.Step) *drone.Step { 49 | dst := new(drone.Step) 50 | *dst = *src 51 | return dst 52 | } 53 | -------------------------------------------------------------------------------- /internal/merge.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package internal 6 | 7 | import "github.com/drone/drone-go/drone" 8 | 9 | // MergeStage merges the source stage with the destination. 10 | func MergeStage(src, dst *drone.Stage) { 11 | dst.Version = src.Version 12 | dst.Created = src.Created 13 | dst.Updated = src.Updated 14 | for i, src := range src.Steps { 15 | dst := dst.Steps[i] 16 | MergeStep(src, dst) 17 | } 18 | } 19 | 20 | // MergeStep merges the source stage with the destination. 21 | func MergeStep(src, dst *drone.Step) { 22 | dst.Version = src.Version 23 | dst.ID = src.ID 24 | dst.StageID = src.StageID 25 | dst.Started = src.Started 26 | dst.Stopped = src.Stopped 27 | dst.Version = src.Version 28 | dst.Schema = src.Schema 29 | } 30 | -------------------------------------------------------------------------------- /internal/merge_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package internal 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/drone/drone-go/drone" 11 | 12 | "github.com/google/go-cmp/cmp" 13 | ) 14 | 15 | func TestMergeStep(t *testing.T) { 16 | src := &drone.Step{ 17 | ID: 1, 18 | StageID: 2, 19 | Started: 1561256095, 20 | Stopped: 1561256092, 21 | Version: 1, 22 | } 23 | dst := &drone.Step{ 24 | ID: 1, 25 | } 26 | 27 | MergeStep(src, dst) 28 | if src == dst { 29 | t.Errorf("Except copy of step, got reference") 30 | } 31 | 32 | after := &drone.Step{ 33 | ID: 1, 34 | StageID: 2, 35 | Started: 1561256095, 36 | Stopped: 1561256092, 37 | Version: 1, 38 | } 39 | if diff := cmp.Diff(after, src); diff != "" { 40 | t.Errorf("Expect src not modified") 41 | t.Log(diff) 42 | } 43 | if diff := cmp.Diff(after, dst); diff != "" { 44 | t.Errorf("Expect src values copied to dst") 45 | t.Log(diff) 46 | } 47 | } 48 | 49 | func TestMergeStage(t *testing.T) { 50 | dst := &drone.Stage{ 51 | ID: 1, 52 | Steps: []*drone.Step{ 53 | { 54 | ID: 1, 55 | }, 56 | }, 57 | } 58 | src := &drone.Stage{ 59 | ID: 1, 60 | Created: 1561256095, 61 | Updated: 1561256092, 62 | Version: 1, 63 | Steps: []*drone.Step{ 64 | { 65 | ID: 1, 66 | StageID: 2, 67 | Started: 1561256095, 68 | Stopped: 1561256092, 69 | Version: 1, 70 | }, 71 | }, 72 | } 73 | 74 | MergeStage(src, dst) 75 | if src == dst { 76 | t.Errorf("Except copy of stage, got reference") 77 | } 78 | 79 | after := &drone.Stage{ 80 | ID: 1, 81 | Created: 1561256095, 82 | Updated: 1561256092, 83 | Version: 1, 84 | Steps: []*drone.Step{ 85 | { 86 | ID: 1, 87 | StageID: 2, 88 | Started: 1561256095, 89 | Stopped: 1561256092, 90 | Version: 1, 91 | }, 92 | }, 93 | } 94 | if diff := cmp.Diff(after, dst); diff != "" { 95 | t.Errorf("Expect src values copied to dst") 96 | t.Log(diff) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /labels/labels.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package labels 6 | 7 | import ( 8 | "fmt" 9 | "time" 10 | 11 | "github.com/drone/drone-go/drone" 12 | ) 13 | 14 | // now returns the current time. 15 | var now = time.Now 16 | 17 | // FromRepo returns container labels derived from the 18 | // Repository metadata. 19 | func FromRepo(v *drone.Repo) map[string]string { 20 | return map[string]string{ 21 | "io.drone.repo.namespace": v.Namespace, 22 | "io.drone.repo.name": v.Name, 23 | "io.drone.repo.slug": v.Slug, 24 | } 25 | } 26 | 27 | // FromBuild returns container labels derived from the 28 | // Build metadata. 29 | func FromBuild(v *drone.Build) map[string]string { 30 | return map[string]string{ 31 | "io.drone.build.number": fmt.Sprint(v.Number), 32 | } 33 | } 34 | 35 | // FromStage returns container labels derived from the 36 | // Stage metadata. 37 | func FromStage(v *drone.Stage) map[string]string { 38 | return map[string]string{ 39 | "io.drone.stage.name": v.Name, 40 | "io.drone.stage.number": fmt.Sprint(v.Number), 41 | } 42 | } 43 | 44 | // FromStep returns container labels derived from the 45 | // Step metadata. 46 | func FromStep(v *drone.Step) map[string]string { 47 | return map[string]string{ 48 | "io.drone.step.number": fmt.Sprint(v.Number), 49 | "io.drone.step.name": v.Name, 50 | } 51 | } 52 | 53 | // FromSystem returns container labels derived from the 54 | // System metadata. 55 | func FromSystem(v *drone.System) map[string]string { 56 | return map[string]string{ 57 | "io.drone": "true", 58 | "io.drone.protected": "false", 59 | "io.drone.system.host": v.Host, 60 | "io.drone.system.proto": v.Proto, 61 | "io.drone.system.version": v.Version, 62 | } 63 | } 64 | 65 | // WithTimeout returns container labels that define 66 | // timeout and expiration values. 67 | func WithTimeout(v *drone.Repo) map[string]string { 68 | return map[string]string{ 69 | "io.drone.ttl": fmt.Sprint(time.Duration(v.Timeout) * time.Minute), 70 | "io.drone.expires": fmt.Sprint(now().Add(time.Duration(v.Timeout)*time.Minute + time.Hour).Unix()), 71 | "io.drone.created": fmt.Sprint(now().Unix()), 72 | } 73 | } 74 | 75 | // Combine is a helper function combines one or more maps of 76 | // labels into a single map. 77 | func Combine(labels ...map[string]string) map[string]string { 78 | c := map[string]string{} 79 | for _, e := range labels { 80 | for k, v := range e { 81 | c[k] = v 82 | } 83 | } 84 | return c 85 | } 86 | -------------------------------------------------------------------------------- /labels/labels_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package labels 6 | -------------------------------------------------------------------------------- /livelog/copy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package livelog 6 | 7 | import ( 8 | "bufio" 9 | "io" 10 | ) 11 | 12 | // Copy copies from src to dst and removes until either EOF 13 | // is reached on src or an error occurs. 14 | func Copy(dst io.Writer, src io.ReadCloser) error { 15 | r := bufio.NewReader(src) 16 | for { 17 | bytes, err := r.ReadBytes('\n') 18 | if _, err := dst.Write(bytes); err != nil { 19 | return err 20 | } 21 | if err != nil { 22 | if err != io.EOF { 23 | return err 24 | } 25 | return nil 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /livelog/copy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package livelog 6 | -------------------------------------------------------------------------------- /livelog/extractor/writer.go: -------------------------------------------------------------------------------- 1 | package extractor 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "io" 8 | "os" 9 | "regexp" 10 | ) 11 | 12 | var ( 13 | prefix = []byte("\u001B]1338;") 14 | suffix = []byte("\u001B]0m") 15 | re = regexp.MustCompilePOSIX("\u001B]1338;((.*?)\u001B]0m)") 16 | disableCards = os.Getenv("DRONE_FLAG_ENABLE_CARDS") == "false" 17 | ) 18 | 19 | type Writer struct { 20 | base io.Writer 21 | file []byte 22 | chunked bool 23 | } 24 | 25 | func New(w io.Writer) *Writer { 26 | return &Writer{w, nil, false} 27 | } 28 | 29 | func (e *Writer) Write(p []byte) (n int, err error) { 30 | if disableCards { 31 | return e.base.Write(p) 32 | } 33 | if bytes.HasPrefix(p, prefix) == false && e.chunked == false { 34 | return e.base.Write(p) 35 | } 36 | n = len(p) 37 | 38 | // if the data does not include the ansi suffix, 39 | // it exceeds the size of the buffer and is chunked. 40 | e.chunked = !bytes.Contains(p, suffix) 41 | 42 | // trim the ansi prefix and suffix from the data, 43 | // and also trim any spacing or newlines that could 44 | // cause confusion. 45 | p = bytes.TrimSpace(p) 46 | p = bytes.TrimPrefix(p, prefix) 47 | p = bytes.TrimSuffix(p, suffix) 48 | 49 | e.file = append(e.file, p...) 50 | return n, nil 51 | } 52 | 53 | func (e *Writer) File() ([]byte, bool) { 54 | if len(e.file) == 0 { 55 | return nil, false 56 | } 57 | data, err := base64.StdEncoding.DecodeString(string(e.file)) 58 | if err != nil { 59 | return nil, false 60 | } 61 | if isJSON(data) { 62 | return data, true 63 | } 64 | return nil, false 65 | } 66 | 67 | func isJSON(data []byte) bool { 68 | var js json.RawMessage 69 | return json.Unmarshal(data, &js) == nil 70 | } 71 | -------------------------------------------------------------------------------- /livelog/livelog_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package livelog 6 | 7 | import ( 8 | "context" 9 | "testing" 10 | "time" 11 | 12 | "github.com/drone/drone-go/drone" 13 | "github.com/drone/runner-go/client" 14 | 15 | "github.com/google/go-cmp/cmp" 16 | ) 17 | 18 | func TestLineWriterSingle(t *testing.T) { 19 | client := new(mockClient) 20 | w := New(client, 1) 21 | w.SetInterval(time.Duration(0)) 22 | w.num = 4 23 | w.Write([]byte("foo\nbar\n")) 24 | 25 | a := w.pending 26 | b := []*drone.Line{ 27 | {Number: 4, Message: "foo\n"}, 28 | {Number: 5, Message: "bar\n"}, 29 | {Number: 6, Message: ""}, 30 | } 31 | if diff := cmp.Diff(a, b); diff != "" { 32 | t.Fail() 33 | t.Log(diff) 34 | } 35 | 36 | w.Close() 37 | a = client.uploaded 38 | if diff := cmp.Diff(a, b); diff != "" { 39 | t.Fail() 40 | t.Log(diff) 41 | } 42 | 43 | if len(w.pending) > 0 { 44 | t.Errorf("Expect empty buffer") 45 | } 46 | } 47 | 48 | func TestLineWriterLimit(t *testing.T) { 49 | client := new(mockClient) 50 | w := New(client, 0) 51 | if got, want := w.limit, defaultLimit; got != want { 52 | t.Errorf("Expect default buffer limit %d, got %d", want, got) 53 | } 54 | w.SetLimit(6) 55 | if got, want := w.limit, 6; got != want { 56 | t.Errorf("Expect custom buffer limit %d, got %d", want, got) 57 | } 58 | 59 | w.Write([]byte("foo")) 60 | w.Write([]byte("bar")) 61 | w.Write([]byte("baz")) 62 | 63 | if got, want := w.size, 6; got != want { 64 | t.Errorf("Expect buffer size %d, got %d", want, got) 65 | } 66 | 67 | a := w.history 68 | b := []*drone.Line{ 69 | {Number: 1, Message: "bar"}, 70 | {Number: 2, Message: "baz"}, 71 | } 72 | if diff := cmp.Diff(a, b); diff != "" { 73 | t.Fail() 74 | t.Log(diff) 75 | } 76 | } 77 | 78 | type mockClient struct { 79 | client.Client 80 | lines []*drone.Line 81 | uploaded []*drone.Line 82 | } 83 | 84 | func (m *mockClient) Batch(ctx context.Context, id int64, lines []*drone.Line) error { 85 | m.lines = append(m.lines, lines...) 86 | return nil 87 | } 88 | 89 | func (m *mockClient) Upload(ctx context.Context, id int64, lines []*drone.Line) error { 90 | m.uploaded = lines 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /logger/context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package logger 6 | 7 | import ( 8 | "context" 9 | "net/http" 10 | ) 11 | 12 | type loggerKey struct{} 13 | 14 | // WithContext returns a new context with the provided logger. 15 | // Use in combination with logger.WithField for great effect. 16 | func WithContext(ctx context.Context, logger Logger) context.Context { 17 | return context.WithValue(ctx, loggerKey{}, logger) 18 | } 19 | 20 | // FromContext retrieves the current logger from the context. 21 | func FromContext(ctx context.Context) Logger { 22 | logger := ctx.Value(loggerKey{}) 23 | if logger == nil { 24 | return Default 25 | } 26 | return logger.(Logger) 27 | } 28 | 29 | // FromRequest retrieves the current logger from the request. If no 30 | // logger is available, the default logger is returned. 31 | func FromRequest(r *http.Request) Logger { 32 | return FromContext(r.Context()) 33 | } 34 | -------------------------------------------------------------------------------- /logger/context_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package logger 6 | 7 | import ( 8 | "context" 9 | "net/http" 10 | "testing" 11 | ) 12 | 13 | func TestContext(t *testing.T) { 14 | entry := Discard() 15 | 16 | ctx := WithContext(context.Background(), entry) 17 | got := FromContext(ctx) 18 | 19 | if got != entry { 20 | t.Errorf("Expected Logger from context") 21 | } 22 | } 23 | 24 | func TestEmptyContext(t *testing.T) { 25 | got := FromContext(context.Background()) 26 | if got == nil { 27 | t.Errorf("Expected Logger from context") 28 | } 29 | if _, ok := got.(*discard); !ok { 30 | t.Errorf("Expected discard Logger from context") 31 | } 32 | } 33 | 34 | func TestRequest(t *testing.T) { 35 | entry := Discard() 36 | 37 | ctx := WithContext(context.Background(), entry) 38 | req := new(http.Request) 39 | req = req.WithContext(ctx) 40 | 41 | got := FromRequest(req) 42 | 43 | if got != entry { 44 | t.Errorf("Expected Logger from http.Request") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /logger/dumper.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package logger 6 | 7 | import ( 8 | "io" 9 | "net/http" 10 | "net/http/httputil" 11 | "os" 12 | ) 13 | 14 | // Dumper dumps the http.Request and http.Response 15 | // message payload for debugging purposes. 16 | type Dumper interface { 17 | DumpRequest(*http.Request) 18 | DumpResponse(*http.Response) 19 | } 20 | 21 | // DiscardDumper returns a no-op dumper. 22 | func DiscardDumper() Dumper { 23 | return new(discardDumper) 24 | } 25 | 26 | type discardDumper struct{} 27 | 28 | func (*discardDumper) DumpRequest(*http.Request) {} 29 | func (*discardDumper) DumpResponse(*http.Response) {} 30 | 31 | // StandardDumper returns a standard dumper. 32 | func StandardDumper(body bool) Dumper { 33 | return &standardDumper{out: os.Stdout, body: body} 34 | } 35 | 36 | type standardDumper struct { 37 | body bool 38 | out io.Writer 39 | } 40 | 41 | func (s *standardDumper) DumpRequest(req *http.Request) { 42 | dump, _ := httputil.DumpRequestOut(req, s.body) 43 | s.out.Write(dump) 44 | } 45 | 46 | func (s *standardDumper) DumpResponse(res *http.Response) { 47 | dump, _ := httputil.DumpResponse(res, s.body) 48 | s.out.Write(dump) 49 | } 50 | -------------------------------------------------------------------------------- /logger/dumper_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package logger 6 | 7 | import ( 8 | "bytes" 9 | "net/http" 10 | "os" 11 | "testing" 12 | ) 13 | 14 | func TestStandardDumper(t *testing.T) { 15 | d := StandardDumper(true) 16 | if s, ok := d.(*standardDumper); !ok { 17 | t.Errorf("Expect standard dumper") 18 | } else if s.out != os.Stdout { 19 | t.Errorf("Expect standard dumper set to stdout") 20 | } 21 | } 22 | 23 | func TestDiscardDumper(t *testing.T) { 24 | d := DiscardDumper() 25 | if _, ok := d.(*discardDumper); !ok { 26 | t.Errorf("Expect discard dumper") 27 | } 28 | } 29 | 30 | func TestStandardDumper_DumpRequest(t *testing.T) { 31 | buf := new(bytes.Buffer) 32 | r, _ := http.NewRequest("GET", "http://example.com", nil) 33 | d := StandardDumper(true).(*standardDumper) 34 | d.out = buf 35 | d.DumpRequest(r) 36 | 37 | want := "GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n" 38 | got := buf.String() 39 | if got != want { 40 | t.Errorf("Got dumped request %q", got) 41 | } 42 | } 43 | 44 | func TestStandardDumper_DumpResponse(t *testing.T) { 45 | buf := new(bytes.Buffer) 46 | r := &http.Response{ 47 | Status: "200 OK", 48 | StatusCode: 200, 49 | Proto: "HTTP/1.0", 50 | ProtoMajor: 1, 51 | ProtoMinor: 0, 52 | } 53 | d := StandardDumper(true).(*standardDumper) 54 | d.out = buf 55 | d.DumpResponse(r) 56 | 57 | want := "HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n" 58 | got := buf.String() 59 | if got != want { 60 | t.Errorf("Got dumped request %q", got) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package logger defines interfaces that logger drivers 6 | // implement to log messages. 7 | package logger 8 | 9 | // A Logger represents an active logging object that generates 10 | // lines of output to an io.Writer. 11 | type Logger interface { 12 | Debug(args ...interface{}) 13 | Debugf(format string, args ...interface{}) 14 | Debugln(args ...interface{}) 15 | 16 | Error(args ...interface{}) 17 | Errorf(format string, args ...interface{}) 18 | Errorln(args ...interface{}) 19 | 20 | Info(args ...interface{}) 21 | Infof(format string, args ...interface{}) 22 | Infoln(args ...interface{}) 23 | 24 | Trace(args ...interface{}) 25 | Tracef(format string, args ...interface{}) 26 | Traceln(args ...interface{}) 27 | 28 | Warn(args ...interface{}) 29 | Warnf(format string, args ...interface{}) 30 | Warnln(args ...interface{}) 31 | 32 | WithError(error) Logger 33 | WithField(string, interface{}) Logger 34 | } 35 | 36 | // Default returns the default logger. 37 | var Default = Discard() 38 | 39 | // Discard returns a no-op logger 40 | func Discard() Logger { 41 | return &discard{} 42 | } 43 | 44 | type discard struct{} 45 | 46 | func (*discard) Debug(args ...interface{}) {} 47 | func (*discard) Debugf(format string, args ...interface{}) {} 48 | func (*discard) Debugln(args ...interface{}) {} 49 | func (*discard) Error(args ...interface{}) {} 50 | func (*discard) Errorf(format string, args ...interface{}) {} 51 | func (*discard) Errorln(args ...interface{}) {} 52 | func (*discard) Info(args ...interface{}) {} 53 | func (*discard) Infof(format string, args ...interface{}) {} 54 | func (*discard) Infoln(args ...interface{}) {} 55 | func (*discard) Trace(args ...interface{}) {} 56 | func (*discard) Tracef(format string, args ...interface{}) {} 57 | func (*discard) Traceln(args ...interface{}) {} 58 | func (*discard) Warn(args ...interface{}) {} 59 | func (*discard) Warnf(format string, args ...interface{}) {} 60 | func (*discard) Warnln(args ...interface{}) {} 61 | func (d *discard) WithError(error) Logger { return d } 62 | func (d *discard) WithField(string, interface{}) Logger { return d } 63 | -------------------------------------------------------------------------------- /logger/logger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package logger 6 | 7 | import "testing" 8 | 9 | func TestWithError(t *testing.T) { 10 | d := &discard{} 11 | if d.WithError(nil) != d { 12 | t.Errorf("Expect WithError to return base logger") 13 | } 14 | } 15 | 16 | func TestWithField(t *testing.T) { 17 | d := &discard{} 18 | if d.WithField("hello", "world") != d { 19 | t.Errorf("Expect WithField to return base logger") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /logger/logrus.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package logger 6 | 7 | import "github.com/sirupsen/logrus" 8 | 9 | // Logrus returns a Logger that wraps a logrus.Entry. 10 | func Logrus(entry *logrus.Entry) Logger { 11 | return &wrapLogrus{entry} 12 | } 13 | 14 | type wrapLogrus struct { 15 | *logrus.Entry 16 | } 17 | 18 | func (w *wrapLogrus) WithError(err error) Logger { 19 | return &wrapLogrus{w.Entry.WithError(err)} 20 | return nil 21 | } 22 | 23 | func (w *wrapLogrus) WithField(key string, value interface{}) Logger { 24 | return &wrapLogrus{w.Entry.WithField(key, value)} 25 | } 26 | -------------------------------------------------------------------------------- /logger/logrus_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package logger 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func TestLogrus(t *testing.T) { 14 | logger := Logrus( 15 | logrus.NewEntry( 16 | logrus.StandardLogger(), 17 | ), 18 | ) 19 | if _, ok := logger.(*wrapLogrus); !ok { 20 | t.Errorf("Expect wrapped logrus") 21 | } 22 | if _, ok := logger.WithError(nil).(*wrapLogrus); !ok { 23 | t.Errorf("Expect WithError wraps logrus") 24 | } 25 | if _, ok := logger.WithField("foo", "bar").(*wrapLogrus); !ok { 26 | t.Errorf("Expect WithField logrus") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /manifest/clone.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | // Clone configures the git clone. 8 | type Clone struct { 9 | Disable bool `json:"disable,omitempty"` 10 | Depth int `json:"depth,omitempty"` 11 | Retries int `json:"retries,omitempty"` 12 | SkipVerify bool `json:"skip_verify,omitempty" yaml:"skip_verify"` 13 | Trace bool `json:"trace,omitempty"` 14 | } 15 | -------------------------------------------------------------------------------- /manifest/clone_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | -------------------------------------------------------------------------------- /manifest/concur.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | // Concurrency limits pipeline concurrency. 8 | type Concurrency struct { 9 | Limit int `json:"limit,omitempty"` 10 | } 11 | -------------------------------------------------------------------------------- /manifest/concur_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | -------------------------------------------------------------------------------- /manifest/cond.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | import filepath "github.com/bmatcuk/doublestar" 8 | 9 | // Match provides match creteria for evaluation. 10 | type Match struct { 11 | Action string 12 | Branch string 13 | Cron string 14 | Event string 15 | Instance string 16 | Ref string 17 | Repo string 18 | Target string 19 | Paths []string 20 | } 21 | 22 | // Conditions defines a group of conditions. 23 | type Conditions struct { 24 | Action Condition `json:"action,omitempty"` 25 | Cron Condition `json:"cron,omitempty"` 26 | Ref Condition `json:"ref,omitempty"` 27 | Repo Condition `json:"repo,omitempty"` 28 | Instance Condition `json:"instance,omitempty"` 29 | Target Condition `json:"target,omitempty"` 30 | Event Condition `json:"event,omitempty"` 31 | Branch Condition `json:"branch,omitempty"` 32 | Status Condition `json:"status,omitempty"` 33 | Paths Condition `json:"paths,omitempty"` 34 | } 35 | 36 | // Match returns true if the string matches the include 37 | // patterns and does not match any of the exclude patterns. 38 | func (c Conditions) Match(m Match) bool { 39 | return c.Cron.Match(m.Cron) && 40 | c.Ref.Match(m.Ref) && 41 | c.Repo.Match(m.Repo) && 42 | c.Instance.Match(m.Instance) && 43 | c.Target.Match(m.Target) && 44 | c.Event.Match(m.Event) && 45 | c.Branch.Match(m.Branch) && 46 | c.Action.Match(m.Action) 47 | } 48 | 49 | // Condition defines a runtime condition. 50 | type Condition struct { 51 | Include []string `yaml:"include,omitempty" json:"include,omitempty"` 52 | Exclude []string `yaml:"exclude,omitempty" json:"exclude,omitempty"` 53 | } 54 | 55 | // Match returns true if the string matches the include 56 | // patterns and does not match any of the exclude patterns. 57 | func (c *Condition) Match(v string) bool { 58 | if c.Excludes(v) { 59 | return false 60 | } 61 | if c.Includes(v) { 62 | return true 63 | } 64 | if len(c.Include) == 0 { 65 | return true 66 | } 67 | return false 68 | } 69 | 70 | // Includes returns true if the string matches the include 71 | // patterns. 72 | func (c *Condition) Includes(v string) bool { 73 | for _, pattern := range c.Include { 74 | if ok, _ := filepath.Match(pattern, v); ok { 75 | return true 76 | } 77 | } 78 | return false 79 | } 80 | 81 | // Excludes returns true if the string matches the exclude 82 | // patterns. 83 | func (c *Condition) Excludes(v string) bool { 84 | for _, pattern := range c.Exclude { 85 | if ok, _ := filepath.Match(pattern, v); ok { 86 | return true 87 | } 88 | } 89 | return false 90 | } 91 | 92 | // UnmarshalYAML implements yml unmarshalling. 93 | func (c *Condition) UnmarshalYAML(unmarshal func(interface{}) error) error { 94 | var out1 string 95 | var out2 []string 96 | var out3 = struct { 97 | Include []string 98 | Exclude []string 99 | }{} 100 | 101 | err := unmarshal(&out1) 102 | if err == nil { 103 | c.Include = []string{out1} 104 | return nil 105 | } 106 | 107 | unmarshal(&out2) 108 | unmarshal(&out3) 109 | 110 | c.Exclude = out3.Exclude 111 | c.Include = append( 112 | out3.Include, 113 | out2..., 114 | ) 115 | 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /manifest/cond_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | -------------------------------------------------------------------------------- /manifest/driver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | // registered drivers. 8 | var drivers []Driver 9 | 10 | // Register registers the parsing driver. 11 | func Register(driver Driver) { 12 | drivers = append(drivers, driver) 13 | } 14 | 15 | // Driver defines a parser driver that can be used to parse 16 | // resource-specific Yaml documents. 17 | type Driver func(r *RawResource) (Resource, bool, error) 18 | -------------------------------------------------------------------------------- /manifest/driver_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | -------------------------------------------------------------------------------- /manifest/env.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | type ( 8 | // Variable represents an environment variable that 9 | // can be defined as a string literal or as a reference 10 | // to a secret. 11 | Variable struct { 12 | Value string `json:"value,omitempty"` 13 | Secret string `json:"from_secret,omitempty" yaml:"from_secret"` 14 | } 15 | 16 | // variable is a temporary type used to unmarshal 17 | // variables with references to secrets. 18 | variable struct { 19 | Value string 20 | Secret string `yaml:"from_secret"` 21 | } 22 | ) 23 | 24 | // UnmarshalYAML implements yaml unmarshalling. 25 | func (v *Variable) UnmarshalYAML(unmarshal func(interface{}) error) error { 26 | d := new(variable) 27 | err := unmarshal(&d.Value) 28 | if err != nil { 29 | err = unmarshal(d) 30 | } 31 | v.Value = d.Value 32 | v.Secret = d.Secret 33 | return err 34 | } 35 | 36 | // MarshalYAML implements yaml marshalling. 37 | func (v *Variable) MarshalYAML() (interface{}, error) { 38 | if v.Secret != "" { 39 | m := map[string]interface{}{} 40 | m["from_secret"] = v.Secret 41 | return m, nil 42 | } 43 | if v.Value != "" { 44 | return v.Value, nil 45 | } 46 | return nil, nil 47 | } 48 | -------------------------------------------------------------------------------- /manifest/env_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/buildkite/yaml" 11 | ) 12 | 13 | func TestEnv(t *testing.T) { 14 | tests := []struct { 15 | yaml string 16 | value string 17 | from string 18 | }{ 19 | { 20 | yaml: "bar", 21 | value: "bar", 22 | }, 23 | { 24 | yaml: "from_secret: username", 25 | from: "username", 26 | }, 27 | } 28 | for _, test := range tests { 29 | in := []byte(test.yaml) 30 | out := new(Variable) 31 | err := yaml.Unmarshal(in, out) 32 | if err != nil { 33 | t.Error(err) 34 | return 35 | } 36 | if got, want := out.Value, test.value; got != want { 37 | t.Errorf("Want variable value %q, got %q", want, got) 38 | } 39 | if got, want := out.Secret, test.from; got != want { 40 | t.Errorf("Want variable from_secret %q, got %q", want, got) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /manifest/lookup.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | import "errors" 8 | 9 | // Lookup returns the named resource from the Manifest. 10 | func Lookup(name string, manifest *Manifest) (Resource, error) { 11 | for _, resource := range manifest.Resources { 12 | if isNameMatch(resource.GetName(), name) { 13 | return resource, nil 14 | } 15 | } 16 | return nil, errors.New("resource not found") 17 | } 18 | 19 | // helper function returns true if the name matches. 20 | func isNameMatch(a, b string) bool { 21 | return a == b || 22 | (a == "" && b == "default") || 23 | (b == "" && a == "default") 24 | } 25 | -------------------------------------------------------------------------------- /manifest/lookup_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | import "testing" 8 | 9 | type resourceImpl struct { 10 | Name, Kind, Type, Version string 11 | } 12 | 13 | func (r *resourceImpl) GetVersion() string { return r.Version } 14 | func (r *resourceImpl) GetKind() string { return r.Kind } 15 | func (r *resourceImpl) GetType() string { return r.Type } 16 | func (r *resourceImpl) GetName() string { return r.Name } 17 | 18 | func TestLookup(t *testing.T) { 19 | want := &resourceImpl{Name: "default"} 20 | m := &Manifest{ 21 | Resources: []Resource{want}, 22 | } 23 | got, err := Lookup("default", m) 24 | if err != nil { 25 | t.Error(err) 26 | } 27 | if got != want { 28 | t.Errorf("Expect resource not found error") 29 | } 30 | } 31 | 32 | func TestLookupNotFound(t *testing.T) { 33 | m := &Manifest{ 34 | Resources: []Resource{ 35 | &Secret{ 36 | Kind: "secret", 37 | Name: "password", 38 | }, 39 | }, 40 | } 41 | _, err := Lookup("default", m) 42 | if err == nil { 43 | t.Errorf("Expect resource not found error") 44 | } 45 | } 46 | 47 | func TestNameMatch(t *testing.T) { 48 | tests := []struct { 49 | a, b string 50 | match bool 51 | }{ 52 | {"a", "b", false}, 53 | {"a", "a", true}, 54 | {"", "default", true}, 55 | } 56 | for _, test := range tests { 57 | got, want := isNameMatch(test.a, test.b), test.match 58 | if got != want { 59 | t.Errorf("Expect %q and %q match is %v", test.a, test.b, want) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /manifest/manifest.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package manifest provides definitions for the Yaml schema. 6 | package manifest 7 | 8 | // Resource enums. 9 | const ( 10 | KindApproval = "approval" 11 | KindDeployment = "deployment" 12 | KindPipeline = "pipeline" 13 | KindSecret = "secret" 14 | KindSignature = "signature" 15 | ) 16 | 17 | type ( 18 | // Manifest is a collection of Drone resources. 19 | Manifest struct { 20 | Resources []Resource 21 | } 22 | 23 | // Resource represents a Drone resource. 24 | Resource interface { 25 | GetVersion() string 26 | GetKind() string 27 | GetType() string 28 | GetName() string 29 | } 30 | 31 | // ConcurrentResource is a resource with concurrency limits. 32 | ConcurrentResource interface { 33 | Resource 34 | GetConcurrency() Concurrency 35 | } 36 | 37 | // DependantResource is a resource with runtime dependencies. 38 | DependantResource interface { 39 | Resource 40 | GetDependsOn() []string 41 | } 42 | 43 | // PlatformResource is a resource with platform requirements. 44 | PlatformResource interface { 45 | Resource 46 | GetPlatform() Platform 47 | } 48 | 49 | // RoutedResource is a resource that can be routed to 50 | // specific build nodes. 51 | RoutedResource interface { 52 | Resource 53 | GetNodes() map[string]string 54 | } 55 | 56 | // TriggeredResource is a resource with trigger rules. 57 | TriggeredResource interface { 58 | Resource 59 | GetTrigger() Conditions 60 | } 61 | 62 | // RawResource is a raw encoded resource with the common 63 | // metadata extracted. 64 | RawResource struct { 65 | Version string 66 | Kind string 67 | Type string 68 | Name string 69 | Deps []string `yaml:"depends_on"` 70 | Node map[string]string 71 | Concurrency Concurrency 72 | Platform Platform 73 | Data []byte `yaml:"-"` 74 | } 75 | ) 76 | -------------------------------------------------------------------------------- /manifest/param.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | type ( 8 | // Parameter represents an configuration parameter that 9 | // can be defined as a literal or as a reference 10 | // to a secret. 11 | Parameter struct { 12 | Value interface{} `json:"value,omitempty"` 13 | Secret string `json:"from_secret,omitempty" yaml:"from_secret"` 14 | } 15 | 16 | // parameter is a tempoary type used to unmarshal 17 | // parameters with references to secrets. 18 | parameter struct { 19 | Secret string `yaml:"from_secret"` 20 | } 21 | ) 22 | 23 | // UnmarshalYAML implements yaml unmarshalling. 24 | func (p *Parameter) UnmarshalYAML(unmarshal func(interface{}) error) error { 25 | d := new(parameter) 26 | err := unmarshal(d) 27 | if err == nil && d.Secret != "" { 28 | p.Secret = d.Secret 29 | return nil 30 | } 31 | var i interface{} 32 | err = unmarshal(&i) 33 | p.Value = i 34 | return err 35 | } 36 | 37 | // MarshalYAML implements yaml marshalling. 38 | func (p *Parameter) MarshalYAML() (interface{}, error) { 39 | if p.Secret != "" { 40 | m := map[string]interface{}{} 41 | m["from_secret"] = p.Secret 42 | return m, nil 43 | } 44 | if p.Value != "" { 45 | return p.Value, nil 46 | } 47 | return nil, nil 48 | } 49 | -------------------------------------------------------------------------------- /manifest/param_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/buildkite/yaml" 11 | ) 12 | 13 | func TestParam(t *testing.T) { 14 | tests := []struct { 15 | yaml string 16 | value interface{} 17 | from string 18 | }{ 19 | { 20 | yaml: "bar", 21 | value: "bar", 22 | }, 23 | { 24 | yaml: "from_secret: username", 25 | from: "username", 26 | }, 27 | } 28 | for _, test := range tests { 29 | in := []byte(test.yaml) 30 | out := new(Parameter) 31 | err := yaml.Unmarshal(in, out) 32 | if err != nil { 33 | t.Error(err) 34 | return 35 | } 36 | if got, want := out.Value, test.value; got != want { 37 | t.Errorf("Want value %q, got %q", want, got) 38 | } 39 | if got, want := out.Secret, test.from; got != want { 40 | t.Errorf("Want from_secret %q, got %q", want, got) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /manifest/parse_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | import ( 8 | "encoding/json" 9 | "io/ioutil" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | func diff(file string) (string, error) { 15 | a, err := ParseFile(file) 16 | if err != nil { 17 | return "", err 18 | } 19 | d, err := ioutil.ReadFile(file + ".golden") 20 | if err != nil { 21 | return "", err 22 | } 23 | b := new(Manifest) 24 | err = json.Unmarshal(d, b) 25 | if err != nil { 26 | return "", err 27 | } 28 | return cmp.Diff(a, b), nil 29 | } 30 | -------------------------------------------------------------------------------- /manifest/platform.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | // Platform defines the target platform. 8 | type Platform struct { 9 | OS string `json:"os,omitempty"` 10 | Arch string `json:"arch,omitempty"` 11 | Variant string `json:"variant,omitempty"` 12 | Version string `json:"version,omitempty"` 13 | } 14 | -------------------------------------------------------------------------------- /manifest/secret.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | import ( 8 | "errors" 9 | 10 | "github.com/buildkite/yaml" 11 | ) 12 | 13 | var _ Resource = (*Secret)(nil) 14 | 15 | type ( 16 | // Secret is a resource that provides encrypted data 17 | // and pointers to external data (i.e. from vault). 18 | Secret struct { 19 | Version string `json:"version,omitempty"` 20 | Kind string `json:"kind,omitempty"` 21 | Type string `json:"type,omitempty"` 22 | Name string `json:"name,omitempty"` 23 | Data string `json:"data,omitempty"` 24 | Get SecretGet `json:"get,omitempty"` 25 | } 26 | 27 | // SecretGet defines a request to get a secret from 28 | // an external sevice at the specified path, or with the 29 | // specified name. 30 | SecretGet struct { 31 | Path string `json:"path,omitempty"` 32 | Name string `json:"name,omitempty"` 33 | Key string `json:"key,omitempty"` 34 | } 35 | ) 36 | 37 | func init() { 38 | Register(secretFunc) 39 | } 40 | 41 | func secretFunc(r *RawResource) (Resource, bool, error) { 42 | if r.Kind != KindSecret { 43 | return nil, false, nil 44 | } 45 | out := new(Secret) 46 | err := yaml.Unmarshal(r.Data, out) 47 | return out, true, err 48 | } 49 | 50 | // GetVersion returns the resource version. 51 | func (s *Secret) GetVersion() string { return s.Version } 52 | 53 | // GetKind returns the resource kind. 54 | func (s *Secret) GetKind() string { return s.Kind } 55 | 56 | // GetType returns the resource type. 57 | func (s *Secret) GetType() string { return s.Type } 58 | 59 | // GetName returns the resource name. 60 | func (s *Secret) GetName() string { return s.Name } 61 | 62 | // Validate returns an error if the secret is invalid. 63 | func (s *Secret) Validate() error { 64 | if len(s.Data) == 0 && 65 | len(s.Get.Path) == 0 && 66 | len(s.Get.Name) == 0 { 67 | return errors.New("yaml: invalid secret resource") 68 | } 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /manifest/secret_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/buildkite/yaml" 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | var mockSecretYaml = []byte(` 15 | --- 16 | kind: secret 17 | name: username 18 | 19 | data: b2N0b2NhdA== 20 | `) 21 | 22 | var mockSecret = &Secret{ 23 | Kind: "secret", 24 | Name: "username", 25 | Data: "b2N0b2NhdA==", 26 | } 27 | 28 | func TestSecretUnmarshal(t *testing.T) { 29 | a := new(Secret) 30 | b := mockSecret 31 | yaml.Unmarshal(mockSecretYaml, a) 32 | if diff := cmp.Diff(a, b); diff != "" { 33 | t.Error("Failed to parse secret") 34 | t.Log(diff) 35 | } 36 | } 37 | 38 | func TestSecretValidate(t *testing.T) { 39 | secret := new(Secret) 40 | 41 | secret.Data = "some-data" 42 | if err := secret.Validate(); err != nil { 43 | t.Error(err) 44 | return 45 | } 46 | 47 | secret.Get.Path = "secret/data/docker" 48 | if err := secret.Validate(); err != nil { 49 | t.Error(err) 50 | return 51 | } 52 | 53 | secret.Data = "" 54 | secret.Get.Path = "" 55 | if err := secret.Validate(); err == nil { 56 | t.Errorf("Expect invalid secret error") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /manifest/signature.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | import ( 8 | "errors" 9 | 10 | "github.com/buildkite/yaml" 11 | ) 12 | 13 | var _ Resource = (*Signature)(nil) 14 | 15 | type ( 16 | // Signature is a resource that provides an hmac 17 | // signature of combined resources. This signature 18 | // can be used to validate authenticity and prevent 19 | // tampering. 20 | Signature struct { 21 | Version string `json:"version,omitempty"` 22 | Kind string `json:"kind"` 23 | Type string `json:"type"` 24 | Name string `json:"name"` 25 | Hmac string `json:"hmac"` 26 | } 27 | ) 28 | 29 | func init() { 30 | Register(signatureFunc) 31 | } 32 | 33 | func signatureFunc(r *RawResource) (Resource, bool, error) { 34 | if r.Kind != KindSignature { 35 | return nil, false, nil 36 | } 37 | out := new(Signature) 38 | err := yaml.Unmarshal(r.Data, out) 39 | return out, true, err 40 | } 41 | 42 | // GetVersion returns the resource version. 43 | func (s *Signature) GetVersion() string { return s.Version } 44 | 45 | // GetKind returns the resource kind. 46 | func (s *Signature) GetKind() string { return s.Kind } 47 | 48 | // GetType returns the resource type. 49 | func (s *Signature) GetType() string { return s.Type } 50 | 51 | // GetName returns the resource name. 52 | func (s *Signature) GetName() string { return s.Name } 53 | 54 | // Validate returns an error if the signature is invalid. 55 | func (s Signature) Validate() error { 56 | if s.Hmac == "" { 57 | return errors.New("yaml: invalid signature. missing hash") 58 | } 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /manifest/signature_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/buildkite/yaml" 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | var mockSignatureYaml = []byte(` 15 | --- 16 | kind: signature 17 | hmac: N2NmYjA3ODQwNTY1ODFlY2E5MGJmOWI1NDk0NDFhMTEK 18 | `) 19 | 20 | var mockSignature = &Signature{ 21 | Kind: "signature", 22 | Hmac: "N2NmYjA3ODQwNTY1ODFlY2E5MGJmOWI1NDk0NDFhMTEK", 23 | } 24 | 25 | func TestSignatureUnmarshal(t *testing.T) { 26 | a := new(Signature) 27 | b := mockSignature 28 | yaml.Unmarshal(mockSignatureYaml, a) 29 | if diff := cmp.Diff(a, b); diff != "" { 30 | t.Error("Failed to parse signature") 31 | t.Log(diff) 32 | } 33 | } 34 | 35 | func TestSignatureValidate(t *testing.T) { 36 | sig := Signature{Hmac: "1234"} 37 | if err := sig.Validate(); err != nil { 38 | t.Error(err) 39 | return 40 | } 41 | 42 | sig.Hmac = "" 43 | if err := sig.Validate(); err == nil { 44 | t.Errorf("Expect invalid signature error") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /manifest/unit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | import ( 8 | "github.com/docker/go-units" 9 | ) 10 | 11 | // BytesSize stores a human-readable size in bytes, 12 | // kibibytes, mebibytes, gibibytes, or tebibytes 13 | // (eg. "44kiB", "17MiB"). 14 | type BytesSize int64 15 | 16 | // UnmarshalYAML implements yaml unmarshalling. 17 | func (b *BytesSize) UnmarshalYAML(unmarshal func(interface{}) error) error { 18 | var intType int64 19 | if err := unmarshal(&intType); err == nil { 20 | *b = BytesSize(intType) 21 | return nil 22 | } 23 | 24 | var stringType string 25 | if err := unmarshal(&stringType); err != nil { 26 | return err 27 | } 28 | 29 | intType, err := units.RAMInBytes(stringType) 30 | if err == nil { 31 | *b = BytesSize(intType) 32 | } 33 | return err 34 | } 35 | 36 | // String returns a human-readable size in bytes, 37 | // kibibytes, mebibytes, gibibytes, or tebibytes 38 | // (eg. "44kiB", "17MiB"). 39 | func (b BytesSize) String() string { 40 | return units.BytesSize(float64(b)) 41 | } 42 | -------------------------------------------------------------------------------- /manifest/unit_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/buildkite/yaml" 11 | ) 12 | 13 | func TestBytesSize(t *testing.T) { 14 | tests := []struct { 15 | yaml string 16 | size int64 17 | text string 18 | }{ 19 | { 20 | yaml: "1KiB", 21 | size: 1024, 22 | text: "1KiB", 23 | }, 24 | { 25 | yaml: "100Mi", 26 | size: 104857600, 27 | text: "100MiB", 28 | }, 29 | { 30 | yaml: "1024", 31 | size: 1024, 32 | text: "1KiB", 33 | }, 34 | } 35 | for _, test := range tests { 36 | in := []byte(test.yaml) 37 | out := BytesSize(0) 38 | err := yaml.Unmarshal(in, &out) 39 | if err != nil { 40 | t.Error(err) 41 | return 42 | } 43 | if got, want := int64(out), test.size; got != want { 44 | t.Errorf("Want byte size %d, got %d", want, got) 45 | } 46 | if got, want := out.String(), test.text; got != want { 47 | t.Errorf("Want byte text %s, got %s", want, got) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /manifest/workspace.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | 7 | // Workspace configures the project path on disk. 8 | type Workspace struct { 9 | Path string `json:"path,omitempty"` 10 | } 11 | -------------------------------------------------------------------------------- /manifest/workspace_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package manifest 6 | -------------------------------------------------------------------------------- /pipeline/pipeline.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package pipeline defines interfaces for managing and reporting 6 | // pipeline state. 7 | package pipeline 8 | -------------------------------------------------------------------------------- /pipeline/reporter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package pipeline 6 | 7 | import "context" 8 | 9 | // A Reporter reports the pipeline status. 10 | type Reporter interface { 11 | // ReportStage reports the stage status. 12 | ReportStage(context.Context, *State) error 13 | 14 | // ReportStep reports the named step status. 15 | ReportStep(context.Context, *State, string) error 16 | } 17 | 18 | // NopReporter returns a noop reporter. 19 | func NopReporter() Reporter { 20 | return new(nopReporter) 21 | } 22 | 23 | type nopReporter struct{} 24 | 25 | func (*nopReporter) ReportStage(context.Context, *State) error { return nil } 26 | func (*nopReporter) ReportStep(context.Context, *State, string) error { return nil } 27 | -------------------------------------------------------------------------------- /pipeline/reporter/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package reporter provides Reporter implementations. 6 | package reporter 7 | -------------------------------------------------------------------------------- /pipeline/reporter/history/entry.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package history 6 | 7 | import ( 8 | "time" 9 | 10 | "github.com/drone/drone-go/drone" 11 | ) 12 | 13 | // Entry represents a history entry. 14 | type Entry struct { 15 | Stage *drone.Stage `json:"stage"` 16 | Build *drone.Build `json:"build"` 17 | Repo *drone.Repo `json:"repo"` 18 | Created time.Time `json:"created"` 19 | Updated time.Time `json:"updated"` 20 | } 21 | 22 | // ByTimestamp sorts a list of entries by timestamp 23 | type ByTimestamp []*Entry 24 | 25 | func (a ByTimestamp) Len() int { return len(a) } 26 | func (a ByTimestamp) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 27 | 28 | func (a ByTimestamp) Less(i, j int) bool { 29 | return a[i].Stage.ID > a[j].Stage.ID 30 | } 31 | 32 | // ByStatus sorts a list of entries by status 33 | type ByStatus []*Entry 34 | 35 | func (a ByStatus) Len() int { return len(a) } 36 | func (a ByStatus) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 37 | 38 | func (a ByStatus) Less(i, j int) bool { 39 | return order(a[i].Stage) < order(a[j].Stage) 40 | } 41 | 42 | func order(stage *drone.Stage) int64 { 43 | switch stage.Status { 44 | case drone.StatusPending: 45 | return 0 46 | case drone.StatusRunning: 47 | return 1 48 | default: 49 | return 2 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pipeline/reporter/history/entry_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package history 6 | 7 | import ( 8 | "sort" 9 | "testing" 10 | 11 | "github.com/drone/drone-go/drone" 12 | 13 | "github.com/google/go-cmp/cmp" 14 | ) 15 | 16 | func TestSort(t *testing.T) { 17 | before := []*Entry{ 18 | {Stage: &drone.Stage{ID: 1, Status: drone.StatusPassing}}, 19 | {Stage: &drone.Stage{ID: 2, Status: drone.StatusPassing}}, 20 | {Stage: &drone.Stage{ID: 3, Status: drone.StatusPending}}, 21 | {Stage: &drone.Stage{ID: 4, Status: drone.StatusRunning}}, 22 | {Stage: &drone.Stage{ID: 5, Status: drone.StatusPassing}}, 23 | } 24 | 25 | after := []*Entry{ 26 | {Stage: &drone.Stage{ID: 3, Status: drone.StatusPending}}, 27 | {Stage: &drone.Stage{ID: 4, Status: drone.StatusRunning}}, 28 | {Stage: &drone.Stage{ID: 5, Status: drone.StatusPassing}}, 29 | {Stage: &drone.Stage{ID: 2, Status: drone.StatusPassing}}, 30 | {Stage: &drone.Stage{ID: 1, Status: drone.StatusPassing}}, 31 | } 32 | 33 | s1 := ByTimestamp(before) 34 | s2 := ByStatus(before) 35 | sort.Sort(s1) 36 | sort.Sort(s2) 37 | 38 | if diff := cmp.Diff(before, after); diff != "" { 39 | t.Errorf("Expect entries sorted by status") 40 | t.Log(diff) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pipeline/reporter/history/history.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package history implements a tracer that provides access to 6 | // pipeline execution history. 7 | package history 8 | 9 | import ( 10 | "context" 11 | "sync" 12 | "time" 13 | 14 | "github.com/drone/runner-go/internal" 15 | "github.com/drone/runner-go/pipeline" 16 | ) 17 | 18 | var _ pipeline.Reporter = (*History)(nil) 19 | 20 | // default number of historical entries. 21 | const defaultLimit = 25 22 | 23 | // History tracks pending, running and complete pipeline stages 24 | // processed by the system. 25 | type History struct { 26 | sync.Mutex 27 | base pipeline.Reporter 28 | limit int 29 | items []*Entry 30 | } 31 | 32 | // New returns a new History recorder that wraps the base 33 | // reporter. 34 | func New(base pipeline.Reporter) *History { 35 | return &History{base: base} 36 | } 37 | 38 | // ReportStage adds or updates the pipeline history. 39 | func (h *History) ReportStage(ctx context.Context, state *pipeline.State) error { 40 | h.Lock() 41 | h.update(state) 42 | h.prune() 43 | h.Unlock() 44 | return h.base.ReportStage(ctx, state) 45 | } 46 | 47 | // ReportStep adds or updates the pipeline history. 48 | func (h *History) ReportStep(ctx context.Context, state *pipeline.State, name string) error { 49 | h.Lock() 50 | h.update(state) 51 | h.prune() 52 | h.Unlock() 53 | return h.base.ReportStep(ctx, state, name) 54 | } 55 | 56 | // Entries returns a list of entries. 57 | func (h *History) Entries() []*Entry { 58 | h.Lock() 59 | var entries []*Entry 60 | for _, src := range h.items { 61 | dst := new(Entry) 62 | *dst = *src 63 | entries = append(entries, dst) 64 | } 65 | h.Unlock() 66 | return entries 67 | } 68 | 69 | // Entry returns the entry by id. 70 | func (h *History) Entry(id int64) *Entry { 71 | h.Lock() 72 | defer h.Unlock() 73 | for _, src := range h.items { 74 | if src.Stage.ID == id { 75 | dst := new(Entry) 76 | *dst = *src 77 | return dst 78 | } 79 | } 80 | return nil 81 | } 82 | 83 | // Limit returns the history limit. 84 | func (h *History) Limit() int { 85 | if h.limit == 0 { 86 | return defaultLimit 87 | } 88 | return h.limit 89 | } 90 | 91 | func (h *History) update(state *pipeline.State) { 92 | for _, v := range h.items { 93 | if v.Stage.ID == state.Stage.ID { 94 | v.Stage = internal.CloneStage(state.Stage) 95 | v.Build = internal.CloneBuild(state.Build) 96 | v.Repo = internal.CloneRepo(state.Repo) 97 | v.Updated = time.Now().UTC() 98 | return 99 | } 100 | } 101 | h.items = append(h.items, &Entry{ 102 | Stage: internal.CloneStage(state.Stage), 103 | Build: internal.CloneBuild(state.Build), 104 | Repo: internal.CloneRepo(state.Repo), 105 | Created: time.Now(), 106 | Updated: time.Now(), 107 | }) 108 | } 109 | 110 | func (h *History) prune() { 111 | if len(h.items) > h.Limit() { 112 | h.items = h.items[:h.Limit()-1] 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /pipeline/reporter/remote/remote.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package remote provides a reporter and streamer that sends the 6 | // pipeline status and logs to the central server. 7 | package remote 8 | 9 | import ( 10 | "context" 11 | "io" 12 | 13 | "github.com/drone/runner-go/client" 14 | "github.com/drone/runner-go/internal" 15 | "github.com/drone/runner-go/livelog" 16 | "github.com/drone/runner-go/pipeline" 17 | ) 18 | 19 | var _ pipeline.Reporter = (*Remote)(nil) 20 | var _ pipeline.Streamer = (*Remote)(nil) 21 | 22 | // Remote implements a pipeline reporter that reports state 23 | // changes and results to a remote server instance. 24 | type Remote struct { 25 | client client.Client 26 | } 27 | 28 | // New returns a remote reporter. 29 | func New(client client.Client) *Remote { 30 | return &Remote{ 31 | client: client, 32 | } 33 | } 34 | 35 | // ReportStage reports the stage status. 36 | func (s *Remote) ReportStage(ctx context.Context, state *pipeline.State) error { 37 | state.Lock() 38 | src := state.Stage 39 | cpy := internal.CloneStage(src) 40 | state.Unlock() 41 | err := s.client.Update(ctx, cpy) 42 | if err == nil { 43 | state.Lock() 44 | internal.MergeStage(cpy, src) 45 | state.Unlock() 46 | } 47 | return err 48 | } 49 | 50 | // ReportStep reports the step status. 51 | func (s *Remote) ReportStep(ctx context.Context, state *pipeline.State, name string) error { 52 | src := state.Find(name) 53 | state.Lock() 54 | cpy := internal.CloneStep(src) 55 | state.Unlock() 56 | err := s.client.UpdateStep(ctx, cpy) 57 | if err == nil { 58 | state.Lock() 59 | internal.MergeStep(cpy, src) 60 | state.Unlock() 61 | } 62 | return err 63 | } 64 | 65 | // Stream returns an io.WriteCloser to stream the stdout 66 | // and stderr of the pipeline step to the server. 67 | func (s *Remote) Stream(ctx context.Context, state *pipeline.State, name string) io.WriteCloser { 68 | src := state.Find(name) 69 | return livelog.New(s.client, src.ID) 70 | } 71 | -------------------------------------------------------------------------------- /pipeline/reporter/remote/remote_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package remote 6 | 7 | import ( 8 | "context" 9 | "testing" 10 | 11 | "github.com/drone/drone-go/drone" 12 | "github.com/drone/runner-go/client" 13 | "github.com/drone/runner-go/livelog" 14 | "github.com/drone/runner-go/pipeline" 15 | 16 | "github.com/google/go-cmp/cmp" 17 | ) 18 | 19 | var nocontext = context.Background() 20 | 21 | func TestReportStep(t *testing.T) { 22 | step := &drone.Step{Name: "clone"} 23 | state := &pipeline.State{ 24 | Stage: &drone.Stage{ 25 | Steps: []*drone.Step{step}, 26 | }, 27 | } 28 | 29 | c := new(mockClient) 30 | r := New(c) 31 | err := r.ReportStep(nocontext, state, step.Name) 32 | if err != nil { 33 | t.Error(err) 34 | } 35 | if state.Stage.Steps[0] != step { 36 | t.Errorf("Expect step updated, not replaced") 37 | } 38 | after := &drone.Step{ 39 | Name: "clone", 40 | ID: 1, 41 | StageID: 2, 42 | Started: 1561256080, 43 | Stopped: 1561256090, 44 | Version: 42, 45 | } 46 | if diff := cmp.Diff(after, step); diff != "" { 47 | t.Errorf("Expect response merged with step") 48 | t.Log(diff) 49 | } 50 | } 51 | 52 | func TestReportStage(t *testing.T) { 53 | stage := &drone.Stage{ 54 | Created: 0, 55 | Updated: 0, 56 | Version: 0, 57 | } 58 | state := &pipeline.State{ 59 | Stage: stage, 60 | } 61 | 62 | c := new(mockClient) 63 | r := New(c) 64 | err := r.ReportStage(nocontext, state) 65 | if err != nil { 66 | t.Error(err) 67 | } 68 | if state.Stage != stage { 69 | t.Errorf("Expect stage updated, not replaced") 70 | } 71 | after := &drone.Stage{ 72 | Created: 1561256080, 73 | Updated: 1561256090, 74 | Version: 42, 75 | } 76 | if diff := cmp.Diff(after, state.Stage); diff != "" { 77 | t.Errorf("Expect response merged with stage") 78 | t.Log(diff) 79 | } 80 | } 81 | 82 | func TestStream(t *testing.T) { 83 | state := &pipeline.State{ 84 | Stage: &drone.Stage{ 85 | Steps: []*drone.Step{ 86 | { 87 | ID: 1, 88 | Name: "clone", 89 | }, 90 | }, 91 | }, 92 | } 93 | 94 | c := new(mockClient) 95 | r := New(c) 96 | w := r.Stream(nocontext, state, "clone") 97 | 98 | if _, ok := w.(*livelog.Writer); !ok { 99 | t.Errorf("Expect livelog writer") 100 | } 101 | } 102 | 103 | type mockClient struct { 104 | *client.HTTPClient 105 | } 106 | 107 | func (m *mockClient) Update(_ context.Context, stage *drone.Stage) error { 108 | stage.Version = 42 109 | stage.Created = 1561256080 110 | stage.Updated = 1561256090 111 | return nil 112 | } 113 | 114 | func (m *mockClient) UpdateStep(_ context.Context, step *drone.Step) error { 115 | step.ID = 1 116 | step.StageID = 2 117 | step.Started = 1561256080 118 | step.Stopped = 1561256090 119 | step.Version = 42 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /pipeline/reporter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package pipeline 6 | -------------------------------------------------------------------------------- /pipeline/runtime/const.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package runtime 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | ) 11 | 12 | // 13 | // run policy 14 | // 15 | 16 | // RunPolicy defines the policy for starting containers 17 | // based on the point-in-time pass or fail state of 18 | // the pipeline. 19 | type RunPolicy int 20 | 21 | // RunPolicy enumeration. 22 | const ( 23 | RunOnSuccess RunPolicy = iota 24 | RunOnFailure 25 | RunAlways 26 | RunNever 27 | ) 28 | 29 | func (r RunPolicy) String() string { 30 | return runPolicyID[r] 31 | } 32 | 33 | var runPolicyID = map[RunPolicy]string{ 34 | RunOnSuccess: "on-success", 35 | RunOnFailure: "on-failure", 36 | RunAlways: "always", 37 | RunNever: "never", 38 | } 39 | 40 | var runPolicyName = map[string]RunPolicy{ 41 | "": RunOnSuccess, 42 | "on-success": RunOnSuccess, 43 | "on-failure": RunOnFailure, 44 | "always": RunAlways, 45 | "never": RunNever, 46 | } 47 | 48 | // MarshalJSON marshals the string representation of the 49 | // run type to JSON. 50 | func (r *RunPolicy) MarshalJSON() ([]byte, error) { 51 | buffer := bytes.NewBufferString(`"`) 52 | buffer.WriteString(runPolicyID[*r]) 53 | buffer.WriteString(`"`) 54 | return buffer.Bytes(), nil 55 | } 56 | 57 | // UnmarshalJSON unmarshals the json representation of the 58 | // run type from a string value. 59 | func (r *RunPolicy) UnmarshalJSON(b []byte) error { 60 | // unmarshal as string 61 | var s string 62 | err := json.Unmarshal(b, &s) 63 | if err != nil { 64 | return err 65 | } 66 | // lookup value 67 | *r = runPolicyName[s] 68 | return nil 69 | } 70 | 71 | // 72 | // failure policy 73 | // 74 | 75 | // ErrPolicy defines the step error policy 76 | type ErrPolicy int 77 | 78 | // ErrPolicy enumeration. 79 | const ( 80 | ErrFail ErrPolicy = iota 81 | ErrFailFast 82 | ErrIgnore 83 | ) 84 | 85 | func (p ErrPolicy) String() string { 86 | return errPolicyID[p] 87 | } 88 | 89 | var errPolicyID = map[ErrPolicy]string{ 90 | ErrFail: "fail", 91 | ErrFailFast: "fail-fast", 92 | ErrIgnore: "ignore", 93 | } 94 | 95 | var errPolicyName = map[string]ErrPolicy{ 96 | "": ErrFail, 97 | "fail": ErrFail, 98 | "fail-fast": ErrFailFast, 99 | "fast": ErrFailFast, 100 | "always": ErrFail, 101 | "ignore": ErrIgnore, 102 | } 103 | 104 | // MarshalJSON marshals the string representation of the 105 | // pull type to JSON. 106 | func (p *ErrPolicy) MarshalJSON() ([]byte, error) { 107 | buffer := bytes.NewBufferString(`"`) 108 | buffer.WriteString(errPolicyID[*p]) 109 | buffer.WriteString(`"`) 110 | return buffer.Bytes(), nil 111 | } 112 | 113 | // UnmarshalJSON unmarshals the json representation of the 114 | // pull type from a string value. 115 | func (p *ErrPolicy) UnmarshalJSON(b []byte) error { 116 | // unmarshal as string 117 | var s string 118 | err := json.Unmarshal(b, &s) 119 | if err != nil { 120 | return err 121 | } 122 | // lookup value 123 | *p = errPolicyName[s] 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /pipeline/runtime/const_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package runtime 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "testing" 11 | ) 12 | 13 | // 14 | // runtime policy unit tests. 15 | // 16 | 17 | func TestRunPolicy_Marshal(t *testing.T) { 18 | tests := []struct { 19 | policy RunPolicy 20 | data string 21 | }{ 22 | { 23 | policy: RunAlways, 24 | data: `"always"`, 25 | }, 26 | { 27 | policy: RunOnFailure, 28 | data: `"on-failure"`, 29 | }, 30 | { 31 | policy: RunOnSuccess, 32 | data: `"on-success"`, 33 | }, 34 | { 35 | policy: RunNever, 36 | data: `"never"`, 37 | }, 38 | } 39 | for _, test := range tests { 40 | data, err := json.Marshal(&test.policy) 41 | if err != nil { 42 | t.Error(err) 43 | return 44 | } 45 | if bytes.Equal([]byte(test.data), data) == false { 46 | t.Errorf("Failed to marshal policy %s", test.policy) 47 | } 48 | } 49 | } 50 | 51 | func TestRunPolicy_Unmarshal(t *testing.T) { 52 | tests := []struct { 53 | policy RunPolicy 54 | data string 55 | }{ 56 | { 57 | policy: RunAlways, 58 | data: `"always"`, 59 | }, 60 | { 61 | policy: RunOnFailure, 62 | data: `"on-failure"`, 63 | }, 64 | { 65 | policy: RunOnSuccess, 66 | data: `"on-success"`, 67 | }, 68 | { 69 | policy: RunNever, 70 | data: `"never"`, 71 | }, 72 | { 73 | // no policy should default to on-success 74 | policy: RunOnSuccess, 75 | data: `""`, 76 | }, 77 | } 78 | for _, test := range tests { 79 | var policy RunPolicy 80 | err := json.Unmarshal([]byte(test.data), &policy) 81 | if err != nil { 82 | t.Error(err) 83 | return 84 | } 85 | if got, want := policy, test.policy; got != want { 86 | t.Errorf("Want policy %q, got %q", want, got) 87 | } 88 | } 89 | } 90 | 91 | func TestRunPolicy_UnmarshalTypeError(t *testing.T) { 92 | var policy RunPolicy 93 | err := json.Unmarshal([]byte("[]"), &policy) 94 | if _, ok := err.(*json.UnmarshalTypeError); !ok { 95 | t.Errorf("Expect unmarshal error return when JSON invalid") 96 | } 97 | } 98 | 99 | func TestRunPolicy_String(t *testing.T) { 100 | tests := []struct { 101 | policy RunPolicy 102 | value string 103 | }{ 104 | { 105 | policy: RunAlways, 106 | value: "always", 107 | }, 108 | { 109 | policy: RunOnFailure, 110 | value: "on-failure", 111 | }, 112 | { 113 | policy: RunOnSuccess, 114 | value: "on-success", 115 | }, 116 | } 117 | for _, test := range tests { 118 | if got, want := test.policy.String(), test.value; got != want { 119 | t.Errorf("Want policy string %q, got %q", want, got) 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /pipeline/runtime/execer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package runtime 6 | 7 | import "testing" 8 | 9 | func TestExec(t *testing.T) { 10 | t.Skip() 11 | } 12 | 13 | func TestExec_NonZeroExit(t *testing.T) { 14 | t.Skip() 15 | } 16 | 17 | func TestExec_Exit78(t *testing.T) { 18 | t.Skip() 19 | } 20 | 21 | func TestExec_Error(t *testing.T) { 22 | t.Skip() 23 | } 24 | 25 | func TestExec_CtxError(t *testing.T) { 26 | t.Skip() 27 | } 28 | 29 | func TestExec_ReportError(t *testing.T) { 30 | t.Skip() 31 | } 32 | 33 | func TestExec_SkipCtxDone(t *testing.T) { 34 | t.Skip() 35 | } 36 | -------------------------------------------------------------------------------- /pipeline/runtime/replacer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package runtime 6 | 7 | import ( 8 | "io" 9 | "strings" 10 | ) 11 | 12 | // replacer is an io.Writer that finds and masks sensitive data. 13 | type replacer struct { 14 | w io.WriteCloser 15 | r *strings.Replacer 16 | } 17 | 18 | // newReplacer returns a replacer that wraps io.Writer w. 19 | func newReplacer(w io.WriteCloser, secrets []Secret) io.WriteCloser { 20 | var oldnew []string 21 | for _, secret := range secrets { 22 | v := secret.GetValue() 23 | if len(v) == 0 || secret.IsMasked() == false { 24 | continue 25 | } 26 | 27 | for _, part := range strings.Split(v, "\n") { 28 | part = strings.TrimSpace(part) 29 | 30 | // avoid masking empty or single character 31 | // strings. 32 | if len(part) < 2 { 33 | continue 34 | } 35 | 36 | masked := "******" 37 | oldnew = append(oldnew, part) 38 | oldnew = append(oldnew, masked) 39 | } 40 | } 41 | if len(oldnew) == 0 { 42 | return w 43 | } 44 | return &replacer{ 45 | w: w, 46 | r: strings.NewReplacer(oldnew...), 47 | } 48 | } 49 | 50 | // Write writes p to the base writer. The method scans for any 51 | // sensitive data in p and masks before writing. 52 | func (r *replacer) Write(p []byte) (n int, err error) { 53 | _, err = r.w.Write([]byte(r.r.Replace(string(p)))) 54 | return len(p), err 55 | } 56 | 57 | // Close closes the base writer. 58 | func (r *replacer) Close() error { 59 | return r.w.Close() 60 | } 61 | -------------------------------------------------------------------------------- /pipeline/runtime/runner_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package runtime 6 | -------------------------------------------------------------------------------- /pipeline/streamer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package pipeline 6 | 7 | import ( 8 | "context" 9 | "io" 10 | ) 11 | 12 | // A Streamer streams the pipeline logs. 13 | type Streamer interface { 14 | // Stream returns an io.WriteCloser to stream the stdout 15 | // and stderr of the pipeline step. 16 | Stream(context.Context, *State, string) io.WriteCloser 17 | } 18 | 19 | // NopStreamer returns a noop streamer. 20 | func NopStreamer() Streamer { 21 | return new(nopStreamer) 22 | } 23 | 24 | type nopStreamer struct{} 25 | 26 | func (*nopStreamer) Stream(context.Context, *State, string) io.WriteCloser { 27 | return new(nopWriteCloser) 28 | } 29 | 30 | type nopWriteCloser struct{} 31 | 32 | func (*nopWriteCloser) Close() error { return nil } 33 | func (*nopWriteCloser) Write(p []byte) (int, error) { return len(p), nil } 34 | -------------------------------------------------------------------------------- /pipeline/streamer/console/console.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package console provides a streamer that writes the pipeline 6 | // output to stdout. 7 | package console 8 | 9 | import ( 10 | "context" 11 | "io" 12 | "os" 13 | 14 | "github.com/drone/runner-go/pipeline" 15 | ) 16 | 17 | var _ pipeline.Streamer = (*Console)(nil) 18 | 19 | // Console implements a pipeline streamer that writes the 20 | // pipeline logs to the console using os.Stdout. 21 | type Console struct { 22 | seq *sequence 23 | col *sequence 24 | tty bool 25 | } 26 | 27 | // New returns a new console recorder. 28 | func New(tty bool) *Console { 29 | return &Console{ 30 | tty: tty, 31 | seq: new(sequence), 32 | col: new(sequence), 33 | } 34 | } 35 | 36 | // Stream returns an io.WriteCloser that prints formatted log 37 | // lines to the console with step name, line number, and optional 38 | // coloring. 39 | func (s *Console) Stream(_ context.Context, _ *pipeline.State, name string) io.WriteCloser { 40 | if s.tty { 41 | return &pretty{ 42 | base: os.Stdout, 43 | color: colors[s.col.next()%len(colors)], 44 | name: name, 45 | seq: s.seq, 46 | } 47 | } 48 | return &plain{ 49 | base: os.Stdout, 50 | name: name, 51 | seq: s.seq, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pipeline/streamer/console/plain.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package console 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "strings" 11 | ) 12 | 13 | // plain text line format with line number. 14 | const plainf = "[%s:%d] %s\n" 15 | 16 | type plain struct { 17 | base io.Writer 18 | name string 19 | seq *sequence 20 | } 21 | 22 | func (w *plain) Write(b []byte) (int, error) { 23 | for _, part := range split(b) { 24 | fmt.Fprintf(w.base, plainf, w.name, w.seq.next(), part) 25 | } 26 | return len(b), nil 27 | } 28 | 29 | func (w *plain) Close() error { 30 | return nil 31 | } 32 | 33 | func split(b []byte) []string { 34 | s := string(b) 35 | s = strings.TrimSuffix(s, "\n") 36 | return strings.Split(s, "\n") 37 | } 38 | -------------------------------------------------------------------------------- /pipeline/streamer/console/plain_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package console 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | func TestPlain(t *testing.T) { 15 | buf := new(bytes.Buffer) 16 | 17 | sess := New(false) 18 | w := sess.Stream(nil, nil, "clone").(*plain) 19 | w.base = buf 20 | w.Write([]byte("hello\nworld")) 21 | w.Close() 22 | 23 | got, want := buf.String(), "[clone:1] hello\n[clone:2] world\n" 24 | if diff := cmp.Diff(got, want); diff != "" { 25 | t.Errorf("Invalid plain text log output") 26 | t.Log(diff) 27 | } 28 | } 29 | 30 | func TestSplit(t *testing.T) { 31 | tests := []struct { 32 | before string 33 | after []string 34 | }{ 35 | { 36 | before: "hello world", 37 | after: []string{"hello world"}, 38 | }, 39 | { 40 | before: "hello world\n", 41 | after: []string{"hello world"}, 42 | }, 43 | { 44 | before: "hello\nworld\n", 45 | after: []string{"hello", "world"}, 46 | }, 47 | { 48 | before: "hello\n\nworld\n", 49 | after: []string{"hello", "", "world"}, 50 | }, 51 | { 52 | before: "\nhello\n\nworld\n", 53 | after: []string{"", "hello", "", "world"}, 54 | }, 55 | { 56 | before: "\n", 57 | after: []string{""}, 58 | }, 59 | { 60 | before: "\n\n", 61 | after: []string{"", ""}, 62 | }, 63 | } 64 | for _, test := range tests { 65 | b := []byte(test.before) 66 | got, want := split(b), test.after 67 | if diff := cmp.Diff(got, want); diff != "" { 68 | t.Errorf("Invalid split") 69 | t.Log(diff) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pipeline/streamer/console/pretty.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package console 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | ) 11 | 12 | // pretty line format with line number and coloring. 13 | const prettyf = "\033[%s[%s:%d]\033[0m %s\n" 14 | 15 | // available terminal colors 16 | var colors = []string{ 17 | "32m", // green 18 | "33m", // yellow 19 | "34m", // blue 20 | "35m", // magenta 21 | "36m", // cyan 22 | } 23 | 24 | type pretty struct { 25 | base io.Writer 26 | color string 27 | name string 28 | seq *sequence 29 | } 30 | 31 | func (w *pretty) Write(b []byte) (int, error) { 32 | for _, part := range split(b) { 33 | fmt.Fprintf(w.base, prettyf, w.color, w.name, w.seq.next(), part) 34 | } 35 | return len(b), nil 36 | } 37 | 38 | func (w *pretty) Close() error { 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /pipeline/streamer/console/pretty_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package console 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | func TestPretty(t *testing.T) { 15 | buf := new(bytes.Buffer) 16 | 17 | sess := New(true) 18 | w := sess.Stream(nil, nil, "clone").(*pretty) 19 | w.base = buf 20 | w.Write([]byte("hello\nworld")) 21 | w.Close() 22 | 23 | got, want := buf.String(), "\x1b[33m[clone:1]\x1b[0m hello\n\x1b[33m[clone:2]\x1b[0m world\n" 24 | if diff := cmp.Diff(got, want); diff != "" { 25 | t.Errorf("Invalid plain text log output") 26 | t.Log(diff) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pipeline/streamer/console/sequence.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package console 6 | 7 | import "sync" 8 | 9 | // sequence provides a thread-safe counter. 10 | type sequence struct { 11 | sync.Mutex 12 | value int 13 | } 14 | 15 | // next returns the next sequence value. 16 | func (s *sequence) next() int { 17 | s.Lock() 18 | s.value++ 19 | i := s.value 20 | s.Unlock() 21 | return i 22 | } 23 | 24 | // curr returns the current sequence value. 25 | func (s *sequence) curr() int { 26 | s.Lock() 27 | i := s.value 28 | s.Unlock() 29 | return i 30 | } 31 | -------------------------------------------------------------------------------- /pipeline/streamer/console/sequence_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package console 6 | 7 | import "testing" 8 | 9 | func TestSequence(t *testing.T) { 10 | c := new(sequence) 11 | if got, want := c.curr(), 0; got != want { 12 | t.Errorf("Want curr sequence value %d, got %d", want, got) 13 | } 14 | if got, want := c.next(), 1; got != want { 15 | t.Errorf("Want next sequence value %d, got %d", want, got) 16 | } 17 | if got, want := c.curr(), 1; got != want { 18 | t.Errorf("Want curr sequence value %d, got %d", want, got) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pipeline/streamer/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package streamer provides Streamer implementations. 6 | package streamer 7 | -------------------------------------------------------------------------------- /pipeline/streamer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package pipeline 6 | -------------------------------------------------------------------------------- /pipeline/uploader.go: -------------------------------------------------------------------------------- 1 | package pipeline 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Uploader interface { 8 | UploadCard(context.Context, []byte, *State, string) error 9 | } 10 | 11 | func NopUploader() Uploader { 12 | return new(nopUploader) 13 | } 14 | 15 | type nopUploader struct{} 16 | 17 | func (*nopUploader) UploadCard(context.Context, []byte, *State, string) error { return nil } 18 | -------------------------------------------------------------------------------- /pipeline/uploader/upload.go: -------------------------------------------------------------------------------- 1 | package uploader 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/drone/drone-go/drone" 8 | "github.com/drone/runner-go/client" 9 | "github.com/drone/runner-go/internal" 10 | "github.com/drone/runner-go/pipeline" 11 | ) 12 | 13 | var _ pipeline.Uploader = (*Upload)(nil) 14 | 15 | type Upload struct { 16 | client client.Client 17 | } 18 | 19 | func New(client client.Client) *Upload { 20 | return &Upload{ 21 | client: client, 22 | } 23 | } 24 | 25 | func (s *Upload) UploadCard(ctx context.Context, bytes []byte, state *pipeline.State, stepName string) error { 26 | src := state.Find(stepName) 27 | card := drone.CardInput{} 28 | err := json.Unmarshal(bytes, &card) 29 | if err != nil { 30 | return err 31 | } 32 | err = s.client.UploadCard(ctx, src.ID, &card) 33 | if err != nil { 34 | return err 35 | } 36 | // update step schema 37 | state.Lock() 38 | src.Schema = card.Schema 39 | cpy := internal.CloneStep(src) 40 | state.Unlock() 41 | err = s.client.UpdateStep(ctx, cpy) 42 | if err == nil { 43 | state.Lock() 44 | internal.MergeStep(cpy, src) 45 | state.Unlock() 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /pipeline/uploader/upload_test.go: -------------------------------------------------------------------------------- 1 | package uploader 2 | -------------------------------------------------------------------------------- /pipeline/uploader_test.go: -------------------------------------------------------------------------------- 1 | package pipeline 2 | -------------------------------------------------------------------------------- /poller/poller.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package poller 6 | 7 | import ( 8 | "context" 9 | "sync" 10 | 11 | "github.com/drone/drone-go/drone" 12 | "github.com/drone/runner-go/client" 13 | "github.com/drone/runner-go/logger" 14 | ) 15 | 16 | var noContext = context.Background() 17 | 18 | // Poller polls the server for pending stages and dispatches 19 | // for execution by the Runner. 20 | type Poller struct { 21 | Client client.Client 22 | Filter *client.Filter 23 | 24 | // Dispatch is dispatches the resource for processing. 25 | // It is invoked by the poller when a resource is 26 | // received by the remote system. 27 | Dispatch func(context.Context, *drone.Stage) error 28 | } 29 | 30 | // Poll opens N connections to the server to poll for pending 31 | // stages for execution. Pending stages are dispatched to a 32 | // Runner for execution. 33 | func (p *Poller) Poll(ctx context.Context, n int) { 34 | var wg sync.WaitGroup 35 | for i := 0; i < n; i++ { 36 | wg.Add(1) 37 | go func(i int) { 38 | for { 39 | select { 40 | case <-ctx.Done(): 41 | wg.Done() 42 | return 43 | default: 44 | p.poll(ctx, i+1) 45 | } 46 | } 47 | }(i) 48 | } 49 | 50 | wg.Wait() 51 | } 52 | 53 | // poll requests a stage for execution from the server, and then 54 | // dispatches for execution. 55 | func (p *Poller) poll(ctx context.Context, thread int) error { 56 | log := logger.FromContext(ctx).WithField("thread", thread) 57 | log.WithField("thread", thread).Debug("poller: request stage from remote server") 58 | 59 | // request a new build stage for execution from the central 60 | // build server. 61 | stage, err := p.Client.Request(ctx, p.Filter) 62 | if err == context.Canceled || err == context.DeadlineExceeded { 63 | log.WithError(err).Trace("poller: no stage returned") 64 | return nil 65 | } 66 | if err != nil { 67 | log.WithError(err).Error("poller: cannot request stage") 68 | return err 69 | } 70 | 71 | // exit if a nil or empty stage is returned from the system 72 | // and allow the runner to retry. 73 | if stage == nil || stage.ID == 0 { 74 | return nil 75 | } 76 | 77 | return p.Dispatch( 78 | logger.WithContext(noContext, log), stage) 79 | } 80 | -------------------------------------------------------------------------------- /poller/poller_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package poller 6 | 7 | import "testing" 8 | 9 | func TestPoll(t *testing.T) { 10 | t.Skip() 11 | } 12 | 13 | func TestPoll_NilStage(t *testing.T) { 14 | t.Skip() 15 | } 16 | 17 | func TestPoll_EmptyStage(t *testing.T) { 18 | t.Skip() 19 | } 20 | 21 | func TestPoll_RequestError(t *testing.T) { 22 | t.Skip() 23 | } 24 | -------------------------------------------------------------------------------- /registry/auths/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package auths 6 | 7 | import ( 8 | "bytes" 9 | "encoding/base64" 10 | "encoding/json" 11 | "io" 12 | "net/url" 13 | "os" 14 | "strings" 15 | 16 | "github.com/drone/drone-go/drone" 17 | ) 18 | 19 | type ( 20 | // config represents the Docker client configuration, 21 | // typically located at ~/.docker/config.json 22 | config struct { 23 | Auths map[string]auth `json:"auths"` 24 | } 25 | 26 | // auth stores the registry authentication string. 27 | auth struct { 28 | Auth string `json:"auth"` 29 | Username string `json:"username,omitempty"` 30 | Password string `json:"password,omitempty"` 31 | } 32 | ) 33 | 34 | // Parse parses the registry credential from the reader. 35 | func Parse(r io.Reader) ([]*drone.Registry, error) { 36 | c := new(config) 37 | err := json.NewDecoder(r).Decode(c) 38 | if err != nil { 39 | return nil, err 40 | } 41 | var auths []*drone.Registry 42 | for k, v := range c.Auths { 43 | username, password := v.Username, v.Password 44 | if v.Auth != "" { 45 | username, password = decode(v.Auth) 46 | } 47 | auths = append(auths, &drone.Registry{ 48 | Address: hostname(k), 49 | Username: username, 50 | Password: password, 51 | }) 52 | } 53 | return auths, nil 54 | } 55 | 56 | // ParseFile parses the registry credential file. 57 | func ParseFile(filepath string) ([]*drone.Registry, error) { 58 | f, err := os.Open(filepath) 59 | if err != nil { 60 | return nil, err 61 | } 62 | defer f.Close() 63 | return Parse(f) 64 | } 65 | 66 | // ParseString parses the registry credential file. 67 | func ParseString(s string) ([]*drone.Registry, error) { 68 | return Parse(strings.NewReader(s)) 69 | } 70 | 71 | // ParseBytes parses the registry credential file. 72 | func ParseBytes(b []byte) ([]*drone.Registry, error) { 73 | return Parse(bytes.NewReader(b)) 74 | } 75 | 76 | // Header returns the json marshaled, base64 encoded 77 | // credential string that can be passed to the docker 78 | // registry authentication header. 79 | func Header(username, password string) string { 80 | v := struct { 81 | Username string `json:"username,omitempty"` 82 | Password string `json:"password,omitempty"` 83 | }{ 84 | Username: username, 85 | Password: password, 86 | } 87 | buf, _ := json.Marshal(&v) 88 | return base64.URLEncoding.EncodeToString(buf) 89 | } 90 | 91 | // encode returns the encoded credentials. 92 | func encode(username, password string) string { 93 | return base64.StdEncoding.EncodeToString( 94 | []byte(username + ":" + password), 95 | ) 96 | } 97 | 98 | // decode returns the decoded credentials. 99 | func decode(s string) (username, password string) { 100 | d, err := base64.StdEncoding.DecodeString(s) 101 | if err != nil { 102 | return 103 | } 104 | parts := strings.SplitN(string(d), ":", 2) 105 | if len(parts) > 0 { 106 | username = parts[0] 107 | } 108 | if len(parts) > 1 { 109 | password = parts[1] 110 | } 111 | return 112 | } 113 | 114 | // hostname returns the trimmed hostname from the 115 | // registry url. 116 | func hostname(s string) string { 117 | uri, _ := url.Parse(s) 118 | if uri != nil && uri.Host != "" { 119 | s = uri.Host 120 | } 121 | return s 122 | } 123 | -------------------------------------------------------------------------------- /registry/auths/encode.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package auths 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | 11 | "github.com/drone/drone-go/drone" 12 | ) 13 | 14 | // Encode encodes the registry credentials to using the 15 | // docker config json format and returns the resulting 16 | // data in string format. 17 | func Encode(registry ...*drone.Registry) string { 18 | c := new(config) 19 | c.Auths = map[string]auth{} 20 | for _, r := range registry { 21 | c.Auths[r.Address] = auth{ 22 | Auth: encode(r.Username, r.Password), 23 | } 24 | } 25 | buf := new(bytes.Buffer) 26 | enc := json.NewEncoder(buf) 27 | enc.Encode(c) 28 | return buf.String() 29 | } 30 | -------------------------------------------------------------------------------- /registry/auths/encode_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package auths 6 | 7 | import ( 8 | "strings" 9 | "testing" 10 | 11 | "github.com/drone/drone-go/drone" 12 | ) 13 | 14 | func TestEncode(t *testing.T) { 15 | endpoint := "docker.io" 16 | username := "octocat" 17 | password := "correct-horse-battery-staple" 18 | registry := &drone.Registry{ 19 | Username: username, 20 | Password: password, 21 | Address: endpoint, 22 | } 23 | got := Encode(registry, registry, registry) 24 | want := `{"auths":{"docker.io":{"auth":"b2N0b2NhdDpjb3JyZWN0LWhvcnNlLWJhdHRlcnktc3RhcGxl"}}}` 25 | if strings.TrimSpace(got) != strings.TrimSpace(want) { 26 | t.Errorf("Unexpected encoding: %q want %q", got, want) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /registry/auths/testdata/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auths": { 3 | "https://index.docker.io/v1/": { 4 | "auth": "b2N0b2NhdDpjb3JyZWN0LWhvcnNlLWJhdHRlcnktc3RhcGxl" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /registry/auths/testdata/config2.json: -------------------------------------------------------------------------------- 1 | { 2 | "auths": { 3 | "https://gcr.io": { 4 | "auth": "b2N0b2NhdDpjb3JyZWN0LWhvcnNlLWJhdHRlcnktc3RhcGxl" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /registry/combine.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package registry 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/drone/drone-go/drone" 11 | ) 12 | 13 | // Combine returns a new combined registry provider, capable of 14 | // sourcing registry credentials from multiple providers. 15 | func Combine(sources ...Provider) Provider { 16 | return &combined{sources} 17 | } 18 | 19 | type combined struct { 20 | sources []Provider 21 | } 22 | 23 | func (p *combined) List(ctx context.Context, in *Request) ([]*drone.Registry, error) { 24 | var out []*drone.Registry 25 | for _, source := range p.sources { 26 | list, err := source.List(ctx, in) 27 | if err != nil { 28 | return nil, err 29 | } 30 | out = append(out, list...) 31 | } 32 | return out, nil 33 | } 34 | -------------------------------------------------------------------------------- /registry/combine_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package registry 6 | 7 | import ( 8 | "errors" 9 | "testing" 10 | 11 | "github.com/drone/drone-go/drone" 12 | ) 13 | 14 | func TestCombine(t *testing.T) { 15 | a := &drone.Registry{} 16 | b := &drone.Registry{} 17 | aa := mockProvider{out: []*drone.Registry{a}} 18 | bb := mockProvider{out: []*drone.Registry{b}} 19 | p := Combine(&aa, &bb) 20 | out, err := p.List(noContext, nil) 21 | if err != nil { 22 | t.Error(err) 23 | return 24 | } 25 | if len(out) != 2 { 26 | t.Errorf("Expect combined registry output") 27 | return 28 | } 29 | if out[0] != a { 30 | t.Errorf("Unexpected registry at index 0") 31 | } 32 | if out[1] != b { 33 | t.Errorf("Unexpected registry at index 1") 34 | } 35 | } 36 | 37 | func TestCombineError(t *testing.T) { 38 | e := errors.New("not found") 39 | m := mockProvider{err: e} 40 | p := Combine(&m) 41 | _, err := p.List(noContext, nil) 42 | if err != e { 43 | t.Errorf("Expect error") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /registry/external.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package registry 6 | 7 | import ( 8 | "context" 9 | "time" 10 | 11 | "github.com/drone/drone-go/drone" 12 | "github.com/drone/drone-go/plugin/registry" 13 | "github.com/drone/runner-go/logger" 14 | ) 15 | 16 | // External returns a new external registry credentials 17 | // provider. The external credentials provider makes an 18 | // external API call to list and return credentials. 19 | func External(endpoint, token string, insecure bool) Provider { 20 | provider := &external{} 21 | if endpoint != "" { 22 | provider.client = registry.Client(endpoint, token, insecure) 23 | } 24 | return provider 25 | } 26 | 27 | type external struct { 28 | client registry.Plugin 29 | } 30 | 31 | func (p *external) List(ctx context.Context, in *Request) ([]*drone.Registry, error) { 32 | if p.client == nil { 33 | return nil, nil 34 | } 35 | 36 | logger := logger.FromContext(ctx) 37 | 38 | // include a timeout to prevent an API call from 39 | // hanging the build process indefinitely. The 40 | // external service must return a request within 41 | // one minute. 42 | ctx, cancel := context.WithTimeout(ctx, time.Minute) 43 | defer cancel() 44 | 45 | req := ®istry.Request{ 46 | Repo: *in.Repo, 47 | Build: *in.Build, 48 | } 49 | res, err := p.client.List(ctx, req) 50 | if err != nil { 51 | logger.WithError(err).Debug("registry: external: cannot get credentials") 52 | return nil, err 53 | } 54 | 55 | // if no error is returned and the list is empty, 56 | // this indicates the client returned No Content, 57 | // and we should exit with no credentials, but no error. 58 | if len(res) == 0 { 59 | logger.Trace("registry: external: credential list is empty") 60 | return nil, nil 61 | } 62 | 63 | for _, v := range res { 64 | logger. 65 | WithField("address", v.Address). 66 | WithField("username", v.Username). 67 | Trace("registry: external: received credentials") 68 | } 69 | 70 | return res, nil 71 | } 72 | -------------------------------------------------------------------------------- /registry/external_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package registry 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "testing" 11 | 12 | "github.com/drone/drone-go/drone" 13 | "github.com/drone/drone-go/plugin/registry" 14 | "github.com/google/go-cmp/cmp" 15 | ) 16 | 17 | func TestExternal(t *testing.T) { 18 | req := &Request{ 19 | Build: &drone.Build{Event: drone.EventPush}, 20 | Repo: &drone.Repo{Private: false}, 21 | } 22 | want := []*drone.Registry{ 23 | { 24 | Address: "index.docker.io", 25 | Username: "octocat", 26 | Password: "correct-horse-battery-staple", 27 | }, 28 | } 29 | provider := External("http://localhost", "secret", false) 30 | provider.(*external).client = &mockPlugin{out: want} 31 | got, err := provider.List(noContext, req) 32 | if err != nil { 33 | t.Error(err) 34 | } 35 | if diff := cmp.Diff(got, want); diff != "" { 36 | t.Errorf(diff) 37 | } 38 | } 39 | 40 | // This test verifies that if the remote API call to the 41 | // external plugin returns an error, the provider returns the 42 | // error to the caller. 43 | func TestExternal_ClientError(t *testing.T) { 44 | req := &Request{ 45 | Build: &drone.Build{Event: drone.EventPush}, 46 | Repo: &drone.Repo{Private: false}, 47 | } 48 | want := errors.New("Not Found") 49 | provider := External("http://localhost", "secret", false) 50 | provider.(*external).client = &mockPlugin{err: want} 51 | _, got := provider.List(noContext, req) 52 | if got != want { 53 | t.Errorf("Want error %s, got %s", want, got) 54 | } 55 | } 56 | 57 | // This test verifies that if no endpoint is configured the 58 | // provider exits immediately and returns a nil slice and nil 59 | // error. 60 | func TestExternal_NoEndpoint(t *testing.T) { 61 | provider := External("", "", false) 62 | res, err := provider.List(noContext, nil) 63 | if err != nil { 64 | t.Errorf("Expect nil error, provider disabled") 65 | } 66 | if res != nil { 67 | t.Errorf("Expect nil secret, provider disabled") 68 | } 69 | } 70 | 71 | // This test verifies that nil credentials and a nil error 72 | // are returned if the registry endpoint returns no content. 73 | func TestExternal_NotFound(t *testing.T) { 74 | req := &Request{ 75 | Repo: &drone.Repo{}, 76 | Build: &drone.Build{}, 77 | } 78 | provider := External("http://localhost", "secret", false) 79 | provider.(*external).client = &mockPlugin{} 80 | res, err := provider.List(noContext, req) 81 | if err != nil { 82 | t.Errorf("Expect nil error, registry list empty") 83 | } 84 | if res != nil { 85 | t.Errorf("Expect nil registry credentials") 86 | } 87 | } 88 | 89 | type mockPlugin struct { 90 | out []*drone.Registry 91 | err error 92 | } 93 | 94 | func (m *mockPlugin) List(context.Context, *registry.Request) ([]*drone.Registry, error) { 95 | return m.out, m.err 96 | } 97 | -------------------------------------------------------------------------------- /registry/file.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package registry 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/drone/drone-go/drone" 11 | "github.com/drone/runner-go/logger" 12 | "github.com/drone/runner-go/registry/auths" 13 | ) 14 | 15 | // File returns a new registry credential provider that 16 | // parses and returns credentials from the Docker user 17 | // configuration file. 18 | func File(path string) Provider { 19 | return &file{path} 20 | } 21 | 22 | type file struct { 23 | path string 24 | } 25 | 26 | func (p *file) List(ctx context.Context, _ *Request) ([]*drone.Registry, error) { 27 | if p.path == "" { 28 | return nil, nil 29 | } 30 | 31 | logger := logger.FromContext(ctx) 32 | logger.WithField("path", p.path). 33 | Trace("registry: file: parsing credentials file") 34 | 35 | // load the registry credentials from the file. 36 | res, err := auths.ParseFile(p.path) 37 | if err != nil { 38 | logger.WithError(err). 39 | Debug("registry: file: cannot parse credentials file") 40 | return nil, err 41 | } 42 | 43 | // if no error is returned and the list is empty, 44 | // this indicates the client returned No Content, 45 | // and we should exit with no credentials, but no error. 46 | if len(res) == 0 { 47 | logger.Trace("registry: file: credential list is empty") 48 | return nil, nil 49 | } 50 | 51 | for _, v := range res { 52 | logger. 53 | WithField("address", v.Address). 54 | WithField("username", v.Username). 55 | Trace("registry: file: received credentials") 56 | } 57 | 58 | return res, err 59 | } 60 | -------------------------------------------------------------------------------- /registry/file_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package registry 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/drone/drone-go/drone" 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | func TestFile(t *testing.T) { 15 | p := File("auths/testdata/config.json") 16 | got, err := p.List(noContext, nil) 17 | if err != nil { 18 | t.Error(err) 19 | return 20 | } 21 | want := []*drone.Registry{ 22 | { 23 | Address: "index.docker.io", 24 | Username: "octocat", 25 | Password: "correct-horse-battery-staple", 26 | }, 27 | } 28 | if diff := cmp.Diff(got, want); diff != "" { 29 | t.Errorf(diff) 30 | } 31 | } 32 | 33 | func TestFileEmptyPath(t *testing.T) { 34 | p := File("") 35 | out, err := p.List(noContext, nil) 36 | if err != nil { 37 | t.Error(err) 38 | } 39 | if len(out) != 0 { 40 | t.Errorf("Expect empty registry credentials") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /registry/registry.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package registry provides registry credentials used 6 | // to pull private images from a registry. 7 | package registry 8 | 9 | import ( 10 | "context" 11 | 12 | "github.com/drone/drone-go/drone" 13 | ) 14 | 15 | // Request provides arguments for requesting a secret from 16 | // a secret Provider. 17 | type Request struct { 18 | Repo *drone.Repo 19 | Build *drone.Build 20 | } 21 | 22 | // Provider is the interface that must be implemented by a 23 | // registry provider. 24 | type Provider interface { 25 | // Find finds and returns a list of registry credentials. 26 | List(context.Context, *Request) ([]*drone.Registry, error) 27 | } 28 | -------------------------------------------------------------------------------- /registry/registry_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package registry 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/drone/drone-go/drone" 11 | ) 12 | 13 | var noContext = context.Background() 14 | 15 | type mockProvider struct { 16 | out []*drone.Registry 17 | err error 18 | } 19 | 20 | func (p *mockProvider) List(context.Context, *Request) ([]*drone.Registry, error) { 21 | return p.out, p.err 22 | } 23 | -------------------------------------------------------------------------------- /registry/static.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package registry 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/drone/drone-go/drone" 11 | ) 12 | 13 | // Static returns a new static registry credential provider. 14 | // The static secret provider finds and returns the static list 15 | // of registry credentials. 16 | func Static(registries []*drone.Registry) Provider { 17 | return &static{registries} 18 | } 19 | 20 | type static struct { 21 | registries []*drone.Registry 22 | } 23 | 24 | func (p *static) List(context.Context, *Request) ([]*drone.Registry, error) { 25 | return p.registries, nil 26 | } 27 | -------------------------------------------------------------------------------- /registry/static_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package registry 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/drone/drone-go/drone" 11 | ) 12 | 13 | func TestStatic(t *testing.T) { 14 | a := &drone.Registry{} 15 | b := &drone.Registry{} 16 | p := Static([]*drone.Registry{a, b}) 17 | out, err := p.List(noContext, nil) 18 | if err != nil { 19 | t.Error(err) 20 | return 21 | } 22 | if len(out) != 2 { 23 | t.Errorf("Expect combined registry output") 24 | return 25 | } 26 | if out[0] != a { 27 | t.Errorf("Unexpected registry at index 0") 28 | } 29 | if out[1] != b { 30 | t.Errorf("Unexpected registry at index 1") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /secret/combine.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package secret 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/drone/drone-go/drone" 11 | ) 12 | 13 | // Combine returns a new combined secret provider, capable of 14 | // sourcing secrets from multiple providers. 15 | func Combine(sources ...Provider) Provider { 16 | return &combined{sources} 17 | } 18 | 19 | type combined struct { 20 | sources []Provider 21 | } 22 | 23 | func (p *combined) Find(ctx context.Context, in *Request) (*drone.Secret, error) { 24 | for _, source := range p.sources { 25 | secret, err := source.Find(ctx, in) 26 | if err != nil { 27 | return nil, err 28 | } 29 | if secret == nil { 30 | continue 31 | } 32 | // if the secret object is not nil, but is empty 33 | // we should assume the secret service returned a 34 | // 204 no content, and proceed to the next service 35 | // in the chain. 36 | if secret.Data == "" { 37 | continue 38 | } 39 | return secret, nil 40 | } 41 | return nil, nil 42 | } 43 | -------------------------------------------------------------------------------- /secret/combine_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package secret 6 | 7 | import ( 8 | "errors" 9 | "testing" 10 | 11 | "github.com/drone/drone-go/drone" 12 | ) 13 | 14 | func TestCombine(t *testing.T) { 15 | secrets := []*drone.Secret{ 16 | {Name: "docker_username", Data: "octocat"}, 17 | {Name: "docker_password", Data: "correct-horse-battery-staple"}, 18 | } 19 | args := &Request{ 20 | Name: "docker_password", 21 | Build: &drone.Build{Event: drone.EventPush}, 22 | } 23 | service := Combine(Static(secrets[:1]), Static(secrets[1:])) 24 | secret, err := service.Find(noContext, args) 25 | if err != nil { 26 | t.Error(err) 27 | return 28 | } 29 | if secret != secrets[1] { 30 | t.Errorf("expect docker_password") 31 | } 32 | } 33 | 34 | func TestCombine_Error(t *testing.T) { 35 | args := &Request{ 36 | Name: "slack_token", 37 | Build: &drone.Build{Event: drone.EventPush}, 38 | } 39 | want := errors.New("cannot find secret") 40 | mock := &mockProvider{err: want} 41 | service := Combine(mock) 42 | _, got := service.Find(noContext, args) 43 | if got != want { 44 | t.Errorf("expect error") 45 | } 46 | } 47 | 48 | func TestCombine_NotFound(t *testing.T) { 49 | secrets := []*drone.Secret{ 50 | {Name: "docker_username", Data: "octocat"}, 51 | {Name: "docker_password", Data: "correct-horse-battery-staple"}, 52 | } 53 | args := &Request{ 54 | Name: "slack_token", 55 | Build: &drone.Build{Event: drone.EventPush}, 56 | } 57 | service := Combine(Static(secrets)) 58 | secret, err := service.Find(noContext, args) 59 | if err != nil { 60 | t.Error(err) 61 | } 62 | if secret != nil { 63 | t.Errorf("expect nil secret") 64 | } 65 | } 66 | 67 | func TestCombine_Empty(t *testing.T) { 68 | secrets := []*drone.Secret{ 69 | {Name: "docker_username", Data: ""}, 70 | {Name: "docker_password", Data: ""}, 71 | } 72 | args := &Request{ 73 | Name: "docker_password", 74 | Build: &drone.Build{Event: drone.EventPush}, 75 | } 76 | service := Combine(Static(secrets)) 77 | secret, err := service.Find(noContext, args) 78 | if err != nil { 79 | t.Error(err) 80 | } 81 | if secret != nil { 82 | t.Errorf("expect nil secret") 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /secret/encrypted.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package secret 6 | 7 | import ( 8 | "context" 9 | "crypto/aes" 10 | "crypto/cipher" 11 | "encoding/base64" 12 | "errors" 13 | 14 | "github.com/drone/runner-go/logger" 15 | "github.com/drone/runner-go/manifest" 16 | 17 | "github.com/drone/drone-go/drone" 18 | ) 19 | 20 | // Encrypted returns a new encrypted secret provider. The 21 | // encrypted secret provider finds and decrypts secrets stored 22 | // inline in the (yaml) configuration. 23 | func Encrypted() Provider { 24 | return new(encrypted) 25 | } 26 | 27 | type encrypted struct{} 28 | 29 | func (p *encrypted) Find(ctx context.Context, in *Request) (*drone.Secret, error) { 30 | logger := logger.FromContext(ctx). 31 | WithField("name", in.Name). 32 | WithField("kind", "secret") 33 | 34 | // lookup the named secret in the manifest. If the 35 | // secret does not exist, return a nil variable, 36 | // allowing the next secret controller in the chain 37 | // to be invoked. 38 | data, ok := getEncrypted(in.Conf, in.Name) 39 | if !ok { 40 | logger.Trace("secret: encrypted: no matching secret") 41 | return nil, nil 42 | } 43 | 44 | // if the build event is a pull request and the source 45 | // repository is a fork, the secret is not exposed to 46 | // the pipeline, for security reasons. 47 | if in.Repo.Private == false && 48 | in.Build.Event == drone.EventPullRequest && 49 | in.Build.Fork != "" { 50 | logger.Trace("secret: encrypted: restricted from forks") 51 | return nil, nil 52 | } 53 | 54 | decoded, err := base64.StdEncoding.DecodeString(string(data)) 55 | if err != nil { 56 | logger.WithError(err).Debug("secret: encrypted: cannot decode") 57 | return nil, err 58 | } 59 | 60 | decrypted, err := decrypt(decoded, []byte(in.Repo.Secret)) 61 | if err != nil { 62 | logger.WithError(err).Debug("secret: encrypted: cannot decrypt") 63 | return nil, err 64 | } 65 | 66 | logger.Trace("secret: encrypted: found matching secret") 67 | 68 | return &drone.Secret{ 69 | Name: in.Name, 70 | Data: string(decrypted), 71 | }, nil 72 | } 73 | 74 | func getEncrypted(spec *manifest.Manifest, match string) (data string, ok bool) { 75 | for _, resource := range spec.Resources { 76 | secret, ok := resource.(*manifest.Secret) 77 | if !ok { 78 | continue 79 | } 80 | if secret.Name != match { 81 | continue 82 | } 83 | if secret.Data == "" { 84 | continue 85 | } 86 | return secret.Data, true 87 | } 88 | return 89 | } 90 | 91 | func decrypt(ciphertext []byte, key []byte) (plaintext []byte, err error) { 92 | block, err := aes.NewCipher(key[:]) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | gcm, err := cipher.NewGCM(block) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | if len(ciphertext) < gcm.NonceSize() { 103 | return nil, errors.New("malformed ciphertext") 104 | } 105 | 106 | return gcm.Open(nil, 107 | ciphertext[:gcm.NonceSize()], 108 | ciphertext[gcm.NonceSize():], 109 | nil, 110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /secret/external.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package secret 6 | 7 | import ( 8 | "context" 9 | "time" 10 | 11 | "github.com/drone/runner-go/logger" 12 | "github.com/drone/runner-go/manifest" 13 | 14 | "github.com/drone/drone-go/drone" 15 | "github.com/drone/drone-go/plugin/secret" 16 | ) 17 | 18 | // External returns a new external secret provider. The 19 | // external secret provider makes an external API call to find 20 | // and return a named secret. 21 | func External(endpoint, token string, insecure bool) Provider { 22 | provider := &external{} 23 | if endpoint != "" { 24 | provider.client = secret.Client(endpoint, token, insecure) 25 | } 26 | return provider 27 | } 28 | 29 | type external struct { 30 | client secret.Plugin 31 | } 32 | 33 | func (p *external) Find(ctx context.Context, in *Request) (*drone.Secret, error) { 34 | if p.client == nil { 35 | return nil, nil 36 | } 37 | 38 | logger := logger.FromContext(ctx). 39 | WithField("name", in.Name). 40 | WithField("kind", "secret") 41 | 42 | // lookup the named secret in the manifest. If the 43 | // secret does not exist, return a nil variable, 44 | // allowing the next secret controller in the chain 45 | // to be invoked. 46 | path, name, ok := getExternal(in.Conf, in.Name) 47 | if !ok { 48 | logger.Trace("secret: external: no matching secret") 49 | return nil, nil 50 | } 51 | 52 | // include a timeout to prevent an API call from 53 | // hanging the build process indefinitely. The 54 | // external service must return a request within 55 | // one minute. 56 | ctx, cancel := context.WithTimeout(ctx, time.Minute) 57 | defer cancel() 58 | 59 | req := &secret.Request{ 60 | Name: name, 61 | Path: path, 62 | Repo: *in.Repo, 63 | Build: *in.Build, 64 | } 65 | res, err := p.client.Find(ctx, req) 66 | if err != nil { 67 | logger.WithError(err).Debug("secret: external: cannot get secret") 68 | return nil, err 69 | } 70 | 71 | // if no error is returned and the secret is empty, 72 | // this indicates the client returned No Content, 73 | // and we should exit with no secret, but no error. 74 | if res.Data == "" { 75 | logger.Trace("secret: external: secret is empty") 76 | return nil, nil 77 | } 78 | 79 | logger.Trace("secret: external: found matching secret") 80 | 81 | return &drone.Secret{ 82 | Name: in.Name, 83 | Data: res.Data, 84 | PullRequest: res.Pull, 85 | }, nil 86 | } 87 | 88 | func getExternal(spec *manifest.Manifest, match string) (path, name string, ok bool) { 89 | for _, resource := range spec.Resources { 90 | secret, ok := resource.(*manifest.Secret) 91 | if !ok { 92 | continue 93 | } 94 | if secret.Name != match { 95 | continue 96 | } 97 | if secret.Get.Name == "" && secret.Get.Path == "" { 98 | continue 99 | } 100 | return secret.Get.Path, secret.Get.Name, true 101 | } 102 | return 103 | } 104 | -------------------------------------------------------------------------------- /secret/secret.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package secret provides secrets to a pipeline. 6 | package secret 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/drone/drone-go/drone" 12 | "github.com/drone/runner-go/manifest" 13 | ) 14 | 15 | // Request provides arguments for requesting a secret from 16 | // a secret Provider. 17 | type Request struct { 18 | Name string 19 | Repo *drone.Repo 20 | Build *drone.Build 21 | Conf *manifest.Manifest 22 | } 23 | 24 | // Provider is the interface that must be implemented by a 25 | // secret provider. 26 | type Provider interface { 27 | // Find finds and returns a requested secret. 28 | Find(context.Context, *Request) (*drone.Secret, error) 29 | } 30 | -------------------------------------------------------------------------------- /secret/secret_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package secret 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/drone/drone-go/drone" 11 | ) 12 | 13 | type mockProvider struct { 14 | sec *drone.Secret 15 | err error 16 | } 17 | 18 | func (p *mockProvider) Find(context.Context, *Request) (*drone.Secret, error) { 19 | return p.sec, p.err 20 | } 21 | -------------------------------------------------------------------------------- /secret/static.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package secret 6 | 7 | import ( 8 | "context" 9 | "strings" 10 | 11 | "github.com/drone/runner-go/logger" 12 | 13 | "github.com/drone/drone-go/drone" 14 | ) 15 | 16 | // Static returns a new static secret provider. The static 17 | // secret provider finds and returns a named secret from the 18 | // static list. 19 | func Static(secrets []*drone.Secret) Provider { 20 | return &static{secrets} 21 | } 22 | 23 | // StaticVars returns a new static secret provider. The static 24 | // secret provider finds and returns a named secret from the 25 | // static key value pairs. 26 | func StaticVars(vars map[string]string) Provider { 27 | var secrets []*drone.Secret 28 | for k, v := range vars { 29 | secrets = append(secrets, &drone.Secret{ 30 | Name: k, 31 | Data: v, 32 | }) 33 | } 34 | return Static(secrets) 35 | } 36 | 37 | type static struct { 38 | secrets []*drone.Secret 39 | } 40 | 41 | func (p *static) Find(ctx context.Context, in *Request) (*drone.Secret, error) { 42 | logger := logger.FromContext(ctx). 43 | WithField("name", in.Name). 44 | WithField("kind", "secret") 45 | 46 | for _, secret := range p.secrets { 47 | if !strings.EqualFold(secret.Name, in.Name) { 48 | continue 49 | } 50 | // The secret can be restricted to non-pull request 51 | // events. If the secret is restricted, return 52 | // empty results. 53 | if secret.PullRequest == false && 54 | in.Build.Event == drone.EventPullRequest { 55 | logger.Trace("secret: database: restricted from pull requests") 56 | continue 57 | } 58 | 59 | logger.Trace("secret: database: found matching secret") 60 | return secret, nil 61 | } 62 | 63 | logger.Trace("secret: database: no matching secret") 64 | return nil, nil 65 | } 66 | -------------------------------------------------------------------------------- /secret/static_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package secret 6 | 7 | import ( 8 | "context" 9 | "testing" 10 | 11 | "github.com/drone/drone-go/drone" 12 | ) 13 | 14 | var noContext = context.Background() 15 | 16 | func TestStatic(t *testing.T) { 17 | secrets := []*drone.Secret{ 18 | {Name: "docker_username"}, 19 | {Name: "docker_password"}, 20 | } 21 | args := &Request{ 22 | Name: "docker_password", 23 | Build: &drone.Build{Event: drone.EventPush}, 24 | } 25 | service := Static(secrets) 26 | secret, err := service.Find(noContext, args) 27 | if err != nil { 28 | t.Error(err) 29 | return 30 | } 31 | if secret != secrets[1] { 32 | t.Errorf("expect docker_password") 33 | } 34 | } 35 | 36 | func TestStaticVars(t *testing.T) { 37 | secrets := map[string]string{ 38 | "docker_username": "octocat", 39 | "docker_password": "correct-horse-battery-staple", 40 | } 41 | args := &Request{ 42 | Name: "docker_password", 43 | Build: &drone.Build{Event: drone.EventPush}, 44 | } 45 | service := StaticVars(secrets) 46 | secret, err := service.Find(noContext, args) 47 | if err != nil { 48 | t.Error(err) 49 | return 50 | } 51 | if secret.Data != secrets["docker_password"] { 52 | t.Errorf("expect docker_password") 53 | } 54 | } 55 | 56 | func TestStaticNotFound(t *testing.T) { 57 | secrets := []*drone.Secret{ 58 | {Name: "docker_username"}, 59 | {Name: "docker_password"}, 60 | } 61 | args := &Request{ 62 | Name: "slack_token", 63 | Build: &drone.Build{Event: drone.EventPush}, 64 | } 65 | service := Static(secrets) 66 | secret, err := service.Find(noContext, args) 67 | if err != nil { 68 | t.Error(err) 69 | return 70 | } 71 | if secret != nil { 72 | t.Errorf("Expect secret not found") 73 | } 74 | } 75 | 76 | func TestStaticPullRequestDisabled(t *testing.T) { 77 | secrets := []*drone.Secret{ 78 | {Name: "docker_username"}, 79 | {Name: "docker_password", PullRequest: false}, 80 | } 81 | args := &Request{ 82 | Name: "docker_password", 83 | Build: &drone.Build{Event: drone.EventPullRequest}, 84 | } 85 | service := Static(secrets) 86 | secret, err := service.Find(noContext, args) 87 | if err != nil { 88 | t.Error(err) 89 | return 90 | } 91 | if secret != nil { 92 | t.Errorf("Expect secret not found") 93 | } 94 | } 95 | 96 | func TestStaticPullRequestEnabled(t *testing.T) { 97 | secrets := []*drone.Secret{ 98 | {Name: "docker_username"}, 99 | {Name: "docker_password", PullRequest: true}, 100 | } 101 | args := &Request{ 102 | Name: "docker_password", 103 | Build: &drone.Build{Event: drone.EventPullRequest}, 104 | } 105 | service := Static(secrets) 106 | secret, err := service.Find(noContext, args) 107 | if err != nil { 108 | t.Error(err) 109 | return 110 | } 111 | if err != nil { 112 | t.Error(err) 113 | return 114 | } 115 | if secret != secrets[1] { 116 | t.Errorf("expect docker_username") 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /shell/bash/bash.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package shell provides functions for converting shell commands 6 | // to shell scripts. 7 | package bash 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "strings" 13 | ) 14 | 15 | // Suffix provides the shell script suffix. For posix systems 16 | // this value is an empty string. 17 | const Suffix = "" 18 | 19 | // Command returns the shell command and arguments. 20 | func Command() (string, []string) { 21 | return "/bin/sh", []string{"-e"} 22 | } 23 | 24 | func Script(commands []string) string { 25 | return script(commands, true) 26 | } 27 | 28 | func SilentScript(commands []string) string { 29 | return script(commands, false) 30 | } 31 | 32 | // Script converts a slice of individual shell commands to a posix-compliant shell script. 33 | func script(commands []string, trace bool) string { 34 | buf := new(bytes.Buffer) 35 | fmt.Fprintln(buf) 36 | fmt.Fprintf(buf, optionScript) 37 | fmt.Fprintln(buf) 38 | for _, command := range commands { 39 | escaped := fmt.Sprintf("%q", command) 40 | escaped = strings.Replace(escaped, "$", `\$`, -1) 41 | var stringToWrite string 42 | if trace { 43 | stringToWrite = fmt.Sprintf( 44 | traceScript, 45 | escaped, 46 | command, 47 | ) 48 | } else { 49 | stringToWrite = "\n" + command + "\n" 50 | } 51 | buf.WriteString(stringToWrite) 52 | } 53 | return buf.String() 54 | } 55 | 56 | // optionScript is a helper script this is added to the build 57 | // to set shell options, in this case, to exit on error. 58 | const optionScript = "set -e" 59 | 60 | // traceScript is a helper script that is added to 61 | // the build script to trace a command. 62 | const traceScript = ` 63 | echo + %s 64 | %s 65 | ` 66 | -------------------------------------------------------------------------------- /shell/bash/bash_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package bash 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestCommands(t *testing.T) { 13 | cmd, args := Command() 14 | { 15 | got, want := cmd, "/bin/sh" 16 | if !reflect.DeepEqual(got, want) { 17 | t.Errorf("Want command %v, got %v", want, got) 18 | } 19 | } 20 | { 21 | got, want := args, []string{"-e"} 22 | if !reflect.DeepEqual(got, want) { 23 | t.Errorf("Want command %v, got %v", want, got) 24 | } 25 | } 26 | } 27 | 28 | func TestScript(t *testing.T) { 29 | got, want := Script([]string{"go build", "go test"}), exampleScript 30 | if got != want { 31 | t.Errorf("Want %q, got %q", want, got) 32 | } 33 | } 34 | 35 | func TestSilentScript(t *testing.T) { 36 | got, want := SilentScript([]string{"go build", "go test"}), exampleSilentScript 37 | if got != want { 38 | t.Errorf("Want %q, got %q", want, got) 39 | } 40 | } 41 | 42 | var exampleScript = ` 43 | set -e 44 | 45 | echo + "go build" 46 | go build 47 | 48 | echo + "go test" 49 | go test 50 | ` 51 | 52 | var exampleSilentScript = ` 53 | set -e 54 | 55 | go build 56 | 57 | go test 58 | ` 59 | -------------------------------------------------------------------------------- /shell/powershell/powershell.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package powershell provides functions for converting shell 6 | // commands to powershell scripts. 7 | package powershell 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "strings" 13 | ) 14 | 15 | // Suffix provides the shell script suffix. 16 | const Suffix = ".ps1" 17 | 18 | // Command returns the Powershell command and arguments. 19 | func Command() (string, []string) { 20 | return "powershell", []string{ 21 | "-noprofile", 22 | "-noninteractive", 23 | "-command", 24 | } 25 | } 26 | 27 | func Script(commands []string) string { 28 | return script(commands, true) 29 | } 30 | 31 | func SilentScript(commands []string) string { 32 | return script(commands, false) 33 | } 34 | 35 | // Script converts a slice of individual shell commands to 36 | // a powershell script. 37 | func script(commands []string, trace bool) string { 38 | buf := new(bytes.Buffer) 39 | fmt.Fprintln(buf) 40 | fmt.Fprintf(buf, optionScript) 41 | fmt.Fprintln(buf) 42 | for _, command := range commands { 43 | escaped := fmt.Sprintf("%q", "+ "+command) 44 | escaped = strings.Replace(escaped, "$", "`$", -1) 45 | var stringToWrite string 46 | if trace { 47 | stringToWrite = fmt.Sprintf( 48 | traceScript, 49 | escaped, 50 | command, 51 | ) 52 | } else { 53 | stringToWrite = "\n" + command + "\nif ($LastExitCode -gt 0) { exit $LastExitCode }\n" 54 | } 55 | buf.WriteString(stringToWrite) 56 | } 57 | return buf.String() 58 | } 59 | 60 | // optionScript is a helper script this is added to the build 61 | // to set shell options, in this case, to exit on error. 62 | const optionScript = `$erroractionpreference = "stop"` 63 | 64 | // traceScript is a helper script that is added to the build script to trace a command. 65 | const traceScript = ` 66 | echo %s 67 | %s 68 | if ($LastExitCode -gt 0) { exit $LastExitCode } 69 | ` 70 | -------------------------------------------------------------------------------- /shell/powershell/powershell_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package powershell 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestCommands(t *testing.T) { 13 | cmd, args := Command() 14 | { 15 | got, want := cmd, "powershell" 16 | if !reflect.DeepEqual(got, want) { 17 | t.Errorf("Want command %v, got %v", want, got) 18 | } 19 | } 20 | { 21 | got, want := args, []string{ 22 | "-noprofile", 23 | "-noninteractive", 24 | "-command", 25 | } 26 | if !reflect.DeepEqual(got, want) { 27 | t.Errorf("Want args %v, got %v", want, got) 28 | } 29 | } 30 | } 31 | 32 | func TestScript(t *testing.T) { 33 | got, want := Script([]string{"go build", "go test"}), exampleScript 34 | if got != want { 35 | t.Errorf("Want %q\ngot %q", want, got) 36 | } 37 | } 38 | 39 | func TestSilentScript(t *testing.T) { 40 | got, want := SilentScript([]string{"go build", "go test"}), exampleSilentScript 41 | if got != want { 42 | t.Errorf("Want \n%q\ngot\n%q", want, got) 43 | } 44 | } 45 | 46 | var exampleScript = ` 47 | $erroractionpreference = "stop" 48 | 49 | echo "+ go build" 50 | go build 51 | if ($LastExitCode -gt 0) { exit $LastExitCode } 52 | 53 | echo "+ go test" 54 | go test 55 | if ($LastExitCode -gt 0) { exit $LastExitCode } 56 | ` 57 | 58 | var exampleSilentScript = ` 59 | $erroractionpreference = "stop" 60 | 61 | go build 62 | if ($LastExitCode -gt 0) { exit $LastExitCode } 63 | 64 | go test 65 | if ($LastExitCode -gt 0) { exit $LastExitCode } 66 | ` 67 | -------------------------------------------------------------------------------- /shell/shell.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // +build !windows 6 | 7 | package shell 8 | 9 | import "github.com/drone/runner-go/shell/bash" 10 | 11 | // Suffix provides the shell script suffix. 12 | const Suffix = "" 13 | 14 | // Command returns the powershell command and arguments. 15 | func Command() (string, []string) { 16 | return bash.Command() 17 | } 18 | 19 | // Script converts a slice of individual shell commands to 20 | // a powershell script. 21 | func Script(commands []string) string { 22 | return bash.Script(commands) 23 | } 24 | -------------------------------------------------------------------------------- /shell/shell_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // +build !windows 6 | 7 | package shell 8 | 9 | import ( 10 | "reflect" 11 | "testing" 12 | ) 13 | 14 | func TestCommands(t *testing.T) { 15 | cmd, args := Command() 16 | { 17 | got, want := cmd, "/bin/sh" 18 | if !reflect.DeepEqual(got, want) { 19 | t.Errorf("Want command %v, got %v", want, got) 20 | } 21 | } 22 | { 23 | got, want := args, []string{"-e"} 24 | if !reflect.DeepEqual(got, want) { 25 | t.Errorf("Want command %v, got %v", want, got) 26 | } 27 | } 28 | } 29 | 30 | func TestScript(t *testing.T) { 31 | got, want := Script([]string{"go build", "go test"}), exampleScript 32 | if got != want { 33 | t.Errorf("Want %q, got %q", want, got) 34 | } 35 | } 36 | 37 | var exampleScript = ` 38 | set -e 39 | 40 | echo + "go build" 41 | go build 42 | 43 | echo + "go test" 44 | go test 45 | ` 46 | -------------------------------------------------------------------------------- /shell/shell_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // +build windows 6 | 7 | package shell 8 | 9 | import "github.com/drone/runner-go/shell/powershell" 10 | 11 | // Suffix provides the shell script suffix. 12 | const Suffix = ".ps1" 13 | 14 | // Command returns the powershell command and arguments. 15 | func Command() (string, []string) { 16 | return powershell.Command() 17 | } 18 | 19 | // Script converts a slice of individual shell commands to 20 | // a powershell script. 21 | func Script(commands []string) string { 22 | return powershell.Script(commands) 23 | } 24 | --------------------------------------------------------------------------------