cloud/shepherd/equinix: split out control loop logic

This is in preparation for implementing the recoverer/rebooter inside
the shepherd.

In the future this will likely be split away from from the shepherd and
end up as a generic bmdb library. But let's first wait for concrete
usages outside of the shepherd component.

Change-Id: I69b9a2e913dcefa2c6558e271b6853285c6120b3
Reviewed-on: https://review.monogon.dev/c/monogon/+/1559
Tested-by: Jenkins CI
Reviewed-by: Tim Windelschmidt <tim@monogon.tech>
diff --git a/cloud/shepherd/equinix/manager/initializer_test.go b/cloud/shepherd/equinix/manager/initializer_test.go
index 6e82b98..20c2f16 100644
--- a/cloud/shepherd/equinix/manager/initializer_test.go
+++ b/cloud/shepherd/equinix/manager/initializer_test.go
@@ -64,12 +64,16 @@
 	return nil
 }
 
-// TestInitializerSmokes makes sure the Initializer doesn't go up in flames on
-// the happy path.
-func TestInitializerSmokes(t *testing.T) {
-	ic := InitializerConfig{
-		DBQueryLimiter: rate.NewLimiter(rate.Every(time.Second), 10),
-	}
+type initializerDut struct {
+	f    *fakequinix
+	i    *Initializer
+	bmdb *bmdb.Connection
+	ctx  context.Context
+}
+
+func newInitializerDut(t *testing.T) *initializerDut {
+	t.Helper()
+
 	_, key, _ := ed25519.GenerateKey(rand.Reader)
 	sc := SharedConfig{
 		ProjectId:    "noproject",
@@ -77,22 +81,23 @@
 		Key:          key,
 		DevicePrefix: "test-",
 	}
-	ac := AgentConfig{
+	ic := InitializerConfig{
+		ControlLoopConfig: ControlLoopConfig{
+			DBQueryLimiter: rate.NewLimiter(rate.Every(time.Second), 10),
+		},
 		Executable:        []byte("beep boop i'm a real program"),
 		TargetPath:        "/fake/path",
 		Endpoint:          "example.com:1234",
 		SSHConnectTimeout: time.Second,
 		SSHExecTimeout:    time.Second,
 	}
+
 	f := newFakequinix(sc.ProjectId, 100)
-	i, err := ic.New(f, &sc, &ac)
+	i, err := NewInitializer(f, ic, &sc)
 	if err != nil {
 		t.Fatalf("Could not create Initializer: %v", err)
 	}
 
-	ctx, ctxC := context.WithCancel(context.Background())
-	defer ctxC()
-
 	b := bmdb.BMDB{
 		Config: bmdb.Config{
 			Database: component.CockroachConfig{
@@ -107,12 +112,32 @@
 		t.Fatalf("Could not create in-memory BMDB: %v", err)
 	}
 
+	ctx, ctxC := context.WithCancel(context.Background())
+	t.Cleanup(ctxC)
+
 	if err := sc.SSHEquinixEnsure(ctx, f); err != nil {
 		t.Fatalf("Failed to ensure SSH key: %v", err)
 	}
 
 	i.sshClient = &fakeSSHClient{}
-	go i.Run(ctx, conn)
+	go RunControlLoop(ctx, conn, i)
+
+	return &initializerDut{
+		f:    f,
+		i:    i,
+		bmdb: conn,
+		ctx:  ctx,
+	}
+}
+
+// TestInitializerSmokes makes sure the Initializer doesn't go up in flames on
+// the happy path.
+func TestInitializerSmokes(t *testing.T) {
+	dut := newInitializerDut(t)
+	f := dut.f
+	ctx := dut.ctx
+	conn := dut.bmdb
+	sc := dut.i.sharedConfig
 
 	reservations, _ := f.ListReservations(ctx, sc.ProjectId)
 	kid, err := sc.sshEquinixId(ctx, f)
@@ -161,8 +186,6 @@
 		}
 	}
 
-	go i.Run(ctx, conn)
-
 	// Expect to find 0 machines needing start.
 	for {
 		time.Sleep(100 * time.Millisecond)