added registry plugin helpers
parent
48a0db6f4e
commit
ef1504fb90
@ -0,0 +1,97 @@
|
||||
// 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 auths
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/drone/drone-go/drone"
|
||||
)
|
||||
|
||||
// config represents the Docker client configuration,
|
||||
// typically located at ~/.docker/config.json
|
||||
type config struct {
|
||||
Auths map[string]struct {
|
||||
Auth string `json:"auth"`
|
||||
} `json:"auths"`
|
||||
}
|
||||
|
||||
// Parse parses the registry credential from the reader.
|
||||
func Parse(r io.Reader) ([]*drone.Registry, error) {
|
||||
c := new(config)
|
||||
err := json.NewDecoder(r).Decode(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var auths []*drone.Registry
|
||||
for k, v := range c.Auths {
|
||||
username, password := decode(v.Auth)
|
||||
auths = append(auths, &drone.Registry{
|
||||
Address: hostname(k),
|
||||
Username: username,
|
||||
Password: password,
|
||||
})
|
||||
}
|
||||
return auths, nil
|
||||
}
|
||||
|
||||
// ParseFile parses the registry credential file.
|
||||
func ParseFile(filepath string) ([]*drone.Registry, error) {
|
||||
f, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return Parse(f)
|
||||
}
|
||||
|
||||
// ParseString parses the registry credential file.
|
||||
func ParseString(s string) ([]*drone.Registry, error) {
|
||||
return Parse(strings.NewReader(s))
|
||||
}
|
||||
|
||||
// ParseBytes parses the registry credential file.
|
||||
func ParseBytes(b []byte) ([]*drone.Registry, error) {
|
||||
return Parse(bytes.NewReader(b))
|
||||
}
|
||||
|
||||
// encode returns the encoded credentials.
|
||||
func encode(username, password string) string {
|
||||
return base64.StdEncoding.EncodeToString(
|
||||
[]byte(username + ":" + password),
|
||||
)
|
||||
}
|
||||
|
||||
// decode returns the decoded credentials.
|
||||
func decode(s string) (username, password string) {
|
||||
d, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
parts := strings.SplitN(string(d), ":", 2)
|
||||
if len(parts) > 0 {
|
||||
username = parts[0]
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
password = parts[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// hostname returns the trimmed hostname from the
|
||||
// registry url.
|
||||
func hostname(s string) string {
|
||||
uri, _ := url.Parse(s)
|
||||
if uri.Host != "" {
|
||||
s = uri.Host
|
||||
}
|
||||
return s
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
// 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 auths
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-go/drone"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
got, err := ParseString(sample)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
want := []*drone.Registry{
|
||||
{
|
||||
Address: "index.docker.io",
|
||||
Username: "octocat",
|
||||
Password: "correct-horse-battery-staple",
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBytes(t *testing.T) {
|
||||
got, err := ParseBytes([]byte(sample))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
want := []*drone.Registry{
|
||||
{
|
||||
Address: "index.docker.io",
|
||||
Username: "octocat",
|
||||
Password: "correct-horse-battery-staple",
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseErr(t *testing.T) {
|
||||
_, err := ParseString("")
|
||||
if err == nil {
|
||||
t.Errorf("Expect unmarshal error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFile(t *testing.T) {
|
||||
got, err := ParseFile("./testdata/config.json")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
want := []*drone.Registry{
|
||||
{
|
||||
Address: "index.docker.io",
|
||||
Username: "octocat",
|
||||
Password: "correct-horse-battery-staple",
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFileErr(t *testing.T) {
|
||||
_, err := ParseFile("./testdata/x.json")
|
||||
if _, ok := err.(*os.PathError); !ok {
|
||||
t.Errorf("Expect error when file does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeDecode(t *testing.T) {
|
||||
username := "octocat"
|
||||
password := "correct-horse-battery-staple"
|
||||
|
||||
encoded := encode(username, password)
|
||||
decodedUsername, decodedPassword := decode(encoded)
|
||||
if got, want := decodedUsername, username; got != want {
|
||||
t.Errorf("Want decoded username %s, got %s", want, got)
|
||||
}
|
||||
if got, want := decodedPassword, password; got != want {
|
||||
t.Errorf("Want decoded password %s, got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeInvalid(t *testing.T) {
|
||||
username, password := decode("b2N0b2NhdDp==")
|
||||
if username != "" || password != "" {
|
||||
t.Errorf("Expect decoding error")
|
||||
}
|
||||
}
|
||||
|
||||
var sample = `{
|
||||
"auths": {
|
||||
"https://index.docker.io/v1/": {
|
||||
"auth": "b2N0b2NhdDpjb3JyZWN0LWhvcnNlLWJhdHRlcnktc3RhcGxl"
|
||||
}
|
||||
}
|
||||
}`
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"auths": {
|
||||
"https://index.docker.io/v1/": {
|
||||
"auth": "b2N0b2NhdDpjb3JyZWN0LWhvcnNlLWJhdHRlcnktc3RhcGxl"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"auths": {
|
||||
"https://gcr.io": {
|
||||
"auth": "b2N0b2NhdDpjb3JyZWN0LWhvcnNlLWJhdHRlcnktc3RhcGxl"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
// 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 registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/drone/drone-go/drone"
|
||||
)
|
||||
|
||||
// Combine returns a new combined registry provider, capable of
|
||||
// sourcing registry credentials 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) ([]*drone.Registry, error) {
|
||||
var out []*drone.Registry
|
||||
for _, source := range p.sources {
|
||||
list, err := source.List(ctx, in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, list...)
|
||||
}
|
||||
return out, nil
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
// 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 registry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-go/drone"
|
||||
)
|
||||
|
||||
func TestCombine(t *testing.T) {
|
||||
a := &drone.Registry{}
|
||||
b := &drone.Registry{}
|
||||
aa := mockProvider{out: []*drone.Registry{a}}
|
||||
bb := mockProvider{out: []*drone.Registry{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 registry output")
|
||||
return
|
||||
}
|
||||
if out[0] != a {
|
||||
t.Errorf("Unexpected registry at index 0")
|
||||
}
|
||||
if out[1] != b {
|
||||
t.Errorf("Unexpected registry at index 1")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
// 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 registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/drone/drone-go/drone"
|
||||
"github.com/drone/drone-go/plugin/registry"
|
||||
"github.com/drone/runner-go/logger"
|
||||
)
|
||||
|
||||
// External returns a new external registry credentials
|
||||
// provider. The external credentials provider makes an
|
||||
// external API call to list and return credentials.
|
||||
func External(endpoint, token string, insecure bool) Provider {
|
||||
provider := &external{}
|
||||
if endpoint != "" {
|
||||
provider.client = registry.Client(endpoint, token, insecure)
|
||||
}
|
||||
return provider
|
||||
}
|
||||
|
||||
type external struct {
|
||||
client registry.Plugin
|
||||
}
|
||||
|
||||
func (p *external) List(ctx context.Context, in *Request) ([]*drone.Registry, 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 := ®istry.Request{
|
||||
Repo: *in.Repo,
|
||||
Build: *in.Build,
|
||||
}
|
||||
res, err := p.client.List(ctx, req)
|
||||
if err != nil {
|
||||
logger.WithError(err).Debug("registry: external: cannot get credentials")
|
||||
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("registry: external: credential list is empty")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
logger.Trace("registry: external: credential list returned")
|
||||
|
||||
return res, nil
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
// 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 registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-go/drone"
|
||||
"github.com/drone/drone-go/plugin/registry"
|
||||
"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 := []*drone.Registry{
|
||||
{
|
||||
Address: "index.docker.io",
|
||||
Username: "octocat",
|
||||
Password: "correct-horse-battery-staple",
|
||||
},
|
||||
}
|
||||
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 []*drone.Registry
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mockPlugin) List(context.Context, *registry.Request) ([]*drone.Registry, error) {
|
||||
return m.out, m.err
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
// 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 registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/drone/drone-go/drone"
|
||||
"github.com/drone/runner-go/registry/auths"
|
||||
)
|
||||
|
||||
// File returns a new registry credential provider that
|
||||
// parses and returns credentials from the Docker user
|
||||
// configuration file.
|
||||
func File(path string) Provider {
|
||||
return &file{path}
|
||||
}
|
||||
|
||||
type file struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (p *file) List(context.Context, *Request) ([]*drone.Registry, error) {
|
||||
if p.path == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return auths.ParseFile(p.path)
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
// 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 registry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-go/drone"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestFile(t *testing.T) {
|
||||
p := File("auths/testdata/config.json")
|
||||
got, err := p.List(noContext, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
want := []*drone.Registry{
|
||||
{
|
||||
Address: "index.docker.io",
|
||||
Username: "octocat",
|
||||
Password: "correct-horse-battery-staple",
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileEmptyPath(t *testing.T) {
|
||||
p := File("")
|
||||
out, err := p.List(noContext, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(out) != 0 {
|
||||
t.Errorf("Expect empty registry credentials")
|
||||
}
|
||||
}
|
@ -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 registry provides registry credentials used
|
||||
// to pull private images from a registry.
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/drone/drone-go/drone"
|
||||
)
|
||||
|
||||
// Request provides arguments for requesting a secret from
|
||||
// a secret Provider.
|
||||
type Request struct {
|
||||
Repo *drone.Repo
|
||||
Build *drone.Build
|
||||
}
|
||||
|
||||
// Provider is the interface that must be implemented by a
|
||||
// registry provider.
|
||||
type Provider interface {
|
||||
// Find finds and returns a list of registry credentials.
|
||||
List(context.Context, *Request) ([]*drone.Registry, error)
|
||||
}
|
@ -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 registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/drone/drone-go/drone"
|
||||
)
|
||||
|
||||
var noContext = context.Background()
|
||||
|
||||
type mockProvider struct {
|
||||
out []*drone.Registry
|
||||
err error
|
||||
}
|
||||
|
||||
func (p *mockProvider) List(context.Context, *Request) ([]*drone.Registry, error) {
|
||||
return p.out, p.err
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
// 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 registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/drone/drone-go/drone"
|
||||
)
|
||||
|
||||
// Static returns a new static registry credential provider.
|
||||
// The static secret provider finds and returns the static list
|
||||
// of registry credentials.
|
||||
func Static(registries []*drone.Registry) Provider {
|
||||
return &static{registries}
|
||||
}
|
||||
|
||||
type static struct {
|
||||
registries []*drone.Registry
|
||||
}
|
||||
|
||||
func (p *static) List(context.Context, *Request) ([]*drone.Registry, error) {
|
||||
return p.registries, nil
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
// 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 registry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/drone/drone-go/drone"
|
||||
)
|
||||
|
||||
func TestStatic(t *testing.T) {
|
||||
a := &drone.Registry{}
|
||||
b := &drone.Registry{}
|
||||
p := Static([]*drone.Registry{a, b})
|
||||
out, err := p.List(noContext, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if len(out) != 2 {
|
||||
t.Errorf("Expect combined registry output")
|
||||
return
|
||||
}
|
||||
if out[0] != a {
|
||||
t.Errorf("Unexpected registry at index 0")
|
||||
}
|
||||
if out[1] != b {
|
||||
t.Errorf("Unexpected registry at index 1")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue