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/mini/main.go b/cloud/shepherd/mini/main.go
new file mode 100644
index 0000000..67231c0
--- /dev/null
+++ b/cloud/shepherd/mini/main.go
@@ -0,0 +1,191 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"os"
+	"strings"
+
+	"k8s.io/klog/v2"
+
+	"source.monogon.dev/cloud/bmaas/bmdb"
+	"source.monogon.dev/cloud/bmaas/bmdb/model"
+	"source.monogon.dev/cloud/bmaas/bmdb/webug"
+	"source.monogon.dev/cloud/lib/component"
+	"source.monogon.dev/cloud/shepherd"
+	"source.monogon.dev/cloud/shepherd/manager"
+	clicontext "source.monogon.dev/metropolis/cli/pkg/context"
+)
+
+type Config struct {
+	Component   component.ComponentConfig
+	BMDB        bmdb.BMDB
+	WebugConfig webug.Config
+
+	InitializerConfig manager.InitializerConfig
+	ProvisionerConfig manager.ProvisionerConfig
+	RecovererConfig   manager.RecovererConfig
+
+	SSHConfig        sshConfig
+	DeviceListSource string
+	ProviderType     model.Provider
+}
+
+// TODO(q3k): factor this out to BMDB library?
+func runtimeInfo() string {
+	hostname, _ := os.Hostname()
+	if hostname == "" {
+		hostname = "UNKNOWN"
+	}
+	return fmt.Sprintf("host %s", hostname)
+}
+
+func (c *Config) RegisterFlags() {
+	c.Component.RegisterFlags("shepherd")
+	c.BMDB.ComponentName = "shepherd-mini"
+	c.BMDB.RuntimeInfo = runtimeInfo()
+	c.BMDB.Database.RegisterFlags("bmdb")
+	c.WebugConfig.RegisterFlags()
+
+	c.InitializerConfig.RegisterFlags()
+	c.ProvisionerConfig.RegisterFlags()
+	c.RecovererConfig.RegisterFlags()
+
+	c.SSHConfig.RegisterFlags()
+	flag.StringVar(&c.DeviceListSource, "mini_device_list_url", "", "The url from where to fetch the device list. For local paths use file:// as scheme")
+	flag.Func("mini_provider", "The provider this mini shepherd should emulate. Supported values are: lumen,equinix", func(s string) error {
+		switch s {
+		case strings.ToLower(string(model.ProviderEquinix)):
+			c.ProviderType = model.ProviderEquinix
+		case strings.ToLower(string(model.ProviderLumen)):
+			c.ProviderType = model.ProviderLumen
+		default:
+			return fmt.Errorf("invalid provider name")
+		}
+		return nil
+	})
+}
+
+type deviceList []machine
+
+func (dl deviceList) asMap() map[shepherd.ProviderID]machine {
+	mm := make(map[shepherd.ProviderID]machine)
+	for _, m := range dl {
+		mm[m.ProviderID] = m
+	}
+	return mm
+}
+
+func fetchDeviceList(s string) (deviceList, error) {
+	var r io.Reader
+	u, err := url.Parse(s)
+	if err != nil {
+		return nil, fmt.Errorf("failed parsing device list url: %v", err)
+	}
+
+	if u.Scheme != "file" {
+		resp, err := http.Get(u.String())
+		if err != nil {
+			return nil, err
+		}
+		defer resp.Body.Close()
+
+		if resp.StatusCode != http.StatusOK {
+			return nil, fmt.Errorf("invalid status code: %d != %v", http.StatusOK, resp.StatusCode)
+		}
+		r = resp.Body
+	} else {
+		f, err := os.Open(u.Path)
+		if err != nil {
+			return nil, err
+		}
+		defer f.Close()
+		r = f
+	}
+
+	var d deviceList
+	dec := json.NewDecoder(r)
+	dec.DisallowUnknownFields()
+	if err := dec.Decode(&d); err != nil {
+		return nil, err
+	}
+
+	klog.Infof("Fetched device list with %d entries", len(d))
+
+	return d, nil
+}
+
+func main() {
+	var c Config
+	c.RegisterFlags()
+
+	flag.Parse()
+	if flag.NArg() > 0 {
+		klog.Exitf("unexpected positional arguments: %v", flag.Args())
+	}
+
+	registry := c.Component.PrometheusRegistry()
+	c.BMDB.EnableMetrics(registry)
+
+	ctx := clicontext.WithInterrupt(context.Background())
+	c.Component.StartPrometheus(ctx)
+
+	conn, err := c.BMDB.Open(true)
+	if err != nil {
+		klog.Exitf("Failed to open BMDB connection: %v", err)
+	}
+
+	sshClient, err := c.SSHConfig.NewClient()
+	if err != nil {
+		klog.Exitf("Failed to create SSH client: %v", err)
+	}
+
+	if c.DeviceListSource == "" {
+		klog.Exitf("-mini_device_list_source must be set")
+	}
+
+	list, err := fetchDeviceList(c.DeviceListSource)
+	if err != nil {
+		klog.Exitf("Failed to fetch device list: %v", err)
+	}
+
+	mini := &provider{
+		providerType: c.ProviderType,
+		machines:     list.asMap(),
+	}
+
+	provisioner, err := manager.NewProvisioner(mini, c.ProvisionerConfig)
+	if err != nil {
+		klog.Exitf("%v", err)
+	}
+
+	initializer, err := manager.NewInitializer(mini, sshClient, c.InitializerConfig)
+	if err != nil {
+		klog.Exitf("%v", err)
+	}
+
+	go func() {
+		err = provisioner.Run(ctx, conn)
+		if err != nil {
+			klog.Exit(err)
+		}
+	}()
+	go func() {
+		err = manager.RunControlLoop(ctx, conn, initializer)
+		if err != nil {
+			klog.Exit(err)
+		}
+	}()
+	go func() {
+		if err := c.WebugConfig.Start(ctx, conn); err != nil && err != ctx.Err() {
+			klog.Exitf("Failed to start webug: %v", err)
+		}
+	}()
+
+	<-ctx.Done()
+}