diff --git a/environ/environ.go b/environ/environ.go index 37d1c9d..30d88d5 100644 --- a/environ/environ.go +++ b/environ/environ.go @@ -34,7 +34,7 @@ func System(system *drone.System) map[string]string { } // Repo returns a set of environment variables containing -// repostiory metadata. +// repository metadata. func Repo(repo *drone.Repo) map[string]string { return map[string]string{ "DRONE_REPO": repo.Slug, diff --git a/environ/provider/combine.go b/environ/provider/combine.go new file mode 100644 index 0000000..94521fa --- /dev/null +++ b/environ/provider/combine.go @@ -0,0 +1,36 @@ +// Copyright 2019 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by the Polyform License +// that can be found in the LICENSE file. + +package provider + +import ( + "context" + + "github.com/drone/runner-go/environ" +) + +// Combine returns a new combined environment provider, +// capable of sourcing environment variables from multiple +// providers. +func Combine(sources ...Provider) Provider { + return &combined{sources} +} + +type combined struct { + sources []Provider +} + +func (p *combined) List(ctx context.Context, in *Request) (map[string]string, error) { + out := map[string]string{} + for _, source := range p.sources { + got, err := source.List(ctx, in) + if err != nil { + return nil, err + } + if got != nil { + out = environ.Combine(got, out) + } + } + return out, nil +} diff --git a/environ/provider/combine_test.go b/environ/provider/combine_test.go new file mode 100644 index 0000000..0497223 --- /dev/null +++ b/environ/provider/combine_test.go @@ -0,0 +1,43 @@ +// Copyright 2019 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by the Polyform License +// that can be found in the LICENSE file. + +package provider + +import ( + "errors" + "testing" +) + +func TestCombine(t *testing.T) { + a := map[string]string{"a": "b"} + b := map[string]string{"c": "d"} + aa := Static(a) + bb := Static(b) + p := Combine(aa, bb) + out, err := p.List(noContext, nil) + if err != nil { + t.Error(err) + return + } + if len(out) != 2 { + t.Errorf("Expect combined variable output") + return + } + if out["a"] != "b" { + t.Errorf("Missing variable") + } + if out["c"] != "d" { + t.Errorf("Missing variable") + } +} + +func TestCombineError(t *testing.T) { + e := errors.New("not found") + m := mockProvider{err: e} + p := Combine(&m) + _, err := p.List(noContext, nil) + if err != e { + t.Errorf("Expect error") + } +} diff --git a/environ/provider/external.go b/environ/provider/external.go new file mode 100644 index 0000000..95f6f11 --- /dev/null +++ b/environ/provider/external.go @@ -0,0 +1,65 @@ +// Copyright 2019 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by the Polyform License +// that can be found in the LICENSE file. + +package provider + +import ( + "context" + "time" + + "github.com/drone/drone-go/plugin/environ" + "github.com/drone/runner-go/logger" +) + +// External returns a new external environment variable +// provider. This provider makes an external API call to +// list and return environment variables. +func External(endpoint, token string, insecure bool) Provider { + provider := &external{} + if endpoint != "" { + provider.client = environ.Client(endpoint, token, insecure) + } + return provider +} + +type external struct { + client environ.Plugin +} + +func (p *external) List(ctx context.Context, in *Request) (map[string]string, error) { + if p.client == nil { + return nil, nil + } + + logger := logger.FromContext(ctx) + + // include a timeout to prevent an API call from + // hanging the build process indefinitely. The + // external service must return a request within + // one minute. + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + + req := &environ.Request{ + Repo: *in.Repo, + Build: *in.Build, + } + res, err := p.client.List(ctx, req) + if err != nil { + logger.WithError(err).Debug("environment: external: cannot get environment variable list") + return nil, err + } + + // if no error is returned and the list is empty, + // this indicates the client returned No Content, + // and we should exit with no credentials, but no error. + if len(res) == 0 { + logger.Trace("environment: external: environment variable list is empty") + return nil, nil + } + + logger.Trace("environment: external: environment variable list returned") + + return res, nil +} diff --git a/environ/provider/external_test.go b/environ/provider/external_test.go new file mode 100644 index 0000000..d88db6e --- /dev/null +++ b/environ/provider/external_test.go @@ -0,0 +1,90 @@ +// Copyright 2019 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by the Polyform License +// that can be found in the LICENSE file. + +package provider + +import ( + "context" + "errors" + "testing" + + "github.com/drone/drone-go/drone" + "github.com/drone/drone-go/plugin/environ" + "github.com/google/go-cmp/cmp" +) + +func TestExternal(t *testing.T) { + req := &Request{ + Build: &drone.Build{Event: drone.EventPush}, + Repo: &drone.Repo{Private: false}, + } + want := map[string]string{"a": "b"} + provider := External("http://localhost", "secret", false) + provider.(*external).client = &mockPlugin{out: want} + got, err := provider.List(noContext, req) + if err != nil { + t.Error(err) + } + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf(diff) + } +} + +// This test verifies that if the remote API call to the +// external plugin returns an error, the provider returns the +// error to the caller. +func TestExternal_ClientError(t *testing.T) { + req := &Request{ + Build: &drone.Build{Event: drone.EventPush}, + Repo: &drone.Repo{Private: false}, + } + want := errors.New("Not Found") + provider := External("http://localhost", "secret", false) + provider.(*external).client = &mockPlugin{err: want} + _, got := provider.List(noContext, req) + if got != want { + t.Errorf("Want error %s, got %s", want, got) + } +} + +// This test verifies that if no endpoint is configured the +// provider exits immediately and returns a nil slice and nil +// error. +func TestExternal_NoEndpoint(t *testing.T) { + provider := External("", "", false) + res, err := provider.List(noContext, nil) + if err != nil { + t.Errorf("Expect nil error, provider disabled") + } + if res != nil { + t.Errorf("Expect nil secret, provider disabled") + } +} + +// This test verifies that nil credentials and a nil error +// are returned if the registry endpoint returns no content. +func TestExternal_NotFound(t *testing.T) { + req := &Request{ + Repo: &drone.Repo{}, + Build: &drone.Build{}, + } + provider := External("http://localhost", "secret", false) + provider.(*external).client = &mockPlugin{} + res, err := provider.List(noContext, req) + if err != nil { + t.Errorf("Expect nil error, registry list empty") + } + if res != nil { + t.Errorf("Expect nil registry credentials") + } +} + +type mockPlugin struct { + out map[string]string + err error +} + +func (m *mockPlugin) List(context.Context, *environ.Request) (map[string]string, error) { + return m.out, m.err +} diff --git a/environ/provider/provider.go b/environ/provider/provider.go new file mode 100644 index 0000000..d3dbe73 --- /dev/null +++ b/environ/provider/provider.go @@ -0,0 +1,27 @@ +// Copyright 2019 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by the Polyform License +// that can be found in the LICENSE file. + +// Package provider provides environment variables to +// a pipeline. +package provider + +import ( + "context" + + "github.com/drone/drone-go/drone" +) + +// Request provides arguments for requesting a environment +// variables from an environment Provider. +type Request struct { + Repo *drone.Repo + Build *drone.Build +} + +// Provider is the interface that must be implemented by an +// environment provider. +type Provider interface { + // List returns a list of environment variables. + List(context.Context, *Request) (map[string]string, error) +} diff --git a/environ/provider/provider_test.go b/environ/provider/provider_test.go new file mode 100644 index 0000000..c440d2f --- /dev/null +++ b/environ/provider/provider_test.go @@ -0,0 +1,20 @@ +// Copyright 2019 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by the Polyform License +// that can be found in the LICENSE file. + +package provider + +import ( + "context" +) + +var noContext = context.Background() + +type mockProvider struct { + out map[string]string + err error +} + +func (p *mockProvider) List(context.Context, *Request) (map[string]string, error) { + return p.out, p.err +} diff --git a/environ/provider/static.go b/environ/provider/static.go new file mode 100644 index 0000000..ac80178 --- /dev/null +++ b/environ/provider/static.go @@ -0,0 +1,22 @@ +// Copyright 2019 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by the Polyform License +// that can be found in the LICENSE file. + +package provider + +import "context" + +// Static returns a new static environment variable provider. +// The static provider finds and returns the static list +// of static environment variables. +func Static(params map[string]string) Provider { + return &static{params} +} + +type static struct { + params map[string]string +} + +func (p *static) List(context.Context, *Request) (map[string]string, error) { + return p.params, nil +} diff --git a/environ/provider/static_test.go b/environ/provider/static_test.go new file mode 100644 index 0000000..b78e9f0 --- /dev/null +++ b/environ/provider/static_test.go @@ -0,0 +1,23 @@ +// Copyright 2019 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by the Polyform License +// that can be found in the LICENSE file. + +package provider + +import ( + "reflect" + "testing" +) + +func TestStatic(t *testing.T) { + a := map[string]string{"a": "b"} + p := Static(a) + b, err := p.List(noContext, nil) + if err != nil { + t.Error(err) + return + } + if !reflect.DeepEqual(a, b) { + t.Errorf("Unexpected environment variable output") + } +} diff --git a/go.mod b/go.mod index 08c17d7..f1d4cc1 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/buildkite/yaml v2.1.0+incompatible github.com/coreos/go-semver v0.3.0 github.com/docker/go-units v0.4.0 - github.com/drone/drone-go v1.0.5-0.20190504210458-4d6116b897ba + github.com/drone/drone-go v1.1.1-0.20191119212130-1d2e07e87e79 github.com/google/go-cmp v0.3.0 github.com/sirupsen/logrus v1.4.2 golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 diff --git a/go.sum b/go.sum index d120c0a..905cd67 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,11 @@ 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.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/drone-go v1.1.0 h1:2mritc5b7PhQWvILNyzaImZMRWVbMmmZ5Q0UDwwO7SI= +github.com/drone/drone-go v1.1.1-0.20191116205420-0558dc8f86d4 h1:3Oh/CK+UmQOJAsTdl/aLT+oUSZI5CbQgeSZjhlQ6qeY= +github.com/drone/drone-go v1.1.1-0.20191116205420-0558dc8f86d4/go.mod h1:GxyeGClYohaKNYJv/ZpsmVHtMJ7WhoT+uDaJNcDIrk4= +github.com/drone/drone-go v1.1.1-0.20191119212130-1d2e07e87e79 h1:jW+dJ8HrZ1CbazlsYoriOOCQnVJ2NkfNczLHs6UMU6I= +github.com/drone/drone-go v1.1.1-0.20191119212130-1d2e07e87e79/go.mod h1:GxyeGClYohaKNYJv/ZpsmVHtMJ7WhoT+uDaJNcDIrk4= 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=