cloud/b/b/reflection: add single-line HumanValue rendering, Index
This is in preparation for printing fields in bmcli.
Change-Id: I1aa178da1a50e8dd0c572a238f92daa536f9fcd9
Reviewed-on: https://review.monogon.dev/c/monogon/+/2170
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/cloud/bmaas/bmdb/reflection/BUILD.bazel b/cloud/bmaas/bmdb/reflection/BUILD.bazel
index a324dab..0922542 100644
--- a/cloud/bmaas/bmdb/reflection/BUILD.bazel
+++ b/cloud/bmaas/bmdb/reflection/BUILD.bazel
@@ -15,6 +15,8 @@
"@io_k8s_klog_v2//:klog",
"@org_golang_google_protobuf//encoding/prototext",
"@org_golang_google_protobuf//proto",
+ "@org_golang_google_protobuf//reflect/protopath",
+ "@org_golang_google_protobuf//reflect/protorange",
"@org_golang_google_protobuf//reflect/protoreflect",
],
)
diff --git a/cloud/bmaas/bmdb/reflection/reflection.go b/cloud/bmaas/bmdb/reflection/reflection.go
index 5942a30..b9f6d11 100644
--- a/cloud/bmaas/bmdb/reflection/reflection.go
+++ b/cloud/bmaas/bmdb/reflection/reflection.go
@@ -22,6 +22,8 @@
"github.com/google/uuid"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/reflect/protopath"
+ "google.golang.org/protobuf/reflect/protorange"
)
// GetMachinesOpts influences the behaviour of GetMachines.
@@ -318,6 +320,24 @@
return nil
}
+// DisplayOption is an opaque argument used to influence the display style of a
+// tag value when returned from HumanValue.
+type DisplayOption string
+
+const (
+ // DisplaySingleLine limits display to a single line (i.e. don't try to
+ // pretty-print long values by inserting newlines and indents).
+ DisplaySingleLine DisplayOption = "single-line"
+)
+
+func (r *Tag) HumanValue(opts ...DisplayOption) string {
+ var kvs []string
+ for _, field := range r.Fields {
+ kvs = append(kvs, fmt.Sprintf("%s: %s", field.Type.NativeName, field.HumanValue(opts...)))
+ }
+ return strings.Join(kvs, ", ")
+}
+
// TagField value which is part of a Tag set on a Machine.
type TagField struct {
// Type describing this field.
@@ -331,14 +351,19 @@
// HumanValue returns a human-readable (best effort) representation of the field
// value.
-func (r *TagField) HumanValue() string {
+func (r *TagField) HumanValue(opts ...DisplayOption) string {
switch {
case r.proto != nil:
- opts := prototext.MarshalOptions{
+ mopts := prototext.MarshalOptions{
Multiline: true,
Indent: "\t",
}
- return opts.Format(r.proto)
+ for _, opt := range opts {
+ if opt == DisplaySingleLine {
+ mopts.Multiline = false
+ }
+ }
+ return mopts.Format(r.proto)
case r.text != nil:
return *r.text
case r.bytes != nil:
@@ -350,6 +375,39 @@
}
}
+// Index attempts to index into a structured tag field (currently only protobuf
+// fields) by a 'field.subfield.subsubfield' selector.
+//
+// The selector for Protobuf fields follows the convention from 'protorange',
+// which is a semi-standardized format used in the Protobuf ecosystem. See
+// https://pkg.go.dev/google.golang.org/protobuf/reflect/protorange for more
+// details.
+//
+// An error will be returned if the TagField is not a protobuf field or if the
+// given selector does not point to a known message field.
+func (r *TagField) Index(k string) (string, error) {
+ if r.Type.ProtoType == nil {
+ return "", fmt.Errorf("can only index proto fields")
+ }
+ k = fmt.Sprintf("(%s).%s", r.Type.ProtoType.Descriptor().FullName(), k)
+
+ var res string
+ var found bool
+ ref := r.proto.ProtoReflect()
+ protorange.Range(ref, func(values protopath.Values) error {
+ if values.Path.String() == k {
+ res = values.Index(-1).Value.String()
+ found = true
+ }
+ return nil
+ })
+
+ if !found {
+ return "", fmt.Errorf("protobuf field not found")
+ }
+ return res, nil
+}
+
// Backoff on a Machine.
type Backoff struct {
// Process which established Backoff.
diff --git a/cloud/bmaas/bmdb/reflection_test.go b/cloud/bmaas/bmdb/reflection_test.go
index 6b8b506..38118cb 100644
--- a/cloud/bmaas/bmdb/reflection_test.go
+++ b/cloud/bmaas/bmdb/reflection_test.go
@@ -285,5 +285,13 @@
if !strings.Contains(v, "manufacturer:") {
t.Errorf("Invalid serialized prototext: %s", v)
}
+ fv, err := machine.Tags["HardwareReport"].Field("hardware_report_raw").Index("report.cpu[0].cores")
+ if err != nil {
+ t.Errorf("Could not get report.cpu[0].cores from hardware_report_raw: %v", err)
+ } else {
+ if want, got := "1", fv; want != got {
+ t.Errorf("report.cpu[0].cores should be %q, got %q", want, got)
+ }
+ }
}
}