Merge pull request #27699 from hashicorp/mildwonkey/protocolv6
Mildwonkey/protocolv6
This commit is contained in:
commit
7943312ebd
|
@ -6,6 +6,7 @@ import (
|
|||
"unsafe"
|
||||
|
||||
"github.com/hashicorp/hcl/v2/hcldec"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
var mapLabelNames = []string{"key"}
|
||||
|
@ -183,9 +184,40 @@ func (b *Block) DecoderSpec() hcldec.Spec {
|
|||
}
|
||||
|
||||
func (a *Attribute) decoderSpec(name string) hcldec.Spec {
|
||||
return &hcldec.AttrSpec{
|
||||
Name: name,
|
||||
Type: a.Type,
|
||||
Required: a.Required,
|
||||
ret := &hcldec.AttrSpec{Name: name}
|
||||
if a == nil {
|
||||
return ret
|
||||
}
|
||||
|
||||
if a.NestedType != nil {
|
||||
// FIXME: a panic() is a bad UX. Fix this, probably by extending
|
||||
// InternalValidate() to check Attribute schemas as well and calling it
|
||||
// when we get the schema from the provider in Context().
|
||||
if a.Type != cty.NilType {
|
||||
panic("Invalid attribute schema: NestedType and Type cannot both be set. This is a bug in the provider.")
|
||||
}
|
||||
|
||||
ty := a.NestedType.ImpliedType()
|
||||
ret.Type = ty
|
||||
ret.Required = a.Required || a.NestedType.MinItems > 0
|
||||
return ret
|
||||
}
|
||||
|
||||
ret.Type = a.Type
|
||||
ret.Required = a.Required
|
||||
return ret
|
||||
}
|
||||
|
||||
// listOptionalAttrsFromObject is a helper function which does *not* recurse
|
||||
// into NestedType Attributes, because the optional types for each of those will
|
||||
// belong to their own cty.Object definitions. It is used in other functions
|
||||
// which themselves handle that recursion.
|
||||
func listOptionalAttrsFromObject(o *Object) []string {
|
||||
var ret []string
|
||||
for name, attr := range o.Attributes {
|
||||
if attr.Optional == true {
|
||||
ret = append(ret, name)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
|
|
@ -426,3 +426,456 @@ func TestBlockDecoderSpec(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttributeDecoderSpec(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
Schema *Attribute
|
||||
TestBody hcl.Body
|
||||
Want cty.Value
|
||||
DiagCount int
|
||||
}{
|
||||
"empty": {
|
||||
&Attribute{},
|
||||
hcl.EmptyBody(),
|
||||
cty.NilVal,
|
||||
0,
|
||||
},
|
||||
"nil": {
|
||||
nil,
|
||||
hcl.EmptyBody(),
|
||||
cty.NilVal,
|
||||
0,
|
||||
},
|
||||
"optional string (null)": {
|
||||
&Attribute{
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
hcltest.MockBody(&hcl.BodyContent{}),
|
||||
cty.NullVal(cty.String),
|
||||
0,
|
||||
},
|
||||
"optional string": {
|
||||
&Attribute{
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"attr": {
|
||||
Name: "attr",
|
||||
Expr: hcltest.MockExprLiteral(cty.StringVal("bar")),
|
||||
},
|
||||
},
|
||||
}),
|
||||
cty.StringVal("bar"),
|
||||
0,
|
||||
},
|
||||
"NestedType with required string": {
|
||||
&Attribute{
|
||||
NestedType: &Object{
|
||||
Nesting: NestingSingle,
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"attr": {
|
||||
Name: "attr",
|
||||
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("bar"),
|
||||
})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("bar"),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
"NestedType with optional attributes": {
|
||||
&Attribute{
|
||||
NestedType: &Object{
|
||||
Nesting: NestingSingle,
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"bar": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"attr": {
|
||||
Name: "attr",
|
||||
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("bar"),
|
||||
})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("bar"),
|
||||
"bar": cty.NullVal(cty.String),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
"NestedType with missing required string": {
|
||||
&Attribute{
|
||||
NestedType: &Object{
|
||||
Nesting: NestingSingle,
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"attr": {
|
||||
Name: "attr",
|
||||
Expr: hcltest.MockExprLiteral(cty.EmptyObjectVal),
|
||||
},
|
||||
},
|
||||
}),
|
||||
cty.UnknownVal(cty.Object(map[string]cty.Type{
|
||||
"foo": cty.String,
|
||||
})),
|
||||
1,
|
||||
},
|
||||
// NestedModes
|
||||
"NestedType NestingModeList valid": {
|
||||
&Attribute{
|
||||
NestedType: &Object{
|
||||
Nesting: NestingList,
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"attr": {
|
||||
Name: "attr",
|
||||
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("bar"),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("baz"),
|
||||
}),
|
||||
})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
|
||||
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
"NestedType NestingModeList invalid": {
|
||||
&Attribute{
|
||||
NestedType: &Object{
|
||||
Nesting: NestingList,
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"attr": {
|
||||
Name: "attr",
|
||||
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
||||
// "foo" should be a string, not a list
|
||||
"foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}),
|
||||
})})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{"foo": cty.String}))),
|
||||
1,
|
||||
},
|
||||
"NestedType NestingModeSet valid": {
|
||||
&Attribute{
|
||||
NestedType: &Object{
|
||||
Nesting: NestingSet,
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"attr": {
|
||||
Name: "attr",
|
||||
Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("bar"),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("baz"),
|
||||
}),
|
||||
})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
|
||||
cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
"NestedType NestingModeSet invalid": {
|
||||
&Attribute{
|
||||
NestedType: &Object{
|
||||
Nesting: NestingSet,
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"attr": {
|
||||
Name: "attr",
|
||||
Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
||||
// "foo" should be a string, not a list
|
||||
"foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}),
|
||||
})})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{"foo": cty.String}))),
|
||||
1,
|
||||
},
|
||||
"NestedType NestingModeMap valid": {
|
||||
&Attribute{
|
||||
NestedType: &Object{
|
||||
Nesting: NestingMap,
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"attr": {
|
||||
Name: "attr",
|
||||
Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
|
||||
"one": cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("bar"),
|
||||
}),
|
||||
"two": cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("baz"),
|
||||
}),
|
||||
})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
cty.MapVal(map[string]cty.Value{
|
||||
"one": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
|
||||
"two": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
"NestedType NestingModeMap invalid": {
|
||||
&Attribute{
|
||||
NestedType: &Object{
|
||||
Nesting: NestingMap,
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"attr": {
|
||||
Name: "attr",
|
||||
Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
|
||||
"one": cty.ObjectVal(map[string]cty.Value{
|
||||
// "foo" should be a string, not a list
|
||||
"foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}),
|
||||
}),
|
||||
})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{"foo": cty.String}))),
|
||||
1,
|
||||
},
|
||||
"deeply NestedType NestingModeList valid": {
|
||||
&Attribute{
|
||||
NestedType: &Object{
|
||||
Nesting: NestingList,
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": {
|
||||
NestedType: &Object{
|
||||
Nesting: NestingList,
|
||||
Attributes: map[string]*Attribute{
|
||||
"bar": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"attr": {
|
||||
Name: "attr",
|
||||
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")}),
|
||||
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("boz")}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("biz")}),
|
||||
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("buz")}),
|
||||
}),
|
||||
}),
|
||||
})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{"foo": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")}),
|
||||
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("boz")}),
|
||||
})}),
|
||||
cty.ObjectVal(map[string]cty.Value{"foo": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("biz")}),
|
||||
cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("buz")}),
|
||||
})}),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
"deeply NestedType NestingList invalid": {
|
||||
&Attribute{
|
||||
NestedType: &Object{
|
||||
Nesting: NestingList,
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": {
|
||||
NestedType: &Object{
|
||||
Nesting: NestingList,
|
||||
Attributes: map[string]*Attribute{
|
||||
"bar": {
|
||||
Type: cty.Number,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"attr": {
|
||||
Name: "attr",
|
||||
Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ListVal([]cty.Value{
|
||||
// bar should be a Number
|
||||
cty.ObjectVal(map[string]cty.Value{"bar": cty.True}),
|
||||
cty.ObjectVal(map[string]cty.Value{"bar": cty.False}),
|
||||
}),
|
||||
}),
|
||||
})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
|
||||
"foo": cty.List(cty.Object(map[string]cty.Type{"bar": cty.Number})),
|
||||
}))),
|
||||
1,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
spec := test.Schema.decoderSpec("attr")
|
||||
got, diags := hcldec.Decode(test.TestBody, spec, nil)
|
||||
if len(diags) != test.DiagCount {
|
||||
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
|
||||
for _, diag := range diags {
|
||||
t.Logf("- %s", diag.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if !got.RawEquals(test.Want) {
|
||||
t.Logf("[INFO] implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec)))
|
||||
t.Errorf("wrong result\ngot: %s\nwant: %s", dump.Value(got), dump.Value(test.Want))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestAttributeDecodeSpec_panic is a temporary test which verifies that
|
||||
// decoderSpec panics when an invalid Attribute schema is encountered. It will
|
||||
// be removed when InternalValidate() is extended to validate Attribute specs
|
||||
// (and is used). See the #FIXME in decoderSpec.
|
||||
func TestAttributeDecoderSpec_panic(t *testing.T) {
|
||||
attrS := &Attribute{
|
||||
Type: cty.Object(map[string]cty.Type{
|
||||
"nested_attribute": cty.String,
|
||||
}),
|
||||
NestedType: &Object{},
|
||||
Optional: true,
|
||||
}
|
||||
|
||||
defer func() { recover() }()
|
||||
attrS.decoderSpec("attr")
|
||||
t.Errorf("expected panic")
|
||||
}
|
||||
|
|
|
@ -26,6 +26,9 @@ func (b *Block) EmptyValue() cty.Value {
|
|||
// the value that would be returned if there were no definition of the attribute
|
||||
// at all, ignoring any required constraint.
|
||||
func (a *Attribute) EmptyValue() cty.Value {
|
||||
if a.NestedType != nil {
|
||||
return cty.NullVal(a.NestedType.ImpliedType())
|
||||
}
|
||||
return cty.NullVal(a.Type)
|
||||
}
|
||||
|
||||
|
|
|
@ -168,3 +168,90 @@ func TestBlockEmptyValue(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Attribute EmptyValue() is well covered by the Block tests above; these tests
|
||||
// focus on the behavior with NestedType field inside an Attribute
|
||||
func TestAttributeEmptyValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
Schema *Attribute
|
||||
Want cty.Value
|
||||
}{
|
||||
{
|
||||
&Attribute{},
|
||||
cty.NilVal,
|
||||
},
|
||||
{
|
||||
&Attribute{
|
||||
Type: cty.String,
|
||||
},
|
||||
cty.NullVal(cty.String),
|
||||
},
|
||||
{
|
||||
&Attribute{
|
||||
NestedType: &Object{
|
||||
Nesting: NestingSingle,
|
||||
Attributes: map[string]*Attribute{
|
||||
"str": {Type: cty.String, Required: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"str": cty.String,
|
||||
})),
|
||||
},
|
||||
{
|
||||
&Attribute{
|
||||
NestedType: &Object{
|
||||
Nesting: NestingList,
|
||||
Attributes: map[string]*Attribute{
|
||||
"str": {Type: cty.String, Required: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.NullVal(cty.List(
|
||||
cty.Object(map[string]cty.Type{
|
||||
"str": cty.String,
|
||||
}),
|
||||
)),
|
||||
},
|
||||
{
|
||||
&Attribute{
|
||||
NestedType: &Object{
|
||||
Nesting: NestingMap,
|
||||
Attributes: map[string]*Attribute{
|
||||
"str": {Type: cty.String, Required: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.NullVal(cty.Map(
|
||||
cty.Object(map[string]cty.Type{
|
||||
"str": cty.String,
|
||||
}),
|
||||
)),
|
||||
},
|
||||
{
|
||||
&Attribute{
|
||||
NestedType: &Object{
|
||||
Nesting: NestingSet,
|
||||
Attributes: map[string]*Attribute{
|
||||
"str": {Type: cty.String, Required: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.NullVal(cty.Set(
|
||||
cty.Object(map[string]cty.Type{
|
||||
"str": cty.String,
|
||||
}),
|
||||
)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%#v", test.Schema), func(t *testing.T) {
|
||||
got := test.Schema.EmptyValue()
|
||||
if !test.Want.RawEquals(got) {
|
||||
t.Errorf("wrong result\nschema: %s\ngot: %s\nwant: %s", spew.Sdump(test.Schema), dump.Value(got), dump.Value(test.Want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,3 +40,56 @@ func (b *Block) ContainsSensitive() bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ImpliedType returns the cty.Type that would result from decoding a NestedType
|
||||
// Attribute using the receiving block schema.
|
||||
//
|
||||
// ImpliedType always returns a result, even if the given schema is
|
||||
// inconsistent. Code that creates configschema.Object objects should be tested
|
||||
// using the InternalValidate method to detect any inconsistencies that would
|
||||
// cause this method to fall back on defaults and assumptions.
|
||||
func (o *Object) ImpliedType() cty.Type {
|
||||
if o == nil {
|
||||
return cty.EmptyObject
|
||||
}
|
||||
|
||||
attrTys := make(map[string]cty.Type, len(o.Attributes))
|
||||
for name, attrS := range o.Attributes {
|
||||
if attrS.NestedType != nil {
|
||||
attrTys[name] = attrS.NestedType.ImpliedType()
|
||||
} else {
|
||||
attrTys[name] = attrS.Type
|
||||
}
|
||||
}
|
||||
optAttrs := listOptionalAttrsFromObject(o)
|
||||
if len(optAttrs) > 0 {
|
||||
return cty.ObjectWithOptionalAttrs(attrTys, optAttrs)
|
||||
}
|
||||
|
||||
switch o.Nesting {
|
||||
case NestingSingle:
|
||||
return cty.Object(attrTys)
|
||||
case NestingList:
|
||||
return cty.List(cty.Object(attrTys))
|
||||
case NestingMap:
|
||||
return cty.Map(cty.Object(attrTys))
|
||||
case NestingSet:
|
||||
return cty.Set(cty.Object(attrTys))
|
||||
default: // Should never happen
|
||||
panic("invalid Nesting")
|
||||
}
|
||||
}
|
||||
|
||||
// ContainsSensitive returns true if any of the attributes of the receiving
|
||||
// Object are marked as sensitive.
|
||||
func (o *Object) ContainsSensitive() bool {
|
||||
for _, attrS := range o.Attributes {
|
||||
if attrS.Sensitive {
|
||||
return true
|
||||
}
|
||||
if attrS.NestedType != nil {
|
||||
return attrS.NestedType.ContainsSensitive()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -122,3 +122,197 @@ func TestBlockImpliedType(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectImpliedType(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
Schema *Object
|
||||
Want cty.Type
|
||||
}{
|
||||
"nil": {
|
||||
nil,
|
||||
cty.EmptyObject,
|
||||
},
|
||||
"attributes": {
|
||||
&Object{
|
||||
Attributes: map[string]*Attribute{
|
||||
"optional": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"required": {
|
||||
Type: cty.Number,
|
||||
Required: true,
|
||||
},
|
||||
"computed": {
|
||||
Type: cty.List(cty.Bool),
|
||||
Computed: true,
|
||||
},
|
||||
"optional_computed": {
|
||||
Type: cty.Map(cty.Bool),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectWithOptionalAttrs(
|
||||
map[string]cty.Type{
|
||||
"optional": cty.String,
|
||||
"required": cty.Number,
|
||||
"computed": cty.List(cty.Bool),
|
||||
"optional_computed": cty.Map(cty.Bool),
|
||||
},
|
||||
[]string{"optional", "optional_computed"},
|
||||
),
|
||||
},
|
||||
"nested attributes": {
|
||||
&Object{
|
||||
Attributes: map[string]*Attribute{
|
||||
"nested_type": {
|
||||
NestedType: &Object{
|
||||
Attributes: map[string]*Attribute{
|
||||
"optional": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"required": {
|
||||
Type: cty.Number,
|
||||
Required: true,
|
||||
},
|
||||
"computed": {
|
||||
Type: cty.List(cty.Bool),
|
||||
Computed: true,
|
||||
},
|
||||
"optional_computed": {
|
||||
Type: cty.Map(cty.Bool),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"nested_type": cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"optional": cty.String,
|
||||
"required": cty.Number,
|
||||
"computed": cty.List(cty.Bool),
|
||||
"optional_computed": cty.Map(cty.Bool),
|
||||
}, []string{"optional", "optional_computed"}),
|
||||
}, []string{"nested_type"}),
|
||||
},
|
||||
"NestingList": {
|
||||
&Object{
|
||||
Nesting: NestingList,
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": {Type: cty.String},
|
||||
},
|
||||
},
|
||||
cty.List(cty.Object(map[string]cty.Type{"foo": cty.String})),
|
||||
},
|
||||
"NestingMap": {
|
||||
&Object{
|
||||
Nesting: NestingMap,
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": {Type: cty.String},
|
||||
},
|
||||
},
|
||||
cty.Map(cty.Object(map[string]cty.Type{"foo": cty.String})),
|
||||
},
|
||||
"NestingSet": {
|
||||
&Object{
|
||||
Nesting: NestingSet,
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": {Type: cty.String},
|
||||
},
|
||||
},
|
||||
cty.Set(cty.Object(map[string]cty.Type{"foo": cty.String})),
|
||||
},
|
||||
"deeply nested NestingList": {
|
||||
&Object{
|
||||
Nesting: NestingList,
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": {
|
||||
NestedType: &Object{
|
||||
Nesting: NestingList,
|
||||
Attributes: map[string]*Attribute{
|
||||
"bar": {Type: cty.String},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.List(cty.Object(map[string]cty.Type{"foo": cty.List(cty.Object(map[string]cty.Type{"bar": cty.String}))})),
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := test.Schema.ImpliedType()
|
||||
if !got.Equals(test.Want) {
|
||||
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectContainsSensitive(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
Schema *Object
|
||||
Want bool
|
||||
}{
|
||||
"object contains sensitive": {
|
||||
&Object{
|
||||
Attributes: map[string]*Attribute{
|
||||
"sensitive": {Sensitive: true},
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
"no sensitive attrs": {
|
||||
&Object{
|
||||
Attributes: map[string]*Attribute{
|
||||
"insensitive": {},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
"nested object contains sensitive": {
|
||||
&Object{
|
||||
Attributes: map[string]*Attribute{
|
||||
"nested": {
|
||||
NestedType: &Object{
|
||||
Attributes: map[string]*Attribute{
|
||||
"sensitive": {Sensitive: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
"nested obj, no sensitive attrs": {
|
||||
&Object{
|
||||
Attributes: map[string]*Attribute{
|
||||
"nested": {
|
||||
NestedType: &Object{
|
||||
Attributes: map[string]*Attribute{
|
||||
"public": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := test.Schema.ContainsSensitive()
|
||||
if got != test.Want {
|
||||
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -38,8 +38,13 @@ type Block struct {
|
|||
// Attribute represents a configuration attribute, within a block.
|
||||
type Attribute struct {
|
||||
// Type is a type specification that the attribute's value must conform to.
|
||||
// It conflicts with NestedType.
|
||||
Type cty.Type
|
||||
|
||||
// NestedType indicates that the attribute is a NestedBlock-style object.
|
||||
// This field conflicts with Type.
|
||||
NestedType *Object
|
||||
|
||||
// Description is an English-language description of the purpose and
|
||||
// usage of the attribute. A description should be concise and use only
|
||||
// one or two sentences, leaving full definition to longer-form
|
||||
|
@ -72,6 +77,25 @@ type Attribute struct {
|
|||
Deprecated bool
|
||||
}
|
||||
|
||||
// Object represents the embedding of a structural object inside an Attribute.
|
||||
type Object struct {
|
||||
// Attributes describes the nested attributes which may appear inside the
|
||||
// Object.
|
||||
Attributes map[string]*Attribute
|
||||
|
||||
// Nesting provides the nesting mode for this Object, which determines how
|
||||
// many instances of the Object are allowed, how many labels it expects, and
|
||||
// how the resulting data will be converted into a data structure.
|
||||
Nesting NestingMode
|
||||
|
||||
// MinItems and MaxItems set, for the NestingList and NestingSet nesting
|
||||
// modes, lower and upper limits on the number of child blocks allowed
|
||||
// of the given type. If both are left at zero, no limit is applied.
|
||||
// These fields are ignored for other nesting modes and must both be left
|
||||
// at zero.
|
||||
MinItems, MaxItems int
|
||||
}
|
||||
|
||||
// NestedBlock represents the embedding of one block within another.
|
||||
type NestedBlock struct {
|
||||
// Block is the description of the block that's nested.
|
||||
|
@ -98,6 +122,8 @@ type NestedBlock struct {
|
|||
// blocks.
|
||||
type NestingMode int
|
||||
|
||||
// Object represents the embedding of a NestedBl
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer -type=NestingMode
|
||||
|
||||
const (
|
||||
|
|
|
@ -0,0 +1,321 @@
|
|||
// Terraform Plugin RPC protocol version 6.0
|
||||
//
|
||||
// This file defines version 6.0 of the RPC protocol. To implement a plugin
|
||||
// against this protocol, copy this definition into your own codebase and
|
||||
// use protoc to generate stubs for your target language.
|
||||
//
|
||||
// This file will not be updated. Any minor versions of protocol 6 to follow
|
||||
// should copy this file and modify the copy while maintaing backwards
|
||||
// compatibility. Breaking changes, if any are required, will come
|
||||
// in a subsequent major version with its own separate proto definition.
|
||||
//
|
||||
// Note that only the proto files included in a release tag of Terraform are
|
||||
// official protocol releases. Proto files taken from other commits may include
|
||||
// incomplete changes or features that did not make it into a final release.
|
||||
// In all reasonable cases, plugin developers should take the proto file from
|
||||
// the tag of the most recent release of Terraform, and not from the master
|
||||
// branch or any other development branch.
|
||||
//
|
||||
syntax = "proto3";
|
||||
option go_package = "github.com/hashicorp/terraform/internal/tfplugin6";
|
||||
|
||||
package tfplugin6;
|
||||
|
||||
// DynamicValue is an opaque encoding of terraform data, with the field name
|
||||
// indicating the encoding scheme used.
|
||||
message DynamicValue {
|
||||
bytes msgpack = 1;
|
||||
bytes json = 2;
|
||||
}
|
||||
|
||||
message Diagnostic {
|
||||
enum Severity {
|
||||
INVALID = 0;
|
||||
ERROR = 1;
|
||||
WARNING = 2;
|
||||
}
|
||||
Severity severity = 1;
|
||||
string summary = 2;
|
||||
string detail = 3;
|
||||
AttributePath attribute = 4;
|
||||
}
|
||||
|
||||
message AttributePath {
|
||||
message Step {
|
||||
oneof selector {
|
||||
// Set "attribute_name" to represent looking up an attribute
|
||||
// in the current object value.
|
||||
string attribute_name = 1;
|
||||
// Set "element_key_*" to represent looking up an element in
|
||||
// an indexable collection type.
|
||||
string element_key_string = 2;
|
||||
int64 element_key_int = 3;
|
||||
}
|
||||
}
|
||||
repeated Step steps = 1;
|
||||
}
|
||||
|
||||
message StopProvider {
|
||||
message Request {
|
||||
}
|
||||
message Response {
|
||||
string Error = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// RawState holds the stored state for a resource to be upgraded by the
|
||||
// provider. It can be in one of two formats, the current json encoded format
|
||||
// in bytes, or the legacy flatmap format as a map of strings.
|
||||
message RawState {
|
||||
bytes json = 1;
|
||||
map<string, string> flatmap = 2;
|
||||
}
|
||||
|
||||
enum StringKind {
|
||||
PLAIN = 0;
|
||||
MARKDOWN = 1;
|
||||
}
|
||||
|
||||
// Schema is the configuration schema for a Resource or Provider.
|
||||
message Schema {
|
||||
message Block {
|
||||
int64 version = 1;
|
||||
repeated Attribute attributes = 2;
|
||||
repeated NestedBlock block_types = 3;
|
||||
string description = 4;
|
||||
StringKind description_kind = 5;
|
||||
bool deprecated = 6;
|
||||
}
|
||||
|
||||
message Attribute {
|
||||
string name = 1;
|
||||
bytes type = 2;
|
||||
Object nested_type = 10;
|
||||
string description = 3;
|
||||
bool required = 4;
|
||||
bool optional = 5;
|
||||
bool computed = 6;
|
||||
bool sensitive = 7;
|
||||
StringKind description_kind = 8;
|
||||
bool deprecated = 9;
|
||||
}
|
||||
|
||||
message NestedBlock {
|
||||
enum NestingMode {
|
||||
INVALID = 0;
|
||||
SINGLE = 1;
|
||||
LIST = 2;
|
||||
SET = 3;
|
||||
MAP = 4;
|
||||
GROUP = 5;
|
||||
}
|
||||
|
||||
string type_name = 1;
|
||||
Block block = 2;
|
||||
NestingMode nesting = 3;
|
||||
int64 min_items = 4;
|
||||
int64 max_items = 5;
|
||||
}
|
||||
|
||||
message Object {
|
||||
enum NestingMode {
|
||||
INVALID = 0;
|
||||
SINGLE = 1;
|
||||
LIST = 2;
|
||||
SET = 3;
|
||||
MAP = 4;
|
||||
}
|
||||
|
||||
repeated Attribute attributes = 1;
|
||||
NestingMode nesting = 3;
|
||||
int64 min_items = 4;
|
||||
int64 max_items = 5;
|
||||
}
|
||||
|
||||
// The version of the schema.
|
||||
// Schemas are versioned, so that providers can upgrade a saved resource
|
||||
// state when the schema is changed.
|
||||
int64 version = 1;
|
||||
|
||||
// Block is the top level configuration block for this schema.
|
||||
Block block = 2;
|
||||
}
|
||||
|
||||
service Provider {
|
||||
//////// Information about what a provider supports/expects
|
||||
rpc GetProviderSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response);
|
||||
rpc ValidateProviderConfig(ValidateProviderConfig.Request) returns (ValidateProviderConfig.Response);
|
||||
rpc ValidateResourceConfig(ValidateResourceConfig.Request) returns (ValidateResourceConfig.Response);
|
||||
rpc ValidateDataSourceConfig(ValidateDataSourceConfig.Request) returns (ValidateDataSourceConfig.Response);
|
||||
rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response);
|
||||
|
||||
//////// One-time initialization, called before other functions below
|
||||
rpc ConfigureProvider(ConfigureProvider.Request) returns (ConfigureProvider.Response);
|
||||
|
||||
//////// Managed Resource Lifecycle
|
||||
rpc ReadResource(ReadResource.Request) returns (ReadResource.Response);
|
||||
rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response);
|
||||
rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response);
|
||||
rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response);
|
||||
|
||||
rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response);
|
||||
|
||||
//////// Graceful Shutdown
|
||||
rpc StopProvider(StopProvider.Request) returns (StopProvider.Response);
|
||||
}
|
||||
|
||||
message GetProviderSchema {
|
||||
message Request {
|
||||
}
|
||||
message Response {
|
||||
Schema provider = 1;
|
||||
map<string, Schema> resource_schemas = 2;
|
||||
map<string, Schema> data_source_schemas = 3;
|
||||
repeated Diagnostic diagnostics = 4;
|
||||
Schema provider_meta = 5;
|
||||
}
|
||||
}
|
||||
|
||||
message ValidateProviderConfig {
|
||||
message Request {
|
||||
DynamicValue config = 1;
|
||||
}
|
||||
message Response {
|
||||
repeated Diagnostic diagnostics = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message UpgradeResourceState {
|
||||
message Request {
|
||||
string type_name = 1;
|
||||
|
||||
// version is the schema_version number recorded in the state file
|
||||
int64 version = 2;
|
||||
|
||||
// raw_state is the raw states as stored for the resource. Core does
|
||||
// not have access to the schema of prior_version, so it's the
|
||||
// provider's responsibility to interpret this value using the
|
||||
// appropriate older schema. The raw_state will be the json encoded
|
||||
// state, or a legacy flat-mapped format.
|
||||
RawState raw_state = 3;
|
||||
}
|
||||
message Response {
|
||||
// new_state is a msgpack-encoded data structure that, when interpreted with
|
||||
// the _current_ schema for this resource type, is functionally equivalent to
|
||||
// that which was given in prior_state_raw.
|
||||
DynamicValue upgraded_state = 1;
|
||||
|
||||
// diagnostics describes any errors encountered during migration that could not
|
||||
// be safely resolved, and warnings about any possibly-risky assumptions made
|
||||
// in the upgrade process.
|
||||
repeated Diagnostic diagnostics = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message ValidateResourceConfig {
|
||||
message Request {
|
||||
string type_name = 1;
|
||||
DynamicValue config = 2;
|
||||
}
|
||||
message Response {
|
||||
repeated Diagnostic diagnostics = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message ValidateDataSourceConfig {
|
||||
message Request {
|
||||
string type_name = 1;
|
||||
DynamicValue config = 2;
|
||||
}
|
||||
message Response {
|
||||
repeated Diagnostic diagnostics = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message ConfigureProvider {
|
||||
message Request {
|
||||
string terraform_version = 1;
|
||||
DynamicValue config = 2;
|
||||
}
|
||||
message Response {
|
||||
repeated Diagnostic diagnostics = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message ReadResource {
|
||||
message Request {
|
||||
string type_name = 1;
|
||||
DynamicValue current_state = 2;
|
||||
bytes private = 3;
|
||||
DynamicValue provider_meta = 4;
|
||||
}
|
||||
message Response {
|
||||
DynamicValue new_state = 1;
|
||||
repeated Diagnostic diagnostics = 2;
|
||||
bytes private = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message PlanResourceChange {
|
||||
message Request {
|
||||
string type_name = 1;
|
||||
DynamicValue prior_state = 2;
|
||||
DynamicValue proposed_new_state = 3;
|
||||
DynamicValue config = 4;
|
||||
bytes prior_private = 5;
|
||||
DynamicValue provider_meta = 6;
|
||||
}
|
||||
|
||||
message Response {
|
||||
DynamicValue planned_state = 1;
|
||||
repeated AttributePath requires_replace = 2;
|
||||
bytes planned_private = 3;
|
||||
repeated Diagnostic diagnostics = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message ApplyResourceChange {
|
||||
message Request {
|
||||
string type_name = 1;
|
||||
DynamicValue prior_state = 2;
|
||||
DynamicValue planned_state = 3;
|
||||
DynamicValue config = 4;
|
||||
bytes planned_private = 5;
|
||||
DynamicValue provider_meta = 6;
|
||||
}
|
||||
message Response {
|
||||
DynamicValue new_state = 1;
|
||||
bytes private = 2;
|
||||
repeated Diagnostic diagnostics = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message ImportResourceState {
|
||||
message Request {
|
||||
string type_name = 1;
|
||||
string id = 2;
|
||||
}
|
||||
|
||||
message ImportedResource {
|
||||
string type_name = 1;
|
||||
DynamicValue state = 2;
|
||||
bytes private = 3;
|
||||
}
|
||||
|
||||
message Response {
|
||||
repeated ImportedResource imported_resources = 1;
|
||||
repeated Diagnostic diagnostics = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message ReadDataSource {
|
||||
message Request {
|
||||
string type_name = 1;
|
||||
DynamicValue config = 2;
|
||||
DynamicValue provider_meta = 3;
|
||||
}
|
||||
message Response {
|
||||
DynamicValue state = 1;
|
||||
repeated Diagnostic diagnostics = 2;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
|
||||
# We do not run protoc under go:generate because we want to ensure that all
|
||||
# dependencies of go:generate are "go get"-able for general dev environment
|
||||
# usability. To compile all protobuf files in this repository, run
|
||||
# "make protobuf" at the top-level.
|
||||
|
||||
set -eu
|
||||
|
||||
SOURCE="${BASH_SOURCE[0]}"
|
||||
while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done
|
||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
|
||||
cd "$DIR"
|
||||
|
||||
protoc --go_out=paths=source_relative,plugins=grpc:. ./tfplugin6.proto
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
../../docs/plugin-protocol/tfplugin6.0.proto
|
|
@ -5,14 +5,29 @@ import (
|
|||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// AllAttributesNull constructs a non-null cty.Value of the object type implied
|
||||
// AllBlockAttributesNull constructs a non-null cty.Value of the object type implied
|
||||
// by the given schema that has all of its leaf attributes set to null and all
|
||||
// of its nested block collections set to zero-length.
|
||||
//
|
||||
// This simulates what would result from decoding an empty configuration block
|
||||
// with the given schema, except that it does not produce errors
|
||||
func AllAttributesNull(schema *configschema.Block) cty.Value {
|
||||
func AllBlockAttributesNull(schema *configschema.Block) cty.Value {
|
||||
// "All attributes null" happens to be the definition of EmptyValue for
|
||||
// a Block, so we can just delegate to that.
|
||||
return schema.EmptyValue()
|
||||
}
|
||||
|
||||
// AllAttributesNull returns a cty.Value of the object type implied by the given
|
||||
// attriubutes that has all of its leaf attributes set to null.
|
||||
func AllAttributesNull(attrs map[string]*configschema.Attribute) cty.Value {
|
||||
newAttrs := make(map[string]cty.Value, len(attrs))
|
||||
|
||||
for name, attr := range attrs {
|
||||
if attr.NestedType != nil {
|
||||
newAttrs[name] = AllAttributesNull(attr.NestedType.Attributes)
|
||||
} else {
|
||||
newAttrs[name] = cty.NullVal(attr.Type)
|
||||
}
|
||||
}
|
||||
return cty.ObjectVal(newAttrs)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
)
|
||||
|
||||
// ProposedNewObject constructs a proposed new object value by combining the
|
||||
// ProposedNew constructs a proposed new object value by combining the
|
||||
// computed attribute values from "prior" with the configured attribute values
|
||||
// from "config".
|
||||
//
|
||||
|
@ -24,7 +24,7 @@ import (
|
|||
// heuristic based on matching non-computed attribute values and so it may
|
||||
// produce strange results with more "extreme" cases, such as a nested set
|
||||
// block where _all_ attributes are computed.
|
||||
func ProposedNewObject(schema *configschema.Block, prior, config cty.Value) cty.Value {
|
||||
func ProposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value {
|
||||
// If the config and prior are both null, return early here before
|
||||
// populating the prior block. The prevents non-null blocks from appearing
|
||||
// the proposed state value.
|
||||
|
@ -37,12 +37,12 @@ func ProposedNewObject(schema *configschema.Block, prior, config cty.Value) cty.
|
|||
// similar to the result of decoding an empty configuration block,
|
||||
// which simplifies our handling of the top-level attributes/blocks
|
||||
// below by giving us one non-null level of object to pull values from.
|
||||
prior = AllAttributesNull(schema)
|
||||
prior = AllBlockAttributesNull(schema)
|
||||
}
|
||||
return proposedNewObject(schema, prior, config)
|
||||
return proposedNew(schema, prior, config)
|
||||
}
|
||||
|
||||
// PlannedDataResourceObject is similar to ProposedNewObject but tailored for
|
||||
// PlannedDataResourceObject is similar to proposedNewBlock but tailored for
|
||||
// planning data resources in particular. Specifically, it replaces the values
|
||||
// of any Computed attributes not set in the configuration with an unknown
|
||||
// value, which serves as a placeholder for a value to be filled in by the
|
||||
|
@ -51,35 +51,212 @@ func ProposedNewObject(schema *configschema.Block, prior, config cty.Value) cty.
|
|||
// Data resources are different because the planning of them is handled
|
||||
// entirely within Terraform Core and not subject to customization by the
|
||||
// provider. This function is, in effect, producing an equivalent result to
|
||||
// passing the ProposedNewObject result into a provider's PlanResourceChange
|
||||
// passing the proposedNewBlock result into a provider's PlanResourceChange
|
||||
// function, assuming a fixed implementation of PlanResourceChange that just
|
||||
// fills in unknown values as needed.
|
||||
func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty.Value {
|
||||
// Our trick here is to run the ProposedNewObject logic with an
|
||||
// Our trick here is to run the proposedNewBlock logic with an
|
||||
// entirely-unknown prior value. Because of cty's unknown short-circuit
|
||||
// behavior, any operation on prior returns another unknown, and so
|
||||
// unknown values propagate into all of the parts of the resulting value
|
||||
// that would normally be filled in by preserving the prior state.
|
||||
prior := cty.UnknownVal(schema.ImpliedType())
|
||||
return proposedNewObject(schema, prior, config)
|
||||
return proposedNew(schema, prior, config)
|
||||
}
|
||||
|
||||
func proposedNewObject(schema *configschema.Block, prior, config cty.Value) cty.Value {
|
||||
func proposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value {
|
||||
if config.IsNull() || !config.IsKnown() {
|
||||
// This is a weird situation, but we'll allow it anyway to free
|
||||
// callers from needing to specifically check for these cases.
|
||||
return prior
|
||||
}
|
||||
if (!prior.Type().IsObjectType()) || (!config.Type().IsObjectType()) {
|
||||
panic("ProposedNewObject only supports object-typed values")
|
||||
panic("ProposedNew only supports object-typed values")
|
||||
}
|
||||
|
||||
// From this point onwards, we can assume that both values are non-null
|
||||
// object types, and that the config value itself is known (though it
|
||||
// may contain nested values that are unknown.)
|
||||
newAttrs := proposedNewAttributes(schema.Attributes, prior, config)
|
||||
|
||||
newAttrs := map[string]cty.Value{}
|
||||
for name, attr := range schema.Attributes {
|
||||
// Merging nested blocks is a little more complex, since we need to
|
||||
// correlate blocks between both objects and then recursively propose
|
||||
// a new object for each. The correlation logic depends on the nesting
|
||||
// mode for each block type.
|
||||
for name, blockType := range schema.BlockTypes {
|
||||
priorV := prior.GetAttr(name)
|
||||
configV := config.GetAttr(name)
|
||||
newAttrs[name] = proposedNewNestedBlock(blockType, priorV, configV)
|
||||
}
|
||||
|
||||
return cty.ObjectVal(newAttrs)
|
||||
}
|
||||
|
||||
func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.Value) cty.Value {
|
||||
var newV cty.Value
|
||||
|
||||
switch schema.Nesting {
|
||||
|
||||
case configschema.NestingSingle, configschema.NestingGroup:
|
||||
newV = ProposedNew(&schema.Block, prior, config)
|
||||
|
||||
case configschema.NestingList:
|
||||
// Nested blocks are correlated by index.
|
||||
configVLen := 0
|
||||
if config.IsKnown() && !config.IsNull() {
|
||||
configVLen = config.LengthInt()
|
||||
}
|
||||
if configVLen > 0 {
|
||||
newVals := make([]cty.Value, 0, configVLen)
|
||||
for it := config.ElementIterator(); it.Next(); {
|
||||
idx, configEV := it.Element()
|
||||
if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
|
||||
// If there is no corresponding prior element then
|
||||
// we just take the config value as-is.
|
||||
newVals = append(newVals, configEV)
|
||||
continue
|
||||
}
|
||||
priorEV := prior.Index(idx)
|
||||
|
||||
newEV := ProposedNew(&schema.Block, priorEV, configEV)
|
||||
newVals = append(newVals, newEV)
|
||||
}
|
||||
// Despite the name, a NestingList might also be a tuple, if
|
||||
// its nested schema contains dynamically-typed attributes.
|
||||
if config.Type().IsTupleType() {
|
||||
newV = cty.TupleVal(newVals)
|
||||
} else {
|
||||
newV = cty.ListVal(newVals)
|
||||
}
|
||||
} else {
|
||||
// Despite the name, a NestingList might also be a tuple, if
|
||||
// its nested schema contains dynamically-typed attributes.
|
||||
if config.Type().IsTupleType() {
|
||||
newV = cty.EmptyTupleVal
|
||||
} else {
|
||||
newV = cty.ListValEmpty(schema.ImpliedType())
|
||||
}
|
||||
}
|
||||
|
||||
case configschema.NestingMap:
|
||||
// Despite the name, a NestingMap may produce either a map or
|
||||
// object value, depending on whether the nested schema contains
|
||||
// dynamically-typed attributes.
|
||||
if config.Type().IsObjectType() {
|
||||
// Nested blocks are correlated by key.
|
||||
configVLen := 0
|
||||
if config.IsKnown() && !config.IsNull() {
|
||||
configVLen = config.LengthInt()
|
||||
}
|
||||
if configVLen > 0 {
|
||||
newVals := make(map[string]cty.Value, configVLen)
|
||||
atys := config.Type().AttributeTypes()
|
||||
for name := range atys {
|
||||
configEV := config.GetAttr(name)
|
||||
if !prior.IsKnown() || prior.IsNull() || !prior.Type().HasAttribute(name) {
|
||||
// If there is no corresponding prior element then
|
||||
// we just take the config value as-is.
|
||||
newVals[name] = configEV
|
||||
continue
|
||||
}
|
||||
priorEV := prior.GetAttr(name)
|
||||
|
||||
newEV := ProposedNew(&schema.Block, priorEV, configEV)
|
||||
newVals[name] = newEV
|
||||
}
|
||||
// Although we call the nesting mode "map", we actually use
|
||||
// object values so that elements might have different types
|
||||
// in case of dynamically-typed attributes.
|
||||
newV = cty.ObjectVal(newVals)
|
||||
} else {
|
||||
newV = cty.EmptyObjectVal
|
||||
}
|
||||
} else {
|
||||
configVLen := 0
|
||||
if config.IsKnown() && !config.IsNull() {
|
||||
configVLen = config.LengthInt()
|
||||
}
|
||||
if configVLen > 0 {
|
||||
newVals := make(map[string]cty.Value, configVLen)
|
||||
for it := config.ElementIterator(); it.Next(); {
|
||||
idx, configEV := it.Element()
|
||||
k := idx.AsString()
|
||||
if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
|
||||
// If there is no corresponding prior element then
|
||||
// we just take the config value as-is.
|
||||
newVals[k] = configEV
|
||||
continue
|
||||
}
|
||||
priorEV := prior.Index(idx)
|
||||
|
||||
newEV := ProposedNew(&schema.Block, priorEV, configEV)
|
||||
newVals[k] = newEV
|
||||
}
|
||||
newV = cty.MapVal(newVals)
|
||||
} else {
|
||||
newV = cty.MapValEmpty(schema.ImpliedType())
|
||||
}
|
||||
}
|
||||
|
||||
case configschema.NestingSet:
|
||||
if !config.Type().IsSetType() {
|
||||
panic("configschema.NestingSet value is not a set as expected")
|
||||
}
|
||||
|
||||
// Nested blocks are correlated by comparing the element values
|
||||
// after eliminating all of the computed attributes. In practice,
|
||||
// this means that any config change produces an entirely new
|
||||
// nested object, and we only propagate prior computed values
|
||||
// if the non-computed attribute values are identical.
|
||||
var cmpVals [][2]cty.Value
|
||||
if prior.IsKnown() && !prior.IsNull() {
|
||||
cmpVals = setElementCompareValues(&schema.Block, prior, false)
|
||||
}
|
||||
configVLen := 0
|
||||
if config.IsKnown() && !config.IsNull() {
|
||||
configVLen = config.LengthInt()
|
||||
}
|
||||
if configVLen > 0 {
|
||||
used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value
|
||||
newVals := make([]cty.Value, 0, configVLen)
|
||||
for it := config.ElementIterator(); it.Next(); {
|
||||
_, configEV := it.Element()
|
||||
var priorEV cty.Value
|
||||
for i, cmp := range cmpVals {
|
||||
if used[i] {
|
||||
continue
|
||||
}
|
||||
if cmp[1].RawEquals(configEV) {
|
||||
priorEV = cmp[0]
|
||||
used[i] = true // we can't use this value on a future iteration
|
||||
break
|
||||
}
|
||||
}
|
||||
if priorEV == cty.NilVal {
|
||||
priorEV = cty.NullVal(schema.ImpliedType())
|
||||
}
|
||||
|
||||
newEV := ProposedNew(&schema.Block, priorEV, configEV)
|
||||
newVals = append(newVals, newEV)
|
||||
}
|
||||
newV = cty.SetVal(newVals)
|
||||
} else {
|
||||
newV = cty.SetValEmpty(schema.Block.ImpliedType())
|
||||
}
|
||||
|
||||
default:
|
||||
// Should never happen, since the above cases are comprehensive.
|
||||
panic(fmt.Sprintf("unsupported block nesting mode %s", schema.Nesting))
|
||||
}
|
||||
return newV
|
||||
}
|
||||
|
||||
func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, config cty.Value) map[string]cty.Value {
|
||||
if prior.IsNull() {
|
||||
prior = AllAttributesNull(attrs)
|
||||
}
|
||||
newAttrs := make(map[string]cty.Value, len(attrs))
|
||||
for name, attr := range attrs {
|
||||
priorV := prior.GetAttr(name)
|
||||
configV := config.GetAttr(name)
|
||||
var newV cty.Value
|
||||
|
@ -102,181 +279,170 @@ func proposedNewObject(schema *configschema.Block, prior, config cty.Value) cty.
|
|||
// priorV may also be null, but that's okay.
|
||||
newV = priorV
|
||||
default:
|
||||
// For non-computed attributes, we always take the config value,
|
||||
// even if it is null. If it's _required_ then null values
|
||||
// should've been caught during an earlier validation step, and
|
||||
// so we don't really care about that here.
|
||||
newV = configV
|
||||
if attr.NestedType != nil {
|
||||
// For non-computed NestedType attributes, we need to descend
|
||||
// into the individual nested attributes to build the final
|
||||
// value, unless the entire nested attribute is unknown.
|
||||
if !configV.IsKnown() {
|
||||
newV = configV
|
||||
} else {
|
||||
newV = proposedNewNestedType(attr.NestedType, priorV, configV)
|
||||
}
|
||||
} else {
|
||||
// For non-computed attributes, we always take the config value,
|
||||
// even if it is null. If it's _required_ then null values
|
||||
// should've been caught during an earlier validation step, and
|
||||
// so we don't really care about that here.
|
||||
newV = configV
|
||||
}
|
||||
}
|
||||
newAttrs[name] = newV
|
||||
}
|
||||
return newAttrs
|
||||
}
|
||||
|
||||
// Merging nested blocks is a little more complex, since we need to
|
||||
// correlate blocks between both objects and then recursively propose
|
||||
// a new object for each. The correlation logic depends on the nesting
|
||||
// mode for each block type.
|
||||
for name, blockType := range schema.BlockTypes {
|
||||
priorV := prior.GetAttr(name)
|
||||
configV := config.GetAttr(name)
|
||||
var newV cty.Value
|
||||
switch blockType.Nesting {
|
||||
func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value) cty.Value {
|
||||
var newV cty.Value
|
||||
switch schema.Nesting {
|
||||
case configschema.NestingSingle:
|
||||
newAttrs := proposedNewAttributes(schema.Attributes, prior, config)
|
||||
newV = cty.ObjectVal(newAttrs)
|
||||
|
||||
case configschema.NestingSingle, configschema.NestingGroup:
|
||||
newV = ProposedNewObject(&blockType.Block, priorV, configV)
|
||||
case configschema.NestingList:
|
||||
// Nested blocks are correlated by index.
|
||||
configVLen := 0
|
||||
if config.IsKnown() && !config.IsNull() {
|
||||
configVLen = config.LengthInt()
|
||||
}
|
||||
if configVLen > 0 {
|
||||
newVals := make([]cty.Value, 0, configVLen)
|
||||
for it := config.ElementIterator(); it.Next(); {
|
||||
idx, configEV := it.Element()
|
||||
if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
|
||||
// If there is no corresponding prior element then
|
||||
// we just take the config value as-is.
|
||||
newVals = append(newVals, configEV)
|
||||
continue
|
||||
}
|
||||
priorEV := prior.Index(idx)
|
||||
|
||||
case configschema.NestingList:
|
||||
// Nested blocks are correlated by index.
|
||||
newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
|
||||
newVals = append(newVals, cty.ObjectVal(newEV))
|
||||
}
|
||||
// Despite the name, a NestingList might also be a tuple, if
|
||||
// its nested schema contains dynamically-typed attributes.
|
||||
if config.Type().IsTupleType() {
|
||||
newV = cty.TupleVal(newVals)
|
||||
} else {
|
||||
newV = cty.ListVal(newVals)
|
||||
}
|
||||
} else {
|
||||
newV = cty.NullVal(schema.ImpliedType())
|
||||
}
|
||||
|
||||
case configschema.NestingMap:
|
||||
// Despite the name, a NestingMap may produce either a map or
|
||||
// object value, depending on whether the nested schema contains
|
||||
// dynamically-typed attributes.
|
||||
if config.Type().IsObjectType() {
|
||||
// Nested blocks are correlated by key.
|
||||
configVLen := 0
|
||||
if configV.IsKnown() && !configV.IsNull() {
|
||||
configVLen = configV.LengthInt()
|
||||
if config.IsKnown() && !config.IsNull() {
|
||||
configVLen = config.LengthInt()
|
||||
}
|
||||
if configVLen > 0 {
|
||||
newVals := make([]cty.Value, 0, configVLen)
|
||||
for it := configV.ElementIterator(); it.Next(); {
|
||||
idx, configEV := it.Element()
|
||||
if priorV.IsKnown() && (priorV.IsNull() || !priorV.HasIndex(idx).True()) {
|
||||
newVals := make(map[string]cty.Value, configVLen)
|
||||
atys := config.Type().AttributeTypes()
|
||||
for name := range atys {
|
||||
configEV := config.GetAttr(name)
|
||||
if !prior.IsKnown() || prior.IsNull() || !prior.Type().HasAttribute(name) {
|
||||
// If there is no corresponding prior element then
|
||||
// we just take the config value as-is.
|
||||
newVals = append(newVals, configEV)
|
||||
newVals[name] = configEV
|
||||
continue
|
||||
}
|
||||
priorEV := priorV.Index(idx)
|
||||
|
||||
newEV := ProposedNewObject(&blockType.Block, priorEV, configEV)
|
||||
newVals = append(newVals, newEV)
|
||||
}
|
||||
// Despite the name, a NestingList might also be a tuple, if
|
||||
// its nested schema contains dynamically-typed attributes.
|
||||
if configV.Type().IsTupleType() {
|
||||
newV = cty.TupleVal(newVals)
|
||||
} else {
|
||||
newV = cty.ListVal(newVals)
|
||||
priorEV := prior.GetAttr(name)
|
||||
newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
|
||||
newVals[name] = cty.ObjectVal(newEV)
|
||||
}
|
||||
// Although we call the nesting mode "map", we actually use
|
||||
// object values so that elements might have different types
|
||||
// in case of dynamically-typed attributes.
|
||||
newV = cty.ObjectVal(newVals)
|
||||
} else {
|
||||
// Despite the name, a NestingList might also be a tuple, if
|
||||
// its nested schema contains dynamically-typed attributes.
|
||||
if configV.Type().IsTupleType() {
|
||||
newV = cty.EmptyTupleVal
|
||||
} else {
|
||||
newV = cty.ListValEmpty(blockType.ImpliedType())
|
||||
}
|
||||
}
|
||||
|
||||
case configschema.NestingMap:
|
||||
// Despite the name, a NestingMap may produce either a map or
|
||||
// object value, depending on whether the nested schema contains
|
||||
// dynamically-typed attributes.
|
||||
if configV.Type().IsObjectType() {
|
||||
// Nested blocks are correlated by key.
|
||||
configVLen := 0
|
||||
if configV.IsKnown() && !configV.IsNull() {
|
||||
configVLen = configV.LengthInt()
|
||||
}
|
||||
if configVLen > 0 {
|
||||
newVals := make(map[string]cty.Value, configVLen)
|
||||
atys := configV.Type().AttributeTypes()
|
||||
for name := range atys {
|
||||
configEV := configV.GetAttr(name)
|
||||
if !priorV.IsKnown() || priorV.IsNull() || !priorV.Type().HasAttribute(name) {
|
||||
// If there is no corresponding prior element then
|
||||
// we just take the config value as-is.
|
||||
newVals[name] = configEV
|
||||
continue
|
||||
}
|
||||
priorEV := priorV.GetAttr(name)
|
||||
|
||||
newEV := ProposedNewObject(&blockType.Block, priorEV, configEV)
|
||||
newVals[name] = newEV
|
||||
}
|
||||
// Although we call the nesting mode "map", we actually use
|
||||
// object values so that elements might have different types
|
||||
// in case of dynamically-typed attributes.
|
||||
newV = cty.ObjectVal(newVals)
|
||||
} else {
|
||||
newV = cty.EmptyObjectVal
|
||||
}
|
||||
} else {
|
||||
configVLen := 0
|
||||
if configV.IsKnown() && !configV.IsNull() {
|
||||
configVLen = configV.LengthInt()
|
||||
}
|
||||
if configVLen > 0 {
|
||||
newVals := make(map[string]cty.Value, configVLen)
|
||||
for it := configV.ElementIterator(); it.Next(); {
|
||||
idx, configEV := it.Element()
|
||||
k := idx.AsString()
|
||||
if priorV.IsKnown() && (priorV.IsNull() || !priorV.HasIndex(idx).True()) {
|
||||
// If there is no corresponding prior element then
|
||||
// we just take the config value as-is.
|
||||
newVals[k] = configEV
|
||||
continue
|
||||
}
|
||||
priorEV := priorV.Index(idx)
|
||||
|
||||
newEV := ProposedNewObject(&blockType.Block, priorEV, configEV)
|
||||
newVals[k] = newEV
|
||||
}
|
||||
newV = cty.MapVal(newVals)
|
||||
} else {
|
||||
newV = cty.MapValEmpty(blockType.ImpliedType())
|
||||
}
|
||||
}
|
||||
|
||||
case configschema.NestingSet:
|
||||
if !configV.Type().IsSetType() {
|
||||
panic("configschema.NestingSet value is not a set as expected")
|
||||
}
|
||||
|
||||
// Nested blocks are correlated by comparing the element values
|
||||
// after eliminating all of the computed attributes. In practice,
|
||||
// this means that any config change produces an entirely new
|
||||
// nested object, and we only propagate prior computed values
|
||||
// if the non-computed attribute values are identical.
|
||||
var cmpVals [][2]cty.Value
|
||||
if priorV.IsKnown() && !priorV.IsNull() {
|
||||
cmpVals = setElementCompareValues(&blockType.Block, priorV, false)
|
||||
newV = cty.NullVal(schema.ImpliedType())
|
||||
}
|
||||
} else {
|
||||
configVLen := 0
|
||||
if configV.IsKnown() && !configV.IsNull() {
|
||||
configVLen = configV.LengthInt()
|
||||
if config.IsKnown() && !config.IsNull() {
|
||||
configVLen = config.LengthInt()
|
||||
}
|
||||
if configVLen > 0 {
|
||||
used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value
|
||||
newVals := make([]cty.Value, 0, configVLen)
|
||||
for it := configV.ElementIterator(); it.Next(); {
|
||||
_, configEV := it.Element()
|
||||
var priorEV cty.Value
|
||||
for i, cmp := range cmpVals {
|
||||
if used[i] {
|
||||
continue
|
||||
}
|
||||
if cmp[1].RawEquals(configEV) {
|
||||
priorEV = cmp[0]
|
||||
used[i] = true // we can't use this value on a future iteration
|
||||
break
|
||||
}
|
||||
}
|
||||
if priorEV == cty.NilVal {
|
||||
priorEV = cty.NullVal(blockType.ImpliedType())
|
||||
newVals := make(map[string]cty.Value, configVLen)
|
||||
for it := config.ElementIterator(); it.Next(); {
|
||||
idx, configEV := it.Element()
|
||||
k := idx.AsString()
|
||||
if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
|
||||
// If there is no corresponding prior element then
|
||||
// we just take the config value as-is.
|
||||
newVals[k] = configEV
|
||||
continue
|
||||
}
|
||||
priorEV := prior.Index(idx)
|
||||
|
||||
newEV := ProposedNewObject(&blockType.Block, priorEV, configEV)
|
||||
newVals = append(newVals, newEV)
|
||||
newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
|
||||
newVals[k] = cty.ObjectVal(newEV)
|
||||
}
|
||||
newV = cty.SetVal(newVals)
|
||||
newV = cty.MapVal(newVals)
|
||||
} else {
|
||||
newV = cty.SetValEmpty(blockType.Block.ImpliedType())
|
||||
newV = cty.NullVal(schema.ImpliedType())
|
||||
}
|
||||
|
||||
default:
|
||||
// Should never happen, since the above cases are comprehensive.
|
||||
panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting))
|
||||
}
|
||||
|
||||
newAttrs[name] = newV
|
||||
case configschema.NestingSet:
|
||||
// Nested blocks are correlated by comparing the element values
|
||||
// after eliminating all of the computed attributes. In practice,
|
||||
// this means that any config change produces an entirely new
|
||||
// nested object, and we only propagate prior computed values
|
||||
// if the non-computed attribute values are identical.
|
||||
var cmpVals [][2]cty.Value
|
||||
if prior.IsKnown() && !prior.IsNull() {
|
||||
cmpVals = setElementCompareValuesFromObject(schema, prior)
|
||||
}
|
||||
configVLen := 0
|
||||
if config.IsKnown() && !config.IsNull() {
|
||||
configVLen = config.LengthInt()
|
||||
}
|
||||
if configVLen > 0 {
|
||||
used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value
|
||||
newVals := make([]cty.Value, 0, configVLen)
|
||||
for it := config.ElementIterator(); it.Next(); {
|
||||
_, configEV := it.Element()
|
||||
var priorEV cty.Value
|
||||
for i, cmp := range cmpVals {
|
||||
if used[i] {
|
||||
continue
|
||||
}
|
||||
if cmp[1].RawEquals(configEV) {
|
||||
priorEV = cmp[0]
|
||||
used[i] = true // we can't use this value on a future iteration
|
||||
break
|
||||
}
|
||||
}
|
||||
if priorEV == cty.NilVal {
|
||||
newVals = append(newVals, configEV)
|
||||
} else {
|
||||
newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
|
||||
newVals = append(newVals, cty.ObjectVal(newEV))
|
||||
}
|
||||
}
|
||||
newV = cty.SetVal(newVals)
|
||||
} else {
|
||||
newV = cty.NullVal(schema.ImpliedType())
|
||||
}
|
||||
}
|
||||
|
||||
return cty.ObjectVal(newAttrs)
|
||||
return newV
|
||||
}
|
||||
|
||||
// setElementCompareValues takes a known, non-null value of a cty.Set type and
|
||||
|
@ -290,7 +456,7 @@ func proposedNewObject(schema *configschema.Block, prior, config cty.Value) cty.
|
|||
// value and the one-indexed element is the corresponding "compare value".
|
||||
//
|
||||
// This is intended to help correlate prior elements with configured elements
|
||||
// in ProposedNewObject. The result is a heuristic rather than an exact science,
|
||||
// in proposedNewBlock. The result is a heuristic rather than an exact science,
|
||||
// since e.g. two separate elements may reduce to the same value through this
|
||||
// process. The caller must therefore be ready to deal with duplicates.
|
||||
func setElementCompareValues(schema *configschema.Block, set cty.Value, isConfig bool) [][2]cty.Value {
|
||||
|
@ -407,3 +573,51 @@ func setElementCompareValue(schema *configschema.Block, v cty.Value, isConfig bo
|
|||
|
||||
return cty.ObjectVal(attrs)
|
||||
}
|
||||
|
||||
// setElementCompareValues takes a known, non-null value of a cty.Set type and
|
||||
// returns a table -- constructed of two-element arrays -- that maps original
|
||||
// set element values to corresponding values that have all of the computed
|
||||
// values removed, making them suitable for comparison with values obtained
|
||||
// from configuration. The element type of the set must conform to the implied
|
||||
// type of the given schema, or this function will panic.
|
||||
//
|
||||
// In the resulting slice, the zeroth element of each array is the original
|
||||
// value and the one-indexed element is the corresponding "compare value".
|
||||
//
|
||||
// This is intended to help correlate prior elements with configured elements
|
||||
// in proposedNewBlock. The result is a heuristic rather than an exact science,
|
||||
// since e.g. two separate elements may reduce to the same value through this
|
||||
// process. The caller must therefore be ready to deal with duplicates.
|
||||
func setElementCompareValuesFromObject(schema *configschema.Object, set cty.Value) [][2]cty.Value {
|
||||
ret := make([][2]cty.Value, 0, set.LengthInt())
|
||||
for it := set.ElementIterator(); it.Next(); {
|
||||
_, ev := it.Element()
|
||||
ret = append(ret, [2]cty.Value{ev, setElementCompareValueFromObject(schema, ev)})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// setElementCompareValue creates a new value that has all of the same
|
||||
// non-computed attribute values as the one given but has all computed
|
||||
// attribute values forced to null.
|
||||
//
|
||||
// The input value must conform to the schema's implied type, and the return
|
||||
// value is guaranteed to conform to it.
|
||||
func setElementCompareValueFromObject(schema *configschema.Object, v cty.Value) cty.Value {
|
||||
if v.IsNull() || !v.IsKnown() {
|
||||
return v
|
||||
}
|
||||
attrs := map[string]cty.Value{}
|
||||
|
||||
for name, attr := range schema.Attributes {
|
||||
attrV := v.GetAttr(name)
|
||||
switch {
|
||||
case attr.Computed:
|
||||
attrs[name] = cty.NullVal(attr.Type)
|
||||
default:
|
||||
attrs[name] = attrV
|
||||
}
|
||||
}
|
||||
|
||||
return cty.ObjectVal(attrs)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
)
|
||||
|
||||
func TestProposedNewObject(t *testing.T) {
|
||||
func TestProposedNew(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
Schema *configschema.Block
|
||||
Prior cty.Value
|
||||
|
@ -33,6 +33,18 @@ func TestProposedNewObject(t *testing.T) {
|
|||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blop": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"baz": {
|
||||
|
@ -57,6 +69,9 @@ func TestProposedNewObject(t *testing.T) {
|
|||
cty.NullVal(cty.DynamicPseudoType),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("hello"),
|
||||
"bloop": cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"blop": cty.String,
|
||||
})),
|
||||
"bar": cty.NullVal(cty.String),
|
||||
"baz": cty.ObjectVal(map[string]cty.Value{
|
||||
"boz": cty.StringVal("world"),
|
||||
|
@ -76,6 +91,9 @@ func TestProposedNewObject(t *testing.T) {
|
|||
// usually changes them to "unknown" during PlanResourceChange,
|
||||
// to indicate that the value will be decided during apply.
|
||||
"bar": cty.NullVal(cty.String),
|
||||
"bloop": cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"blop": cty.String,
|
||||
})),
|
||||
|
||||
"baz": cty.ObjectVal(map[string]cty.Value{
|
||||
"boz": cty.StringVal("world"),
|
||||
|
@ -90,6 +108,18 @@ func TestProposedNewObject(t *testing.T) {
|
|||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blop": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"baz": {
|
||||
|
@ -109,14 +139,20 @@ func TestProposedNewObject(t *testing.T) {
|
|||
cty.NullVal(cty.DynamicPseudoType),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("bar"),
|
||||
"bloop": cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"blop": cty.String,
|
||||
})),
|
||||
"baz": cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"boz": cty.String,
|
||||
})),
|
||||
}),
|
||||
// The baz block does not exist in the config, and therefore
|
||||
// shouldn't be planned.
|
||||
// The bloop attribue and baz block does not exist in the config,
|
||||
// and therefore shouldn't be planned.
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("bar"),
|
||||
"bloop": cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"blop": cty.String,
|
||||
})),
|
||||
"baz": cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"boz": cty.String,
|
||||
})),
|
||||
|
@ -141,6 +177,21 @@ func TestProposedNewObject(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSet,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blop": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Computed: true,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.NullVal(cty.DynamicPseudoType),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
|
@ -149,6 +200,11 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"boz": cty.StringVal("world"),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("blub"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"baz": cty.SetVal([]cty.Value{
|
||||
|
@ -156,6 +212,11 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"boz": cty.StringVal("world"),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("blub"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
"prior attributes": {
|
||||
|
@ -179,6 +240,18 @@ func TestProposedNewObject(t *testing.T) {
|
|||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blop": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
|
@ -186,18 +259,27 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"bar": cty.StringVal("petit dejeuner"),
|
||||
"baz": cty.StringVal("grande dejeuner"),
|
||||
"boz": cty.StringVal("a la monde"),
|
||||
"bloop": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("glub"),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("hello"),
|
||||
"bar": cty.NullVal(cty.String),
|
||||
"baz": cty.NullVal(cty.String),
|
||||
"boz": cty.StringVal("world"),
|
||||
"bloop": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("bleep"),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("hello"),
|
||||
"bar": cty.StringVal("petit dejeuner"),
|
||||
"baz": cty.StringVal("grande dejeuner"),
|
||||
"boz": cty.StringVal("world"),
|
||||
"bloop": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("bleep"),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
"prior nested single": {
|
||||
|
@ -221,24 +303,54 @@ func TestProposedNewObject(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blop": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
"bleep": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ObjectVal(map[string]cty.Value{
|
||||
"bar": cty.StringVal("beep"),
|
||||
"baz": cty.StringVal("boop"),
|
||||
}),
|
||||
"bloop": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("glub"),
|
||||
"bleep": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ObjectVal(map[string]cty.Value{
|
||||
"bar": cty.StringVal("bap"),
|
||||
"baz": cty.NullVal(cty.String),
|
||||
}),
|
||||
"bloop": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("glub"),
|
||||
"bleep": cty.StringVal("beep"),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ObjectVal(map[string]cty.Value{
|
||||
"bar": cty.StringVal("bap"),
|
||||
"baz": cty.StringVal("boop"),
|
||||
}),
|
||||
"bloop": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("glub"),
|
||||
"bleep": cty.StringVal("beep"),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
"prior nested list": {
|
||||
|
@ -262,6 +374,20 @@ func TestProposedNewObject(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blop": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ListVal([]cty.Value{
|
||||
|
@ -270,6 +396,14 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"baz": cty.StringVal("boop"),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("bar"),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("baz"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ListVal([]cty.Value{
|
||||
|
@ -282,6 +416,14 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"baz": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("bar"),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("baz"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ListVal([]cty.Value{
|
||||
|
@ -294,6 +436,14 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"baz": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("bar"),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("baz"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
"prior nested list with dynamic": {
|
||||
|
@ -317,6 +467,24 @@ func TestProposedNewObject(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blop": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
Required: true,
|
||||
},
|
||||
"blub": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.TupleVal([]cty.Value{
|
||||
|
@ -325,6 +493,16 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"baz": cty.StringVal("boop"),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("bar"),
|
||||
"blub": cty.StringVal("glub"),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("baz"),
|
||||
"blub": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.TupleVal([]cty.Value{
|
||||
|
@ -337,6 +515,12 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"baz": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("bar"),
|
||||
"blub": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.TupleVal([]cty.Value{
|
||||
|
@ -349,6 +533,12 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"baz": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("bar"),
|
||||
"blub": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
"prior nested map": {
|
||||
|
@ -372,6 +562,20 @@ func TestProposedNewObject(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingMap,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blop": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.MapVal(map[string]cty.Value{
|
||||
|
@ -384,6 +588,14 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"baz": cty.StringVal("boot"),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.MapVal(map[string]cty.Value{
|
||||
"a": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("glub"),
|
||||
}),
|
||||
"b": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("blub"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.MapVal(map[string]cty.Value{
|
||||
|
@ -396,6 +608,14 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"baz": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.MapVal(map[string]cty.Value{
|
||||
"a": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("glub"),
|
||||
}),
|
||||
"c": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("blub"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.MapVal(map[string]cty.Value{
|
||||
|
@ -408,6 +628,14 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"baz": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.MapVal(map[string]cty.Value{
|
||||
"a": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("glub"),
|
||||
}),
|
||||
"c": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("blub"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
"prior nested map with dynamic": {
|
||||
|
@ -431,6 +659,20 @@ func TestProposedNewObject(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingMap,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blop": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ObjectVal(map[string]cty.Value{
|
||||
|
@ -443,6 +685,14 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"baz": cty.ListVal([]cty.Value{cty.StringVal("boot")}),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("glub"),
|
||||
}),
|
||||
"b": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.NumberIntVal(13),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ObjectVal(map[string]cty.Value{
|
||||
|
@ -455,6 +705,14 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"baz": cty.NullVal(cty.List(cty.String)),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("blep"),
|
||||
}),
|
||||
"c": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.NumberIntVal(13),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ObjectVal(map[string]cty.Value{
|
||||
|
@ -467,6 +725,14 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"baz": cty.NullVal(cty.List(cty.String)),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("blep"),
|
||||
}),
|
||||
"c": cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.NumberIntVal(13),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
"prior nested set": {
|
||||
|
@ -492,6 +758,24 @@ func TestProposedNewObject(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSet,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blop": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
"bleep": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.SetVal([]cty.Value{
|
||||
|
@ -504,6 +788,16 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"baz": cty.StringVal("boot"),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("glubglub"),
|
||||
"bleep": cty.NullVal(cty.String),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("glubglub"),
|
||||
"bleep": cty.StringVal("beep"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.SetVal([]cty.Value{
|
||||
|
@ -516,6 +810,16 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"baz": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("glubglub"),
|
||||
"bleep": cty.NullVal(cty.String),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("glub"),
|
||||
"bleep": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.SetVal([]cty.Value{
|
||||
|
@ -528,6 +832,16 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"baz": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("glubglub"),
|
||||
"bleep": cty.NullVal(cty.String),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("glub"),
|
||||
"bleep": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
"sets differing only by unknown": {
|
||||
|
@ -546,6 +860,20 @@ func TestProposedNewObject(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSet,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blop": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.NullVal(cty.DynamicPseudoType),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
|
@ -557,6 +885,14 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"optional": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"multi": cty.SetVal([]cty.Value{
|
||||
|
@ -570,6 +906,14 @@ func TestProposedNewObject(t *testing.T) {
|
|||
"optional": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
"bloop": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
"nested list in set": {
|
||||
|
@ -854,14 +1198,297 @@ func TestProposedNewObject(t *testing.T) {
|
|||
}),
|
||||
}),
|
||||
},
|
||||
// This example has a mixture of optional, computed and required in a deeply-nested NestedType attribute
|
||||
"deeply NestedType": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"foo": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bar": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Attributes: testAttributes,
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
"baz": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Attributes: testAttributes,
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// prior
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ObjectVal(map[string]cty.Value{
|
||||
"bar": cty.NullVal(cty.DynamicPseudoType),
|
||||
"baz": cty.ObjectVal(map[string]cty.Value{
|
||||
"optional": cty.NullVal(cty.String),
|
||||
"computed": cty.StringVal("hello"),
|
||||
"optional_computed": cty.StringVal("prior"),
|
||||
"required": cty.StringVal("present"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
// config
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ObjectVal(map[string]cty.Value{
|
||||
"bar": cty.UnknownVal(cty.Object(map[string]cty.Type{ // explicit unknown from the config
|
||||
"optional": cty.String,
|
||||
"computed": cty.String,
|
||||
"optional_computed": cty.String,
|
||||
"required": cty.String,
|
||||
})),
|
||||
"baz": cty.ObjectVal(map[string]cty.Value{
|
||||
"optional": cty.NullVal(cty.String),
|
||||
"computed": cty.NullVal(cty.String),
|
||||
"optional_computed": cty.StringVal("hello"),
|
||||
"required": cty.StringVal("present"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
// want
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ObjectVal(map[string]cty.Value{
|
||||
"bar": cty.UnknownVal(cty.Object(map[string]cty.Type{ // explicit unknown preserved from the config
|
||||
"optional": cty.String,
|
||||
"computed": cty.String,
|
||||
"optional_computed": cty.String,
|
||||
"required": cty.String,
|
||||
})),
|
||||
"baz": cty.ObjectVal(map[string]cty.Value{
|
||||
"optional": cty.NullVal(cty.String), // config is null
|
||||
"computed": cty.StringVal("hello"), // computed values come from prior
|
||||
"optional_computed": cty.StringVal("hello"), // config takes precedent over prior in opt+computed
|
||||
"required": cty.StringVal("present"), // value from config
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
"deeply nested set": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"foo": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSet,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bar": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSet,
|
||||
Attributes: testAttributes,
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// prior values
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bar": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"optional": cty.StringVal("prior"),
|
||||
"computed": cty.StringVal("prior"),
|
||||
"optional_computed": cty.StringVal("prior"),
|
||||
"required": cty.StringVal("prior"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bar": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
||||
"optional": cty.StringVal("other_prior"),
|
||||
"computed": cty.StringVal("other_prior"),
|
||||
"optional_computed": cty.StringVal("other_prior"),
|
||||
"required": cty.StringVal("other_prior"),
|
||||
})}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
// config differs from prior
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bar": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
||||
"optional": cty.StringVal("configured"),
|
||||
"computed": cty.NullVal(cty.String), // computed attrs are null in config
|
||||
"optional_computed": cty.StringVal("configured"),
|
||||
"required": cty.StringVal("configured"),
|
||||
})}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bar": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
||||
"optional": cty.NullVal(cty.String), // explicit null in config
|
||||
"computed": cty.NullVal(cty.String), // computed attrs are null in config
|
||||
"optional_computed": cty.StringVal("other_configured"),
|
||||
"required": cty.StringVal("other_configured"),
|
||||
})}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
// want:
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bar": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
||||
"optional": cty.StringVal("configured"),
|
||||
"computed": cty.NullVal(cty.String),
|
||||
"optional_computed": cty.StringVal("configured"),
|
||||
"required": cty.StringVal("configured"),
|
||||
})}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bar": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
|
||||
"optional": cty.NullVal(cty.String), // explicit null in config is preserved
|
||||
"computed": cty.NullVal(cty.String),
|
||||
"optional_computed": cty.StringVal("other_configured"),
|
||||
"required": cty.StringVal("other_configured"),
|
||||
})}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
"expected null NestedTypes": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"single": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bar": {Type: cty.String},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
"list": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bar": {Type: cty.String},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
"set": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSet,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bar": {Type: cty.String},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
"map": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingMap,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bar": {Type: cty.String},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
"nested_map": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingMap,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"inner": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Attributes: testAttributes,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"single": cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")}),
|
||||
"list": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")})}),
|
||||
"map": cty.MapVal(map[string]cty.Value{
|
||||
"map_entry": cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")}),
|
||||
}),
|
||||
"set": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")})}),
|
||||
"nested_map": cty.MapVal(map[string]cty.Value{
|
||||
"a": cty.ObjectVal(map[string]cty.Value{
|
||||
"inner": cty.ObjectVal(map[string]cty.Value{
|
||||
"optional": cty.StringVal("foo"),
|
||||
"computed": cty.StringVal("foo"),
|
||||
"optional_computed": cty.StringVal("foo"),
|
||||
"required": cty.StringVal("foo"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"single": cty.ObjectVal(map[string]cty.Value{"bar": cty.NullVal(cty.String)}),
|
||||
"list": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{"bar": cty.String}))),
|
||||
"map": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{"bar": cty.String}))),
|
||||
"set": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{"bar": cty.String}))),
|
||||
"nested_map": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
|
||||
"inner": cty.Object(map[string]cty.Type{
|
||||
"optional": cty.String,
|
||||
"computed": cty.String,
|
||||
"optional_computed": cty.String,
|
||||
"required": cty.String,
|
||||
}),
|
||||
}))),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"single": cty.ObjectVal(map[string]cty.Value{"bar": cty.NullVal(cty.String)}),
|
||||
"list": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{"bar": cty.String}))),
|
||||
"map": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{"bar": cty.String}))),
|
||||
"set": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{"bar": cty.String}))),
|
||||
"nested_map": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
|
||||
"inner": cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"optional": cty.String,
|
||||
"computed": cty.String,
|
||||
"optional_computed": cty.String,
|
||||
"required": cty.String,
|
||||
}, []string{"optional", "optional_computed"}),
|
||||
}))),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := ProposedNewObject(test.Schema, test.Prior, test.Config)
|
||||
got := ProposedNew(test.Schema, test.Prior, test.Config)
|
||||
if !got.RawEquals(test.Want) {
|
||||
t.Errorf("wrong result\ngot: %swant: %s", dump.Value(got), dump.Value(test.Want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var testAttributes = map[string]*configschema.Attribute{
|
||||
"optional": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"computed": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"optional_computed": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
Optional: true,
|
||||
},
|
||||
"required": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -53,18 +53,10 @@ func assertPlanValid(schema *configschema.Block, priorState, config, plannedStat
|
|||
|
||||
impTy := schema.ImpliedType()
|
||||
|
||||
for name, attrS := range schema.Attributes {
|
||||
plannedV := plannedState.GetAttr(name)
|
||||
configV := config.GetAttr(name)
|
||||
priorV := cty.NullVal(attrS.Type)
|
||||
if !priorState.IsNull() {
|
||||
priorV = priorState.GetAttr(name)
|
||||
}
|
||||
// verify attributes
|
||||
moreErrs := assertPlannedAttrsValid(schema.Attributes, priorState, config, plannedState, path)
|
||||
errs = append(errs, moreErrs...)
|
||||
|
||||
path := append(path, cty.GetAttrStep{Name: name})
|
||||
moreErrs := assertPlannedValueValid(attrS, priorV, configV, plannedV, path)
|
||||
errs = append(errs, moreErrs...)
|
||||
}
|
||||
for name, blockS := range schema.BlockTypes {
|
||||
path := append(path, cty.GetAttrStep{Name: name})
|
||||
plannedV := plannedState.GetAttr(name)
|
||||
|
@ -229,13 +221,34 @@ func assertPlanValid(schema *configschema.Block, priorState, config, plannedStat
|
|||
return errs
|
||||
}
|
||||
|
||||
func assertPlannedAttrsValid(schema map[string]*configschema.Attribute, priorState, config, plannedState cty.Value, path cty.Path) []error {
|
||||
var errs []error
|
||||
for name, attrS := range schema {
|
||||
moreErrs := assertPlannedAttrValid(name, attrS, priorState, config, plannedState, path)
|
||||
errs = append(errs, moreErrs...)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func assertPlannedAttrValid(name string, attrS *configschema.Attribute, priorState, config, plannedState cty.Value, path cty.Path) []error {
|
||||
plannedV := plannedState.GetAttr(name)
|
||||
configV := config.GetAttr(name)
|
||||
priorV := cty.NullVal(attrS.Type)
|
||||
if !priorState.IsNull() {
|
||||
priorV = priorState.GetAttr(name)
|
||||
}
|
||||
path = append(path, cty.GetAttrStep{Name: name})
|
||||
|
||||
return assertPlannedValueValid(attrS, priorV, configV, plannedV, path)
|
||||
}
|
||||
|
||||
func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, plannedV cty.Value, path cty.Path) []error {
|
||||
var errs []error
|
||||
if plannedV.RawEquals(configV) {
|
||||
// This is the easy path: provider didn't change anything at all.
|
||||
return errs
|
||||
}
|
||||
if plannedV.RawEquals(priorV) && !priorV.IsNull() {
|
||||
if plannedV.RawEquals(priorV) && !priorV.IsNull() && !configV.IsNull() {
|
||||
// Also pretty easy: there is a prior value and the provider has
|
||||
// returned it unchanged. This indicates that configV and plannedV
|
||||
// are functionally equivalent and so the provider wishes to disregard
|
||||
|
@ -248,6 +261,11 @@ func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, pla
|
|||
return errs
|
||||
}
|
||||
|
||||
// If this attribute has a NestedType, validate the nested object
|
||||
if attrS.NestedType != nil {
|
||||
return assertPlannedObjectValid(attrS.NestedType, priorV, configV, plannedV, path)
|
||||
}
|
||||
|
||||
// If none of the above conditions match, the provider has made an invalid
|
||||
// change to this attribute.
|
||||
if priorV.IsNull() {
|
||||
|
@ -265,3 +283,151 @@ func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, pla
|
|||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func assertPlannedObjectValid(schema *configschema.Object, prior, config, planned cty.Value, path cty.Path) []error {
|
||||
var errs []error
|
||||
|
||||
if planned.IsNull() && !config.IsNull() {
|
||||
errs = append(errs, path.NewErrorf("planned for absense but config wants existence"))
|
||||
return errs
|
||||
}
|
||||
if config.IsNull() && !planned.IsNull() {
|
||||
errs = append(errs, path.NewErrorf("planned for existence but config wants absense"))
|
||||
return errs
|
||||
}
|
||||
if planned.IsNull() {
|
||||
// No further checks possible if the planned value is null
|
||||
return errs
|
||||
}
|
||||
|
||||
switch schema.Nesting {
|
||||
case configschema.NestingSingle, configschema.NestingGroup:
|
||||
moreErrs := assertPlannedAttrsValid(schema.Attributes, prior, config, planned, path)
|
||||
errs = append(errs, moreErrs...)
|
||||
|
||||
case configschema.NestingList:
|
||||
// A NestingList might either be a list or a tuple, depending on
|
||||
// whether there are dynamically-typed attributes inside. However,
|
||||
// both support a similar-enough API that we can treat them the
|
||||
// same for our purposes here.
|
||||
|
||||
plannedL := planned.LengthInt()
|
||||
configL := config.LengthInt()
|
||||
if plannedL != configL {
|
||||
errs = append(errs, path.NewErrorf("count in plan (%d) disagrees with count in config (%d)", plannedL, configL))
|
||||
return errs
|
||||
}
|
||||
for it := planned.ElementIterator(); it.Next(); {
|
||||
idx, plannedEV := it.Element()
|
||||
path := append(path, cty.IndexStep{Key: idx})
|
||||
if !plannedEV.IsKnown() {
|
||||
errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
|
||||
continue
|
||||
}
|
||||
if !config.HasIndex(idx).True() {
|
||||
continue // should never happen since we checked the lengths above
|
||||
}
|
||||
configEV := config.Index(idx)
|
||||
priorEV := cty.NullVal(schema.ImpliedType())
|
||||
if !prior.IsNull() && prior.HasIndex(idx).True() {
|
||||
priorEV = prior.Index(idx)
|
||||
}
|
||||
|
||||
moreErrs := assertPlannedAttrsValid(schema.Attributes, priorEV, configEV, plannedEV, path)
|
||||
errs = append(errs, moreErrs...)
|
||||
}
|
||||
|
||||
case configschema.NestingMap:
|
||||
// A NestingMap might either be a map or an object, depending on
|
||||
// whether there are dynamically-typed attributes inside, but
|
||||
// that's decided statically and so all values will have the same
|
||||
// kind.
|
||||
if planned.Type().IsObjectType() {
|
||||
plannedAtys := planned.Type().AttributeTypes()
|
||||
configAtys := config.Type().AttributeTypes()
|
||||
for k := range plannedAtys {
|
||||
if _, ok := configAtys[k]; !ok {
|
||||
errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k))
|
||||
continue
|
||||
}
|
||||
path := append(path, cty.GetAttrStep{Name: k})
|
||||
|
||||
plannedEV := planned.GetAttr(k)
|
||||
if !plannedEV.IsKnown() {
|
||||
errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
|
||||
continue
|
||||
}
|
||||
configEV := config.GetAttr(k)
|
||||
priorEV := cty.NullVal(schema.ImpliedType())
|
||||
if !prior.IsNull() && prior.Type().HasAttribute(k) {
|
||||
priorEV = prior.GetAttr(k)
|
||||
}
|
||||
moreErrs := assertPlannedAttrsValid(schema.Attributes, priorEV, configEV, plannedEV, path)
|
||||
errs = append(errs, moreErrs...)
|
||||
}
|
||||
for k := range configAtys {
|
||||
if _, ok := plannedAtys[k]; !ok {
|
||||
errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", k))
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
plannedL := planned.LengthInt()
|
||||
configL := config.LengthInt()
|
||||
if plannedL != configL {
|
||||
errs = append(errs, path.NewErrorf("block count in plan (%d) disagrees with count in config (%d)", plannedL, configL))
|
||||
return errs
|
||||
}
|
||||
for it := planned.ElementIterator(); it.Next(); {
|
||||
idx, plannedEV := it.Element()
|
||||
path := append(path, cty.IndexStep{Key: idx})
|
||||
if !plannedEV.IsKnown() {
|
||||
errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
|
||||
continue
|
||||
}
|
||||
k := idx.AsString()
|
||||
if !config.HasIndex(idx).True() {
|
||||
errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k))
|
||||
continue
|
||||
}
|
||||
configEV := config.Index(idx)
|
||||
priorEV := cty.NullVal(schema.ImpliedType())
|
||||
if !prior.IsNull() && prior.HasIndex(idx).True() {
|
||||
priorEV = prior.Index(idx)
|
||||
}
|
||||
moreErrs := assertPlannedObjectValid(schema, priorEV, configEV, plannedEV, path)
|
||||
errs = append(errs, moreErrs...)
|
||||
}
|
||||
for it := config.ElementIterator(); it.Next(); {
|
||||
idx, _ := it.Element()
|
||||
if !planned.HasIndex(idx).True() {
|
||||
errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", idx.AsString()))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case configschema.NestingSet:
|
||||
// Because set elements have no identifier with which to correlate
|
||||
// them, we can't robustly validate the plan for a nested block
|
||||
// backed by a set, and so unfortunately we need to just trust the
|
||||
// provider to do the right thing. :(
|
||||
//
|
||||
// (In principle we could correlate elements by matching the
|
||||
// subset of attributes explicitly set in config, except for the
|
||||
// special diff suppression rule which allows for there to be a
|
||||
// planned value that is constructed by mixing part of a prior
|
||||
// value with part of a config value, creating an entirely new
|
||||
// element that is not present in either prior nor config.)
|
||||
for it := planned.ElementIterator(); it.Next(); {
|
||||
idx, plannedEV := it.Element()
|
||||
path := append(path, cty.IndexStep{Key: idx})
|
||||
if !plannedEV.IsKnown() {
|
||||
errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
|
|
@ -579,6 +579,545 @@ func TestAssertPlanValid(t *testing.T) {
|
|||
}),
|
||||
nil,
|
||||
},
|
||||
|
||||
// Attributes with NestedTypes
|
||||
"NestedType attr, no computed, all match": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"a": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"b": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"b": cty.StringVal("b value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"b": cty.StringVal("b value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"b": cty.StringVal("b value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
"NestedType attr, no computed, plan matches, no prior": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"a": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"b": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"a": cty.List(cty.Object(map[string]cty.Type{
|
||||
"b": cty.String,
|
||||
})),
|
||||
})),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"b": cty.StringVal("c value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"b": cty.StringVal("c value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
"NestedType, no computed, invalid change in plan": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"a": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"b": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"a": cty.List(cty.Object(map[string]cty.Type{
|
||||
"b": cty.String,
|
||||
})),
|
||||
})),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"b": cty.StringVal("c value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"b": cty.StringVal("new c value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
[]string{
|
||||
`.a[0].b: planned value cty.StringVal("new c value") does not match config value cty.StringVal("c value")`,
|
||||
},
|
||||
},
|
||||
"NestedType attr, no computed, invalid change in plan sensitive": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"a": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"b": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"a": cty.List(cty.Object(map[string]cty.Type{
|
||||
"b": cty.String,
|
||||
})),
|
||||
})),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"b": cty.StringVal("b value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"b": cty.StringVal("new b value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
[]string{
|
||||
`.a[0].b: sensitive planned value does not match config value`,
|
||||
},
|
||||
},
|
||||
"NestedType attr, no computed, diff suppression in plan": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"a": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"b": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"b": cty.StringVal("b value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"b": cty.StringVal("new b value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"b": cty.StringVal("b value"), // plan uses value from prior object
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
"NestedType attr, no computed, all null": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"a": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"b": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.NullVal(cty.DynamicPseudoType),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.NullVal(cty.DynamicPseudoType),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.NullVal(cty.DynamicPseudoType),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
"NestedType attr, no computed, all zero value": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"a": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"b": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
|
||||
"b": cty.String,
|
||||
}))),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
|
||||
"b": cty.String,
|
||||
}))),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"a": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
|
||||
"b": cty.String,
|
||||
}))),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
"NestedType NestingSet attribute to null": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSet,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blop": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("ok"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
|
||||
"blop": cty.String,
|
||||
}))),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
|
||||
"blop": cty.String,
|
||||
}))),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
"NestedType deep nested optional set attribute to null": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bleep": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSet,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blome": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bleep": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blome": cty.StringVal("ok"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bleep": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.NullVal(cty.Set(
|
||||
cty.Object(map[string]cty.Type{
|
||||
"blome": cty.String,
|
||||
}),
|
||||
)),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bleep": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.NullVal(cty.List(
|
||||
cty.Object(map[string]cty.Type{
|
||||
"blome": cty.String,
|
||||
}),
|
||||
)),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
"NestedType deep nested set": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bleep": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSet,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blome": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bleep": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blome": cty.StringVal("ok"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
// Note: bloop is null in the config
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bleep": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.NullVal(cty.Set(
|
||||
cty.Object(map[string]cty.Type{
|
||||
"blome": cty.String,
|
||||
}),
|
||||
)),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
// provider sends back the prior value, not matching the config
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bleep": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blome": cty.StringVal("ok"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
nil, // we cannot validate individual set elements, and trust the provider's response
|
||||
},
|
||||
"NestedType nested computed list attribute": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blop": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("ok"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
|
||||
"blop": cty.String,
|
||||
}))),
|
||||
}),
|
||||
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("ok"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
"NestedType nested list attribute to null": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blop": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("ok"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
|
||||
"blop": cty.String,
|
||||
}))),
|
||||
}),
|
||||
|
||||
// provider returned the old value
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("ok"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
[]string{".bloop: planned for existence but config wants absense"},
|
||||
},
|
||||
"NestedType nested set attribute to null": {
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bloop": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSet,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"blop": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("ok"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
|
||||
"blop": cty.String,
|
||||
}))),
|
||||
}),
|
||||
// provider returned the old value
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"bloop": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"blop": cty.StringVal("ok"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
[]string{".bloop: planned for existence but config wants absense"},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
package convert
|
||||
|
||||
import (
|
||||
proto "github.com/hashicorp/terraform/internal/tfplugin6"
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// WarnsAndErrorsToProto converts the warnings and errors return by the legacy
|
||||
// provider to protobuf diagnostics.
|
||||
func WarnsAndErrsToProto(warns []string, errs []error) (diags []*proto.Diagnostic) {
|
||||
for _, w := range warns {
|
||||
diags = AppendProtoDiag(diags, w)
|
||||
}
|
||||
|
||||
for _, e := range errs {
|
||||
diags = AppendProtoDiag(diags, e)
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
// AppendProtoDiag appends a new diagnostic from a warning string or an error.
|
||||
// This panics if d is not a string or error.
|
||||
func AppendProtoDiag(diags []*proto.Diagnostic, d interface{}) []*proto.Diagnostic {
|
||||
switch d := d.(type) {
|
||||
case cty.PathError:
|
||||
ap := PathToAttributePath(d.Path)
|
||||
diags = append(diags, &proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_ERROR,
|
||||
Summary: d.Error(),
|
||||
Attribute: ap,
|
||||
})
|
||||
case error:
|
||||
diags = append(diags, &proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_ERROR,
|
||||
Summary: d.Error(),
|
||||
})
|
||||
case string:
|
||||
diags = append(diags, &proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_WARNING,
|
||||
Summary: d,
|
||||
})
|
||||
case *proto.Diagnostic:
|
||||
diags = append(diags, d)
|
||||
case []*proto.Diagnostic:
|
||||
diags = append(diags, d...)
|
||||
}
|
||||
return diags
|
||||
}
|
||||
|
||||
// ProtoToDiagnostics converts a list of proto.Diagnostics to a tf.Diagnostics.
|
||||
func ProtoToDiagnostics(ds []*proto.Diagnostic) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
for _, d := range ds {
|
||||
var severity tfdiags.Severity
|
||||
|
||||
switch d.Severity {
|
||||
case proto.Diagnostic_ERROR:
|
||||
severity = tfdiags.Error
|
||||
case proto.Diagnostic_WARNING:
|
||||
severity = tfdiags.Warning
|
||||
}
|
||||
|
||||
var newDiag tfdiags.Diagnostic
|
||||
|
||||
// if there's an attribute path, we need to create a AttributeValue diagnostic
|
||||
if d.Attribute != nil {
|
||||
path := AttributePathToPath(d.Attribute)
|
||||
newDiag = tfdiags.AttributeValue(severity, d.Summary, d.Detail, path)
|
||||
} else {
|
||||
newDiag = tfdiags.WholeContainingBody(severity, d.Summary, d.Detail)
|
||||
}
|
||||
|
||||
diags = diags.Append(newDiag)
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
// AttributePathToPath takes the proto encoded path and converts it to a cty.Path
|
||||
func AttributePathToPath(ap *proto.AttributePath) cty.Path {
|
||||
var p cty.Path
|
||||
for _, step := range ap.Steps {
|
||||
switch selector := step.Selector.(type) {
|
||||
case *proto.AttributePath_Step_AttributeName:
|
||||
p = p.GetAttr(selector.AttributeName)
|
||||
case *proto.AttributePath_Step_ElementKeyString:
|
||||
p = p.Index(cty.StringVal(selector.ElementKeyString))
|
||||
case *proto.AttributePath_Step_ElementKeyInt:
|
||||
p = p.Index(cty.NumberIntVal(selector.ElementKeyInt))
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// AttributePathToPath takes a cty.Path and converts it to a proto-encoded path.
|
||||
func PathToAttributePath(p cty.Path) *proto.AttributePath {
|
||||
ap := &proto.AttributePath{}
|
||||
for _, step := range p {
|
||||
switch selector := step.(type) {
|
||||
case cty.GetAttrStep:
|
||||
ap.Steps = append(ap.Steps, &proto.AttributePath_Step{
|
||||
Selector: &proto.AttributePath_Step_AttributeName{
|
||||
AttributeName: selector.Name,
|
||||
},
|
||||
})
|
||||
case cty.IndexStep:
|
||||
key := selector.Key
|
||||
switch key.Type() {
|
||||
case cty.String:
|
||||
ap.Steps = append(ap.Steps, &proto.AttributePath_Step{
|
||||
Selector: &proto.AttributePath_Step_ElementKeyString{
|
||||
ElementKeyString: key.AsString(),
|
||||
},
|
||||
})
|
||||
case cty.Number:
|
||||
v, _ := key.AsBigFloat().Int64()
|
||||
ap.Steps = append(ap.Steps, &proto.AttributePath_Step{
|
||||
Selector: &proto.AttributePath_Step_ElementKeyInt{
|
||||
ElementKeyInt: v,
|
||||
},
|
||||
})
|
||||
default:
|
||||
// We'll bail early if we encounter anything else, and just
|
||||
// return the valid prefix.
|
||||
return ap
|
||||
}
|
||||
}
|
||||
}
|
||||
return ap
|
||||
}
|
|
@ -0,0 +1,367 @@
|
|||
package convert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
proto "github.com/hashicorp/terraform/internal/tfplugin6"
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
var ignoreUnexported = cmpopts.IgnoreUnexported(
|
||||
proto.Diagnostic{},
|
||||
proto.Schema_Block{},
|
||||
proto.Schema_NestedBlock{},
|
||||
proto.Schema_Attribute{},
|
||||
)
|
||||
|
||||
func TestProtoDiagnostics(t *testing.T) {
|
||||
diags := WarnsAndErrsToProto(
|
||||
[]string{
|
||||
"warning 1",
|
||||
"warning 2",
|
||||
},
|
||||
[]error{
|
||||
errors.New("error 1"),
|
||||
errors.New("error 2"),
|
||||
},
|
||||
)
|
||||
|
||||
expected := []*proto.Diagnostic{
|
||||
{
|
||||
Severity: proto.Diagnostic_WARNING,
|
||||
Summary: "warning 1",
|
||||
},
|
||||
{
|
||||
Severity: proto.Diagnostic_WARNING,
|
||||
Summary: "warning 2",
|
||||
},
|
||||
{
|
||||
Severity: proto.Diagnostic_ERROR,
|
||||
Summary: "error 1",
|
||||
},
|
||||
{
|
||||
Severity: proto.Diagnostic_ERROR,
|
||||
Summary: "error 2",
|
||||
},
|
||||
}
|
||||
|
||||
if !cmp.Equal(expected, diags, ignoreUnexported) {
|
||||
t.Fatal(cmp.Diff(expected, diags, ignoreUnexported))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagnostics(t *testing.T) {
|
||||
type diagFlat struct {
|
||||
Severity tfdiags.Severity
|
||||
Attr []interface{}
|
||||
Summary string
|
||||
Detail string
|
||||
}
|
||||
|
||||
tests := map[string]struct {
|
||||
Cons func([]*proto.Diagnostic) []*proto.Diagnostic
|
||||
Want []diagFlat
|
||||
}{
|
||||
"nil": {
|
||||
func(diags []*proto.Diagnostic) []*proto.Diagnostic {
|
||||
return diags
|
||||
},
|
||||
nil,
|
||||
},
|
||||
"error": {
|
||||
func(diags []*proto.Diagnostic) []*proto.Diagnostic {
|
||||
return append(diags, &proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_ERROR,
|
||||
Summary: "simple error",
|
||||
})
|
||||
},
|
||||
[]diagFlat{
|
||||
{
|
||||
Severity: tfdiags.Error,
|
||||
Summary: "simple error",
|
||||
},
|
||||
},
|
||||
},
|
||||
"detailed error": {
|
||||
func(diags []*proto.Diagnostic) []*proto.Diagnostic {
|
||||
return append(diags, &proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_ERROR,
|
||||
Summary: "simple error",
|
||||
Detail: "detailed error",
|
||||
})
|
||||
},
|
||||
[]diagFlat{
|
||||
{
|
||||
Severity: tfdiags.Error,
|
||||
Summary: "simple error",
|
||||
Detail: "detailed error",
|
||||
},
|
||||
},
|
||||
},
|
||||
"warning": {
|
||||
func(diags []*proto.Diagnostic) []*proto.Diagnostic {
|
||||
return append(diags, &proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_WARNING,
|
||||
Summary: "simple warning",
|
||||
})
|
||||
},
|
||||
[]diagFlat{
|
||||
{
|
||||
Severity: tfdiags.Warning,
|
||||
Summary: "simple warning",
|
||||
},
|
||||
},
|
||||
},
|
||||
"detailed warning": {
|
||||
func(diags []*proto.Diagnostic) []*proto.Diagnostic {
|
||||
return append(diags, &proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_WARNING,
|
||||
Summary: "simple warning",
|
||||
Detail: "detailed warning",
|
||||
})
|
||||
},
|
||||
[]diagFlat{
|
||||
{
|
||||
Severity: tfdiags.Warning,
|
||||
Summary: "simple warning",
|
||||
Detail: "detailed warning",
|
||||
},
|
||||
},
|
||||
},
|
||||
"multi error": {
|
||||
func(diags []*proto.Diagnostic) []*proto.Diagnostic {
|
||||
diags = append(diags, &proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_ERROR,
|
||||
Summary: "first error",
|
||||
}, &proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_ERROR,
|
||||
Summary: "second error",
|
||||
})
|
||||
return diags
|
||||
},
|
||||
[]diagFlat{
|
||||
{
|
||||
Severity: tfdiags.Error,
|
||||
Summary: "first error",
|
||||
},
|
||||
{
|
||||
Severity: tfdiags.Error,
|
||||
Summary: "second error",
|
||||
},
|
||||
},
|
||||
},
|
||||
"warning and error": {
|
||||
func(diags []*proto.Diagnostic) []*proto.Diagnostic {
|
||||
diags = append(diags, &proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_WARNING,
|
||||
Summary: "warning",
|
||||
}, &proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_ERROR,
|
||||
Summary: "error",
|
||||
})
|
||||
return diags
|
||||
},
|
||||
[]diagFlat{
|
||||
{
|
||||
Severity: tfdiags.Warning,
|
||||
Summary: "warning",
|
||||
},
|
||||
{
|
||||
Severity: tfdiags.Error,
|
||||
Summary: "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
"attr error": {
|
||||
func(diags []*proto.Diagnostic) []*proto.Diagnostic {
|
||||
diags = append(diags, &proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_ERROR,
|
||||
Summary: "error",
|
||||
Detail: "error detail",
|
||||
Attribute: &proto.AttributePath{
|
||||
Steps: []*proto.AttributePath_Step{
|
||||
{
|
||||
Selector: &proto.AttributePath_Step_AttributeName{
|
||||
AttributeName: "attribute_name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return diags
|
||||
},
|
||||
[]diagFlat{
|
||||
{
|
||||
Severity: tfdiags.Error,
|
||||
Summary: "error",
|
||||
Detail: "error detail",
|
||||
Attr: []interface{}{"attribute_name"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"multi attr": {
|
||||
func(diags []*proto.Diagnostic) []*proto.Diagnostic {
|
||||
diags = append(diags,
|
||||
&proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_ERROR,
|
||||
Summary: "error 1",
|
||||
Detail: "error 1 detail",
|
||||
Attribute: &proto.AttributePath{
|
||||
Steps: []*proto.AttributePath_Step{
|
||||
{
|
||||
Selector: &proto.AttributePath_Step_AttributeName{
|
||||
AttributeName: "attr",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_ERROR,
|
||||
Summary: "error 2",
|
||||
Detail: "error 2 detail",
|
||||
Attribute: &proto.AttributePath{
|
||||
Steps: []*proto.AttributePath_Step{
|
||||
{
|
||||
Selector: &proto.AttributePath_Step_AttributeName{
|
||||
AttributeName: "attr",
|
||||
},
|
||||
},
|
||||
{
|
||||
Selector: &proto.AttributePath_Step_AttributeName{
|
||||
AttributeName: "sub",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_WARNING,
|
||||
Summary: "warning",
|
||||
Detail: "warning detail",
|
||||
Attribute: &proto.AttributePath{
|
||||
Steps: []*proto.AttributePath_Step{
|
||||
{
|
||||
Selector: &proto.AttributePath_Step_AttributeName{
|
||||
AttributeName: "attr",
|
||||
},
|
||||
},
|
||||
{
|
||||
Selector: &proto.AttributePath_Step_ElementKeyInt{
|
||||
ElementKeyInt: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Selector: &proto.AttributePath_Step_AttributeName{
|
||||
AttributeName: "sub",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&proto.Diagnostic{
|
||||
Severity: proto.Diagnostic_ERROR,
|
||||
Summary: "error 3",
|
||||
Detail: "error 3 detail",
|
||||
Attribute: &proto.AttributePath{
|
||||
Steps: []*proto.AttributePath_Step{
|
||||
{
|
||||
Selector: &proto.AttributePath_Step_AttributeName{
|
||||
AttributeName: "attr",
|
||||
},
|
||||
},
|
||||
{
|
||||
Selector: &proto.AttributePath_Step_ElementKeyString{
|
||||
ElementKeyString: "idx",
|
||||
},
|
||||
},
|
||||
{
|
||||
Selector: &proto.AttributePath_Step_AttributeName{
|
||||
AttributeName: "sub",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return diags
|
||||
},
|
||||
[]diagFlat{
|
||||
{
|
||||
Severity: tfdiags.Error,
|
||||
Summary: "error 1",
|
||||
Detail: "error 1 detail",
|
||||
Attr: []interface{}{"attr"},
|
||||
},
|
||||
{
|
||||
Severity: tfdiags.Error,
|
||||
Summary: "error 2",
|
||||
Detail: "error 2 detail",
|
||||
Attr: []interface{}{"attr", "sub"},
|
||||
},
|
||||
{
|
||||
Severity: tfdiags.Warning,
|
||||
Summary: "warning",
|
||||
Detail: "warning detail",
|
||||
Attr: []interface{}{"attr", 1, "sub"},
|
||||
},
|
||||
{
|
||||
Severity: tfdiags.Error,
|
||||
Summary: "error 3",
|
||||
Detail: "error 3 detail",
|
||||
Attr: []interface{}{"attr", "idx", "sub"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
flattenTFDiags := func(ds tfdiags.Diagnostics) []diagFlat {
|
||||
var flat []diagFlat
|
||||
for _, item := range ds {
|
||||
desc := item.Description()
|
||||
|
||||
var attr []interface{}
|
||||
|
||||
for _, a := range tfdiags.GetAttribute(item) {
|
||||
switch step := a.(type) {
|
||||
case cty.GetAttrStep:
|
||||
attr = append(attr, step.Name)
|
||||
case cty.IndexStep:
|
||||
switch step.Key.Type() {
|
||||
case cty.Number:
|
||||
i, _ := step.Key.AsBigFloat().Int64()
|
||||
attr = append(attr, int(i))
|
||||
case cty.String:
|
||||
attr = append(attr, step.Key.AsString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flat = append(flat, diagFlat{
|
||||
Severity: item.Severity(),
|
||||
Attr: attr,
|
||||
Summary: desc.Summary,
|
||||
Detail: desc.Detail,
|
||||
})
|
||||
}
|
||||
return flat
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// we take the
|
||||
tfDiags := ProtoToDiagnostics(tc.Cons(nil))
|
||||
|
||||
flat := flattenTFDiags(tfDiags)
|
||||
|
||||
if !cmp.Equal(flat, tc.Want, typeComparer, valueComparer, equateEmpty) {
|
||||
t.Fatal(cmp.Diff(flat, tc.Want, typeComparer, valueComparer, equateEmpty))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,301 @@
|
|||
package convert
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
proto "github.com/hashicorp/terraform/internal/tfplugin6"
|
||||
"github.com/hashicorp/terraform/providers"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// ConfigSchemaToProto takes a *configschema.Block and converts it to a
|
||||
// proto.Schema_Block for a grpc response.
|
||||
func ConfigSchemaToProto(b *configschema.Block) *proto.Schema_Block {
|
||||
block := &proto.Schema_Block{
|
||||
Description: b.Description,
|
||||
DescriptionKind: protoStringKind(b.DescriptionKind),
|
||||
Deprecated: b.Deprecated,
|
||||
}
|
||||
|
||||
for _, name := range sortedKeys(b.Attributes) {
|
||||
a := b.Attributes[name]
|
||||
|
||||
attr := &proto.Schema_Attribute{
|
||||
Name: name,
|
||||
Description: a.Description,
|
||||
DescriptionKind: protoStringKind(a.DescriptionKind),
|
||||
Optional: a.Optional,
|
||||
Computed: a.Computed,
|
||||
Required: a.Required,
|
||||
Sensitive: a.Sensitive,
|
||||
Deprecated: a.Deprecated,
|
||||
}
|
||||
|
||||
if a.Type != cty.NilType {
|
||||
ty, err := json.Marshal(a.Type)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
attr.Type = ty
|
||||
}
|
||||
|
||||
if a.NestedType != nil {
|
||||
attr.NestedType = configschemaObjectToProto(a.NestedType)
|
||||
}
|
||||
|
||||
block.Attributes = append(block.Attributes, attr)
|
||||
}
|
||||
|
||||
for _, name := range sortedKeys(b.BlockTypes) {
|
||||
b := b.BlockTypes[name]
|
||||
block.BlockTypes = append(block.BlockTypes, protoSchemaNestedBlock(name, b))
|
||||
}
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
func protoStringKind(k configschema.StringKind) proto.StringKind {
|
||||
switch k {
|
||||
default:
|
||||
return proto.StringKind_PLAIN
|
||||
case configschema.StringMarkdown:
|
||||
return proto.StringKind_MARKDOWN
|
||||
}
|
||||
}
|
||||
|
||||
func protoSchemaNestedBlock(name string, b *configschema.NestedBlock) *proto.Schema_NestedBlock {
|
||||
var nesting proto.Schema_NestedBlock_NestingMode
|
||||
switch b.Nesting {
|
||||
case configschema.NestingSingle:
|
||||
nesting = proto.Schema_NestedBlock_SINGLE
|
||||
case configschema.NestingGroup:
|
||||
nesting = proto.Schema_NestedBlock_GROUP
|
||||
case configschema.NestingList:
|
||||
nesting = proto.Schema_NestedBlock_LIST
|
||||
case configschema.NestingSet:
|
||||
nesting = proto.Schema_NestedBlock_SET
|
||||
case configschema.NestingMap:
|
||||
nesting = proto.Schema_NestedBlock_MAP
|
||||
default:
|
||||
nesting = proto.Schema_NestedBlock_INVALID
|
||||
}
|
||||
return &proto.Schema_NestedBlock{
|
||||
TypeName: name,
|
||||
Block: ConfigSchemaToProto(&b.Block),
|
||||
Nesting: nesting,
|
||||
MinItems: int64(b.MinItems),
|
||||
MaxItems: int64(b.MaxItems),
|
||||
}
|
||||
}
|
||||
|
||||
// ProtoToProviderSchema takes a proto.Schema and converts it to a providers.Schema.
|
||||
func ProtoToProviderSchema(s *proto.Schema) providers.Schema {
|
||||
return providers.Schema{
|
||||
Version: s.Version,
|
||||
Block: ProtoToConfigSchema(s.Block),
|
||||
}
|
||||
}
|
||||
|
||||
// ProtoToConfigSchema takes the GetSchcema_Block from a grpc response and converts it
|
||||
// to a terraform *configschema.Block.
|
||||
func ProtoToConfigSchema(b *proto.Schema_Block) *configschema.Block {
|
||||
block := &configschema.Block{
|
||||
Attributes: make(map[string]*configschema.Attribute),
|
||||
BlockTypes: make(map[string]*configschema.NestedBlock),
|
||||
|
||||
Description: b.Description,
|
||||
DescriptionKind: schemaStringKind(b.DescriptionKind),
|
||||
Deprecated: b.Deprecated,
|
||||
}
|
||||
|
||||
for _, a := range b.Attributes {
|
||||
attr := &configschema.Attribute{
|
||||
Description: a.Description,
|
||||
DescriptionKind: schemaStringKind(a.DescriptionKind),
|
||||
Required: a.Required,
|
||||
Optional: a.Optional,
|
||||
Computed: a.Computed,
|
||||
Sensitive: a.Sensitive,
|
||||
Deprecated: a.Deprecated,
|
||||
}
|
||||
|
||||
if a.Type != nil {
|
||||
if err := json.Unmarshal(a.Type, &attr.Type); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if a.NestedType != nil {
|
||||
attr.NestedType = protoObjectToConfigSchema(a.NestedType)
|
||||
}
|
||||
|
||||
block.Attributes[a.Name] = attr
|
||||
}
|
||||
|
||||
for _, b := range b.BlockTypes {
|
||||
block.BlockTypes[b.TypeName] = schemaNestedBlock(b)
|
||||
}
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
func schemaStringKind(k proto.StringKind) configschema.StringKind {
|
||||
switch k {
|
||||
default:
|
||||
return configschema.StringPlain
|
||||
case proto.StringKind_MARKDOWN:
|
||||
return configschema.StringMarkdown
|
||||
}
|
||||
}
|
||||
|
||||
func schemaNestedBlock(b *proto.Schema_NestedBlock) *configschema.NestedBlock {
|
||||
var nesting configschema.NestingMode
|
||||
switch b.Nesting {
|
||||
case proto.Schema_NestedBlock_SINGLE:
|
||||
nesting = configschema.NestingSingle
|
||||
case proto.Schema_NestedBlock_GROUP:
|
||||
nesting = configschema.NestingGroup
|
||||
case proto.Schema_NestedBlock_LIST:
|
||||
nesting = configschema.NestingList
|
||||
case proto.Schema_NestedBlock_MAP:
|
||||
nesting = configschema.NestingMap
|
||||
case proto.Schema_NestedBlock_SET:
|
||||
nesting = configschema.NestingSet
|
||||
default:
|
||||
// In all other cases we'll leave it as the zero value (invalid) and
|
||||
// let the caller validate it and deal with this.
|
||||
}
|
||||
|
||||
nb := &configschema.NestedBlock{
|
||||
Nesting: nesting,
|
||||
MinItems: int(b.MinItems),
|
||||
MaxItems: int(b.MaxItems),
|
||||
}
|
||||
|
||||
nested := ProtoToConfigSchema(b.Block)
|
||||
nb.Block = *nested
|
||||
return nb
|
||||
}
|
||||
|
||||
func protoObjectToConfigSchema(b *proto.Schema_Object) *configschema.Object {
|
||||
var nesting configschema.NestingMode
|
||||
switch b.Nesting {
|
||||
case proto.Schema_Object_SINGLE:
|
||||
nesting = configschema.NestingSingle
|
||||
case proto.Schema_Object_LIST:
|
||||
nesting = configschema.NestingList
|
||||
case proto.Schema_Object_MAP:
|
||||
nesting = configschema.NestingMap
|
||||
case proto.Schema_Object_SET:
|
||||
nesting = configschema.NestingSet
|
||||
default:
|
||||
// In all other cases we'll leave it as the zero value (invalid) and
|
||||
// let the caller validate it and deal with this.
|
||||
}
|
||||
|
||||
object := &configschema.Object{
|
||||
Attributes: make(map[string]*configschema.Attribute),
|
||||
Nesting: nesting,
|
||||
MinItems: int(b.MinItems),
|
||||
MaxItems: int(b.MaxItems),
|
||||
}
|
||||
|
||||
for _, a := range b.Attributes {
|
||||
attr := &configschema.Attribute{
|
||||
Description: a.Description,
|
||||
DescriptionKind: schemaStringKind(a.DescriptionKind),
|
||||
Required: a.Required,
|
||||
Optional: a.Optional,
|
||||
Computed: a.Computed,
|
||||
Sensitive: a.Sensitive,
|
||||
Deprecated: a.Deprecated,
|
||||
}
|
||||
|
||||
if a.Type != nil {
|
||||
if err := json.Unmarshal(a.Type, &attr.Type); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if a.NestedType != nil {
|
||||
attr.NestedType = protoObjectToConfigSchema(a.NestedType)
|
||||
}
|
||||
|
||||
object.Attributes[a.Name] = attr
|
||||
}
|
||||
|
||||
return object
|
||||
}
|
||||
|
||||
// sortedKeys returns the lexically sorted keys from the given map. This is
|
||||
// used to make schema conversions are deterministic. This panics if map keys
|
||||
// are not a string.
|
||||
func sortedKeys(m interface{}) []string {
|
||||
v := reflect.ValueOf(m)
|
||||
keys := make([]string, v.Len())
|
||||
|
||||
mapKeys := v.MapKeys()
|
||||
for i, k := range mapKeys {
|
||||
keys[i] = k.Interface().(string)
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func configschemaObjectToProto(b *configschema.Object) *proto.Schema_Object {
|
||||
var nesting proto.Schema_Object_NestingMode
|
||||
switch b.Nesting {
|
||||
case configschema.NestingSingle:
|
||||
nesting = proto.Schema_Object_SINGLE
|
||||
case configschema.NestingList:
|
||||
nesting = proto.Schema_Object_LIST
|
||||
case configschema.NestingSet:
|
||||
nesting = proto.Schema_Object_SET
|
||||
case configschema.NestingMap:
|
||||
nesting = proto.Schema_Object_MAP
|
||||
default:
|
||||
nesting = proto.Schema_Object_INVALID
|
||||
}
|
||||
|
||||
attributes := make([]*proto.Schema_Attribute, len(b.Attributes))
|
||||
|
||||
for _, name := range sortedKeys(b.Attributes) {
|
||||
a := b.Attributes[name]
|
||||
|
||||
attr := &proto.Schema_Attribute{
|
||||
Name: name,
|
||||
Description: a.Description,
|
||||
DescriptionKind: protoStringKind(a.DescriptionKind),
|
||||
Optional: a.Optional,
|
||||
Computed: a.Computed,
|
||||
Required: a.Required,
|
||||
Sensitive: a.Sensitive,
|
||||
Deprecated: a.Deprecated,
|
||||
}
|
||||
|
||||
if a.Type != cty.NilType {
|
||||
ty, err := json.Marshal(a.Type)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
attr.Type = ty
|
||||
}
|
||||
|
||||
if a.NestedType != nil {
|
||||
attr.NestedType = configschemaObjectToProto(a.NestedType)
|
||||
}
|
||||
|
||||
attributes = append(attributes, attr)
|
||||
}
|
||||
|
||||
return &proto.Schema_Object{
|
||||
Attributes: attributes,
|
||||
Nesting: nesting,
|
||||
MinItems: int64(b.MinItems),
|
||||
MaxItems: int64(b.MaxItems),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,568 @@
|
|||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
proto "github.com/hashicorp/terraform/internal/tfplugin6"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
var (
|
||||
equateEmpty = cmpopts.EquateEmpty()
|
||||
typeComparer = cmp.Comparer(cty.Type.Equals)
|
||||
valueComparer = cmp.Comparer(cty.Value.RawEquals)
|
||||
)
|
||||
|
||||
// Test that we can convert configschema to protobuf types and back again.
|
||||
func TestConvertSchemaBlocks(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
Block *proto.Schema_Block
|
||||
Want *configschema.Block
|
||||
}{
|
||||
"attributes": {
|
||||
&proto.Schema_Block{
|
||||
Attributes: []*proto.Schema_Attribute{
|
||||
{
|
||||
Name: "computed",
|
||||
Type: []byte(`["list","bool"]`),
|
||||
Computed: true,
|
||||
},
|
||||
{
|
||||
Name: "optional",
|
||||
Type: []byte(`"string"`),
|
||||
Optional: true,
|
||||
},
|
||||
{
|
||||
Name: "optional_computed",
|
||||
Type: []byte(`["map","bool"]`),
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
{
|
||||
Name: "required",
|
||||
Type: []byte(`"number"`),
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "nested_type",
|
||||
NestedType: &proto.Schema_Object{
|
||||
Nesting: proto.Schema_Object_SINGLE,
|
||||
Attributes: []*proto.Schema_Attribute{
|
||||
{
|
||||
Name: "computed",
|
||||
Type: []byte(`["list","bool"]`),
|
||||
Computed: true,
|
||||
},
|
||||
{
|
||||
Name: "optional",
|
||||
Type: []byte(`"string"`),
|
||||
Optional: true,
|
||||
},
|
||||
{
|
||||
Name: "optional_computed",
|
||||
Type: []byte(`["map","bool"]`),
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
{
|
||||
Name: "required",
|
||||
Type: []byte(`"number"`),
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "deeply_nested_type",
|
||||
NestedType: &proto.Schema_Object{
|
||||
Nesting: proto.Schema_Object_SINGLE,
|
||||
Attributes: []*proto.Schema_Attribute{
|
||||
{
|
||||
Name: "first_level",
|
||||
NestedType: &proto.Schema_Object{
|
||||
Nesting: proto.Schema_Object_SINGLE,
|
||||
Attributes: []*proto.Schema_Attribute{
|
||||
{
|
||||
Name: "computed",
|
||||
Type: []byte(`["list","bool"]`),
|
||||
Computed: true,
|
||||
},
|
||||
{
|
||||
Name: "optional",
|
||||
Type: []byte(`"string"`),
|
||||
Optional: true,
|
||||
},
|
||||
{
|
||||
Name: "optional_computed",
|
||||
Type: []byte(`["map","bool"]`),
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
{
|
||||
Name: "required",
|
||||
Type: []byte(`"number"`),
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "nested_list",
|
||||
NestedType: &proto.Schema_Object{
|
||||
Nesting: proto.Schema_Object_LIST,
|
||||
Attributes: []*proto.Schema_Attribute{
|
||||
{
|
||||
Name: "required",
|
||||
Type: []byte(`"string"`),
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
MinItems: 3,
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "nested_set",
|
||||
NestedType: &proto.Schema_Object{
|
||||
Nesting: proto.Schema_Object_SET,
|
||||
Attributes: []*proto.Schema_Attribute{
|
||||
{
|
||||
Name: "required",
|
||||
Type: []byte(`"string"`),
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "nested_map",
|
||||
NestedType: &proto.Schema_Object{
|
||||
Nesting: proto.Schema_Object_MAP,
|
||||
Attributes: []*proto.Schema_Attribute{
|
||||
{
|
||||
Name: "required",
|
||||
Type: []byte(`"string"`),
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"computed": {
|
||||
Type: cty.List(cty.Bool),
|
||||
Computed: true,
|
||||
},
|
||||
"optional": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"optional_computed": {
|
||||
Type: cty.Map(cty.Bool),
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"required": {
|
||||
Type: cty.Number,
|
||||
Required: true,
|
||||
},
|
||||
"nested_type": {
|
||||
NestedType: &configschema.Object{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"computed": {
|
||||
Type: cty.List(cty.Bool),
|
||||
Computed: true,
|
||||
},
|
||||
"optional": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"optional_computed": {
|
||||
Type: cty.Map(cty.Bool),
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"required": {
|
||||
Type: cty.Number,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingSingle,
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
"deeply_nested_type": {
|
||||
NestedType: &configschema.Object{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"first_level": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"computed": {
|
||||
Type: cty.List(cty.Bool),
|
||||
Computed: true,
|
||||
},
|
||||
"optional": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"optional_computed": {
|
||||
Type: cty.Map(cty.Bool),
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"required": {
|
||||
Type: cty.Number,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingSingle,
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
"nested_list": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"required": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
MinItems: 3,
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
"nested_map": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingMap,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"required": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
"nested_set": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingSet,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"required": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"blocks": {
|
||||
&proto.Schema_Block{
|
||||
BlockTypes: []*proto.Schema_NestedBlock{
|
||||
{
|
||||
TypeName: "list",
|
||||
Nesting: proto.Schema_NestedBlock_LIST,
|
||||
Block: &proto.Schema_Block{},
|
||||
},
|
||||
{
|
||||
TypeName: "map",
|
||||
Nesting: proto.Schema_NestedBlock_MAP,
|
||||
Block: &proto.Schema_Block{},
|
||||
},
|
||||
{
|
||||
TypeName: "set",
|
||||
Nesting: proto.Schema_NestedBlock_SET,
|
||||
Block: &proto.Schema_Block{},
|
||||
},
|
||||
{
|
||||
TypeName: "single",
|
||||
Nesting: proto.Schema_NestedBlock_SINGLE,
|
||||
Block: &proto.Schema_Block{
|
||||
Attributes: []*proto.Schema_Attribute{
|
||||
{
|
||||
Name: "foo",
|
||||
Type: []byte(`"dynamic"`),
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"list": &configschema.NestedBlock{
|
||||
Nesting: configschema.NestingList,
|
||||
},
|
||||
"map": &configschema.NestedBlock{
|
||||
Nesting: configschema.NestingMap,
|
||||
},
|
||||
"set": &configschema.NestedBlock{
|
||||
Nesting: configschema.NestingSet,
|
||||
},
|
||||
"single": &configschema.NestedBlock{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"foo": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"deep block nesting": {
|
||||
&proto.Schema_Block{
|
||||
BlockTypes: []*proto.Schema_NestedBlock{
|
||||
{
|
||||
TypeName: "single",
|
||||
Nesting: proto.Schema_NestedBlock_SINGLE,
|
||||
Block: &proto.Schema_Block{
|
||||
BlockTypes: []*proto.Schema_NestedBlock{
|
||||
{
|
||||
TypeName: "list",
|
||||
Nesting: proto.Schema_NestedBlock_LIST,
|
||||
Block: &proto.Schema_Block{
|
||||
BlockTypes: []*proto.Schema_NestedBlock{
|
||||
{
|
||||
TypeName: "set",
|
||||
Nesting: proto.Schema_NestedBlock_SET,
|
||||
Block: &proto.Schema_Block{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"single": &configschema.NestedBlock{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Block: configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"list": &configschema.NestedBlock{
|
||||
Nesting: configschema.NestingList,
|
||||
Block: configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"set": &configschema.NestedBlock{
|
||||
Nesting: configschema.NestingSet,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
converted := ProtoToConfigSchema(tc.Block)
|
||||
if !cmp.Equal(converted, tc.Want, typeComparer, valueComparer, equateEmpty) {
|
||||
t.Fatal(cmp.Diff(converted, tc.Want, typeComparer, valueComparer, equateEmpty))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test that we can convert configschema to protobuf types and back again.
|
||||
func TestConvertProtoSchemaBlocks(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
Want *proto.Schema_Block
|
||||
Block *configschema.Block
|
||||
}{
|
||||
"attributes": {
|
||||
&proto.Schema_Block{
|
||||
Attributes: []*proto.Schema_Attribute{
|
||||
{
|
||||
Name: "computed",
|
||||
Type: []byte(`["list","bool"]`),
|
||||
Computed: true,
|
||||
},
|
||||
{
|
||||
Name: "optional",
|
||||
Type: []byte(`"string"`),
|
||||
Optional: true,
|
||||
},
|
||||
{
|
||||
Name: "optional_computed",
|
||||
Type: []byte(`["map","bool"]`),
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
{
|
||||
Name: "required",
|
||||
Type: []byte(`"number"`),
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"computed": {
|
||||
Type: cty.List(cty.Bool),
|
||||
Computed: true,
|
||||
},
|
||||
"optional": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"optional_computed": {
|
||||
Type: cty.Map(cty.Bool),
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"required": {
|
||||
Type: cty.Number,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"blocks": {
|
||||
&proto.Schema_Block{
|
||||
BlockTypes: []*proto.Schema_NestedBlock{
|
||||
{
|
||||
TypeName: "list",
|
||||
Nesting: proto.Schema_NestedBlock_LIST,
|
||||
Block: &proto.Schema_Block{},
|
||||
},
|
||||
{
|
||||
TypeName: "map",
|
||||
Nesting: proto.Schema_NestedBlock_MAP,
|
||||
Block: &proto.Schema_Block{},
|
||||
},
|
||||
{
|
||||
TypeName: "set",
|
||||
Nesting: proto.Schema_NestedBlock_SET,
|
||||
Block: &proto.Schema_Block{},
|
||||
},
|
||||
{
|
||||
TypeName: "single",
|
||||
Nesting: proto.Schema_NestedBlock_SINGLE,
|
||||
Block: &proto.Schema_Block{
|
||||
Attributes: []*proto.Schema_Attribute{
|
||||
{
|
||||
Name: "foo",
|
||||
Type: []byte(`"dynamic"`),
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"list": &configschema.NestedBlock{
|
||||
Nesting: configschema.NestingList,
|
||||
},
|
||||
"map": &configschema.NestedBlock{
|
||||
Nesting: configschema.NestingMap,
|
||||
},
|
||||
"set": &configschema.NestedBlock{
|
||||
Nesting: configschema.NestingSet,
|
||||
},
|
||||
"single": &configschema.NestedBlock{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"foo": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"deep block nesting": {
|
||||
&proto.Schema_Block{
|
||||
BlockTypes: []*proto.Schema_NestedBlock{
|
||||
{
|
||||
TypeName: "single",
|
||||
Nesting: proto.Schema_NestedBlock_SINGLE,
|
||||
Block: &proto.Schema_Block{
|
||||
BlockTypes: []*proto.Schema_NestedBlock{
|
||||
{
|
||||
TypeName: "list",
|
||||
Nesting: proto.Schema_NestedBlock_LIST,
|
||||
Block: &proto.Schema_Block{
|
||||
BlockTypes: []*proto.Schema_NestedBlock{
|
||||
{
|
||||
TypeName: "set",
|
||||
Nesting: proto.Schema_NestedBlock_SET,
|
||||
Block: &proto.Schema_Block{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"single": &configschema.NestedBlock{
|
||||
Nesting: configschema.NestingSingle,
|
||||
Block: configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"list": &configschema.NestedBlock{
|
||||
Nesting: configschema.NestingList,
|
||||
Block: configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"set": &configschema.NestedBlock{
|
||||
Nesting: configschema.NestingSet,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
converted := ConfigSchemaToProto(tc.Block)
|
||||
if !cmp.Equal(converted, tc.Want, typeComparer, equateEmpty, ignoreUnexported) {
|
||||
t.Fatal(cmp.Diff(converted, tc.Want, typeComparer, equateEmpty, ignoreUnexported))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package plugin6
|
||||
|
||||
// plugin6 builds on types in package plugin to include support for plugin
|
||||
// protocol v6. The main gRPC functions use by terraform (and initialized in
|
||||
// init.go), such as Serve, are in the plugin package. The version of those
|
||||
// functions in this package are used by various mocks and in tests.
|
||||
|
||||
// When provider protocol v5 is deprecated, some functions may need to be moved
|
||||
// here, or the existing functions updated, before removing the plugin pacakge.
|
|
@ -0,0 +1,74 @@
|
|||
package plugin6
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"runtime"
|
||||
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// grpcErr extracts some known error types and formats them into better
|
||||
// representations for core. This must only be called from plugin methods.
|
||||
// Since we don't use RPC status errors for the plugin protocol, these do not
|
||||
// contain any useful details, and we can return some text that at least
|
||||
// indicates the plugin call and possible error condition.
|
||||
func grpcErr(err error) (diags tfdiags.Diagnostics) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// extract the method name from the caller.
|
||||
pc, _, _, ok := runtime.Caller(1)
|
||||
if !ok {
|
||||
logger.Error("unknown grpc call", "error", err)
|
||||
return diags.Append(err)
|
||||
}
|
||||
|
||||
f := runtime.FuncForPC(pc)
|
||||
|
||||
// Function names will contain the full import path. Take the last
|
||||
// segment, which will let users know which method was being called.
|
||||
_, requestName := path.Split(f.Name())
|
||||
|
||||
// Here we can at least correlate the error in the logs to a particular binary.
|
||||
logger.Error(requestName, "error", err)
|
||||
|
||||
// TODO: while this expands the error codes into somewhat better messages,
|
||||
// this still does not easily link the error to an actual user-recognizable
|
||||
// plugin. The grpc plugin does not know its configured name, and the
|
||||
// errors are in a list of diagnostics, making it hard for the caller to
|
||||
// annotate the returned errors.
|
||||
switch status.Code(err) {
|
||||
case codes.Unavailable:
|
||||
// This case is when the plugin has stopped running for some reason,
|
||||
// and is usually the result of a crash.
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Plugin did not respond",
|
||||
fmt.Sprintf("The plugin encountered an error, and failed to respond to the %s call. "+
|
||||
"The plugin logs may contain more details.", requestName),
|
||||
))
|
||||
case codes.Canceled:
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Request cancelled",
|
||||
fmt.Sprintf("The %s request was cancelled.", requestName),
|
||||
))
|
||||
case codes.Unimplemented:
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Unsupported plugin method",
|
||||
fmt.Sprintf("The %s method is not supported by this plugin.", requestName),
|
||||
))
|
||||
default:
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Plugin error",
|
||||
fmt.Sprintf("The plugin returned an unexpected error from %s: %v", requestName, err),
|
||||
))
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,617 @@
|
|||
package plugin6
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
plugin "github.com/hashicorp/go-plugin"
|
||||
"github.com/hashicorp/terraform/internal/logging"
|
||||
proto6 "github.com/hashicorp/terraform/internal/tfplugin6"
|
||||
"github.com/hashicorp/terraform/plugin6/convert"
|
||||
"github.com/hashicorp/terraform/providers"
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
"github.com/zclconf/go-cty/cty/msgpack"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var logger = logging.HCLogger()
|
||||
|
||||
// GRPCProviderPlugin implements plugin.GRPCPlugin for the go-plugin package.
|
||||
type GRPCProviderPlugin struct {
|
||||
plugin.Plugin
|
||||
GRPCProvider func() proto6.ProviderServer
|
||||
}
|
||||
|
||||
func (p *GRPCProviderPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
|
||||
return &GRPCProvider{
|
||||
client: proto6.NewProviderClient(c),
|
||||
ctx: ctx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *GRPCProviderPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
|
||||
proto6.RegisterProviderServer(s, p.GRPCProvider())
|
||||
return nil
|
||||
}
|
||||
|
||||
// GRPCProvider handles the client, or core side of the plugin rpc connection.
|
||||
// The GRPCProvider methods are mostly a translation layer between the
|
||||
// terraform provioders types and the grpc proto types, directly converting
|
||||
// between the two.
|
||||
type GRPCProvider struct {
|
||||
// PluginClient provides a reference to the plugin.Client which controls the plugin process.
|
||||
// This allows the GRPCProvider a way to shutdown the plugin process.
|
||||
PluginClient *plugin.Client
|
||||
|
||||
// TestServer contains a grpc.Server to close when the GRPCProvider is being
|
||||
// used in an end to end test of a provider.
|
||||
TestServer *grpc.Server
|
||||
|
||||
// Proto client use to make the grpc service calls.
|
||||
client proto6.ProviderClient
|
||||
|
||||
// this context is created by the plugin package, and is canceled when the
|
||||
// plugin process ends.
|
||||
ctx context.Context
|
||||
|
||||
// schema stores the schema for this provider. This is used to properly
|
||||
// serialize the state for requests.
|
||||
mu sync.Mutex
|
||||
schemas providers.GetSchemaResponse
|
||||
}
|
||||
|
||||
func New(client proto6.ProviderClient, ctx context.Context) GRPCProvider {
|
||||
return GRPCProvider{
|
||||
client: client,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// getSchema is used internally to get the saved provider schema. The schema
|
||||
// should have already been fetched from the provider, but we have to
|
||||
// synchronize access to avoid being called concurrently with GetSchema.
|
||||
func (p *GRPCProvider) getSchema() providers.GetSchemaResponse {
|
||||
p.mu.Lock()
|
||||
// unlock inline in case GetSchema needs to be called
|
||||
if p.schemas.Provider.Block != nil {
|
||||
p.mu.Unlock()
|
||||
return p.schemas
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
// the schema should have been fetched already, but give it another shot
|
||||
// just in case things are being called out of order. This may happen for
|
||||
// tests.
|
||||
schemas := p.GetSchema()
|
||||
if schemas.Diagnostics.HasErrors() {
|
||||
panic(schemas.Diagnostics.Err())
|
||||
}
|
||||
|
||||
return schemas
|
||||
}
|
||||
|
||||
// getResourceSchema is a helper to extract the schema for a resource, and
|
||||
// panics if the schema is not available.
|
||||
func (p *GRPCProvider) getResourceSchema(name string) providers.Schema {
|
||||
schema := p.getSchema()
|
||||
resSchema, ok := schema.ResourceTypes[name]
|
||||
if !ok {
|
||||
panic("unknown resource type " + name)
|
||||
}
|
||||
return resSchema
|
||||
}
|
||||
|
||||
// gettDatasourceSchema is a helper to extract the schema for a datasource, and
|
||||
// panics if that schema is not available.
|
||||
func (p *GRPCProvider) getDatasourceSchema(name string) providers.Schema {
|
||||
schema := p.getSchema()
|
||||
dataSchema, ok := schema.DataSources[name]
|
||||
if !ok {
|
||||
panic("unknown data source " + name)
|
||||
}
|
||||
return dataSchema
|
||||
}
|
||||
|
||||
// getProviderMetaSchema is a helper to extract the schema for the meta info
|
||||
// defined for a provider,
|
||||
func (p *GRPCProvider) getProviderMetaSchema() providers.Schema {
|
||||
schema := p.getSchema()
|
||||
return schema.ProviderMeta
|
||||
}
|
||||
|
||||
func (p *GRPCProvider) GetSchema() (resp providers.GetSchemaResponse) {
|
||||
logger.Trace("GRPCProvider.v6: GetSchema")
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.schemas.Provider.Block != nil {
|
||||
return p.schemas
|
||||
}
|
||||
|
||||
resp.ResourceTypes = make(map[string]providers.Schema)
|
||||
resp.DataSources = make(map[string]providers.Schema)
|
||||
|
||||
// Some providers may generate quite large schemas, and the internal default
|
||||
// grpc response size limit is 4MB. 64MB should cover most any use case, and
|
||||
// if we get providers nearing that we may want to consider a finer-grained
|
||||
// API to fetch individual resource schemas.
|
||||
// Note: this option is marked as EXPERIMENTAL in the grpc API.
|
||||
const maxRecvSize = 64 << 20
|
||||
protoResp, err := p.client.GetProviderSchema(p.ctx, new(proto6.GetProviderSchema_Request), grpc.MaxRecvMsgSizeCallOption{MaxRecvMsgSize: maxRecvSize})
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
|
||||
return resp
|
||||
}
|
||||
|
||||
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
|
||||
|
||||
if protoResp.Provider == nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(errors.New("missing provider schema"))
|
||||
return resp
|
||||
}
|
||||
|
||||
resp.Provider = convert.ProtoToProviderSchema(protoResp.Provider)
|
||||
if protoResp.ProviderMeta == nil {
|
||||
logger.Debug("No provider meta schema returned")
|
||||
} else {
|
||||
resp.ProviderMeta = convert.ProtoToProviderSchema(protoResp.ProviderMeta)
|
||||
}
|
||||
|
||||
for name, res := range protoResp.ResourceSchemas {
|
||||
resp.ResourceTypes[name] = convert.ProtoToProviderSchema(res)
|
||||
}
|
||||
|
||||
for name, data := range protoResp.DataSourceSchemas {
|
||||
resp.DataSources[name] = convert.ProtoToProviderSchema(data)
|
||||
}
|
||||
|
||||
p.schemas = resp
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *GRPCProvider) ValidateProviderConfig(r providers.PrepareProviderConfigRequest) (resp providers.PrepareProviderConfigResponse) {
|
||||
logger.Trace("GRPCProvider.v6: ValidateProviderConfig")
|
||||
|
||||
schema := p.getSchema()
|
||||
ty := schema.Provider.Block.ImpliedType()
|
||||
|
||||
mp, err := msgpack.Marshal(r.Config, ty)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
|
||||
protoReq := &proto6.ValidateProviderConfig_Request{
|
||||
Config: &proto6.DynamicValue{Msgpack: mp},
|
||||
}
|
||||
|
||||
protoResp, err := p.client.ValidateProviderConfig(p.ctx, protoReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
|
||||
return resp
|
||||
}
|
||||
|
||||
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *GRPCProvider) ValidateResourceTypeConfig(r providers.ValidateResourceTypeConfigRequest) (resp providers.ValidateResourceTypeConfigResponse) {
|
||||
logger.Trace("GRPCProvider.v6: ValidateResourceTypeConfig")
|
||||
resourceSchema := p.getResourceSchema(r.TypeName)
|
||||
|
||||
mp, err := msgpack.Marshal(r.Config, resourceSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
|
||||
protoReq := &proto6.ValidateResourceConfig_Request{
|
||||
TypeName: r.TypeName,
|
||||
Config: &proto6.DynamicValue{Msgpack: mp},
|
||||
}
|
||||
|
||||
protoResp, err := p.client.ValidateResourceConfig(p.ctx, protoReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
|
||||
return resp
|
||||
}
|
||||
|
||||
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *GRPCProvider) ValidateDataSourceConfig(r providers.ValidateDataSourceConfigRequest) (resp providers.ValidateDataSourceConfigResponse) {
|
||||
logger.Trace("GRPCProvider.v6: ValidateDataSourceConfig")
|
||||
|
||||
dataSchema := p.getDatasourceSchema(r.TypeName)
|
||||
|
||||
mp, err := msgpack.Marshal(r.Config, dataSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
|
||||
protoReq := &proto6.ValidateDataSourceConfig_Request{
|
||||
TypeName: r.TypeName,
|
||||
Config: &proto6.DynamicValue{Msgpack: mp},
|
||||
}
|
||||
|
||||
protoResp, err := p.client.ValidateDataSourceConfig(p.ctx, protoReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
|
||||
return resp
|
||||
}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *GRPCProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) {
|
||||
logger.Trace("GRPCProvider.v6: UpgradeResourceState")
|
||||
|
||||
resSchema := p.getResourceSchema(r.TypeName)
|
||||
|
||||
protoReq := &proto6.UpgradeResourceState_Request{
|
||||
TypeName: r.TypeName,
|
||||
Version: int64(r.Version),
|
||||
RawState: &proto6.RawState{
|
||||
Json: r.RawStateJSON,
|
||||
Flatmap: r.RawStateFlatmap,
|
||||
},
|
||||
}
|
||||
|
||||
protoResp, err := p.client.UpgradeResourceState(p.ctx, protoReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
|
||||
return resp
|
||||
}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
|
||||
|
||||
ty := resSchema.Block.ImpliedType()
|
||||
resp.UpgradedState = cty.NullVal(ty)
|
||||
if protoResp.UpgradedState == nil {
|
||||
return resp
|
||||
}
|
||||
|
||||
state, err := decodeDynamicValue(protoResp.UpgradedState, ty)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
resp.UpgradedState = state
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *GRPCProvider) Configure(r providers.ConfigureRequest) (resp providers.ConfigureResponse) {
|
||||
logger.Trace("GRPCProvider.v6: Configure")
|
||||
|
||||
schema := p.getSchema()
|
||||
|
||||
var mp []byte
|
||||
|
||||
// we don't have anything to marshal if there's no config
|
||||
mp, err := msgpack.Marshal(r.Config, schema.Provider.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
|
||||
protoReq := &proto6.ConfigureProvider_Request{
|
||||
TerraformVersion: r.TerraformVersion,
|
||||
Config: &proto6.DynamicValue{
|
||||
Msgpack: mp,
|
||||
},
|
||||
}
|
||||
|
||||
protoResp, err := p.client.ConfigureProvider(p.ctx, protoReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
|
||||
return resp
|
||||
}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *GRPCProvider) Stop() error {
|
||||
logger.Trace("GRPCProvider.v6: Stop")
|
||||
|
||||
resp, err := p.client.StopProvider(p.ctx, new(proto6.StopProvider_Request))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.Error != "" {
|
||||
return errors.New(resp.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GRPCProvider) ReadResource(r providers.ReadResourceRequest) (resp providers.ReadResourceResponse) {
|
||||
logger.Trace("GRPCProvider.v6: ReadResource")
|
||||
|
||||
resSchema := p.getResourceSchema(r.TypeName)
|
||||
metaSchema := p.getProviderMetaSchema()
|
||||
|
||||
mp, err := msgpack.Marshal(r.PriorState, resSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
|
||||
protoReq := &proto6.ReadResource_Request{
|
||||
TypeName: r.TypeName,
|
||||
CurrentState: &proto6.DynamicValue{Msgpack: mp},
|
||||
Private: r.Private,
|
||||
}
|
||||
|
||||
if metaSchema.Block != nil {
|
||||
metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
protoReq.ProviderMeta = &proto6.DynamicValue{Msgpack: metaMP}
|
||||
}
|
||||
|
||||
protoResp, err := p.client.ReadResource(p.ctx, protoReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
|
||||
return resp
|
||||
}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
|
||||
|
||||
state, err := decodeDynamicValue(protoResp.NewState, resSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
resp.NewState = state
|
||||
resp.Private = protoResp.Private
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
|
||||
logger.Trace("GRPCProvider.v6: PlanResourceChange")
|
||||
|
||||
resSchema := p.getResourceSchema(r.TypeName)
|
||||
metaSchema := p.getProviderMetaSchema()
|
||||
|
||||
priorMP, err := msgpack.Marshal(r.PriorState, resSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
|
||||
configMP, err := msgpack.Marshal(r.Config, resSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
|
||||
propMP, err := msgpack.Marshal(r.ProposedNewState, resSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
|
||||
protoReq := &proto6.PlanResourceChange_Request{
|
||||
TypeName: r.TypeName,
|
||||
PriorState: &proto6.DynamicValue{Msgpack: priorMP},
|
||||
Config: &proto6.DynamicValue{Msgpack: configMP},
|
||||
ProposedNewState: &proto6.DynamicValue{Msgpack: propMP},
|
||||
PriorPrivate: r.PriorPrivate,
|
||||
}
|
||||
|
||||
if metaSchema.Block != nil {
|
||||
metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
protoReq.ProviderMeta = &proto6.DynamicValue{Msgpack: metaMP}
|
||||
}
|
||||
|
||||
protoResp, err := p.client.PlanResourceChange(p.ctx, protoReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
|
||||
return resp
|
||||
}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
|
||||
|
||||
state, err := decodeDynamicValue(protoResp.PlannedState, resSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
resp.PlannedState = state
|
||||
|
||||
for _, p := range protoResp.RequiresReplace {
|
||||
resp.RequiresReplace = append(resp.RequiresReplace, convert.AttributePathToPath(p))
|
||||
}
|
||||
|
||||
resp.PlannedPrivate = protoResp.PlannedPrivate
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *GRPCProvider) ApplyResourceChange(r providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
|
||||
logger.Trace("GRPCProvider.v6: ApplyResourceChange")
|
||||
|
||||
resSchema := p.getResourceSchema(r.TypeName)
|
||||
metaSchema := p.getProviderMetaSchema()
|
||||
|
||||
priorMP, err := msgpack.Marshal(r.PriorState, resSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
plannedMP, err := msgpack.Marshal(r.PlannedState, resSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
configMP, err := msgpack.Marshal(r.Config, resSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
|
||||
protoReq := &proto6.ApplyResourceChange_Request{
|
||||
TypeName: r.TypeName,
|
||||
PriorState: &proto6.DynamicValue{Msgpack: priorMP},
|
||||
PlannedState: &proto6.DynamicValue{Msgpack: plannedMP},
|
||||
Config: &proto6.DynamicValue{Msgpack: configMP},
|
||||
PlannedPrivate: r.PlannedPrivate,
|
||||
}
|
||||
|
||||
if metaSchema.Block != nil {
|
||||
metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
protoReq.ProviderMeta = &proto6.DynamicValue{Msgpack: metaMP}
|
||||
}
|
||||
|
||||
protoResp, err := p.client.ApplyResourceChange(p.ctx, protoReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
|
||||
return resp
|
||||
}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
|
||||
|
||||
resp.Private = protoResp.Private
|
||||
|
||||
state, err := decodeDynamicValue(protoResp.NewState, resSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
resp.NewState = state
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *GRPCProvider) ImportResourceState(r providers.ImportResourceStateRequest) (resp providers.ImportResourceStateResponse) {
|
||||
logger.Trace("GRPCProvider.v6: ImportResourceState")
|
||||
|
||||
protoReq := &proto6.ImportResourceState_Request{
|
||||
TypeName: r.TypeName,
|
||||
Id: r.ID,
|
||||
}
|
||||
|
||||
protoResp, err := p.client.ImportResourceState(p.ctx, protoReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
|
||||
return resp
|
||||
}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
|
||||
|
||||
for _, imported := range protoResp.ImportedResources {
|
||||
resource := providers.ImportedResource{
|
||||
TypeName: imported.TypeName,
|
||||
Private: imported.Private,
|
||||
}
|
||||
|
||||
resSchema := p.getResourceSchema(resource.TypeName)
|
||||
state, err := decodeDynamicValue(imported.State, resSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
resource.State = state
|
||||
resp.ImportedResources = append(resp.ImportedResources, resource)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *GRPCProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) {
|
||||
logger.Trace("GRPCProvider.v6: ReadDataSource")
|
||||
|
||||
dataSchema := p.getDatasourceSchema(r.TypeName)
|
||||
metaSchema := p.getProviderMetaSchema()
|
||||
|
||||
config, err := msgpack.Marshal(r.Config, dataSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
|
||||
protoReq := &proto6.ReadDataSource_Request{
|
||||
TypeName: r.TypeName,
|
||||
Config: &proto6.DynamicValue{
|
||||
Msgpack: config,
|
||||
},
|
||||
}
|
||||
|
||||
if metaSchema.Block != nil {
|
||||
metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
protoReq.ProviderMeta = &proto6.DynamicValue{Msgpack: metaMP}
|
||||
}
|
||||
|
||||
protoResp, err := p.client.ReadDataSource(p.ctx, protoReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
|
||||
return resp
|
||||
}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
|
||||
|
||||
state, err := decodeDynamicValue(protoResp.State, dataSchema.Block.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
resp.State = state
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// closing the grpc connection is final, and terraform will call it at the end of every phase.
|
||||
func (p *GRPCProvider) Close() error {
|
||||
logger.Trace("GRPCProvider.v6: Close")
|
||||
|
||||
// Make sure to stop the server if we're not running within go-plugin.
|
||||
if p.TestServer != nil {
|
||||
p.TestServer.Stop()
|
||||
}
|
||||
|
||||
// Check this since it's not automatically inserted during plugin creation.
|
||||
// It's currently only inserted by the command package, because that is
|
||||
// where the factory is built and is the only point with access to the
|
||||
// plugin.Client.
|
||||
if p.PluginClient == nil {
|
||||
logger.Debug("provider has no plugin.Client")
|
||||
return nil
|
||||
}
|
||||
|
||||
p.PluginClient.Kill()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decode a DynamicValue from either the JSON or MsgPack encoding.
|
||||
func decodeDynamicValue(v *proto6.DynamicValue, ty cty.Type) (cty.Value, error) {
|
||||
// always return a valid value
|
||||
var err error
|
||||
res := cty.NullVal(ty)
|
||||
if v == nil {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(v.Msgpack) > 0:
|
||||
res, err = msgpack.Unmarshal(v.Msgpack, ty)
|
||||
case len(v.Json) > 0:
|
||||
res, err = ctyjson.Unmarshal(v.Json, ty)
|
||||
}
|
||||
return res, err
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package plugin6
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-plugin"
|
||||
proto "github.com/hashicorp/terraform/internal/tfplugin6"
|
||||
)
|
||||
|
||||
const (
|
||||
// The constants below are the names of the plugins that can be dispensed
|
||||
// from the plugin server.
|
||||
ProviderPluginName = "provider"
|
||||
ProvisionerPluginName = "provisioner"
|
||||
|
||||
// DefaultProtocolVersion is the protocol version assumed for legacy clients that don't specify
|
||||
// a particular version during their handshake. This is the version used when Terraform 0.10
|
||||
// and 0.11 launch plugins that were built with support for both versions 4 and 5, and must
|
||||
// stay unchanged at 4 until we intentionally build plugins that are not compatible with 0.10 and
|
||||
// 0.11.
|
||||
DefaultProtocolVersion = 4
|
||||
)
|
||||
|
||||
// Handshake is the HandshakeConfig used to configure clients and servers.
|
||||
var Handshake = plugin.HandshakeConfig{
|
||||
// The ProtocolVersion is the version that must match between TF core
|
||||
// and TF plugins. This should be bumped whenever a change happens in
|
||||
// one or the other that makes it so that they can't safely communicate.
|
||||
// This could be adding a new interface value, it could be how
|
||||
// helper/schema computes diffs, etc.
|
||||
ProtocolVersion: DefaultProtocolVersion,
|
||||
|
||||
// The magic cookie values should NEVER be changed.
|
||||
MagicCookieKey: "TF_PLUGIN_MAGIC_COOKIE",
|
||||
MagicCookieValue: "d602bf8f470bc67ca7faa0386276bbdd4330efaf76d1a219cb4d6991ca9872b2",
|
||||
}
|
||||
|
||||
type GRPCProviderFunc func() proto.ProviderServer
|
||||
|
||||
// ServeOpts are the configurations to serve a plugin.
|
||||
type ServeOpts struct {
|
||||
// Wrapped versions of the above plugins will automatically shimmed and
|
||||
// added to the GRPC functions when possible.
|
||||
GRPCProviderFunc GRPCProviderFunc
|
||||
}
|
||||
|
||||
// Serve serves a plugin. This function never returns and should be the final
|
||||
// function called in the main function of the plugin.
|
||||
func Serve(opts *ServeOpts) {
|
||||
plugin.Serve(&plugin.ServeConfig{
|
||||
HandshakeConfig: Handshake,
|
||||
VersionedPlugins: pluginSet(opts),
|
||||
GRPCServer: plugin.DefaultGRPCServer,
|
||||
})
|
||||
}
|
||||
|
||||
func pluginSet(opts *ServeOpts) map[int]plugin.PluginSet {
|
||||
plugins := map[int]plugin.PluginSet{}
|
||||
|
||||
// add the new protocol versions if they're configured
|
||||
if opts.GRPCProviderFunc != nil {
|
||||
plugins[5] = plugin.PluginSet{}
|
||||
if opts.GRPCProviderFunc != nil {
|
||||
plugins[6]["provider"] = &GRPCProviderPlugin{
|
||||
GRPCProvider: opts.GRPCProviderFunc,
|
||||
}
|
||||
}
|
||||
}
|
||||
return plugins
|
||||
}
|
|
@ -649,7 +649,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
|||
return plan, state, diags
|
||||
}
|
||||
|
||||
proposedNewVal := objchange.ProposedNewObject(schema, unmarkedPriorVal, configValIgnored)
|
||||
proposedNewVal := objchange.ProposedNew(schema, unmarkedPriorVal, configValIgnored)
|
||||
|
||||
// Call pre-diff hook
|
||||
diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) {
|
||||
|
@ -861,7 +861,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
|||
}
|
||||
|
||||
// create a new proposed value from the null state and the config
|
||||
proposedNewVal = objchange.ProposedNewObject(schema, nullPriorVal, unmarkedConfigVal)
|
||||
proposedNewVal = objchange.ProposedNew(schema, nullPriorVal, unmarkedConfigVal)
|
||||
|
||||
resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{
|
||||
TypeName: n.Addr.Resource.Resource.Type,
|
||||
|
@ -1423,7 +1423,7 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, currentSt
|
|||
// While we don't propose planned changes for data sources, we can
|
||||
// generate a proposed value for comparison to ensure the data source
|
||||
// is returning a result following the rules of the provider contract.
|
||||
proposedVal := objchange.ProposedNewObject(schema, unmarkedPriorVal, unmarkedConfigVal)
|
||||
proposedVal := objchange.ProposedNew(schema, unmarkedPriorVal, unmarkedConfigVal)
|
||||
if errs := objchange.AssertObjectCompatible(schema, proposedVal, newVal); len(errs) > 0 {
|
||||
// Resources have the LegacyTypeSystem field to signal when they are
|
||||
// using an SDK which may not produce precise values. While data
|
||||
|
|
Loading…
Reference in New Issue