initial commit
commit
102fd9ab4f
@ -0,0 +1,9 @@
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: golang:1.12
|
||||
commands:
|
||||
- go test ./...
|
||||
|
@ -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…
Reference in New Issue