initial commit

pull/1/head
Brad Rydzewski 5 years ago
commit 102fd9ab4f

@ -0,0 +1,9 @@
kind: pipeline
name: default
steps:
- name: test
image: golang:1.12
commands:
- go test ./...

1
.gitignore vendored

@ -0,0 +1 @@
NOTES*

@ -0,0 +1,18 @@
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.0.0] - 2019-07-01
### Added
- defined runner manifest schema
- defined runner remote protocol
- helpers for generating environment variables
- helpers for generating clone scripts
- helpers for generating shell scripts
- support for encrypted secrets
- support for static secrets
- support for remote secrets
- support for buffered log streaming
- handler to provide healtcheck support
- handler to provide runner dashboard

@ -0,0 +1,44 @@
The Parity Public License 6.0.0
Contributor: Drone.IO, Inc.
Source Code: github.com/drone/runner-go
This license lets you use and share this software for free, as
long as you contribute software you make with it. Specifically:
If you follow the rules below, you may do everything with this
software that would otherwise infringe either the contributor's
copyright in it, any patent claim the contributor can license,
or both.
1. Contribute changes and additions you make to this software.
2. If you combine this software with other software, contribute
that other software.
3. Contribute software you develop, deploy, monitor, or run with
this software.
4. Ensure everyone who gets a copy of this software from you, in
source code or any other form, gets the text of this license
and the contributor and source code lines above.
5. Do not make any legal claim against anyone accusing this
software, with or without changes, alone or with other
software, of infringing any patent claim.
To contribute software, publish all its source code, in the
preferred form for making changes, through a freely accessible
distribution system widely used for similar source code, and
license contributions not already licensed to the public on terms
as permissive as this license accordingly.
You are excused for unknowingly breaking 1, 2, or 3 if you
contribute as required, or stop doing anything requiring this
license, within 30 days of learning you broke the rule.
**As far as the law allows, this software comes as is, without
any warranty, and the contributor will not be liable to anyone
for any damages related to this software or this license, for any
kind of legal claim.**

@ -0,0 +1,4 @@
# runner-go
A collection of helper packages to extend Drone with customer runners.

@ -0,0 +1,91 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
// Package client provides a client for using the runner API.
package client
import (
"context"
"errors"
"github.com/drone/drone-go/drone"
)
// V1 is version 1 of the runner API
const V1 = "application/vnd.drone.runner.v1+json"
// ErrOptimisticLock is returned by if the struct being
// modified has a Version field and the value is not equal
// to the current value in the database
var ErrOptimisticLock = errors.New("Optimistic Lock Error")
type (
// Filter is used to filter the builds that are pulled
// from the queue.
Filter struct {
Kind string `json:"kind"`
Type string `json:"type"`
OS string `json:"os"`
Arch string `json:"arch"`
Variant string `json:"variant"`
Kernel string `json:"kernel"`
Labels map[string]string `json:"labels,omitempty"`
}
// File represents a file from the version control
// repository. It is used by the runner to provide the
// yaml configuration file to the runner.
File struct {
Data []byte
Hash []byte
}
// Context provides the runner with the build context and
// includes all environment data required to execute the
// build.
Context struct {
Build *drone.Build `json:"build"`
Config *File `json:"config"`
Netrc *drone.Netrc `json:"netrc"`
Repo *drone.Repo `json:"repository"`
Secrets []*drone.Secret `json:"secrets"`
System *drone.System `json:"system"`
}
)
// A Client manages communication with the runner.
type Client interface {
// Join notifies the server the runner is joining the cluster.
Join(ctx context.Context, machine string) error
// Leave notifies the server the runner is leaving the cluster.
Leave(ctx context.Context, machine string) error
// Ping sends a ping message to the server to test connectivity.
Ping(ctx context.Context, machine string) error
// Request requests the next available build stage for execution.
Request(ctx context.Context, args *Filter) (*drone.Stage, error)
// Accept accepts the build stage for execution.
Accept(ctx context.Context, stage *drone.Stage) error
// Detail gets the build stage details for execution.
Detail(ctx context.Context, stage *drone.Stage) (*Context, error)
// Update updates the build stage.
Update(ctxt context.Context, step *drone.Stage) error
// UpdateStep updates the build step.
UpdateStep(ctx context.Context, stage *drone.Step) error
// Watch watches for build cancellation requests.
Watch(ctx context.Context, stage int64) (bool, error)
// Batch batch writes logs to the build logs.
Batch(ctx context.Context, step int64, lines []*drone.Line) error
// Upload uploads the full logs to the server.
Upload(ctx context.Context, step int64, lines []*drone.Line) error
}

@ -0,0 +1,337 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package client
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/drone/drone-go/drone"
"github.com/drone/runner-go/logger"
)
const (
endpointNode = "/rpc/v2/nodes/%s"
endpointPing = "/rpc/v2/ping"
endpointStages = "/rpc/v2/stage"
endpointStage = "/rpc/v2/stage/%d"
endpointStep = "/rpc/v2/step/%d"
endpointWatch = "/rpc/v2/build/%d/watch"
endpointBatch = "/rpc/v2/step/%d/logs/batch"
endpointUpload = "/rpc/v2/step/%d/logs/upload"
)
var _ Client = (*HTTPClient)(nil)
// defaultClient is the default http.Client.
var defaultClient = &http.Client{
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},
}
// New returns a new runner client.
func New(endpoint, secret string, skipverify bool) *HTTPClient {
client := &HTTPClient{
Endpoint: endpoint,
Secret: secret,
SkipVerify: skipverify,
}
if skipverify {
client.Client = &http.Client{
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
}
return client
}
// An HTTPClient manages communication with the runner API.
type HTTPClient struct {
Client *http.Client
Logger logger.Logger
Dumper logger.Dumper
Endpoint string
Secret string
SkipVerify bool
}
// Join notifies the server the runner is joining the cluster.
func (p *HTTPClient) Join(ctx context.Context, machine string) error {
return nil
}
// Leave notifies the server the runner is leaving the cluster.
func (p *HTTPClient) Leave(ctx context.Context, machine string) error {
return nil
}
// Ping sends a ping message to the server to test connectivity.
func (p *HTTPClient) Ping(ctx context.Context, machine string) error {
_, err := p.do(ctx, endpointPing, "POST", nil, nil)
return err
}
// Request requests the next available build stage for execution.
func (p *HTTPClient) Request(ctx context.Context, args *Filter) (*drone.Stage, error) {
src := args
dst := new(drone.Stage)
_, err := p.retry(ctx, endpointStages, "POST", src, dst)
return dst, err
}
// Accept accepts the build stage for execution.
func (p *HTTPClient) Accept(ctx context.Context, stage *drone.Stage) error {
uri := fmt.Sprintf(endpointStage+"?machine=%s", stage.ID, stage.Machine)
src := stage
dst := new(drone.Stage)
_, err := p.retry(ctx, uri, "POST", nil, dst)
if dst != nil {
src.Updated = dst.Updated
src.Version = dst.Version
}
return err
}
// Detail gets the build stage details for execution.
func (p *HTTPClient) Detail(ctx context.Context, stage *drone.Stage) (*Context, error) {
uri := fmt.Sprintf(endpointStage, stage.ID)
dst := new(Context)
_, err := p.retry(ctx, uri, "GET", nil, dst)
return dst, err
}
// Update updates the build stage.
func (p *HTTPClient) Update(ctx context.Context, stage *drone.Stage) error {
uri := fmt.Sprintf(endpointStage, stage.ID)
src := stage
dst := new(drone.Stage)
for i, step := range src.Steps {
// a properly implemented runner should never encounter
// input errors. these checks are included to help
// developers creating new runners.
if step.Number == 0 {
return fmt.Errorf("step[%d] missing number", i)
}
if step.StageID == 0 {
return fmt.Errorf("step[%d] missing stage id", i)
}
if step.Status == drone.StatusRunning &&
step.Started == 0 {
return fmt.Errorf("step[%d] missing start time", i)
}
}
_, err := p.retry(ctx, uri, "PUT", src, dst)
if dst != nil {
src.Updated = dst.Updated
src.Version = dst.Version
set := map[int]*drone.Step{}
for _, step := range dst.Steps {
set[step.Number] = step
}
for _, step := range src.Steps {
from, ok := set[step.Number]
if ok {
step.ID = from.ID
step.StageID = from.StageID
step.Started = from.Started
step.Stopped = from.Stopped
step.Version = from.Version
}
}
}
return err
}
// UpdateStep updates the build step.
func (p *HTTPClient) UpdateStep(ctx context.Context, step *drone.Step) error {
uri := fmt.Sprintf(endpointStep, step.ID)
src := step
dst := new(drone.Step)
_, err := p.retry(ctx, uri, "PUT", src, dst)
if dst != nil {
src.Version = dst.Version
}
return err
}
// Watch watches for build cancellation requests.
func (p *HTTPClient) Watch(ctx context.Context, build int64) (bool, error) {
uri := fmt.Sprintf(endpointWatch, build)
res, err := p.retry(ctx, uri, "POST", nil, nil)
if err != nil {
return false, err
}
if res.StatusCode == 200 {
return true, nil
}
return false, nil
}
// Batch batch writes logs to the build logs.
func (p *HTTPClient) Batch(ctx context.Context, step int64, lines []*drone.Line) error {
uri := fmt.Sprintf(endpointBatch, step)
_, err := p.do(ctx, uri, "POST", &lines, nil)
return err
}
// Upload uploads the full logs to the server.
func (p *HTTPClient) Upload(ctx context.Context, step int64, lines []*drone.Line) error {
uri := fmt.Sprintf(endpointUpload, step)
_, err := p.retry(ctx, uri, "POST", &lines, nil)
return err
}
func (p *HTTPClient) retry(ctx context.Context, method, path string, in, out interface{}) (*http.Response, error) {
for {
res, err := p.do(ctx, method, path, in, out)
// do not retry on Canceled or DeadlineExceeded
if err := ctx.Err(); err != nil {
p.logger().Tracef("http: context canceled")
return res, err
}
// do not retry on optimisitic lock errors
if err == ErrOptimisticLock {
return res, err
}
if res != nil {
// Check the response code. We retry on 500-range
// responses to allow the server time to recover, as
// 500's are typically not permanent errors and may
// relate to outages on the server side.
if res.StatusCode > 501 {
p.logger().Tracef("http: server error: re-connect and re-try: %s", err)
time.Sleep(time.Second * 10)
continue
}
// We also retry on 204 no content response codes,
// used by the server when a long-polling request
// is intentionally disconnected and should be
// automatically reconnected.
if res.StatusCode == 204 {
p.logger().Tracef("http: no content returned: re-connect and re-try")
time.Sleep(time.Second * 10)
continue
}
} else if err != nil {
p.logger().Tracef("http: request error: %s", err)
time.Sleep(time.Second * 10)
continue
}
return res, err
}
}
// do is a helper function that posts a signed http request with
// the input encoded and response decoded from json.
func (p *HTTPClient) do(ctx context.Context, path, method string, in, out interface{}) (*http.Response, error) {
var buf bytes.Buffer
// marshal the input payload into json format and copy
// to an io.ReadCloser.
if in != nil {
json.NewEncoder(&buf).Encode(in)
}
endpoint := p.Endpoint + path
req, err := http.NewRequest(method, endpoint, &buf)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
// the request should include the secret shared between
// the agent and server for authorization.
req.Header.Add("X-Drone-Token", p.Secret)
if p.Dumper != nil {
p.Dumper.DumpRequest(req)
}
res, err := p.client().Do(req)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return res, err
}
if p.Dumper != nil {
p.Dumper.DumpResponse(res)
}
// if the response body return no content we exit
// immediately. We do not read or unmarshal the response
// and we do not return an error.
if res.StatusCode == 204 {
return res, nil
}
// Check the response for a 409 conflict. This indicates an
// optimistic lock error, in which case multiple clients may
// be attempting to update the same record. Convert this error
// code to a proper error.
if res.StatusCode == 409 {
return nil, ErrOptimisticLock
}
// else read the response body into a byte slice.
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return res, err
}
if res.StatusCode > 299 {
// if the response body includes an error message
// we should return the error string.
if len(body) != 0 {
return res, errors.New(
string(body),
)
}
// if the response body is empty we should return
// the default status code text.
return res, errors.New(
http.StatusText(res.StatusCode),
)
}
if out == nil {
return res, nil
}
return res, json.Unmarshal(body, out)
}
// client is a helper funciton that returns the default client
// if a custom client is not defined.
func (p *HTTPClient) client() *http.Client {
if p.Client == nil {
return defaultClient
}
return p.Client
}
// logger is a helper funciton that returns the default logger
// if a custom logger is not defined.
func (p *HTTPClient) logger() logger.Logger {
if p.Logger == nil {
return logger.Discard()
}
return p.Logger
}

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

@ -0,0 +1,75 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
// Package clone provides utilities for cloning commits.
package clone
import "fmt"
//
// IMPORTANT: DO NOT MODIFY THIS FILE
//
// this file must not be changed unless the changes have been
// discussed and approved by the project maintainers in the
// GitHub issue tracker.
//
// Args provide arguments to clone a repository.
type Args struct {
Branch string
Commit string
Ref string
Remote string
Depth int
Tags bool
NoFF bool
}
// Commands returns posix-compliant commands to clone a
// repository and checkout a commit.
func Commands(args Args) []string {
switch {
case isTag(args.Ref):
return tag(args)
case isPullRequest(args.Ref):
return pull(args)
default:
return branch(args)
}
}
// branch returns posix-compliant commands to clone a repository
// and checkout the named branch.
func branch(args Args) []string {
return []string{
"git init",
fmt.Sprintf("git remote add origin %s", args.Remote),
fmt.Sprintf("git fetch %s origin +refs/heads/%s:", fetchFlags(args), args.Branch),
fmt.Sprintf("git checkout %s -b %s", args.Commit, args.Branch),
}
}
// tag returns posix-compliant commands to clone a repository
// and checkout the tag by reference path.
func tag(args Args) []string {
return []string{
"git init",
fmt.Sprintf("git remote add origin %s", args.Remote),
fmt.Sprintf("git fetch %s origin +%s:", fetchFlags(args), args.Ref),
"git checkout -qf FETCH_HEAD",
}
}
// pull returns posix-compliant commands to clone a repository
// and checkout the pull request by reference path.
func pull(args Args) []string {
return []string{
"git init",
fmt.Sprintf("git remote add origin %s", args.Remote),
fmt.Sprintf("git fetch %s origin +refs/heads/%s:", fetchFlags(args), args.Branch),
fmt.Sprintf("git checkout %s", args.Branch),
fmt.Sprintf("git fetch origin %s:", args.Ref),
fmt.Sprintf("git merge %s %s", mergeFlags(args), args.Commit),
}
}

@ -0,0 +1,80 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package clone
import (
"testing"
"github.com/google/go-cmp/cmp"
)
func TestCommandsTag(t *testing.T) {
args := Args{
Depth: 50,
Remote: "https://github.com/octocat/hello-world.git",
Ref: "refs/tags/v1.0.0",
}
got := Commands(args)
want := []string{
"git init",
"git remote add origin https://github.com/octocat/hello-world.git",
"git fetch --depth=50 origin +refs/tags/v1.0.0:",
"git checkout -qf FETCH_HEAD",
}
if diff := cmp.Diff(got, want); diff != "" {
t.Fail()
t.Log(diff)
}
}
func TestCommandsBranch(t *testing.T) {
args := Args{
Branch: "develop",
Commit: "3650a5d21bbf086fa8d2f16b0067ddeecfa604df",
Depth: 50,
NoFF: true,
Remote: "https://github.com/octocat/hello-world.git",
Ref: "refs/heads/develop",
Tags: true,
}
got := Commands(args)
want := []string{
"git init",
"git remote add origin https://github.com/octocat/hello-world.git",
"git fetch --depth=50 --tags origin +refs/heads/develop:",
"git checkout 3650a5d21bbf086fa8d2f16b0067ddeecfa604df -b develop",
}
if diff := cmp.Diff(got, want); diff != "" {
t.Log(want)
t.Fail()
t.Log(diff)
}
}
func TestCommandsPullRequest(t *testing.T) {
args := Args{
Branch: "master",
Commit: "3650a5d21bbf086fa8d2f16b0067ddeecfa604df",
Depth: 50,
NoFF: true,
Remote: "https://github.com/octocat/hello-world.git",
Ref: "refs/pull/42/head",
Tags: true,
}
got := Commands(args)
want := []string{
"git init",
"git remote add origin https://github.com/octocat/hello-world.git",
"git fetch --depth=50 --tags origin +refs/heads/master:",
"git checkout master",
"git fetch origin refs/pull/42/head:",
"git merge --no-ff 3650a5d21bbf086fa8d2f16b0067ddeecfa604df",
}
if diff := cmp.Diff(got, want); diff != "" {
t.Log(want)
t.Fail()
t.Log(diff)
}
}

@ -0,0 +1,45 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package clone
// Config provides the Git Configuration parameters.
type Config struct {
User User
Trace bool
SkipVerify bool
}
// User provides the Git user parameters.
type User struct {
Name string
Email string
}
// Environ returns a set of global Git environment variables,
// from the configuration input.
func Environ(config Config) map[string]string {
environ := map[string]string{
"GIT_AUTHOR_NAME": "drone",
"GIT_AUTHOR_EMAIL": "noreply@drone",
"GIT_COMMITTER_NAME": "drone",
"GIT_COMMITTER_EMAIL": "noreply@drone",
"GIT_TERMINAL_PROMPT": "0",
}
if s := config.User.Name; s != "" {
environ["GIT_AUTHOR_NAME"] = s
environ["GIT_COMMITTER_NAME"] = s
}
if s := config.User.Email; s != "" {
environ["GIT_AUTHOR_EMAIL"] = s
environ["GIT_COMMITTER_EMAIL"] = s
}
if config.Trace {
environ["GIT_TRACE"] = "true"
}
if config.SkipVerify {
environ["GIT_SSL_NO_VERIFY"] = "true"
}
return environ
}

