cloud/bmaas: implement BMDB reflection

This is the foundation for runtime introspection of BMDBs, to be used in
debug and operator tooling.

Change-Id: Id1eb0cd1dfd94c5d4dafde82448695497525e24f
Reviewed-on: https://review.monogon.dev/c/monogon/+/1131
Tested-by: Jenkins CI
Reviewed-by: Leopold Schabel <leo@monogon.tech>
diff --git a/cloud/bmaas/bmdb/reflection_test.go b/cloud/bmaas/bmdb/reflection_test.go
new file mode 100644
index 0000000..73aa397
--- /dev/null
+++ b/cloud/bmaas/bmdb/reflection_test.go
@@ -0,0 +1,209 @@
+package bmdb
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/google/uuid"
+
+	"source.monogon.dev/cloud/bmaas/bmdb/model"
+	"source.monogon.dev/cloud/bmaas/bmdb/reflection"
+)
+
+// TestReflection exercises the BMDB reflection schema reflection and data
+// retrieval code. Ideally this code would live in //cloud/bmaas/bmdb/reflection,
+// but due to namespacing issues it lives here.
+func TestReflection(t *testing.T) {
+	b := dut()
+	conn, err := b.Open(true)
+	if err != nil {
+		t.Fatalf("Open failed: %v", err)
+	}
+
+	ctx, ctxC := context.WithCancel(context.Background())
+	defer ctxC()
+
+	sess, err := conn.StartSession(ctx)
+	if err != nil {
+		t.Fatalf("StartSession: %v", err)
+	}
+
+	// Create 10 test machines.
+	var mids []uuid.UUID
+	sess.Transact(ctx, func(q *model.Queries) error {
+		for i := 0; i < 10; i += 1 {
+			mach, err := q.NewMachine(ctx)
+			if err != nil {
+				return err
+			}
+			err = q.MachineAddProvided(ctx, model.MachineAddProvidedParams{
+				MachineID:  mach.MachineID,
+				Provider:   model.ProviderEquinix,
+				ProviderID: fmt.Sprintf("test-%d", i),
+			})
+			if err != nil {
+				return err
+			}
+			mids = append(mids, mach.MachineID)
+		}
+		return nil
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	// Start and fail work on one of the machines with an hour long backoff.
+	w, err := sess.Work(ctx, model.ProcessUnitTest1, func(q *model.Queries) ([]uuid.UUID, error) {
+		return mids[0:1], nil
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	to := time.Hour
+	w.Fail(ctx, &to, "failure test")
+
+	// On another machine, create a failure with a 1 second backoff.
+	w, err = sess.Work(ctx, model.ProcessUnitTest1, func(q *model.Queries) ([]uuid.UUID, error) {
+		return mids[1:2], nil
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	to = time.Second
+	w.Fail(ctx, &to, "failure test")
+	// Later on in the test we must wait for this backoff to actually elapse. Start
+	// counting now.
+	elapsed := time.NewTicker(to * 1)
+	defer elapsed.Stop()
+
+	// On another machine, create work and don't finish it yet.
+	_, err = sess.Work(ctx, model.ProcessUnitTest1, func(q *model.Queries) ([]uuid.UUID, error) {
+		return mids[2:3], nil
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	schema, err := conn.Reflect(ctx)
+	if err != nil {
+		t.Fatalf("ReflectTagTypes: %v", err)
+	}
+
+	// Dump all in strict mode.
+	opts := &reflection.GetMachinesOpts{
+		Strict: true,
+	}
+	res, err := schema.GetMachines(ctx, opts)
+	if err != nil {
+		t.Fatalf("Dump failed: %v", err)
+	}
+	if res.Query == "" {
+		t.Errorf("Query not set on result")
+	}
+	machines := res.Data
+	if want, got := 10, len(machines); want != got {
+		t.Fatalf("Expected %d machines in dump, got %d", want, got)
+	}
+
+	// Expect Provided tag on all machines. Do a detailed check on fields, too.
+	for _, machine := range machines {
+		tag, ok := machine.Tags["Provided"]
+		if !ok {
+			t.Errorf("No Provided tag on machine.")
+			continue
+		}
+		if want, got := "Provided", tag.Type.Name(); want != got {
+			t.Errorf("Provided tag should have type %q, got %q", want, got)
+		}
+		if provider := tag.Field("provider"); provider != nil {
+			if want, got := provider.HumanValue(), "Equinix"; want != got {
+				t.Errorf("Wanted Provided.provider value %q, got %q", want, got)
+			}
+		} else {
+			t.Errorf("Provider tag has no provider field")
+		}
+		if providerId := tag.Field("provider_id"); providerId != nil {
+			if !strings.HasPrefix(providerId.HumanValue(), "test-") {
+				t.Errorf("Unexpected provider_id value %q", providerId.HumanValue())
+			}
+		} else {
+			t.Errorf("Provider tag has no provider_id field")
+		}
+	}
+
+	// Now just dump one machine.
+	opts.FilterMachine = &mids[0]
+	res, err = schema.GetMachines(ctx, opts)
+	if err != nil {
+		t.Fatalf("Dump failed: %v", err)
+	}
+	machines = res.Data
+	if want, got := 1, len(machines); want != got {
+		t.Fatalf("Expected %d machines in dump, got %d", want, got)
+	}
+	if want, got := mids[0].String(), machines[0].ID.String(); want != got {
+		t.Fatalf("Expected machine %s, got %s", want, got)
+	}
+
+	// Now dump a machine that doesn't exist. That should just return an empty list.
+	fakeMid := uuid.New()
+	opts.FilterMachine = &fakeMid
+	res, err = schema.GetMachines(ctx, opts)
+	if err != nil {
+		t.Fatalf("Dump failed: %v", err)
+	}
+	machines = res.Data
+	if want, got := 0, len(machines); want != got {
+		t.Fatalf("Expected %d machines in dump, got %d", want, got)
+	}
+
+	// Finally, check the special case machines. The first one should have an active
+	// backoff.
+	opts.FilterMachine = &mids[0]
+	res, err = schema.GetMachines(ctx, opts)
+	if err != nil {
+		t.Errorf("Dump failed: %v", err)
+	} else {
+		machine := res.Data[0]
+		if _, ok := machine.Backoffs["UnitTest1"]; !ok {
+			t.Errorf("Expected UnitTest1 backoff on machine")
+		}
+	}
+	// The second one should have an expired backoff that shouldn't be reported in a
+	// normal call..
+	<-elapsed.C
+	opts.FilterMachine = &mids[1]
+	res, err = schema.GetMachines(ctx, opts)
+	if err != nil {
+		t.Errorf("Dump failed: %v", err)
+	} else {
+		machine := res.Data[0]
+		if _, ok := machine.Backoffs["UnitTest1"]; ok {
+			t.Errorf("Expected no UnitTest1 backoff on machine")
+		}
+	}
+	// But if we ask for expired backoffs, we should get it.
+	opts.ExpiredBackoffs = true
+	res, err = schema.GetMachines(ctx, opts)
+	if err != nil {
+		t.Errorf("Dump failed: %v", err)
+	} else {
+		machine := res.Data[0]
+		if _, ok := machine.Backoffs["UnitTest1"]; !ok {
+			t.Errorf("Expected UnitTest1 backoff on machine")
+		}
+	}
+	// Finally, the third machine should have an active Work item.
+	opts.FilterMachine = &mids[2]
+	res, err = schema.GetMachines(ctx, opts)
+	if err != nil {
+		t.Errorf("Dump failed: %v", err)
+	} else {
+		machine := res.Data[0]
+		if _, ok := machine.Work["UnitTest1"]; !ok {
+			t.Errorf("Expected UnitTest1 work item on machine")
+		}
+	}
+}