cloud: split shepherd up

Change-Id: I8e386d9eaaf17543743e1e8a37a8d71426910d59
Reviewed-on: https://review.monogon.dev/c/monogon/+/2213
Reviewed-by: Serge Bazanski <serge@monogon.tech>
Tested-by: Jenkins CI
diff --git a/cloud/shepherd/provider/equinix/fakequinix_test.go b/cloud/shepherd/provider/equinix/fakequinix_test.go
new file mode 100644
index 0000000..bd0df4a
--- /dev/null
+++ b/cloud/shepherd/provider/equinix/fakequinix_test.go
@@ -0,0 +1,221 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"sync"
+
+	"github.com/google/uuid"
+	"github.com/packethost/packngo"
+)
+
+// fakequinix implements a wrapngo.Client for testing. It starts out with a
+// number of made up hardware reservations, and allows for creating devices and
+// SSH keys.
+type fakequinix struct {
+	mu sync.Mutex
+
+	pid          string
+	devices      map[string]*packngo.Device
+	reservations map[string]*packngo.HardwareReservation
+	sshKeys      map[string]*packngo.SSHKey
+	reboots      map[string]int
+}
+
+// newFakequinix makes a fakequinix with a given fake project ID and number of
+// hardware reservations to create.
+func newFakequinix(pid string, numReservations int) *fakequinix {
+	f := fakequinix{
+		pid:          pid,
+		devices:      make(map[string]*packngo.Device),
+		reservations: make(map[string]*packngo.HardwareReservation),
+		sshKeys:      make(map[string]*packngo.SSHKey),
+		reboots:      make(map[string]int),
+	}
+
+	for i := 0; i < numReservations; i++ {
+		uid := uuid.New()
+		f.reservations[uid.String()] = &packngo.HardwareReservation{
+			ID:            uid.String(),
+			ShortID:       uid.String(),
+			Provisionable: true,
+		}
+	}
+
+	return &f
+}
+
+func (f *fakequinix) notFound() error {
+	return &packngo.ErrorResponse{
+		Response: &http.Response{
+			StatusCode: http.StatusNotFound,
+		},
+	}
+}
+
+func (f *fakequinix) GetDevice(_ context.Context, pid, did string, _ *packngo.ListOptions) (*packngo.Device, error) {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+
+	val := f.devices[did]
+	if val == nil {
+		return nil, f.notFound()
+	}
+	return val, nil
+}
+
+func (f *fakequinix) ListDevices(_ context.Context, pid string) ([]packngo.Device, error) {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+
+	if pid != f.pid {
+		return nil, nil
+	}
+	var res []packngo.Device
+	for _, dev := range f.devices {
+		res = append(res, *dev)
+	}
+	return res, nil
+}
+
+func (f *fakequinix) UpdateDevice(ctx context.Context, id string, r *packngo.DeviceUpdateRequest) (*packngo.Device, error) {
+	return nil, fmt.Errorf("not implemented")
+}
+
+// MoveReservation is not implemented in fakequinix
+func (f *fakequinix) MoveReservation(_ context.Context, hardwareReservationDID, projectID string) (*packngo.HardwareReservation, error) {
+	return nil, &packngo.ErrorResponse{
+		Response: &http.Response{
+			StatusCode: http.StatusNotImplemented,
+		},
+	}
+}
+
+func (f *fakequinix) DeleteDevice(_ context.Context, id string) error {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+
+	if _, ok := f.devices[id]; !ok {
+		return f.notFound()
+	}
+
+	delete(f.devices, id)
+
+	return nil
+}
+
+func (f *fakequinix) CreateDevice(_ context.Context, request *packngo.DeviceCreateRequest) (*packngo.Device, error) {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+
+	rid := request.HardwareReservationID
+	res := f.reservations[rid]
+	if res == nil {
+		return nil, f.notFound()
+	}
+	if res.Device != nil {
+		return nil, f.notFound()
+	}
+
+	dev := &packngo.Device{
+		ID:    uuid.New().String(),
+		State: "active",
+		HardwareReservation: &packngo.HardwareReservation{
+			ID: rid,
+		},
+		Network: []*packngo.IPAddressAssignment{
+			{
+				IpAddressCommon: packngo.IpAddressCommon{
+					Public:  true,
+					Address: "1.2.3.4",
+				},
+			},
+		},
+		Facility: &packngo.Facility{
+			Code: "wad",
+		},
+		Hostname: request.Hostname,
+		OS: &packngo.OS{
+			Name: request.OS,
+			Slug: request.OS,
+		},
+	}
+	res.Device = dev
+	res.Provisionable = false
+
+	f.devices[dev.ID] = dev
+	return dev, nil
+}
+
+func (f *fakequinix) ListReservations(_ context.Context, pid string) ([]packngo.HardwareReservation, error) {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+
+	var res []packngo.HardwareReservation
+	for _, r := range f.reservations {
+		res = append(res, *r)
+	}
+
+	return res, nil
+}
+
+func (f *fakequinix) ListSSHKeys(_ context.Context) ([]packngo.SSHKey, error) {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+
+	var res []packngo.SSHKey
+	for _, key := range f.sshKeys {
+		res = append(res, *key)
+	}
+
+	return res, nil
+}
+
+func (f *fakequinix) CreateSSHKey(_ context.Context, req *packngo.SSHKeyCreateRequest) (*packngo.SSHKey, error) {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+
+	for _, k := range f.sshKeys {
+		if k.Key == req.Key {
+			return nil, f.notFound()
+		}
+		if k.Label == req.Label {
+			return nil, f.notFound()
+		}
+	}
+
+	uid := uuid.New().String()
+	f.sshKeys[uid] = &packngo.SSHKey{
+		ID:    uid,
+		Label: req.Label,
+		Key:   req.Key,
+	}
+
+	return f.sshKeys[uid], nil
+}
+
+func (f *fakequinix) UpdateSSHKey(_ context.Context, kid string, req *packngo.SSHKeyUpdateRequest) (*packngo.SSHKey, error) {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+
+	key := f.sshKeys[kid]
+	if key == nil {
+		return nil, f.notFound()
+	}
+	key.Key = *req.Key
+
+	return key, nil
+}
+
+func (f *fakequinix) RebootDevice(_ context.Context, did string) error {
+	f.mu.Lock()
+	defer f.mu.Unlock()
+
+	f.reboots[did]++
+
+	return nil
+}
+
+func (f *fakequinix) Close() {
+}