@ -0,0 +1,52 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package clone
import (
"testing"
"github.com/google/go-cmp/cmp"
)
func TestEnvironDefault(t *testing.T) {
c := Config{}
a := Environ(c)
b := map[string]string{
"GIT_AUTHOR_NAME": "drone",
"GIT_AUTHOR_EMAIL": "noreply@drone",
"GIT_COMMITTER_NAME": "drone",
"GIT_COMMITTER_EMAIL": "noreply@drone",
"GIT_TERMINAL_PROMPT": "0",
}
if diff := cmp.Diff(a, b); diff != "" {
t.Fail()
t.Log(diff)
}
}
func TestEnviron(t *testing.T) {
c := Config{
User: User{
Name: "The Octocat",
Email: "octocat@github.com",
},
Trace: true,
SkipVerify: true,
}
a := Environ(c)
b := map[string]string{
"GIT_AUTHOR_NAME": "The Octocat",
"GIT_AUTHOR_EMAIL": "octocat@github.com",
"GIT_COMMITTER_NAME": "The Octocat",
"GIT_COMMITTER_EMAIL": "octocat@github.com",
"GIT_TERMINAL_PROMPT": "0",
"GIT_TRACE": "true",
"GIT_SSL_NO_VERIFY": "true",
}
if diff := cmp.Diff(a, b); diff != "" {
t.Fail()
t.Log(diff)
}
}

@ -0,0 +1,48 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package clone
import (
"fmt"
"strings"
)
// fetchFlags is a helper function that returns a string of
// optional git-fetch command line flags.
func fetchFlags(args Args) string {
var flags []string
if depth := args.Depth; depth > 0 {
flag := fmt.Sprintf("--depth=%d", depth)
flags = append(flags, flag)
}
if args.Tags {
flags = append(flags, "--tags")
}
return strings.Join(flags, " ")
}
// mergeFlags is a helper function that returns a string of
// optional git-merge command line flags.
func mergeFlags(args Args) string {
var flags []string
if args.NoFF {
flags = append(flags, "--no-ff")
}
return strings.Join(flags, " ")
}
// isTag returns true if the reference path points to
// a tag object.
func isTag(ref string) bool {
return strings.HasPrefix(ref, "refs/tags/")
}
// isPullRequest returns true if the reference path points to
// a pull request object.
func isPullRequest(ref string) bool {
return strings.HasPrefix(ref, "refs/pull/") ||
strings.HasPrefix(ref, "refs/pull-requests/") ||
strings.HasPrefix(ref, "refs/merge-requests/")
}

@ -0,0 +1,86 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package clone
import "testing"
func TestFetchFlags(t *testing.T) {
var args Args
if got, want := fetchFlags(args), ""; got != want {
t.Errorf("Want %q, got %q", want, got)
}
args.Tags = true
if got, want := fetchFlags(args), "--tags"; got != want {
t.Errorf("Want %q, got %q", want, got)
}
args.Tags = false
args.Depth = 50
if got, want := fetchFlags(args), "--depth=50"; got != want {
t.Errorf("Want %q, got %q", want, got)
}
}
func TestMergeFlags(t *testing.T) {
var args Args
if got, want := mergeFlags(args), ""; got != want {
t.Errorf("Want %q, got %q", want, got)
}
args.NoFF = true
if got, want := mergeFlags(args), "--no-ff"; got != want {
t.Errorf("Want %q, got %q", want, got)
}
}
func TestIsTag(t *testing.T) {
tests := []struct {
s string
v bool
}{
{
s: "refs/heads/master",
v: false,
},
{
s: "refs/pull/1/head",
v: false,
},
{
s: "refs/tags/v1.0.0",
v: true,
},
}
for _, test := range tests {
if got, want := isTag(test.s), test.v; got != want {
t.Errorf("Want tag %v for %s", want, test.s)
}
}
}
func TestIsPullRequst(t *testing.T) {
tests := []struct {
s string
v bool
}{
{
s: "refs/heads/master",
v: false,
},
{
s: "refs/pull/1/head",
v: true,
},
{
s: "refs/pull/2/merge",
v: true,
},
}
for _, test := range tests {
if got, want := isPullRequest(test.s), test.v; got != want {
t.Errorf("Want pull request %v for %s", want, test.s)
}
}
}

@ -0,0 +1,243 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
// Package environ provides utilities for generating environment
// variables for a build pipeline.
package environ
import (
"fmt"
"regexp"
"sort"
"strings"
"time"
"github.com/drone/drone-go/drone"
)
// regular expression to extract the pull request number
// from the git ref (e.g. refs/pulls/{d}/head)
var re = regexp.MustCompile("\\d+")
// System returns a set of environment variables containing
// system metadata.
func System(system *drone.System) map[string]string {
return map[string]string{
"CI": "true",
"DRONE": "true",
"DRONE_SYSTEM_PROTO": system.Proto,
"DRONE_SYSTEM_HOST": system.Host,
"DRONE_SYSTEM_HOSTNAME": system.Host,
"DRONE_SYSTEM_VERSION": fmt.Sprint(system.Version),
}
}
// Repo returns a set of environment variables containing
// repostiory metadata.
func Repo(repo *drone.Repo) map[string]string {
return map[string]string{
"DRONE_REPO": repo.Slug,
"DRONE_REPO_SCM": repo.SCM,
"DRONE_REPO_OWNER": repo.Namespace,
"DRONE_REPO_NAMESPACE": repo.Namespace,
"DRONE_REPO_NAME": repo.Name,
"DRONE_REPO_LINK": repo.Link,
"DRONE_REPO_BRANCH": repo.Branch,
"DRONE_REMOTE_URL": repo.HTTPURL,
"DRONE_GIT_HTTP_URL": repo.HTTPURL,
"DRONE_GIT_SSH_URL": repo.SSHURL,
"DRONE_REPO_VISIBILITY": repo.Visibility,
"DRONE_REPO_PRIVATE": fmt.Sprint(repo.Private),
}
}
// Stage returns a set of environment variables containing
// stage metadata.
func Stage(stage *drone.Stage) map[string]string {
env := map[string]string{
"DRONE_STAGE_KIND": stage.Kind,
"DRONE_STAGE_TYPE": stage.Type,
"DRONE_STAGE_NAME": stage.Name,
"DRONE_STAGE_NUMBER": fmt.Sprint(stage.Number),
"DRONE_STAGE_MACHINE": stage.Machine,
"DRONE_STAGE_OS": stage.OS,
"DRONE_STAGE_ARCH": stage.Arch,
"DRONE_STAGE_VARIANT": stage.Variant,
"DRONE_STAGE_STATUS": "success",
"DRONE_STAGE_STARTED": fmt.Sprint(stage.Started),
"DRONE_STAGE_FINISHED": fmt.Sprint(stage.Stopped),
"DRONE_STAGE_DEPENDS_ON": strings.Join(stage.DependsOn, ","),
}
if isStageFailing(stage) {
env["DRONE_STAGE_STATUS"] = "failure"
env["DRONE_FAILED_STEPS"] = strings.Join(failedSteps(stage), ",")
}
if stage.Started == 0 {
env["DRONE_STAGE_STARTED"] = fmt.Sprint(time.Now().Unix())
}
if stage.Stopped == 0 {
env["DRONE_STAGE_FINISHED"] = fmt.Sprint(time.Now().Unix())
}
return env
}
// Step returns a set of environment variables containing the
// step metadata.
func Step(step *drone.Step) map[string]string {
return map[string]string{
"DRONE_STEP_NAME": step.Name,
"DRONE_STEP_NUMBER": fmt.Sprint(step.Number),
}
}
// Build returns a set of environment variables containing
// build metadata.
func Build(build *drone.Build) map[string]string {
env := map[string]string{
"DRONE_BRANCH": build.Target,
"DRONE_SOURCE_BRANCH": build.Source,
"DRONE_TARGET_BRANCH": build.Target,
"DRONE_COMMIT": build.After,
"DRONE_COMMIT_SHA": build.After,
"DRONE_COMMIT_BEFORE": build.Before,
"DRONE_COMMIT_AFTER": build.After,
"DRONE_COMMIT_REF": build.Ref,
"DRONE_COMMIT_BRANCH": build.Target,
"DRONE_COMMIT_LINK": build.Link,
"DRONE_COMMIT_MESSAGE": build.Message,
"DRONE_COMMIT_AUTHOR": build.Author,
"DRONE_COMMIT_AUTHOR_EMAIL": build.AuthorEmail,
"DRONE_COMMIT_AUTHOR_AVATAR": build.AuthorAvatar,
"DRONE_COMMIT_AUTHOR_NAME": build.AuthorName,
"DRONE_BUILD_NUMBER": fmt.Sprint(build.Number),
"DRONE_BUILD_PARENT": fmt.Sprint(build.Parent),
"DRONE_BUILD_EVENT": build.Event,
"DRONE_BUILD_ACTION": build.Action,
"DRONE_BUILD_STATUS": "success",
"DRONE_BUILD_CREATED": fmt.Sprint(build.Created),
"DRONE_BUILD_STARTED": fmt.Sprint(build.Started),
"DRONE_BUILD_FINISHED": fmt.Sprint(build.Finished),
"DRONE_DEPLOY_TO": build.Deploy,
}
if isBuildFailing(build) {
env["DRONE_BUILD_STATUS"] = "failure"
env["DRONE_FAILED_STAGES"] = strings.Join(failedStages(build), ",")
}
if build.Started == 0 {
env["DRONE_BUILD_STARTED"] = fmt.Sprint(time.Now().Unix())
}
if build.Finished == 0 {
env["DRONE_BUILD_FINISHED"] = fmt.Sprint(time.Now().Unix())
}
if strings.HasPrefix(build.Ref, "refs/tags/") {
env["DRONE_TAG"] = strings.TrimPrefix(build.Ref, "refs/tags/")
}
if build.Event == drone.EventPullRequest {
env["DRONE_PULL_REQUEST"] = re.FindString(build.Ref)
}
return env
}
// Link returns a set of environment variables containing
// resource urls to the build.
func Link(repo *drone.Repo, build *drone.Build, system *drone.System) map[string]string {
return map[string]string{
"DRONE_BUILD_LINK": fmt.Sprintf(
"%s://%s/%s/%d",
system.Proto,
system.Host,
repo.Slug,
build.Number,
),
}
}
// Combine is a helper function combines one or more maps of
// environment variables into a single map.
func Combine(env ...map[string]string) map[string]string {
c := map[string]string{}
for _, e := range env {
for k, v := range e {
c[k] = v
}
}
return c
}
// Slice is a helper function that converts a map of environment
// variables to a slice of string values in key=value format.
func Slice(env map[string]string) []string {
s := []string{}
for k, v := range env {
s = append(s, k+"="+v)
}
sort.Strings(s)
return s
}
// helper function returns true of the build is failing.
func isBuildFailing(build *drone.Build) bool {
if build.Status == drone.StatusError ||
build.Status == drone.StatusFailing ||
build.Status == drone.StatusKilled {
return true
}
for _, stage := range build.Stages {
if stage.Status == drone.StatusError ||
stage.Status == drone.StatusFailing ||
stage.Status == drone.StatusKilled {
return true
}
}
return false
}
// helper function returns true of the stage is failing.
func isStageFailing(stage *drone.Stage) bool {
if stage.Status == drone.StatusError ||
stage.Status == drone.StatusFailing ||
stage.Status == drone.StatusKilled {
return true
}
for _, step := range stage.Steps {
if step.ErrIgnore && step.Status == drone.StatusFailing {
continue
}
if step.Status == drone.StatusError ||
step.Status == drone.StatusFailing ||
step.Status == drone.StatusKilled {
return true
}
}
return false
}
// helper function returns the failed steps.
func failedSteps(stage *drone.Stage) []string {
var steps []string
for _, step := range stage.Steps {
if step.ErrIgnore && step.Status == drone.StatusFailing {
continue
}
if step.Status == drone.StatusError ||
step.Status == drone.StatusFailing ||
step.Status == drone.StatusKilled {
steps = append(steps, step.Name)
}
}
return steps
}
// helper function returns the failed stages.
func failedStages(build *drone.Build) []string {
var stages []string
for _, stage := range build.Stages {
if stage.Status == drone.StatusError ||
stage.Status == drone.StatusFailing ||
stage.Status == drone.StatusKilled {
stages = append(stages, stage.Name)
}
}
return stages
}

@ -0,0 +1,448 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package environ
import (
"testing"
"github.com/drone/drone-go/drone"
"github.com/google/go-cmp/cmp"
)
func TestRepo(t *testing.T) {
v := &drone.Repo{
ID: 1,
UID: "2",
UserID: 3,
Namespace: "octocat",
Name: "hello-world",
Slug: "octocat/hello-world",
SCM: "git",
HTTPURL: "https://github.com/octocat/hello-world.git",
SSHURL: "git@github.com:octocat/hello-world",
Link: "https://github.com/octocat/hello-world",
Branch: "master",
Private: true,
Visibility: "internal",
}
a := Repo(v)
b := map[string]string{
"DRONE_REPO": "octocat/hello-world",
"DRONE_REPO_SCM": "git",
"DRONE_REPO_OWNER": "octocat",
"DRONE_REPO_NAMESPACE": "octocat",
"DRONE_REPO_NAME": "hello-world",
"DRONE_REPO_LINK": "https://github.com/octocat/hello-world",
"DRONE_REPO_BRANCH": "master",
"DRONE_REMOTE_URL": "https://github.com/octocat/hello-world.git",
"DRONE_GIT_HTTP_URL": "https://github.com/octocat/hello-world.git",
"DRONE_GIT_SSH_URL": "git@github.com:octocat/hello-world",
"DRONE_REPO_VISIBILITY": "internal",
"DRONE_REPO_PRIVATE": "true",
}
if diff := cmp.Diff(a, b); diff != "" {
t.Fail()
t.Log(diff)
}
}
func TestBuild(t *testing.T) {
v := &drone.Build{
Source: "develop",
Target: "master",
After: "762941318ee16e59dabbacb1b4049eec22f0d303",
Before: "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d",
Ref: "refs/pull/32/head",
Link: "https://github.com/octocat/Hello-World/commit/762941318ee16e59dabbacb1b4049eec22f0d303",
Message: "updated README",
Author: "octocat",
AuthorAvatar: "https://avatars0.githubusercontent.com/u/583231",
AuthorEmail: "octocat@github.com",
AuthorName: "The Octocat",
Number: 1,
Parent: 2,
Event: "pull_request",
Action: "opened",
Deploy: "prod",
Status: drone.StatusFailing,
Created: 1561421740,
Started: 1561421746,
Finished: 1561421753,
Stages: []*drone.Stage{
{
Name: "backend",
Number: 1,
Status: drone.StatusPassing,
},
{
Name: "frontend",
Number: 2,
Status: drone.StatusFailing,
},
},
}
a := Build(v)
b := map[string]string{
"DRONE_BRANCH": "master",
"DRONE_BUILD_NUMBER": "1",
"DRONE_BUILD_PARENT": "2",
"DRONE_BUILD_STATUS": "failure",
"DRONE_BUILD_EVENT": "pull_request",
"DRONE_BUILD_ACTION": "opened",
"DRONE_BUILD_CREATED": "1561421740",
"DRONE_BUILD_STARTED": "1561421746",
"DRONE_BUILD_FINISHED": "1561421753",
"DRONE_COMMIT": "762941318ee16e59dabbacb1b4049eec22f0d303",
"DRONE_COMMIT_BEFORE": "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d",
"DRONE_COMMIT_AFTER": "762941318ee16e59dabbacb1b4049eec22f0d303",
"DRONE_COMMIT_BRANCH": "master",
"DRONE_COMMIT_LINK": "https://github.com/octocat/Hello-World/commit/762941318ee16e59dabbacb1b4049eec22f0d303",
"DRONE_COMMIT_MESSAGE": "updated README",
"DRONE_COMMIT_REF": "refs/pull/32/head",
"DRONE_COMMIT_AUTHOR": "octocat",
"DRONE_COMMIT_AUTHOR_AVATAR": "https://avatars0.githubusercontent.com/u/583231",
"DRONE_COMMIT_AUTHOR_EMAIL": "octocat@github.com",
"DRONE_COMMIT_AUTHOR_NAME": "The Octocat",
"DRONE_COMMIT_SHA": "762941318ee16e59dabbacb1b4049eec22f0d303",
"DRONE_DEPLOY_TO": "prod",
"DRONE_FAILED_STAGES": "frontend",
"DRONE_PULL_REQUEST": "32",
"DRONE_SOURCE_BRANCH": "develop",
"DRONE_TARGET_BRANCH": "master",
}
if diff := cmp.Diff(a, b); diff != "" {
t.Fail()
t.Log(diff)
}
v.Started = 0
v.Finished = 0
a = Build(v)
if a["DRONE_BUILD_STARTED"] == "0" {
t.Errorf("Expect non-zero started time")
}
if a["DRONE_BUILD_FINISHED"] == "0" {
t.Errorf("Expect non-zero stopped time")
}
v.Ref = "refs/tags/v1.0.0"
a = Build(v)
if a["DRONE_TAG"] != "v1.0.0" {
t.Errorf("Expect tag extraced from ref")
}
}
func TestSystem(t *testing.T) {
v := &drone.System{
Proto: "http",
Host: "drone.company.com",
Link: "http://drone.company.com",
Version: "v1.0.0",
}
a := System(v)
b := map[string]string{
"CI": "true",
"DRONE": "true",
"DRONE_SYSTEM_HOST": "drone.company.com",
"DRONE_SYSTEM_HOSTNAME": "drone.company.com",
"DRONE_SYSTEM_PROTO": "http",
"DRONE_SYSTEM_VERSION": "v1.0.0",
}
if diff := cmp.Diff(a, b); diff != "" {
t.Fail()
t.Log(diff)
}
}
func TestStep(t *testing.T) {
v := &drone.Step{
Name: "clone",
Number: 1,
}
a := Step(v)
b := map[string]string{
"DRONE_STEP_NAME": "clone",
"DRONE_STEP_NUMBER": "1",
}
if diff := cmp.Diff(a, b); diff != "" {
t.Fail()
t.Log(diff)
}
}
func TestStage(t *testing.T) {
v := &drone.Stage{
Kind: "pipeline",
Type: "docker",
Name: "deploy",
Number: 1,
Machine: "laptop",
OS: "linux",
Arch: "arm64",
Variant: "7",
Status: drone.StatusFailing,
Started: 1561421746,
Stopped: 1561421753,
DependsOn: []string{"backend", "frontend"},
Steps: []*drone.Step{
{
Name: "clone",
Number: 1,
Status: drone.StatusPassing,
},
{
Name: "test",
Number: 2,
Status: drone.StatusFailing,
},
},
}
a := Stage(v)
b := map[string]string{
"DRONE_STAGE_KIND": "pipeline",
"DRONE_STAGE_TYPE": "docker",
"DRONE_STAGE_NAME": "deploy",
"DRONE_STAGE_NUMBER": "1",
"DRONE_STAGE_MACHINE": "laptop",
"DRONE_STAGE_OS": "linux",
"DRONE_STAGE_ARCH": "arm64",
"DRONE_STAGE_VARIANT": "7",
"DRONE_STAGE_STATUS": "failure",
"DRONE_STAGE_STARTED": "1561421746",
"DRONE_STAGE_FINISHED": "1561421753",
"DRONE_STAGE_DEPENDS_ON": "backend,frontend",
"DRONE_FAILED_STEPS": "test",
}
if diff := cmp.Diff(a, b); diff != "" {
t.Fail()
t.Log(diff)
}
v.Started = 0
v.Stopped = 0
a = Stage(v)
if a["DRONE_STAGE_STARTED"] == "0" {
t.Errorf("Expect non-zero started time")
}
if a["DRONE_STAGE_FINISHED"] == "0" {
t.Errorf("Expect non-zero stopped time")
}
}
func TestLink(t *testing.T) {
sys := &drone.System{
Proto: "http",
Host: "drone.company.com",
}
build := &drone.Build{Number: 42}
repo := &drone.Repo{Slug: "octocat/hello-world"}
a := Link(repo, build, sys)
b := map[string]string{
"DRONE_BUILD_LINK": "http://drone.company.com/octocat/hello-world/42",
}
if diff := cmp.Diff(a, b); diff != "" {
t.Fail()
t.Log(diff)
}
}
func TestSlice(t *testing.T) {
v := map[string]string{
"CI": "true",
"DRONE": "true",
}
a := Slice(v)
b := []string{"CI=true", "DRONE=true"}
if diff := cmp.Diff(a, b); diff != "" {
t.Fail()
t.Log(diff)
}
}
func TestCombine(t *testing.T) {
v1 := map[string]string{
"CI": "true",
"DRONE": "true",
}
v2 := map[string]string{
"CI": "false",
"DRONE_SYSTEM_HOST": "drone.company.com",
"DRONE_SYSTEM_HOSTNAME": "drone.company.com",
"DRONE_SYSTEM_PROTO": "http",
"DRONE_SYSTEM_VERSION": "v1.0.0",
}
a := Combine(v1, v2)
b := map[string]string{
"CI": "false",
"DRONE": "true",
"DRONE_SYSTEM_HOST": "drone.company.com",
"DRONE_SYSTEM_HOSTNAME": "drone.company.com",
"DRONE_SYSTEM_PROTO": "http",
"DRONE_SYSTEM_VERSION": "v1.0.0",
}
if diff := cmp.Diff(a, b); diff != "" {
t.Fail()
t.Log(diff)
}
}
func Test_isStageFailing(t *testing.T) {
s := &drone.Stage{
Status: drone.StatusPassing,
Steps: []*drone.Step{
{
Status: drone.StatusPassing,
},
{
ErrIgnore: true,
Status: drone.StatusFailing,
},
},
}
if isStageFailing(s) {
t.Errorf("Expect stage not failing if ErrIgnore")
}
s = &drone.Stage{
Status: drone.StatusFailing,
}
if isStageFailing(s) == false {
t.Errorf("Expect stage failing")
}
s = &drone.Stage{
Status: drone.StatusRunning,
Steps: []*drone.Step{
{
Status: drone.StatusPassing,
},
{
ErrIgnore: false,
Status: drone.StatusFailing,
},
},
}
if isStageFailing(s) == false {
t.Errorf("Expect stage failing if step failing")
}
}
func Test_isBuildFailing(t *testing.T) {
v := &drone.Build{
Status: drone.StatusPassing,
}
if isBuildFailing(v) == true {
t.Errorf("Expect build passing")
}
v.Status = drone.StatusFailing
if isBuildFailing(v) == false {
t.Errorf("Expect build failing")
}
v.Status = drone.StatusRunning
v.Stages = []*drone.Stage{
{Status: drone.StatusPassing},
{Status: drone.StatusFailing},
}
if isBuildFailing(v) == false {
t.Errorf("Expect build failing if stage failing")
}
v.Stages = []*drone.Stage{
{Status: drone.StatusPassing},
{Status: drone.StatusRunning},
{Status: drone.StatusPending},
}
if isBuildFailing(v) == true {
t.Errorf("Expect build passing if all stages passing")
}
}
func Test_failedSteps(t *testing.T) {
s := &drone.Stage{
Status: drone.StatusRunning,
Steps: []*drone.Step{
{
Name: "clone",
Status: drone.StatusPassing,
},
{
Name: "test",
Status: drone.StatusFailing,
},
{
Name: "integration",
Status: drone.StatusFailing,
},
{
Name: "experimental",
ErrIgnore: true,
Status: drone.StatusFailing,
},
},
}
a, b := failedSteps(s), []string{"test", "integration"}
if diff := cmp.Diff(a, b); diff != "" {
t.Fail()
t.Log(diff)
}
}
func Test_failedStages(t *testing.T) {
v := &drone.Build{
Status: drone.StatusRunning,
Stages: []*drone.Stage{
{
Name: "step_blocked",
Status: drone.StatusBlocked,
},
{
Name: "step_declined",
Status: drone.StatusDeclined,
},
{
Name: "step_error",
Status: drone.StatusError,
},
{
Name: "step_failing",
Status: drone.StatusFailing,
},
{
Name: "step_killed",
Status: drone.StatusKilled,
},
{
Name: "step_passing",
Status: drone.StatusPassing,
},
{
Name: "step_pending",
Status: drone.StatusPending,
},
{
Name: "step_running",
Status: drone.StatusRunning,
},
{
Name: "step_skipped",
Status: drone.StatusSkipped,
},
{
Name: "step_waiting",
Status: drone.StatusWaiting,
},
},
}
a, b := failedStages(v), []string{"step_error", "step_failing", "step_killed"}
if diff := cmp.Diff(a, b); diff != "" {
t.Fail()
t.Log(diff)
}
}

