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
This commit is contained in:
Elim Tsiagbey authored and GitHub committed 2024-10-28 11:02:12 -04:00
1 parent ba53ae16b2
commit beaa93d3d8
2 files changed
+186

No files matched your search

+38
View File
@@ -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 {
+148
View File
@@ -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()))
})
}
}