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")
+ }
+ }
+}