@ -0,0 +1,23 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package environ
import "os"
// function used to expand environment variables.
var getenv = os.Getenv
// Expand is a helper function to expand the PATH parameter in
// the pipeline environment.
func Expand(env map[string]string) map[string]string {
c := map[string]string{}
for k, v := range env {
c[k] = v
}
if path := c["PATH"]; path != "" {
c["PATH"] = os.Expand(path, getenv)
}
return c
}

@ -0,0 +1,39 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package environ
import (
"os"
"testing"
)
func TestExpand(t *testing.T) {
defer func() {
getenv = os.Getenv
}()
getenv = func(string) string {
return "/bin:/usr/local/bin"
}
before := map[string]string{
"USER": "root",
"HOME": "/home/$USER", // does not expect
"PATH": "/go/bin:$PATH",
}
after := Expand(before)
if got, want := after["PATH"], "/go/bin:/bin:/usr/local/bin"; got != want {
t.Errorf("Got PATH %q, want %q", got, want)
}
if got, want := after["USER"], "root"; got != want {
t.Errorf("Got USER %q, want %q", got, want)
}
// only the PATH variable should expand. No other variables
// should be expanded.
if got, want := after["HOME"], "/home/$USER"; got != want {
t.Errorf("Got HOME %q, want %q", got, want)
}
}

@ -0,0 +1,17 @@
module github.com/drone/runner-go
go 1.12
require (
github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d
github.com/bmatcuk/doublestar v1.1.1
github.com/buildkite/yaml v2.1.0+incompatible
github.com/docker/go-units v0.4.0
github.com/drone/drone-go v1.0.5-0.20190504210458-4d6116b897ba
github.com/drone/signal v1.0.0
github.com/google/go-cmp v0.3.0
github.com/sirupsen/logrus v1.4.2
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4
golang.org/x/sync v0.0.0-20190423024810-112230192c58
gopkg.in/yaml.v2 v2.2.2
)

@ -0,0 +1,42 @@
github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d h1:j6oB/WPCigdOkxtuPl1VSIiLpy7Mdsu6phQffbF19Ng=
github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs=
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e h1:rl2Aq4ZODqTDkeSqQBy+fzpZPamacO1Srp8zq7jf2Sc=
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY=
github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/buildkite/yaml v2.1.0+incompatible h1:xirI+ql5GzfikVNDmt+yeiXpf/v1Gt03qXTtT5WXdr8=
github.com/buildkite/yaml v2.1.0+incompatible/go.mod h1:UoU8vbcwu1+vjZq01+KrpSeLBgQQIjL/H7Y6KwikUrI=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/drone/drone-go v1.0.4 h1:Yom1lix1Lmk3KmKIsBSQJF1bw0YR2lDGaFQrXxqHMko=
github.com/drone/drone-go v1.0.4/go.mod h1:GxyeGClYohaKNYJv/ZpsmVHtMJ7WhoT+uDaJNcDIrk4=
github.com/drone/drone-go v1.0.5-0.20190504210458-4d6116b897ba h1:GKiT4UPBligLXJAP1zRllHvTUygAAlgS3t9LM9aasp0=
github.com/drone/drone-go v1.0.5-0.20190504210458-4d6116b897ba/go.mod h1:GxyeGClYohaKNYJv/ZpsmVHtMJ7WhoT+uDaJNcDIrk4=
github.com/drone/signal v1.0.0 h1:NrnM2M/4yAuU/tXs6RP1a1ZfxnaHwYkd0kJurA1p6uI=
github.com/drone/signal v1.0.0/go.mod h1:S8t92eFT0g4WUgEc/LxG+LCuiskpMNsG0ajAMGnyZpc=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

@ -0,0 +1,70 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
// Package handler provides HTTP handlers that expose pipeline
// state and status.
package handler
import (
"net/http"
"sort"
"github.com/drone/drone-go/drone"
"github.com/drone/runner-go/pipeline/history"
)
// HandleHealth returns a http.HandlerFunc that returns a 200
// if the service is healthly.
func HandleHealth(t *history.History) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// TODO(bradrydzewski) iterate through the list of
// pending or running stages and write an error message
// if the timeout is exceeded.
nocache(w)
w.WriteHeader(200)
}
}
// HandleIndex returns a http.HandlerFunc that displays a list
// of currently and previously executed builds.
func HandleIndex(t *history.History) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
d := t.Entries()
s1 := history.ByTimestamp(d)
s2 := history.ByStatus(d)
sort.Sort(s1)
sort.Sort(s2)
if r.Header.Get("Accept") == "application/json" {
nocache(w)
renderJSON(w, d)
} else {
nocache(w)
render(w, "index.tmpl", &data{Items: d})
}
}
}
// data is a template data structure that provides helper
// functions for calculating the system state.
type data struct {
Items []*history.Entry
}
// helper function returns true if the history is empty.
func (d *data) Empty() bool {
return len(d.Items) == 0
}
// helper function returns true if no running builds exists.
func (d *data) Idle() bool {
for _, item := range d.Items {
switch item.Stage.Status {
case drone.StatusPending, drone.StatusRunning:
return false
}
}
return true
}

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

@ -0,0 +1,28 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package handler
import (
"net/http"
"time"
)
// unix epoch time
var epoch = time.Unix(0, 0).Format(time.RFC1123)
// http headers to disable caching.
var noCacheHeaders = map[string]string{
"Expires": epoch,
"Cache-Control": "no-cache, private, max-age=0",
"Pragma": "no-cache",
"X-Accel-Expires": "0",
}
// helper function to prevent http response caching.
func nocache(w http.ResponseWriter) {
for k, v := range noCacheHeaders {
w.Header().Set(k, v)
}
}

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

@ -0,0 +1,30 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package handler
import (
"encoding/json"
"net/http"
"github.com/drone/runner-go/handler/template"
)
// renderJSON writes the json-encoded representation of v to
// the response body.
func renderJSON(w http.ResponseWriter, v interface{}) {
for k, v := range noCacheHeaders {
w.Header().Set(k, v)
}
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
enc.Encode(v)
}
// render writes the template to the response body.
func render(w http.ResponseWriter, t string, v interface{}) {
w.Header().Set("Content-Type", "text/html")
template.T.ExecuteTemplate(w, t, v)
}

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

@ -0,0 +1,39 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package router
import (
"net/http"
"github.com/drone/runner-go/handler"
"github.com/drone/runner-go/handler/static"
"github.com/drone/runner-go/pipeline/history"
"github.com/99designs/basicauth-go"
)
// Config provides router configuration.
type Config struct {
Username string
Password string
Realm string
}
// New returns a new route handler.
func New(tracer *history.History, config Config) http.Handler {
// middleware to require basic authentication.
auth := basicauth.New(config.Realm, map[string][]string{
config.Username: {config.Password},
})
// handler to serve static assets for the dashboard.
fs := http.FileServer(static.New())
mux := http.NewServeMux()
mux.Handle("/static/", http.StripPrefix("/static/", fs))
mux.HandleFunc("/healthz", handler.HandleHealth(tracer))
mux.Handle("/", auth(handler.HandleIndex(tracer)))
return mux
}

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

@ -0,0 +1,9 @@
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" xmlns:serif="http://www.serif.com/" fill-rule="evenodd"
clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414">
<path fill="none" d="M0 0h20v20H0z"/>
<g fill="#FFFFFF" fill-rule="nonzero">
<path d="M5.434 6.566a.802.802 0 0 1 1.132-1.132l7.778 7.778a.802.802 0 0 1-1.132 1.132L5.434 6.566z"/>
<path serif:id="Line-13"
d="M13.212 5.434a.802.802 0 0 1 1.132 1.132l-7.778 7.778a.802.802 0 0 1-1.132-1.132l7.778-7.778z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 551 B

@ -0,0 +1,7 @@
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd"
stroke-linejoin="round" stroke-miterlimit="1.414">
<path fill="none" d="M0 0h20v20H0z"/>
<path
d="M7.684 5.655v1.142c.001-.021.004-.018.013-.002.063.105.17.253.318.437.323.399.821.94 1.489 1.617a.92.92 0 0 1 .267.632l.018.975a.928.928 0 0 1-.287.684c-.67.639-1.167 1.156-1.489 1.544a3.55 3.55 0 0 0-.311.424c-.015.026-.017.029-.018-.009v1.246h4.632v-1.246c-.001.038-.003.035-.018.009a3.55 3.55 0 0 0-.311-.424c-.322-.388-.819-.905-1.489-1.544a.928.928 0 0 1-.287-.684l.018-.975a.92.92 0 0 1 .267-.632c.668-.677 1.166-1.218 1.489-1.617.148-.184.255-.332.318-.437.009-.016.012-.019.013.002V5.655H7.684zM6 6.884V5.138C6 4.509 6.518 4 7.158 4h5.684C13.482 4 14 4.509 14 5.138v1.746c0 .615-.616 1.401-2.092 2.911l-.007.378C13.357 11.58 14 12.389 14 13.048v1.814c0 .629-.518 1.138-1.158 1.138H7.158C6.518 16 6 15.491 6 14.862v-1.814c0-.659.643-1.468 2.099-2.875l-.007-.378C6.616 8.285 6 7.499 6 6.884z"
fill="#FFFFFF" fill-rule="nonzero"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1,8 @@
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<circle cx="10" cy="10" r="5"/>
<path
d="M5.974 2.431c.481-.257.99-.469 1.521-.63L8.095 0h3.81l.6 1.8c.53.162 1.04.374 1.521.631l1.698-.849 2.694 2.694-.85 1.698c.258.481.47.99.632 1.521l1.8.6v3.81l-1.8.6a8.518 8.518 0 0 1-.631 1.521l.849 1.698-2.694 2.694-1.698-.85c-.481.258-.99.47-1.521.632l-.6 1.8h-3.81l-.6-1.8a8.518 8.518 0 0 1-1.521-.631l-1.698.849-2.694-2.694.85-1.698a8.518 8.518 0 0 1-.632-1.521l-1.8-.6v-3.81l1.8-.6c.162-.53.374-1.04.631-1.521l-.849-1.698 2.694-2.694 1.698.85zM10 14a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"
fill="#ffbe00"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 690 B

@ -0,0 +1,8 @@
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd" transform="translate(3,3)">
<g fill="#FFFFFF" fill-rule="nonzero">
<path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14zm0-1.463A5.537 5.537 0 1 0 7 1.463a5.537 5.537 0 0 0 0 11.074z"/>
<path d="M2.25 3.456A.853.853 0 1 1 3.456 2.25l8.294 8.294a.853.853 0 0 1-1.206 1.206L2.25 3.456z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 430 B

@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" class="icon-drone-sleep" width="80px" height="80px">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="12.604" y1="69.845" x2="69.397" y2="13.052"
gradientTransform="matrix(1 0 0 -1 0 82.2)">
<stop offset="0" stop-color="#b4d3fd"/>
<stop offset="1" stop-color="#84b0f3"/>
</linearGradient>
<path
d="M80.2 39.9v.8c0 1.4-.1 2.9-.3 4.1-.4 3.4-1.2 6.6-2.4 9.7C71.7 69.4 57.2 80 40.2 80 22.8 80 8.1 69 2.6 53.6c-1-2.7-1.6-5.7-2.1-8.7-.1-1.1-.2-2.3-.3-3.5V40c0-2.1.1-4.2.4-6.2C3.6 14.7 20.2 0 40.2 0s36.6 14.6 39.5 33.8c.2 1.9.5 4 .5 6.1z"
fill="url(#SVGID_1_)"/>
<path fill="#fff"
d="M80.2 40.7c0 1.4-.1 2.9-.3 4.1-.4 3.4-1.2 6.6-2.4 9.7-3.3 2.1-7.3 4-10.9 5.3-8.6 3-17.5 4.2-26.3 4.2-8.9-.1-17.8-1.5-26.3-4.7-4-1.4-7.7-3.4-11.4-5.7h-.1c-1-2.7-1.6-5.7-2.1-8.7-.1-1.1-.2-2.2-.2-3.4v-1.2C5 44.6 11 47.8 17.4 49.9c7.3 2.4 15.2 3.6 23 3.6 7.8.1 15.7-.9 23-3.3 6.2-1.9 12.5-5.2 16.8-9.5z"/>
<ellipse fill="#fff" cx="40.2" cy="56.8" rx="21.3" ry="15.4"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="32.275" y1="735.607" x2="47.536" y2="750.868"
gradientTransform="translate(0 -686)">
<stop offset="0" stop-color="#74a2e3"/>
<stop offset="1" stop-color="#5c8bdb"/>
</linearGradient>
<ellipse cx="40.2" cy="57.5" rx="12.4" ry="8.9" fill="url(#SVGID_2_)"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="31.04" y1="700.636" x2="58.894" y2="728.49"
gradientTransform="translate(0 -686)">
<stop offset="0" stop-color="#74a2e3"/>
<stop offset="1" stop-color="#5c8bdb"/>
</linearGradient>
<path
d="M55.8 40.9H50c-.3 0-.7-.2-.8-.4-.1-.3-.1-.7.1-.9l4.7-5.8h-4.1c-.4 0-.9-.3-.9-.9 0-.5.3-.9.9-.9h5.8c.3 0 .7.2.8.4.1.3.1.7-.1.9l-4.7 5.8h4.1c.4 0 .9.3.9.9-.1.6-.5.9-.9.9z"
fill="url(#SVGID_3_)"/>
<g>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="43.32" y1="688.355" x2="71.174" y2="716.21"
gradientTransform="translate(0 -686)">
<stop offset="0" stop-color="#74a2e3"/>
<stop offset="1" stop-color="#5c8bdb"/>
</linearGradient>
<path
d="M67.4 28.7h-8.8c-.4 0-.9-.2-1.1-.7-.2-.4-.1-.9.1-1.3l7.3-9h-6.4c-.7 0-1.2-.5-1.2-1.2s.5-1.2 1.2-1.2h8.8c.4 0 .9.2 1.1.7s.1.9-.1 1.3l-7.3 9h6.3c.7 0 1.2.5 1.2 1.2 0 .6-.4 1.2-1.1 1.2z"
fill="url(#SVGID_4_)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -0,0 +1,7 @@
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd"
stroke-linejoin="round" stroke-miterlimit="1.414">
<path fill="none" d="M0 0h20v20H0z"/>
<path
d="M14.577 6.23a.887.887 0 0 1 1.17-.019.704.704 0 0 1 .021 1.063l-6.844 6.439-.025.023a1.11 1.11 0 0 1-1.463-.023l-3.204-3.015a.704.704 0 0 1 .021-1.063.887.887 0 0 1 1.17.019l2.757 2.594 6.397-6.018z"
fill="#FFFFFF" fill-rule="nonzero"/>
</svg>

