This adds checks for unknown attributes through the entire plugin lifecycle for all attributes, secrets, and persisted state. It also fixes the E2E test which seems to have drifted.
190 lines
4.5 KiB
Go
190 lines
4.5 KiB
Go
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mitchellh/mapstructure"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
)
|
|
|
|
// CatalogAttributes is a Go-native representation of the Attributes
|
|
// map.
|
|
type CatalogAttributes struct {
|
|
Region string
|
|
DisableCredentialRotation bool
|
|
}
|
|
|
|
func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
|
unknownFields := structFields(in)
|
|
result := new(CatalogAttributes)
|
|
|
|
var err error
|
|
result.Region, err = getStringValue(in, constRegion, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
delete(unknownFields, constRegion)
|
|
|
|
result.DisableCredentialRotation, err = getBoolValue(in, constDisableCredentialRotation, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
delete(unknownFields, constDisableCredentialRotation)
|
|
|
|
if len(unknownFields) != 0 {
|
|
return nil, fmt.Errorf("unknown catalog attribute fields provided: %s", keysAsString(unknownFields))
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// CatalogSecrets is a Go-native representation of the Secrets map.
|
|
type CatalogSecrets struct {
|
|
AccessKeyId string
|
|
SecretAccessKey string
|
|
}
|
|
|
|
func getCatalogSecrets(in *structpb.Struct) (*CatalogSecrets, error) {
|
|
unknownFields := structFields(in)
|
|
result := new(CatalogSecrets)
|
|
|
|
var err error
|
|
result.AccessKeyId, err = getStringValue(in, constAccessKeyId, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
delete(unknownFields, constAccessKeyId)
|
|
|
|
result.SecretAccessKey, err = getStringValue(in, constSecretAccessKey, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
delete(unknownFields, constSecretAccessKey)
|
|
|
|
if len(unknownFields) != 0 {
|
|
return nil, fmt.Errorf("unknown catalog secret fields provided: %s", keysAsString(unknownFields))
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// SetAttributes is a Go-native representation of the Attributes map that can be
|
|
// used for decoding the incoming map via mapstructure.
|
|
type SetAttributes struct {
|
|
Filters []string
|
|
}
|
|
|
|
func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) {
|
|
var setAttrs SetAttributes
|
|
|
|
// Quick validation to ensure that there's no non-filter attributes
|
|
// here for now. Make this more complex if we add more attributes
|
|
// to host sets.
|
|
unknownFields := structFields(in)
|
|
delete(unknownFields, constDescribeInstancesFilters)
|
|
if len(unknownFields) != 0 {
|
|
return nil, fmt.Errorf("unknown set attribute fields provided: %s", keysAsString(unknownFields))
|
|
}
|
|
|
|
// Mapstructure complains if it expects a slice as output and sees a scalar
|
|
// value. Rather than use WeakDecode and risk unintended consequences, I'm
|
|
// manually making this change if necessary.
|
|
inMap := in.AsMap()
|
|
if filtersRaw, ok := inMap[constDescribeInstancesFilters]; ok {
|
|
switch filterVal := filtersRaw.(type) {
|
|
case string:
|
|
inMap[constDescribeInstancesFilters] = []string{filterVal}
|
|
}
|
|
}
|
|
|
|
if err := mapstructure.Decode(inMap, &setAttrs); err != nil {
|
|
return nil, fmt.Errorf("error decoding set attributes: %w", err)
|
|
}
|
|
|
|
return &setAttrs, nil
|
|
}
|
|
|
|
func getStringValue(in *structpb.Struct, k string, required bool) (string, error) {
|
|
mv := in.AsMap()
|
|
v, ok := mv[k]
|
|
if !ok {
|
|
if required {
|
|
return "", fmt.Errorf("missing required value %q", k)
|
|
}
|
|
|
|
return "", nil
|
|
}
|
|
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("unexpected type for value %q: want string, got %T", k, v)
|
|
}
|
|
|
|
if s == "" && required {
|
|
return "", fmt.Errorf("value %q cannot be empty", k)
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
func getBoolValue(in *structpb.Struct, k string, required bool) (bool, error) {
|
|
mv := in.AsMap()
|
|
v, ok := mv[k]
|
|
if !ok {
|
|
if required {
|
|
return false, fmt.Errorf("missing required value %q", k)
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
b, ok := v.(bool)
|
|
if !ok {
|
|
return false, fmt.Errorf("unexpected type for value %q: want bool, got %T", k, v)
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func getTimeValue(in *structpb.Struct, k string) (time.Time, error) {
|
|
mv := in.AsMap()
|
|
v, ok := mv[k]
|
|
if !ok {
|
|
return time.Time{}, nil
|
|
}
|
|
|
|
tRaw, ok := v.(string)
|
|
if !ok {
|
|
return time.Time{}, fmt.Errorf("unexpected type for value %q: want string, got %T", k, v)
|
|
}
|
|
|
|
t, err := time.Parse(time.RFC3339Nano, tRaw)
|
|
if err != nil {
|
|
return time.Time{}, fmt.Errorf("could not parse time in value %q: %w", k, err)
|
|
}
|
|
|
|
return t, nil
|
|
}
|
|
|
|
func structFields(s *structpb.Struct) map[string]struct{} {
|
|
m := make(map[string]struct{}, len(s.GetFields()))
|
|
for k := range s.GetFields() {
|
|
m[k] = struct{}{}
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
func keysAsString(m map[string]struct{}) string {
|
|
s := make([]string, 0, len(m))
|
|
for k := range m {
|
|
s = append(s, k)
|
|
}
|
|
|
|
sort.Strings(s)
|
|
return strings.Join(s, ", ")
|
|
}
|