cloud/bmaa/reflection: render known protos as prototext

This extends the type and value structures of the reflection code to
support arbitrary Protobuf serialized messages. We currently identify
what message type is contained in a column by a hardcoded lookup table.

Change-Id: I31a260b7ed5582678803d27bf6ba30028cbea266
Reviewed-on: https://review.monogon.dev/c/monogon/+/1539
Reviewed-by: Leopold Schabel <leo@monogon.tech>
Tested-by: Jenkins CI
diff --git a/cloud/bmaas/bmdb/reflection_test.go b/cloud/bmaas/bmdb/reflection_test.go
index 73aa397..3d23efc 100644
--- a/cloud/bmaas/bmdb/reflection_test.go
+++ b/cloud/bmaas/bmdb/reflection_test.go
@@ -8,9 +8,12 @@
 	"time"
 
 	"github.com/google/uuid"
+	"google.golang.org/protobuf/proto"
 
+	apb "source.monogon.dev/cloud/agent/api"
 	"source.monogon.dev/cloud/bmaas/bmdb/model"
 	"source.monogon.dev/cloud/bmaas/bmdb/reflection"
+	"source.monogon.dev/cloud/bmaas/server/api"
 )
 
 // TestReflection exercises the BMDB reflection schema reflection and data
@@ -207,3 +210,76 @@
 		}
 	}
 }
+
+// TestReflectionProtoFields ensures that the basic proto field introspection
+// functionality works.
+func TestReflectionProtoFields(t *testing.T) {
+	s := dut()
+	ctx, ctxC := context.WithCancel(context.Background())
+	defer ctxC()
+
+	bmdb, err := s.Open(true)
+	if err != nil {
+		t.Fatalf("Open: %v", err)
+	}
+	sess, err := bmdb.StartSession(ctx)
+	if err != nil {
+		t.Fatalf("StartSession: %v", err)
+	}
+	var machine model.Machine
+	err = sess.Transact(ctx, func(q *model.Queries) error {
+		machine, err = q.NewMachine(ctx)
+		if err != nil {
+			return err
+		}
+
+		report := &api.AgentHardwareReport{
+			Report: &apb.Node{
+				Manufacturer:         "Charles Babbage",
+				Product:              "Analytical Engine",
+				SerialNumber:         "183701",
+				MemoryInstalledBytes: 14375,
+				MemoryUsableRatio:    1.0,
+				Cpu: []*apb.CPU{
+					{
+						Architecture:    nil,
+						HardwareThreads: 1,
+						Cores:           1,
+					},
+				},
+			},
+			Warning: []string{"something went wrong"},
+		}
+		b, _ := proto.Marshal(report)
+		return q.MachineSetHardwareReport(ctx, model.MachineSetHardwareReportParams{
+			MachineID:         machine.MachineID,
+			HardwareReportRaw: b,
+		})
+	})
+	if err != nil {
+		t.Fatalf("Failed to submit hardware report: %v", err)
+	}
+
+	schem, err := bmdb.Reflect(ctx)
+	if err != nil {
+		t.Fatalf("Failed to reflect on database: %v", err)
+	}
+
+	machines, err := schem.GetMachines(ctx, &reflection.GetMachinesOpts{FilterMachine: &machine.MachineID, Strict: true})
+	if err != nil {
+		t.Fatalf("Failed to get machine: %v", err)
+	}
+	if len(machines.Data) != 1 {
+		t.Errorf("Expected one machine, got %d", len(machines.Data))
+	} else {
+		machine := machines.Data[0]
+		ty := machine.Tags["HardwareReport"].Field("hardware_report_raw").Type.HumanType()
+		if want, got := "cloud.bmaas.server.api.AgentHardwareReport", ty; want != got {
+			t.Errorf("Mismatch in type: wanted %q, got %q", want, got)
+		}
+		v := machine.Tags["HardwareReport"].Field("hardware_report_raw").HumanValue()
+		if !strings.Contains(v, "manufacturer:") {
+			t.Errorf("Invalid serialized prototext: %s", v)
+		}
+	}
+}