After

Width:  |  Height:  |  Size: 472 B

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Drone Runner Dashboard</title>
<link rel="stylesheet" type="text/css" href="reset.css">
<link rel="stylesheet" type="text/css" href="style.css">
<script src="timeago.js" type="text/javascript"></script>
</head>
<body>
<header>
<div class="logo">
<svg viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M12.086 5.814l-.257.258 10.514 10.514C20.856 18.906 20 21.757 20 25c0 9.014 6.618 15 15 15 3.132 0 6.018-.836 8.404-2.353l10.568 10.568C48.497 55.447 39.796 60 30 60 13.434 60 0 46.978 0 30 0 19.903 4.751 11.206 12.086 5.814zm5.002-2.97C20.998 1.015 25.378 0 30 0c16.566 0 30 13.022 30 30 0 4.67-1.016 9.04-2.835 12.923l-9.508-9.509C49.144 31.094 50 28.243 50 25c0-9.014-6.618-15-15-15-3.132 0-6.018.836-8.404 2.353l-9.508-9.508zM35 34c-5.03 0-9-3.591-9-9s3.97-9 9-9c5.03 0 9 3.591 9 9s-3.97 9-9 9z" id="a"></path></defs><use fill="#FFF" xlink:href="#a" fill-rule="evenodd"></use></svg>
</div>
</header>
<main>
<section>
<header>
<h1>Dashboard</h1>
</header>
<div class="alert sleeping">
<p>There is no recent activity to display.</p>
</div>
<article class="cards stages">
<div class="card stage">
<h2>{{ .Repo.Slug }}</h2>
<img src="https://avatars0.githubusercontent.com/u/817538" />
<span class="connector"></span>
<span class="status {{ .Stage.Status }}"></span>
<span class="desc">processing the <em>{{ .Stage.Name }}</em> stage for build <em>#{{ .Build.Number }}</em></span>
<span class="time" datetime="{{ if .Stage.Updated }}{{ .Stage.Updated }}{{ else }}{{ .Stage.Created }}{{ end }}"></span>
</div>
<div class="card stage">
<h2>octocat/hello-world</h2>
<img src="https://avatars0.githubusercontent.com/u/817538" />
<span class="connector"></span>
<span class="status pending"></span>
<span class="desc">processing the <em>default</em> stage for build <em>#25</em></span>
<span class="time" datetime="2016-07-07T09:24:17Z"></span>
</div>
<div class="card stage">
<h2>octocat/hello-world</h2>
<img src="https://avatars0.githubusercontent.com/u/817538" />
<span class="connector"></span>
<span class="status running"></span>
<span class="desc">processing the <em>default</em> stage for build <em>#25</em></span>
<span class="time" datetime="2016-07-07T09:24:17Z"></span>
</div>
<div class="card stage">
<h2>octocat/hello-world</h2>
<img src="https://avatars0.githubusercontent.com/u/817538" />
<span class="connector"></span>
<span class="status success"></span>
<span class="desc">processing the <em>default</em> stage for build <em>#25</em></span>
<span class="time" datetime="2016-07-07T09:24:17Z"></span>
</div>
</article>
</section>
</main>
<footer></footer>
<script>
timeago.render(document.querySelectorAll('.time'));
</script>
</body>
</html>

@ -0,0 +1,43 @@
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

@ -0,0 +1,210 @@
:root {
--font-sans: -apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;
--font-mono: Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace;
}
html, body {
background: #f7f8fa;
color: #1e375a;
width: 100%;
height: 100%;
font-family: var(--font-sans);
font-size: 15px;
}
main {
box-sizing: border-box;
max-width: 800px;
margin: 0px auto;
margin-bottom: 30px;
}
main section > header h1 {
height: 41px;
font-size: 30px;
font-weight: 400;
font-style: normal;
font-stretch: normal;
line-height: normal;
letter-spacing: normal;
color: #1e375a;
margin: 30px 0;
}
body > header {
height: 56px;
box-shadow: 0 2px 4px 0 rgba(30,55,90,.05);
box-sizing: border-box;
background-color: rgba(30,55,90,.97);
padding: 0 15px;
display: flex;
align-items: center;
}
header .logo {
width: 30px;
height: 30px;
}
.card {
box-shadow: 0 2px 4px 0 rgba(30,55,90,.1);
box-sizing: border-box;
border: 1px solid rgba(30,55,90,.05);
border-radius: 4px;
background-color: #fff;
margin-bottom: 10px;
}
/*
* stage card component
*/
.stage {
padding: 15px;
display: grid;
grid-gap: 10px 0px;
grid-template-columns: 30px 30px auto 150px;
}
.stage img {
border-radius: 50%;
width: 20px;
height: 20px;
grid-row: 2;
grid-column: 2;
}
.stage h2 {
color: #1e375a;
font-size: 18px;
grid-row: 1;
grid-column: 2 / span 3;
}
.stage .desc {
color: rgba(30,55,90,.6);
font-size: 14px;
grid-row: 2;
grid-column: 3;
align-items: center;
display: flex;
}
.stage time,
.stage .time {
color: rgba(30,55,90,.6);
font-size: 14px;
grid-row: 2;
grid-column: 4;
text-align: right;
display: flex;
align-items: center;
justify-content: flex-end;
}
.stage em {
background-color: rgba(35,100,210,.07);
border-radius: 2px;
color: #2364d2;
padding: 0 4px;
margin: 0 4px;
line-height: 17px;
}
.stage .connector {
width: 15px;
height: 15px;
opacity: .2;
border-bottom-left-radius: 8px;
border-left: 1px solid #1e375a;
border-bottom: 1px solid #1e375a;
display: block;
grid-row: 2;
grid-column: 1;
margin-top: -4px;
margin-left: 9px;
}
/**
* alert components
*/
.alert.sleeping {
box-shadow: 0 2px 4px 0 rgba(30,55,90,.1);
box-sizing: border-box;
border: 1px solid rgba(30,55,90,.05);
border-radius: 4px;
margin-bottom: 10px;
padding: 30px;
padding-top: 130px;
text-align: center;
background-color: #FFFFFF;
background-image: url(icons/sleeping.svg);
background-size: 80px;
background-position-x: center;
background-position-y: 30px;
background-repeat: no-repeat;
}
/**
* status component.
*/
.status {
background-color: #ff4164;
border-radius: 50%;
display: block;
width: 20px;
height: 20px;
background-image: url('icons/failure.svg');
}
.status.skipped {
background-color: #96a5be;
background-image: url('icons/skipped.svg');
}
.status.pending {
background-color: #96a5be;
background-image: url('icons/pending.svg');
animation: wrench 2s ease infinite;
}
.status.running {
background-image: url('icons/running.svg');
background-color: transparent;
animation: spin 2s linear infinite;
}
.status.success,
.status.passing {
background-color: #19d78c;
background-image: url('icons/success.svg');
}
/*
* animations
*/
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(359deg); }
}
@keyframes wrench {
0% { transform: rotate(-12deg); }
8% { transform: rotate(12deg); }
10% { transform: rotate(24deg); }
18% { transform: rotate(-24deg); }
20% { transform: rotate(-24deg); }
28% { transform: rotate(24deg); }
30% { transform: rotate(24deg); }
38% { transform: rotate(-24deg); }
40% { transform: rotate(-24deg); }
48% { transform: rotate(24deg); }
50% { transform: rotate(24deg); }
58% { transform: rotate(-24deg); }
60% { transform: rotate(-24deg); }
68% { transform: rotate(24deg); }
75%, 100% { transform: rotate(0deg); }
}

@ -0,0 +1,105 @@
!function(t, e) {
"object" == typeof exports && "undefined" != typeof module ? e(exports) : "function" == typeof define && define.amd ? define(["exports"], e) : e(t.timeago = {})
}(this, function(t) {
"use strict";
var f = [60, 60, 24, 7, 365 / 7 / 12, 12]
, o = function(t) {
return parseInt(t)
}
, n = function(t) {
return t instanceof Date ? t : !isNaN(t) || /^\d+$/.test(t) ? new Date(o(t)) : (t = (t || "").trim().replace(/\.\d+/, "").replace(/-/, "/").replace(/-/, "/").replace(/(\d)T(\d)/, "$1 $2").replace(/Z/, " UTC").replace(/([\+\-]\d\d)\:?(\d\d)/, " $1$2"),
new Date(t))
}
, s = function(t, e) {
for (var n = 0, r = t < 0 ? 1 : 0, a = t = Math.abs(t); f[n] <= t && n < f.length; n++)
t /= f[n];
return (0 === (n *= 2) ? 9 : 1) < (t = o(t)) && (n += 1),
e(t, n, a)[r].replace("%s", t)
}
, d = function(t, e) {
return ((e = e ? n(e) : new Date) - n(t)) / 1e3
}
, r = "second_minute_hour_day_week_month_year".split("_")
, a = "秒_分钟_小时_天_周_个月_年".split("_")
, e = function(t, e) {
if (0 === e)
return ["just now", "right now"];
var n = r[parseInt(e / 2)];
return 1 < t && (n += "s"),
["".concat(t, " ").concat(n, " ago"), "in ".concat(t, " ").concat(n)]
}
, i = {
en_US: e,
zh_CN: function(t, e) {
if (0 === e)
return ["刚刚", "片刻后"];
var n = a[parseInt(e / 2)];
return ["".concat(t, " ").concat(n, "前"), "".concat(t, " ").concat(n, "后")]
}
}
, c = function(t) {
return i[t] || e
}
, l = "timeago-tid"
, u = function(t, e) {
return t.getAttribute ? t.getAttribute(e) : t.attr ? t.attr(e) : void 0
}
, p = function(t) {
return u(t, l)
}
, _ = {}
, v = function(t) {
clearTimeout(t),
delete _[t]
}
, h = function t(e, n, r, a) {
v(p(e));
var o = d(n, a);
e.innerHTML = s(o, r);
var i, c, u = setTimeout(function() {
t(e, n, r, a)
}, 1e3 * function(t) {
for (var e = 1, n = 0, r = Math.abs(t); f[n] <= t && n < f.length; n++)
t /= f[n],
e *= f[n];
return r = (r %= e) ? e - r : e,
Math.ceil(r)
}(o), 2147483647);
_[u] = 0,
c = u,
(i = e).setAttribute ? i.setAttribute(l, c) : i.attr && i.attr(l, c)
};
t.version = "4.0.0-beta.2",
t.format = function(t, e, n) {
var r = d(t, n);
return s(r, c(e))
}
,
t.render = function(t, e, n) {
var r;
void 0 === t.length && (t = [t]);
for (var a = 0; a < t.length; a++) {
r = t[a];
var o = u(r, "datetime")
, i = c(e);
h(r, o, i, n)
}
return t
}
,
t.cancel = function(t) {
if (t)
v(p(t));
else
for (var e in _)
v(e)
}
,
t.register = function(t, e) {
i[t] = e
}
,
Object.defineProperty(t, "__esModule", {
value: !0
})
});

@ -0,0 +1,7 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package static
//go:generate togo http -package static -output static_gen.go

File diff suppressed because it is too large Load Diff

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="10">
<title>Dashboard</title>
<link rel="stylesheet" type="text/css" href="/static/reset.css">
<link rel="stylesheet" type="text/css" href="/static/style.css">
<script src="/static/timeago.js" type="text/javascript"></script>
</head>
<body>
<header>
<div class="logo">
<svg viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M12.086 5.814l-.257.258 10.514 10.514C20.856 18.906 20 21.757 20 25c0 9.014 6.618 15 15 15 3.132 0 6.018-.836 8.404-2.353l10.568 10.568C48.497 55.447 39.796 60 30 60 13.434 60 0 46.978 0 30 0 19.903 4.751 11.206 12.086 5.814zm5.002-2.97C20.998 1.015 25.378 0 30 0c16.566 0 30 13.022 30 30 0 4.67-1.016 9.04-2.835 12.923l-9.508-9.509C49.144 31.094 50 28.243 50 25c0-9.014-6.618-15-15-15-3.132 0-6.018.836-8.404 2.353l-9.508-9.508zM35 34c-5.03 0-9-3.591-9-9s3.97-9 9-9c5.03 0 9 3.591 9 9s-3.97 9-9 9z" id="a"></path></defs><use fill="#FFF" xlink:href="#a" fill-rule="evenodd"></use></svg>
</div>
</header>
<main>
<section>
<header>
<h1>Dashboard</h1>
</header>
<article class="cards stages">
{{ if .Empty }}
<div class="alert sleeping">
<p>There is no recent activity to display.</p>
</div>
{{ else if .Idle }}
<div class="alert sleeping">
<p>This runner is currently idle.</p>
</div>
{{ end }}
{{ range .Items }}
<div class="card stage">
<h2>{{ .Repo.Slug }}</h2>
<img src="{{ .Build.AuthorAvatar }}" />
<span class="connector"></span>
<span class="status {{ .Stage.Status }}"></span>
<span class="desc">assigned stage <em>{{ .Stage.Name }}</em> for build <em>#{{ .Build.Number }}</em></span>
<span class="time" datetime="{{ if .Stage.Started }}{{ timestamp .Stage.Started }}{{ else }}{{ timestamp .Stage.Created }}{{ end }}"></span>
</div>
{{ end }}
</article>
</section>
</main>
<footer></footer>
<script>
timeago.render(document.querySelectorAll('.time'));
</script>
</body>
</html>

@ -0,0 +1,17 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package template
import (
"time"
)
//go:generate togo tmpl -func funcMap -format html
var funcMap = map[string]interface{}{
"timestamp": func(v int64) string {
return time.Unix(v, 0).UTC().Format("2006-01-02T15:04:05Z")
},
}

