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/BUILD.bazel b/cloud/bmaas/bmdb/reflection/BUILD.bazel
index f685b98..a324dab 100644
--- a/cloud/bmaas/bmdb/reflection/BUILD.bazel
+++ b/cloud/bmaas/bmdb/reflection/BUILD.bazel
@@ -9,8 +9,12 @@
     importpath = "source.monogon.dev/cloud/bmaas/bmdb/reflection",
     visibility = ["//visibility:public"],
     deps = [
+        "//cloud/bmaas/server/api",
         "@com_github_google_uuid//:uuid",
         "@com_github_iancoleman_strcase//:strcase",
         "@io_k8s_klog_v2//:klog",
+        "@org_golang_google_protobuf//encoding/prototext",
+        "@org_golang_google_protobuf//proto",
+        "@org_golang_google_protobuf//reflect/protoreflect",
     ],
 )
diff --git a/cloud/bmaas/bmdb/reflection/reflection.go b/cloud/bmaas/bmdb/reflection/reflection.go
index 1e2a414..6a45d27 100644
--- a/cloud/bmaas/bmdb/reflection/reflection.go
+++ b/cloud/bmaas/bmdb/reflection/reflection.go
@@ -17,7 +17,11 @@
 	"strings"
 	"time"
 
+	"k8s.io/klog/v2"
+
 	"github.com/google/uuid"
+	"google.golang.org/protobuf/encoding/prototext"
+	"google.golang.org/protobuf/proto"
 )
 
 // GetMachinesOpts influences the behaviour of GetMachines.
@@ -321,12 +325,19 @@
 	text  *string
 	bytes *[]byte
 	time  *time.Time
+	proto proto.Message
 }
 
 // HumanValue returns a human-readable (best effort) representation of the field
 // value.
 func (r *TagField) HumanValue() string {
 	switch {
+	case r.proto != nil:
+		opts := prototext.MarshalOptions{
+			Multiline: true,
+			Indent:    "\t",
+		}
+		return opts.Format(r.proto)
 	case r.text != nil:
 		return *r.text
 	case r.bytes != nil:
@@ -385,6 +396,16 @@
 		copied := make([]byte, len(src2))
 		copy(copied[:], src2)
 		r.bytes = &copied
+
+		if r.Type.ProtoType != nil {
+			msg := r.Type.ProtoType.New().Interface()
+			err := proto.Unmarshal(*r.bytes, msg)
+			if err != nil {
+				klog.Warningf("Could not unmarshal %s: %v", r.Type.NativeName, err)
+			} else {
+				r.proto = msg
+			}
+		}
 	case "USER-DEFINED":
 		switch r.Type.NativeUDTName {
 		case "provider":
diff --git a/cloud/bmaas/bmdb/reflection/schema.go b/cloud/bmaas/bmdb/reflection/schema.go
index a29c16b..b869f8a 100644
--- a/cloud/bmaas/bmdb/reflection/schema.go
+++ b/cloud/bmaas/bmdb/reflection/schema.go
@@ -7,7 +7,11 @@
 	"strings"
 
 	"github.com/iancoleman/strcase"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/reflect/protoreflect"
 	"k8s.io/klog/v2"
+
+	"source.monogon.dev/cloud/bmaas/server/api"
 )
 
 // Schema contains information about the tag types in a BMDB. It also contains an
@@ -60,11 +64,27 @@
 	// NativeUDTName is the CockroachDB user-defined-type name of this field. This is
 	// only valid if NativeType is 'USER-DEFINED'.
 	NativeUDTName string
+	// ProtoType is set non-nil if the field is a serialized protobuf of the same
+	// type as the given protoreflect.Message.
+	ProtoType protoreflect.Message
+}
+
+// knownProtoFields is a mapping from column name of a field containing a
+// serialized protobuf to an instance of a proto.Message that will be used to
+// parse that column's data.
+//
+// Just mapping from column name is fine enough for now as we have mostly unique
+// column names, and these column names uniquely map to a single type.
+var knownProtoFields = map[string]proto.Message{
+	"hardware_report_raw": &api.AgentHardwareReport{},
 }
 
 // HumanType returns a human-readable representation of the field's type. This is
 // not well-defined, and should be used only informatively.
 func (r *TagFieldType) HumanType() string {
+	if r.ProtoType != nil {
+		return fmt.Sprintf("%s", r.ProtoType.Descriptor().FullName())
+	}
 	switch r.NativeType {
 	case "USER-DEFINED":
 		return r.NativeUDTName
@@ -134,11 +154,15 @@
 				foundMachineID = true
 				continue
 			}
-			tag.Fields = append(tag.Fields, TagFieldType{
+			field := TagFieldType{
 				NativeName:    column_name,
 				NativeType:    data_type,
 				NativeUDTName: udt_name,
-			})
+			}
+			if t, ok := knownProtoFields[column_name]; ok {
+				field.ProtoType = t.ProtoReflect()
+			}
+			tag.Fields = append(tag.Fields, field)
 		}
 
 		// Make sure there's a machine_id key in the table, then remove it.