From beaa93d3d813fd64c6bfefe5c26c34adb7fb9969 Mon Sep 17 00:00:00 2001 From: Elim Tsiagbey Date: Mon, 28 Oct 2024 11:02:12 -0400 Subject: [PATCH] feat: NormalizeSetData (#11) * feat: NormalizeSetData & NormalizeCatalogData - Add support for always normalizing host set filters as an array. This implementation follows exactly what we do for AWS as well for consistency - No-op for NormalizeCatalogData --- plugin/service/host/plugin.go | 38 ++++++++ plugin/service/host/plugin_test.go | 148 +++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) diff --git a/plugin/service/host/plugin.go b/plugin/service/host/plugin.go index 5188a6f..a4a4821 100644 --- a/plugin/service/host/plugin.go +++ b/plugin/service/host/plugin.go @@ -18,6 +18,8 @@ import ( "google.golang.org/api/option" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" ) // HostPlugin implements the HostPluginServiceServer interface for the @@ -270,6 +272,42 @@ func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalo return &pb.OnDeleteCatalogResponse{}, nil } +// NormalizeCatalogData is called to normalize the catalog data. +func (p *HostPlugin) NormalizeCatalogData(ctx context.Context, req *pb.NormalizeCatalogDataRequest) (*pb.NormalizeCatalogDataResponse, error) { + // No-op, Catalog fields do not require any normalization + return &pb.NormalizeCatalogDataResponse{Attributes: req.Attributes}, nil +} + +// NormalizeSetData currently ensures that "filters" is an array value, even +// though it's accepted as a string value for CLI UX reasons +func (p *HostPlugin) NormalizeSetData(ctx context.Context, req *pb.NormalizeSetDataRequest) (*pb.NormalizeSetDataResponse, error) { + if req.Attributes == nil { + return &pb.NormalizeSetDataResponse{}, nil + } + + val := req.Attributes.Fields["filters"] + if val == nil { + return &pb.NormalizeSetDataResponse{Attributes: req.Attributes}, nil + } + + switch val.GetKind().(type) { + case *structpb.Value_ListValue: + return &pb.NormalizeSetDataResponse{Attributes: req.Attributes}, nil + case *structpb.Value_StringValue: + stringVal := val.Kind.(*structpb.Value_StringValue) + retAttrs := proto.Clone(req.Attributes).(*structpb.Struct) + retAttrs.Fields["filters"] = structpb.NewListValue( + &structpb.ListValue{ + Values: []*structpb.Value{ + structpb.NewStringValue(stringVal.StringValue), + }, + }) + return &pb.NormalizeSetDataResponse{Attributes: retAttrs}, nil + default: + return &pb.NormalizeSetDataResponse{}, status.Error(codes.InvalidArgument, "attribute.filters must be a string or list") + } +} + // OnCreateSet is called when a dynamic host set is created. func (p *HostPlugin) OnCreateSet(_ context.Context, req *pb.OnCreateSetRequest) (*pb.OnCreateSetResponse, error) { if err := validateSet(req.GetSet()); err != nil { diff --git a/plugin/service/host/plugin_test.go b/plugin/service/host/plugin_test.go index 8311b31..74e973e 100644 --- a/plugin/service/host/plugin_test.go +++ b/plugin/service/host/plugin_test.go @@ -1190,3 +1190,151 @@ func TestUpdateSet(t *testing.T) { }) } } + +func TestNormalizeSetData(t *testing.T) { + ctx := context.Background() + p := &HostPlugin{} + + cases := []struct { + name string + req *pb.NormalizeSetDataRequest + expectedRsp *pb.NormalizeSetDataResponse + expectedErrContains string + }{ + { + name: "nil attributes", + req: &pb.NormalizeSetDataRequest{}, + expectedRsp: &pb.NormalizeSetDataResponse{ + Attributes: nil, + }, + }, + { + name: "nil filters", + req: &pb.NormalizeSetDataRequest{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{}, + }, + }, + expectedRsp: &pb.NormalizeSetDataResponse{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{}, + }, + }, + }, + { + name: "filters not a string", + req: &pb.NormalizeSetDataRequest{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "filters": structpb.NewListValue(&structpb.ListValue{ + Values: []*structpb.Value{ + structpb.NewStringValue("status = RUNNING"), + }, + }), + }, + }, + }, + expectedRsp: &pb.NormalizeSetDataResponse{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "filters": structpb.NewListValue(&structpb.ListValue{ + Values: []*structpb.Value{ + structpb.NewStringValue("status = RUNNING"), + }, + }), + }, + }, + }, + }, + { + name: "filters is a string", + req: &pb.NormalizeSetDataRequest{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "filters": structpb.NewStringValue("status = RUNNING"), + }, + }, + }, + expectedRsp: &pb.NormalizeSetDataResponse{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "filters": structpb.NewListValue(&structpb.ListValue{ + Values: []*structpb.Value{ + structpb.NewStringValue("status = RUNNING"), + }, + }), + }, + }, + }, + }, + { + name: "filters with multiple values", + req: &pb.NormalizeSetDataRequest{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "filters": structpb.NewStringValue(`(cpuPlatform = "Intel Skylake") OR (cpuPlatform = "Intel Broadwell") AND (scheduling.automaticRestart = true)`), + }, + }, + }, + expectedRsp: &pb.NormalizeSetDataResponse{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "filters": structpb.NewListValue(&structpb.ListValue{ + Values: []*structpb.Value{ + structpb.NewStringValue(`(cpuPlatform = "Intel Skylake") OR (cpuPlatform = "Intel Broadwell") AND (scheduling.automaticRestart = true)`), + }, + }), + }, + }, + }, + }, + { + name: "filters is an number", + req: &pb.NormalizeSetDataRequest{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "filters": structpb.NewNumberValue(1), + }, + }, + }, + expectedErrContains: "attribute.filters must be a string or list", + }, + { + name: "filters is an boolean", + req: &pb.NormalizeSetDataRequest{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "filters": structpb.NewBoolValue(true), + }, + }, + }, + expectedErrContains: "attribute.filters must be a string or list", + }, + { + name: "filters is null", + req: &pb.NormalizeSetDataRequest{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "filters": structpb.NewNullValue(), + }, + }, + }, + expectedErrContains: "attribute.filters must be a string or list", + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + + actual, err := p.NormalizeSetData(ctx, tc.req) + if tc.expectedErrContains != "" { + require.ErrorContains(err, tc.expectedErrContains) + return + } + require.NoError(err) + require.Empty(cmp.Diff(tc.expectedRsp, actual, protocmp.Transform())) + }) + } +}