@ -0,0 +1,86 @@
package template
import "html/template"
// list of embedded template files.
var files = []struct {
name string
data string
}{
{
name: "index.tmpl",
data: index,
},
}
// T exposes the embedded templates.
var T *template.Template
func init() {
T = template.New("_").Funcs(funcMap)
for _, file := range files {
T = template.Must(
T.New(file.name).Parse(file.data),
)
}
}
//
// embedded template files.
//
// files/index.tmpl
var index = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="10">
<title>Dashboard</title>
<link rel="stylesheet" type="text/css" href="/static/reset.css">
<link rel="stylesheet" type="text/css" href="/static/style.css">
<script src="/static/timeago.js" type="text/javascript"></script>
</head>
<body>
<header>
<div class="logo">
<svg viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M12.086 5.814l-.257.258 10.514 10.514C20.856 18.906 20 21.757 20 25c0 9.014 6.618 15 15 15 3.132 0 6.018-.836 8.404-2.353l10.568 10.568C48.497 55.447 39.796 60 30 60 13.434 60 0 46.978 0 30 0 19.903 4.751 11.206 12.086 5.814zm5.002-2.97C20.998 1.015 25.378 0 30 0c16.566 0 30 13.022 30 30 0 4.67-1.016 9.04-2.835 12.923l-9.508-9.509C49.144 31.094 50 28.243 50 25c0-9.014-6.618-15-15-15-3.132 0-6.018.836-8.404 2.353l-9.508-9.508zM35 34c-5.03 0-9-3.591-9-9s3.97-9 9-9c5.03 0 9 3.591 9 9s-3.97 9-9 9z" id="a"></path></defs><use fill="#FFF" xlink:href="#a" fill-rule="evenodd"></use></svg>
</div>
</header>
<main>
<section>
<header>
<h1>Dashboard</h1>
</header>
<article class="cards stages">
{{ if .Empty }}
<div class="alert sleeping">
<p>There is no recent activity to display.</p>
</div>
{{ else if .Idle }}
<div class="alert sleeping">
<p>This runner is currently idle.</p>
</div>
{{ end }}
{{ range .Items }}
<div class="card stage">
<h2>{{ .Repo.Slug }}</h2>
<img src="{{ .Build.AuthorAvatar }}" />
<span class="connector"></span>
<span class="status {{ .Stage.Status }}"></span>
<span class="desc">assigned stage <em>{{ .Stage.Name }}</em> for build <em>#{{ .Build.Number }}</em></span>
<span class="time" datetime="{{ if .Stage.Started }}{{ timestamp .Stage.Started }}{{ else }}{{ timestamp .Stage.Created }}{{ end }}"></span>
</div>
{{ end }}
</article>
</section>
</main>
<footer></footer>
<script>
timeago.render(document.querySelectorAll('.time'));
</script>
</body>
</html>`

@ -0,0 +1,52 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package internal
import "github.com/drone/drone-go/drone"
// CloneRepo returns a copy of the Repository.
func CloneRepo(src *drone.Repo) *drone.Repo {
dst := new(drone.Repo)
*dst = *src
return dst
}
// CloneBuild returns a copy of the Build.
func CloneBuild(src *drone.Build) *drone.Build {
dst := new(drone.Build)
*dst = *src
dst.Stages = append(src.Stages[:0:0], src.Stages...)
dst.Params = map[string]string{}
for k, v := range src.Params {
dst.Params[k] = v
}
for i, v := range src.Stages {
dst.Stages[i] = CloneStage(v)
}
return dst
}
// CloneStage returns a copy of the Stage.
func CloneStage(src *drone.Stage) *drone.Stage {
dst := new(drone.Stage)
*dst = *src
dst.DependsOn = append(src.DependsOn[:0:0], src.DependsOn...)
dst.Steps = append(src.Steps[:0:0], src.Steps...)
dst.Labels = map[string]string{}
for k, v := range src.Labels {
dst.Labels[k] = v
}
for i, v := range src.Steps {
dst.Steps[i] = CloneStep(v)
}
return dst
}
// CloneStep returns a copy of the Step.
func CloneStep(src *drone.Step) *drone.Step {
dst := new(drone.Step)
*dst = *src
return dst
}

@ -0,0 +1,229 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package internal
import (
"testing"
"github.com/drone/drone-go/drone"
"github.com/google/go-cmp/cmp"
)
func TestCloneRepo(t *testing.T) {
src := &drone.Repo{
ID: 1,
UID: "2",
UserID: 3,
Namespace: "octocat",
Name: "hello-world",
Slug: "octocat/hello-world",
SCM: "git",
HTTPURL: "https://github.com/octocat/hello-world.git",
SSHURL: "git@github.com:octocat/hello-world.git",
Link: "https://github.com/octocat/hello-world",
Branch: "master",
Private: true,
Visibility: "public",
Active: true,
Config: ".drone.yml",
Trusted: true,
Protected: true,
IgnoreForks: true,
IgnorePulls: true,
Timeout: 60,
Counter: 50,
Synced: 1561256365,
Created: 1561256505,
Updated: 1561256511,
Version: 1,
}
dst := CloneRepo(src)
if src == dst {
t.Errorf("Except copy of repo, got reference")
}
if diff := cmp.Diff(src, dst); diff != "" {
t.Errorf("Expect copy of values")
t.Log(diff)
}
}
func TestCloneBuild(t *testing.T) {
src := &drone.Build{
ID: 1,
RepoID: 2,
Number: 3,
Parent: 4,
Status: drone.StatusFailing,
Error: "",
Event: drone.EventPush,
Action: "created",
Link: "https://github.com/octocat/Hello-World/commit/7fd1a60b01f91b314f59955a4e4d4e80d8edf11d",
Timestamp: 1561256041,
Title: "",
Message: "updated README",
Before: "553c2077f0edc3d5dc5d17262f6aa498e69d6f8e",
After: "762941318ee16e59dabbacb1b4049eec22f0d303",
Ref: "refs/heads/master",
Fork: "spaceghost/hello-world",
Source: "develop",
Target: "master",
Author: "octocat",
AuthorName: "The Octocat",
AuthorEmail: "octocat@github.com",
AuthorAvatar: "https://avatars2.githubusercontent.com/u/251370",
Sender: "spaceghost",
Params: map[string]string{"memory": "high"},
Cron: "nightly",
Deploy: "production",
Started: 1561256065,
Finished: 1561256082,
Created: 1561256050,
Updated: 1561256052,
Version: 1,
Stages: []*drone.Stage{
{
ID: 1,
BuildID: 2,
Number: 3,
Name: "build",
Kind: "pipeline",
Type: "docker",
Status: drone.StatusPassing,
Error: "",
ErrIgnore: true,
ExitCode: 0,
Machine: "server1",
OS: "linux",
Arch: "amd64",
Variant: "",
Kernel: "",
Limit: 0,
Started: 1561256065,
Stopped: 1561256505,
Created: 1561256356,
Updated: 1561256082,
Version: 1,
OnSuccess: true,
OnFailure: true,
DependsOn: []string{"clone"},
Labels: map[string]string{"foo": "bar"},
},
},
}
dst := CloneBuild(src)
if src == dst {
t.Errorf("Except copy of build, got reference")
}
if diff := cmp.Diff(src, dst); diff != "" {
t.Errorf("Expect copy of values")
t.Log(diff)
}
if src.Stages[0] == dst.Stages[0] {
t.Errorf("Except copy of stages, got reference")
}
if diff := cmp.Diff(src.Stages[0], dst.Stages[0]); diff != "" {
t.Errorf("Expect copy of stage values")
t.Log(diff)
}
}
func TestCloneStage(t *testing.T) {
src := &drone.Stage{
ID: 1,
BuildID: 2,
Number: 3,
Name: "build",
Kind: "pipeline",
Type: "docker",
Status: drone.StatusPassing,
Error: "",
ErrIgnore: true,
ExitCode: 0,
Machine: "server1",
OS: "linux",
Arch: "amd64",
Variant: "",
Kernel: "",
Limit: 0,
Started: 1561256065,
Stopped: 1561256505,
Created: 1561256356,
Updated: 1561256082,
Version: 1,
OnSuccess: true,
OnFailure: true,
DependsOn: []string{"clone"},
Labels: map[string]string{"foo": "bar"},
Steps: []*drone.Step{
{
ID: 1,
StageID: 2,
Number: 3,
Name: "foo",
Status: drone.StatusFailing,
Error: "",
ErrIgnore: false,
ExitCode: 255,
Started: 1561256065,
Stopped: 1561256082,
Version: 1,
},
},
}
dst := CloneStage(src)
if src == dst {
t.Errorf("Except copy of stage, got reference")
}
if src.Steps[0] == dst.Steps[0] {
t.Errorf("Except copy of step, got reference")
}
if diff := cmp.Diff(src, dst); diff != "" {
t.Errorf("Expect copy of step values")
t.Log(diff)
}
if diff := cmp.Diff(src.Steps[0], dst.Steps[0]); diff != "" {
t.Errorf("Expect copy of stage values")
t.Log(diff)
}
}
func TestCloneStep(t *testing.T) {
src := &drone.Step{
ID: 1,
StageID: 2,
Number: 3,
Name: "foo",
Status: drone.StatusFailing,
Error: "",
ErrIgnore: false,
ExitCode: 255,
Started: 1561256065,
Stopped: 1561256082,
Version: 1,
}
dst := CloneStep(src)
if src == dst {
t.Errorf("Except copy of step, got reference")
}
if diff := cmp.Diff(src, dst); diff != "" {
t.Errorf("Expect copy of values")
t.Log(diff)
}
dst.ID = 101
dst.StageID = 102
dst.Number = 103
dst.Name = "bar"
dst.ErrIgnore = true
dst.ExitCode = 0
dst.Status = drone.StatusPassing
dst.Started = 1561256356
dst.Stopped = 1561256365
dst.Version = 2
if diff := cmp.Diff(src, dst); diff == "" {
t.Errorf("Expect copy of values, got reference")
}
}

@ -0,0 +1,28 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package internal
import "github.com/drone/drone-go/drone"
// MergeStage merges the source stage with the destination.
func MergeStage(src, dst *drone.Stage) {
dst.Version = src.Version
dst.Created = src.Created
dst.Updated = src.Updated
for i, src := range src.Steps {
dst := dst.Steps[i]
MergeStep(src, dst)
}
}
// MergeStep merges the source stage with the destination.
func MergeStep(src, dst *drone.Step) {
dst.Version = src.Version
dst.ID = src.ID
dst.StageID = src.StageID
dst.Started = src.Started
dst.Stopped = src.Stopped
dst.Version = src.Version
}

@ -0,0 +1,98 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package internal
import (
"testing"
"github.com/drone/drone-go/drone"
"github.com/google/go-cmp/cmp"
)
func TestMergeStep(t *testing.T) {
src := &drone.Step{
ID: 1,
StageID: 2,
Started: 1561256095,
Stopped: 1561256092,
Version: 1,
}
dst := &drone.Step{
ID: 1,
}
MergeStep(src, dst)
if src == dst {
t.Errorf("Except copy of step, got reference")
}
after := &drone.Step{
ID: 1,
StageID: 2,
Started: 1561256095,
Stopped: 1561256092,
Version: 1,
}
if diff := cmp.Diff(after, src); diff != "" {
t.Errorf("Expect src not modified")
t.Log(diff)
}
if diff := cmp.Diff(after, dst); diff != "" {
t.Errorf("Expect src values copied to dst")
t.Log(diff)
}
}
func TestMergeStage(t *testing.T) {
dst := &drone.Stage{
ID: 1,
Steps: []*drone.Step{
{
ID: 1,
},
},
}
src := &drone.Stage{
ID: 1,
Created: 1561256095,
Updated: 1561256092,
Version: 1,
Steps: []*drone.Step{
{
ID: 1,
StageID: 2,
Started: 1561256095,
Stopped: 1561256092,
Version: 1,
},
},
}
MergeStage(src, dst)
if src == dst {
t.Errorf("Except copy of stage, got reference")
}
after := &drone.Stage{
ID: 1,
Created: 1561256095,
Updated: 1561256092,
Version: 1,
Steps: []*drone.Step{
{
ID: 1,
StageID: 2,
Started: 1561256095,
Stopped: 1561256092,
Version: 1,
},
},
}
if diff := cmp.Diff(after, dst); diff != "" {
t.Errorf("Expect src values copied to dst")
t.Log(diff)
}
}

@ -0,0 +1,195 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
// Package livelog provides a Writer that collects pipeline
// output and streams to the central server.
package livelog
import (
"context"
"strings"
"sync"
"time"
"github.com/drone/drone-go/drone"
"github.com/drone/runner-go/client"
)
// defaultLimit is the default maximum log size in bytes.
const defaultLimit = 5242880 // 5MB
// Writer is an io.Writer that sends logs to the server.
type Writer struct {
sync.Mutex
client client.Client
id int64
num int
now time.Time
size int
limit int
interval time.Duration
pending []*drone.Line
history []*drone.Line
closed bool
close chan struct{}
ready chan struct{}
}
// New returns a new Wrtier.
func New(client client.Client, id int64) *Writer {
b := &Writer{
client: client,
id: id,
now: time.Now(),
limit: defaultLimit,
interval: time.Second,
close: make(chan struct{}),
ready: make(chan struct{}, 1),
}
go b.start()
return b
}
// SetLimit sets the Writer limit.
func (b *Writer) SetLimit(limit int) {
b.limit = limit
}
// SetInterval sets the Writer flusher interval.
func (b *Writer) SetInterval(interval time.Duration) {
b.interval = interval
}
// Write uploads the live log stream to the server.
func (b *Writer) Write(p []byte) (n int, err error) {
for _, part := range split(p) {
line := &drone.Line{
Number: b.num,
Message: part,
Timestamp: int64(time.Since(b.now).Seconds()),
}
for b.size+len(p) > b.limit {
b.stop() // buffer is full, step streaming data
b.size -= len(b.history[0].Message)
b.history = b.history[1:]
}
b.size = b.size + len(part)
b.num++
if b.stopped() == false {
b.Lock()
b.pending = append(b.pending, line)
b.Unlock()
}
b.Lock()
b.history = append(b.history, line)
b.Unlock()
}
select {
case b.ready <- struct{}{}:
default:
}
return len(p), nil
}
// Close closes the writer and uploads the full contents to
// the server.
func (b *Writer) Close() error {
if b.stop() {
b.flush()
}
return b.upload()
}
// upload uploads the full log history to the server.
func (b *Writer) upload() error {
return b.client.Upload(
context.Background(), b.id, b.history)
}
// flush batch uploads all buffered logs to the server.
func (b *Writer) flush() error {
b.Lock()
lines := b.copy()
b.clear()
b.Unlock()
if len(lines) == 0 {
return nil
}
return b.client.Batch(
context.Background(), b.id, lines)
}
// copy returns a copy of the buffered lines.
func (b *Writer) copy() []*drone.Line {
return append(b.pending[:0:0], b.pending...)
}
// clear clears the buffer.
func (b *Writer) clear() {
b.pending = b.pending[:0]
}
func (b *Writer) stop() bool {
b.Lock()
var closed bool
if b.closed == false {
close(b.close)
closed = true
}
b.Unlock()
return closed
}
func (b *Writer) stopped() bool {
b.Lock()
closed := b.closed
b.Unlock()
return closed
}
func (b *Writer) start() error {
for {
select {
case <-b.close:
return nil
case <-b.ready:
select {
case <-b.close:
return nil
case <-time.After(b.interval):
// we intentionally ignore errors. log streams
// are ephemeral and are considered low prioirty
// because they are not required for drone to
// operator, and the impact of failure is minimal
b.flush()
}
}
}
}
func split(p []byte) []string {
s := string(p)
v := []string{s}
// kubernetes buffers the output and may combine
// multiple lines into a single block of output.
// Split into multiple lines.
//
// note that docker output always inclines a line
// feed marker. This needs to be accounted for when
// splitting the output into multiple lines.
if strings.Contains(strings.TrimSuffix(s, "\n"), "\n") {
v = strings.SplitAfter(s, "\n")
}
return v
}

@ -0,0 +1,92 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package livelog
import (
"context"
"testing"
"time"
"github.com/drone/drone-go/drone"
"github.com/drone/runner-go/client"
"github.com/google/go-cmp/cmp"
)
func TestLineWriterSingle(t *testing.T) {
client := new(mockClient)
w := New(client, 1)
w.SetInterval(time.Duration(0))
w.num = 4
w.Write([]byte("foo\nbar\n"))
a := w.pending
b := []*drone.Line{
{Number: 4, Message: "foo\n"},
{Number: 5, Message: "bar\n"},
{Number: 6, Message: ""},
}
if diff := cmp.Diff(a, b); diff != "" {
t.Fail()
t.Log(diff)
}
w.Close()
a = client.uploaded
if diff := cmp.Diff(a, b); diff != "" {
t.Fail()
t.Log(diff)
}
if len(w.pending) > 0 {
t.Errorf("Expect empty buffer")
}
}
func TestLineWriterLimit(t *testing.T) {
client := new(mockClient)
w := New(client, 0)
if got, want := w.limit, defaultLimit; got != want {
t.Errorf("Expect default buffer limit %d, got %d", want, got)
}
w.SetLimit(6)
if got, want := w.limit, 6; got != want {
t.Errorf("Expect custom buffer limit %d, got %d", want, got)
}
w.Write([]byte("foo"))
w.Write([]byte("bar"))
w.Write([]byte("baz"))
if got, want := w.size, 6; got != want {
t.Errorf("Expect buffer size %d, got %d", want, got)
}
a := w.history
b := []*drone.Line{
{Number: 1, Message: "bar"},
{Number: 2, Message: "baz"},
}
if diff := cmp.Diff(a, b); diff != "" {
t.Fail()
t.Log(diff)
}
}
type mockClient struct {
client.Client
lines []*drone.Line
uploaded []*drone.Line
}
func (m *mockClient) Batch(ctx context.Context, id int64, lines []*drone.Line) error {
m.lines = append(m.lines, lines...)
return nil
}
func (m *mockClient) Upload(ctx context.Context, id int64, lines []*drone.Line) error {
m.uploaded = lines
return nil
}

@ -0,0 +1,33 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package logger
import (
"context"
"net/http"
)
type loggerKey struct{}
// WithContext returns a new context with the provided logger.
// Use in combination with logger.WithField for great effect.
func WithContext(ctx context.Context, logger Logger) context.Context {
return context.WithValue(ctx, loggerKey{}, logger)
}
// FromContext retrieves the current logger from the context.
func FromContext(ctx context.Context) Logger {
logger := ctx.Value(loggerKey{})
if logger == nil {
return Discard()
}
return logger.(Logger)
}
// FromRequest retrieves the current logger from the request. If no
// logger is available, the default logger is returned.
func FromRequest(r *http.Request) Logger {
return FromContext(r.Context())
}

@ -0,0 +1,46 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package logger
import (
"context"
"net/http"
"testing"
)
func TestContext(t *testing.T) {
entry := Discard()
ctx := WithContext(context.Background(), entry)
got := FromContext(ctx)
if got != entry {
t.Errorf("Expected Logger from context")
}
}
func TestEmptyContext(t *testing.T) {
got := FromContext(context.Background())
if got == nil {
t.Errorf("Expected Logger from context")
}
if _, ok := got.(*discard); !ok {
t.Errorf("Expected discard Logger from context")
}
}
func TestRequest(t *testing.T) {
entry := Discard()
ctx := WithContext(context.Background(), entry)
req := new(http.Request)
req = req.WithContext(ctx)
got := FromRequest(req)
if got != entry {
t.Errorf("Expected Logger from http.Request")
}
}

@ -0,0 +1,49 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package logger
import (
"io"
"net/http"
"net/http/httputil"
"os"
)
// Dumper dumps the http.Request and http.Response
// message payload for debugging purposes.
type Dumper interface {
DumpRequest(*http.Request)
DumpResponse(*http.Response)
}
// DiscardDumper returns a no-op dumper.
func DiscardDumper() Dumper {
return new(discardDumper)
}
type discardDumper struct{}
func (*discardDumper) DumpRequest(*http.Request) {}
func (*discardDumper) DumpResponse(*http.Response) {}
// StandardDumper returns a standard dumper.
func StandardDumper(body bool) Dumper {
return &standardDumper{out: os.Stdout, body: body}
}
type standardDumper struct {
body bool
out io.Writer
}
func (s *standardDumper) DumpRequest(req *http.Request) {
dump, _ := httputil.DumpRequestOut(req, s.body)
s.out.Write(dump)
}
func (s *standardDumper) DumpResponse(res *http.Response) {
dump, _ := httputil.DumpResponse(res, s.body)
s.out.Write(dump)
}

@ -0,0 +1,62 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package logger
import (
"bytes"
"net/http"
"os"
"testing"
)
func TestStandardDumper(t *testing.T) {
d := StandardDumper(true)
if s, ok := d.(*standardDumper); !ok {
t.Errorf("Expect standard dumper")
} else if s.out != os.Stdout {
t.Errorf("Expect standard dumper set to stdout")
}
}
func TestDiscardDumper(t *testing.T) {
d := DiscardDumper()
if _, ok := d.(*discardDumper); !ok {
t.Errorf("Expect discard dumper")
}
}
func TestStandardDumper_DumpRequest(t *testing.T) {
buf := new(bytes.Buffer)
r, _ := http.NewRequest("GET", "http://example.com", nil)
d := StandardDumper(true).(*standardDumper)
d.out = buf
d.DumpRequest(r)
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"
got := buf.String()
if got != want {
t.Errorf("Got dumped request %q", got)
}
}
func TestStandardDumper_DumpResponse(t *testing.T) {
buf := new(bytes.Buffer)
r := &http.Response{
Status: "200 OK",
StatusCode: 200,
Proto: "HTTP/1.0",
ProtoMajor: 1,
ProtoMinor: 0,
}
d := StandardDumper(true).(*standardDumper)
d.out = buf
d.DumpResponse(r)
want := "HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n"
got := buf.String()
if got != want {
t.Errorf("Got dumped request %q", got)
}
}

@ -0,0 +1,59 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
// Package logger defines interfaces that logger drivers
// implement to log messages.
package logger
// A Logger represents an active logging object that generates
// lines of output to an io.Writer.
type Logger interface {
Debug(args ...interface{})
Debugf(format string, args ...interface{})
Debugln(args ...interface{})
Error(args ...interface{})
Errorf(format string, args ...interface{})
Errorln(args ...interface{})
Info(args ...interface{})
Infof(format string, args ...interface{})
Infoln(args ...interface{})
Trace(args ...interface{})
Tracef(format string, args ...interface{})
Traceln(args ...interface{})
Warn(args ...interface{})
Warnf(format string, args ...interface{})
Warnln(args ...interface{})
WithError(error) Logger
WithField(string, interface{}) Logger
}
// Discard returns a no-op logger
func Discard() Logger {
return &discard{}
}
type discard struct{}
func (*discard) Debug(args ...interface{}) {}
func (*discard) Debugf(format string, args ...interface{}) {}
func (*discard) Debugln(args ...interface{}) {}
func (*discard) Error(args ...interface{}) {}
func (*discard) Errorf(format string, args ...interface{}) {}
func (*discard) Errorln(args ...interface{}) {}
func (*discard) Info(args ...interface{}) {}
func (*discard) Infof(format string, args ...interface{}) {}
func (*discard) Infoln(args ...interface{}) {}
func (*discard) Trace(args ...interface{}) {}
func (*discard) Tracef(format string, args ...interface{}) {}
func (*discard) Traceln(args ...interface{}) {}
func (*discard) Warn(args ...interface{}) {}
func (*discard) Warnf(format string, args ...interface{}) {}
func (*discard) Warnln(args ...interface{}) {}
func (d *discard) WithError(error) Logger { return d }
func (d *discard) WithField(string, interface{}) Logger { return d }

@ -0,0 +1,21 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package logger
import "testing"
func TestWithError(t *testing.T) {
d := &discard{}
if d.WithError(nil) != d {
t.Errorf("Expect WithError to return base logger")
}
}
func TestWithField(t *testing.T) {
d := &discard{}
if d.WithField("hello", "world") != d {
t.Errorf("Expect WithField to return base logger")
}
}

@ -0,0 +1,24 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package logger
import "github.com/sirupsen/logrus"
// Logrus returns a Logger that wraps a logrus.Logger.
func Logrus(logrus *logrus.Logger) Logger {
return &wrapLogrus{logrus}
}
type wrapLogrus struct {
*logrus.Logger
}
func (w *wrapLogrus) WithError(err error) Logger {
return &wrapLogrus{w.Logger.WithError(err).Logger}
}
func (w *wrapLogrus) WithField(key string, value interface{}) Logger {
return &wrapLogrus{w.Logger.WithField(key, value).Logger}
}

@ -0,0 +1,24 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package logger
import (
"testing"
"github.com/sirupsen/logrus"
)
func TestLogrus(t *testing.T) {
logger := Logrus(logrus.StandardLogger())
if _, ok := logger.(*wrapLogrus); !ok {
t.Errorf("Expect wrapped logrus")
}
if _, ok := logger.WithError(nil).(*wrapLogrus); !ok {
t.Errorf("Expect WithError wraps logrus")
}
if _, ok := logger.WithField("foo", "bar").(*wrapLogrus); !ok {
t.Errorf("Expect WithField logrus")
}
}

@ -0,0 +1,13 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
// Clone configures the git clone.
type Clone struct {
Disable bool `json:"disable,omitempty"`
Depth int `json:"depth,omitempty"`
SkipVerify bool `json:"skip_verify,omitempty" yaml:"skip_verify"`
Trace bool `json:"trace,omitempty"`
}

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

@ -0,0 +1,117 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
import filepath "github.com/bmatcuk/doublestar"
// Match provides match creteria for evaluation.
type Match struct {
Action string
Branch string
Cron string
Event string
Instance string
Ref string
Repo string
Target string
Paths []string
}
// Conditions defines a group of conditions.
type Conditions struct {
Action Condition `json:"action,omitempty"`
Cron Condition `json:"cron,omitempty"`
Ref Condition `json:"ref,omitempty"`
Repo Condition `json:"repo,omitempty"`
Instance Condition `json:"instance,omitempty"`
Target Condition `json:"target,omitempty"`
Event Condition `json:"event,omitempty"`
Branch Condition `json:"branch,omitempty"`
Status Condition `json:"status,omitempty"`
Paths Condition `json:"paths,omitempty"`
}
// Match returns true if the string matches the include
// patterns and does not match any of the exclude patterns.
func (c Conditions) Match(m Match) bool {
return c.Cron.Match(m.Cron) &&
c.Ref.Match(m.Ref) &&
c.Repo.Match(m.Repo) &&
c.Instance.Match(m.Instance) &&
c.Target.Match(m.Target) &&
c.Event.Match(m.Event) &&
c.Branch.Match(m.Branch) &&
c.Action.Match(m.Action)
}
// Condition defines a runtime condition.
type Condition struct {
Include []string `yaml:"include,omitempty" json:"include,omitempty"`
Exclude []string `yaml:"exclude,omitempty" json:"exclude,omitempty"`
}
// Match returns true if the string matches the include
// patterns and does not match any of the exclude patterns.
func (c *Condition) Match(v string) bool {
if c.Excludes(v) {
return false
}
if c.Includes(v) {
return true
}
if len(c.Include) == 0 {
return true
}
return false
}
// Includes returns true if the string matches the include
// patterns.
func (c *Condition) Includes(v string) bool {
for _, pattern := range c.Include {
if ok, _ := filepath.Match(pattern, v); ok {
return true
}
}
return false
}
// Excludes returns true if the string matches the exclude
// patterns.
func (c *Condition) Excludes(v string) bool {
for _, pattern := range c.Exclude {
if ok, _ := filepath.Match(pattern, v); ok {
return true
}
}
return false
}
// UnmarshalYAML implements yml unmarshalling.
func (c *Condition) UnmarshalYAML(unmarshal func(interface{}) error) error {
var out1 string
var out2 []string
var out3 = struct {
Include []string
Exclude []string
}{}
err := unmarshal(&out1)
if err == nil {
c.Include = []string{out1}
return nil
}
unmarshal(&out2)
unmarshal(&out3)
c.Exclude = out3.Exclude
c.Include = append(
out3.Include,
out2...,
)
return nil
}

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

@ -0,0 +1,17 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
// registered drivers.
var drivers []Driver
// Register registers the parsing driver.
func Register(driver Driver) {
drivers = append(drivers, driver)
}
// Driver defines a parser driver that can be used to parse
// resource-specific Yaml documents.
type Driver func(r *RawResource) (Resource, bool, error)

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

@ -0,0 +1,47 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
type (
// Variable represents an environment variable that
// can be defined as a string literal or as a reference
// to a secret.
Variable struct {
Value string `json:"value,omitempty"`
Secret string `json:"from_secret,omitempty" yaml:"from_secret"`
}
// variable is a tempoary type used to unmarshal
// variables with references to secrets.
variable struct {
Value string
Secret string `yaml:"from_secret"`
}
)
// UnmarshalYAML implements yaml unmarshalling.
func (v *Variable) UnmarshalYAML(unmarshal func(interface{}) error) error {
d := new(variable)
err := unmarshal(&d.Value)
if err != nil {
err = unmarshal(d)
}
v.Value = d.Value
v.Secret = d.Secret
return err
}
// MarshalYAML implements yaml marshalling.
func (v *Variable) MarshalYAML() (interface{}, error) {
if v.Secret != "" {
m := map[string]interface{}{}
m["from_secret"] = v.Secret
return m, nil
}
if v.Value != "" {
return v.Value, nil
}
return nil, nil
}

@ -0,0 +1,43 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
import (
"testing"
"github.com/buildkite/yaml"
)
func TestEnv(t *testing.T) {
tests := []struct {
yaml string
value string
from string
}{
{
yaml: "bar",
value: "bar",
},
{
yaml: "from_secret: username",
from: "username",
},
}
for _, test := range tests {
in := []byte(test.yaml)
out := new(Variable)
err := yaml.Unmarshal(in, out)
if err != nil {
t.Error(err)
return
}
if got, want := out.Value, test.value; got != want {
t.Errorf("Want variable value %q, got %q", want, got)
}
if got, want := out.Secret, test.from; got != want {
t.Errorf("Want variable from_secret %q, got %q", want, got)
}
}
}

@ -0,0 +1,60 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
// Package manifest provides definitions for the Yaml schema.
package manifest
// Resource enums.
const (
KindApproval = "approval"
KindDeployment = "deployment"
KindPipeline = "pipeline"
KindSecret = "secret"
KindSignature = "signature"
)
type (
// Manifest is a collection of Drone resources.
Manifest struct {
Resources []Resource
}
// Resource represents a Drone resource.
Resource interface {
GetVersion() string
GetKind() string
GetType() string
GetName() string
}
// DependantResource is a resoure with runtime dependencies.
DependantResource interface {
Resource
GetDependsOn() []string
}
// PlatformResource is a resoure with platform requirements.
PlatformResource interface {
Resource
GetPlatform() Platform
}
// TriggeredResource is a resoure with trigger rules.
TriggeredResource interface {
Resource
GetTrigger() Conditions
}
// RawResource is a raw encoded resource with the common
// metadata extracted.
RawResource struct {
Version string
Kind string
Type string
Name string
Deps []string `yaml:"depends_on"`
Platform Platform
Data []byte `yaml:"-"`
}
)

@ -0,0 +1,48 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
type (
// Parameter represents an configuration parameter that
// can be defined as a literal or as a reference
// to a secret.
Parameter struct {
Value interface{} `json:"value,omitempty"`
Secret string `json:"from_secret,omitempty" yaml:"from_secret"`
}
// parameter is a tempoary type used to unmarshal
// parameters with references to secrets.
parameter struct {
Secret string `yaml:"from_secret"`
}
)
// UnmarshalYAML implements yaml unmarshalling.
func (p *Parameter) UnmarshalYAML(unmarshal func(interface{}) error) error {
d := new(parameter)
err := unmarshal(d)
if err == nil && d.Secret != "" {
p.Secret = d.Secret
return nil
}
var i interface{}
err = unmarshal(&i)
p.Value = i
return err
}
// MarshalYAML implements yaml marshalling.
func (p *Parameter) MarshalYAML() (interface{}, error) {
if p.Secret != "" {
m := map[string]interface{}{}
m["from_secret"] = p.Secret
return m, nil
}
if p.Value != "" {
return p.Value, nil
}
return nil, nil
}

@ -0,0 +1,43 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
import (
"testing"
"github.com/buildkite/yaml"
)
func TestParam(t *testing.T) {
tests := []struct {
yaml string
value interface{}
from string
}{
{
yaml: "bar",
value: "bar",
},
{
yaml: "from_secret: username",
from: "username",
},
}
for _, test := range tests {
in := []byte(test.yaml)
out := new(Parameter)
err := yaml.Unmarshal(in, out)
if err != nil {
t.Error(err)
return
}
if got, want := out.Value, test.value; got != want {
t.Errorf("Want value %q, got %q", want, got)
}
if got, want := out.Secret, test.from; got != want {
t.Errorf("Want from_secret %q, got %q", want, got)
}
}
}

@ -0,0 +1,154 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
import (
"bufio"
"bytes"
"io"
"os"
"strings"
"github.com/buildkite/yaml"
)
// Parse parses the configuration from io.Reader r.
func Parse(r io.Reader) (*Manifest, error) {
resources, err := ParseRaw(r)
if err != nil {
return nil, err
}
manifest := new(Manifest)
for _, raw := range resources {
if raw == nil {
continue
}
resource, err := parseRaw(raw)
if err != nil {
return nil, err
}
if resource == nil {
continue
}
manifest.Resources = append(
manifest.Resources,
resource,
)
}
return manifest, nil
}
// ParseBytes parses the configuration from bytes b.
func ParseBytes(b []byte) (*Manifest, error) {
return Parse(
bytes.NewBuffer(b),
)
}
// ParseString parses the configuration from string s.
func ParseString(s string) (*Manifest, error) {
return ParseBytes(
[]byte(s),
)
}
// ParseFile parses the configuration from path p.
func ParseFile(p string) (*Manifest, error) {
f, err := os.Open(p)
if err != nil {
return nil, err
}
defer f.Close()
return Parse(f)
}
func parseRaw(r *RawResource) (Resource, error) {
for _, fn := range drivers {
res, ok, err := fn(r)
if ok {
return res, err
}
}
return nil, nil
}
// ParseRaw parses the multi-document yaml from the
// io.Reader and returns a slice of raw resources.
func ParseRaw(r io.Reader) ([]*RawResource, error) {
const newline = '\n'
var resources []*RawResource
var resource *RawResource
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
if isSeparator(line) {
resource = nil
}
if resource == nil {
resource = &RawResource{}
resources = append(resources, resource)
}
if isSeparator(line) {
continue
}
if isTerminator(line) {
break
}
if scanner.Err() == io.EOF {
break
}
resource.Data = append(
resource.Data,
line...,
)
resource.Data = append(
resource.Data,
newline,
)
}
for _, resource := range resources {
err := yaml.Unmarshal(resource.Data, resource)
if err != nil {
return nil, err
}
}
return resources, nil
}
// ParseRawString parses the multi-document yaml from s
// and returns a slice of raw resources.
func ParseRawString(s string) ([]*RawResource, error) {
return ParseRaw(
strings.NewReader(s),
)
}
// ParseRawBytes parses the multi-document yaml from b
// and returns a slice of raw resources.
func ParseRawBytes(b []byte) ([]*RawResource, error) {
return ParseRaw(
bytes.NewReader(b),
)
}
// ParseRawFile parses the multi-document yaml from path p
// and returns a slice of raw resources.
func ParseRawFile(p string) ([]*RawResource, error) {
f, err := os.Open(p)
if err != nil {
return nil, err
}
defer f.Close()
return ParseRaw(f)
}
func isSeparator(s string) bool {
return strings.HasPrefix(s, "---")
}
func isTerminator(s string) bool {
return strings.HasPrefix(s, "...")
}

@ -0,0 +1,29 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
import (
"encoding/json"
"io/ioutil"
"github.com/google/go-cmp/cmp"
)
func diff(file string) (string, error) {
a, err := ParseFile(file)
if err != nil {
return "", err
}
d, err := ioutil.ReadFile(file + ".golden")
if err != nil {
return "", err
}
b := new(Manifest)
err = json.Unmarshal(d, b)
if err != nil {
return "", err
}
return cmp.Diff(a, b), nil
}

@ -0,0 +1,13 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
// Platform defines the target platform.
type Platform struct {
OS string `json:"os,omitempty"`
Arch string `json:"arch,omitempty"`
Variant string `json:"variant,omitempty"`
Version string `json:"version,omitempty"`
}

@ -0,0 +1,70 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
import (
"errors"
"github.com/buildkite/yaml"
)
var _ Resource = (*Secret)(nil)
type (
// Secret is a resource that provides encrypted data
// and pointers to external data (i.e. from vault).
Secret struct {
Version string `json:"version,omitempty"`
Kind string `json:"kind,omitempty"`
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
Data string `json:"data,omitempty"`
Get SecretGet `json:"get,omitempty"`
}
// SecretGet defines a request to get a secret from
// an external sevice at the specified path, or with the
// specified name.
SecretGet struct {
Path string `json:"path,omitempty"`
Name string `json:"name,omitempty"`
Key string `json:"key,omitempty"`
}
)
func init() {
Register(secretFunc)
}
func secretFunc(r *RawResource) (Resource, bool, error) {
if r.Kind != KindSecret {
return nil, false, nil
}
out := new(Secret)
err := yaml.Unmarshal(r.Data, out)
return out, true, err
}
// GetVersion returns the resource version.
func (s *Secret) GetVersion() string { return s.Version }
// GetKind returns the resource kind.
func (s *Secret) GetKind() string { return s.Kind }
// GetType returns the resource type.
func (s *Secret) GetType() string { return s.Type }
// GetName returns the resource name.
func (s *Secret) GetName() string { return s.Name }
// Validate returns an error if the secret is invalid.
func (s *Secret) Validate() error {
if len(s.Data) == 0 &&
len(s.Get.Path) == 0 &&
len(s.Get.Name) == 0 {
return errors.New("yaml: invalid secret resource")
}
return nil
}

@ -0,0 +1,58 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
import (
"testing"
"github.com/buildkite/yaml"
"github.com/google/go-cmp/cmp"
)
var mockSecretYaml = []byte(`
---
kind: secret
name: username
data: b2N0b2NhdA==
`)
var mockSecret = &Secret{
Kind: "secret",
Name: "username",
Data: "b2N0b2NhdA==",
}
func TestSecretUnmarshal(t *testing.T) {
a := new(Secret)
b := mockSecret
yaml.Unmarshal(mockSecretYaml, a)
if diff := cmp.Diff(a, b); diff != "" {
t.Error("Failed to parse secret")
t.Log(diff)
}
}
func TestSecretValidate(t *testing.T) {
secret := new(Secret)
secret.Data = "some-data"
if err := secret.Validate(); err != nil {
t.Error(err)
return
}
secret.Get.Path = "secret/data/docker"
if err := secret.Validate(); err != nil {
t.Error(err)
return
}
secret.Data = ""
secret.Get.Path = ""
if err := secret.Validate(); err == nil {
t.Errorf("Expect invalid secret error")
}
}

@ -0,0 +1,60 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
import (
"errors"
"github.com/buildkite/yaml"
)
var _ Resource = (*Signature)(nil)
type (
// Signature is a resource that provides an hmac
// signature of combined resources. This signature
// can be used to validate authenticity and prevent
// tampering.
Signature struct {
Version string `json:"version,omitempty"`
Kind string `json:"kind"`
Type string `json:"type"`
Name string `json:"name"`
Hmac string `json:"hmac"`
}
)
func init() {
Register(signatureFunc)
}
func signatureFunc(r *RawResource) (Resource, bool, error) {
if r.Kind != KindSignature {
return nil, false, nil
}
out := new(Signature)
err := yaml.Unmarshal(r.Data, out)
return out, true, err
}
// GetVersion returns the resource version.
func (s *Signature) GetVersion() string { return s.Version }
// GetKind returns the resource kind.
func (s *Signature) GetKind() string { return s.Kind }
// GetType returns the resource type.
func (s *Signature) GetType() string { return s.Type }
// GetName returns the resource name.
func (s *Signature) GetName() string { return s.Name }
// Validate returns an error if the signature is invalid.
func (s Signature) Validate() error {
if s.Hmac == "" {
return errors.New("yaml: invalid signature. missing hash")
}
return nil
}

@ -0,0 +1,46 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
import (
"testing"
"github.com/buildkite/yaml"
"github.com/google/go-cmp/cmp"
)
var mockSignatureYaml = []byte(`
---
kind: signature
hmac: N2NmYjA3ODQwNTY1ODFlY2E5MGJmOWI1NDk0NDFhMTEK
`)
var mockSignature = &Signature{
Kind: "signature",
Hmac: "N2NmYjA3ODQwNTY1ODFlY2E5MGJmOWI1NDk0NDFhMTEK",
}
func TestSignatureUnmarshal(t *testing.T) {
a := new(Signature)
b := mockSignature
yaml.Unmarshal(mockSignatureYaml, a)
if diff := cmp.Diff(a, b); diff != "" {
t.Error("Failed to parse signature")
t.Log(diff)
}
}
func TestSignatureValidate(t *testing.T) {
sig := Signature{Hmac: "1234"}
if err := sig.Validate(); err != nil {
t.Error(err)
return
}
sig.Hmac = ""
if err := sig.Validate(); err == nil {
t.Errorf("Expect invalid signature error")
}
}

@ -0,0 +1,41 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
import (
"github.com/docker/go-units"
)
// BytesSize stores a human-readable size in bytes,
// kibibytes, mebibytes, gibibytes, or tebibytes
// (eg. "44kiB", "17MiB").
type BytesSize int64
// UnmarshalYAML implements yaml unmarshalling.
func (b *BytesSize) UnmarshalYAML(unmarshal func(interface{}) error) error {
var intType int64
if err := unmarshal(&intType); err == nil {
*b = BytesSize(intType)
return nil
}
var stringType string
if err := unmarshal(&stringType); err != nil {
return err
}
intType, err := units.RAMInBytes(stringType)
if err == nil {
*b = BytesSize(intType)
}
return err
}
// String returns a human-readable size in bytes,
// kibibytes, mebibytes, gibibytes, or tebibytes
// (eg. "44kiB", "17MiB").
func (b BytesSize) String() string {
return units.BytesSize(float64(b))
}

@ -0,0 +1,50 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
import (
"testing"
"github.com/buildkite/yaml"
)
func TestBytesSize(t *testing.T) {
tests := []struct {
yaml string
size int64
text string
}{
{
yaml: "1KiB",
size: 1024,
text: "1KiB",
},
{
yaml: "100Mi",
size: 104857600,
text: "100MiB",
},
{
yaml: "1024",
size: 1024,
text: "1KiB",
},
}
for _, test := range tests {
in := []byte(test.yaml)
out := BytesSize(0)
err := yaml.Unmarshal(in, &out)
if err != nil {
t.Error(err)
return
}
if got, want := int64(out), test.size; got != want {
t.Errorf("Want byte size %d, got %d", want, got)
}
if got, want := out.String(), test.text; got != want {
t.Errorf("Want byte text %s, got %s", want, got)
}
}
}

@ -0,0 +1,10 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package manifest
// Workspace configures the project path on disk.
type Workspace struct {
Path string `json:"path,omitempty"`
}

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

@ -0,0 +1,53 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
// Package console provides a streamer that writes the pipeline
// output to stdout.
package console
import (
"context"
"io"
"os"
"github.com/drone/runner-go/pipeline"
)
var _ pipeline.Streamer = (*Console)(nil)
// Console implements a pipeline streamer that writes the
// pipeline logs to the console using os.Stdout.
type Console struct {
seq *sequence
col *sequence
tty bool
}
// New returns a new console recorder.
func New(tty bool) *Console {
return &Console{
tty: tty,
seq: new(sequence),
col: new(sequence),
}
}
// Stream returns an io.WriteCloser that prints formatted log
// lines to the console with step name, line number, and optional
// coloring.
func (s *Console) Stream(_ context.Context, _ *pipeline.State, name string) io.WriteCloser {
if s.tty {
return &pretty{
base: os.Stdout,
color: colors[s.col.next()%len(colors)],
name: name,
seq: s.seq,
}
}
return &plain{
base: os.Stdout,
name: name,
seq: s.seq,
}
}

@ -0,0 +1,37 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package console
import (
"fmt"
"io"
"strings"
)
// plain text line format with line number.
const plainf = "[%s:%d] %s\n"
type plain struct {
base io.Writer
name string
seq *sequence
}
func (w *plain) Write(b []byte) (int, error) {
for _, part := range split(b) {
fmt.Fprintf(w.base, plainf, w.name, w.seq.next(), part)
}
return len(b), nil
}
func (w *plain) Close() error {
return nil
}
func split(b []byte) []string {
s := string(b)
s = strings.TrimSuffix(s, "\n")
return strings.Split(s, "\n")
}

@ -0,0 +1,72 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package console
import (
"bytes"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestPlain(t *testing.T) {
buf := new(bytes.Buffer)
sess := New(false)
w := sess.Stream(nil, nil, "clone").(*plain)
w.base = buf
w.Write([]byte("hello\nworld"))
w.Close()
got, want := buf.String(), "[clone:1] hello\n[clone:2] world\n"
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Invalid plain text log output")
t.Log(diff)
}
}
func TestSplit(t *testing.T) {
tests := []struct {
before string
after []string
}{
{
before: "hello world",
after: []string{"hello world"},
},
{
before: "hello world\n",
after: []string{"hello world"},
},
{
before: "hello\nworld\n",
after: []string{"hello", "world"},
},
{
before: "hello\n\nworld\n",
after: []string{"hello", "", "world"},
},
{
before: "\nhello\n\nworld\n",
after: []string{"", "hello", "", "world"},
},
{
before: "\n",
after: []string{""},
},
{
before: "\n\n",
after: []string{"", ""},
},
}
for _, test := range tests {
b := []byte(test.before)
got, want := split(b), test.after
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Invalid split")
t.Log(diff)
}
}
}

@ -0,0 +1,40 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package console
import (
"fmt"
"io"
)
// pretty line format with line number and coloring.
const prettyf = "\033[%s[%s:%d]\033[0m %s\n"
// available terminal colors
var colors = []string{
"32m", // green
"33m", // yellow
"34m", // blue
"35m", // magenta
"36m", // cyan
}
type pretty struct {
base io.Writer
color string
name string
seq *sequence
}
func (w *pretty) Write(b []byte) (int, error) {
for _, part := range split(b) {
fmt.Fprintf(w.base, prettyf, w.color, w.name, w.seq.next(), part)
}
return len(b), nil
}
func (w *pretty) Close() error {
return nil
}

@ -0,0 +1,28 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package console
import (
"bytes"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestPretty(t *testing.T) {
buf := new(bytes.Buffer)
sess := New(true)
w := sess.Stream(nil, nil, "clone").(*pretty)
w.base = buf
w.Write([]byte("hello\nworld"))
w.Close()
got, want := buf.String(), "\x1b[33m[clone:1]\x1b[0m hello\n\x1b[33m[clone:2]\x1b[0m world\n"
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("Invalid plain text log output")
t.Log(diff)
}
}

@ -0,0 +1,30 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package console
import "sync"
// sequence provides a thread-safe counter.
type sequence struct {
sync.Mutex
value int
}
// next returns the next sequence value.
func (s *sequence) next() int {
s.Lock()
s.value++
i := s.value
s.Unlock()
return i
}
// curr returns the current sequence value.
func (s *sequence) curr() int {
s.Lock()
i := s.value
s.Unlock()
return i
}

@ -0,0 +1,20 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package console
import "testing"
func TestSequence(t *testing.T) {
c := new(sequence)
if got, want := c.curr(), 0; got != want {
t.Errorf("Want curr sequence value %d, got %d", want, got)
}
if got, want := c.next(), 1; got != want {
t.Errorf("Want next sequence value %d, got %d", want, got)
}
if got, want := c.curr(), 1; got != want {
t.Errorf("Want curr sequence value %d, got %d", want, got)
}
}

@ -0,0 +1,51 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package history
import (
"time"
"github.com/drone/drone-go/drone"
)
// Entry represents a history entry.
type Entry struct {
Stage *drone.Stage `json:"stage"`
Build *drone.Build `json:"build"`
Repo *drone.Repo `json:"repo"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
// ByTimestamp sorts a list of entries by timestamp
type ByTimestamp []*Entry
func (a ByTimestamp) Len() int { return len(a) }
func (a ByTimestamp) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByTimestamp) Less(i, j int) bool {
return a[i].Stage.ID > a[j].Stage.ID
}
// ByStatus sorts a list of entries by status
type ByStatus []*Entry
func (a ByStatus) Len() int { return len(a) }
func (a ByStatus) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByStatus) Less(i, j int) bool {
return order(a[i].Stage) < order(a[j].Stage)
}
func order(stage *drone.Stage) int64 {
switch stage.Status {
case drone.StatusPending:
return 0
case drone.StatusRunning:
return 1
default:
return 2
}
}

