178 lines
3.9 KiB
Go
178 lines
3.9 KiB
Go
// 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,
|
|
},
|
|
)),
|
|
)
|
|
}
|