remove drone-runtime dependency [ci skip]
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
// Use of this source code is governed by the Polyform License
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package platform
|
||||
package docker
|
||||
71
internal/docker/image/image.go
Normal file
71
internal/docker/image/image.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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 image
|
||||
|
||||
import "github.com/docker/distribution/reference"
|
||||
|
||||
// Trim returns the short image name without tag.
|
||||
func Trim(name string) string {
|
||||
ref, err := reference.ParseAnyReference(name)
|
||||
if err != nil {
|
||||
return name
|
||||
}
|
||||
named, err := reference.ParseNamed(ref.String())
|
||||
if err != nil {
|
||||
return name
|
||||
}
|
||||
named = reference.TrimNamed(named)
|
||||
return reference.FamiliarName(named)
|
||||
}
|
||||
|
||||
// Expand returns the fully qualified image name.
|
||||
func Expand(name string) string {
|
||||
ref, err := reference.ParseAnyReference(name)
|
||||
if err != nil {
|
||||
return name
|
||||
}
|
||||
named, err := reference.ParseNamed(ref.String())
|
||||
if err != nil {
|
||||
return name
|
||||
}
|
||||
named = reference.TagNameOnly(named)
|
||||
return named.String()
|
||||
}
|
||||
|
||||
// Match returns true if the image name matches
|
||||
// an image in the list. Note the image tag is not used
|
||||
// in the matching logic.
|
||||
func Match(from string, to ...string) bool {
|
||||
from = Trim(from)
|
||||
for _, match := range to {
|
||||
if from == Trim(match) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MatchTag returns true if the image name matches
|
||||
// an image in the list, including the tag.
|
||||
func MatchTag(a, b string) bool {
|
||||
return Expand(a) == Expand(b)
|
||||
}
|
||||
|
||||
// MatchHostname returns true if the image hostname
|
||||
// matches the specified hostname.
|
||||
func MatchHostname(image, hostname string) bool {
|
||||
ref, err := reference.ParseAnyReference(image)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
named, err := reference.ParseNamed(ref.String())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if hostname == "index.docker.io" {
|
||||
hostname = "docker.io"
|
||||
}
|
||||
return reference.Domain(named) == hostname
|
||||
}
|
||||
299
internal/docker/image/image_test.go
Normal file
299
internal/docker/image/image_test.go
Normal file
@@ -0,0 +1,299 @@
|
||||
// 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 image
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_trimImage(t *testing.T) {
|
||||
testdata := []struct {
|
||||
from string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
from: "golang",
|
||||
want: "golang",
|
||||
},
|
||||
{
|
||||
from: "golang:latest",
|
||||
want: "golang",
|
||||
},
|
||||
{
|
||||
from: "golang:1.0.0",
|
||||
want: "golang",
|
||||
},
|
||||
{
|
||||
from: "library/golang",
|
||||
want: "golang",
|
||||
},
|
||||
{
|
||||
from: "library/golang:latest",
|
||||
want: "golang",
|
||||
},
|
||||
{
|
||||
from: "library/golang:1.0.0",
|
||||
want: "golang",
|
||||
},
|
||||
{
|
||||
from: "index.docker.io/library/golang:1.0.0",
|
||||
want: "golang",
|
||||
},
|
||||
{
|
||||
from: "docker.io/library/golang:1.0.0",
|
||||
want: "golang",
|
||||
},
|
||||
{
|
||||
from: "gcr.io/library/golang:1.0.0",
|
||||
want: "gcr.io/library/golang",
|
||||
},
|
||||
// error cases, return input unmodified
|
||||
{
|
||||
from: "foo/bar?baz:boo",
|
||||
want: "foo/bar?baz:boo",
|
||||
},
|
||||
}
|
||||
for _, test := range testdata {
|
||||
got, want := Trim(test.from), test.want
|
||||
if got != want {
|
||||
t.Errorf("Want image %q trimmed to %q, got %q", test.from, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_expandImage(t *testing.T) {
|
||||
testdata := []struct {
|
||||
from string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
from: "golang",
|
||||
want: "docker.io/library/golang:latest",
|
||||
},
|
||||
{
|
||||
from: "golang:latest",
|
||||
want: "docker.io/library/golang:latest",
|
||||
},
|
||||
{
|
||||
from: "golang:1.0.0",
|
||||
want: "docker.io/library/golang:1.0.0",
|
||||
},
|
||||
{
|
||||
from: "library/golang",
|
||||
want: "docker.io/library/golang:latest",
|
||||
},
|
||||
{
|
||||
from: "library/golang:latest",
|
||||
want: "docker.io/library/golang:latest",
|
||||
},
|
||||
{
|
||||
from: "library/golang:1.0.0",
|
||||
want: "docker.io/library/golang:1.0.0",
|
||||
},
|
||||
{
|
||||
from: "index.docker.io/library/golang:1.0.0",
|
||||
want: "docker.io/library/golang:1.0.0",
|
||||
},
|
||||
{
|
||||
from: "gcr.io/golang",
|
||||
want: "gcr.io/golang:latest",
|
||||
},
|
||||
{
|
||||
from: "gcr.io/golang:1.0.0",
|
||||
want: "gcr.io/golang:1.0.0",
|
||||
},
|
||||
// error cases, return input unmodified
|
||||
{
|
||||
from: "foo/bar?baz:boo",
|
||||
want: "foo/bar?baz:boo",
|
||||
},
|
||||
}
|
||||
for _, test := range testdata {
|
||||
got, want := Expand(test.from), test.want
|
||||
if got != want {
|
||||
t.Errorf("Want image %q expanded to %q, got %q", test.from, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_matchImage(t *testing.T) {
|
||||
testdata := []struct {
|
||||
from, to string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
from: "golang",
|
||||
to: "golang",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
from: "golang:latest",
|
||||
to: "golang",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
from: "library/golang:latest",
|
||||
to: "golang",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
from: "index.docker.io/library/golang:1.0.0",
|
||||
to: "golang",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
from: "golang",
|
||||
to: "golang:latest",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
from: "library/golang:latest",
|
||||
to: "library/golang",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
from: "gcr.io/golang",
|
||||
to: "gcr.io/golang",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
from: "gcr.io/golang:1.0.0",
|
||||
to: "gcr.io/golang",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
from: "gcr.io/golang:latest",
|
||||
to: "gcr.io/golang",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
from: "gcr.io/golang",
|
||||
to: "gcr.io/golang:latest",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
from: "golang",
|
||||
to: "library/golang",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
from: "golang",
|
||||
to: "gcr.io/project/golang",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
from: "golang",
|
||||
to: "gcr.io/library/golang",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
from: "golang",
|
||||
to: "gcr.io/golang",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, test := range testdata {
|
||||
got, want := Match(test.from, test.to), test.want
|
||||
if got != want {
|
||||
t.Errorf("Want image %q matching %q is %v", test.from, test.to, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_matchHostname(t *testing.T) {
|
||||
testdata := []struct {
|
||||
image, hostname string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
image: "golang",
|
||||
hostname: "docker.io",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
image: "golang:latest",
|
||||
hostname: "docker.io",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
image: "golang:latest",
|
||||
hostname: "index.docker.io",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
image: "library/golang:latest",
|
||||
hostname: "docker.io",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
image: "docker.io/library/golang:1.0.0",
|
||||
hostname: "docker.io",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
image: "gcr.io/golang",
|
||||
hostname: "docker.io",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
image: "gcr.io/golang:1.0.0",
|
||||
hostname: "gcr.io",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
image: "1.2.3.4:8000/golang:1.0.0",
|
||||
hostname: "1.2.3.4:8000",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
image: "*&^%",
|
||||
hostname: "1.2.3.4:8000",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, test := range testdata {
|
||||
got, want := MatchHostname(test.image, test.hostname), test.want
|
||||
if got != want {
|
||||
t.Errorf("Want image %q matching hostname %q is %v", test.image, test.hostname, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_matchTag(t *testing.T) {
|
||||
testdata := []struct {
|
||||
a, b string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
a: "golang:1.0",
|
||||
b: "golang:1.0",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
a: "golang",
|
||||
b: "golang:latest",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
a: "docker.io/library/golang",
|
||||
b: "golang:latest",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
a: "golang",
|
||||
b: "golang:1.0",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
a: "golang:1.0",
|
||||
b: "golang:2.0",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, test := range testdata {
|
||||
got, want := MatchTag(test.a, test.b), test.want
|
||||
if got != want {
|
||||
t.Errorf("Want image %q matching image tag %q is %v", test.a, test.b, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
188
internal/docker/stdcopy/stdcopy.go
Normal file
188
internal/docker/stdcopy/stdcopy.go
Normal file
@@ -0,0 +1,188 @@
|
||||
// Copyright 2018 Docker, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package stdcopy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// StdType is the type of standard stream
|
||||
// a writer can multiplex to.
|
||||
type StdType byte
|
||||
|
||||
const (
|
||||
// Stdin represents standard input stream type.
|
||||
Stdin StdType = iota
|
||||
// Stdout represents standard output stream type.
|
||||
Stdout
|
||||
// Stderr represents standard error steam type.
|
||||
Stderr
|
||||
|
||||
stdWriterPrefixLen = 8
|
||||
stdWriterFdIndex = 0
|
||||
stdWriterSizeIndex = 4
|
||||
|
||||
startingBufLen = 32*1024 + stdWriterPrefixLen + 1
|
||||
)
|
||||
|
||||
var bufPool = &sync.Pool{New: func() interface{} { return bytes.NewBuffer(nil) }}
|
||||
|
||||
// stdWriter is wrapper of io.Writer with extra customized info.
|
||||
type stdWriter struct {
|
||||
io.Writer
|
||||
prefix byte
|
||||
}
|
||||
|
||||
// Write sends the buffer to the underneath writer.
|
||||
// It inserts the prefix header before the buffer,
|
||||
// so stdcopy.StdCopy knows where to multiplex the output.
|
||||
// It makes stdWriter to implement io.Writer.
|
||||
func (w *stdWriter) Write(p []byte) (n int, err error) {
|
||||
if w == nil || w.Writer == nil {
|
||||
return 0, errors.New("Writer not instantiated")
|
||||
}
|
||||
if p == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
header := [stdWriterPrefixLen]byte{stdWriterFdIndex: w.prefix}
|
||||
binary.BigEndian.PutUint32(header[stdWriterSizeIndex:], uint32(len(p)))
|
||||
buf := bufPool.Get().(*bytes.Buffer)
|
||||
buf.Write(header[:])
|
||||
buf.Write(p)
|
||||
|
||||
n, err = w.Writer.Write(buf.Bytes())
|
||||
n -= stdWriterPrefixLen
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
bufPool.Put(buf)
|
||||
return
|
||||
}
|
||||
|
||||
// NewStdWriter instantiates a new Writer.
|
||||
// Everything written to it will be encapsulated using a custom format,
|
||||
// and written to the underlying `w` stream.
|
||||
// This allows multiple write streams (e.g. stdout and stderr) to be muxed into a single connection.
|
||||
// `t` indicates the id of the stream to encapsulate.
|
||||
// It can be stdcopy.Stdin, stdcopy.Stdout, stdcopy.Stderr.
|
||||
func NewStdWriter(w io.Writer, t StdType) io.Writer {
|
||||
return &stdWriter{
|
||||
Writer: w,
|
||||
prefix: byte(t),
|
||||
}
|
||||
}
|
||||
|
||||
// StdCopy is a modified version of io.Copy.
|
||||
//
|
||||
// StdCopy will demultiplex `src`, assuming that it contains two streams,
|
||||
// previously multiplexed together using a StdWriter instance.
|
||||
// As it reads from `src`, StdCopy will write to `dstout` and `dsterr`.
|
||||
//
|
||||
// StdCopy will read until it hits EOF on `src`. It will then return a nil error.
|
||||
// In other words: if `err` is non nil, it indicates a real underlying error.
|
||||
//
|
||||
// `written` will hold the total number of bytes written to `dstout` and `dsterr`.
|
||||
func StdCopy(dstout, dsterr io.Writer, src io.Reader) (written int64, err error) {
|
||||
var (
|
||||
buf = make([]byte, startingBufLen)
|
||||
bufLen = len(buf)
|
||||
nr, nw int
|
||||
er, ew error
|
||||
out io.Writer
|
||||
frameSize int
|
||||
)
|
||||
|
||||
for {
|
||||
// Make sure we have at least a full header
|
||||
for nr < stdWriterPrefixLen {
|
||||
var nr2 int
|
||||
nr2, er = src.Read(buf[nr:])
|
||||
nr += nr2
|
||||
if er == io.EOF {
|
||||
if nr < stdWriterPrefixLen {
|
||||
return written, nil
|
||||
}
|
||||
break
|
||||
}
|
||||
if er != nil {
|
||||
return 0, er
|
||||
}
|
||||
}
|
||||
|
||||
// Check the first byte to know where to write
|
||||
switch StdType(buf[stdWriterFdIndex]) {
|
||||
case Stdin:
|
||||
fallthrough
|
||||
case Stdout:
|
||||
// Write on stdout
|
||||
out = dstout
|
||||
case Stderr:
|
||||
// Write on stderr
|
||||
out = dsterr
|
||||
default:
|
||||
return 0, fmt.Errorf("Unrecognized input header: %d", buf[stdWriterFdIndex])
|
||||
}
|
||||
|
||||
// Retrieve the size of the frame
|
||||
frameSize = int(binary.BigEndian.Uint32(buf[stdWriterSizeIndex : stdWriterSizeIndex+4]))
|
||||
|
||||
// Check if the buffer is big enough to read the frame.
|
||||
// Extend it if necessary.
|
||||
if frameSize+stdWriterPrefixLen > bufLen {
|
||||
buf = append(buf, make([]byte, frameSize+stdWriterPrefixLen-bufLen+1)...)
|
||||
bufLen = len(buf)
|
||||
}
|
||||
|
||||
// While the amount of bytes read is less than the size of the frame + header, we keep reading
|
||||
for nr < frameSize+stdWriterPrefixLen {
|
||||
var nr2 int
|
||||
nr2, er = src.Read(buf[nr:])
|
||||
nr += nr2
|
||||
if er == io.EOF {
|
||||
if nr < frameSize+stdWriterPrefixLen {
|
||||
return written, nil
|
||||
}
|
||||
break
|
||||
}
|
||||
if er != nil {
|
||||
return 0, er
|
||||
}
|
||||
}
|
||||
|
||||
// Write the retrieved frame (without header)
|
||||
nw, ew = out.Write(buf[stdWriterPrefixLen : frameSize+stdWriterPrefixLen])
|
||||
if ew != nil {
|
||||
return 0, ew
|
||||
}
|
||||
// If the frame has not been fully written: error
|
||||
if nw != frameSize {
|
||||
return 0, io.ErrShortWrite
|
||||
}
|
||||
written += int64(nw)
|
||||
|
||||
// Move the rest of the buffer to the beginning
|
||||
copy(buf, buf[frameSize+stdWriterPrefixLen:])
|
||||
// Move the index
|
||||
nr -= frameSize + stdWriterPrefixLen
|
||||
}
|
||||
}
|
||||
56
internal/encoder/encoder.go
Normal file
56
internal/encoder/encoder.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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 encoder
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/buildkite/yaml"
|
||||
json "github.com/ghodss/yaml"
|
||||
)
|
||||
|
||||
// Encode encodes an interface value as a string. This function
|
||||
// assumes all types were unmarshaled by the yaml.v2 library.
|
||||
// The yaml.v2 package only supports a subset of primitive types.
|
||||
func Encode(v interface{}) string {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return v
|
||||
case bool:
|
||||
return strconv.FormatBool(v)
|
||||
case int:
|
||||
return strconv.Itoa(v)
|
||||
case float64:
|
||||
return strconv.FormatFloat(v, 'g', -1, 64)
|
||||
case []byte:
|
||||
return base64.StdEncoding.EncodeToString(v)
|
||||
case []interface{}:
|
||||
return encodeSlice(v)
|
||||
default:
|
||||
return encodeMap(v)
|
||||
}
|
||||
}
|
||||
|
||||
// helper function encodes a parameter in map format.
|
||||
func encodeMap(v interface{}) string {
|
||||
yml, _ := yaml.Marshal(v)
|
||||
out, _ := json.YAMLToJSON(yml)
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// helper function encodes a parameter in slice format.
|
||||
func encodeSlice(v interface{}) string {
|
||||
out, _ := yaml.Marshal(v)
|
||||
|
||||
in := []string{}
|
||||
err := yaml.Unmarshal(out, &in)
|
||||
if err == nil {
|
||||
return strings.Join(in, ",")
|
||||
}
|
||||
out, _ = json.YAMLToJSON(out)
|
||||
return string(out)
|
||||
}
|
||||
63
internal/encoder/encoder_test.go
Normal file
63
internal/encoder/encoder_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// 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 encoder
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEncode(t *testing.T) {
|
||||
testdatum := []struct {
|
||||
data interface{}
|
||||
text string
|
||||
}{
|
||||
{
|
||||
data: "foo",
|
||||
text: "foo",
|
||||
},
|
||||
{
|
||||
data: true,
|
||||
text: "true",
|
||||
},
|
||||
{
|
||||
data: 42,
|
||||
text: "42",
|
||||
},
|
||||
{
|
||||
data: float64(42.424242),
|
||||
text: "42.424242",
|
||||
},
|
||||
{
|
||||
data: []interface{}{"foo", "bar", "baz"},
|
||||
text: "foo,bar,baz",
|
||||
},
|
||||
{
|
||||
data: []interface{}{1, 1, 2, 3, 5, 8},
|
||||
text: "1,1,2,3,5,8",
|
||||
},
|
||||
{
|
||||
data: []byte("foo"),
|
||||
text: "Zm9v",
|
||||
},
|
||||
{
|
||||
data: []interface{}{
|
||||
struct {
|
||||
Name string `json:"name"`
|
||||
}{
|
||||
Name: "john",
|
||||
},
|
||||
},
|
||||
text: `[{"name":"john"}]`,
|
||||
},
|
||||
{
|
||||
data: map[interface{}]interface{}{"foo": "bar"},
|
||||
text: `{"foo":"bar"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testdata := range testdatum {
|
||||
if got, want := Encode(testdata.data), testdata.text; got != want {
|
||||
t.Errorf("Want interface{} encoded to %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
// 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 platform contains code to provision and destroy server
|
||||
// instances on the Digital Ocean cloud platform.
|
||||
package platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/drone/runner-go/logger"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type (
|
||||
// RegisterArgs provides arguments to register the SSH
|
||||
// public key with the account.
|
||||
RegisterArgs struct {
|
||||
Fingerprint string
|
||||
Name string
|
||||
Data string
|
||||
Token string
|
||||
}
|
||||
|
||||
// DestroyArgs provides arguments to destroy the server
|
||||
// instance.
|
||||
DestroyArgs struct {
|
||||
ID int
|
||||
IP string
|
||||
Token string
|
||||
}
|
||||
|
||||
// ProvisionArgs provides arguments to provision instances.
|
||||
ProvisionArgs struct {
|
||||
Key string
|
||||
Image string
|
||||
Name string
|
||||
Region string
|
||||
Size string
|
||||
Token string
|
||||
}
|
||||
|
||||
// Instance represents a provisioned server instance.
|
||||
Instance struct {
|
||||
ID int
|
||||
IP string
|
||||
}
|
||||
)
|
||||
|
||||
// Provision provisions the server instance.
|
||||
func Provision(ctx context.Context, args ProvisionArgs) (Instance, error) {
|
||||
res := Instance{}
|
||||
req := &godo.DropletCreateRequest{
|
||||
Name: args.Name,
|
||||
Region: args.Region,
|
||||
Size: args.Size,
|
||||
Tags: []string{"drone"},
|
||||
IPv6: false,
|
||||
SSHKeys: []godo.DropletCreateSSHKey{
|
||||
{Fingerprint: args.Key},
|
||||
},
|
||||
Image: godo.DropletCreateImage{
|
||||
Slug: args.Image,
|
||||
},
|
||||
}
|
||||
|
||||
logger := logger.FromContext(ctx).
|
||||
WithField("region", req.Region).
|
||||
WithField("image", req.Image.Slug).
|
||||
WithField("size", req.Size).
|
||||
WithField("name", req.Name)
|
||||
|
||||
logger.Debug("instance create")
|
||||
|
||||
client := newClient(ctx, args.Token)
|
||||
droplet, _, err := client.Droplets.Create(ctx, req)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("cannot create instance")
|
||||
return res, err
|
||||
}
|
||||
|
||||
// record the droplet ID
|
||||
res.ID = droplet.ID
|
||||
|
||||
logger.WithField("name", req.Name).
|
||||
Info("instance created")
|
||||
|
||||
// poll the digitalocean endpoint for server updates
|
||||
// and exit when a network address is allocated.
|
||||
interval := time.Duration(0)
|
||||
poller:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logger.WithField("name", req.Name).
|
||||
Debug("cannot ascertain network")
|
||||
|
||||
return res, ctx.Err()
|
||||
case <-time.After(interval):
|
||||
interval = time.Second * 30
|
||||
|
||||
logger.WithField("name", req.Name).
|
||||
Debug("find instance network")
|
||||
|
||||
droplet, _, err = client.Droplets.Get(ctx, res.ID)
|
||||
if err != nil {
|
||||
logger.WithError(err).
|
||||
Error("cannot find instance")
|
||||
return res, err
|
||||
}
|
||||
|
||||
for _, network := range droplet.Networks.V4 {
|
||||
if network.Type == "public" {
|
||||
res.IP = network.IPAddress
|
||||
}
|
||||
}
|
||||
|
||||
if res.IP != "" {
|
||||
break poller
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.WithField("name", req.Name).
|
||||
WithField("ip", res.IP).
|
||||
WithField("id", res.ID).
|
||||
Debug("instance network ready")
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Destroy destroys the server instance.
|
||||
func Destroy(ctx context.Context, args DestroyArgs) error {
|
||||
client := newClient(ctx, args.Token)
|
||||
_, err := client.Droplets.Delete(ctx, args.ID)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).
|
||||
WithError(err).
|
||||
WithField("id", args.ID).
|
||||
WithField("ip", args.IP).
|
||||
Error("cannot terminate server")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// RegisterKey registers the ssh public key with the account if
|
||||
// it is not already registered.
|
||||
func RegisterKey(ctx context.Context, args RegisterArgs) error {
|
||||
client := newClient(ctx, args.Token)
|
||||
_, _, err := client.Keys.GetByFingerprint(ctx, args.Fingerprint)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if the ssh key does not exists we attempt to register
|
||||
// with the digital ocean account.
|
||||
_, _, err = client.Keys.Create(ctx, &godo.KeyCreateRequest{
|
||||
Name: args.Name,
|
||||
PublicKey: args.Data,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// helper function returns a new docker client.
|
||||
func newClient(ctx context.Context, token string) *godo.Client {
|
||||
return godo.NewClient(
|
||||
oauth2.NewClient(ctx, oauth2.StaticTokenSource(
|
||||
&oauth2.Token{
|
||||
AccessToken: token,
|
||||
},
|
||||
)),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user