@ -0,0 +1,42 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package history
import (
"sort"
"testing"
"github.com/drone/drone-go/drone"
"github.com/google/go-cmp/cmp"
)
func TestSort(t *testing.T) {
before := []*Entry{
{Stage: &drone.Stage{ID: 1, Status: drone.StatusPassing}},
{Stage: &drone.Stage{ID: 2, Status: drone.StatusPassing}},
{Stage: &drone.Stage{ID: 3, Status: drone.StatusPending}},
{Stage: &drone.Stage{ID: 4, Status: drone.StatusRunning}},
{Stage: &drone.Stage{ID: 5, Status: drone.StatusPassing}},
}
after := []*Entry{
{Stage: &drone.Stage{ID: 3, Status: drone.StatusPending}},
{Stage: &drone.Stage{ID: 4, Status: drone.StatusRunning}},
{Stage: &drone.Stage{ID: 5, Status: drone.StatusPassing}},
{Stage: &drone.Stage{ID: 2, Status: drone.StatusPassing}},
{Stage: &drone.Stage{ID: 1, Status: drone.StatusPassing}},
}
s1 := ByTimestamp(before)
s2 := ByStatus(before)
sort.Sort(s1)
sort.Sort(s2)
if diff := cmp.Diff(before, after); diff != "" {
t.Errorf("Expect entries sorted by status")
t.Log(diff)
}
}

