mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 08:56:43 -06:00
UnifiedStorage: Add ResourceTable format (#96506)
This commit is contained in:
parent
11a4a366c6
commit
7985fa573e
File diff suppressed because it is too large
Load Diff
@ -108,24 +108,24 @@ message ErrorDetails {
|
||||
}
|
||||
|
||||
message ErrorCause {
|
||||
// A machine-readable description of the cause of the error. If this value is
|
||||
// empty there is no information available.
|
||||
string reason = 1;
|
||||
// A human-readable description of the cause of the error. This field may be
|
||||
// presented as-is to a reader.
|
||||
// +optional
|
||||
string message = 2;
|
||||
// The field of the resource that has caused this error, as named by its JSON
|
||||
// serialization. May include dot and postfix notation for nested attributes.
|
||||
// Arrays are zero-indexed. Fields may appear more than once in an array of
|
||||
// causes due to fields having multiple errors.
|
||||
// Optional.
|
||||
//
|
||||
// Examples:
|
||||
// "name" - the field "name" on the current resource
|
||||
// "items[0].name" - the field "name" on the first array entry in "items"
|
||||
// +optional
|
||||
string field = 3;
|
||||
// A machine-readable description of the cause of the error. If this value is
|
||||
// empty there is no information available.
|
||||
string reason = 1;
|
||||
// A human-readable description of the cause of the error. This field may be
|
||||
// presented as-is to a reader.
|
||||
// +optional
|
||||
string message = 2;
|
||||
// The field of the resource that has caused this error, as named by its JSON
|
||||
// serialization. May include dot and postfix notation for nested attributes.
|
||||
// Arrays are zero-indexed. Fields may appear more than once in an array of
|
||||
// causes due to fields having multiple errors.
|
||||
// Optional.
|
||||
//
|
||||
// Examples:
|
||||
// "name" - the field "name" on the current resource
|
||||
// "items[0].name" - the field "name" on the first array entry in "items"
|
||||
// +optional
|
||||
string field = 3;
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
@ -448,6 +448,118 @@ message HealthCheckResponse {
|
||||
ServingStatus status = 1;
|
||||
}
|
||||
|
||||
// ResourceTable is a protobuf variation of the kubernetes Table object.
|
||||
// This format allows specifying a flexible set of columns related to a given resource
|
||||
message ResourceTable {
|
||||
// Columns describes each column in the returned items array. The number of cells per row
|
||||
// will always match the number of column definitions.
|
||||
repeated ResourceTableColumnDefinition columns = 1;
|
||||
|
||||
// rows is the list of items in the table.
|
||||
repeated ResourceTableRow rows = 2;
|
||||
|
||||
// When more results exist, pass this in the next request
|
||||
string next_page_token = 3;
|
||||
|
||||
// ResourceVersion of the list response
|
||||
// +optional
|
||||
int64 resource_version = 4;
|
||||
|
||||
// remainingItemCount is the number of subsequent items in the list which are not included in this
|
||||
// list response. If the list request contained label or field selectors, then the number of
|
||||
// remaining items is unknown and the field will be left unset and omitted during serialization.
|
||||
// If the list is complete (either because it is not chunking or because this is the last chunk),
|
||||
// then there are no more remaining items and this field will be left unset and omitted during
|
||||
// serialization.
|
||||
//
|
||||
// The intended use of the remainingItemCount is *estimating* the size of a collection. Clients
|
||||
// should not rely on the remainingItemCount to be set or to be exact.
|
||||
// +optional
|
||||
int64 remaining_item_count = 5;
|
||||
}
|
||||
|
||||
// TableColumnDefinition contains information about a column returned in the Table.
|
||||
message ResourceTableColumnDefinition {
|
||||
// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more.
|
||||
// When converted to a k8s Table, this will become two fields: type and format
|
||||
enum ColumnType {
|
||||
UNKNOWN_TYPE = 0;
|
||||
STRING = 1;
|
||||
BOOLEAN = 2;
|
||||
INT32 = 3;
|
||||
INT64 = 4;
|
||||
FLOAT = 5;
|
||||
DOUBLE = 6;
|
||||
DATE = 7;
|
||||
DATE_TIME = 8;
|
||||
BINARY = 9;
|
||||
OBJECT = 10; // map[string]any
|
||||
}
|
||||
|
||||
// These values are not part of standard k8s format
|
||||
// however these are useful when indexing and analyzing results
|
||||
message Properties {
|
||||
// All values in this columns should be unique
|
||||
bool unique_values = 1;
|
||||
|
||||
// The string value is free text; using text analyzers is appropriate
|
||||
bool free_text = 2;
|
||||
|
||||
// The value(s) are reasonable to use for search refinement
|
||||
// When indexing, these values would be good to add to an index
|
||||
bool filterable = 3;
|
||||
|
||||
// When true, every value should exist
|
||||
// not_null with a nil default_value should be an error
|
||||
bool not_null = 4;
|
||||
|
||||
// When missing, this value can be used
|
||||
bytes default_value = 5;
|
||||
}
|
||||
|
||||
// name is a human readable name for the column.
|
||||
string name = 1;
|
||||
|
||||
// Defines the column type. In k8s, this will resolve into both the type and format fields
|
||||
ColumnType type = 2;
|
||||
|
||||
// The value is an arry of given type
|
||||
bool is_array = 3;
|
||||
|
||||
// description is a human readable description of this column.
|
||||
string description = 4;
|
||||
|
||||
// Properties about this column (helpful for indexing and search)
|
||||
Properties properties = 5;
|
||||
|
||||
// priority is an integer defining the relative importance of this column compared to others. Lower
|
||||
// numbers are considered higher priority. Columns that may be omitted in limited space scenarios
|
||||
// should be given a higher priority.
|
||||
int32 priority = 6;
|
||||
}
|
||||
|
||||
// TableRow is an individual row in a table.
|
||||
message ResourceTableRow {
|
||||
// The resource referenced by this row
|
||||
ResourceKey key = 1;
|
||||
|
||||
// The resource version for the given values
|
||||
int64 resource_version = 2;
|
||||
|
||||
// Cells will be as wide as the column definitions array
|
||||
// Numeric values will be encoded using big endian bytes
|
||||
// All arrays will be JSON encoded
|
||||
repeated bytes cells = 3;
|
||||
|
||||
// This field may contains the additional information about each object based on the request.
|
||||
// The value will be at least a partial object metadata, and perhaps the full object metadata.
|
||||
// When this value exists, it should include both the key and the resource_version otherwise
|
||||
// they may be lost in the conversion to k8s resource
|
||||
// +optional
|
||||
bytes object = 4;
|
||||
}
|
||||
|
||||
|
||||
//----------------------------
|
||||
// Blob Support
|
||||
//----------------------------
|
||||
|
549
pkg/storage/unified/resource/table.go
Normal file
549
pkg/storage/unified/resource/table.go
Normal file
@ -0,0 +1,549 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
reflect "reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter"
|
||||
)
|
||||
|
||||
// Convert the the protobuf model into k8s (will decode each value)
|
||||
func (x *ResourceTable) ToK8s() (metav1.Table, error) {
|
||||
table := metav1.Table{
|
||||
ListMeta: metav1.ListMeta{
|
||||
Continue: x.NextPageToken,
|
||||
},
|
||||
}
|
||||
if x.RemainingItemCount > 0 {
|
||||
table.RemainingItemCount = &x.RemainingItemCount
|
||||
}
|
||||
if x.ResourceVersion > 0 {
|
||||
table.ResourceVersion = strconv.FormatInt(x.ResourceVersion, 10)
|
||||
}
|
||||
|
||||
columnCount := len(x.Columns)
|
||||
columns := make([]resourceTableColumn, columnCount)
|
||||
table.ColumnDefinitions = make([]metav1.TableColumnDefinition, columnCount)
|
||||
for i, c := range x.Columns {
|
||||
col, err := newResourceTableColumn(c, i)
|
||||
if err != nil {
|
||||
return table, err
|
||||
}
|
||||
columns[i] = *col
|
||||
table.ColumnDefinitions[i] = metav1.TableColumnDefinition{
|
||||
Name: c.Name,
|
||||
Description: c.Description,
|
||||
Priority: c.Priority,
|
||||
Type: col.OpenAPIType,
|
||||
Format: col.OpenAPIFormat,
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
table.Rows = make([]metav1.TableRow, len(x.Rows))
|
||||
for i, r := range x.Rows {
|
||||
row := metav1.TableRow{
|
||||
Cells: make([]interface{}, len(r.Cells)),
|
||||
}
|
||||
if len(r.Cells) != columnCount {
|
||||
return table, fmt.Errorf("invalid cells size (have=%d, expect=%d)", len(r.Cells), columnCount)
|
||||
}
|
||||
|
||||
for j, v := range r.Cells {
|
||||
row.Cells[j], err = columns[j].Decode(v)
|
||||
if err != nil {
|
||||
col := columns[j]
|
||||
return table, fmt.Errorf("error decoding (row=%d, column=%d, type=%s) %w", i, j, col.def.Type.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// The raw object value
|
||||
if r.Object != nil {
|
||||
row.Object = runtime.RawExtension{
|
||||
Raw: r.Object,
|
||||
}
|
||||
} else if r.Key != nil {
|
||||
obj := &metav1.PartialObjectMetadata{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: r.Key.Name,
|
||||
Namespace: r.Key.Namespace,
|
||||
},
|
||||
}
|
||||
if r.ResourceVersion > 0 {
|
||||
obj.ResourceVersion = strconv.FormatInt(r.ResourceVersion, 10)
|
||||
}
|
||||
row.Object.Object = obj
|
||||
row.Object.Raw, err = json.Marshal(obj)
|
||||
if err != nil {
|
||||
return table, err
|
||||
}
|
||||
}
|
||||
table.Rows[i] = row
|
||||
}
|
||||
return table, err
|
||||
}
|
||||
|
||||
type TableBuilder struct {
|
||||
ResourceTable
|
||||
|
||||
lookup map[string]*resourceTableColumn
|
||||
|
||||
// Just keep track of it
|
||||
hasDuplicateNames bool
|
||||
}
|
||||
|
||||
func NewTableBuilder(cols []*ResourceTableColumnDefinition) (*TableBuilder, error) {
|
||||
table := &TableBuilder{
|
||||
ResourceTable: ResourceTable{
|
||||
Columns: cols,
|
||||
},
|
||||
|
||||
lookup: make(map[string]*resourceTableColumn, len(cols)),
|
||||
}
|
||||
var err error
|
||||
for i, v := range cols {
|
||||
if table.lookup[v.Name] != nil {
|
||||
table.hasDuplicateNames = true
|
||||
continue
|
||||
}
|
||||
table.lookup[v.Name], err = newResourceTableColumn(v, i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return table, err
|
||||
}
|
||||
|
||||
func (x *TableBuilder) AddRow(key *ResourceKey, rv int64, vals map[string]any) error {
|
||||
row := &ResourceTableRow{
|
||||
Key: key,
|
||||
ResourceVersion: rv,
|
||||
Cells: make([][]byte, len(x.Columns)),
|
||||
}
|
||||
|
||||
for k, v := range vals {
|
||||
column, ok := x.lookup[k]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown column: %s", k)
|
||||
}
|
||||
b, err := column.Encode(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
row.Cells[column.index] = b
|
||||
}
|
||||
|
||||
x.Rows = append(x.Rows, row)
|
||||
return nil
|
||||
}
|
||||
|
||||
type resourceTableColumn struct {
|
||||
def *ResourceTableColumnDefinition
|
||||
index int
|
||||
|
||||
// Used for array indexing
|
||||
reader func(iter *jsoniter.Iterator) (any, error)
|
||||
writer func(v any, stream *jsoniter.Stream) error
|
||||
|
||||
OpenAPIType string
|
||||
OpenAPIFormat string
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func newResourceTableColumn(def *ResourceTableColumnDefinition, index int) (*resourceTableColumn, error) {
|
||||
col := &resourceTableColumn{def: def, index: index}
|
||||
|
||||
// Initially ignore the array property, we wil wrap that at the end
|
||||
switch def.Type {
|
||||
case ResourceTableColumnDefinition_UNKNOWN_TYPE:
|
||||
return nil, fmt.Errorf("unknown column type")
|
||||
|
||||
case ResourceTableColumnDefinition_STRING:
|
||||
col.OpenAPIType = "string"
|
||||
col.reader = func(iter *jsoniter.Iterator) (any, error) {
|
||||
return iter.ReadString()
|
||||
}
|
||||
|
||||
case ResourceTableColumnDefinition_BOOLEAN:
|
||||
col.OpenAPIType = "boolean"
|
||||
col.reader = func(iter *jsoniter.Iterator) (any, error) {
|
||||
return iter.ReadBool()
|
||||
}
|
||||
|
||||
case ResourceTableColumnDefinition_INT32:
|
||||
col.OpenAPIType = "number"
|
||||
col.OpenAPIFormat = "int32"
|
||||
col.reader = func(iter *jsoniter.Iterator) (any, error) {
|
||||
return iter.ReadInt32()
|
||||
}
|
||||
|
||||
case ResourceTableColumnDefinition_INT64:
|
||||
col.OpenAPIType = "number"
|
||||
col.OpenAPIFormat = "int64"
|
||||
col.reader = func(iter *jsoniter.Iterator) (any, error) {
|
||||
return iter.ReadInt64()
|
||||
}
|
||||
|
||||
case ResourceTableColumnDefinition_DOUBLE:
|
||||
col.OpenAPIType = "number"
|
||||
col.OpenAPIFormat = "double"
|
||||
col.reader = func(iter *jsoniter.Iterator) (any, error) {
|
||||
return iter.ReadFloat64()
|
||||
}
|
||||
|
||||
case ResourceTableColumnDefinition_FLOAT:
|
||||
col.OpenAPIType = "number"
|
||||
col.OpenAPIFormat = "float"
|
||||
col.reader = func(iter *jsoniter.Iterator) (any, error) {
|
||||
return iter.ReadFloat32()
|
||||
}
|
||||
|
||||
// Encode everything we can -- the lower conversion can happen later?
|
||||
case ResourceTableColumnDefinition_DATE, ResourceTableColumnDefinition_DATE_TIME:
|
||||
col.OpenAPIType = "string"
|
||||
col.OpenAPIFormat = "date"
|
||||
if def.Type == ResourceTableColumnDefinition_DATE_TIME {
|
||||
col.OpenAPIFormat = "date_time"
|
||||
}
|
||||
col.writer = func(v any, stream *jsoniter.Stream) error {
|
||||
var t time.Time
|
||||
|
||||
switch typed := v.(type) {
|
||||
case time.Time:
|
||||
t = typed
|
||||
case *time.Time:
|
||||
t = *typed
|
||||
case int64:
|
||||
t = time.UnixMilli(typed)
|
||||
default:
|
||||
return fmt.Errorf("unsupported date conversion (%t)", v)
|
||||
}
|
||||
|
||||
// encode as millis has fastest parsing
|
||||
stream.WriteInt64(t.UnixMilli())
|
||||
return stream.Error
|
||||
}
|
||||
col.reader = func(iter *jsoniter.Iterator) (any, error) {
|
||||
nxt, err := iter.WhatIsNext()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch nxt {
|
||||
case jsoniter.NumberValue:
|
||||
ts, err := iter.ReadInt64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return time.UnixMilli(ts).UTC(), nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected JSON for date: %+v", nxt)
|
||||
}
|
||||
}
|
||||
|
||||
case ResourceTableColumnDefinition_BINARY:
|
||||
col.OpenAPIType = "binary"
|
||||
col.writer = func(v any, stream *jsoniter.Stream) error {
|
||||
b, ok := v.([]byte)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected binary type, found: %t", v)
|
||||
}
|
||||
str := base64.StdEncoding.EncodeToString(b)
|
||||
stream.WriteString(str)
|
||||
return stream.Error
|
||||
}
|
||||
col.reader = func(iter *jsoniter.Iterator) (any, error) {
|
||||
str, err := iter.ReadString()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return base64.StdEncoding.DecodeString(str)
|
||||
}
|
||||
|
||||
case ResourceTableColumnDefinition_OBJECT:
|
||||
col.OpenAPIType = "string"
|
||||
col.OpenAPIFormat = "json"
|
||||
|
||||
col.reader = func(iter *jsoniter.Iterator) (any, error) {
|
||||
return iter.Read()
|
||||
}
|
||||
}
|
||||
|
||||
return col, nil
|
||||
}
|
||||
|
||||
func (x *resourceTableColumn) IsNotNil() bool {
|
||||
if x.def.Properties != nil {
|
||||
return x.def.Properties.NotNull
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func (x *resourceTableColumn) Encode(v any) ([]byte, error) {
|
||||
if v == nil {
|
||||
if x.IsNotNil() {
|
||||
return nil, fmt.Errorf("expecting non-null value")
|
||||
}
|
||||
return nil, nil // no types to write
|
||||
}
|
||||
|
||||
// Arrays will always use JSON formatting
|
||||
if !x.def.IsArray {
|
||||
switch x.def.Type {
|
||||
case ResourceTableColumnDefinition_STRING:
|
||||
{
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expecting a string field")
|
||||
}
|
||||
return []byte(s), nil
|
||||
}
|
||||
case ResourceTableColumnDefinition_BINARY:
|
||||
{
|
||||
s, ok := v.([]byte)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expecting a byte array")
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
case ResourceTableColumnDefinition_BOOLEAN:
|
||||
{
|
||||
b, ok := v.(bool)
|
||||
if !ok {
|
||||
switch typed := v.(type) {
|
||||
case *bool:
|
||||
b = *typed
|
||||
case int:
|
||||
b = typed != 0
|
||||
case int32:
|
||||
b = typed != 0
|
||||
case int64:
|
||||
b = typed != 0
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected input for double field: %t", v)
|
||||
}
|
||||
}
|
||||
if b {
|
||||
return []byte{1}, nil
|
||||
}
|
||||
return []byte{0}, nil
|
||||
}
|
||||
case ResourceTableColumnDefinition_DATE_TIME, ResourceTableColumnDefinition_DATE:
|
||||
{
|
||||
f, ok := v.(time.Time)
|
||||
if !ok {
|
||||
switch typed := v.(type) {
|
||||
case *time.Time:
|
||||
f = *typed
|
||||
case metav1.Time:
|
||||
f = typed.Time
|
||||
case *metav1.Time:
|
||||
f = typed.Time
|
||||
case int64:
|
||||
f = time.UnixMilli(typed)
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected input for time field: %t", v)
|
||||
}
|
||||
}
|
||||
ts := f.UnixMilli()
|
||||
var buf bytes.Buffer
|
||||
err := binary.Write(&buf, binary.BigEndian, ts)
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
case ResourceTableColumnDefinition_DOUBLE:
|
||||
{
|
||||
f, ok := v.(float64)
|
||||
if !ok {
|
||||
switch typed := v.(type) {
|
||||
case int:
|
||||
f = float64(typed)
|
||||
case int64:
|
||||
f = float64(typed)
|
||||
case float32:
|
||||
f = float64(typed)
|
||||
case uint64:
|
||||
f = float64(typed)
|
||||
case uint:
|
||||
f = float64(typed)
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected input for double field: %t", v)
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := binary.Write(&buf, binary.BigEndian, f)
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
case ResourceTableColumnDefinition_INT64:
|
||||
{
|
||||
f, ok := v.(int64)
|
||||
if !ok {
|
||||
switch typed := v.(type) {
|
||||
case int:
|
||||
f = int64(typed)
|
||||
case int32:
|
||||
f = int64(typed)
|
||||
case float32:
|
||||
f = int64(typed)
|
||||
case uint64:
|
||||
f = int64(typed)
|
||||
case uint:
|
||||
f = int64(typed)
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected input for int64 field: %t", v)
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := binary.Write(&buf, binary.BigEndian, f)
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
default:
|
||||
// use JSON encoding below
|
||||
}
|
||||
}
|
||||
|
||||
buff := bytes.NewBuffer(make([]byte, 0, 128))
|
||||
cfg := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
stream := cfg.BorrowStream(buff)
|
||||
defer cfg.ReturnStream(stream)
|
||||
var err error
|
||||
|
||||
writer := func(v any) error {
|
||||
if v == nil {
|
||||
stream.WriteNil() // only happens in an array
|
||||
} else if x.writer != nil {
|
||||
return x.writer(v, stream)
|
||||
} else {
|
||||
stream.WriteVal(v)
|
||||
}
|
||||
return stream.Error
|
||||
}
|
||||
|
||||
if x.def.IsArray {
|
||||
stream.WriteArrayStart()
|
||||
|
||||
switch reflect.TypeOf(v).Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
s := reflect.ValueOf(v)
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
if i > 0 {
|
||||
stream.WriteMore()
|
||||
}
|
||||
sub := s.Index(i).Interface()
|
||||
err = writer(sub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
default:
|
||||
// single value? just write it and we will see?
|
||||
err = writer(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
stream.WriteArrayEnd()
|
||||
} else {
|
||||
err = writer(v)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if stream.Error != nil {
|
||||
return nil, stream.Error
|
||||
}
|
||||
|
||||
err = stream.Flush()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.RawMessage(buff.Bytes()), nil
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func (x *resourceTableColumn) Decode(buff []byte) (any, error) {
|
||||
if len(buff) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if !x.def.IsArray {
|
||||
switch x.def.Type {
|
||||
case ResourceTableColumnDefinition_STRING:
|
||||
return string(buff), nil
|
||||
case ResourceTableColumnDefinition_BINARY:
|
||||
return buff, nil
|
||||
case ResourceTableColumnDefinition_BOOLEAN:
|
||||
if len(buff) == 1 {
|
||||
return buff[0] != 0, nil
|
||||
}
|
||||
case ResourceTableColumnDefinition_DOUBLE:
|
||||
{
|
||||
var f float64
|
||||
count, err := binary.Decode(buff, binary.BigEndian, &f)
|
||||
if count == 8 && err == nil {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
case ResourceTableColumnDefinition_INT64:
|
||||
{
|
||||
var f int64
|
||||
count, err := binary.Decode(buff, binary.BigEndian, &f)
|
||||
if count == 8 && err == nil {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
case ResourceTableColumnDefinition_DATE_TIME, ResourceTableColumnDefinition_DATE:
|
||||
{
|
||||
var f int64
|
||||
count, err := binary.Decode(buff, binary.BigEndian, &f)
|
||||
if count == 8 && err == nil {
|
||||
return time.UnixMilli(f).UTC(), nil
|
||||
}
|
||||
}
|
||||
default:
|
||||
// use JSON decoding below
|
||||
}
|
||||
}
|
||||
|
||||
iter, err := jsoniter.ParseBytes(jsoniter.ConfigDefault, buff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if x.def.IsArray {
|
||||
vals := []any{} // it may have nulls
|
||||
|
||||
for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err := x.reader(iter)
|
||||
//nolint:errorlint
|
||||
if err != nil && err != io.EOF { // EOF is normal when jsoniter is done
|
||||
return nil, err
|
||||
}
|
||||
vals = append(vals, v)
|
||||
}
|
||||
|
||||
return vals, iter.ReadError()
|
||||
}
|
||||
|
||||
v, err := x.reader(iter)
|
||||
//nolint:errorlint
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, err
|
||||
}
|
343
pkg/storage/unified/resource/table_test.go
Normal file
343
pkg/storage/unified/resource/table_test.go
Normal file
@ -0,0 +1,343 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// AssertTableSnapshot will match a ResourceTable vs the saved value
|
||||
func AssertTableSnapshot(t *testing.T, path string, table *ResourceTable) {
|
||||
t.Helper()
|
||||
|
||||
k8sTable, err := table.ToK8s()
|
||||
require.NoError(t, err, "unable to create table response", path)
|
||||
actual, err := json.MarshalIndent(k8sTable, "", " ")
|
||||
require.NoError(t, err, "unable to write table json", path)
|
||||
|
||||
// Safe to disable, this is a test.
|
||||
// nolint:gosec
|
||||
expected, err := os.ReadFile(path)
|
||||
if err != nil || len(expected) < 1 {
|
||||
assert.Fail(t, "missing file")
|
||||
} else if assert.JSONEq(t, string(expected), string(actual)) {
|
||||
return // everything is OK
|
||||
}
|
||||
|
||||
// Write the snapshot
|
||||
// Safe to disable, this is a test.
|
||||
// nolint:gosec
|
||||
err = os.WriteFile(path, actual, 0600)
|
||||
require.NoError(t, err)
|
||||
fmt.Printf("Updated table snapshot: %s\n", path)
|
||||
}
|
||||
|
||||
func TestTableFormat(t *testing.T) {
|
||||
columns := []*ResourceTableColumnDefinition{
|
||||
{
|
||||
Name: "title",
|
||||
Type: ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
{
|
||||
Name: "stats.count",
|
||||
Type: ResourceTableColumnDefinition_INT64,
|
||||
},
|
||||
{
|
||||
Name: "number",
|
||||
Type: ResourceTableColumnDefinition_DOUBLE,
|
||||
|
||||
Description: "float64 value",
|
||||
},
|
||||
{
|
||||
Name: "tags",
|
||||
Type: ResourceTableColumnDefinition_STRING,
|
||||
IsArray: true,
|
||||
},
|
||||
}
|
||||
|
||||
var err error
|
||||
builder, err := NewTableBuilder(columns)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = builder.AddRow(&ResourceKey{
|
||||
Namespace: "default",
|
||||
Group: "ggg",
|
||||
Resource: "xyz", // does not have a home in table!
|
||||
Name: "aaa",
|
||||
}, 10, map[string]any{
|
||||
"title": "AAA",
|
||||
"number": 12345,
|
||||
"tags": "one", // becomes an array
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = builder.AddRow(&ResourceKey{
|
||||
Namespace: "default",
|
||||
Group: "ggg",
|
||||
Resource: "xyz", // does not have a home in table!
|
||||
Name: "bbb",
|
||||
}, 10, map[string]any{
|
||||
"title": "BBB",
|
||||
"stats.count": 12345,
|
||||
"tags": []string{"one", "two"}, // becomes an array
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check the snapshot
|
||||
AssertTableSnapshot(t, filepath.Join("testdata", "simple-table.json"), &builder.ResourceTable)
|
||||
}
|
||||
|
||||
func TestColumnEncoding(t *testing.T) {
|
||||
tests := []struct {
|
||||
// The table definition
|
||||
def *ResourceTableColumnDefinition
|
||||
|
||||
// Passed to the encode function
|
||||
input any
|
||||
|
||||
// Expected error from input
|
||||
input_err error
|
||||
|
||||
// Skip the encode step
|
||||
raw []byte
|
||||
|
||||
// Expected output from decode
|
||||
output any
|
||||
|
||||
// Expected error from decode
|
||||
output_err error
|
||||
}{
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
input: "aaa", // expects output to match
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_STRING,
|
||||
IsArray: true,
|
||||
},
|
||||
input: "bbb",
|
||||
output: []any{"bbb"},
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_INT64,
|
||||
},
|
||||
input: 12345,
|
||||
output: int64(12345),
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_INT64,
|
||||
IsArray: true,
|
||||
},
|
||||
input: 12345,
|
||||
output: []any{int64(12345)},
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_DOUBLE,
|
||||
},
|
||||
input: 12345,
|
||||
output: float64(12345),
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_DOUBLE,
|
||||
IsArray: true,
|
||||
},
|
||||
input: 12345,
|
||||
output: []any{float64(12345)},
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_BOOLEAN,
|
||||
},
|
||||
input: true,
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_BOOLEAN,
|
||||
IsArray: true,
|
||||
},
|
||||
input: []any{true, false, true},
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_FLOAT,
|
||||
},
|
||||
input: 23.4,
|
||||
output: float32(23.4),
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_FLOAT,
|
||||
IsArray: true,
|
||||
},
|
||||
input: 23.4,
|
||||
output: []any{float32(23.4)},
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_INT32,
|
||||
},
|
||||
input: 56,
|
||||
output: int32(56),
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_INT32,
|
||||
IsArray: true,
|
||||
},
|
||||
input: 56,
|
||||
output: []any{int32(56)},
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_DATE_TIME,
|
||||
},
|
||||
input: time.UnixMilli(946674000000).UTC(),
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_DATE_TIME,
|
||||
IsArray: true,
|
||||
},
|
||||
input: time.UnixMilli(946674000000).UTC(),
|
||||
output: []any{
|
||||
time.UnixMilli(946674000000).UTC(),
|
||||
},
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_DATE,
|
||||
},
|
||||
input: time.UnixMilli(946674000000).UTC(),
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_DATE,
|
||||
IsArray: true,
|
||||
},
|
||||
input: time.UnixMilli(946674000000).UTC(),
|
||||
output: []any{
|
||||
time.UnixMilli(946674000000).UTC(),
|
||||
},
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_BINARY,
|
||||
},
|
||||
input: []byte{1, 2, 3, 4},
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_BINARY,
|
||||
IsArray: true,
|
||||
},
|
||||
input: []any{
|
||||
[]byte{1, 2, 3, 4},
|
||||
},
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_OBJECT,
|
||||
},
|
||||
input: map[string]any{
|
||||
"hello": "world",
|
||||
},
|
||||
},
|
||||
{
|
||||
def: &ResourceTableColumnDefinition{
|
||||
Type: ResourceTableColumnDefinition_OBJECT,
|
||||
IsArray: true,
|
||||
},
|
||||
input: map[string]any{
|
||||
"hello": "world",
|
||||
},
|
||||
output: []any{
|
||||
map[string]any{
|
||||
"hello": "world",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Keep track of the types that have tests
|
||||
testedTypes := make(map[ResourceTableColumnDefinition_ColumnType]bool)
|
||||
testedArrays := make(map[ResourceTableColumnDefinition_ColumnType]bool)
|
||||
for _, test := range tests {
|
||||
var sb strings.Builder
|
||||
if test.def.IsArray {
|
||||
sb.WriteString("[]")
|
||||
testedArrays[test.def.Type] = true
|
||||
} else {
|
||||
testedTypes[test.def.Type] = true
|
||||
}
|
||||
sb.WriteString(test.def.Type.String())
|
||||
if test.def.Name != "" {
|
||||
sb.WriteString("(")
|
||||
sb.WriteString(test.def.Name)
|
||||
sb.WriteString(")")
|
||||
}
|
||||
sb.WriteString("=")
|
||||
sb.WriteString(fmt.Sprintf("%v", test.input))
|
||||
|
||||
t.Run(sb.String(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
col, err := newResourceTableColumn(test.def, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
buff := test.raw
|
||||
if buff == nil {
|
||||
buff, err = col.Encode(test.input)
|
||||
if test.input_err != nil {
|
||||
require.Equal(t, test.input_err, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
out, err := col.Decode(buff)
|
||||
if test.output_err != nil {
|
||||
require.Equal(t, test.output_err, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
if test.output != nil {
|
||||
require.Equal(t, test.output, out)
|
||||
} else {
|
||||
require.Equal(t, test.input, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("ensure type coverage", func(t *testing.T) {
|
||||
missingTypes := []string{}
|
||||
missingArrays := []string{}
|
||||
|
||||
// Make sure we have at least one test for each type
|
||||
for i := ResourceTableColumnDefinition_STRING; i <= ResourceTableColumnDefinition_OBJECT; i++ {
|
||||
if !testedTypes[i] {
|
||||
missingTypes = append(missingTypes, i.String())
|
||||
}
|
||||
if !testedArrays[i] {
|
||||
missingArrays = append(missingArrays, i.String())
|
||||
}
|
||||
}
|
||||
|
||||
require.Empty(t, missingTypes, "missing tests for types")
|
||||
require.Empty(t, missingArrays, "missing array tests for types")
|
||||
})
|
||||
}
|
72
pkg/storage/unified/resource/testdata/simple-table.json
vendored
Normal file
72
pkg/storage/unified/resource/testdata/simple-table.json
vendored
Normal file
@ -0,0 +1,72 @@
|
||||
{
|
||||
"metadata": {},
|
||||
"columnDefinitions": [
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"format": "",
|
||||
"description": "",
|
||||
"priority": 0
|
||||
},
|
||||
{
|
||||
"name": "stats.count",
|
||||
"type": "number",
|
||||
"format": "int64",
|
||||
"description": "",
|
||||
"priority": 0
|
||||
},
|
||||
{
|
||||
"name": "number",
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"description": "float64 value",
|
||||
"priority": 0
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"type": "string",
|
||||
"format": "",
|
||||
"description": "",
|
||||
"priority": 0
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"cells": [
|
||||
"AAA",
|
||||
null,
|
||||
12345,
|
||||
[
|
||||
"one"
|
||||
]
|
||||
],
|
||||
"object": {
|
||||
"metadata": {
|
||||
"name": "aaa",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "10",
|
||||
"creationTimestamp": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"BBB",
|
||||
12345,
|
||||
null,
|
||||
[
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
],
|
||||
"object": {
|
||||
"metadata": {
|
||||
"name": "bbb",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "10",
|
||||
"creationTimestamp": null
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user