cloud/shepherd/equinix/manager: init

This adds implementation managing Equinix Metal server lifecycle as
part of the BMaaS project.

Co-authored-by: Mateusz Zalega <mateusz@monogon.tech>
Supersedes: https://review.monogon.dev/c/monogon/+/990
Change-Id: I5537b2d07763985ad27aecac544ed19f933d6727
Reviewed-on: https://review.monogon.dev/c/monogon/+/1129
Reviewed-by: Leopold Schabel <leo@monogon.tech>
Reviewed-by: Mateusz Zalega <mateusz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/cloud/shepherd/equinix/manager/server/BUILD.bazel b/cloud/shepherd/equinix/manager/server/BUILD.bazel
new file mode 100644
index 0000000..8593004
--- /dev/null
+++ b/cloud/shepherd/equinix/manager/server/BUILD.bazel
@@ -0,0 +1,22 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "server_lib",
+    srcs = ["main.go"],
+    importpath = "source.monogon.dev/cloud/shepherd/equinix/manager/server",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//cloud/bmaas/bmdb",
+        "//cloud/lib/component",
+        "//cloud/shepherd/equinix/manager",
+        "//cloud/shepherd/equinix/wrapngo",
+        "//metropolis/cli/pkg/context",
+        "@io_k8s_klog//:klog",
+    ],
+)
+
+go_binary(
+    name = "server",
+    embed = [":server_lib"],
+    visibility = ["//visibility:public"],
+)
diff --git a/cloud/shepherd/equinix/manager/server/main.go b/cloud/shepherd/equinix/manager/server/main.go
new file mode 100644
index 0000000..567ebd7
--- /dev/null
+++ b/cloud/shepherd/equinix/manager/server/main.go
@@ -0,0 +1,107 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"os"
+	"strings"
+
+	"k8s.io/klog"
+
+	"source.monogon.dev/cloud/bmaas/bmdb"
+	"source.monogon.dev/cloud/lib/component"
+	"source.monogon.dev/cloud/shepherd/equinix/manager"
+	"source.monogon.dev/cloud/shepherd/equinix/wrapngo"
+	clicontext "source.monogon.dev/metropolis/cli/pkg/context"
+)
+
+type Config struct {
+	Component component.ComponentConfig
+	BMDB      bmdb.BMDB
+
+	SharedConfig      manager.SharedConfig
+	AgentConfig       manager.AgentConfig
+	ProvisionerConfig manager.ProvisionerConfig
+	InitializerConfig manager.InitializerConfig
+	API               wrapngo.Opts
+}
+
+// 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-equinix"
+	c.BMDB.RuntimeInfo = runtimeInfo()
+	c.BMDB.Database.RegisterFlags("bmdb")
+
+	c.SharedConfig.RegisterFlags("")
+	c.AgentConfig.RegisterFlags()
+	c.ProvisionerConfig.RegisterFlags()
+	c.InitializerConfig.RegisterFlags()
+	c.API.RegisterFlags()
+}
+
+func main() {
+	c := &Config{}
+	c.RegisterFlags()
+	flag.Parse()
+
+	ctx := clicontext.WithInterrupt(context.Background())
+
+	if c.API.APIKey == "" || c.API.User == "" {
+		klog.Exitf("-equinix_api_username and -equinix_api_key must be set")
+	}
+	api := wrapngo.New(&c.API)
+
+	// These variables are _very_ important to configure correctly, otherwise someone
+	// running this locally with prod creds will actually destroy production
+	// data.
+	if strings.Contains(c.SharedConfig.KeyLabel, "FIXME") {
+		klog.Exitf("refusing to run with -equinix_ssh_key_label %q, please set it to something unique", c.SharedConfig.KeyLabel)
+	}
+	if strings.Contains(c.SharedConfig.DevicePrefix, "FIXME") {
+		klog.Exitf("refusing to run with -equinix_device_prefix %q, please set it to something unique", c.SharedConfig.DevicePrefix)
+	}
+
+	klog.Infof("Ensuring our SSH key is configured...")
+	if err := c.SharedConfig.SSHEquinixEnsure(ctx, api); err != nil {
+		klog.Exitf("Ensuring SSH key failed: %v", err)
+	}
+
+	provisioner, err := c.ProvisionerConfig.New(api, &c.SharedConfig)
+	if err != nil {
+		klog.Exitf("%v", err)
+	}
+
+	initializer, err := c.InitializerConfig.New(api, &c.SharedConfig, &c.AgentConfig)
+	if err != nil {
+		klog.Exitf("%v", err)
+	}
+
+	conn, err := c.BMDB.Open(true)
+	if err != nil {
+		klog.Exitf("Failed to open BMDB connection: %v", err)
+	}
+	go func() {
+		err = provisioner.Run(ctx, conn)
+		if err != nil {
+			klog.Exit(err)
+		}
+	}()
+	go func() {
+		err = initializer.Run(ctx, conn)
+		if err != nil {
+			klog.Exit(err)
+		}
+	}()
+
+	<-ctx.Done()
+}