@ -0,0 +1,98 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
// Package history implements a tracer that provides access to
// pipeline execution history.
package history
import (
"context"
"sync"
"time"
"github.com/drone/runner-go/internal"
"github.com/drone/runner-go/pipeline"
)
var _ pipeline.Reporter = (*History)(nil)
// default number of historical entries.
const defaultLimit = 25
// History tracks pending, running and complete pipeline stages
// processed by the system.
type History struct {
sync.Mutex
base pipeline.Reporter
limit int
items []*Entry
}
// New returns a new History recorder that wraps the base
// reporter.
func New(base pipeline.Reporter) *History {
return &History{base: base}
}
// ReportStage adds or updates the pipeline history.
func (h *History) ReportStage(ctx context.Context, state *pipeline.State) error {
h.Lock()
h.update(state)
h.Unlock()
return h.base.ReportStage(ctx, state)
}
// ReportStep adds or updates the pipeline history.
func (h *History) ReportStep(ctx context.Context, state *pipeline.State, name string) error {
h.Lock()
h.update(state)
h.Unlock()
return h.base.ReportStep(ctx, state, name)
}
// Entries returns a list of entries.
func (h *History) Entries() []*Entry {
h.Lock()
var entries []*Entry
for _, src := range h.items {
dst := new(Entry)
*dst = *src
entries = append(entries, dst)
}
h.Unlock()
return entries
}
// Limit returns the history limit.
func (h *History) Limit() int {
if h.limit == 0 {
return defaultLimit
}
return h.limit
}
func (h *History) update(state *pipeline.State) {
for _, v := range h.items {
if v.Stage.ID == state.Stage.ID {
v.Stage = internal.CloneStage(state.Stage)
v.Build = internal.CloneBuild(state.Build)
v.Repo = internal.CloneRepo(state.Repo)
v.Updated = time.Now().UTC()
return
}
}
h.items = append(h.items, &Entry{
Stage: internal.CloneStage(state.Stage),
Build: internal.CloneBuild(state.Build),
Repo: internal.CloneRepo(state.Repo),
Created: time.Now(),
Updated: time.Now(),
})
}
func (h *History) prune() {
if len(h.items) > h.Limit() {
h.items = h.items[:h.Limit()-1]
}
}

@ -0,0 +1,198 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package history
import (
"context"
"testing"
"github.com/drone/drone-go/drone"
"github.com/drone/runner-go/pipeline"
"github.com/google/go-cmp/cmp"
)
func TestReportStage(t *testing.T) {
r := &drone.Repo{ID: 1}
b := &drone.Build{ID: 2, Params: map[string]string{}}
s := &drone.Stage{ID: 3, Labels: map[string]string{}}
v := New(&nopReporter{})
v.ReportStage(nil, &pipeline.State{
Repo: r,
Build: b,
Stage: s,
})
if v.items[0].Repo == r {
t.Errorf("Expect copy of repository")
}
if v.items[0].Build == b {
t.Errorf("Expect copy of build")
}
if v.items[0].Stage == s {
t.Errorf("Expect copy of stage")
}
if diff := cmp.Diff(v.items[0].Repo, r); diff != "" {
t.Errorf("Expect repository data copied")
t.Log(diff)
}
if diff := cmp.Diff(v.items[0].Build, b); diff != "" {
t.Errorf("Expect build data copied")
t.Log(diff)
}
if diff := cmp.Diff(v.items[0].Stage, s); diff != "" {
t.Errorf("Expect stage data copied")
t.Log(diff)
}
if v.items[0].Updated.IsZero() {
t.Errorf("Expect created timestamp non-zero")
}
if v.items[0].Created.IsZero() {
t.Errorf("Expect updated timestamp non-zero")
}
}
func TestReportStep(t *testing.T) {
r := &drone.Repo{ID: 1}
b := &drone.Build{ID: 2, Params: map[string]string{}}
s := &drone.Stage{ID: 3, Labels: map[string]string{}}
v := New(&nopReporter{})
v.ReportStep(nil, &pipeline.State{
Repo: r,
Build: b,
Stage: s,
}, "foo")
if v.items[0].Repo == r {
t.Errorf("Expect copy of repository")
}
if v.items[0].Build == b {
t.Errorf("Expect copy of build")
}
if v.items[0].Stage == s {
t.Errorf("Expect copy of stage")
}
if diff := cmp.Diff(v.items[0].Repo, r); diff != "" {
t.Errorf("Expect repository data copied")
t.Log(diff)
}
if diff := cmp.Diff(v.items[0].Build, b); diff != "" {
t.Errorf("Expect build data copied")
t.Log(diff)
}
if diff := cmp.Diff(v.items[0].Stage, s); diff != "" {
t.Errorf("Expect stage data copied")
t.Log(diff)
}
if v.items[0].Updated.IsZero() {
t.Errorf("Expect created timestamp non-zero")
}
if v.items[0].Created.IsZero() {
t.Errorf("Expect updated timestamp non-zero")
}
}
func TestEntries(t *testing.T) {
s := new(drone.Stage)
v := History{}
v.items = append(v.items, &Entry{Stage: s})
list := v.Entries()
if got, want := len(list), len(v.items); got != want {
t.Errorf("Want %d entries, got %d", want, got)
}
if v.items[0] == list[0] {
t.Errorf("Expect copy of Entry, got reference")
}
if v.items[0].Stage != list[0].Stage {
t.Errorf("Expect reference to Stage, got copy")
}
}
func TestLimit(t *testing.T) {
v := History{}
if got, want := v.Limit(), defaultLimit; got != want {
t.Errorf("Want default limit %d, got %d", want, got)
}
v.limit = 5
if got, want := v.Limit(), 5; got != want {
t.Errorf("Want custom limit %d, got %d", want, got)
}
}
func TestInsert(t *testing.T) {
stage := &drone.Stage{ID: 42, Labels: map[string]string{}}
state := &pipeline.State{
Stage: stage,
Repo: &drone.Repo{},
Build: &drone.Build{},
}
v := History{}
v.update(state)
if v.items[0].Stage == stage {
t.Errorf("Expect stage replaced")
}
if v.items[0].Updated.IsZero() {
t.Errorf("Expect entry timestamp updated")
}
if diff := cmp.Diff(v.items[0].Stage, stage); diff != "" {
t.Errorf("Expect stage data copied")
t.Log(diff)
}
}
func TestUpdate(t *testing.T) {
stage1 := &drone.Stage{ID: 42, Labels: map[string]string{}}
stage2 := &drone.Stage{ID: 42, Labels: map[string]string{}}
state := &pipeline.State{
Stage: stage1,
Repo: &drone.Repo{},
Build: &drone.Build{},
}
v := History{}
v.items = append(v.items, &Entry{Stage: stage1})
v.update(state)
if v.items[0].Stage == stage1 {
t.Errorf("Expect stage replaced")
}
if v.items[0].Stage == stage2 {
t.Errorf("Expect stage replaced")
}
if v.items[0].Updated.IsZero() {
t.Errorf("Expect entry timestamp updated")
}
if diff := cmp.Diff(v.items[0].Stage, stage1); diff != "" {
t.Errorf("Expect stage data copied")
t.Log(diff)
}
}
func TestPrune(t *testing.T) {
v := History{}
v.limit = 3
v.items = append(v.items, nil)
v.items = append(v.items, nil)
v.items = append(v.items, nil)
v.items = append(v.items, nil)
v.items = append(v.items, nil)
v.prune()
if got, want := len(v.items), 2; got != want {
t.Errorf("Want pruned entry len %d, got %d", want, got)
}
}
type nopReporter struct{}
func (*nopReporter) ReportStage(context.Context, *pipeline.State) error { return nil }
func (*nopReporter) ReportStep(context.Context, *pipeline.State, string) error { return nil }

