diff --git a/sdk/framework/backend.go b/sdk/framework/backend.go index 903c2e8309..416d5d856f 100644 --- a/sdk/framework/backend.go +++ b/sdk/framework/backend.go @@ -625,6 +625,8 @@ func (t FieldType) Zero() interface{} { return http.Header{} case TypeFloat: return 0.0 + case TypeTime: + return time.Time{} default: panic("unknown type: " + t.String()) } diff --git a/sdk/framework/field_data.go b/sdk/framework/field_data.go index da161cb6ad..ce21b14941 100644 --- a/sdk/framework/field_data.go +++ b/sdk/framework/field_data.go @@ -40,7 +40,7 @@ func (d *FieldData) Validate() error { switch schema.Type { case TypeBool, TypeInt, TypeMap, TypeDurationSecond, TypeSignedDurationSecond, TypeString, TypeLowerCaseString, TypeNameString, TypeSlice, TypeStringSlice, TypeCommaStringSlice, - TypeKVPairs, TypeCommaIntSlice, TypeHeader, TypeFloat: + TypeKVPairs, TypeCommaIntSlice, TypeHeader, TypeFloat, TypeTime: _, _, err := d.getPrimitive(field, schema) if err != nil { return errwrap.Wrapf(fmt.Sprintf("error converting input %v for field %q: {{err}}", value, field), err) @@ -133,7 +133,7 @@ func (d *FieldData) GetOkErr(k string) (interface{}, bool, error) { switch schema.Type { case TypeBool, TypeInt, TypeMap, TypeDurationSecond, TypeSignedDurationSecond, TypeString, TypeLowerCaseString, TypeNameString, TypeSlice, TypeStringSlice, TypeCommaStringSlice, - TypeKVPairs, TypeCommaIntSlice, TypeHeader, TypeFloat: + TypeKVPairs, TypeCommaIntSlice, TypeHeader, TypeFloat, TypeTime: return d.getPrimitive(k, schema) default: return nil, false, @@ -221,6 +221,19 @@ func (d *FieldData) getPrimitive(k string, schema *FieldSchema) (interface{}, bo } return result, true, nil + case TypeTime: + switch inp := raw.(type) { + case nil: + // Handle nil interface{} as a non-error case + return nil, false, nil + default: + time, err := parseutil.ParseAbsoluteTime(inp) + if err != nil { + return nil, false, err + } + return time, true, nil + } + case TypeCommaIntSlice: var result []int config := &mapstructure.DecoderConfig{ diff --git a/sdk/framework/field_data_test.go b/sdk/framework/field_data_test.go index d3f91e7a7c..a34c2b599a 100644 --- a/sdk/framework/field_data_test.go +++ b/sdk/framework/field_data_test.go @@ -4,6 +4,7 @@ import ( "net/http" "reflect" "testing" + "time" ) func TestFieldDataGet(t *testing.T) { @@ -931,6 +932,40 @@ func TestFieldDataGet(t *testing.T) { 0.0, true, }, + + "type time, not supplied": { + map[string]*FieldSchema{ + "foo": {Type: TypeTime}, + }, + map[string]interface{}{}, + "foo", + time.Time{}, + false, + }, + "type time, string value": { + map[string]*FieldSchema{ + "foo": {Type: TypeTime}, + }, + map[string]interface{}{ + "foo": "2021-12-11T09:08:07Z", + }, + "foo", + // Comparison uses DeepEqual() so better match exactly, + // can't have a different location. + time.Date(2021, 12, 11, 9, 8, 7, 0, time.UTC), + false, + }, + "type time, invalid value": { + map[string]*FieldSchema{ + "foo": {Type: TypeTime}, + }, + map[string]interface{}{ + "foo": "2021-13-11T09:08:07+02:00", + }, + "foo", + time.Time{}, + true, + }, } for name, tc := range cases { diff --git a/sdk/framework/field_type.go b/sdk/framework/field_type.go index cd631360c7..db1db01d42 100644 --- a/sdk/framework/field_type.go +++ b/sdk/framework/field_type.go @@ -56,6 +56,9 @@ const ( // TypeFloat parses both float32 and float64 values TypeFloat + + // TypeTime represents absolute time, using an RFC3999 format on the wire + TypeTime ) func (t FieldType) String() string { @@ -82,6 +85,8 @@ func (t FieldType) String() string { return "header" case TypeFloat: return "float" + case TypeTime: + return "time" default: return "unknown type" } diff --git a/sdk/helper/parseutil/parseutil.go b/sdk/helper/parseutil/parseutil.go index face457f1f..56ad7b1156 100644 --- a/sdk/helper/parseutil/parseutil.go +++ b/sdk/helper/parseutil/parseutil.go @@ -67,6 +67,54 @@ func ParseDurationSecond(in interface{}) (time.Duration, error) { return dur, nil } +func ParseAbsoluteTime(in interface{}) (time.Time, error) { + var t time.Time + switch inp := in.(type) { + case nil: + // return default of zero + return t, nil + case string: + // Allow RFC3339 with nanoseconds, or without, + // or an epoch time as an integer. + var err error + t, err = time.Parse(time.RFC3339Nano, inp) + if err == nil { + break + } + t, err = time.Parse(time.RFC3339, inp) + if err == nil { + break + } + epochTime, err := strconv.ParseInt(inp, 10, 64) + if err == nil { + t = time.Unix(epochTime, 0) + break + } + return t, errors.New("could not parse string as date and time") + case json.Number: + epochTime, err := inp.Int64() + if err != nil { + return t, err + } + t = time.Unix(epochTime, 0) + case int: + t = time.Unix(int64(inp), 0) + case int32: + t = time.Unix(int64(inp), 0) + case int64: + t = time.Unix(inp, 0) + case uint: + t = time.Unix(int64(inp), 0) + case uint32: + t = time.Unix(int64(inp), 0) + case uint64: + t = time.Unix(int64(inp), 0) + default: + return t, errors.New("could not parse time from input type") + } + return t, nil +} + func ParseInt(in interface{}) (int64, error) { var ret int64 jsonIn, ok := in.(json.Number) diff --git a/sdk/helper/parseutil/parseutil_test.go b/sdk/helper/parseutil/parseutil_test.go index 7168a45820..e2399a5eed 100644 --- a/sdk/helper/parseutil/parseutil_test.go +++ b/sdk/helper/parseutil/parseutil_test.go @@ -30,6 +30,86 @@ func Test_ParseDurationSecond(t *testing.T) { } } +func Test_ParseAbsoluteTime(t *testing.T) { + testCases := []struct { + inp interface{} + valid bool + expected time.Time + }{ + { + "2020-12-11T09:08:07.654321Z", + true, + time.Date(2020, 12, 11, 9, 8, 7, 654321000, time.UTC), + }, + { + "2020-12-11T09:08:07+02:00", + true, + time.Date(2020, 12, 11, 7, 8, 7, 0, time.UTC), + }, + { + "2021-12-11T09:08:07Z", + true, + time.Date(2021, 12, 11, 9, 8, 7, 0, time.UTC), + }, + { + "2021-12-11T09:08:07", + false, + time.Time{}, + }, + { + "1670749687", + true, + time.Date(2022, 12, 11, 9, 8, 7, 0, time.UTC), + }, + { + 1670749687, + true, + time.Date(2022, 12, 11, 9, 8, 7, 0, time.UTC), + }, + { + uint32(1670749687), + true, + time.Date(2022, 12, 11, 9, 8, 7, 0, time.UTC), + }, + { + json.Number("1670749687"), + true, + time.Date(2022, 12, 11, 9, 8, 7, 0, time.UTC), + }, + { + nil, + true, + time.Time{}, + }, + { + struct{}{}, + false, + time.Time{}, + }, + { + true, + false, + time.Time{}, + }, + } + for _, tc := range testCases { + outp, err := ParseAbsoluteTime(tc.inp) + if err != nil { + if tc.valid { + t.Errorf("failed to parse: %v", tc.inp) + } + continue + } + if err == nil && !tc.valid { + t.Errorf("no error for: %v", tc.inp) + continue + } + if !outp.Equal(tc.expected) { + t.Errorf("input %v parsed as %v, expected %v", tc.inp, outp, tc.expected) + } + } +} + func Test_ParseBool(t *testing.T) { outp, err := ParseBool("true") if err != nil { diff --git a/vendor/github.com/hashicorp/vault/sdk/framework/backend.go b/vendor/github.com/hashicorp/vault/sdk/framework/backend.go index 903c2e8309..416d5d856f 100644 --- a/vendor/github.com/hashicorp/vault/sdk/framework/backend.go +++ b/vendor/github.com/hashicorp/vault/sdk/framework/backend.go @@ -625,6 +625,8 @@ func (t FieldType) Zero() interface{} { return http.Header{} case TypeFloat: return 0.0 + case TypeTime: + return time.Time{} default: panic("unknown type: " + t.String()) } diff --git a/vendor/github.com/hashicorp/vault/sdk/framework/field_data.go b/vendor/github.com/hashicorp/vault/sdk/framework/field_data.go index da161cb6ad..ce21b14941 100644 --- a/vendor/github.com/hashicorp/vault/sdk/framework/field_data.go +++ b/vendor/github.com/hashicorp/vault/sdk/framework/field_data.go @@ -40,7 +40,7 @@ func (d *FieldData) Validate() error { switch schema.Type { case TypeBool, TypeInt, TypeMap, TypeDurationSecond, TypeSignedDurationSecond, TypeString, TypeLowerCaseString, TypeNameString, TypeSlice, TypeStringSlice, TypeCommaStringSlice, - TypeKVPairs, TypeCommaIntSlice, TypeHeader, TypeFloat: + TypeKVPairs, TypeCommaIntSlice, TypeHeader, TypeFloat, TypeTime: _, _, err := d.getPrimitive(field, schema) if err != nil { return errwrap.Wrapf(fmt.Sprintf("error converting input %v for field %q: {{err}}", value, field), err) @@ -133,7 +133,7 @@ func (d *FieldData) GetOkErr(k string) (interface{}, bool, error) { switch schema.Type { case TypeBool, TypeInt, TypeMap, TypeDurationSecond, TypeSignedDurationSecond, TypeString, TypeLowerCaseString, TypeNameString, TypeSlice, TypeStringSlice, TypeCommaStringSlice, - TypeKVPairs, TypeCommaIntSlice, TypeHeader, TypeFloat: + TypeKVPairs, TypeCommaIntSlice, TypeHeader, TypeFloat, TypeTime: return d.getPrimitive(k, schema) default: return nil, false, @@ -221,6 +221,19 @@ func (d *FieldData) getPrimitive(k string, schema *FieldSchema) (interface{}, bo } return result, true, nil + case TypeTime: + switch inp := raw.(type) { + case nil: + // Handle nil interface{} as a non-error case + return nil, false, nil + default: + time, err := parseutil.ParseAbsoluteTime(inp) + if err != nil { + return nil, false, err + } + return time, true, nil + } + case TypeCommaIntSlice: var result []int config := &mapstructure.DecoderConfig{ diff --git a/vendor/github.com/hashicorp/vault/sdk/framework/field_type.go b/vendor/github.com/hashicorp/vault/sdk/framework/field_type.go index cd631360c7..db1db01d42 100644 --- a/vendor/github.com/hashicorp/vault/sdk/framework/field_type.go +++ b/vendor/github.com/hashicorp/vault/sdk/framework/field_type.go @@ -56,6 +56,9 @@ const ( // TypeFloat parses both float32 and float64 values TypeFloat + + // TypeTime represents absolute time, using an RFC3999 format on the wire + TypeTime ) func (t FieldType) String() string { @@ -82,6 +85,8 @@ func (t FieldType) String() string { return "header" case TypeFloat: return "float" + case TypeTime: + return "time" default: return "unknown type" } diff --git a/vendor/github.com/hashicorp/vault/sdk/helper/parseutil/parseutil.go b/vendor/github.com/hashicorp/vault/sdk/helper/parseutil/parseutil.go index face457f1f..56ad7b1156 100644 --- a/vendor/github.com/hashicorp/vault/sdk/helper/parseutil/parseutil.go +++ b/vendor/github.com/hashicorp/vault/sdk/helper/parseutil/parseutil.go @@ -67,6 +67,54 @@ func ParseDurationSecond(in interface{}) (time.Duration, error) { return dur, nil } +func ParseAbsoluteTime(in interface{}) (time.Time, error) { + var t time.Time + switch inp := in.(type) { + case nil: + // return default of zero + return t, nil + case string: + // Allow RFC3339 with nanoseconds, or without, + // or an epoch time as an integer. + var err error + t, err = time.Parse(time.RFC3339Nano, inp) + if err == nil { + break + } + t, err = time.Parse(time.RFC3339, inp) + if err == nil { + break + } + epochTime, err := strconv.ParseInt(inp, 10, 64) + if err == nil { + t = time.Unix(epochTime, 0) + break + } + return t, errors.New("could not parse string as date and time") + case json.Number: + epochTime, err := inp.Int64() + if err != nil { + return t, err + } + t = time.Unix(epochTime, 0) + case int: + t = time.Unix(int64(inp), 0) + case int32: + t = time.Unix(int64(inp), 0) + case int64: + t = time.Unix(inp, 0) + case uint: + t = time.Unix(int64(inp), 0) + case uint32: + t = time.Unix(int64(inp), 0) + case uint64: + t = time.Unix(int64(inp), 0) + default: + return t, errors.New("could not parse time from input type") + } + return t, nil +} + func ParseInt(in interface{}) (int64, error) { var ret int64 jsonIn, ok := in.(json.Number)