@ -0,0 +1,7 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
// Package pipeline defines interfaces for managing and reporting
// pipeline state.
package pipeline

@ -0,0 +1,70 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
// Package remote provides a reporter and streamer that sends the
// pipeline status and logs to the central server.
package remote
import (
"context"
"io"
"github.com/drone/runner-go/client"
"github.com/drone/runner-go/internal"
"github.com/drone/runner-go/livelog"
"github.com/drone/runner-go/pipeline"
)
var _ pipeline.Reporter = (*Remote)(nil)
var _ pipeline.Streamer = (*Remote)(nil)
// Remote implements a pipeline reporter that reports state
// changes and results to a remote server instance.
type Remote struct {
client client.Client
}
// New returns a remote reporter.
func New(client client.Client) *Remote {
return &Remote{
client: client,
}
}
// ReportStage reports the stage status.
func (s *Remote) ReportStage(ctx context.Context, state *pipeline.State) error {
state.Lock()
src := state.Stage
cpy := internal.CloneStage(src)
state.Unlock()
err := s.client.Update(ctx, cpy)
if err == nil {
state.Lock()
internal.MergeStage(cpy, src)
state.Unlock()
}
return err
}
// ReportStep reports the step status.
func (s *Remote) ReportStep(ctx context.Context, state *pipeline.State, name string) error {
src := state.Find(name)
state.Lock()
cpy := internal.CloneStep(src)
state.Unlock()
err := s.client.UpdateStep(ctx, cpy)
if err == nil {
state.Lock()
internal.MergeStep(cpy, src)
state.Unlock()
}
return err
}
// Stream returns an io.WriteCloser to stream the stdout
// and stderr of the pipeline step to the server.
func (s *Remote) Stream(ctx context.Context, state *pipeline.State, name string) io.WriteCloser {
src := state.Find(name)
return livelog.New(s.client, src.ID)
}

@ -0,0 +1,121 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package remote
import (
"context"
"testing"
"github.com/drone/drone-go/drone"
"github.com/drone/runner-go/client"
"github.com/drone/runner-go/livelog"
"github.com/drone/runner-go/pipeline"
"github.com/google/go-cmp/cmp"
)
var nocontext = context.Background()
func TestReportStep(t *testing.T) {
step := &drone.Step{Name: "clone"}
state := &pipeline.State{
Stage: &drone.Stage{
Steps: []*drone.Step{step},
},
}
c := new(mockClient)
r := New(c)
err := r.ReportStep(nocontext, state, step.Name)
if err != nil {
t.Error(err)
}
if state.Stage.Steps[0] != step {
t.Errorf("Expect step updated, not replaced")
}
after := &drone.Step{
Name: "clone",
ID: 1,
StageID: 2,
Started: 1561256080,
Stopped: 1561256090,
Version: 42,
}
if diff := cmp.Diff(after, step); diff != "" {
t.Errorf("Expect response merged with step")
t.Log(diff)
}
}
func TestReportStage(t *testing.T) {
stage := &drone.Stage{
Created: 0,
Updated: 0,
Version: 0,
}
state := &pipeline.State{
Stage: stage,
}
c := new(mockClient)
r := New(c)
err := r.ReportStage(nocontext, state)
if err != nil {
t.Error(err)
}
if state.Stage != stage {
t.Errorf("Expect stage updated, not replaced")
}
after := &drone.Stage{
Created: 1561256080,
Updated: 1561256090,
Version: 42,
}
if diff := cmp.Diff(after, state.Stage); diff != "" {
t.Errorf("Expect response merged with stage")
t.Log(diff)
}
}
func TestStream(t *testing.T) {
state := &pipeline.State{
Stage: &drone.Stage{
Steps: []*drone.Step{
{
ID: 1,
Name: "clone",
},
},
},
}
c := new(mockClient)
r := New(c)
w := r.Stream(nocontext, state, "clone")
if _, ok := w.(*livelog.Writer); !ok {
t.Errorf("Expect livelog writer")
}
}
type mockClient struct {
*client.HTTPClient
}
func (m *mockClient) Update(_ context.Context, stage *drone.Stage) error {
stage.Version = 42
stage.Created = 1561256080
stage.Updated = 1561256090
return nil
}
func (m *mockClient) UpdateStep(_ context.Context, step *drone.Step) error {
step.ID = 1
step.StageID = 2
step.Started = 1561256080
step.Stopped = 1561256090
step.Version = 42
return nil
}

@ -0,0 +1,26 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package pipeline
import "context"
// A Reporter reports the pipeline status.
type Reporter interface {
// ReportStage reports the stage status.
ReportStage(context.Context, *State) error
// ReportStep reports the named step status.
ReportStep(context.Context, *State, string) error
}
// NopReporter returns a noop reporter.
func NopReporter() Reporter {
return new(nopReporter)
}
type nopReporter struct{}
func (*nopReporter) ReportStage(context.Context, *State) error { return nil }
func (*nopReporter) ReportStep(context.Context, *State, string) error { return nil }

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

@ -0,0 +1,351 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package pipeline
import (
"sync"
"time"
"github.com/drone/drone-go/drone"
)
// State stores the pipeline state.
type State struct {
sync.Mutex
Build *drone.Build
Repo *drone.Repo
Stage *drone.Stage
System *drone.System
}
// Cancel cancels the pipeline.
func (s *State) Cancel() {
s.Lock()
s.skipall()
s.killall()
s.update()
s.Unlock()
}
// Cancelled returns true if the pipeline is cancelled.
func (s *State) Cancelled() bool {
s.Lock()
v := s.killed()
s.Unlock()
return v
}
// Fail fails the named pipeline step with error.
func (s *State) Fail(name string, err error) {
s.Lock()
v := s.find(name)
s.fail(v, err)
s.update()
s.Unlock()
}
// FailAll fails the entire pipeline.
func (s *State) FailAll(err error) {
s.Lock()
s.failAll(err)
s.skipall()
s.update()
s.Unlock()
}
// Failed returns true if the pipeline failed.
func (s *State) Failed() bool {
s.Lock()
v := s.failed()
s.Unlock()
return v
}
// Skip skips the named pipeline step.
func (s *State) Skip(name string) {
s.Lock()
v := s.find(name)
s.skip(v)
s.update()
s.Unlock()
}
// SkipAll skips all pipeilne steps.
func (s *State) SkipAll() {
s.Lock()
s.skipall()
s.update()
s.Unlock()
}
// Skipped returns true if all pipeline steps are skipped.
func (s *State) Skipped() bool {
s.Lock()
v := s.skipped()
s.Unlock()
return v
}
// Start sets the named pipeline step to started.
func (s *State) Start(name string) {
s.Lock()
v := s.find(name)
s.start(v)
s.Unlock()
}
// Finish sets the pipeline step to finished.
func (s *State) Finish(name string, code int) {
s.Lock()
v := s.find(name)
s.finish(v, code)
s.update()
s.Unlock()
}
// FinishAll finishes all pipeline steps.
func (s *State) FinishAll() {
s.Lock()
s.finishAll()
s.update()
s.Unlock()
}
// Find returns the named pipeline step.
func (s *State) Find(name string) *drone.Step {
s.Lock()
v := s.find(name)
s.Unlock()
return v
}
//
// Helper functions. INTERNAL USE ONLY
//
// helper function skips all pipeline steps.
func (s *State) skipall() {
for _, v := range s.Stage.Steps {
s.skip(v)
}
}
// helper function that updates the state of an individual step
// to indicate the step to skipped.
func (s *State) skip(v *drone.Step) {
if v.Status == drone.StatusPending {
v.Started = time.Now().Unix()
v.Stopped = time.Now().Unix()
v.Status = drone.StatusSkipped
v.ExitCode = 0
v.Error = ""
}
}
// helper function returns true if the overall pipeline is
// finished and remaining steps skipped.
func (s *State) skipped() bool {
if s.finished() == false {
return false
}
for _, v := range s.Stage.Steps {
if v.Status == drone.StatusSkipped {
return true
}
}
return false
}
// helper function kills all pipeline steps.
func (s *State) killall() {
s.Stage.Error = ""
s.Stage.ExitCode = 0
s.Stage.Status = drone.StatusKilled
s.Stage.Stopped = time.Now().Unix()
if s.Stage.Started == 0 {
s.Stage.Started = s.Stage.Stopped
}
for _, v := range s.Stage.Steps {
s.kill(v)
}
}
// helper function that updates the state of an individual step
// to indicate the step to killed.
func (s *State) kill(v *drone.Step) {
if v.Status == drone.StatusRunning {
v.Status = drone.StatusKilled
v.Stopped = time.Now().Unix()
v.ExitCode = 137
v.Error = ""
if v.Started == 0 {
v.Started = v.Stopped
}
}
}
// helper function returns true if the overall pipeline status
// is killed.
func (s *State) killed() bool {
return s.Stage.Status == drone.StatusKilled
}
// helper function that updates the state of an individual step
// to indicate the step is started.
func (s *State) start(v *drone.Step) {
if v.Status == drone.StatusPending {
v.Status = drone.StatusRunning
v.Started = time.Now().Unix()
v.Stopped = 0
v.ExitCode = 0
v.Error = ""
}
}
// helper function updates the state of an individual step
// based on the exit code.
func (s *State) finish(v *drone.Step, code int) {
switch v.Status {
case drone.StatusRunning, drone.StatusPending:
default:
return
}
v.ExitCode = code
v.Stopped = time.Now().Unix()
if v.Started == 0 {
v.Started = v.Stopped
}
switch code {
case 0, 78:
v.Status = drone.StatusPassing
default:
v.Status = drone.StatusFailing
}
}
// helper function returns true if the overall pipeline status
// is failing.
func (s *State) finished() bool {
for _, v := range s.Stage.Steps {
switch v.Status {
case drone.StatusRunning, drone.StatusPending:
return false
}
}
return true
}
// helper function finishes all pipeline steps.
func (s *State) finishAll() {
for _, v := range s.Stage.Steps {
switch v.Status {
case drone.StatusPending:
s.skip(v)
case drone.StatusRunning:
s.finish(v, 0)
}
}
switch s.Stage.Status {
case drone.StatusRunning, drone.StatusPending:
s.Stage.Stopped = time.Now().Unix()
s.Stage.Status = drone.StatusPassing
if s.Stage.Started == 0 {
s.Stage.Started = s.Stage.Stopped
}
if s.failed() {
s.Stage.Status = drone.StatusFailing
}
default:
if s.Stage.Started == 0 {
s.Stage.Started = time.Now().Unix()
}
if s.Stage.Stopped == 0 {
s.Stage.Stopped = time.Now().Unix()
}
}
}
// helper function fails an individual step.
func (s *State) fail(v *drone.Step, err error) {
v.Status = drone.StatusError
v.Error = err.Error()
v.ExitCode = 255
v.Stopped = time.Now().Unix()
if v.Started == 0 {
v.Started = v.Stopped
}
}
// helper function fails the overall pipeline.
func (s *State) failAll(err error) {
switch s.Stage.Status {
case drone.StatusPending,
drone.StatusRunning:
s.Stage.Status = drone.StatusError
s.Stage.Error = err.Error()
s.Stage.Stopped = time.Now().Unix()
if s.Stage.Started == 0 {
s.Stage.Started = s.Stage.Stopped
}
}
}
// helper function returns true if the overall pipeline status
// is failing.
func (s *State) failed() bool {
switch s.Stage.Status {
case drone.StatusFailing,
drone.StatusError,
drone.StatusKilled:
return true
}
for _, v := range s.Stage.Steps {
if v.ErrIgnore {
continue
}
switch v.Status {
case drone.StatusFailing,
drone.StatusError,
drone.StatusKilled:
return true
}
}
return false
}
// helper function updates the build and stage based on the
// aggregate
func (s *State) update() {
for _, v := range s.Stage.Steps {
switch v.Status {
case drone.StatusKilled:
s.Stage.ExitCode = 137
s.Stage.Status = drone.StatusKilled
s.Build.Status = drone.StatusKilled
return
case drone.StatusError:
s.Stage.Status = drone.StatusError
s.Build.Status = drone.StatusError
return
case drone.StatusFailing:
if v.ErrIgnore == false {
s.Stage.Status = drone.StatusFailing
s.Build.Status = drone.StatusFailing
return
}
}
}
}
// helper function finds the step by name.
func (s *State) find(name string) *drone.Step {
for _, step := range s.Stage.Steps {
if step.Name == name {
return step
}
}
panic("step not found: " + name)
}

@ -0,0 +1,176 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package pipeline
import (
"errors"
"testing"
"github.com/drone/drone-go/drone"
)
func TestStateKill(t *testing.T) {
step := &drone.Step{Name: "clone", Status: drone.StatusPending}
state := &State{
Stage: &drone.Stage{
Steps: []*drone.Step{step},
},
}
state.kill(step)
if got, want := step.Status, drone.StatusPending; got != want {
t.Errorf("Expect a non-running step cannot be killed")
}
step.Status = drone.StatusRunning
state.kill(step)
if got, want := step.Status, drone.StatusKilled; got != want {
t.Errorf("Want status %s, got %s", want, got)
}
if got, want := step.Error, ""; got != want {
t.Errorf("Want error %q, got %q", want, got)
}
if got, want := step.ExitCode, 137; got != want {
t.Errorf("Want exit code %d, got %d", want, got)
}
if got, want := step.Stopped, step.Started; got != want {
t.Errorf("Want stopped %d, got %d", want, got)
}
if step.Started == 0 {
t.Errorf("Expect step started is non-zero value")
}
}
func TestStateKilled(t *testing.T) {
state := &State{}
state.Stage = &drone.Stage{Status: drone.StatusError}
if state.killed() == true {
t.Errorf("Expect killed false, got true")
}
state.Stage.Status = drone.StatusKilled
if state.killed() == false {
t.Errorf("Expect killed true, got false")
}
}
func TestStateFinished(t *testing.T) {
step := &drone.Step{}
state := &State{
Stage: &drone.Stage{
Steps: []*drone.Step{step},
},
}
step.Status = drone.StatusRunning
if state.finished() == true {
t.Errorf("Expect finished false")
}
step.Status = drone.StatusPending
if state.finished() == true {
t.Errorf("Expect finished false")
}
step.Status = drone.StatusKilled
if state.finished() == false {
t.Errorf("Expect finished true")
}
}
func TestStateSkipped(t *testing.T) {
state := &State{
Stage: &drone.Stage{
Steps: []*drone.Step{
{Status: drone.StatusPassing},
{Status: drone.StatusRunning},
},
},
}
if state.skipped() == true {
t.Errorf("Expect skipped false")
}
state.Stage.Steps[1].Status = drone.StatusPassing
if state.skipped() == true {
t.Errorf("Expect skipped false")
}
state.Stage.Steps[1].Status = drone.StatusSkipped
if state.skipped() == false {
t.Errorf("Expect skipped true")
}
}
func TestStateStarted(t *testing.T) {
step := &drone.Step{Name: "clone", Status: drone.StatusPending}
state := &State{
Stage: &drone.Stage{
Steps: []*drone.Step{step},
},
}
state.start(step)
if got, want := step.Status, drone.StatusRunning; got != want {
t.Errorf("Want status %s, got %s", want, got)
}
if got, want := step.Error, ""; got != want {
t.Errorf("Want error %q, got %q", want, got)
}
if got, want := step.ExitCode, 0; got != want {
t.Errorf("Want exit code %d, got %d", want, got)
}
if got, want := step.Stopped, int64(0); got != want {
t.Errorf("Want stopped %d, got %d", want, got)
}
if step.Started == 0 {
t.Errorf("Expect step started is non-zero value")
}
}
func TestStateFinish(t *testing.T) {
t.Skip()
}
func TestStateFail(t *testing.T) {
step := &drone.Step{Name: "clone"}
state := &State{
Stage: &drone.Stage{
Steps: []*drone.Step{step},
},
}
state.fail(step, errors.New("this is an error"))
if got, want := step.Status, drone.StatusError; got != want {
t.Errorf("Want status %s, got %s", want, got)
}
if got, want := step.Error, "this is an error"; got != want {
t.Errorf("Want error %q, got %q", want, got)
}
if got, want := step.ExitCode, 255; got != want {
t.Errorf("Want exit code %d, got %d", want, got)
}
if got, want := step.Stopped, step.Started; got != want {
t.Errorf("Want started %d, got %d", want, got)
}
if step.Stopped == 0 {
t.Errorf("Expect step stopped is non-zero value")
}
}
func TestStateFind(t *testing.T) {
step := &drone.Step{Name: "clone"}
state := &State{
Stage: &drone.Stage{
Steps: []*drone.Step{step},
},
}
if got, want := state.find("clone"), step; got != want {
t.Errorf("Expect find returns the named step")
}
defer func() {
if r := recover(); r == nil {
t.Errorf("Expect recover from panic")
}
}()
state.find("test")
}

@ -0,0 +1,33 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package pipeline
import (
"context"
"io"
)
// A Streamer streams the pipeline logs.
type Streamer interface {
// Stream returns an io.WriteCloser to stream the stdout
// and stderr of the pipeline step.
Stream(context.Context, *State, string) io.WriteCloser
}
// NopStreamer returns a noop streamer.
func NopStreamer() Streamer {
return new(nopStreamer)
}
type nopStreamer struct{}
func (*nopStreamer) Stream(context.Context, *State, string) io.WriteCloser {
return new(nopWriteCloser)
}
type nopWriteCloser struct{}
func (*nopWriteCloser) Close() error { return nil }
func (*nopWriteCloser) Write(p []byte) (int, error) { return len(p), nil }

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

@ -0,0 +1,42 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Parity Public License
// that can be found in the LICENSE file.
package secret
import (
"context"
"github.com/drone/drone-go/drone"
)
// Combine returns a new combined secret provider, capable of
// sourcing secrets from multiple providers.
func Combine(sources ...Provider) Provider {
return &combined{sources}
}
type combined struct {
sources []Provider
}
func (p *combined) Find(ctx context.Context, in *Request) (*drone.Secret, error) {
for _, source := range p.sources {
secret, err := source.Find(ctx, in)
if err != nil {
return nil, err
}
if secret == nil {
continue
}
// if the secret object is not nil, but is empty
// we should assume the secret service returned a
// 204 no content, and proceed to the next service
// in the chain.
if secret.Data == "" {
continue
}
return secret, nil
}
return nil, nil
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save