mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-25 01:00:16 -05:00
Feature branch: Ephemeral resources (#2852)
Signed-off-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
This commit is contained in:
@@ -82,6 +82,17 @@ func ParseModuleInstanceStr(str string) (ModuleInstance, tfdiags.Diagnostics) {
|
||||
return addr, diags
|
||||
}
|
||||
|
||||
// MustParseModuleInstanceStr is a wrapper around ParseModuleInstanceStr that panics if
|
||||
// it returns an error.
|
||||
// This is mainly meant for being used in unit tests.
|
||||
func MustParseModuleInstanceStr(str string) ModuleInstance {
|
||||
result, diags := ParseModuleInstanceStr(str)
|
||||
if diags.HasErrors() {
|
||||
panic(diags.Err().Error())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// parseModuleInstancePrefix parses a module instance address from the given
|
||||
// traversal, returning the module instance address and the remaining
|
||||
// traversal.
|
||||
|
||||
@@ -299,3 +299,18 @@ func (e *MoveEndpoint) internalAddrType() TargetableAddrType {
|
||||
panic(fmt.Sprintf("unsupported address type %T", addr))
|
||||
}
|
||||
}
|
||||
|
||||
// SubjectAllowed is validating what types of resource can be used with the moved block.
|
||||
// At the time of writing, it was only ensuring that the moved blocks cannot be used against ephemeral resources.
|
||||
// This can later be expanded with more rules
|
||||
func (e *MoveEndpoint) SubjectAllowed() bool {
|
||||
if e == nil {
|
||||
return false
|
||||
}
|
||||
switch addr := e.relSubject.(type) {
|
||||
case AbsMoveableResource:
|
||||
return addr.AffectedAbsResource().Resource.Mode != EphemeralResourceMode
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,3 +635,55 @@ func TestMoveEndpointConfigMoveable(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubjectAllowed(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
target AbsMoveable
|
||||
want bool
|
||||
}{
|
||||
"ephemeral resource": {
|
||||
AbsResource{Resource: Resource{Mode: EphemeralResourceMode}},
|
||||
false,
|
||||
},
|
||||
"managed resource": {
|
||||
AbsResource{Resource: Resource{Mode: ManagedResourceMode}},
|
||||
true,
|
||||
},
|
||||
"data source": {
|
||||
AbsResource{Resource: Resource{Mode: DataResourceMode}},
|
||||
true,
|
||||
},
|
||||
"ephemeral resource instance": {
|
||||
AbsResourceInstance{Resource: ResourceInstance{Resource: Resource{Mode: EphemeralResourceMode}}},
|
||||
false,
|
||||
},
|
||||
"managed resource instance": {
|
||||
AbsResourceInstance{Resource: ResourceInstance{Resource: Resource{Mode: ManagedResourceMode}}},
|
||||
true,
|
||||
},
|
||||
"data source instance": {
|
||||
AbsResourceInstance{Resource: ResourceInstance{Resource: Resource{Mode: DataResourceMode}}},
|
||||
true,
|
||||
},
|
||||
"module instance": {
|
||||
ModuleInstance{},
|
||||
true,
|
||||
},
|
||||
"module": {
|
||||
ModuleInstance{},
|
||||
true,
|
||||
},
|
||||
"module call": {
|
||||
AbsModuleCall{},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
m := &MoveEndpoint{relSubject: tt.target}
|
||||
if got, want := m.SubjectAllowed(), tt.want; got != want {
|
||||
t.Errorf("unexpected allowed resource for a moved block. expected: %t; got: %t", want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,18 @@ func parseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) {
|
||||
}
|
||||
remain := traversal[1:] // trim off "data" so we can use our shared resource reference parser
|
||||
return parseResourceRef(DataResourceMode, rootRange, remain)
|
||||
case "ephemeral":
|
||||
if len(traversal) < 3 {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid reference",
|
||||
Detail: `The "ephemeral" object must be followed by two attribute names: the ephemeral resource type and its name.`,
|
||||
Subject: traversal.SourceRange().Ptr(),
|
||||
})
|
||||
return nil, diags
|
||||
}
|
||||
remain := traversal[1:] // trim off "ephemeral" so we can use our shared resource reference parser
|
||||
return parseResourceRef(EphemeralResourceMode, rootRange, remain)
|
||||
case "resource":
|
||||
// This is an alias for the normal case of just using a managed resource
|
||||
// type as a top-level symbol, which will serve as an escape mechanism
|
||||
@@ -303,14 +315,16 @@ func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Tra
|
||||
var what string
|
||||
switch mode {
|
||||
case DataResourceMode:
|
||||
what = "data source"
|
||||
what = "a data source"
|
||||
case EphemeralResourceMode:
|
||||
what = "an ephemeral resource"
|
||||
default:
|
||||
what = "resource type"
|
||||
what = "a resource type"
|
||||
}
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid reference",
|
||||
Detail: fmt.Sprintf(`A reference to a %s must be followed by at least one attribute access, specifying the resource name.`, what),
|
||||
Detail: fmt.Sprintf(`A reference to %s must be followed by at least one attribute access, specifying the resource name.`, what),
|
||||
Subject: traversal[1].SourceRange().Ptr(),
|
||||
})
|
||||
return nil, diags
|
||||
|
||||
@@ -320,6 +320,104 @@ func TestParseRef(t *testing.T) {
|
||||
`The "data" object must be followed by two attribute names: the data source type and the resource name.`,
|
||||
},
|
||||
|
||||
// ephemeral
|
||||
{
|
||||
`ephemeral.external.foo`,
|
||||
&Reference{
|
||||
Subject: Resource{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "external",
|
||||
Name: "foo",
|
||||
},
|
||||
SourceRange: tfdiags.SourceRange{
|
||||
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||
End: tfdiags.SourcePos{Line: 1, Column: 23, Byte: 22},
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`ephemeral.external.foo.bar`,
|
||||
&Reference{
|
||||
Subject: ResourceInstance{
|
||||
Resource: Resource{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "external",
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
SourceRange: tfdiags.SourceRange{
|
||||
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||
End: tfdiags.SourcePos{Line: 1, Column: 23, Byte: 22},
|
||||
},
|
||||
Remaining: hcl.Traversal{
|
||||
hcl.TraverseAttr{
|
||||
Name: "bar",
|
||||
SrcRange: hcl.Range{
|
||||
Start: hcl.Pos{Line: 1, Column: 23, Byte: 22},
|
||||
End: hcl.Pos{Line: 1, Column: 27, Byte: 26},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`ephemeral.external.foo["baz"].bar`,
|
||||
&Reference{
|
||||
Subject: ResourceInstance{
|
||||
Resource: Resource{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "external",
|
||||
Name: "foo",
|
||||
},
|
||||
Key: StringKey("baz"),
|
||||
},
|
||||
SourceRange: tfdiags.SourceRange{
|
||||
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||
End: tfdiags.SourcePos{Line: 1, Column: 30, Byte: 29},
|
||||
},
|
||||
Remaining: hcl.Traversal{
|
||||
hcl.TraverseAttr{
|
||||
Name: "bar",
|
||||
SrcRange: hcl.Range{
|
||||
Start: hcl.Pos{Line: 1, Column: 30, Byte: 29},
|
||||
End: hcl.Pos{Line: 1, Column: 34, Byte: 33},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`ephemeral.external.foo["baz"]`,
|
||||
&Reference{
|
||||
Subject: ResourceInstance{
|
||||
Resource: Resource{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "external",
|
||||
Name: "foo",
|
||||
},
|
||||
Key: StringKey("baz"),
|
||||
},
|
||||
SourceRange: tfdiags.SourceRange{
|
||||
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||
End: tfdiags.SourcePos{Line: 1, Column: 30, Byte: 29},
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`ephemeral`,
|
||||
nil,
|
||||
`The "ephemeral" object must be followed by two attribute names: the ephemeral resource type and its name.`,
|
||||
},
|
||||
{
|
||||
`ephemeral.external`,
|
||||
nil,
|
||||
`The "ephemeral" object must be followed by two attribute names: the ephemeral resource type and its name.`,
|
||||
},
|
||||
|
||||
// local
|
||||
{
|
||||
`local.foo`,
|
||||
|
||||
@@ -74,9 +74,13 @@ func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Trav
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
mode := ManagedResourceMode
|
||||
if remain.RootName() == "data" {
|
||||
switch remain.RootName() {
|
||||
case "data":
|
||||
mode = DataResourceMode
|
||||
remain = remain[1:]
|
||||
case "ephemeral":
|
||||
mode = EphemeralResourceMode
|
||||
remain = remain[1:]
|
||||
}
|
||||
|
||||
typeName, name, diags := parseResourceTypeAndName(remain, mode)
|
||||
@@ -135,9 +139,14 @@ func parseResourceUnderModule(moduleAddr Module, remain hcl.Traversal) (ConfigRe
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
mode := ManagedResourceMode
|
||||
if remain.RootName() == "data" {
|
||||
|
||||
switch remain.RootName() {
|
||||
case "data":
|
||||
mode = DataResourceMode
|
||||
remain = remain[1:]
|
||||
case "ephemeral":
|
||||
mode = EphemeralResourceMode
|
||||
remain = remain[1:]
|
||||
}
|
||||
|
||||
typeName, name, diags := parseResourceTypeAndName(remain, mode)
|
||||
@@ -205,6 +214,13 @@ func parseResourceTypeAndName(remain hcl.Traversal, mode ResourceMode) (typeName
|
||||
Detail: "A data source name is required.",
|
||||
Subject: remain[0].SourceRange().Ptr(),
|
||||
})
|
||||
case EphemeralResourceMode:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid address",
|
||||
Detail: "An ephemeral resource name is required.",
|
||||
Subject: remain[0].SourceRange().Ptr(),
|
||||
})
|
||||
default:
|
||||
panic("unknown mode")
|
||||
}
|
||||
|
||||
@@ -323,7 +323,158 @@ func TestParseTarget(t *testing.T) {
|
||||
},
|
||||
``,
|
||||
},
|
||||
// ephemeral
|
||||
|
||||
{
|
||||
`ephemeral.aws_instance.baz`,
|
||||
&Target{
|
||||
Subject: AbsResource{
|
||||
Resource: Resource{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "aws_instance",
|
||||
Name: "baz",
|
||||
},
|
||||
Module: RootModuleInstance,
|
||||
},
|
||||
SourceRange: tfdiags.SourceRange{
|
||||
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||
End: tfdiags.SourcePos{Line: 1, Column: 27, Byte: 26},
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`ephemeral.aws_instance.baz[1]`,
|
||||
&Target{
|
||||
Subject: AbsResourceInstance{
|
||||
Resource: ResourceInstance{
|
||||
Resource: Resource{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "aws_instance",
|
||||
Name: "baz",
|
||||
},
|
||||
Key: IntKey(1),
|
||||
},
|
||||
Module: RootModuleInstance,
|
||||
},
|
||||
SourceRange: tfdiags.SourceRange{
|
||||
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||
End: tfdiags.SourcePos{Line: 1, Column: 30, Byte: 29},
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`module.foo.ephemeral.aws_instance.baz`,
|
||||
&Target{
|
||||
Subject: AbsResource{
|
||||
Resource: Resource{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "aws_instance",
|
||||
Name: "baz",
|
||||
},
|
||||
Module: ModuleInstance{
|
||||
{Name: "foo"},
|
||||
},
|
||||
},
|
||||
SourceRange: tfdiags.SourceRange{
|
||||
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||
End: tfdiags.SourcePos{Line: 1, Column: 38, Byte: 37},
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`module.foo.module.bar.ephemeral.aws_instance.baz`,
|
||||
&Target{
|
||||
Subject: AbsResource{
|
||||
Resource: Resource{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "aws_instance",
|
||||
Name: "baz",
|
||||
},
|
||||
Module: ModuleInstance{
|
||||
{Name: "foo"},
|
||||
{Name: "bar"},
|
||||
},
|
||||
},
|
||||
SourceRange: tfdiags.SourceRange{
|
||||
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||
End: tfdiags.SourcePos{Line: 1, Column: 49, Byte: 48},
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`module.foo.module.bar[0].ephemeral.aws_instance.baz`,
|
||||
&Target{
|
||||
Subject: AbsResource{
|
||||
Resource: Resource{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "aws_instance",
|
||||
Name: "baz",
|
||||
},
|
||||
Module: ModuleInstance{
|
||||
{Name: "foo", InstanceKey: NoKey},
|
||||
{Name: "bar", InstanceKey: IntKey(0)},
|
||||
},
|
||||
},
|
||||
SourceRange: tfdiags.SourceRange{
|
||||
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||
End: tfdiags.SourcePos{Line: 1, Column: 52, Byte: 51},
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`module.foo.module.bar["a"].ephemeral.aws_instance.baz["hello"]`,
|
||||
&Target{
|
||||
Subject: AbsResourceInstance{
|
||||
Resource: ResourceInstance{
|
||||
Resource: Resource{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "aws_instance",
|
||||
Name: "baz",
|
||||
},
|
||||
Key: StringKey("hello"),
|
||||
},
|
||||
Module: ModuleInstance{
|
||||
{Name: "foo", InstanceKey: NoKey},
|
||||
{Name: "bar", InstanceKey: StringKey("a")},
|
||||
},
|
||||
},
|
||||
SourceRange: tfdiags.SourceRange{
|
||||
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||
End: tfdiags.SourcePos{Line: 1, Column: 63, Byte: 62},
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
{
|
||||
`module.foo.module.bar.ephemeral.aws_instance.baz["hello"]`,
|
||||
&Target{
|
||||
Subject: AbsResourceInstance{
|
||||
Resource: ResourceInstance{
|
||||
Resource: Resource{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "aws_instance",
|
||||
Name: "baz",
|
||||
},
|
||||
Key: StringKey("hello"),
|
||||
},
|
||||
Module: ModuleInstance{
|
||||
{Name: "foo"},
|
||||
{Name: "bar"},
|
||||
},
|
||||
},
|
||||
SourceRange: tfdiags.SourceRange{
|
||||
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||
End: tfdiags.SourcePos{Line: 1, Column: 58, Byte: 57},
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
// errors
|
||||
{
|
||||
`aws_instance`,
|
||||
nil,
|
||||
|
||||
@@ -70,6 +70,16 @@ func ParseRemoveEndpoint(traversal hcl.Traversal) (*RemoveEndpoint, tfdiags.Diag
|
||||
|
||||
return nil, diags
|
||||
}
|
||||
if riAddr.Resource.Mode == EphemeralResourceMode {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Ephemeral resource address is not allowed",
|
||||
Detail: "Ephemeral resources cannot be destroyed, and therefore, 'removed' blocks are not allowed to target them. To remove ephemeral resources from the state, you should remove the ephemeral resource block from the configuration.",
|
||||
Subject: traversal.SourceRange().Ptr(),
|
||||
})
|
||||
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
return &RemoveEndpoint{
|
||||
RelSubject: riAddr,
|
||||
|
||||
@@ -177,6 +177,52 @@ func TestParseRemoveEndpoint(t *testing.T) {
|
||||
nil,
|
||||
`Invalid address: A resource name is required.`,
|
||||
},
|
||||
// ephemeral
|
||||
{
|
||||
`ephemeral.foo.bar`,
|
||||
nil,
|
||||
`Ephemeral resource address is not allowed: Ephemeral resources cannot be destroyed, and therefore, 'removed' blocks are not allowed to target them. To remove ephemeral resources from the state, you should remove the ephemeral resource block from the configuration.`,
|
||||
},
|
||||
{
|
||||
`ephemeral.foo.bar[0]`,
|
||||
nil,
|
||||
`Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
|
||||
},
|
||||
{
|
||||
`ephemeral.foo.bar["a"]`,
|
||||
nil,
|
||||
`Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
|
||||
},
|
||||
{
|
||||
`module.boop.ephemeral.foo.bar[0]`,
|
||||
nil,
|
||||
`Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
|
||||
},
|
||||
{
|
||||
`module.boop.ephemeral.foo.bar["a"]`,
|
||||
nil,
|
||||
`Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
|
||||
},
|
||||
{
|
||||
`module.foo.ephemeral`,
|
||||
nil,
|
||||
`Invalid address: Resource specification must include a resource type and name.`,
|
||||
},
|
||||
{
|
||||
`module.foo.ephemeral.bar`,
|
||||
nil,
|
||||
`Invalid address: Resource specification must include a resource type and name.`,
|
||||
},
|
||||
{
|
||||
`module.foo.ephemeral[0]`,
|
||||
nil,
|
||||
`Invalid address: Resource specification must include a resource type and name.`,
|
||||
},
|
||||
{
|
||||
`module.foo.ephemeral.bar[0]`,
|
||||
nil,
|
||||
`Invalid address: A resource name is required.`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -29,6 +29,8 @@ func (r Resource) String() string {
|
||||
return fmt.Sprintf("%s.%s", r.Type, r.Name)
|
||||
case DataResourceMode:
|
||||
return fmt.Sprintf("data.%s.%s", r.Type, r.Name)
|
||||
case EphemeralResourceMode:
|
||||
return fmt.Sprintf("ephemeral.%s.%s", r.Type, r.Name)
|
||||
default:
|
||||
// Should never happen, but we'll return a string here rather than
|
||||
// crashing just in case it does.
|
||||
@@ -43,7 +45,7 @@ func (r Resource) Equal(o Resource) bool {
|
||||
func (r Resource) Less(o Resource) bool {
|
||||
switch {
|
||||
case r.Mode != o.Mode:
|
||||
return r.Mode == DataResourceMode
|
||||
return ResourceModeLess(r.Mode, o.Mode)
|
||||
|
||||
case r.Type != o.Type:
|
||||
return r.Type < o.Type
|
||||
@@ -511,4 +513,38 @@ const (
|
||||
// DataResourceMode indicates a data resource, as defined by
|
||||
// "data" blocks in configuration.
|
||||
DataResourceMode ResourceMode = 'D'
|
||||
|
||||
// EphemeralResourceMode indicates an ephemeral resource, as defined by
|
||||
// the "ephemeral" blocks in configuration.
|
||||
EphemeralResourceMode ResourceMode = 'E'
|
||||
)
|
||||
|
||||
// ResourceModeLess is comparing two ResourceMode.
|
||||
// The ranking is as follows: EphemeralResourceMode, DataResourceMode, ManagedResourceMode.
|
||||
func ResourceModeLess(a, b ResourceMode) bool {
|
||||
switch a {
|
||||
case ManagedResourceMode:
|
||||
return false // No other mode should be after ManagedResourceMode
|
||||
case DataResourceMode:
|
||||
return b == ManagedResourceMode // DataResourceMode is always lower than ManagedResourceMode
|
||||
case EphemeralResourceMode:
|
||||
return b == ManagedResourceMode || b == DataResourceMode // EphemeralResourceMode is always lower than ManagedResourceMode and DataResourceMode
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ResourceModeBlockName returns the name of the block that the given ResourceMode is mapped to.
|
||||
// At the time of writing this, the string values returned from this one are hardcoded all over the place so this is not
|
||||
// the source of truth for the name of those blocks.
|
||||
func ResourceModeBlockName(rm ResourceMode) string {
|
||||
switch rm {
|
||||
case ManagedResourceMode:
|
||||
return "resource"
|
||||
case DataResourceMode:
|
||||
return "data"
|
||||
case EphemeralResourceMode:
|
||||
return "ephemeral"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package addrs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -26,6 +27,11 @@ func TestResourceEqual_true(t *testing.T) {
|
||||
Type: "a",
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "a",
|
||||
Name: "b",
|
||||
},
|
||||
}
|
||||
for _, r := range resources {
|
||||
t.Run(r.String(), func(t *testing.T) {
|
||||
@@ -53,6 +59,10 @@ func TestResourceEqual_false(t *testing.T) {
|
||||
Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"},
|
||||
Resource{Mode: ManagedResourceMode, Type: "a", Name: "c"},
|
||||
},
|
||||
{
|
||||
Resource{Mode: EphemeralResourceMode, Type: "a", Name: "b"},
|
||||
Resource{Mode: EphemeralResourceMode, Type: "a", Name: "c"},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("%s = %s", tc.left, tc.right), func(t *testing.T) {
|
||||
@@ -85,6 +95,14 @@ func TestResourceInstanceEqual_true(t *testing.T) {
|
||||
},
|
||||
Key: StringKey("x"),
|
||||
},
|
||||
{
|
||||
Resource: Resource{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "a",
|
||||
Name: "b",
|
||||
},
|
||||
Key: StringKey("x"),
|
||||
},
|
||||
}
|
||||
for _, r := range resources {
|
||||
t.Run(r.String(), func(t *testing.T) {
|
||||
@@ -110,6 +128,16 @@ func TestResourceInstanceEqual_false(t *testing.T) {
|
||||
Key: IntKey(0),
|
||||
},
|
||||
},
|
||||
{
|
||||
ResourceInstance{
|
||||
Resource: Resource{Mode: EphemeralResourceMode, Type: "a", Name: "b"},
|
||||
Key: IntKey(0),
|
||||
},
|
||||
ResourceInstance{
|
||||
Resource: Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"},
|
||||
Key: IntKey(0),
|
||||
},
|
||||
},
|
||||
{
|
||||
ResourceInstance{
|
||||
Resource: Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"},
|
||||
@@ -140,6 +168,26 @@ func TestResourceInstanceEqual_false(t *testing.T) {
|
||||
Key: StringKey("0"),
|
||||
},
|
||||
},
|
||||
{
|
||||
ResourceInstance{
|
||||
Resource: Resource{Mode: EphemeralResourceMode, Type: "a", Name: "b"},
|
||||
Key: IntKey(0),
|
||||
},
|
||||
ResourceInstance{
|
||||
Resource: Resource{Mode: EphemeralResourceMode, Type: "a", Name: "b"},
|
||||
Key: StringKey("0"),
|
||||
},
|
||||
},
|
||||
{
|
||||
ResourceInstance{
|
||||
Resource: Resource{Mode: DataResourceMode, Type: "a", Name: "b"},
|
||||
Key: IntKey(0),
|
||||
},
|
||||
ResourceInstance{
|
||||
Resource: Resource{Mode: EphemeralResourceMode, Type: "a", Name: "b"},
|
||||
Key: IntKey(0),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("%s = %s", tc.left, tc.right), func(t *testing.T) {
|
||||
@@ -157,6 +205,7 @@ func TestResourceInstanceEqual_false(t *testing.T) {
|
||||
func TestAbsResourceInstanceEqual_true(t *testing.T) {
|
||||
managed := Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"}
|
||||
data := Resource{Mode: DataResourceMode, Type: "a", Name: "b"}
|
||||
ephemeral := Resource{Mode: EphemeralResourceMode, Type: "a", Name: "b"}
|
||||
|
||||
foo, diags := ParseModuleInstanceStr("module.foo")
|
||||
if len(diags) > 0 {
|
||||
@@ -170,7 +219,9 @@ func TestAbsResourceInstanceEqual_true(t *testing.T) {
|
||||
instances := []AbsResourceInstance{
|
||||
managed.Instance(IntKey(0)).Absolute(foo),
|
||||
data.Instance(IntKey(0)).Absolute(foo),
|
||||
ephemeral.Instance(IntKey(0)).Absolute(foo),
|
||||
managed.Instance(StringKey("a")).Absolute(foobar),
|
||||
ephemeral.Instance(IntKey(0)).Absolute(foobar),
|
||||
}
|
||||
for _, r := range instances {
|
||||
t.Run(r.String(), func(t *testing.T) {
|
||||
@@ -184,6 +235,7 @@ func TestAbsResourceInstanceEqual_true(t *testing.T) {
|
||||
func TestAbsResourceInstanceEqual_false(t *testing.T) {
|
||||
managed := Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"}
|
||||
data := Resource{Mode: DataResourceMode, Type: "a", Name: "b"}
|
||||
ephemeral := Resource{Mode: EphemeralResourceMode, Type: "a", Name: "b"}
|
||||
|
||||
foo, diags := ParseModuleInstanceStr("module.foo")
|
||||
if len(diags) > 0 {
|
||||
@@ -210,6 +262,14 @@ func TestAbsResourceInstanceEqual_false(t *testing.T) {
|
||||
managed.Instance(IntKey(0)).Absolute(foo),
|
||||
managed.Instance(StringKey("0")).Absolute(foo),
|
||||
},
|
||||
{
|
||||
ephemeral.Instance(IntKey(0)).Absolute(foo),
|
||||
ephemeral.Instance(IntKey(0)).Absolute(foobar),
|
||||
},
|
||||
{
|
||||
ephemeral.Instance(StringKey("0")).Absolute(foo),
|
||||
ephemeral.Instance(IntKey(0)).Absolute(foo),
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("%s = %s", tc.left, tc.right), func(t *testing.T) {
|
||||
@@ -305,6 +365,10 @@ func TestConfigResourceEqual_true(t *testing.T) {
|
||||
Resource: Resource{Mode: DataResourceMode, Type: "a", Name: "b"},
|
||||
Module: RootModule,
|
||||
},
|
||||
{
|
||||
Resource: Resource{Mode: EphemeralResourceMode, Type: "a", Name: "b"},
|
||||
Module: RootModule,
|
||||
},
|
||||
{
|
||||
Resource: Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"},
|
||||
Module: Module{"foo"},
|
||||
@@ -313,6 +377,10 @@ func TestConfigResourceEqual_true(t *testing.T) {
|
||||
Resource: Resource{Mode: DataResourceMode, Type: "a", Name: "b"},
|
||||
Module: Module{"foo"},
|
||||
},
|
||||
{
|
||||
Resource: Resource{Mode: EphemeralResourceMode, Type: "a", Name: "b"},
|
||||
Module: Module{"foo"},
|
||||
},
|
||||
}
|
||||
for _, r := range resources {
|
||||
t.Run(r.String(), func(t *testing.T) {
|
||||
@@ -326,6 +394,7 @@ func TestConfigResourceEqual_true(t *testing.T) {
|
||||
func TestConfigResourceEqual_false(t *testing.T) {
|
||||
managed := Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"}
|
||||
data := Resource{Mode: DataResourceMode, Type: "a", Name: "b"}
|
||||
ephemeral := Resource{Mode: EphemeralResourceMode, Type: "a", Name: "b"}
|
||||
|
||||
foo := Module{"foo"}
|
||||
foobar := Module{"foobar"}
|
||||
@@ -337,10 +406,22 @@ func TestConfigResourceEqual_false(t *testing.T) {
|
||||
ConfigResource{Resource: managed, Module: foo},
|
||||
ConfigResource{Resource: data, Module: foo},
|
||||
},
|
||||
{
|
||||
ConfigResource{Resource: managed, Module: foo},
|
||||
ConfigResource{Resource: ephemeral, Module: foo},
|
||||
},
|
||||
{
|
||||
ConfigResource{Resource: data, Module: foo},
|
||||
ConfigResource{Resource: ephemeral, Module: foo},
|
||||
},
|
||||
{
|
||||
ConfigResource{Resource: managed, Module: foo},
|
||||
ConfigResource{Resource: managed, Module: foobar},
|
||||
},
|
||||
{
|
||||
ConfigResource{Resource: ephemeral, Module: foo},
|
||||
ConfigResource{Resource: ephemeral, Module: foobar},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("%s = %s", tc.left, tc.right), func(t *testing.T) {
|
||||
@@ -385,6 +466,17 @@ func TestParseConfigResource(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: "ephemeral.a.b",
|
||||
WantConfigResource: ConfigResource{
|
||||
Module: RootModule,
|
||||
Resource: Resource{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "a",
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: "module.a.b.c",
|
||||
WantConfigResource: ConfigResource{
|
||||
@@ -407,6 +499,17 @@ func TestParseConfigResource(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: "module.a.ephemeral.b.c",
|
||||
WantConfigResource: ConfigResource{
|
||||
Module: []string{"a"},
|
||||
Resource: Resource{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "b",
|
||||
Name: "c",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: "module.a.module.b.c.d",
|
||||
WantConfigResource: ConfigResource{
|
||||
@@ -429,6 +532,17 @@ func TestParseConfigResource(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: "module.a.module.b.ephemeral.c.d",
|
||||
WantConfigResource: ConfigResource{
|
||||
Module: []string{"a", "b"},
|
||||
Resource: Resource{
|
||||
Mode: EphemeralResourceMode,
|
||||
Type: "c",
|
||||
Name: "d",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Input: "module.a.module.b",
|
||||
WantErr: "Module address is not allowed: Expected reference to either resource or data block. Provided reference appears to be a module.",
|
||||
@@ -449,6 +563,10 @@ func TestParseConfigResource(t *testing.T) {
|
||||
Input: "module.a.module.b.data.c.d[0]",
|
||||
WantErr: `Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
|
||||
},
|
||||
{
|
||||
Input: "module.a.module.b.ephemeral.c.d[0]",
|
||||
WantErr: `Resource instance address with keys is not allowed: Resource address cannot be a resource instance (e.g. "null_resource.a[0]"), it must be a resource instead (e.g. "null_resource.a").`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -485,3 +603,82 @@ func TestParseConfigResource(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceLess(t *testing.T) {
|
||||
tests := []struct {
|
||||
left, right Resource
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"},
|
||||
Resource{Mode: DataResourceMode, Type: "a", Name: "b"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
Resource{Mode: DataResourceMode, Type: "a", Name: "b"},
|
||||
Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"},
|
||||
Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"},
|
||||
Resource{Mode: ManagedResourceMode, Type: "a", Name: "c"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"},
|
||||
Resource{Mode: ManagedResourceMode, Type: "b", Name: "b"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
Resource{Mode: EphemeralResourceMode, Type: "a", Name: "b"},
|
||||
Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"},
|
||||
Resource{Mode: EphemeralResourceMode, Type: "a", Name: "b"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
Resource{Mode: EphemeralResourceMode, Type: "a", Name: "b"},
|
||||
Resource{Mode: DataResourceMode, Type: "a", Name: "b"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
Resource{Mode: DataResourceMode, Type: "a", Name: "b"},
|
||||
Resource{Mode: EphemeralResourceMode, Type: "a", Name: "b"},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
wantComparison := ">"
|
||||
if tt.want {
|
||||
wantComparison = "<"
|
||||
}
|
||||
t.Run(fmt.Sprintf("%s %s %s", tt.left, wantComparison, tt.right), func(t *testing.T) {
|
||||
if got, want := tt.left.Less(tt.right), tt.want; got != want {
|
||||
t.Fatalf("wrong expectation between %q and %q. want: %t; got: %t", tt.left, tt.right, want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceSort(t *testing.T) {
|
||||
managed := Resource{Mode: ManagedResourceMode, Type: "a", Name: "b"}
|
||||
data := Resource{Mode: DataResourceMode, Type: "a", Name: "b"}
|
||||
ephemeral := Resource{Mode: EphemeralResourceMode, Type: "a", Name: "b"}
|
||||
|
||||
got := []Resource{managed, data, ephemeral, managed, ephemeral, data}
|
||||
sort.SliceStable(got, func(i, j int) bool { return got[i].Less(got[j]) })
|
||||
|
||||
want := []Resource{ephemeral, ephemeral, data, data, managed, managed}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatalf("expected no diff meaning that sorting is not working properly.\ndiff: %s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,20 +11,26 @@ func _() {
|
||||
_ = x[InvalidResourceMode-0]
|
||||
_ = x[ManagedResourceMode-77]
|
||||
_ = x[DataResourceMode-68]
|
||||
_ = x[EphemeralResourceMode-69]
|
||||
}
|
||||
|
||||
const (
|
||||
_ResourceMode_name_0 = "InvalidResourceMode"
|
||||
_ResourceMode_name_1 = "DataResourceMode"
|
||||
_ResourceMode_name_1 = "DataResourceModeEphemeralResourceMode"
|
||||
_ResourceMode_name_2 = "ManagedResourceMode"
|
||||
)
|
||||
|
||||
var (
|
||||
_ResourceMode_index_1 = [...]uint8{0, 16, 37}
|
||||
)
|
||||
|
||||
func (i ResourceMode) String() string {
|
||||
switch {
|
||||
case i == 0:
|
||||
return _ResourceMode_name_0
|
||||
case i == 68:
|
||||
return _ResourceMode_name_1
|
||||
case 68 <= i && i <= 69:
|
||||
i -= 68
|
||||
return _ResourceMode_name_1[_ResourceMode_index_1[i]:_ResourceMode_index_1[i+1]]
|
||||
case i == 77:
|
||||
return _ResourceMode_name_2
|
||||
default:
|
||||
|
||||
@@ -78,6 +78,11 @@ func (p *Provider) ValidateDataResourceConfig(_ context.Context, req providers.V
|
||||
return res
|
||||
}
|
||||
|
||||
// ValidateEphemeralConfig is used to validate the ephemeral resource configuration values.
|
||||
func (p *Provider) ValidateEphemeralConfig(context.Context, providers.ValidateEphemeralConfigRequest) providers.ValidateEphemeralConfigResponse {
|
||||
panic("Should not be called directly, special case for terraform_remote_state")
|
||||
}
|
||||
|
||||
// Configure configures and initializes the provider.
|
||||
func (p *Provider) ConfigureProvider(context.Context, providers.ConfigureProviderRequest) providers.ConfigureProviderResponse {
|
||||
// At this moment there is nothing to configure for the terraform provider,
|
||||
@@ -125,6 +130,22 @@ func (p *Provider) ReadDataSourceEncrypted(ctx context.Context, req providers.Re
|
||||
return res
|
||||
}
|
||||
|
||||
// OpenEphemeralResource opens an ephemeral resource returning the ephemeral value returned from the provider.
|
||||
func (p *Provider) OpenEphemeralResource(context.Context, providers.OpenEphemeralResourceRequest) providers.OpenEphemeralResourceResponse {
|
||||
panic("Should not be called directly, special case for terraform_remote_state")
|
||||
}
|
||||
|
||||
// RenewEphemeralResource is renewing an ephemeral resource returning only the private information from the provider.
|
||||
func (p *Provider) RenewEphemeralResource(context.Context, providers.RenewEphemeralResourceRequest) providers.RenewEphemeralResourceResponse {
|
||||
panic("Should not be called directly, special case for terraform_remote_state")
|
||||
}
|
||||
|
||||
// CloseEphemeralResource is closing an ephemeral resource to allow the provider to clean up any possible remote information
|
||||
// bound to the previously opened ephemeral resource.
|
||||
func (p *Provider) CloseEphemeralResource(context.Context, providers.CloseEphemeralResourceRequest) providers.CloseEphemeralResourceResponse {
|
||||
panic("Should not be called directly, special case for terraform_remote_state")
|
||||
}
|
||||
|
||||
// Stop is called when the provider should halt any in-flight actions.
|
||||
func (p *Provider) Stop(_ context.Context) error {
|
||||
log.Println("[DEBUG] terraform provider cannot Stop")
|
||||
|
||||
@@ -53,6 +53,7 @@ func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) {
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Ephemeral: true,
|
||||
}
|
||||
resp.Provisioner = schema
|
||||
return resp
|
||||
|
||||
@@ -67,6 +67,7 @@ func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) {
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
Ephemeral: true,
|
||||
}
|
||||
|
||||
resp.Provisioner = schema
|
||||
|
||||
@@ -55,6 +55,7 @@ func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) {
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
Ephemeral: true,
|
||||
}
|
||||
|
||||
resp.Provisioner = schema
|
||||
|
||||
@@ -35,6 +35,10 @@ func collectInitialStatuses(into addrs.Map[addrs.ConfigCheckable, *configCheckab
|
||||
addr := rc.Addr().InModule(moduleAddr)
|
||||
collectInitialStatusForResource(into, addr, rc)
|
||||
}
|
||||
for _, rc := range cfg.Module.EphemeralResources {
|
||||
addr := rc.Addr().InModule(moduleAddr)
|
||||
collectInitialStatusForResource(into, addr, rc)
|
||||
}
|
||||
|
||||
for _, oc := range cfg.Module.Outputs {
|
||||
addr := oc.Addr().InModule(moduleAddr)
|
||||
|
||||
@@ -65,6 +65,16 @@ func TestChecksHappyPath(t *testing.T) {
|
||||
Type: "null_resource",
|
||||
Name: "c",
|
||||
}.InModule(moduleChild)
|
||||
dataFoo := addrs.Resource{
|
||||
Mode: addrs.DataResourceMode,
|
||||
Type: "aws_s3_object",
|
||||
Name: "foo",
|
||||
}.InModule(moduleChild)
|
||||
ephemeralBar := addrs.Resource{
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Type: "aws_secretsmanager_secret_version",
|
||||
Name: "bar",
|
||||
}.InModule(moduleChild)
|
||||
childOutput := addrs.OutputValue{
|
||||
Name: "b",
|
||||
}.InModule(moduleChild)
|
||||
@@ -80,6 +90,15 @@ func TestChecksHappyPath(t *testing.T) {
|
||||
if addr := resourceB; cfg.Children["child"].Module.ResourceByAddr(addr.Resource) == nil {
|
||||
t.Fatalf("configuration does not include %s", addr)
|
||||
}
|
||||
if addr := resourceC; cfg.Children["child"].Module.ResourceByAddr(addr.Resource) == nil {
|
||||
t.Fatalf("configuration does not include %s", addr)
|
||||
}
|
||||
if addr := dataFoo; cfg.Children["child"].Module.ResourceByAddr(addr.Resource) == nil {
|
||||
t.Fatalf("configuration does not include %s", addr)
|
||||
}
|
||||
if addr := ephemeralBar; cfg.Children["child"].Module.ResourceByAddr(addr.Resource) == nil {
|
||||
t.Fatalf("configuration does not include %s", addr)
|
||||
}
|
||||
if addr := resourceNoChecks; cfg.Module.ResourceByAddr(addr.Resource) == nil {
|
||||
t.Fatalf("configuration does not include %s", addr)
|
||||
}
|
||||
@@ -107,6 +126,14 @@ func TestChecksHappyPath(t *testing.T) {
|
||||
t.Errorf("checks not detected for %s", addr)
|
||||
missing++
|
||||
}
|
||||
if addr := dataFoo; !checks.ConfigHasChecks(addr) {
|
||||
t.Errorf("checks not detected for %s", addr)
|
||||
missing++
|
||||
}
|
||||
if addr := ephemeralBar; !checks.ConfigHasChecks(addr) {
|
||||
t.Errorf("checks not detected for %s", addr)
|
||||
missing++
|
||||
}
|
||||
if addr := rootOutput; !checks.ConfigHasChecks(addr) {
|
||||
t.Errorf("checks not detected for %s", addr)
|
||||
missing++
|
||||
@@ -138,6 +165,8 @@ func TestChecksHappyPath(t *testing.T) {
|
||||
resourceA,
|
||||
resourceB,
|
||||
resourceC,
|
||||
dataFoo,
|
||||
ephemeralBar,
|
||||
rootOutput,
|
||||
childOutput,
|
||||
checkBlock,
|
||||
@@ -168,6 +197,8 @@ func TestChecksHappyPath(t *testing.T) {
|
||||
moduleChildInst := addrs.RootModuleInstance.Child("child", addrs.NoKey)
|
||||
resourceInstB := resourceB.Resource.Absolute(moduleChildInst).Instance(addrs.NoKey)
|
||||
resourceInstC0 := resourceC.Resource.Absolute(moduleChildInst).Instance(addrs.IntKey(0))
|
||||
dataInstFoo := dataFoo.Resource.Absolute(moduleChildInst).Instance(addrs.NoKey)
|
||||
ephemeralInstBar := ephemeralBar.Resource.Absolute(moduleChildInst).Instance(addrs.NoKey)
|
||||
resourceInstC1 := resourceC.Resource.Absolute(moduleChildInst).Instance(addrs.IntKey(1))
|
||||
childOutputInst := childOutput.OutputValue.Absolute(moduleChildInst)
|
||||
checkBlockInst := checkBlock.Check.Absolute(addrs.RootModuleInstance)
|
||||
@@ -184,6 +215,12 @@ func TestChecksHappyPath(t *testing.T) {
|
||||
checks.ReportCheckResult(resourceInstC0, addrs.ResourcePostcondition, 0, StatusPass)
|
||||
checks.ReportCheckResult(resourceInstC1, addrs.ResourcePostcondition, 0, StatusPass)
|
||||
|
||||
checks.ReportCheckableObjects(dataFoo, addrs.MakeSet[addrs.Checkable](dataInstFoo))
|
||||
checks.ReportCheckResult(dataInstFoo, addrs.ResourcePrecondition, 0, StatusPass)
|
||||
|
||||
checks.ReportCheckableObjects(ephemeralBar, addrs.MakeSet[addrs.Checkable](ephemeralInstBar))
|
||||
checks.ReportCheckResult(ephemeralInstBar, addrs.ResourcePrecondition, 0, StatusPass)
|
||||
|
||||
checks.ReportCheckableObjects(childOutput, addrs.MakeSet[addrs.Checkable](childOutputInst))
|
||||
checks.ReportCheckResult(childOutputInst, addrs.OutputPrecondition, 0, StatusPass)
|
||||
|
||||
@@ -206,7 +243,7 @@ func TestChecksHappyPath(t *testing.T) {
|
||||
t.Errorf("incorrect final aggregate check status for %s: %s, but want %s", configAddr, got, want)
|
||||
}
|
||||
}
|
||||
if got, want := configCount, 6; got != want {
|
||||
if got, want := configCount, 8; got != want {
|
||||
t.Errorf("incorrect number of known config addresses %d; want %d", got, want)
|
||||
}
|
||||
}
|
||||
@@ -218,6 +255,8 @@ func TestChecksHappyPath(t *testing.T) {
|
||||
resourceInstB,
|
||||
resourceInstC0,
|
||||
resourceInstC1,
|
||||
dataInstFoo,
|
||||
ephemeralInstBar,
|
||||
childOutputInst,
|
||||
checkBlockInst,
|
||||
)
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "5.81.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
resource "null_resource" "b" {
|
||||
lifecycle {
|
||||
precondition {
|
||||
@@ -18,6 +26,27 @@ resource "null_resource" "c" {
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_s3_object" "foo" {
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = self.id == ""
|
||||
error_message = "Impossible data."
|
||||
}
|
||||
}
|
||||
bucket = "test-bucket"
|
||||
key = "test-key"
|
||||
}
|
||||
|
||||
ephemeral "aws_secretsmanager_secret_version" "bar" {
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = self.id == ""
|
||||
error_message = "Impossible ephemeral."
|
||||
}
|
||||
}
|
||||
secret_id = "secret-manager-id"
|
||||
}
|
||||
|
||||
output "b" {
|
||||
value = null_resource.b.id
|
||||
|
||||
@@ -26,4 +55,3 @@ output "b" {
|
||||
error_message = "B has no id."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,20 @@
|
||||
package e2etest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/e2e"
|
||||
"github.com/opentofu/opentofu/internal/getproviders"
|
||||
"github.com/opentofu/opentofu/internal/plans"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
@@ -232,3 +238,287 @@ func TestPrimaryChdirOption(t *testing.T) {
|
||||
t.Errorf("incorrect destroy tally; want 0 destroyed:\n%s", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// This test is checking the workflow of the ephemeral resources.
|
||||
// Check also the configuration files for comments. The idea is that at the time of
|
||||
// writing, the configuration was done in such a way to fail later when the
|
||||
// marks will be introduced for ephemeral values. Therefore, this test will
|
||||
// fail later and will require adjustments.
|
||||
//
|
||||
// We want to validate that the plan file, state file and the output contain
|
||||
// only the things that are needed:
|
||||
// - The plan file needs to contain **only** the stubs of the ephemeral resources
|
||||
// and not the values that it generated. This is needed for `tofu apply planfile`
|
||||
// to be able to generate the execution node graphs correctly.
|
||||
// - The state file must not contain the ephemeral resources changes.
|
||||
// - The output should contain no changes related to ephemeral resources, but only
|
||||
// the status update of their execution.
|
||||
func TestEphemeralWorkflowAndOutput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
skipIfCannotAccessNetwork(t)
|
||||
pluginVersionRunner := func(t *testing.T, testdataPath string, providerBuilderFunc func(*testing.T, string)) {
|
||||
tf := e2e.NewBinary(t, tofuBin, testdataPath)
|
||||
providerBuilderFunc(t, tf.WorkDir())
|
||||
|
||||
{ //// INIT
|
||||
_, stderr, err := tf.Run("init", "-plugin-dir=cache")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
{ //// PLAN
|
||||
stdout, stderr, err := tf.Run("plan", "-out=tfplan")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
|
||||
}
|
||||
// TODO ephemeral - this "value_wo" should be shown something like (write-only attribute). This will be handled during the work on the write-only attributes.
|
||||
// TODO ephemeral - "out_ephemeral" should fail later when the marking of the outputs is implemented fully, so that should not be visible in the output
|
||||
expectedChangesOutput := `OpenTofu used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
<= read (data resources)
|
||||
|
||||
OpenTofu will perform the following actions:
|
||||
|
||||
# data.simple_resource.test_data2 will be read during apply
|
||||
# (depends on a resource or a module with changes pending)
|
||||
<= data "simple_resource" "test_data2" {
|
||||
+ id = (known after apply)
|
||||
+ value = "test"
|
||||
}
|
||||
|
||||
# simple_resource.test_res will be created
|
||||
+ resource "simple_resource" "test_res" {
|
||||
+ value = "test value"
|
||||
}
|
||||
|
||||
# simple_resource.test_res_second_provider will be created
|
||||
+ resource "simple_resource" "test_res_second_provider" {
|
||||
+ value = "just a simple resource to ensure that the second provider it's working fine"
|
||||
}
|
||||
|
||||
Plan: 2 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ final_output = "just a simple resource to ensure that the second provider it's working fine"
|
||||
+ out_ephemeral = "rawvalue"`
|
||||
|
||||
expectedResourcesUpdates := map[string]bool{
|
||||
"data.simple_resource.test_data1: Reading...": true,
|
||||
"data.simple_resource.test_data1: Read complete after": true,
|
||||
"ephemeral.simple_resource.test_ephemeral[0]: Opening...": true,
|
||||
"ephemeral.simple_resource.test_ephemeral[0]: Open complete after": true,
|
||||
"ephemeral.simple_resource.test_ephemeral[1]: Opening...": true,
|
||||
"ephemeral.simple_resource.test_ephemeral[1]: Open complete after": true,
|
||||
"ephemeral.simple_resource.test_ephemeral[0]: Closing...": true,
|
||||
"ephemeral.simple_resource.test_ephemeral[0]: Close complete after": true,
|
||||
"ephemeral.simple_resource.test_ephemeral[1]: Closing...": true,
|
||||
"ephemeral.simple_resource.test_ephemeral[1]: Close complete after": true,
|
||||
}
|
||||
out := stripAnsi(stdout)
|
||||
|
||||
if !strings.Contains(out, expectedChangesOutput) {
|
||||
t.Errorf("wrong plan output:\nstdout:%s\nstderr:%s", stdout, stderr)
|
||||
}
|
||||
|
||||
for reg, required := range expectedResourcesUpdates {
|
||||
if strings.Contains(out, reg) {
|
||||
continue
|
||||
}
|
||||
if required {
|
||||
t.Errorf("plan output does not contain required content %q\nout:%s", reg, out)
|
||||
} else {
|
||||
// We don't want to fail the test for outputs that are performance and time dependent
|
||||
// as the renew status updates
|
||||
t.Logf("plan output does not contain %q\nout:%s", reg, out)
|
||||
}
|
||||
}
|
||||
|
||||
// assert plan file content
|
||||
plan, err := tf.Plan("tfplan")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read the plan file: %s", err)
|
||||
}
|
||||
idx := slices.IndexFunc(plan.Changes.Resources, func(src *plans.ResourceInstanceChangeSrc) bool {
|
||||
return src.Addr.Resource.Resource.Mode == addrs.EphemeralResourceMode
|
||||
})
|
||||
if idx < 0 {
|
||||
t.Fatalf("no ephemeral resource found in the plan file")
|
||||
}
|
||||
res := plan.Changes.Resources[idx]
|
||||
if res.Before != nil {
|
||||
t.Errorf("ephemeral resource %q from plan contains before value but it shouldn't: %s", res.Addr.String(), res.Before)
|
||||
}
|
||||
if res.After != nil {
|
||||
t.Errorf("ephemeral resource %q from plan contains after value but it shouldn't: %s", res.Addr.String(), res.After)
|
||||
}
|
||||
if got, want := res.Action, plans.Open; got != want {
|
||||
t.Errorf("ephemeral resource %q from plan contains wrong actions. want %q; got %q", res.Addr.String(), want, got)
|
||||
}
|
||||
}
|
||||
|
||||
{ //// APPLY
|
||||
stdout, stderr, err := tf.Run("apply", "tfplan")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
|
||||
}
|
||||
state, err := tf.LocalState()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read local state: %s", err)
|
||||
}
|
||||
expectedResources := map[string]bool{
|
||||
"data.simple_resource.test_data1": true,
|
||||
"data.simple_resource.test_data2": true,
|
||||
"simple_resource.test_res": true,
|
||||
"simple_resource.test_res_second_provider": true,
|
||||
"ephemeral.simple_resource.test_ephemeral": false,
|
||||
}
|
||||
for res, exists := range expectedResources {
|
||||
_, ok := state.RootModule().Resources[res]
|
||||
if ok != exists {
|
||||
t.Errorf("expected resource %q existence to be %t but got %t", res, exists, ok)
|
||||
}
|
||||
}
|
||||
|
||||
expectedChangesOutput := `Apply complete! Resources: 2 added, 0 changed, 0 destroyed.`
|
||||
// NOTE: the non-required ones are dependent on the performance of the platform that this test is running on.
|
||||
// In CI, if we would make this as required, this test might be flaky.
|
||||
expectedResourcesUpdates := map[string]bool{
|
||||
"ephemeral.simple_resource.test_ephemeral[0]: Opening...": true,
|
||||
"ephemeral.simple_resource.test_ephemeral[0]: Open complete after": true,
|
||||
"ephemeral.simple_resource.test_ephemeral[1]: Opening...": true,
|
||||
"ephemeral.simple_resource.test_ephemeral[1]: Open complete after": true,
|
||||
"data.simple_resource.test_data2: Reading...": true,
|
||||
"data.simple_resource.test_data2: Read complete after": true,
|
||||
"simple_resource.test_res: Creating...": true,
|
||||
"simple_resource.test_res_second_provider: Creating...": true,
|
||||
"simple_resource.test_res_second_provider: Creation complete after": true,
|
||||
"ephemeral.simple_resource.test_ephemeral[0]: Renewing...": false,
|
||||
"ephemeral.simple_resource.test_ephemeral[0]: Renew complete after": false,
|
||||
"ephemeral.simple_resource.test_ephemeral[1]: Renewing...": false,
|
||||
"ephemeral.simple_resource.test_ephemeral[1]: Renew complete after": false,
|
||||
"simple_resource.test_res: Creation complete after": true,
|
||||
"ephemeral.simple_resource.test_ephemeral[0]: Closing...": true,
|
||||
"ephemeral.simple_resource.test_ephemeral[0]: Close complete after": true,
|
||||
"ephemeral.simple_resource.test_ephemeral[1]: Closing...": true,
|
||||
"simple_resource.test_res: Provisioning with 'local-exec'...": true,
|
||||
`simple_resource.test_res (local-exec): Executing: ["/bin/sh" "-c" "echo \"visible test value\""]`: true,
|
||||
"simple_resource.test_res (local-exec): visible test value": true,
|
||||
"simple_resource.test_res (local-exec): (output suppressed due to ephemeral value in config)": true,
|
||||
}
|
||||
out := stripAnsi(stdout)
|
||||
|
||||
if !strings.Contains(out, expectedChangesOutput) {
|
||||
t.Errorf("wrong apply output:\nstdout:%s\nstderr%s", stdout, stderr)
|
||||
}
|
||||
|
||||
for reg, required := range expectedResourcesUpdates {
|
||||
if strings.Contains(out, reg) {
|
||||
continue
|
||||
}
|
||||
if required {
|
||||
t.Errorf("apply output does not contain required content %q\nout:%s", reg, out)
|
||||
} else {
|
||||
// We don't want to fail the test for outputs that are performance and time dependent
|
||||
// as the renew status updates
|
||||
t.Logf("apply output does not contain %q\nout:%s", reg, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
{ //// DESTROY
|
||||
stdout, stderr, err := tf.Run("destroy", "-auto-approve")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected destroy error: %s\nstderr:\n%s", err, stderr)
|
||||
}
|
||||
|
||||
if !strings.Contains(stdout, "Resources: 2 destroyed") {
|
||||
t.Errorf("incorrect destroy tally; want 2 destroyed:\n%s", stdout)
|
||||
}
|
||||
|
||||
state, err := tf.LocalState()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read state file after destroy: %s", err)
|
||||
}
|
||||
|
||||
stateResources := state.RootModule().Resources
|
||||
if len(stateResources) != 0 {
|
||||
t.Errorf("wrong resources in state after destroy; want none, but still have:%s", spew.Sdump(stateResources))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
protoBinBuilder func(t *testing.T, workdir string)
|
||||
}{
|
||||
"proto version 5": {
|
||||
protoBinBuilder: func(t *testing.T, workdir string) {
|
||||
buildSimpleProvider(t, "5", workdir, "simple")
|
||||
},
|
||||
},
|
||||
"proto version 6": {
|
||||
protoBinBuilder: func(t *testing.T, workdir string) {
|
||||
buildSimpleProvider(t, "6", workdir, "simple")
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tt := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
pluginVersionRunner(t, "testdata/ephemeral-workflow", tt.protoBinBuilder)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This function builds and moves to a directory called "cache" inside the workdir,
|
||||
// the version of the provider passed as argument.
|
||||
// Instead of using this function directly, the pre-configured functions buildV5TestProvider and
|
||||
// buildV6TestProvider can be used.
|
||||
func buildSimpleProvider(t *testing.T, version string, workdir string, buildOutName string) {
|
||||
if !canRunGoBuild {
|
||||
// We're running in a separate-build-then-run context, so we can't
|
||||
// currently execute this test which depends on being able to build
|
||||
// new executable at runtime.
|
||||
//
|
||||
// (See the comment on canRunGoBuild's declaration for more information.)
|
||||
t.Skip("can't run without building a new provider executable")
|
||||
}
|
||||
|
||||
var (
|
||||
providerBinFileName string
|
||||
implPkgName string
|
||||
)
|
||||
switch version {
|
||||
case "5":
|
||||
providerBinFileName = "simple"
|
||||
implPkgName = "provider-simple"
|
||||
case "6":
|
||||
providerBinFileName = "simple6"
|
||||
implPkgName = "provider-simple-v6"
|
||||
default:
|
||||
t.Fatalf("invalid version for simple provider")
|
||||
}
|
||||
if buildOutName != "" {
|
||||
providerBinFileName = buildOutName
|
||||
}
|
||||
providerBuildOutDir := filepath.Join(workdir, fmt.Sprintf("terraform-provider-%s", providerBinFileName))
|
||||
providerTmpBinPath := e2e.GoBuild(fmt.Sprintf("github.com/opentofu/opentofu/internal/%s/main", implPkgName), providerBuildOutDir)
|
||||
|
||||
extension := ""
|
||||
if runtime.GOOS == "windows" {
|
||||
extension = ".exe"
|
||||
}
|
||||
|
||||
// Move the provider binaries into a directory that we will point tofu
|
||||
// to using the -plugin-dir cli flag.
|
||||
platform := getproviders.CurrentPlatform.String()
|
||||
hashiDir := "cache/registry.opentofu.org/hashicorp/"
|
||||
providerCacheDir := filepath.Join(workdir, hashiDir, fmt.Sprintf("%s/0.0.1/", providerBinFileName), platform)
|
||||
if err := os.MkdirAll(providerCacheDir, os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
providerFinalBinaryFilePath := filepath.Join(workdir, hashiDir, fmt.Sprintf("%s/0.0.1/", providerBinFileName), platform, fmt.Sprintf("terraform-provider-%s", providerBinFileName)) + extension
|
||||
if err := os.Rename(providerTmpBinPath, providerFinalBinaryFilePath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
89
internal/command/e2etest/testdata/ephemeral-workflow/main.tf
vendored
Normal file
89
internal/command/e2etest/testdata/ephemeral-workflow/main.tf
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
// the provider-plugin tests uses the -plugin-cache flag so terraform pulls the
|
||||
// test binaries instead of reaching out to the registry.
|
||||
terraform {
|
||||
required_providers {
|
||||
simple = {
|
||||
source = "registry.opentofu.org/hashicorp/simple"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "simple" {
|
||||
alias = "s1"
|
||||
}
|
||||
|
||||
data "simple_resource" "test_data1" {
|
||||
provider = simple.s1
|
||||
value = "initial data value"
|
||||
}
|
||||
|
||||
ephemeral "simple_resource" "test_ephemeral" {
|
||||
count = 2
|
||||
provider = simple.s1
|
||||
// Having that "-with-renew" suffix, later when this value will be passed into "simple_resource.test_res.value_wo",
|
||||
// the plugin will delay the response on some requests to allow ephemeral Renew calls to be performed.
|
||||
value = "${data.simple_resource.test_data1.value}-with-renew"
|
||||
}
|
||||
|
||||
resource "simple_resource" "test_res" {
|
||||
provider = simple.s1
|
||||
value = "test value"
|
||||
// NOTE write-only arguments can reference ephemeral values.
|
||||
value_wo = ephemeral.simple_resource.test_ephemeral[0].value
|
||||
provisioner "local-exec" {
|
||||
command = "echo \"visible ${self.value}\""
|
||||
}
|
||||
provisioner "local-exec" {
|
||||
command = "echo \"not visible ${ephemeral.simple_resource.test_ephemeral[0].value}\""
|
||||
}
|
||||
// NOTE: value_wo cannot be used in a provisioner because it is returned as null by the provider so the interpolation fails
|
||||
}
|
||||
|
||||
data "simple_resource" "test_data2" {
|
||||
provider = simple.s1
|
||||
value = "test"
|
||||
lifecycle {
|
||||
precondition {
|
||||
// NOTE: precondition blocks can reference ephemeral values
|
||||
condition = ephemeral.simple_resource.test_ephemeral[0].value != null
|
||||
error_message = "test message"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locals{
|
||||
simple_provider_cfg = ephemeral.simple_resource.test_ephemeral[0].value
|
||||
}
|
||||
|
||||
provider "simple" {
|
||||
alias = "s2"
|
||||
// NOTE: Ensure that ephemeral values can be used to configure a provider.
|
||||
// This is needed in two cases: during plan/apply and also during destroy.
|
||||
// This test has been updated when DestroyEdgeTransformer was updated to
|
||||
// not create dependencies between ephemeral resources and the destroy nodes.
|
||||
// The "i_depend_on" field is just a simple configuration attribute of the provider
|
||||
// to allow creation of dependencies between a resources from a previously
|
||||
// initialized provider and the provider that is configured here.
|
||||
// The "i_depend_on" field is having no functionality behind, in the provider context,
|
||||
// but it's just a way for the "provider" block to create depedencies
|
||||
// to other blocks.
|
||||
i_depend_on = local.simple_provider_cfg
|
||||
}
|
||||
|
||||
resource "simple_resource" "test_res_second_provider" {
|
||||
provider = simple.s2
|
||||
value = "just a simple resource to ensure that the second provider it's working fine"
|
||||
}
|
||||
|
||||
module "call" {
|
||||
source = "./mod"
|
||||
in = ephemeral.simple_resource.test_ephemeral[0].value // NOTE: because variable "in" is marked as ephemeral, this should work as expected.
|
||||
}
|
||||
|
||||
output "out_ephemeral" {
|
||||
value = module.call.out2 // TODO: Because the output ephemeral marking is not done yet entirely, this is working now but remove this output once the marking of outputs are done completely.
|
||||
}
|
||||
|
||||
output "final_output" {
|
||||
value = simple_resource.test_res_second_provider.value
|
||||
}
|
||||
15
internal/command/e2etest/testdata/ephemeral-workflow/mod/main.tf
vendored
Normal file
15
internal/command/e2etest/testdata/ephemeral-workflow/mod/main.tf
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
variable "in" {
|
||||
type = string
|
||||
description = "Variable that is marked as ephemeral and doesn't matter what value is given in, ephemeral or not, the value evaluated for this variable will be marked as ephemeral"
|
||||
ephemeral = true
|
||||
}
|
||||
|
||||
output "out1" {
|
||||
value = var.in
|
||||
ephemeral = true // NOTE: because
|
||||
}
|
||||
|
||||
output "out2" {
|
||||
value = "rawvalue" // TODO ephemeral - this is returning a raw value and since incomplete work, the evaluated value is not marked as ephemeral. Once this will be fixed, the test should fail
|
||||
ephemeral = true
|
||||
}
|
||||
@@ -7,7 +7,6 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -87,7 +86,16 @@ func (c *ImportCommand) Run(args []string) int {
|
||||
}
|
||||
|
||||
if addr.Resource.Resource.Mode != addrs.ManagedResourceMode {
|
||||
diags = diags.Append(errors.New("A managed resource address is required. Importing into a data resource is not allowed."))
|
||||
var what string
|
||||
switch addr.Resource.Resource.Mode {
|
||||
case addrs.DataResourceMode:
|
||||
what = "a data resource"
|
||||
case addrs.EphemeralResourceMode:
|
||||
what = "an ephemeral resource"
|
||||
default:
|
||||
what = "a resource type"
|
||||
}
|
||||
diags = diags.Append(fmt.Errorf("A managed resource address is required. Importing into %s is not allowed.", what))
|
||||
c.showDiagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -883,7 +883,7 @@ func TestImportModuleInputVariableEvaluation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImport_dataResource(t *testing.T) {
|
||||
func TestImport_nonManagedResource(t *testing.T) {
|
||||
t.Chdir(testFixturePath("import-missing-resource-config"))
|
||||
|
||||
statePath := testTempFile(t)
|
||||
@@ -899,19 +899,36 @@ func TestImport_dataResource(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-state", statePath,
|
||||
"data.test_data_source.foo",
|
||||
"bar",
|
||||
}
|
||||
code := c.Run(args)
|
||||
if code != 1 {
|
||||
t.Fatalf("import succeeded; expected failure")
|
||||
cases := []struct {
|
||||
resAddr string
|
||||
expectedErrMsg string
|
||||
}{
|
||||
{
|
||||
resAddr: "data.test_data_source.foo",
|
||||
expectedErrMsg: "A managed resource address is required. Importing into a data resource is not allowed.",
|
||||
},
|
||||
{
|
||||
resAddr: "ephemeral.test_data_source.foo",
|
||||
expectedErrMsg: "A managed resource address is required. Importing into an ephemeral resource is not allowed.",
|
||||
},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.resAddr, func(t *testing.T) {
|
||||
args := []string{
|
||||
"-state", statePath,
|
||||
tt.resAddr,
|
||||
"bar",
|
||||
}
|
||||
code := c.Run(args)
|
||||
if code != 1 {
|
||||
t.Fatalf("import succeeded; expected failure")
|
||||
}
|
||||
|
||||
msg := ui.ErrorWriter.String()
|
||||
if want := `A managed resource address is required`; !strings.Contains(msg, want) {
|
||||
t.Errorf("incorrect message\nwant substring: %s\ngot:\n%s", want, msg)
|
||||
msg := ui.ErrorWriter.String()
|
||||
if want := tt.expectedErrMsg; !strings.Contains(msg, want) {
|
||||
t.Errorf("incorrect message\nwant substring: %s\ngot:\n%s", want, msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,16 @@ func TestMarshalCheckStates(t *testing.T) {
|
||||
outputBInstAddr := addrs.Checkable(addrs.OutputValue{Name: "b"}.Absolute(moduleChildAddr))
|
||||
checkBlockAAddr := addrs.ConfigCheckable(addrs.Check{Name: "a"}.InModule(addrs.RootModule))
|
||||
checkBlockAInstAddr := addrs.Checkable(addrs.Check{Name: "a"}.Absolute(addrs.RootModuleInstance))
|
||||
ephemeralAAddr := addrs.ConfigCheckable(addrs.Resource{
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Type: "test",
|
||||
Name: "a",
|
||||
}.InModule(addrs.RootModule))
|
||||
ephemeralAInstAddr := addrs.Checkable(addrs.Resource{
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Type: "test",
|
||||
Name: "a",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance))
|
||||
|
||||
tests := map[string]struct {
|
||||
Input *states.CheckResults
|
||||
@@ -108,6 +118,17 @@ func TestMarshalCheckStates(t *testing.T) {
|
||||
}),
|
||||
),
|
||||
}),
|
||||
addrs.MakeMapElem(ephemeralAAddr, &states.CheckResultAggregate{
|
||||
Status: checks.StatusFail,
|
||||
ObjectResults: addrs.MakeMap(
|
||||
addrs.MakeMapElem(ephemeralAInstAddr, &states.CheckResultObject{
|
||||
Status: checks.StatusFail,
|
||||
FailureMessages: []string{
|
||||
"foo",
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
},
|
||||
[]any{
|
||||
@@ -132,6 +153,29 @@ func TestMarshalCheckStates(t *testing.T) {
|
||||
},
|
||||
"status": "fail",
|
||||
},
|
||||
map[string]any{
|
||||
"address": map[string]any{
|
||||
"kind": "resource",
|
||||
"mode": "ephemeral",
|
||||
"name": "a",
|
||||
"to_display": "ephemeral.test.a",
|
||||
"type": "test",
|
||||
},
|
||||
"instances": []any{
|
||||
map[string]any{
|
||||
"address": map[string]any{
|
||||
"to_display": `ephemeral.test.a`,
|
||||
},
|
||||
"problems": []any{
|
||||
map[string]any{
|
||||
"message": "foo",
|
||||
},
|
||||
},
|
||||
"status": "fail",
|
||||
},
|
||||
},
|
||||
"status": "fail",
|
||||
},
|
||||
map[string]any{
|
||||
"address": map[string]any{
|
||||
"kind": "output_value",
|
||||
|
||||
@@ -31,6 +31,8 @@ func makeStaticObjectAddr(addr addrs.ConfigCheckable) staticObjectAddr {
|
||||
ret["mode"] = "managed"
|
||||
case addrs.DataResourceMode:
|
||||
ret["mode"] = "data"
|
||||
case addrs.EphemeralResourceMode:
|
||||
ret["mode"] = "ephemeral"
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported resource mode %#v", addr.Resource.Mode))
|
||||
}
|
||||
|
||||
@@ -341,8 +341,13 @@ func marshalModule(c *configs.Config, schemas *tofu.Schemas, addr string) (modul
|
||||
if err != nil {
|
||||
return module, err
|
||||
}
|
||||
ephemeralResources, err := marshalResources(c.Module.EphemeralResources, schemas, addr)
|
||||
if err != nil {
|
||||
return module, err
|
||||
}
|
||||
|
||||
rs = append(managedResources, dataResources...)
|
||||
rs = append(rs, ephemeralResources...)
|
||||
module.Resources = rs
|
||||
|
||||
outputs := make(map[string]output)
|
||||
@@ -520,6 +525,8 @@ func marshalResources(resources map[string]*configs.Resource, schemas *tofu.Sche
|
||||
r.Mode = "managed"
|
||||
case addrs.DataResourceMode:
|
||||
r.Mode = "data"
|
||||
case addrs.EphemeralResourceMode:
|
||||
r.Mode = "ephemeral"
|
||||
default:
|
||||
return rs, fmt.Errorf("resource %s has an unsupported mode %s", r.Address, v.Mode.String())
|
||||
}
|
||||
|
||||
@@ -10,11 +10,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/configs"
|
||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||
"github.com/opentofu/opentofu/internal/providers"
|
||||
"github.com/opentofu/opentofu/internal/tofu"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestFindSourceProviderConfig(t *testing.T) {
|
||||
@@ -114,6 +116,20 @@ func TestFindSourceProviderConfig(t *testing.T) {
|
||||
|
||||
func TestMarshalModule(t *testing.T) {
|
||||
emptySchemas := &tofu.Schemas{}
|
||||
providerAddr := addrs.NewProvider("host", "namespace", "type")
|
||||
resSchema := map[string]providers.Schema{
|
||||
"test_type": {
|
||||
Version: 0,
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := map[string]struct {
|
||||
Input *configs.Config
|
||||
@@ -249,6 +265,90 @@ func TestMarshalModule(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
"resources": {
|
||||
Input: &configs.Config{
|
||||
Module: &configs.Module{
|
||||
ManagedResources: map[string]*configs.Resource{
|
||||
"test_res": {
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Name: "test_res",
|
||||
Type: "test_type",
|
||||
Config: &hclsyntax.Body{
|
||||
Attributes: map[string]*hclsyntax.Attribute{},
|
||||
},
|
||||
Provider: providerAddr,
|
||||
},
|
||||
},
|
||||
DataResources: map[string]*configs.Resource{
|
||||
"test_data": {
|
||||
Mode: addrs.DataResourceMode,
|
||||
Name: "test_data",
|
||||
Type: "test_type",
|
||||
Config: &hclsyntax.Body{
|
||||
Attributes: map[string]*hclsyntax.Attribute{},
|
||||
},
|
||||
Provider: providerAddr,
|
||||
},
|
||||
},
|
||||
EphemeralResources: map[string]*configs.Resource{
|
||||
"test_ephemeral": {
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Name: "test_ephemeral",
|
||||
Type: "test_type",
|
||||
Config: &hclsyntax.Body{
|
||||
Attributes: map[string]*hclsyntax.Attribute{},
|
||||
},
|
||||
Provider: providerAddr,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Schemas: &tofu.Schemas{
|
||||
Providers: map[addrs.Provider]providers.ProviderSchema{
|
||||
providerAddr: {
|
||||
ResourceTypes: resSchema,
|
||||
EphemeralResources: resSchema,
|
||||
DataSources: resSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
Want: module{
|
||||
Outputs: map[string]output{},
|
||||
ModuleCalls: map[string]moduleCall{},
|
||||
Resources: []resource{
|
||||
{
|
||||
Address: "test_type.test_res",
|
||||
Mode: "managed",
|
||||
Type: "test_type",
|
||||
Name: "test_res",
|
||||
ProviderConfigKey: "test",
|
||||
SchemaVersion: ptrTo[uint64](0),
|
||||
Provisioners: nil,
|
||||
Expressions: make(map[string]any),
|
||||
},
|
||||
{
|
||||
Address: "data.test_type.test_data",
|
||||
Mode: "data",
|
||||
Type: "test_type",
|
||||
Name: "test_data",
|
||||
ProviderConfigKey: "test",
|
||||
SchemaVersion: ptrTo[uint64](0),
|
||||
Provisioners: nil,
|
||||
Expressions: make(map[string]any),
|
||||
},
|
||||
{
|
||||
Address: "ephemeral.test_type.test_ephemeral",
|
||||
Mode: "ephemeral",
|
||||
Type: "test_type",
|
||||
Name: "test_ephemeral",
|
||||
ProviderConfigKey: "test",
|
||||
SchemaVersion: ptrTo[uint64](0),
|
||||
Provisioners: nil,
|
||||
Expressions: make(map[string]any),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// TODO: More test cases covering things other than input variables.
|
||||
// (For now the other details are mainly tested in package command,
|
||||
// as part of the tests for "tofu show".)
|
||||
@@ -275,3 +375,12 @@ func TestMarshalModule(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ptrTo is a helper to compensate for the fact that Go doesn't allow
|
||||
// using the '&' operator unless the operand is directly addressable.
|
||||
//
|
||||
// Instead then, this function returns a pointer to a copy of the given
|
||||
// value.
|
||||
func ptrTo[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ const (
|
||||
ActionDelete ChangeAction = "delete"
|
||||
ActionImport ChangeAction = "import"
|
||||
ActionForget ChangeAction = "remove"
|
||||
ActionOpen ChangeAction = "open"
|
||||
)
|
||||
|
||||
func ParseChangeAction(action plans.Action) ChangeAction {
|
||||
@@ -91,6 +92,9 @@ func ParseChangeAction(action plans.Action) ChangeAction {
|
||||
return ActionDelete
|
||||
case plans.Forget:
|
||||
return ActionForget
|
||||
case plans.Open:
|
||||
return ActionOpen
|
||||
// NOTE: Renew and Close missing on purpose since those are not meant to be stored
|
||||
default:
|
||||
return ActionNoOp
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ func (plan Plan) getSchema(change jsonplan.ResourceChange) *jsonprovider.Schema
|
||||
return plan.ProviderSchemas[change.ProviderName].ResourceSchemas[change.Type]
|
||||
case jsonstate.DataResourceMode:
|
||||
return plan.ProviderSchemas[change.ProviderName].DataSourceSchemas[change.Type]
|
||||
case jsonstate.EphemeralResourceMode:
|
||||
return plan.ProviderSchemas[change.ProviderName].EphemeralResourceSchemas[change.Type]
|
||||
default:
|
||||
panic("found unrecognized resource mode: " + change.Mode)
|
||||
}
|
||||
@@ -76,6 +78,10 @@ func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...plans.Q
|
||||
// Don't render anything for deleted data sources.
|
||||
continue
|
||||
}
|
||||
if diff.change.Mode == jsonstate.EphemeralResourceMode {
|
||||
// Do not render ephemeral changes. // TODO ephemeral add e2e test for this
|
||||
continue
|
||||
}
|
||||
|
||||
changes = append(changes, diff)
|
||||
|
||||
@@ -361,6 +367,10 @@ func renderHumanDiffDrift(renderer Renderer, diffs diffs, mode plans.Mode) bool
|
||||
}
|
||||
|
||||
func renderHumanDiff(renderer Renderer, diff diff, cause string) (string, bool) {
|
||||
if diff.change.Mode == jsonstate.EphemeralResourceMode {
|
||||
// render nothing for ephemeral resources
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Internally, our computed diffs can't tell the difference between a
|
||||
// replace action (eg. CreateThenDestroy, DestroyThenCreate) and a simple
|
||||
@@ -569,6 +579,8 @@ func actionDescription(action plans.Action) string {
|
||||
return " [cyan]<=[reset] read (data resources)"
|
||||
case plans.Forget:
|
||||
return " [red].[reset] forget"
|
||||
case plans.Open:
|
||||
panic("ephemeral changes are not meant to be printed")
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unrecognized change type: %s", action.String()))
|
||||
|
||||
@@ -1069,6 +1069,22 @@ new line`),
|
||||
# (2 unchanged attributes hidden)
|
||||
}`,
|
||||
},
|
||||
"open ephemeral": {
|
||||
Action: plans.Open,
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Before: cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("name"),
|
||||
}),
|
||||
After: cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("name"),
|
||||
}),
|
||||
Schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"name": {Type: cty.String, Optional: true},
|
||||
},
|
||||
},
|
||||
ExpectedOutput: ``,
|
||||
},
|
||||
}
|
||||
|
||||
runTestCases(t, testCases)
|
||||
@@ -1685,6 +1701,26 @@ func TestResourceChange_JSON(t *testing.T) {
|
||||
)
|
||||
}`,
|
||||
},
|
||||
"ephemeral resource creation": {
|
||||
Action: plans.Create,
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Before: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("ad4d04ed-ac40-43fc-ad4f-d2fc89b80793"),
|
||||
"json_field": cty.StringVal(`{"secret_value": "8f6fb348-949d-4fa3-98a4-da9e66088257"}`),
|
||||
}),
|
||||
After: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("ad4d04ed-ac40-43fc-ad4f-d2fc89b80793"),
|
||||
"json_field": cty.StringVal(`{"secret_value": "f8b90277-7b0b-4f15-9c31-aed5a642b274"}`),
|
||||
}),
|
||||
Schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||
"json_field": {Type: cty.String, Optional: true},
|
||||
},
|
||||
},
|
||||
RequiredReplace: cty.NewPathSet(),
|
||||
ExpectedOutput: ``,
|
||||
},
|
||||
}
|
||||
runTestCases(t, testCases)
|
||||
}
|
||||
@@ -2290,6 +2326,74 @@ func TestResourceChange_primitiveSet(t *testing.T) {
|
||||
# (1 unchanged attribute hidden)
|
||||
}`,
|
||||
},
|
||||
"fails when ephemeral in the after marks": {
|
||||
Action: plans.Update,
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Before: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||
"ami": cty.StringVal("ami-STATIC"),
|
||||
"set_field": cty.NullVal(cty.Set(cty.String)),
|
||||
}),
|
||||
After: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"ami": cty.StringVal("ami-STATIC"),
|
||||
"set_field": cty.SetVal([]cty.Value{
|
||||
cty.StringVal("new-element"),
|
||||
}),
|
||||
}),
|
||||
AfterValMarks: []cty.PathValueMarks{
|
||||
{
|
||||
Path: cty.GetAttrPath("set_field").IndexInt(0),
|
||||
Marks: map[interface{}]struct{}{
|
||||
marks.Ephemeral: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
Schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||
"ami": {Type: cty.String, Optional: true},
|
||||
"set_field": {Type: cty.Set(cty.String), Optional: true},
|
||||
},
|
||||
},
|
||||
RequiredReplace: cty.NewPathSet(),
|
||||
ExpectedOutput: ``,
|
||||
ExpectedErr: fmt.Errorf("test_instance.example: ephemeral marks found at the following paths:\n.set_field[0]"),
|
||||
},
|
||||
"fails when ephemeral in the before marks": {
|
||||
Action: plans.Update,
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Before: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||
"ami": cty.StringVal("ami-STATIC"),
|
||||
"set_field": cty.NullVal(cty.Set(cty.String)),
|
||||
}),
|
||||
After: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"ami": cty.StringVal("ami-STATIC"),
|
||||
"set_field": cty.SetVal([]cty.Value{
|
||||
cty.StringVal("new-element"),
|
||||
}),
|
||||
}),
|
||||
BeforeValMarks: []cty.PathValueMarks{
|
||||
{
|
||||
Path: cty.GetAttrPath("set_field").IndexInt(0),
|
||||
Marks: map[interface{}]struct{}{
|
||||
marks.Ephemeral: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
Schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||
"ami": {Type: cty.String, Optional: true},
|
||||
"set_field": {Type: cty.Set(cty.String), Optional: true},
|
||||
},
|
||||
},
|
||||
RequiredReplace: cty.NewPathSet(),
|
||||
ExpectedOutput: ``,
|
||||
ExpectedErr: fmt.Errorf("test_instance.example: ephemeral marks found at the following paths:\n.set_field[0]"),
|
||||
},
|
||||
"in-place update - first insertion": {
|
||||
Action: plans.Update,
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
@@ -7172,6 +7276,7 @@ type testCase struct {
|
||||
RequiredReplace cty.PathSet
|
||||
ExpectedOutput string
|
||||
PrevRunAddr addrs.AbsResourceInstance
|
||||
ExpectedErr error
|
||||
}
|
||||
|
||||
func runTestCases(t *testing.T, testCases map[string]testCase) {
|
||||
@@ -7253,23 +7358,41 @@ func runTestCases(t *testing.T, testCases map[string]testCase) {
|
||||
Block: tc.Schema,
|
||||
},
|
||||
},
|
||||
EphemeralResources: map[string]providers.Schema{
|
||||
src.Addr.Resource.Resource.Type: {
|
||||
Block: tc.Schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
jsonchanges, err := jsonplan.MarshalResourceChanges([]*plans.ResourceInstanceChangeSrc{src}, tfschemas)
|
||||
if err != nil {
|
||||
t.Errorf("failed to marshal resource changes: %s", err.Error())
|
||||
return
|
||||
if tc.ExpectedErr == nil {
|
||||
t.Errorf("failed to marshal resource changes.\ngot err:\n%s\nbut no expected err", err)
|
||||
} else {
|
||||
gotErr := err.Error()
|
||||
wantErr := tc.ExpectedErr.Error()
|
||||
if gotErr != wantErr {
|
||||
t.Errorf("failed to marshal resource changes.\ngot err:\n%s\nexpected err:\n%s", gotErr, wantErr)
|
||||
}
|
||||
}
|
||||
} else if tc.ExpectedErr != nil {
|
||||
t.Errorf("failed to marshal resource changes.\nwant err:\n%s\nbut got none", tc.ExpectedErr)
|
||||
}
|
||||
|
||||
jsonschemas := jsonprovider.MarshalForRenderer(tfschemas)
|
||||
change := structured.FromJsonChange(jsonchanges[0].Change, attribute_path.AlwaysMatcher())
|
||||
renderer := Renderer{Colorize: color}
|
||||
diff := diff{
|
||||
change: jsonchanges[0],
|
||||
diff: differ.ComputeDiffForBlock(change, jsonschemas[jsonchanges[0].ProviderName].ResourceSchemas[jsonchanges[0].Type].Block),
|
||||
|
||||
var output string
|
||||
if len(jsonchanges) > 0 {
|
||||
change := structured.FromJsonChange(jsonchanges[0].Change, attribute_path.AlwaysMatcher())
|
||||
renderer := Renderer{Colorize: color}
|
||||
diff := diff{
|
||||
change: jsonchanges[0],
|
||||
diff: differ.ComputeDiffForBlock(change, jsonschemas[jsonchanges[0].ProviderName].ResourceSchemas[jsonchanges[0].Type].Block),
|
||||
}
|
||||
output, _ = renderHumanDiff(renderer, diff, proposedChange)
|
||||
}
|
||||
output, _ := renderHumanDiff(renderer, diff, proposedChange)
|
||||
if diff := cmp.Diff(output, tc.ExpectedOutput); diff != "" {
|
||||
t.Errorf("wrong output\nexpected:\n%s\nactual:\n%s\ndiff:\n%s\n", tc.ExpectedOutput, output, diff)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package jsonformat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
@@ -36,6 +37,8 @@ func (state State) GetSchema(resource jsonstate.Resource) *jsonprovider.Schema {
|
||||
return state.ProviderSchemas[resource.ProviderName].ResourceSchemas[resource.Type]
|
||||
case jsonstate.DataResourceMode:
|
||||
return state.ProviderSchemas[resource.ProviderName].DataSourceSchemas[resource.Type]
|
||||
case jsonstate.EphemeralResourceMode:
|
||||
panic(fmt.Errorf("ephemeral resources are not meant to be stored in the state file but schema for ephemeral %s.%s has been requested", resource.Type, resource.Name))
|
||||
default:
|
||||
panic("found unrecognized resource mode: " + resource.Mode)
|
||||
}
|
||||
@@ -74,6 +77,8 @@ func (state State) renderHumanStateModule(renderer Renderer, module jsonstate.Mo
|
||||
case jsonstate.DataResourceMode:
|
||||
change := structured.FromJsonResource(resource)
|
||||
renderer.Streams.Printf("data %q %q %s", resource.Type, resource.Name, differ.ComputeDiffForBlock(change, schema.Block).RenderHuman(0, opts))
|
||||
case jsonstate.EphemeralResourceMode:
|
||||
panic(fmt.Errorf("ephemeral resource %s %s not allowed to be stored in the state", resource.Type, resource.Name))
|
||||
default:
|
||||
panic("found unrecognized resource mode: " + resource.Mode)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
|
||||
@@ -396,6 +397,16 @@ func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schema
|
||||
r.PreviousAddress = rc.PrevRunAddr.String()
|
||||
}
|
||||
|
||||
if addr.Resource.Resource.Mode == addrs.EphemeralResourceMode {
|
||||
// We need to write ephemeral resources to the plan file to be able to build
|
||||
// the apply graph on `tofu apply <planfile>`.
|
||||
// The DiffTransformer needs the changes from the plan to be able to generate
|
||||
// executable resource instance graph nodes, so we are adding the ephemeral resources too.
|
||||
// Even though we are writing these, the actual values of the ephemeral *must not*
|
||||
// be written to the plan so nullify these.
|
||||
rc.ChangeSrc.Before = nil
|
||||
rc.ChangeSrc.After = nil
|
||||
}
|
||||
dataSource := addr.Resource.Resource.Mode == addrs.DataResourceMode
|
||||
// We create "delete" actions for data resources so we can clean up
|
||||
// their entries in state, but this is an implementation detail that
|
||||
@@ -431,11 +442,14 @@ func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schema
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
marks := rc.BeforeValMarks
|
||||
if schema.ContainsSensitive() {
|
||||
marks = append(marks, schema.ValueMarks(changeV.Before, nil)...)
|
||||
valMarks := rc.BeforeValMarks
|
||||
if schema.ContainsMarks() {
|
||||
valMarks = append(valMarks, schema.ValueMarks(changeV.Before, nil)...)
|
||||
}
|
||||
bs := jsonstate.SensitiveAsBoolWithPathValueMarks(changeV.Before, marks)
|
||||
if err := ensureEphemeralMarksAreValid(addr, valMarks); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bs := jsonstate.SensitiveAsBoolWithPathValueMarks(changeV.Before, valMarks)
|
||||
beforeSensitive, err = ctyjson.Marshal(bs, bs.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -460,11 +474,14 @@ func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schema
|
||||
}
|
||||
afterUnknown = unknownAsBool(changeV.After)
|
||||
}
|
||||
marks := rc.AfterValMarks
|
||||
if schema.ContainsSensitive() {
|
||||
marks = append(marks, schema.ValueMarks(changeV.After, nil)...)
|
||||
valMarks := rc.AfterValMarks
|
||||
if schema.ContainsMarks() {
|
||||
valMarks = append(valMarks, schema.ValueMarks(changeV.After, nil)...)
|
||||
}
|
||||
as := jsonstate.SensitiveAsBoolWithPathValueMarks(changeV.After, marks)
|
||||
if err := ensureEphemeralMarksAreValid(addr, valMarks); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
as := jsonstate.SensitiveAsBoolWithPathValueMarks(changeV.After, valMarks)
|
||||
afterSensitive, err = ctyjson.Marshal(as, as.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -514,6 +531,8 @@ func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schema
|
||||
r.Mode = jsonstate.ManagedResourceMode
|
||||
case addrs.DataResourceMode:
|
||||
r.Mode = jsonstate.DataResourceMode
|
||||
case addrs.EphemeralResourceMode:
|
||||
r.Mode = jsonstate.EphemeralResourceMode
|
||||
default:
|
||||
return nil, fmt.Errorf("resource %s has an unsupported mode %s", r.Address, addr.Resource.Resource.Mode.String())
|
||||
}
|
||||
@@ -562,6 +581,18 @@ func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schema
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func ensureEphemeralMarksAreValid(addr addrs.AbsResourceInstance, valMarks []cty.PathValueMarks) error {
|
||||
// ephemeral resources will have the ephemeral mark at the root of the value, got from schema.ValueMarks
|
||||
// so we don't want to error for those particular ones
|
||||
if addr.Resource.Resource.Mode == addrs.EphemeralResourceMode {
|
||||
return nil
|
||||
}
|
||||
if err := marks.EnsureNoEphemeralMarks(valMarks); err != nil {
|
||||
return fmt.Errorf("%s: %w", addr, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateChange is used to receive two values and calculate the difference
|
||||
// between them in order to return a Change struct
|
||||
func GenerateChange(beforeVal, afterVal cty.Value) (*Change, error) {
|
||||
@@ -868,6 +899,8 @@ func actionString(action string) []string {
|
||||
return []string{"delete", "create"}
|
||||
case "Forget":
|
||||
return []string{"forget"}
|
||||
case "Open":
|
||||
return []string{"open"}
|
||||
default:
|
||||
return []string{action}
|
||||
}
|
||||
@@ -899,6 +932,8 @@ func UnmarshalActions(actions []string) plans.Action {
|
||||
return plans.NoOp
|
||||
case "forget":
|
||||
return plans.Forget
|
||||
case "open":
|
||||
return plans.Open
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -205,6 +205,8 @@ func marshalPlanResources(changeMap map[string]*plans.ResourceInstanceChangeSrc,
|
||||
resource.Mode = "managed"
|
||||
case addrs.DataResourceMode:
|
||||
resource.Mode = "data"
|
||||
case addrs.EphemeralResourceMode:
|
||||
resource.Mode = "ephemeral"
|
||||
default:
|
||||
return nil, fmt.Errorf("resource %s has an unsupported mode %s",
|
||||
r.Addr.String(),
|
||||
|
||||
@@ -24,10 +24,11 @@ type Providers struct {
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
Provider *Schema `json:"provider,omitempty"`
|
||||
ResourceSchemas map[string]*Schema `json:"resource_schemas,omitempty"`
|
||||
DataSourceSchemas map[string]*Schema `json:"data_source_schemas,omitempty"`
|
||||
Functions map[string]*Function `json:"functions,omitempty"`
|
||||
Provider *Schema `json:"provider,omitempty"`
|
||||
ResourceSchemas map[string]*Schema `json:"resource_schemas,omitempty"`
|
||||
DataSourceSchemas map[string]*Schema `json:"data_source_schemas,omitempty"`
|
||||
EphemeralResourceSchemas map[string]*Schema `json:"ephemeral_resource_schemas,omitempty"`
|
||||
Functions map[string]*Function `json:"functions,omitempty"`
|
||||
}
|
||||
|
||||
func newProviders() *Providers {
|
||||
@@ -59,9 +60,10 @@ func Marshal(s *tofu.Schemas) ([]byte, error) {
|
||||
|
||||
func marshalProvider(tps providers.ProviderSchema) *Provider {
|
||||
return &Provider{
|
||||
Provider: marshalSchema(tps.Provider),
|
||||
ResourceSchemas: marshalSchemas(tps.ResourceTypes),
|
||||
DataSourceSchemas: marshalSchemas(tps.DataSources),
|
||||
Functions: marshalFunctions(tps.Functions),
|
||||
Provider: marshalSchema(tps.Provider),
|
||||
ResourceSchemas: marshalSchemas(tps.ResourceTypes),
|
||||
DataSourceSchemas: marshalSchemas(tps.DataSources),
|
||||
EphemeralResourceSchemas: marshalSchemas(tps.EphemeralResources),
|
||||
Functions: marshalFunctions(tps.Functions),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,11 @@ func TestMarshalProvider(t *testing.T) {
|
||||
{
|
||||
providers.ProviderSchema{},
|
||||
&Provider{
|
||||
Provider: &Schema{},
|
||||
ResourceSchemas: map[string]*Schema{},
|
||||
DataSourceSchemas: map[string]*Schema{},
|
||||
Functions: map[string]*Function{},
|
||||
Provider: &Schema{},
|
||||
ResourceSchemas: map[string]*Schema{},
|
||||
DataSourceSchemas: map[string]*Schema{},
|
||||
EphemeralResourceSchemas: map[string]*Schema{},
|
||||
Functions: map[string]*Function{},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -147,6 +148,47 @@ func TestMarshalProvider(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
EphemeralResourceSchemas: map[string]*Schema{
|
||||
"test_ephemeral_resource": {
|
||||
Version: 4,
|
||||
Block: &Block{
|
||||
Attributes: map[string]*Attribute{
|
||||
"id": {
|
||||
AttributeType: json.RawMessage(`"string"`),
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
DescriptionKind: "plain",
|
||||
},
|
||||
"secret": {
|
||||
AttributeType: json.RawMessage(`"string"`),
|
||||
Optional: true,
|
||||
DescriptionKind: "plain",
|
||||
},
|
||||
},
|
||||
BlockTypes: map[string]*BlockType{
|
||||
"notes": {
|
||||
Block: &Block{
|
||||
Attributes: map[string]*Attribute{
|
||||
"secret1": {
|
||||
AttributeType: json.RawMessage(`"string"`),
|
||||
Optional: true,
|
||||
DescriptionKind: "plain",
|
||||
},
|
||||
"secret2": {
|
||||
AttributeType: json.RawMessage(`"string"`),
|
||||
Optional: true,
|
||||
DescriptionKind: "plain",
|
||||
},
|
||||
},
|
||||
DescriptionKind: "plain",
|
||||
},
|
||||
NestingMode: "list",
|
||||
},
|
||||
},
|
||||
DescriptionKind: "plain",
|
||||
},
|
||||
},
|
||||
},
|
||||
Functions: map[string]*Function{},
|
||||
},
|
||||
},
|
||||
@@ -225,6 +267,28 @@ func testProvider() providers.ProviderSchema {
|
||||
},
|
||||
},
|
||||
},
|
||||
EphemeralResources: map[string]providers.Schema{
|
||||
"test_ephemeral_resource": {
|
||||
Version: 4,
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||
"secret": {Type: cty.String, Optional: true},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"notes": {
|
||||
Nesting: configschema.NestingList,
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"secret1": {Type: cty.String, Optional: true},
|
||||
"secret2": {Type: cty.String, Optional: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Functions: map[string]providers.FunctionSpec{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ const (
|
||||
// consuming parser.
|
||||
FormatVersion = "1.0"
|
||||
|
||||
ManagedResourceMode = "managed"
|
||||
DataResourceMode = "data"
|
||||
ManagedResourceMode = "managed"
|
||||
DataResourceMode = "data"
|
||||
EphemeralResourceMode = "ephemeral"
|
||||
)
|
||||
|
||||
// State is the top-level representation of the json format of a tofu
|
||||
@@ -365,8 +366,9 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module
|
||||
|
||||
resAddr := r.Addr.Resource
|
||||
|
||||
instAddr := r.Addr.Instance(k)
|
||||
current := Resource{
|
||||
Address: r.Addr.Instance(k).String(),
|
||||
Address: instAddr.String(),
|
||||
Type: resAddr.Type,
|
||||
Name: resAddr.Name,
|
||||
ProviderName: r.ProviderConfig.Provider.String(),
|
||||
@@ -384,6 +386,8 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module
|
||||
current.Mode = ManagedResourceMode
|
||||
case addrs.DataResourceMode:
|
||||
current.Mode = DataResourceMode
|
||||
case addrs.EphemeralResourceMode:
|
||||
return ret, fmt.Errorf("ephemeral resource %q detected in the current state. This is an error in OpenTofu", resAddr.String())
|
||||
default:
|
||||
return ret, fmt.Errorf("resource %s has an unsupported mode %s",
|
||||
resAddr.String(),
|
||||
@@ -415,11 +419,18 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module
|
||||
|
||||
current.AttributeValues = marshalAttributeValues(riObj.Value)
|
||||
|
||||
value, marks := riObj.Value.UnmarkDeepWithPaths()
|
||||
if schema.ContainsSensitive() {
|
||||
marks = append(marks, schema.ValueMarks(value, nil)...)
|
||||
value, valMarks := riObj.Value.UnmarkDeepWithPaths()
|
||||
if schema.ContainsMarks() {
|
||||
valMarks = append(valMarks, schema.ValueMarks(value, nil)...)
|
||||
}
|
||||
s := SensitiveAsBoolWithPathValueMarks(value, marks)
|
||||
// NOTE: Even though at this point, the resources that are processed here
|
||||
// should have no ephemeral mark, we want to validate that before having
|
||||
// these written to the state.
|
||||
if err := marks.EnsureNoEphemeralMarks(valMarks); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", instAddr, err)
|
||||
}
|
||||
|
||||
s := SensitiveAsBoolWithPathValueMarks(value, valMarks)
|
||||
v, err := ctyjson.Marshal(s, s.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -466,11 +477,17 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module
|
||||
|
||||
deposed.AttributeValues = marshalAttributeValues(riObj.Value)
|
||||
|
||||
value, marks := riObj.Value.UnmarkDeepWithPaths()
|
||||
if schema.ContainsSensitive() {
|
||||
marks = append(marks, schema.ValueMarks(value, nil)...)
|
||||
value, valMarks := riObj.Value.UnmarkDeepWithPaths()
|
||||
if schema.ContainsMarks() {
|
||||
valMarks = append(valMarks, schema.ValueMarks(value, nil)...)
|
||||
}
|
||||
s := SensitiveAsBool(value.MarkWithPaths(marks))
|
||||
// NOTE: Even though at this point, the resources that are processed here
|
||||
// should have no ephemeral mark, we want to validate that before having
|
||||
// these written to the state.
|
||||
if err := marks.EnsureNoEphemeralMarks(valMarks); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", instAddr, err)
|
||||
}
|
||||
s := SensitiveAsBool(value.MarkWithPaths(valMarks))
|
||||
v, err := ctyjson.Marshal(s, s.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -8,6 +8,7 @@ package jsonstate
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -203,13 +204,13 @@ func TestMarshalResources(t *testing.T) {
|
||||
Resources map[string]*states.Resource
|
||||
Schemas *tofu.Schemas
|
||||
Want []Resource
|
||||
Err bool
|
||||
ErrMsg string
|
||||
}{
|
||||
"nil": {
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
"",
|
||||
},
|
||||
"single resource": {
|
||||
map[string]*states.Resource{
|
||||
@@ -251,7 +252,49 @@ func TestMarshalResources(t *testing.T) {
|
||||
SensitiveValues: json.RawMessage("{\"foozles\":true}"),
|
||||
},
|
||||
},
|
||||
false,
|
||||
"",
|
||||
},
|
||||
"single data source": {
|
||||
map[string]*states.Resource{
|
||||
"test_thing.baz": {
|
||||
Addr: addrs.AbsResource{
|
||||
Resource: addrs.Resource{
|
||||
Mode: addrs.DataResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
Instances: map[addrs.InstanceKey]*states.ResourceInstance{
|
||||
addrs.NoKey: {
|
||||
Current: &states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{"foo":"baz"}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
ProviderConfig: addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
},
|
||||
},
|
||||
testSchemas(),
|
||||
[]Resource{
|
||||
{
|
||||
Address: "data.test_thing.bar",
|
||||
Mode: "data",
|
||||
Type: "test_thing",
|
||||
Name: "bar",
|
||||
Index: nil,
|
||||
ProviderName: "registry.opentofu.org/hashicorp/test",
|
||||
AttributeValues: AttributeValues{
|
||||
"foo": json.RawMessage(`"baz"`),
|
||||
"bar": json.RawMessage(`null`),
|
||||
},
|
||||
SensitiveValues: json.RawMessage("{\"bar\":true}"),
|
||||
},
|
||||
},
|
||||
"",
|
||||
},
|
||||
"single resource_with_sensitive": {
|
||||
map[string]*states.Resource{
|
||||
@@ -293,9 +336,9 @@ func TestMarshalResources(t *testing.T) {
|
||||
SensitiveValues: json.RawMessage("{\"foozles\":true}"),
|
||||
},
|
||||
},
|
||||
false,
|
||||
"",
|
||||
},
|
||||
"resource with marks": {
|
||||
"resource with sensitive marks": {
|
||||
map[string]*states.Resource{
|
||||
"test_thing.bar": {
|
||||
Addr: addrs.AbsResource{
|
||||
@@ -339,7 +382,39 @@ func TestMarshalResources(t *testing.T) {
|
||||
SensitiveValues: json.RawMessage(`{"foozles":true}`),
|
||||
},
|
||||
},
|
||||
false,
|
||||
"",
|
||||
},
|
||||
"resource with ephemeral": {
|
||||
map[string]*states.Resource{
|
||||
"test_thing.bar": {
|
||||
Addr: addrs.AbsResource{
|
||||
Resource: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
Instances: map[addrs.InstanceKey]*states.ResourceInstance{
|
||||
addrs.NoKey: {
|
||||
Current: &states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{"foozles":"confuzles"}`),
|
||||
AttrSensitivePaths: []cty.PathValueMarks{{
|
||||
Path: cty.Path{cty.GetAttrStep{Name: "foozles"}},
|
||||
Marks: cty.NewValueMarks(marks.Ephemeral)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ProviderConfig: addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
},
|
||||
},
|
||||
testSchemas(),
|
||||
nil,
|
||||
"test_thing.bar: ephemeral marks found at the following paths:\n.foozles",
|
||||
},
|
||||
"single resource wrong schema": {
|
||||
map[string]*states.Resource{
|
||||
@@ -368,7 +443,7 @@ func TestMarshalResources(t *testing.T) {
|
||||
},
|
||||
testSchemas(),
|
||||
nil,
|
||||
true,
|
||||
"schema version 1 for test_thing.bar in state does not match version 0 from the provider",
|
||||
},
|
||||
"resource with count": {
|
||||
map[string]*states.Resource{
|
||||
@@ -410,7 +485,7 @@ func TestMarshalResources(t *testing.T) {
|
||||
SensitiveValues: json.RawMessage("{\"foozles\":true}"),
|
||||
},
|
||||
},
|
||||
false,
|
||||
"",
|
||||
},
|
||||
"resource with for_each": {
|
||||
map[string]*states.Resource{
|
||||
@@ -452,7 +527,7 @@ func TestMarshalResources(t *testing.T) {
|
||||
SensitiveValues: json.RawMessage("{\"foozles\":true}"),
|
||||
},
|
||||
},
|
||||
false,
|
||||
"",
|
||||
},
|
||||
"deposed resource": {
|
||||
map[string]*states.Resource{
|
||||
@@ -497,7 +572,7 @@ func TestMarshalResources(t *testing.T) {
|
||||
SensitiveValues: json.RawMessage("{\"foozles\":true}"),
|
||||
},
|
||||
},
|
||||
false,
|
||||
"",
|
||||
},
|
||||
"deposed and current resource": {
|
||||
map[string]*states.Resource{
|
||||
@@ -559,7 +634,7 @@ func TestMarshalResources(t *testing.T) {
|
||||
SensitiveValues: json.RawMessage("{\"foozles\":true}"),
|
||||
},
|
||||
},
|
||||
false,
|
||||
"",
|
||||
},
|
||||
"resource with marked map attr": {
|
||||
map[string]*states.Resource{
|
||||
@@ -604,17 +679,48 @@ func TestMarshalResources(t *testing.T) {
|
||||
SensitiveValues: json.RawMessage(`{"data":true}`),
|
||||
},
|
||||
},
|
||||
false,
|
||||
``,
|
||||
},
|
||||
"single ephemeral resource": {
|
||||
map[string]*states.Resource{
|
||||
"test_thing.baz": {
|
||||
Addr: addrs.AbsResource{
|
||||
Resource: addrs.Resource{
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
Instances: map[addrs.InstanceKey]*states.ResourceInstance{
|
||||
addrs.NoKey: {
|
||||
Current: &states.ResourceInstanceObjectSrc{
|
||||
Status: states.ObjectReady,
|
||||
AttrsJSON: []byte(`{"foo":"baz"}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
ProviderConfig: addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
},
|
||||
},
|
||||
testSchemas(),
|
||||
nil,
|
||||
`ephemeral resource "ephemeral.test_thing.bar" detected in the current state. This is an error in OpenTofu`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := marshalResources(test.Resources, addrs.RootModuleInstance, test.Schemas)
|
||||
if test.Err {
|
||||
if test.ErrMsg != "" {
|
||||
if err == nil {
|
||||
t.Fatal("succeeded; want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), test.ErrMsg) {
|
||||
t.Fatalf("expected msg %q in error %q", test.ErrMsg, err.Error())
|
||||
}
|
||||
return
|
||||
} else if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
@@ -862,6 +968,26 @@ func testSchemas() *tofu.Schemas {
|
||||
},
|
||||
},
|
||||
},
|
||||
DataSources: map[string]providers.Schema{
|
||||
"test_thing": {
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"foo": {Type: cty.String, Optional: true, Computed: true},
|
||||
"bar": {Type: cty.String, Optional: true, Sensitive: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
EphemeralResources: map[string]providers.Schema{
|
||||
"test_thing": {
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"foo": {Type: cty.String, Optional: true, Computed: true},
|
||||
"bar": {Type: cty.String, Optional: true, Sensitive: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -493,8 +493,19 @@ func (c *StateMvCommand) sourceObjectAddrs(state *states.State, matched addrs.Ta
|
||||
|
||||
func (c *StateMvCommand) validateResourceMove(addrFrom, addrTo addrs.AbsResource) tfdiags.Diagnostics {
|
||||
const msgInvalidRequest = "Invalid state move request"
|
||||
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
if addrFrom.Resource.Mode == addrs.EphemeralResourceMode || addrTo.Resource.Mode == addrs.EphemeralResourceMode {
|
||||
diags = diags.Append(
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
msgInvalidRequest,
|
||||
"Ephemeral resources cannot be used as sources or targets for the move action. Just update your configuration accordingly.",
|
||||
),
|
||||
)
|
||||
return diags
|
||||
}
|
||||
|
||||
if addrFrom.Resource.Mode != addrTo.Resource.Mode {
|
||||
switch addrFrom.Resource.Mode {
|
||||
case addrs.ManagedResourceMode:
|
||||
@@ -509,6 +520,7 @@ func (c *StateMvCommand) validateResourceMove(addrFrom, addrTo addrs.AbsResource
|
||||
msgInvalidRequest,
|
||||
fmt.Sprintf("Cannot move %s to %s: a data resource can be moved only to another data resource address.", addrFrom, addrTo),
|
||||
))
|
||||
// NOTE: No need for the ephemeral resource in this switch block since it is handled at the top of the method.
|
||||
default:
|
||||
// In case a new mode is added in future, this unhelpful error is better than nothing.
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/states"
|
||||
@@ -1819,6 +1820,107 @@ func TestStateMv_checkRequiredVersion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateResourceMove(t *testing.T) {
|
||||
var (
|
||||
c = &StateMvCommand{}
|
||||
|
||||
managedRes = addrs.AbsResource{Resource: addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_type", Name: "test_name"}}
|
||||
dataRes = addrs.AbsResource{Resource: addrs.Resource{Mode: addrs.DataResourceMode, Type: "test_type", Name: "test_name"}}
|
||||
ephemeralRes = addrs.AbsResource{Resource: addrs.Resource{Mode: addrs.EphemeralResourceMode, Type: "test_type", Name: "test_name"}}
|
||||
)
|
||||
|
||||
tests := map[string]struct {
|
||||
src, target addrs.AbsResource
|
||||
wantDiags tfdiags.Diagnostics
|
||||
}{
|
||||
"resource to resource": {
|
||||
managedRes,
|
||||
managedRes,
|
||||
tfdiags.Diagnostics{},
|
||||
},
|
||||
"data to data": {
|
||||
dataRes,
|
||||
dataRes,
|
||||
tfdiags.Diagnostics{},
|
||||
},
|
||||
"ephemeral to ephemeral": {
|
||||
ephemeralRes,
|
||||
ephemeralRes,
|
||||
tfdiags.Diagnostics{tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Invalid state move request",
|
||||
"Ephemeral resources cannot be used as sources or targets for the move action. Just update your configuration accordingly.",
|
||||
)},
|
||||
},
|
||||
"resource to data": {
|
||||
managedRes,
|
||||
dataRes,
|
||||
tfdiags.Diagnostics{tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Invalid state move request",
|
||||
fmt.Sprintf("Cannot move %s to %s: a managed resource can be moved only to another managed resource address.", managedRes, dataRes),
|
||||
)},
|
||||
},
|
||||
"resource to ephemeral": {
|
||||
managedRes,
|
||||
ephemeralRes,
|
||||
tfdiags.Diagnostics{tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Invalid state move request",
|
||||
"Ephemeral resources cannot be used as sources or targets for the move action. Just update your configuration accordingly.",
|
||||
)},
|
||||
},
|
||||
"data to resource": {
|
||||
dataRes,
|
||||
managedRes,
|
||||
tfdiags.Diagnostics{tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Invalid state move request",
|
||||
fmt.Sprintf("Cannot move %s to %s: a data resource can be moved only to another data resource address.", dataRes, managedRes),
|
||||
)},
|
||||
},
|
||||
"data to ephemeral": {
|
||||
dataRes,
|
||||
ephemeralRes,
|
||||
tfdiags.Diagnostics{tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Invalid state move request",
|
||||
"Ephemeral resources cannot be used as sources or targets for the move action. Just update your configuration accordingly.",
|
||||
)},
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
diags := c.validateResourceMove(tt.src, tt.target)
|
||||
|
||||
if got, want := len(diags), len(tt.wantDiags); got != want {
|
||||
t.Fatalf("expected to have exactly %d diagnostic(s). got: %d", want, got)
|
||||
}
|
||||
for i, wantDiag := range tt.wantDiags {
|
||||
sameDiagnostic(t, diags[i], wantDiag)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func sameDiagnostic(t *testing.T, gotD, wantD tfdiags.Diagnostic) {
|
||||
if got, want := gotD.Severity(), wantD.Severity(); got != want {
|
||||
t.Errorf("wrong severity. got %q; want %q", got, want)
|
||||
}
|
||||
if got, want := gotD.Description().Address, wantD.Description().Address; got != want {
|
||||
t.Errorf("wrong description. got %q; want %q", got, want)
|
||||
}
|
||||
if got, want := gotD.Description().Detail, wantD.Description().Detail; got != want {
|
||||
t.Errorf("wrong detail. got %q; want %q", got, want)
|
||||
}
|
||||
if got, want := gotD.Description().Summary, wantD.Description().Summary; got != want {
|
||||
t.Errorf("wrong summary. got %q; want %q", got, want)
|
||||
}
|
||||
if got, want := gotD.ExtraInfo(), wantD.ExtraInfo(); got != want {
|
||||
t.Errorf("wrong extra info. got %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
const testStateMvOutputOriginal = `
|
||||
test_instance.baz:
|
||||
ID = foo
|
||||
|
||||
@@ -65,6 +65,7 @@ var (
|
||||
},
|
||||
},
|
||||
},
|
||||
// TODO ephemeral - when implementing testing support for ephemeral resources, consider configuring ephemeral schema here
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -96,8 +96,9 @@ func (h *countHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generati
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
// We don't count anything for data resources
|
||||
if addr.Resource.Resource.Mode == addrs.DataResourceMode {
|
||||
// We don't count anything for data resources and neither for the ephemeral ones.
|
||||
// TODO ephemeral - test this after the ephemeral resources are introduced entirely
|
||||
if addr.Resource.Resource.Mode == addrs.DataResourceMode || addr.Resource.Resource.Mode == addrs.EphemeralResourceMode {
|
||||
return tofu.HookActionContinue, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -265,6 +265,38 @@ func TestCountHookPostDiff_DataSource(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountHookPostDiff_Ephemeral(t *testing.T) {
|
||||
h := new(countHook)
|
||||
|
||||
resources := map[string]plans.Action{
|
||||
"foo": plans.Delete,
|
||||
"bar": plans.NoOp,
|
||||
"lorem": plans.Update,
|
||||
"ipsum": plans.Delete,
|
||||
}
|
||||
|
||||
for k, a := range resources {
|
||||
addr := addrs.Resource{
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: k,
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
||||
|
||||
_, _ = h.PostDiff(addr, states.CurrentGen, a, cty.DynamicVal, cty.DynamicVal)
|
||||
}
|
||||
|
||||
expected := new(countHook)
|
||||
expected.ToAdd = 0
|
||||
expected.ToChange = 0
|
||||
expected.ToRemoveAndAdd = 0
|
||||
expected.ToRemove = 0
|
||||
|
||||
if !reflect.DeepEqual(expected, h) {
|
||||
t.Fatalf("Expected %#v, got %#v instead.",
|
||||
expected, h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountHookApply_ChangeOnly(t *testing.T) {
|
||||
h := new(countHook)
|
||||
|
||||
|
||||
@@ -174,3 +174,33 @@ func (h *jsonHook) PostRefresh(addr addrs.AbsResourceInstance, gen states.Genera
|
||||
h.view.Hook(json.NewRefreshComplete(addr, idKey, idValue))
|
||||
return tofu.HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *jsonHook) PreOpen(addr addrs.AbsResourceInstance) (tofu.HookAction, error) {
|
||||
h.view.Hook(json.NewEphemeralStart(addr, "Opening..."))
|
||||
return tofu.HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *jsonHook) PostOpen(addr addrs.AbsResourceInstance, _ error) (tofu.HookAction, error) {
|
||||
h.view.Hook(json.NewEphemeralStop(addr, "Open complete"))
|
||||
return tofu.HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *jsonHook) PreRenew(addr addrs.AbsResourceInstance) (tofu.HookAction, error) {
|
||||
h.view.Hook(json.NewEphemeralStart(addr, "Renewing..."))
|
||||
return tofu.HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *jsonHook) PostRenew(addr addrs.AbsResourceInstance, _ error) (tofu.HookAction, error) {
|
||||
h.view.Hook(json.NewEphemeralStop(addr, "Renew complete"))
|
||||
return tofu.HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *jsonHook) PreClose(addr addrs.AbsResourceInstance) (tofu.HookAction, error) {
|
||||
h.view.Hook(json.NewEphemeralStart(addr, "Closing..."))
|
||||
return tofu.HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *jsonHook) PostClose(addr addrs.AbsResourceInstance, _ error) (tofu.HookAction, error) {
|
||||
h.view.Hook(json.NewEphemeralStop(addr, "Close complete"))
|
||||
return tofu.HookActionContinue, nil
|
||||
}
|
||||
|
||||
@@ -339,6 +339,190 @@ func TestJSONHook_refresh(t *testing.T) {
|
||||
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
||||
}
|
||||
|
||||
func TestJSONHook_ephemeral(t *testing.T) {
|
||||
addr := addrs.Resource{
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
preF func(hook tofu.Hook) (tofu.HookAction, error)
|
||||
postF func(hook tofu.Hook) (tofu.HookAction, error)
|
||||
want []map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "opening",
|
||||
preF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
||||
return hook.PreOpen(addr)
|
||||
},
|
||||
postF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
||||
return hook.PostOpen(addr, nil)
|
||||
},
|
||||
want: []map[string]interface{}{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "ephemeral.test_instance.foo: Opening...",
|
||||
"@module": "tofu.ui",
|
||||
"hook": map[string]any{
|
||||
"Msg": "Opening...",
|
||||
"resource": map[string]any{
|
||||
"addr": "ephemeral.test_instance.foo",
|
||||
"implied_provider": "test",
|
||||
"module": "",
|
||||
"resource": "ephemeral.test_instance.foo",
|
||||
"resource_key": nil,
|
||||
"resource_name": "foo",
|
||||
"resource_type": "test_instance",
|
||||
},
|
||||
},
|
||||
"type": "ephemeral_action_started",
|
||||
},
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "ephemeral.test_instance.foo: Open complete",
|
||||
"@module": "tofu.ui",
|
||||
"hook": map[string]any{
|
||||
"Msg": "Open complete",
|
||||
"resource": map[string]any{
|
||||
"addr": "ephemeral.test_instance.foo",
|
||||
"implied_provider": "test",
|
||||
"module": "",
|
||||
"resource": "ephemeral.test_instance.foo",
|
||||
"resource_key": nil,
|
||||
"resource_name": "foo",
|
||||
"resource_type": "test_instance",
|
||||
},
|
||||
},
|
||||
"type": "ephemeral_action_complete",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "renewing",
|
||||
preF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
||||
return hook.PreRenew(addr)
|
||||
},
|
||||
postF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
||||
return hook.PostRenew(addr, nil)
|
||||
},
|
||||
want: []map[string]interface{}{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "ephemeral.test_instance.foo: Renewing...",
|
||||
"@module": "tofu.ui",
|
||||
"hook": map[string]any{
|
||||
"Msg": "Renewing...",
|
||||
"resource": map[string]any{
|
||||
"addr": "ephemeral.test_instance.foo",
|
||||
"implied_provider": "test",
|
||||
"module": "",
|
||||
"resource": "ephemeral.test_instance.foo",
|
||||
"resource_key": nil,
|
||||
"resource_name": "foo",
|
||||
"resource_type": "test_instance",
|
||||
},
|
||||
},
|
||||
"type": "ephemeral_action_started",
|
||||
},
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "ephemeral.test_instance.foo: Renew complete",
|
||||
"@module": "tofu.ui",
|
||||
"hook": map[string]any{
|
||||
"Msg": "Renew complete",
|
||||
"resource": map[string]any{
|
||||
"addr": "ephemeral.test_instance.foo",
|
||||
"implied_provider": "test",
|
||||
"module": "",
|
||||
"resource": "ephemeral.test_instance.foo",
|
||||
"resource_key": nil,
|
||||
"resource_name": "foo",
|
||||
"resource_type": "test_instance",
|
||||
},
|
||||
},
|
||||
"type": "ephemeral_action_complete",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "closing",
|
||||
preF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
||||
return hook.PreClose(addr)
|
||||
},
|
||||
postF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
||||
return hook.PostClose(addr, nil)
|
||||
},
|
||||
want: []map[string]interface{}{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "ephemeral.test_instance.foo: Closing...",
|
||||
"@module": "tofu.ui",
|
||||
"hook": map[string]any{
|
||||
"Msg": "Closing...",
|
||||
"resource": map[string]any{
|
||||
"addr": "ephemeral.test_instance.foo",
|
||||
"implied_provider": "test",
|
||||
"module": "",
|
||||
"resource": "ephemeral.test_instance.foo",
|
||||
"resource_key": nil,
|
||||
"resource_name": "foo",
|
||||
"resource_type": "test_instance",
|
||||
},
|
||||
},
|
||||
"type": "ephemeral_action_started",
|
||||
},
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "ephemeral.test_instance.foo: Close complete",
|
||||
"@module": "tofu.ui",
|
||||
"hook": map[string]any{
|
||||
"Msg": "Close complete",
|
||||
"resource": map[string]any{
|
||||
"addr": "ephemeral.test_instance.foo",
|
||||
"implied_provider": "test",
|
||||
"module": "",
|
||||
"resource": "ephemeral.test_instance.foo",
|
||||
"resource_key": nil,
|
||||
"resource_name": "foo",
|
||||
"resource_type": "test_instance",
|
||||
},
|
||||
},
|
||||
"type": "ephemeral_action_complete",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
h := newJSONHook(NewJSONView(NewView(streams)))
|
||||
|
||||
action, err := tt.preF(h)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if action != tofu.HookActionContinue {
|
||||
t.Fatalf("Expected hook to continue, given: %#v", action)
|
||||
}
|
||||
|
||||
<-time.After(1100 * time.Millisecond)
|
||||
|
||||
// call postF that will stop the waiting for the action
|
||||
action, err = tt.postF(h)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if action != tofu.HookActionContinue {
|
||||
t.Errorf("Expected hook to continue, given: %#v", action)
|
||||
}
|
||||
|
||||
testJSONViewOutputEquals(t, done(t).Stdout(), tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testHookReturnValues(t *testing.T, action tofu.HookAction, err error) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -78,6 +78,8 @@ const (
|
||||
uiResourceDestroy
|
||||
uiResourceRead
|
||||
uiResourceNoOp
|
||||
// NOTE: Ephemeral hooks are implemented separately,
|
||||
// so there are no uiResource entries for Open/Renew/Close actions.
|
||||
)
|
||||
|
||||
func (h *UiHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (tofu.HookAction, error) {
|
||||
@@ -339,6 +341,120 @@ func (h *UiHook) PostApplyImport(addr addrs.AbsResourceInstance, importing plans
|
||||
return tofu.HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *UiHook) Deferred(addr addrs.AbsResourceInstance, reason string) (tofu.HookAction, error) {
|
||||
id := addr.String()
|
||||
msg := fmt.Sprintf("Deferred due to %s", reason)
|
||||
|
||||
colorized := fmt.Sprintf(
|
||||
h.view.colorize.Color("[reset][bold]%s: %s"),
|
||||
id, msg)
|
||||
|
||||
h.println(colorized)
|
||||
|
||||
return tofu.HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *UiHook) PreOpen(addr addrs.AbsResourceInstance) (tofu.HookAction, error) {
|
||||
return h.preEphemeral(addr, "Opening...", "Still opening...")
|
||||
}
|
||||
|
||||
func (h *UiHook) PostOpen(addr addrs.AbsResourceInstance, _ error) (tofu.HookAction, error) {
|
||||
return h.postEphemeral(addr, "Open complete")
|
||||
}
|
||||
|
||||
func (h *UiHook) PreRenew(addr addrs.AbsResourceInstance) (tofu.HookAction, error) {
|
||||
return h.preEphemeral(addr, "Renewing...", "Still renewing...")
|
||||
}
|
||||
|
||||
func (h *UiHook) PostRenew(addr addrs.AbsResourceInstance, _ error) (tofu.HookAction, error) {
|
||||
return h.postEphemeral(addr, "Renew complete")
|
||||
}
|
||||
|
||||
func (h *UiHook) PreClose(addr addrs.AbsResourceInstance) (tofu.HookAction, error) {
|
||||
return h.preEphemeral(addr, "Closing...", "Still closing...")
|
||||
}
|
||||
|
||||
func (h *UiHook) PostClose(addr addrs.AbsResourceInstance, _ error) (tofu.HookAction, error) {
|
||||
return h.postEphemeral(addr, "Close complete")
|
||||
}
|
||||
|
||||
// preEphemeral is the hook implementation that is used before actions like Renew and Close.
|
||||
// These are specific for ephemeral resources, and we are not using hook methods used for
|
||||
// the rest of the resource types because these particular 2 operations have no action
|
||||
// associated.
|
||||
func (h *UiHook) preEphemeral(addr addrs.AbsResourceInstance, startMsg, stillRunningMsg string) (tofu.HookAction, error) {
|
||||
dispAddr := addr.String()
|
||||
|
||||
h.println(fmt.Sprintf(
|
||||
h.view.colorize.Color("[reset][bold]%s: %s[reset]"),
|
||||
dispAddr,
|
||||
startMsg,
|
||||
))
|
||||
|
||||
key := addr.String()
|
||||
uiState := uiResourceState{
|
||||
DispAddr: key,
|
||||
Start: time.Now().Round(time.Second),
|
||||
DoneCh: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
h.resourcesLock.Lock()
|
||||
h.resources[key] = uiState
|
||||
h.resourcesLock.Unlock()
|
||||
|
||||
go func() {
|
||||
defer close(uiState.done)
|
||||
for {
|
||||
select {
|
||||
case <-uiState.DoneCh:
|
||||
return
|
||||
case <-time.After(h.periodicUiTimer):
|
||||
// Timer up, show status
|
||||
}
|
||||
|
||||
h.println(fmt.Sprintf(
|
||||
h.view.colorize.Color("[reset][bold]%s: %s [%s elapsed][reset]"),
|
||||
uiState.DispAddr,
|
||||
stillRunningMsg,
|
||||
time.Now().Round(time.Second).Sub(uiState.Start),
|
||||
))
|
||||
}
|
||||
}()
|
||||
|
||||
return tofu.HookActionContinue, nil
|
||||
}
|
||||
|
||||
// postEphemeral is the hook implementation that is used after actions like Renew and Close.
|
||||
// These are specific for ephemeral resources, and we are not using hook methods used for
|
||||
// the rest of the resource types because these particular 2 operations have no action
|
||||
// associated.
|
||||
func (h *UiHook) postEphemeral(addr addrs.AbsResourceInstance, msg string) (tofu.HookAction, error) {
|
||||
id := addr.String()
|
||||
|
||||
h.resourcesLock.Lock()
|
||||
state := h.resources[id]
|
||||
if state.DoneCh != nil {
|
||||
close(state.DoneCh)
|
||||
}
|
||||
|
||||
delete(h.resources, id)
|
||||
h.resourcesLock.Unlock()
|
||||
|
||||
addrStr := addr.String()
|
||||
|
||||
colorized := fmt.Sprintf(
|
||||
h.view.colorize.Color("[reset][bold]%s: %s after %s"),
|
||||
addrStr,
|
||||
msg,
|
||||
time.Now().Round(time.Second).Sub(state.Start),
|
||||
)
|
||||
|
||||
h.println(colorized)
|
||||
|
||||
return tofu.HookActionContinue, nil
|
||||
}
|
||||
|
||||
// Wrap calls to the view so that concurrent calls do not interleave println.
|
||||
func (h *UiHook) println(s string) {
|
||||
h.viewLock.Lock()
|
||||
|
||||
@@ -144,6 +144,104 @@ test_instance\.foo: Still modifying... \[id=test, \ds elapsed\]
|
||||
}
|
||||
}
|
||||
|
||||
// Test the ephemeral specific hooks
|
||||
func TestUiHook_ephemeral(t *testing.T) {
|
||||
addr := addrs.Resource{
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
preF func(hook tofu.Hook) (tofu.HookAction, error)
|
||||
postF func(hook tofu.Hook) (tofu.HookAction, error)
|
||||
wantOutput string
|
||||
}{
|
||||
{
|
||||
name: "opening",
|
||||
preF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
||||
return hook.PreOpen(addr)
|
||||
},
|
||||
postF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
||||
return hook.PostOpen(addr, nil)
|
||||
},
|
||||
wantOutput: `ephemeral\.test_instance\.foo: Opening\.\.\.
|
||||
ephemeral\.test_instance\.foo: Still opening\.\.\. \[\ds elapsed\]
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "renewing",
|
||||
preF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
||||
return hook.PreRenew(addr)
|
||||
},
|
||||
postF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
||||
return hook.PostRenew(addr, nil)
|
||||
},
|
||||
wantOutput: `ephemeral\.test_instance\.foo: Renewing\.\.\.
|
||||
ephemeral\.test_instance\.foo: Still renewing\.\.\. \[\ds elapsed\]
|
||||
ephemeral\.test_instance\.foo: Renew complete after \ds
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "closing",
|
||||
preF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
||||
return hook.PreClose(addr)
|
||||
},
|
||||
postF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
||||
return hook.PostClose(addr, nil)
|
||||
},
|
||||
wantOutput: `ephemeral\.test_instance\.foo: Closing\.\.\.
|
||||
ephemeral\.test_instance\.foo: Still closing\.\.\. \[\ds elapsed\]
|
||||
ephemeral\.test_instance\.foo: Close complete after \ds
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := NewView(streams)
|
||||
h := NewUiHook(view)
|
||||
h.periodicUiTimer = 1 * time.Second
|
||||
|
||||
action, err := tt.preF(h)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if action != tofu.HookActionContinue {
|
||||
t.Fatalf("Expected hook to continue, given: %#v", action)
|
||||
}
|
||||
|
||||
<-time.After(1100 * time.Millisecond)
|
||||
|
||||
// stop the background writer
|
||||
uiState := h.resources[addr.String()]
|
||||
// call postF that will stop the waiting for the action
|
||||
action, err = tt.postF(h)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if action != tofu.HookActionContinue {
|
||||
t.Errorf("Expected hook to continue, given: %#v", action)
|
||||
}
|
||||
// wait for the waiting to stop completely
|
||||
<-uiState.done
|
||||
|
||||
result := done(t)
|
||||
output := result.Stdout()
|
||||
if matched, _ := regexp.MatchString(tt.wantOutput, output); !matched {
|
||||
t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", tt.wantOutput, output)
|
||||
}
|
||||
|
||||
expectedErrOutput := ""
|
||||
errOutput := result.Stderr()
|
||||
if errOutput != expectedErrOutput {
|
||||
t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test the PreApply hook's destroy path, including passing a deposed key as
|
||||
// the gen argument.
|
||||
func TestUiHookPreApply_destroy(t *testing.T) {
|
||||
|
||||
@@ -304,8 +304,56 @@ func NewRefreshComplete(addr addrs.AbsResourceInstance, idKey, idValue string) H
|
||||
}
|
||||
}
|
||||
|
||||
// EphemeralStart: triggered by PreOpen, PreRenew and PreClose hooks
|
||||
type ephemeralStart struct {
|
||||
Resource jsonentities.ResourceAddr `json:"resource"`
|
||||
Msg string
|
||||
}
|
||||
|
||||
var _ Hook = (*ephemeralStart)(nil)
|
||||
|
||||
func (h *ephemeralStart) HookType() MessageType {
|
||||
return MessageEphemeralActionStart
|
||||
}
|
||||
|
||||
func (h *ephemeralStart) String() string {
|
||||
return fmt.Sprintf("%s: %s", h.Resource.Addr, h.Msg)
|
||||
}
|
||||
|
||||
func NewEphemeralStart(addr addrs.AbsResourceInstance, startMsg string) Hook {
|
||||
return &ephemeralStart{
|
||||
Resource: jsonentities.NewResourceAddr(addr),
|
||||
Msg: startMsg,
|
||||
}
|
||||
}
|
||||
|
||||
// EphemeralStop: triggered by PostOpen, PostRenew and PostClose hooks
|
||||
type ephemeralStop struct {
|
||||
Resource jsonentities.ResourceAddr `json:"resource"`
|
||||
Msg string
|
||||
}
|
||||
|
||||
var _ Hook = (*ephemeralStop)(nil)
|
||||
|
||||
func (h *ephemeralStop) HookType() MessageType {
|
||||
return MessageEphemeralActionComplete
|
||||
}
|
||||
|
||||
func (h *ephemeralStop) String() string {
|
||||
return fmt.Sprintf("%s: %s", h.Resource.Addr, h.Msg)
|
||||
}
|
||||
|
||||
func NewEphemeralStop(addr addrs.AbsResourceInstance, startMsg string) Hook {
|
||||
return &ephemeralStop{
|
||||
Resource: jsonentities.NewResourceAddr(addr),
|
||||
Msg: startMsg,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the subset of plans.Action values we expect to receive into a
|
||||
// present-tense verb for the applyStart hook message.
|
||||
//
|
||||
// NOTE: Open, Renew and Close missing on purpose since those have their own dedicated hooks.
|
||||
func startActionVerb(action plans.Action) string {
|
||||
switch action {
|
||||
case plans.Create:
|
||||
@@ -334,6 +382,8 @@ func startActionVerb(action plans.Action) string {
|
||||
// Convert the subset of plans.Action values we expect to receive into a
|
||||
// present-tense verb for the applyProgress hook message. This will be
|
||||
// prefixed with "Still ", so it is lower-case.
|
||||
//
|
||||
// NOTE: Open, Renew and Close missing on purpose since those have their own dedicated hooks.
|
||||
func progressActionVerb(action plans.Action) string {
|
||||
switch action {
|
||||
case plans.Create:
|
||||
@@ -362,6 +412,8 @@ func progressActionVerb(action plans.Action) string {
|
||||
// Convert the subset of plans.Action values we expect to receive into a
|
||||
// noun for the applyComplete and applyErrored hook messages. This will be
|
||||
// combined into a phrase like "Creation complete after 1m4s".
|
||||
//
|
||||
// NOTE: Open, Renew and Close missing on purpose since those have their own dedicated hooks.
|
||||
func actionNoun(action plans.Action) string {
|
||||
switch action {
|
||||
case plans.Create:
|
||||
|
||||
@@ -20,16 +20,18 @@ const (
|
||||
MessageOutputs MessageType = "outputs"
|
||||
|
||||
// Hook-driven messages
|
||||
MessageApplyStart MessageType = "apply_start"
|
||||
MessageApplyProgress MessageType = "apply_progress"
|
||||
MessageApplyComplete MessageType = "apply_complete"
|
||||
MessageApplyErrored MessageType = "apply_errored"
|
||||
MessageProvisionStart MessageType = "provision_start"
|
||||
MessageProvisionProgress MessageType = "provision_progress"
|
||||
MessageProvisionComplete MessageType = "provision_complete"
|
||||
MessageProvisionErrored MessageType = "provision_errored"
|
||||
MessageRefreshStart MessageType = "refresh_start"
|
||||
MessageRefreshComplete MessageType = "refresh_complete"
|
||||
MessageApplyStart MessageType = "apply_start"
|
||||
MessageApplyProgress MessageType = "apply_progress"
|
||||
MessageApplyComplete MessageType = "apply_complete"
|
||||
MessageApplyErrored MessageType = "apply_errored"
|
||||
MessageProvisionStart MessageType = "provision_start"
|
||||
MessageProvisionProgress MessageType = "provision_progress"
|
||||
MessageProvisionComplete MessageType = "provision_complete"
|
||||
MessageProvisionErrored MessageType = "provision_errored"
|
||||
MessageRefreshStart MessageType = "refresh_start"
|
||||
MessageRefreshComplete MessageType = "refresh_complete"
|
||||
MessageEphemeralActionStart MessageType = "ephemeral_action_started"
|
||||
MessageEphemeralActionComplete MessageType = "ephemeral_action_complete"
|
||||
|
||||
// Test messages
|
||||
MessageTestAbstract MessageType = "test_abstract"
|
||||
|
||||
@@ -277,6 +277,10 @@ func (v *OperationJSON) PlannedChange(change *plans.ResourceInstanceChangeSrc) {
|
||||
// Avoid rendering data sources on deletion
|
||||
return
|
||||
}
|
||||
if change.Addr.Resource.Resource.Mode == addrs.EphemeralResourceMode {
|
||||
// Ephemeral changes should not be rendered
|
||||
return
|
||||
}
|
||||
v.view.PlannedChange(jsonentities.NewResourceInstanceChange(change))
|
||||
}
|
||||
|
||||
|
||||
@@ -435,6 +435,34 @@ Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperation_planWithEphemeral(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
v := NewOperation(arguments.ViewHuman, true, NewView(streams))
|
||||
|
||||
plan := testPlanWithEphemeral(t)
|
||||
schemas := testSchemas()
|
||||
v.Plan(plan, schemas)
|
||||
|
||||
want := `
|
||||
OpenTofu used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
OpenTofu will perform the following actions:
|
||||
|
||||
# test_resource.foo will be created
|
||||
+ resource "test_resource" "foo" {
|
||||
+ foo = "bar"
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
`
|
||||
|
||||
if got := done(t).Stdout(); got != want {
|
||||
t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
func TestOperation_planNextStep(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
path string
|
||||
@@ -482,6 +510,15 @@ func TestOperationJSON_logs(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
v := &OperationJSON{view: NewJSONView(NewView(streams))}
|
||||
|
||||
// Added an ephemeral resource change to double-check that it's not
|
||||
// shown.
|
||||
v.PlannedChange(&plans.ResourceInstanceChangeSrc{
|
||||
Addr: addrs.AbsResourceInstance{
|
||||
Resource: addrs.ResourceInstance{
|
||||
Resource: addrs.Resource{Mode: addrs.EphemeralResourceMode},
|
||||
},
|
||||
},
|
||||
})
|
||||
v.Cancelled(plans.NormalMode)
|
||||
v.Cancelled(plans.DestroyMode)
|
||||
v.Stopping()
|
||||
|
||||
@@ -133,6 +133,45 @@ func testPlanWithDatasource(t *testing.T) *plans.Plan {
|
||||
return plan
|
||||
}
|
||||
|
||||
func testPlanWithEphemeral(t *testing.T) *plans.Plan {
|
||||
plan := testPlan(t)
|
||||
|
||||
addr := addrs.Resource{
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Type: "test_ephemeral_resource",
|
||||
Name: "bar",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
||||
|
||||
ephemeralVal := cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("C6743020-40BD-4591-81E6-CD08494341D3"),
|
||||
"foo": cty.StringVal("baz"),
|
||||
})
|
||||
priorValRaw, err := plans.NewDynamicValue(cty.NullVal(ephemeralVal.Type()), ephemeralVal.Type())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plannedValRaw, err := plans.NewDynamicValue(ephemeralVal, ephemeralVal.Type())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
plan.Changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{
|
||||
Addr: addr,
|
||||
PrevRunAddr: addr,
|
||||
ProviderAddr: addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
ChangeSrc: plans.ChangeSrc{
|
||||
Action: plans.Open,
|
||||
Before: priorValRaw,
|
||||
After: plannedValRaw,
|
||||
},
|
||||
})
|
||||
|
||||
return plan
|
||||
}
|
||||
|
||||
func testSchemas() *tofu.Schemas {
|
||||
provider := testProvider()
|
||||
return &tofu.Schemas{
|
||||
@@ -178,5 +217,15 @@ func testProviderSchema() *providers.GetProviderSchemaResponse {
|
||||
},
|
||||
},
|
||||
},
|
||||
EphemeralResources: map[string]providers.Schema{
|
||||
"test_ephemeral_resource": {
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Required: true},
|
||||
"foo": {Type: cty.String, Optional: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@ var ConnectionBlockSupersetSchema = &configschema.Block{
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
Ephemeral: true,
|
||||
}
|
||||
|
||||
// IpFormat formats the IP correctly, so we don't provide IPv6 address in an IPv4 format during node communication. We return the ip parameter as is if it's an IPv4 address or a hostname.
|
||||
|
||||
@@ -419,47 +419,9 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements, qualifs
|
||||
// Each resource in the configuration creates an *implicit* provider
|
||||
// dependency, though we'll only record it if there isn't already
|
||||
// an explicit dependency on the same provider.
|
||||
for _, rc := range c.Module.ManagedResources {
|
||||
fqn := rc.Provider
|
||||
if _, exists := reqs[fqn]; exists {
|
||||
// If this is called for a child module, and the provider was added from another implicit reference and not
|
||||
// from a top level required_provider, we need to collect the reference of this resource as well as implicit provider.
|
||||
qualifs.AddImplicitProvider(fqn, getproviders.ResourceRef{
|
||||
CfgRes: rc.Addr().InModule(c.Path),
|
||||
Ref: tfdiags.SourceRangeFromHCL(rc.DeclRange),
|
||||
ProviderAttribute: rc.ProviderConfigRef != nil,
|
||||
})
|
||||
// Explicit dependency already present
|
||||
continue
|
||||
}
|
||||
qualifs.AddImplicitProvider(fqn, getproviders.ResourceRef{
|
||||
CfgRes: rc.Addr().InModule(c.Path),
|
||||
Ref: tfdiags.SourceRangeFromHCL(rc.DeclRange),
|
||||
ProviderAttribute: rc.ProviderConfigRef != nil,
|
||||
})
|
||||
reqs[fqn] = nil
|
||||
}
|
||||
for _, rc := range c.Module.DataResources {
|
||||
fqn := rc.Provider
|
||||
if _, exists := reqs[fqn]; exists {
|
||||
// If this is called for a child module, and the provider was added from another implicit reference and not
|
||||
// from a top level required_provider, we need to collect the reference of this resource as well as implicit provider.
|
||||
qualifs.AddImplicitProvider(fqn, getproviders.ResourceRef{
|
||||
CfgRes: rc.Addr().InModule(c.Path),
|
||||
Ref: tfdiags.SourceRangeFromHCL(rc.DeclRange),
|
||||
ProviderAttribute: rc.ProviderConfigRef != nil,
|
||||
})
|
||||
|
||||
// Explicit dependency already present
|
||||
continue
|
||||
}
|
||||
qualifs.AddImplicitProvider(fqn, getproviders.ResourceRef{
|
||||
CfgRes: rc.Addr().InModule(c.Path),
|
||||
Ref: tfdiags.SourceRangeFromHCL(rc.DeclRange),
|
||||
ProviderAttribute: rc.ProviderConfigRef != nil,
|
||||
})
|
||||
reqs[fqn] = nil
|
||||
}
|
||||
c.collectImplicitProviders(c.Module.ManagedResources, reqs, qualifs)
|
||||
c.collectImplicitProviders(c.Module.DataResources, reqs, qualifs)
|
||||
c.collectImplicitProviders(c.Module.EphemeralResources, reqs, qualifs)
|
||||
|
||||
// Import blocks that are generating config may also have a custom provider
|
||||
// meta argument. Like the provider meta argument used in resource blocks,
|
||||
@@ -573,6 +535,31 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements, qualifs
|
||||
return diags
|
||||
}
|
||||
|
||||
// collectImplicitProviders is checking the provider configuration of each resource.
|
||||
// For the resources whose required provider is not explicitly configured, an implicit one is collected.
|
||||
// This is mainly used for enabling warnings when OpenTofu fails to resolve the implicitly generated provider.
|
||||
func (c *Config) collectImplicitProviders(resources map[string]*Resource, reqs getproviders.Requirements, qualifs *getproviders.ProvidersQualification) {
|
||||
for _, rc := range resources {
|
||||
fqn := rc.Provider
|
||||
if _, exists := reqs[fqn]; exists {
|
||||
// If this is called for a child module, and the provider was added from another implicit reference and not
|
||||
// from a top level required_provider, we need to collect the reference of this resource as well as implicit provider.
|
||||
qualifs.AddImplicitProvider(fqn, getproviders.ResourceRef{
|
||||
CfgRes: rc.Addr().InModule(c.Path),
|
||||
Ref: tfdiags.SourceRangeFromHCL(rc.DeclRange),
|
||||
ProviderAttribute: rc.ProviderConfigRef != nil,
|
||||
})
|
||||
continue
|
||||
}
|
||||
qualifs.AddImplicitProvider(fqn, getproviders.ResourceRef{
|
||||
CfgRes: rc.Addr().InModule(c.Path),
|
||||
Ref: tfdiags.SourceRangeFromHCL(rc.DeclRange),
|
||||
ProviderAttribute: rc.ProviderConfigRef != nil,
|
||||
})
|
||||
reqs[fqn] = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) addProviderRequirementsFromProviderBlock(reqs getproviders.Requirements, provider *Provider) hcl.Diagnostics {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
@@ -1067,11 +1054,12 @@ func (c *Config) transformOverriddenResourcesForTest(run *TestRun, file *TestFil
|
||||
}
|
||||
|
||||
if res.Mode != overrideRes.Mode {
|
||||
// TODO ephemeral - include also the ephemeral resource and the test_file.go#override_ephemeral
|
||||
blockName, targetMode := blockNameOverrideResource, "data"
|
||||
if overrideRes.Mode == addrs.DataResourceMode {
|
||||
blockName, targetMode = blockNameOverrideData, "resource"
|
||||
}
|
||||
// It could be a warning, but for the sake of consistent UX let's make it an error
|
||||
//It could be a warning, but for the sake of consistent UX let's make it an error
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Unsupported `%v` target in `%v` block", targetMode, blockName),
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -718,6 +719,81 @@ func TestConfigAddProviderRequirements(t *testing.T) {
|
||||
qualifs := new(getproviders.ProvidersQualification)
|
||||
diags = cfg.addProviderRequirements(reqs, qualifs, true, false)
|
||||
assertNoDiagnostics(t, diags)
|
||||
if got, want := len(qualifs.Explicit), 1; got != want {
|
||||
t.Fatalf("expected to have %d explicit provider requirement but got %d", want, got)
|
||||
}
|
||||
if got, want := len(qualifs.Implicit), 4; got != want {
|
||||
t.Fatalf("expected to have %d explicit provider requirement but got %d", want, got)
|
||||
}
|
||||
|
||||
checks := []struct {
|
||||
key addrs.Provider
|
||||
want []addrs.Resource
|
||||
}{
|
||||
{
|
||||
// check registry.opentofu.org/hashicorp/aws
|
||||
key: addrs.NewProvider("registry.opentofu.org", "hashicorp", "aws"),
|
||||
want: []addrs.Resource{
|
||||
cfg.Path.Resource(addrs.ManagedResourceMode, "aws_instance", "foo").Resource,
|
||||
cfg.Path.Resource(addrs.DataResourceMode, "aws_s3_object", "baz").Resource,
|
||||
cfg.Path.Resource(addrs.EphemeralResourceMode, "aws_secret", "bar").Resource,
|
||||
},
|
||||
},
|
||||
{
|
||||
// check registry.opentofu.org/hashicorp/null
|
||||
key: addrs.NewProvider("registry.opentofu.org", "hashicorp", "null"),
|
||||
want: []addrs.Resource{
|
||||
cfg.Path.Resource(addrs.ManagedResourceMode, "null_resource", "foo").Resource,
|
||||
},
|
||||
},
|
||||
{
|
||||
// check registry.opentofu.org/hashicorp/local
|
||||
key: addrs.NewProvider("registry.opentofu.org", "hashicorp", "local"),
|
||||
want: []addrs.Resource{
|
||||
cfg.Path.Resource(addrs.ManagedResourceMode, "local_file", "foo").Resource,
|
||||
},
|
||||
},
|
||||
{
|
||||
// check registry.opentofu.org/hashicorp/template
|
||||
key: addrs.NewProvider("registry.opentofu.org", "hashicorp", "template"),
|
||||
want: []addrs.Resource{
|
||||
cfg.Path.Resource(addrs.ManagedResourceMode, "local_file", "bar").Resource,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, c := range checks {
|
||||
t.Run(c.key.String(), func(t *testing.T) {
|
||||
refs := qualifs.Implicit[c.key]
|
||||
if got, want := len(refs), len(c.want); got != want {
|
||||
t.Fatalf("expected to find %d implicit references for provider %q but got %d", want, c.key, got)
|
||||
}
|
||||
|
||||
var refsAddrs []addrs.Resource
|
||||
for _, ref := range refs {
|
||||
refsAddrs = append(refsAddrs, ref.CfgRes.Resource)
|
||||
}
|
||||
sort.Slice(refsAddrs, func(i, j int) bool {
|
||||
return refsAddrs[i].Less(refsAddrs[j])
|
||||
})
|
||||
sort.Slice(c.want, func(i, j int) bool {
|
||||
return c.want[i].Less(c.want[j])
|
||||
})
|
||||
if diff := cmp.Diff(refsAddrs, c.want); diff != "" {
|
||||
t.Fatalf("expected to find specific resources to implicitly reference the provider %s. diff:\n%s", c.key, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wantReqs := getproviders.Requirements{
|
||||
addrs.NewProvider("registry.opentofu.org", "hashicorp", "template"): nil,
|
||||
addrs.NewProvider("registry.opentofu.org", "hashicorp", "local"): nil,
|
||||
addrs.NewProvider("registry.opentofu.org", "hashicorp", "null"): nil,
|
||||
addrs.NewProvider("registry.opentofu.org", "hashicorp", "aws"): nil,
|
||||
addrs.NewProvider("registry.opentofu.org", "hashicorp", "test"): nil,
|
||||
}
|
||||
if diff := cmp.Diff(wantReqs, reqs); diff != "" {
|
||||
t.Fatalf("unexected returned providers qualifications: %s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigImportProviderClashesWithModules(t *testing.T) {
|
||||
@@ -1088,5 +1164,68 @@ func TestIsCallFromRemote(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEphemeralBlocks(t *testing.T) {
|
||||
p := NewParser(nil)
|
||||
f, diags := p.LoadConfigFile("testdata/ephemeral-blocks/main.tf")
|
||||
// check diags
|
||||
{
|
||||
if len(diags) != 6 { // 4 lifecycle unallowed attributes, unallowed connection block and unallowed provisioner block
|
||||
t.Fatalf("expected 6 diagnostics but got only: %d", len(diags))
|
||||
}
|
||||
containsExpectedKeywords := func(diagContent string) bool {
|
||||
for _, k := range []string{"ignore_changes", "prevent_destroy", "create_before_destroy", "replace_triggered_by", "connection", "provisioner"} {
|
||||
if strings.Contains(diagContent, k) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
for _, diag := range diags {
|
||||
if content := diag.Error(); !containsExpectedKeywords(content) {
|
||||
t.Fatalf("expected diagnostic to contain at least one of the keywords: %s", content)
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
if len(f.EphemeralResources) != 2 {
|
||||
t.Fatalf("expected 2 ephemeral resources but got only: %d", len(f.EphemeralResources))
|
||||
}
|
||||
for _, er := range f.EphemeralResources {
|
||||
switch er.Name {
|
||||
case "foo":
|
||||
if er.ForEach == nil {
|
||||
t.Errorf("expected to have a for_each expression but got nothing")
|
||||
}
|
||||
case "bar":
|
||||
attrs, _ := er.Config.JustAttributes()
|
||||
if _, ok := attrs["attribute"]; !ok {
|
||||
t.Errorf("expected to have \"attribute\" but could not find it")
|
||||
}
|
||||
if _, ok := attrs["attribute2"]; !ok {
|
||||
t.Errorf("expected to have \"attribute\" but could not find it")
|
||||
}
|
||||
if er.Count == nil {
|
||||
t.Errorf("expected to have a count expression but got nothing")
|
||||
}
|
||||
if er.ProviderConfigRef == nil || er.ProviderConfigRef.Addr().String() != "provider.test.name" {
|
||||
t.Errorf("expected to have \"provider.test.name\" provider alias configured but instead it was: %+v", er.ProviderConfigRef)
|
||||
}
|
||||
if len(er.Preconditions) != 1 {
|
||||
t.Errorf("expected to have one precondition but got %d", len(er.Preconditions))
|
||||
}
|
||||
if len(er.Postconditions) != 1 {
|
||||
t.Errorf("expected to have one postcondition but got %d", len(er.Postconditions))
|
||||
}
|
||||
if len(er.DependsOn) != 1 {
|
||||
t.Errorf("expected to have a depends_on traversal but got %d", len(er.Postconditions))
|
||||
}
|
||||
if er.Managed != nil {
|
||||
t.Errorf("error in the parsing code. Ephemeral resources are not meant to have a managed object")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,6 +61,24 @@ func (b *Block) ContainsSensitive() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ContainsMarks is a wrapper around Block.ContainsSensitive which adds
|
||||
// another check for the ephemeral nature of the block.
|
||||
// The schema attributes cannot be marked as ephemeral, only the whole block
|
||||
// can have that mark.
|
||||
// Therefore, we don't need to check the schema recursively.
|
||||
//
|
||||
// NOTE: It's important to make the distinction between "schema attributes" and
|
||||
// "value attributes".
|
||||
// A schema attribute cannot have the ephemeral mark, but a value attribute
|
||||
// can be marked as ephemeral if it's referencing attribute(s) from another
|
||||
// ephemeral block.
|
||||
func (b *Block) ContainsMarks() bool {
|
||||
if b.Ephemeral {
|
||||
return true
|
||||
}
|
||||
return b.ContainsSensitive()
|
||||
}
|
||||
|
||||
// ImpliedType returns the cty.Type that would result from decoding a Block's
|
||||
// ImpliedType and getting the resulting AttributeType.
|
||||
//
|
||||
|
||||
@@ -27,6 +27,16 @@ func copyAndExtendPath(path cty.Path, nextSteps ...cty.PathStep) cty.Path {
|
||||
func (b *Block) ValueMarks(val cty.Value, path cty.Path) []cty.PathValueMarks {
|
||||
var pvm []cty.PathValueMarks
|
||||
|
||||
// When the block is marked as ephemeral, the whole value needs to be marked accordingly.
|
||||
// Inner attributes should carry no ephemeral mark.
|
||||
// The ephemerality of the attributes is given by the mark on the val and not by individual marks
|
||||
// as it's the case for the sensitive mark.
|
||||
if b.Ephemeral {
|
||||
pvm = append(pvm, cty.PathValueMarks{
|
||||
Path: path, // raw received path is indicating that the whole value needs to be marked as ephemeral.
|
||||
Marks: cty.NewValueMarks(marks.Ephemeral),
|
||||
})
|
||||
}
|
||||
// We can mark attributes as sensitive even if the value is null
|
||||
for name, attrS := range b.Attributes {
|
||||
if attrS.Sensitive {
|
||||
@@ -156,3 +166,33 @@ func (o *Object) ValueMarks(val cty.Value, path cty.Path) []cty.PathValueMarks {
|
||||
}
|
||||
return pvm
|
||||
}
|
||||
|
||||
// RemoveEphemeralFromWriteOnly gets the value and for the attributes that are
|
||||
// configured as write-only removes the marks.Ephemeral mark.
|
||||
// Write-only arguments are only available in managed resources.
|
||||
// Write-only arguments are the only managed resource's attribute type
|
||||
// that can reference ephemeral values.
|
||||
// Also, the provider framework sdk is responsible with nullify these attributes
|
||||
// before returning back to OpenTofu.
|
||||
//
|
||||
// Therefore, before writing the changes/state of a managed resource to its store,
|
||||
// we want to be sure that the nil value of the attribute is not marked as ephemeral
|
||||
// in case it got its value from evaluating an expression where an ephemeral value has
|
||||
// been involved.
|
||||
func (b *Block) RemoveEphemeralFromWriteOnly(v cty.Value) cty.Value {
|
||||
unmarkedV, valMarks := v.UnmarkDeepWithPaths()
|
||||
for _, pathMark := range valMarks {
|
||||
if _, ok := pathMark.Marks[marks.Ephemeral]; !ok {
|
||||
continue
|
||||
}
|
||||
attr := b.AttributeByPath(pathMark.Path)
|
||||
if attr == nil {
|
||||
continue
|
||||
}
|
||||
if !attr.WriteOnly {
|
||||
continue
|
||||
}
|
||||
delete(pathMark.Marks, marks.Ephemeral)
|
||||
}
|
||||
return unmarkedV.MarkWithPaths(valMarks)
|
||||
}
|
||||
|
||||
@@ -230,5 +230,4 @@ func TestObject_AttributeByPath(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,6 +38,11 @@ type Block struct {
|
||||
DescriptionKind StringKind
|
||||
|
||||
Deprecated bool
|
||||
|
||||
// Ephemeral is a flag indicating that this is an ephemeral block marking it as an "ephemeral context".
|
||||
// There are multiple places where this is set to "true". Generally speaking,
|
||||
// any Block that is meant to accept ephemeral values should have this set as "true".
|
||||
Ephemeral bool
|
||||
}
|
||||
|
||||
// Attribute represents a configuration attribute, within a block.
|
||||
@@ -94,6 +99,8 @@ type Attribute struct {
|
||||
Sensitive bool
|
||||
|
||||
Deprecated bool
|
||||
|
||||
WriteOnly bool
|
||||
}
|
||||
|
||||
// Object represents the embedding of a structural object inside an Attribute.
|
||||
|
||||
@@ -50,8 +50,9 @@ type Module struct {
|
||||
|
||||
ModuleCalls map[string]*ModuleCall
|
||||
|
||||
ManagedResources map[string]*Resource
|
||||
DataResources map[string]*Resource
|
||||
ManagedResources map[string]*Resource
|
||||
DataResources map[string]*Resource
|
||||
EphemeralResources map[string]*Resource
|
||||
|
||||
Moved []*Moved
|
||||
Import []*Import
|
||||
@@ -105,8 +106,9 @@ type File struct {
|
||||
|
||||
ModuleCalls []*ModuleCall
|
||||
|
||||
ManagedResources []*Resource
|
||||
DataResources []*Resource
|
||||
ManagedResources []*Resource
|
||||
DataResources []*Resource
|
||||
EphemeralResources []*Resource
|
||||
|
||||
Moved []*Moved
|
||||
Import []*Import
|
||||
@@ -178,6 +180,7 @@ func NewModule(primaryFiles, overrideFiles []*File, call StaticModuleCall, sourc
|
||||
ModuleCalls: map[string]*ModuleCall{},
|
||||
ManagedResources: map[string]*Resource{},
|
||||
DataResources: map[string]*Resource{},
|
||||
EphemeralResources: map[string]*Resource{},
|
||||
Checks: map[string]*Check{},
|
||||
ProviderMetas: map[addrs.Provider]*ProviderMeta{},
|
||||
Tests: map[string]*TestFile{},
|
||||
@@ -273,6 +276,8 @@ func (m *Module) ResourceByAddr(addr addrs.Resource) *Resource {
|
||||
return m.ManagedResources[key]
|
||||
case addrs.DataResourceMode:
|
||||
return m.DataResources[key]
|
||||
case addrs.EphemeralResourceMode:
|
||||
return m.EphemeralResources[key]
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@@ -466,6 +471,35 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics {
|
||||
m.DataResources[key] = r
|
||||
}
|
||||
|
||||
for _, r := range file.EphemeralResources {
|
||||
key := r.moduleUniqueKey()
|
||||
if existing, exists := m.EphemeralResources[key]; exists {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Duplicate ephemeral resource %q configuration", existing.Type),
|
||||
Detail: fmt.Sprintf("A %s ephemeral resource named %q was already declared at %s. Resource names must be unique per type in each module.", existing.Type, existing.Name, existing.DeclRange),
|
||||
Subject: &r.DeclRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
m.EphemeralResources[key] = r
|
||||
|
||||
// set the provider FQN for the resource
|
||||
if r.ProviderConfigRef != nil {
|
||||
r.Provider = m.ProviderForLocalConfig(r.ProviderConfigAddr())
|
||||
} else {
|
||||
// an invalid resource name (for e.g. "null resource" instead of
|
||||
// "null_resource") can cause a panic down the line in addrs:
|
||||
// https://github.com/hashicorp/terraform/issues/25560
|
||||
implied, err := addrs.ParseProviderPart(r.Addr().ImpliedProvider())
|
||||
if err == nil {
|
||||
r.Provider = m.ImpliedProviderForUnqualifiedType(implied)
|
||||
}
|
||||
// We don't return a diagnostic because the invalid resource name
|
||||
// will already have been caught.
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range file.Checks {
|
||||
if c.DataResource != nil {
|
||||
key := c.DataResource.moduleUniqueKey()
|
||||
@@ -736,6 +770,22 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics {
|
||||
diags = append(diags, mergeDiags...)
|
||||
}
|
||||
|
||||
for _, r := range file.EphemeralResources {
|
||||
key := r.moduleUniqueKey()
|
||||
existing, exists := m.EphemeralResources[key]
|
||||
if !exists {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Missing ephemeral resource to override",
|
||||
Detail: fmt.Sprintf("There is no %s ephemeral resource named %q. An override file can only override an ephemeral block defined in a primary configuration file.", r.Type, r.Name),
|
||||
Subject: &r.DeclRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
mergeDiags := existing.merge(r, m.ProviderRequirements.RequiredProviders)
|
||||
diags = append(diags, mergeDiags...)
|
||||
}
|
||||
|
||||
for _, m := range file.Moved {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
|
||||
@@ -54,6 +54,10 @@ func (v *Variable) merge(ov *Variable) hcl.Diagnostics {
|
||||
if ov.Deprecated != "" {
|
||||
v.Deprecated = ov.Deprecated
|
||||
}
|
||||
if ov.EphemeralSet {
|
||||
v.EphemeralSet = ov.EphemeralSet
|
||||
v.Ephemeral = ov.Ephemeral
|
||||
}
|
||||
if ov.Default != cty.NilVal {
|
||||
v.Default = ov.Default
|
||||
}
|
||||
@@ -156,6 +160,10 @@ func (o *Output) merge(oo *Output) hcl.Diagnostics {
|
||||
if oo.Deprecated != "" {
|
||||
o.Deprecated = oo.Deprecated
|
||||
}
|
||||
if oo.EphemeralSet {
|
||||
o.EphemeralSet = oo.EphemeralSet
|
||||
o.Ephemeral = oo.Ephemeral
|
||||
}
|
||||
|
||||
// We don't allow depends_on to be overridden because that is likely to
|
||||
// cause confusing misbehavior.
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
@@ -33,6 +34,8 @@ func TestModuleOverrideVariable(t *testing.T) {
|
||||
Deprecated: "b_override deprecated",
|
||||
Nullable: false,
|
||||
NullableSet: true,
|
||||
Ephemeral: false,
|
||||
EphemeralSet: true,
|
||||
Type: cty.String,
|
||||
ConstraintType: cty.String,
|
||||
ParsingMode: VariableParseLiteral,
|
||||
@@ -79,6 +82,125 @@ func TestModuleOverrideVariable(t *testing.T) {
|
||||
assertResultDeepEqual(t, got, want)
|
||||
}
|
||||
|
||||
func TestModuleOverrideOutput(t *testing.T) {
|
||||
mod, diags := testModuleFromDir("testdata/valid-modules/override-output")
|
||||
assertNoDiagnostics(t, diags)
|
||||
if mod == nil {
|
||||
t.Fatalf("module is nil")
|
||||
}
|
||||
|
||||
got := mod.Outputs
|
||||
want := map[string]*Output{
|
||||
"fully_overridden": {
|
||||
Name: "fully_overridden",
|
||||
Description: "b_override description",
|
||||
DescriptionSet: true,
|
||||
Expr: &hclsyntax.TemplateExpr{
|
||||
Parts: []hclsyntax.Expression{
|
||||
&hclsyntax.LiteralValueExpr{
|
||||
Val: cty.StringVal("b_override"),
|
||||
SrcRange: hcl.Range{
|
||||
Filename: "testdata/valid-modules/override-output/b_override.tf",
|
||||
Start: hcl.Pos{
|
||||
Line: 2,
|
||||
Column: 12,
|
||||
Byte: 39,
|
||||
},
|
||||
End: hcl.Pos{
|
||||
Line: 2,
|
||||
Column: 22,
|
||||
Byte: 49,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SrcRange: hcl.Range{
|
||||
Filename: "testdata/valid-modules/override-output/b_override.tf",
|
||||
Start: hcl.Pos{
|
||||
Line: 2,
|
||||
Column: 11,
|
||||
Byte: 38,
|
||||
},
|
||||
End: hcl.Pos{
|
||||
Line: 2,
|
||||
Column: 23,
|
||||
Byte: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
Deprecated: "b_override deprecated",
|
||||
Ephemeral: false,
|
||||
EphemeralSet: true,
|
||||
DeclRange: hcl.Range{
|
||||
Filename: filepath.FromSlash("testdata/valid-modules/override-output/primary.tf"),
|
||||
Start: hcl.Pos{
|
||||
Line: 1,
|
||||
Column: 1,
|
||||
Byte: 0,
|
||||
},
|
||||
End: hcl.Pos{
|
||||
Line: 1,
|
||||
Column: 26,
|
||||
Byte: 25,
|
||||
},
|
||||
},
|
||||
},
|
||||
"partially_overridden": {
|
||||
Name: "partially_overridden",
|
||||
Description: "base description",
|
||||
DescriptionSet: true,
|
||||
Expr: &hclsyntax.TemplateExpr{
|
||||
Parts: []hclsyntax.Expression{
|
||||
&hclsyntax.LiteralValueExpr{
|
||||
Val: cty.StringVal("b_override partial"),
|
||||
SrcRange: hcl.Range{
|
||||
Filename: "testdata/valid-modules/override-output/b_override.tf",
|
||||
Start: hcl.Pos{
|
||||
Line: 9,
|
||||
Column: 12,
|
||||
Byte: 197,
|
||||
},
|
||||
End: hcl.Pos{
|
||||
Line: 9,
|
||||
Column: 30,
|
||||
Byte: 215,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SrcRange: hcl.Range{
|
||||
Filename: "testdata/valid-modules/override-output/b_override.tf",
|
||||
Start: hcl.Pos{
|
||||
Line: 9,
|
||||
Column: 11,
|
||||
Byte: 196,
|
||||
},
|
||||
End: hcl.Pos{
|
||||
Line: 9,
|
||||
Column: 31,
|
||||
Byte: 216,
|
||||
},
|
||||
},
|
||||
},
|
||||
Deprecated: "b_override deprecated",
|
||||
DeclRange: hcl.Range{
|
||||
Filename: filepath.FromSlash("testdata/valid-modules/override-output/primary.tf"),
|
||||
Start: hcl.Pos{
|
||||
Line: 6,
|
||||
Column: 1,
|
||||
Byte: 83,
|
||||
},
|
||||
End: hcl.Pos{
|
||||
Line: 6,
|
||||
Column: 30,
|
||||
Byte: 112,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
assertResultDeepEqual(t, got, want)
|
||||
}
|
||||
|
||||
func TestModuleOverrideModule(t *testing.T) {
|
||||
mod, diags := testModuleFromDir("testdata/valid-modules/override-module")
|
||||
assertNoDiagnostics(t, diags)
|
||||
|
||||
@@ -69,6 +69,15 @@ func TestNewModule_resource_providers(t *testing.T) {
|
||||
wantBar := addrs.NewProvider(addrs.DefaultProviderRegistryHost, "bar", "test")
|
||||
|
||||
// root module
|
||||
if got, want := len(cfg.Module.ManagedResources), 2; got != want {
|
||||
t.Fatalf("expected to have %d managed resources in the root module but got %d", want, got)
|
||||
}
|
||||
if got, want := len(cfg.Module.DataResources), 1; got != want {
|
||||
t.Fatalf("expected to have %d data sources in the root module but got %d", want, got)
|
||||
}
|
||||
if got, want := len(cfg.Module.EphemeralResources), 2; got != want {
|
||||
t.Fatalf("expected to have %d ephemeral resources in the root module but got %d", want, got)
|
||||
}
|
||||
if !cfg.Module.ManagedResources["test_instance.explicit"].Provider.Equals(wantFoo) {
|
||||
t.Fatalf("wrong provider for \"test_instance.explicit\"\ngot: %s\nwant: %s",
|
||||
cfg.Module.ManagedResources["test_instance.explicit"].Provider,
|
||||
@@ -90,8 +99,31 @@ func TestNewModule_resource_providers(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
// ephemeral resources test
|
||||
if !cfg.Module.EphemeralResources["ephemeral.test_ephemeral.explicit"].Provider.Equals(wantFoo) {
|
||||
t.Fatalf("wrong provider for \"test_ephemeral.explicit\"\ngot: %s\nwant: %s",
|
||||
cfg.Module.EphemeralResources["test_ephemeral.explicit"].Provider,
|
||||
wantFoo,
|
||||
)
|
||||
}
|
||||
if !cfg.Module.EphemeralResources["ephemeral.test_ephemeral.implicit"].Provider.Equals(wantImplicit) {
|
||||
t.Fatalf("wrong provider for \"test_ephemeral.implicit\"\ngot: %s\nwant: %s",
|
||||
cfg.Module.EphemeralResources["test_instance.implicit"].Provider,
|
||||
wantImplicit,
|
||||
)
|
||||
}
|
||||
|
||||
// child module
|
||||
cm := cfg.Children["child"].Module
|
||||
if got, want := len(cm.ManagedResources), 3; got != want {
|
||||
t.Fatalf("expected to have %d managed resources in the child module but got %d", want, got)
|
||||
}
|
||||
if got, want := len(cm.DataResources), 0; got != want {
|
||||
t.Fatalf("expected to have %d data sources in the child module but got %d", want, got)
|
||||
}
|
||||
if got, want := len(cm.EphemeralResources), 2; got != want {
|
||||
t.Fatalf("expected to have %d ephemeral resources in the child module but got %d", want, got)
|
||||
}
|
||||
if !cm.ManagedResources["test_instance.explicit"].Provider.Equals(wantBar) {
|
||||
t.Fatalf("wrong provider for \"module.child.test_instance.explicit\"\ngot: %s\nwant: %s",
|
||||
cfg.Module.ManagedResources["test_instance.explicit"].Provider,
|
||||
@@ -104,6 +136,19 @@ func TestNewModule_resource_providers(t *testing.T) {
|
||||
wantImplicit,
|
||||
)
|
||||
}
|
||||
// ephemeral
|
||||
if !cm.EphemeralResources["ephemeral.test_ephemeral.other_explicit"].Provider.Equals(wantFoo) {
|
||||
t.Fatalf("wrong provider for \"module.child.ephemeral.test_ephemeral.other_explicit\"\ngot: %s\nwant: %s",
|
||||
cfg.Module.EphemeralResources["ephemeral.test_ephemeral.other_explicit"].Provider,
|
||||
wantFoo,
|
||||
)
|
||||
}
|
||||
if !cm.EphemeralResources["ephemeral.test_ephemeral.other_implicit"].Provider.Equals(wantImplicit) {
|
||||
t.Fatalf("wrong provider for \"module.child.ephemeral.test_ephemeral.other_implicit\"\ngot: %s\nwant: %s",
|
||||
cfg.Module.EphemeralResources["ephemeral.test_ephemeral.other_implicit"].Provider,
|
||||
wantFoo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderForLocalConfig(t *testing.T) {
|
||||
@@ -291,15 +336,21 @@ func TestModule_implied_provider(t *testing.T) {
|
||||
}{
|
||||
{"foo_resource.a", foo},
|
||||
{"data.foo_resource.b", foo},
|
||||
{"bar_resource.c", bar},
|
||||
{"data.bar_resource.d", bar},
|
||||
{"whatever_resource.e", whatever},
|
||||
{"data.whatever_resource.f", whatever},
|
||||
{"ephemeral.foo_resource.c", foo},
|
||||
{"bar_resource.d", bar},
|
||||
{"data.bar_resource.e", bar},
|
||||
{"ephemeral.bar_resource.f", bar},
|
||||
{"whatever_resource.g", whatever},
|
||||
{"data.whatever_resource.h", whatever},
|
||||
{"ephemeral.whatever_resource.i", whatever},
|
||||
}
|
||||
for _, test := range tests {
|
||||
resources := mod.ManagedResources
|
||||
if strings.HasPrefix(test.Address, "data.") {
|
||||
switch test.Address[:strings.Index(test.Address, ".")+1] {
|
||||
case "data.":
|
||||
resources = mod.DataResources
|
||||
case "ephemeral.":
|
||||
resources = mod.EphemeralResources
|
||||
}
|
||||
resource, exists := resources[test.Address]
|
||||
if !exists {
|
||||
@@ -443,3 +494,30 @@ func TestModule_cloud_duplicate_overrides(t *testing.T) {
|
||||
t.Fatalf("expected module error to contain %q\nerror was:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceByAddr(t *testing.T) {
|
||||
managedResource := &Resource{Mode: addrs.ManagedResourceMode, Name: "name", Type: "test_resource"}
|
||||
dataResource := &Resource{Mode: addrs.DataResourceMode, Name: "name", Type: "test_data"}
|
||||
ephemeralResource := &Resource{Mode: addrs.EphemeralResourceMode, Name: "name", Type: "test_ephemeral"}
|
||||
m := Module{
|
||||
ManagedResources: map[string]*Resource{
|
||||
managedResource.Addr().String(): managedResource,
|
||||
},
|
||||
DataResources: map[string]*Resource{
|
||||
dataResource.Addr().String(): dataResource,
|
||||
},
|
||||
EphemeralResources: map[string]*Resource{
|
||||
ephemeralResource.Addr().String(): ephemeralResource,
|
||||
},
|
||||
}
|
||||
if got, want := m.ResourceByAddr(managedResource.Addr()), managedResource; got != want {
|
||||
t.Fatalf("expected resource %+v but got %+v", want, got)
|
||||
}
|
||||
if got, want := m.ResourceByAddr(dataResource.Addr()), dataResource; got != want {
|
||||
t.Fatalf("expected resource %+v but got %+v", want, got)
|
||||
}
|
||||
if got, want := m.ResourceByAddr(ephemeralResource.Addr()), ephemeralResource; got != want {
|
||||
t.Fatalf("expected resource %+v but got %+v", want, got)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,6 +45,23 @@ func decodeMovedBlock(block *hcl.Block) (*Moved, hcl.Diagnostics) {
|
||||
moved.To = to
|
||||
}
|
||||
}
|
||||
// ensure that the moved block is not used against ephemeral resources since there is no use against those
|
||||
if !moved.From.SubjectAllowed() {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid \"moved\" address",
|
||||
Detail: "The resource referenced by the \"from\" attribute is not allowed to be moved.",
|
||||
Subject: &moved.DeclRange,
|
||||
})
|
||||
}
|
||||
if !moved.To.SubjectAllowed() {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid \"moved\" address",
|
||||
Detail: "The resource referenced by the \"to\" attribute is not allowed to be moved.",
|
||||
Subject: &moved.DeclRange,
|
||||
})
|
||||
}
|
||||
|
||||
// we can only move from a module to a module, resource to resource, etc.
|
||||
if !diags.HasErrors() {
|
||||
|
||||
@@ -30,6 +30,8 @@ func TestMovedBlock_decode(t *testing.T) {
|
||||
mod_foo_expr := hcltest.MockExprTraversalSrc("module.foo")
|
||||
mod_bar_expr := hcltest.MockExprTraversalSrc("module.bar")
|
||||
|
||||
ephemeral_ref_expr := hcltest.MockExprTraversalSrc("ephemeral.test.test")
|
||||
|
||||
tests := map[string]struct {
|
||||
input *hcl.Block
|
||||
want *Moved
|
||||
@@ -150,6 +152,30 @@ func TestMovedBlock_decode(t *testing.T) {
|
||||
},
|
||||
"Invalid \"moved\" addresses",
|
||||
},
|
||||
"error: ephemeral not allowed": {
|
||||
&hcl.Block{
|
||||
Type: "moved",
|
||||
Body: hcltest.MockBody(&hcl.BodyContent{
|
||||
Attributes: hcl.Attributes{
|
||||
"to": {
|
||||
Name: "to",
|
||||
Expr: ephemeral_ref_expr,
|
||||
},
|
||||
"from": {
|
||||
Name: "from",
|
||||
Expr: ephemeral_ref_expr,
|
||||
},
|
||||
},
|
||||
}),
|
||||
DefRange: blockRange,
|
||||
},
|
||||
&Moved{
|
||||
To: mustMoveEndpointFromExpr(ephemeral_ref_expr),
|
||||
From: mustMoveEndpointFromExpr(ephemeral_ref_expr),
|
||||
DeclRange: blockRange,
|
||||
},
|
||||
"Invalid \"moved\" address",
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
|
||||
@@ -40,9 +40,11 @@ type Variable struct {
|
||||
Validations []*CheckRule
|
||||
Sensitive bool
|
||||
Deprecated string
|
||||
Ephemeral bool
|
||||
|
||||
DescriptionSet bool
|
||||
SensitiveSet bool
|
||||
EphemeralSet bool
|
||||
|
||||
// Nullable indicates that null is a valid value for this variable. Setting
|
||||
// Nullable to false means that the module can expect this variable to
|
||||
@@ -138,6 +140,12 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
|
||||
}
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["ephemeral"]; exists {
|
||||
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Ephemeral)
|
||||
diags = append(diags, valDiags...)
|
||||
v.EphemeralSet = true
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["nullable"]; exists {
|
||||
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Nullable)
|
||||
diags = append(diags, valDiags...)
|
||||
@@ -418,11 +426,13 @@ type Output struct {
|
||||
DependsOn []hcl.Traversal
|
||||
Sensitive bool
|
||||
Deprecated string
|
||||
Ephemeral bool
|
||||
|
||||
Preconditions []*CheckRule
|
||||
|
||||
DescriptionSet bool
|
||||
SensitiveSet bool
|
||||
EphemeralSet bool
|
||||
|
||||
DeclRange hcl.Range
|
||||
|
||||
@@ -490,6 +500,12 @@ func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostic
|
||||
}
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["ephemeral"]; exists {
|
||||
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Ephemeral)
|
||||
diags = append(diags, valDiags...)
|
||||
o.EphemeralSet = true
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["depends_on"]; exists {
|
||||
deps, depsDiags := decodeDependsOn(attr)
|
||||
diags = append(diags, depsDiags...)
|
||||
@@ -523,6 +539,17 @@ func (o *Output) Addr() addrs.OutputValue {
|
||||
return addrs.OutputValue{Name: o.Name}
|
||||
}
|
||||
|
||||
// UsageRange returns the location where the output value is configured, but if the expression is not configured
|
||||
// then it returns the output definition location.
|
||||
// Useful for generating diagnostics.
|
||||
func (o *Output) UsageRange() hcl.Range {
|
||||
subj := o.DeclRange
|
||||
if o.Expr != nil {
|
||||
subj = o.Expr.Range()
|
||||
}
|
||||
return subj
|
||||
}
|
||||
|
||||
// Local represents a single entry from a "locals" block in a module or file.
|
||||
// The "locals" block itself is not represented, because it serves only to
|
||||
// provide context for us to interpret its contents.
|
||||
@@ -581,6 +608,9 @@ var variableBlockSchema = &hcl.BodySchema{
|
||||
{
|
||||
Name: "sensitive",
|
||||
},
|
||||
{
|
||||
Name: "ephemeral",
|
||||
},
|
||||
{
|
||||
Name: "deprecated",
|
||||
},
|
||||
@@ -610,6 +640,9 @@ var outputBlockSchema = &hcl.BodySchema{
|
||||
{
|
||||
Name: "sensitive",
|
||||
},
|
||||
{
|
||||
Name: "ephemeral",
|
||||
},
|
||||
{
|
||||
Name: "deprecated",
|
||||
},
|
||||
|
||||
@@ -183,6 +183,13 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost
|
||||
file.DataResources = append(file.DataResources, cfg)
|
||||
}
|
||||
|
||||
case "ephemeral":
|
||||
cfg, cfgDiags := decodeEphemeralBlock(block, override)
|
||||
diags = append(diags, cfgDiags...)
|
||||
if cfg != nil {
|
||||
file.EphemeralResources = append(file.EphemeralResources, cfg)
|
||||
}
|
||||
|
||||
case "moved":
|
||||
cfg, cfgDiags := decodeMovedBlock(block)
|
||||
diags = append(diags, cfgDiags...)
|
||||
@@ -298,6 +305,10 @@ var configFileSchema = &hcl.BodySchema{
|
||||
Type: "data",
|
||||
LabelNames: []string{"type", "name"},
|
||||
},
|
||||
{
|
||||
Type: "ephemeral",
|
||||
LabelNames: []string{"type", "name"},
|
||||
},
|
||||
{
|
||||
Type: "moved",
|
||||
},
|
||||
|
||||
@@ -458,6 +458,7 @@ func validateProviderConfigs(parentCall *ModuleCall, cfg *Config, noProviderConf
|
||||
}
|
||||
checkImpliedProviderNames(mod.ManagedResources)
|
||||
checkImpliedProviderNames(mod.DataResources)
|
||||
checkImpliedProviderNames(mod.EphemeralResources)
|
||||
|
||||
// collect providers passed from the parent
|
||||
if parentCall != nil {
|
||||
@@ -523,6 +524,7 @@ func validateProviderConfigs(parentCall *ModuleCall, cfg *Config, noProviderConf
|
||||
}
|
||||
checkProviderKeys(mod.ManagedResources)
|
||||
checkProviderKeys(mod.DataResources)
|
||||
checkProviderKeys(mod.EphemeralResources)
|
||||
|
||||
// Verify that any module calls only refer to named providers, and that
|
||||
// those providers will have a configuration at runtime. This way we can
|
||||
|
||||
@@ -547,6 +547,176 @@ func decodeDataBlock(block *hcl.Block, override, nested bool) (*Resource, hcl.Di
|
||||
return r, diags
|
||||
}
|
||||
|
||||
func decodeEphemeralBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
r := &Resource{
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Type: block.Labels[0],
|
||||
Name: block.Labels[1],
|
||||
DeclRange: block.DefRange,
|
||||
TypeRange: block.LabelRanges[0],
|
||||
}
|
||||
|
||||
content, remain, moreDiags := block.Body.PartialContent(ResourceBlockSchema)
|
||||
diags = append(diags, moreDiags...)
|
||||
r.Config = remain
|
||||
|
||||
if !hclsyntax.ValidIdentifier(r.Type) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid ephemeral resource type name",
|
||||
Detail: badIdentifierDetail,
|
||||
Subject: &block.LabelRanges[0],
|
||||
})
|
||||
}
|
||||
if !hclsyntax.ValidIdentifier(r.Name) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid ephemeral resource name",
|
||||
Detail: badIdentifierDetail,
|
||||
Subject: &block.LabelRanges[1],
|
||||
})
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["count"]; exists {
|
||||
r.Count = attr.Expr
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["for_each"]; exists {
|
||||
r.ForEach = attr.Expr
|
||||
// Cannot have count and for_each on the same resource block
|
||||
if r.Count != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid combination of "count" and "for_each"`,
|
||||
Detail: `The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be created.`,
|
||||
Subject: &attr.NameRange,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["provider"]; exists {
|
||||
var providerDiags hcl.Diagnostics
|
||||
r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider")
|
||||
diags = append(diags, providerDiags...)
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["depends_on"]; exists {
|
||||
deps, depsDiags := decodeDependsOn(attr)
|
||||
diags = append(diags, depsDiags...)
|
||||
r.DependsOn = append(r.DependsOn, deps...)
|
||||
}
|
||||
|
||||
invalidEphemeralLifecycleAttributeDiag := func(field string) *hcl.Diagnostic {
|
||||
return &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid lifecycle configuration for ephemeral resource",
|
||||
Detail: fmt.Sprintf("The lifecycle argument %q cannot be used in ephemeral resources. This is meant to be used strictly in \"resource\" blocks.", field),
|
||||
Subject: &block.DefRange,
|
||||
}
|
||||
}
|
||||
invalidEphemeralBlockDiag := func(field string) *hcl.Diagnostic {
|
||||
return &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid configuration block for ephemeral resource",
|
||||
Detail: fmt.Sprintf("The block type %q cannot be used in ephemeral resources. This is meant to be used strictly in \"resource\" blocks.", field),
|
||||
Subject: &block.DefRange,
|
||||
}
|
||||
}
|
||||
var seenLifecycle *hcl.Block
|
||||
var seenEscapeBlock *hcl.Block
|
||||
for _, block := range content.Blocks {
|
||||
switch block.Type {
|
||||
case "lifecycle":
|
||||
if seenLifecycle != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Duplicate lifecycle block",
|
||||
Detail: fmt.Sprintf("This ephemeral resource already has a lifecycle block at %s.", seenLifecycle.DefRange),
|
||||
Subject: &block.DefRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
seenLifecycle = block
|
||||
|
||||
lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema)
|
||||
diags = append(diags, lcDiags...)
|
||||
|
||||
if _, exists := lcContent.Attributes["create_before_destroy"]; exists {
|
||||
diags = append(diags, invalidEphemeralLifecycleAttributeDiag("create_before_destroy"))
|
||||
}
|
||||
if _, exists := lcContent.Attributes["prevent_destroy"]; exists {
|
||||
diags = append(diags, invalidEphemeralLifecycleAttributeDiag("prevent_destroy"))
|
||||
}
|
||||
if _, exists := lcContent.Attributes["replace_triggered_by"]; exists {
|
||||
diags = append(diags, invalidEphemeralLifecycleAttributeDiag("replace_triggered_by"))
|
||||
}
|
||||
if _, exists := lcContent.Attributes["ignore_changes"]; exists {
|
||||
diags = append(diags, invalidEphemeralLifecycleAttributeDiag("ignore_changes"))
|
||||
}
|
||||
for _, block := range lcContent.Blocks {
|
||||
switch block.Type {
|
||||
case "precondition", "postcondition":
|
||||
cr, moreDiags := decodeCheckRuleBlock(block, override)
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
moreDiags = cr.validateSelfReferences(block.Type, r.Addr())
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
switch block.Type {
|
||||
case "precondition":
|
||||
r.Preconditions = append(r.Preconditions, cr)
|
||||
case "postcondition":
|
||||
r.Postconditions = append(r.Postconditions, cr)
|
||||
}
|
||||
default:
|
||||
// The cases above should be exhaustive for all block types
|
||||
// defined in the lifecycle schema, so this shouldn't happen.
|
||||
panic(fmt.Sprintf("unexpected lifecycle sub-block type %q", block.Type))
|
||||
}
|
||||
}
|
||||
|
||||
case "connection":
|
||||
diags = append(diags, invalidEphemeralBlockDiag("connection"))
|
||||
|
||||
case "provisioner":
|
||||
diags = append(diags, invalidEphemeralBlockDiag("provisioner"))
|
||||
|
||||
case "_":
|
||||
if seenEscapeBlock != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Duplicate escaping block",
|
||||
Detail: fmt.Sprintf(
|
||||
"The special block type \"_\" can be used to force particular arguments to be interpreted as resource-type-specific rather than as meta-arguments, but each resource block can have only one such block. The first escaping block was at %s.",
|
||||
seenEscapeBlock.DefRange,
|
||||
),
|
||||
Subject: &block.DefRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
seenEscapeBlock = block
|
||||
|
||||
// When there's an escaping block its content merges with the
|
||||
// existing config we extracted earlier, so later decoding
|
||||
// will see a blend of both.
|
||||
r.Config = hcl.MergeBodies([]hcl.Body{r.Config, block.Body})
|
||||
|
||||
default:
|
||||
// Any other block types are ones we've reserved for future use,
|
||||
// so they get a generic message.
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Reserved block type name in ephemeral block",
|
||||
Detail: fmt.Sprintf("The block type name %q is reserved for use by OpenTofu in a future version.", block.Type),
|
||||
Subject: &block.TypeRange,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return r, diags
|
||||
}
|
||||
|
||||
// decodeReplaceTriggeredBy decodes and does basic validation of the
|
||||
// replace_triggered_by expressions, ensuring they only contains references to
|
||||
// a single resource, and the only extra variables are count.index or each.key.
|
||||
|
||||
@@ -260,6 +260,7 @@ type TestRunOptions struct {
|
||||
const (
|
||||
blockNameOverrideResource = "override_resource"
|
||||
blockNameOverrideData = "override_data"
|
||||
//blockNameOverrideEphemeral = "override_ephemeral" // TODO ephemeral uncomment this when testing support will be added for ephemerals
|
||||
)
|
||||
|
||||
// OverrideResource contains information about a resource or data block to be overridden.
|
||||
|
||||
40
internal/configs/testdata/ephemeral-blocks/main.tf
vendored
Normal file
40
internal/configs/testdata/ephemeral-blocks/main.tf
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
provider test {
|
||||
alias = "name"
|
||||
}
|
||||
|
||||
ephemeral "test_resource" "foo" {
|
||||
for_each = toset(["a"])
|
||||
}
|
||||
|
||||
ephemeral "test_resource" "bar" {
|
||||
depends_on = [
|
||||
test_resource.foo["a"]
|
||||
]
|
||||
provider = test.name
|
||||
count = 1
|
||||
attribute = "test value"
|
||||
attribute2 = "test value"
|
||||
|
||||
connection {
|
||||
// connection blocks are not allowed so we are expecting an error on this
|
||||
}
|
||||
provisioner "local-exec" {
|
||||
// provisioner blocks are not allowed so we are expecting an error on this
|
||||
}
|
||||
lifecycle {
|
||||
// standard attributes in the lifecycle block are not allowed so we are expecting 4 errors on this
|
||||
create_before_destroy = true
|
||||
prevent_destroy = true
|
||||
replace_triggered_by = true
|
||||
ignore_changes = true
|
||||
// precondition and postconditions are allowed in ephemeral resources
|
||||
precondition {
|
||||
condition = ephemeral.test_resource.foo.id == ""
|
||||
error_message = "precondition error"
|
||||
}
|
||||
postcondition {
|
||||
condition = ephemeral.test_resource.foo.id == ""
|
||||
error_message = "postcondition error"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,10 @@ resource "null_resource" "foo" {
|
||||
|
||||
}
|
||||
|
||||
ephemeral "aws_secret" "bar" {}
|
||||
|
||||
data "aws_s3_object" "baz" {}
|
||||
|
||||
import {
|
||||
id = "directory/filename"
|
||||
to = local_file.foo
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// These resources map to the configured "foo" provider"
|
||||
resource foo_resource "a" {}
|
||||
data foo_resource "b" {}
|
||||
ephemeral foo_resource "c" {}
|
||||
|
||||
// These resources map to a default "hashicorp/bar" provider
|
||||
resource bar_resource "c" {}
|
||||
data bar_resource "d" {}
|
||||
resource bar_resource "d" {}
|
||||
data bar_resource "e" {}
|
||||
ephemeral bar_resource "f" {}
|
||||
|
||||
// These resources map to the configured "whatever" provider, which has FQN
|
||||
// "acme/something".
|
||||
resource whatever_resource "e" {}
|
||||
data whatever_resource "f" {}
|
||||
resource whatever_resource "g" {}
|
||||
data whatever_resource "h" {}
|
||||
ephemeral whatever_resource "i" {}
|
||||
|
||||
@@ -23,3 +23,12 @@ resource "test_instance" "implicit" {
|
||||
resource "test_instance" "other" {
|
||||
provider = foo-test.other
|
||||
}
|
||||
|
||||
ephemeral "test_ephemeral" "other_explicit" {
|
||||
provider = foo-test.other
|
||||
}
|
||||
|
||||
ephemeral "test_ephemeral" "other_implicit" {
|
||||
// since the provider type name "test" does not match an entry in
|
||||
// required_providers, the default provider "test" should be used
|
||||
}
|
||||
|
||||
@@ -23,6 +23,15 @@ data "test_resource" "explicit" {
|
||||
provider = foo-test
|
||||
}
|
||||
|
||||
ephemeral "test_ephemeral" "explicit" {
|
||||
provider = foo-test
|
||||
}
|
||||
|
||||
ephemeral "test_ephemeral" "implicit" {
|
||||
// since the provider type name "test" does not match an entry in
|
||||
// required_providers, the default provider "test" should be used
|
||||
}
|
||||
|
||||
resource "test_instance" "implicit" {
|
||||
// since the provider type name "test" does not match an entry in
|
||||
// required_providers, the default provider "test" should be used
|
||||
|
||||
10
internal/configs/testdata/valid-modules/override-output/a_override.tf
vendored
Normal file
10
internal/configs/testdata/valid-modules/override-output/a_override.tf
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
output "fully_overridden" {
|
||||
value = "a_override"
|
||||
description = "a_override description"
|
||||
deprecated = "a_override deprecated"
|
||||
ephemeral = true
|
||||
}
|
||||
|
||||
output "partially_overridden" {
|
||||
value = "a_override partial"
|
||||
}
|
||||
11
internal/configs/testdata/valid-modules/override-output/b_override.tf
vendored
Normal file
11
internal/configs/testdata/valid-modules/override-output/b_override.tf
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
output "fully_overridden" {
|
||||
value = "b_override"
|
||||
description = "b_override description"
|
||||
deprecated = "b_override deprecated"
|
||||
ephemeral = false
|
||||
}
|
||||
|
||||
output "partially_overridden" {
|
||||
value = "b_override partial"
|
||||
deprecated = "b_override deprecated"
|
||||
}
|
||||
9
internal/configs/testdata/valid-modules/override-output/primary.tf
vendored
Normal file
9
internal/configs/testdata/valid-modules/override-output/primary.tf
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
output "fully_overridden" {
|
||||
value = "base"
|
||||
description = "base description"
|
||||
}
|
||||
|
||||
output "partially_overridden" {
|
||||
value = "base"
|
||||
description = "base description"
|
||||
}
|
||||
@@ -3,6 +3,7 @@ variable "fully_overridden" {
|
||||
description = "a_override description"
|
||||
deprecated = "a_override deprecated"
|
||||
type = string
|
||||
ephemeral = true
|
||||
}
|
||||
|
||||
variable "partially_overridden" {
|
||||
|
||||
@@ -4,6 +4,7 @@ variable "fully_overridden" {
|
||||
description = "b_override description"
|
||||
deprecated = "b_override deprecated"
|
||||
type = string
|
||||
ephemeral = false
|
||||
}
|
||||
|
||||
variable "partially_overridden" {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
"github.com/zclconf/go-cty/cty/msgpack"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// New wraps a providers.Interface to implement a grpc ProviderServer.
|
||||
@@ -33,8 +34,9 @@ type provider struct {
|
||||
|
||||
func (p *provider) GetSchema(_ context.Context, req *tfplugin5.GetProviderSchema_Request) (*tfplugin5.GetProviderSchema_Response, error) {
|
||||
resp := &tfplugin5.GetProviderSchema_Response{
|
||||
ResourceSchemas: make(map[string]*tfplugin5.Schema),
|
||||
DataSourceSchemas: make(map[string]*tfplugin5.Schema),
|
||||
ResourceSchemas: make(map[string]*tfplugin5.Schema),
|
||||
DataSourceSchemas: make(map[string]*tfplugin5.Schema),
|
||||
EphemeralResourceSchemas: make(map[string]*tfplugin5.Schema),
|
||||
}
|
||||
|
||||
resp.Provider = &tfplugin5.Schema{
|
||||
@@ -63,6 +65,12 @@ func (p *provider) GetSchema(_ context.Context, req *tfplugin5.GetProviderSchema
|
||||
Block: convert.ConfigSchemaToProto(dat.Block),
|
||||
}
|
||||
}
|
||||
for typ, dat := range p.schema.EphemeralResources {
|
||||
resp.EphemeralResourceSchemas[typ] = &tfplugin5.Schema{
|
||||
Version: dat.Version,
|
||||
Block: convert.ConfigSchemaToProto(dat.Block),
|
||||
}
|
||||
}
|
||||
|
||||
resp.ServerCapabilities = &tfplugin5.ServerCapabilities{
|
||||
PlanDestroy: p.schema.ServerCapabilities.PlanDestroy,
|
||||
@@ -131,6 +139,26 @@ func (p *provider) ValidateDataSourceConfig(ctx context.Context, req *tfplugin5.
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ValidateEphemeralResourceConfig implements tfplugin5.ProviderServer.
|
||||
func (p *provider) ValidateEphemeralResourceConfig(ctx context.Context, req *tfplugin5.ValidateEphemeralResourceConfig_Request) (*tfplugin5.ValidateEphemeralResourceConfig_Response, error) {
|
||||
resp := &tfplugin5.ValidateEphemeralResourceConfig_Response{}
|
||||
ty := p.schema.EphemeralResources[req.TypeName].Block.ImpliedType()
|
||||
|
||||
configVal, err := decodeDynamicValue(req.Config, ty)
|
||||
if err != nil {
|
||||
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
validateResp := p.provider.ValidateEphemeralConfig(ctx, providers.ValidateEphemeralConfigRequest{
|
||||
TypeName: req.TypeName,
|
||||
Config: configVal,
|
||||
})
|
||||
|
||||
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, validateResp.Diagnostics)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (p *provider) UpgradeResourceState(ctx context.Context, req *tfplugin5.UpgradeResourceState_Request) (*tfplugin5.UpgradeResourceState_Response, error) {
|
||||
resp := &tfplugin5.UpgradeResourceState_Response{}
|
||||
ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType()
|
||||
@@ -392,24 +420,69 @@ func (p *provider) ReadDataSource(ctx context.Context, req *tfplugin5.ReadDataSo
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CloseEphemeralResource implements tfplugin5.ProviderServer.
|
||||
func (p *provider) CloseEphemeralResource(context.Context, *tfplugin5.CloseEphemeralResource_Request) (*tfplugin5.CloseEphemeralResource_Response, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// OpenEphemeralResource implements tfplugin5.ProviderServer.
|
||||
func (p *provider) OpenEphemeralResource(context.Context, *tfplugin5.OpenEphemeralResource_Request) (*tfplugin5.OpenEphemeralResource_Response, error) {
|
||||
panic("unimplemented")
|
||||
func (p *provider) OpenEphemeralResource(ctx context.Context, req *tfplugin5.OpenEphemeralResource_Request) (*tfplugin5.OpenEphemeralResource_Response, error) {
|
||||
resp := &tfplugin5.OpenEphemeralResource_Response{}
|
||||
ty := p.schema.EphemeralResources[req.TypeName].Block.ImpliedType()
|
||||
|
||||
configVal, err := decodeDynamicValue(req.Config, ty)
|
||||
if err != nil {
|
||||
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
openResp := p.provider.OpenEphemeralResource(ctx, providers.OpenEphemeralResourceRequest{
|
||||
TypeName: req.TypeName,
|
||||
Config: configVal,
|
||||
})
|
||||
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, openResp.Diagnostics)
|
||||
if openResp.Diagnostics.HasErrors() {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Result, err = encodeDynamicValue(openResp.Result, ty)
|
||||
if err != nil {
|
||||
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Private = openResp.Private
|
||||
if openResp.RenewAt != nil {
|
||||
resp.RenewAt = timestamppb.New(*openResp.RenewAt)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// RenewEphemeralResource implements tfplugin5.ProviderServer.
|
||||
func (p *provider) RenewEphemeralResource(context.Context, *tfplugin5.RenewEphemeralResource_Request) (*tfplugin5.RenewEphemeralResource_Response, error) {
|
||||
panic("unimplemented")
|
||||
func (p *provider) RenewEphemeralResource(ctx context.Context, req *tfplugin5.RenewEphemeralResource_Request) (*tfplugin5.RenewEphemeralResource_Response, error) {
|
||||
resp := &tfplugin5.RenewEphemeralResource_Response{}
|
||||
|
||||
renewResp := p.provider.RenewEphemeralResource(ctx, providers.RenewEphemeralResourceRequest{
|
||||
TypeName: req.TypeName,
|
||||
Private: req.Private,
|
||||
})
|
||||
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, renewResp.Diagnostics)
|
||||
if renewResp.Diagnostics.HasErrors() {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Private = renewResp.Private
|
||||
if renewResp.RenewAt != nil {
|
||||
resp.RenewAt = timestamppb.New(*renewResp.RenewAt)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ValidateEphemeralResourceConfig implements tfplugin5.ProviderServer.
|
||||
func (p *provider) ValidateEphemeralResourceConfig(context.Context, *tfplugin5.ValidateEphemeralResourceConfig_Request) (*tfplugin5.ValidateEphemeralResourceConfig_Response, error) {
|
||||
panic("unimplemented")
|
||||
// CloseEphemeralResource implements tfplugin5.ProviderServer.
|
||||
func (p *provider) CloseEphemeralResource(ctx context.Context, req *tfplugin5.CloseEphemeralResource_Request) (*tfplugin5.CloseEphemeralResource_Response, error) {
|
||||
resp := &tfplugin5.CloseEphemeralResource_Response{}
|
||||
|
||||
renewResp := p.provider.CloseEphemeralResource(ctx, providers.CloseEphemeralResourceRequest{
|
||||
TypeName: req.TypeName,
|
||||
Private: req.Private,
|
||||
})
|
||||
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, renewResp.Diagnostics)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (p *provider) Stop(ctx context.Context, _ *tfplugin5.Stop_Request) (*tfplugin5.Stop_Response, error) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
ctyjson "github.com/zclconf/go-cty/cty/json"
|
||||
"github.com/zclconf/go-cty/cty/msgpack"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// New wraps a providers.Interface to implement a grpc ProviderServer using
|
||||
@@ -33,8 +34,9 @@ type provider6 struct {
|
||||
|
||||
func (p *provider6) GetProviderSchema(_ context.Context, req *tfplugin6.GetProviderSchema_Request) (*tfplugin6.GetProviderSchema_Response, error) {
|
||||
resp := &tfplugin6.GetProviderSchema_Response{
|
||||
ResourceSchemas: make(map[string]*tfplugin6.Schema),
|
||||
DataSourceSchemas: make(map[string]*tfplugin6.Schema),
|
||||
ResourceSchemas: make(map[string]*tfplugin6.Schema),
|
||||
DataSourceSchemas: make(map[string]*tfplugin6.Schema),
|
||||
EphemeralResourceSchemas: make(map[string]*tfplugin6.Schema),
|
||||
}
|
||||
|
||||
resp.Provider = &tfplugin6.Schema{
|
||||
@@ -63,6 +65,12 @@ func (p *provider6) GetProviderSchema(_ context.Context, req *tfplugin6.GetProvi
|
||||
Block: convert.ConfigSchemaToProto(dat.Block),
|
||||
}
|
||||
}
|
||||
for typ, dat := range p.schema.EphemeralResources {
|
||||
resp.EphemeralResourceSchemas[typ] = &tfplugin6.Schema{
|
||||
Version: dat.Version,
|
||||
Block: convert.ConfigSchemaToProto(dat.Block),
|
||||
}
|
||||
}
|
||||
|
||||
resp.ServerCapabilities = &tfplugin6.ServerCapabilities{
|
||||
PlanDestroy: p.schema.ServerCapabilities.PlanDestroy,
|
||||
@@ -131,6 +139,26 @@ func (p *provider6) ValidateDataResourceConfig(ctx context.Context, req *tfplugi
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ValidateEphemeralResourceConfig implements tfplugin6.ProviderServer.
|
||||
func (p *provider6) ValidateEphemeralResourceConfig(ctx context.Context, req *tfplugin6.ValidateEphemeralResourceConfig_Request) (*tfplugin6.ValidateEphemeralResourceConfig_Response, error) {
|
||||
resp := &tfplugin6.ValidateEphemeralResourceConfig_Response{}
|
||||
ty := p.schema.EphemeralResources[req.TypeName].Block.ImpliedType()
|
||||
|
||||
configVal, err := decodeDynamicValue6(req.Config, ty)
|
||||
if err != nil {
|
||||
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
validateResp := p.provider.ValidateEphemeralConfig(ctx, providers.ValidateEphemeralConfigRequest{
|
||||
TypeName: req.TypeName,
|
||||
Config: configVal,
|
||||
})
|
||||
|
||||
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, validateResp.Diagnostics)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (p *provider6) UpgradeResourceState(ctx context.Context, req *tfplugin6.UpgradeResourceState_Request) (*tfplugin6.UpgradeResourceState_Response, error) {
|
||||
resp := &tfplugin6.UpgradeResourceState_Response{}
|
||||
ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType()
|
||||
@@ -392,24 +420,69 @@ func (p *provider6) ReadDataSource(ctx context.Context, req *tfplugin6.ReadDataS
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CloseEphemeralResource implements tfplugin6.ProviderServer.
|
||||
func (p *provider6) CloseEphemeralResource(context.Context, *tfplugin6.CloseEphemeralResource_Request) (*tfplugin6.CloseEphemeralResource_Response, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// OpenEphemeralResource implements tfplugin6.ProviderServer.
|
||||
func (p *provider6) OpenEphemeralResource(context.Context, *tfplugin6.OpenEphemeralResource_Request) (*tfplugin6.OpenEphemeralResource_Response, error) {
|
||||
panic("unimplemented")
|
||||
func (p *provider6) OpenEphemeralResource(ctx context.Context, req *tfplugin6.OpenEphemeralResource_Request) (*tfplugin6.OpenEphemeralResource_Response, error) {
|
||||
resp := &tfplugin6.OpenEphemeralResource_Response{}
|
||||
ty := p.schema.EphemeralResources[req.TypeName].Block.ImpliedType()
|
||||
|
||||
configVal, err := decodeDynamicValue6(req.Config, ty)
|
||||
if err != nil {
|
||||
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
openResp := p.provider.OpenEphemeralResource(ctx, providers.OpenEphemeralResourceRequest{
|
||||
TypeName: req.TypeName,
|
||||
Config: configVal,
|
||||
})
|
||||
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, openResp.Diagnostics)
|
||||
if openResp.Diagnostics.HasErrors() {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Result, err = encodeDynamicValue6(openResp.Result, ty)
|
||||
if err != nil {
|
||||
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Private = openResp.Private
|
||||
if openResp.RenewAt != nil {
|
||||
resp.RenewAt = timestamppb.New(*openResp.RenewAt)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// RenewEphemeralResource implements tfplugin6.ProviderServer.
|
||||
func (p *provider6) RenewEphemeralResource(context.Context, *tfplugin6.RenewEphemeralResource_Request) (*tfplugin6.RenewEphemeralResource_Response, error) {
|
||||
panic("unimplemented")
|
||||
func (p *provider6) RenewEphemeralResource(ctx context.Context, req *tfplugin6.RenewEphemeralResource_Request) (*tfplugin6.RenewEphemeralResource_Response, error) {
|
||||
resp := &tfplugin6.RenewEphemeralResource_Response{}
|
||||
|
||||
renewResp := p.provider.RenewEphemeralResource(ctx, providers.RenewEphemeralResourceRequest{
|
||||
TypeName: req.TypeName,
|
||||
Private: req.Private,
|
||||
})
|
||||
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, renewResp.Diagnostics)
|
||||
if renewResp.Diagnostics.HasErrors() {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Private = renewResp.Private
|
||||
if renewResp.RenewAt != nil {
|
||||
resp.RenewAt = timestamppb.New(*renewResp.RenewAt)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ValidateEphemeralResourceConfig implements tfplugin6.ProviderServer.
|
||||
func (p *provider6) ValidateEphemeralResourceConfig(context.Context, *tfplugin6.ValidateEphemeralResourceConfig_Request) (*tfplugin6.ValidateEphemeralResourceConfig_Response, error) {
|
||||
panic("unimplemented")
|
||||
// CloseEphemeralResource implements tfplugin6.ProviderServer.
|
||||
func (p *provider6) CloseEphemeralResource(ctx context.Context, req *tfplugin6.CloseEphemeralResource_Request) (*tfplugin6.CloseEphemeralResource_Response, error) {
|
||||
resp := &tfplugin6.CloseEphemeralResource_Response{}
|
||||
|
||||
renewResp := p.provider.CloseEphemeralResource(ctx, providers.CloseEphemeralResourceRequest{
|
||||
TypeName: req.TypeName,
|
||||
Private: req.Private,
|
||||
})
|
||||
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, renewResp.Diagnostics)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (p *provider6) StopProvider(ctx context.Context, _ *tfplugin6.StopProvider_Request) (*tfplugin6.StopProvider_Response, error) {
|
||||
|
||||
@@ -8,6 +8,7 @@ package lang
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"maps"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -16,16 +17,15 @@ import (
|
||||
"github.com/hashicorp/hcl/v2/ext/dynblock"
|
||||
"github.com/hashicorp/hcl/v2/hcldec"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||
"github.com/opentofu/opentofu/internal/instances"
|
||||
"github.com/opentofu/opentofu/internal/lang/blocktoattr"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
// ExpandBlock expands any "dynamic" blocks present in the given body. The
|
||||
@@ -80,6 +80,8 @@ func (s *Scope) EvalBlock(ctx context.Context, body hcl.Body, schema *configsche
|
||||
val, evalDiags := hcldec.Decode(body, spec, hclCtx)
|
||||
diags = diags.Append(enhanceFunctionDiags(evalDiags))
|
||||
|
||||
diags = diags.Append(validEphemeralReferences(schema, val))
|
||||
|
||||
val, depDiags := marks.ExtractDeprecationDiagnosticsWithBody(val, body)
|
||||
diags = diags.Append(depDiags)
|
||||
|
||||
@@ -398,35 +400,37 @@ func (s *Scope) evalContext(ctx context.Context, parent *hcl.EvalContext, refs [
|
||||
type evalVarBuilder struct {
|
||||
s *Scope
|
||||
|
||||
dataResources map[string]map[string]cty.Value
|
||||
managedResources map[string]map[string]cty.Value
|
||||
wholeModules map[string]cty.Value
|
||||
inputVariables map[string]cty.Value
|
||||
localValues map[string]cty.Value
|
||||
outputValues map[string]cty.Value
|
||||
pathAttrs map[string]cty.Value
|
||||
terraformAttrs map[string]cty.Value
|
||||
countAttrs map[string]cty.Value
|
||||
forEachAttrs map[string]cty.Value
|
||||
checkBlocks map[string]cty.Value
|
||||
self cty.Value
|
||||
dataResources map[string]map[string]cty.Value
|
||||
managedResources map[string]map[string]cty.Value
|
||||
ephemeralResources map[string]map[string]cty.Value
|
||||
wholeModules map[string]cty.Value
|
||||
inputVariables map[string]cty.Value
|
||||
localValues map[string]cty.Value
|
||||
outputValues map[string]cty.Value
|
||||
pathAttrs map[string]cty.Value
|
||||
terraformAttrs map[string]cty.Value
|
||||
countAttrs map[string]cty.Value
|
||||
forEachAttrs map[string]cty.Value
|
||||
checkBlocks map[string]cty.Value
|
||||
self cty.Value
|
||||
}
|
||||
|
||||
func (s *Scope) newEvalVarBuilder() *evalVarBuilder {
|
||||
return &evalVarBuilder{
|
||||
s: s,
|
||||
|
||||
dataResources: map[string]map[string]cty.Value{},
|
||||
managedResources: map[string]map[string]cty.Value{},
|
||||
wholeModules: map[string]cty.Value{},
|
||||
inputVariables: map[string]cty.Value{},
|
||||
localValues: map[string]cty.Value{},
|
||||
outputValues: map[string]cty.Value{},
|
||||
pathAttrs: map[string]cty.Value{},
|
||||
terraformAttrs: map[string]cty.Value{},
|
||||
countAttrs: map[string]cty.Value{},
|
||||
forEachAttrs: map[string]cty.Value{},
|
||||
checkBlocks: map[string]cty.Value{},
|
||||
dataResources: map[string]map[string]cty.Value{},
|
||||
ephemeralResources: map[string]map[string]cty.Value{},
|
||||
managedResources: map[string]map[string]cty.Value{},
|
||||
wholeModules: map[string]cty.Value{},
|
||||
inputVariables: map[string]cty.Value{},
|
||||
localValues: map[string]cty.Value{},
|
||||
outputValues: map[string]cty.Value{},
|
||||
pathAttrs: map[string]cty.Value{},
|
||||
terraformAttrs: map[string]cty.Value{},
|
||||
countAttrs: map[string]cty.Value{},
|
||||
forEachAttrs: map[string]cty.Value{},
|
||||
checkBlocks: map[string]cty.Value{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,6 +553,8 @@ func (b *evalVarBuilder) putResourceValue(ctx context.Context, res addrs.Resourc
|
||||
into = b.managedResources
|
||||
case addrs.DataResourceMode:
|
||||
into = b.dataResources
|
||||
case addrs.EphemeralResourceMode:
|
||||
into = b.ephemeralResources
|
||||
case addrs.InvalidResourceMode:
|
||||
panic("BUG: got invalid resource mode")
|
||||
default:
|
||||
@@ -577,6 +583,7 @@ func (b *evalVarBuilder) buildAllVariablesInto(vals map[string]cty.Value) {
|
||||
vals["resource"] = cty.ObjectVal(buildResourceObjects(b.managedResources))
|
||||
|
||||
vals["data"] = cty.ObjectVal(buildResourceObjects(b.dataResources))
|
||||
vals["ephemeral"] = cty.ObjectVal(buildResourceObjects(b.ephemeralResources))
|
||||
vals["module"] = cty.ObjectVal(b.wholeModules)
|
||||
vals["var"] = cty.ObjectVal(b.inputVariables)
|
||||
vals["local"] = cty.ObjectVal(b.localValues)
|
||||
@@ -619,3 +626,72 @@ func normalizeRefValue(val cty.Value, diags tfdiags.Diagnostics) (cty.Value, tfd
|
||||
}
|
||||
return val, diags
|
||||
}
|
||||
|
||||
// validEphemeralReferences is checking if val is containing ephemeral marks.
|
||||
// The schema argument is used to figure out if the value is for an ephemeral
|
||||
// context. If it is, then we don't even validate ephemeral marks.
|
||||
// If val contains any ephemeral mark, we check if the attribute containing
|
||||
// an ephemeral value is a write-only one. If not, we generate a diagnostic.
|
||||
// The diagnostics returned by this method need to go through InConfigBody
|
||||
// by the caller of the evaluator to append additional context to the diagnostic
|
||||
// for an enhanced feedback to the user.
|
||||
//
|
||||
// A nil schema will handle the value as unable to hold any ephemeral mark.
|
||||
func validEphemeralReferences(schema *configschema.Block, val cty.Value) (diags tfdiags.Diagnostics) {
|
||||
// Ephemeral resources can reference values with any mark, so ignore this validation for ephemeral blocks
|
||||
if schema != nil && schema.Ephemeral {
|
||||
return diags
|
||||
}
|
||||
// This is the function for schema != nil.
|
||||
// In the case of schema == nil, the function is recreated below.
|
||||
//
|
||||
// In cases of DynamicPseudoType attribute in the schema, the attribute that is actually
|
||||
// referencing an ephemeral value might be missing from the schema.
|
||||
// Therefore, we search for the first ancestor that exists in the schema.
|
||||
attrFromSchema := func(path cty.Path) (*configschema.Attribute, cty.Path) {
|
||||
attrPath := path
|
||||
attr := schema.AttributeByPath(attrPath)
|
||||
for attr == nil {
|
||||
if len(attrPath) == 0 {
|
||||
log.Printf("[WARN] no valid path found in schema for path \"%#v\"", path)
|
||||
return nil, path
|
||||
}
|
||||
attrPath = attrPath[:len(attrPath)-1]
|
||||
attr = schema.AttributeByPath(attrPath)
|
||||
}
|
||||
return attr, attrPath
|
||||
}
|
||||
|
||||
// We recreate the attribute search in the schema here purely for being sure
|
||||
// that the logic below can run even when the schema is nil.
|
||||
// When there is no schema, there should be no ephemeral value in the block.
|
||||
if schema == nil {
|
||||
attrFromSchema = func(path cty.Path) (*configschema.Attribute, cty.Path) {
|
||||
return nil, path
|
||||
}
|
||||
}
|
||||
|
||||
_, valueMarks := val.UnmarkDeepWithPaths()
|
||||
for _, pathMark := range valueMarks {
|
||||
_, ok := pathMark.Marks[marks.Ephemeral]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the block is not ephemeral, then only its write-only attributes can reference ephemeral values.
|
||||
// To figure it out, we need to find the attribute by the mark path.
|
||||
attr, foundPath := attrFromSchema(pathMark.Path)
|
||||
if attr != nil && attr.WriteOnly {
|
||||
continue
|
||||
}
|
||||
|
||||
diags = diags.Append(tfdiags.AttributeValue(
|
||||
tfdiags.Error,
|
||||
"Ephemeral value used in non-ephemeral context",
|
||||
fmt.Sprintf("Attribute %q is referencing an ephemeral value but ephemeral values can be referenced only by other ephemeral attributes or by write-only ones.", tfdiags.FormatCtyPath(foundPath)),
|
||||
foundPath,
|
||||
))
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
@@ -8,12 +8,16 @@ package lang
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||
"github.com/opentofu/opentofu/internal/instances"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hcldec"
|
||||
@@ -59,6 +63,9 @@ func TestScopeEvalContext(t *testing.T) {
|
||||
"null_resource.multi[1]": cty.ObjectVal(map[string]cty.Value{
|
||||
"attr": cty.StringVal("multi1"),
|
||||
}),
|
||||
"ephemeral.foo_ephemeral.bar": cty.ObjectVal(map[string]cty.Value{
|
||||
"attr": cty.StringVal("baz"),
|
||||
}),
|
||||
},
|
||||
LocalValues: map[string]cty.Value{
|
||||
"foo": cty.StringVal("bar"),
|
||||
@@ -373,6 +380,18 @@ func TestScopeEvalContext(t *testing.T) {
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
`ephemeral.foo_ephemeral.bar`,
|
||||
map[string]cty.Value{
|
||||
"ephemeral": cty.ObjectVal(map[string]cty.Value{
|
||||
"foo_ephemeral": cty.ObjectVal(map[string]cty.Value{
|
||||
"bar": cty.ObjectVal(map[string]cty.Value{
|
||||
"attr": cty.StringVal("baz"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -1024,3 +1043,199 @@ func Test_enhanceFunctionDiags(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidEphemeralReference(t *testing.T) {
|
||||
schema := &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
},
|
||||
"secret": {
|
||||
Type: cty.String,
|
||||
},
|
||||
"secret_wo": {
|
||||
Type: cty.String,
|
||||
WriteOnly: true,
|
||||
},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"nested_simple": {
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"inner_nested_simple": {
|
||||
Nesting: configschema.NestingSingle,
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"inner_nested_simple_attr": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
WriteOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingSingle,
|
||||
},
|
||||
"nested_set": {
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"inner_nested_set": {
|
||||
Nesting: configschema.NestingSingle,
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"inner_nested_set_attr": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
WriteOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingSet,
|
||||
},
|
||||
},
|
||||
}
|
||||
tests := map[string]struct {
|
||||
schema *configschema.Block
|
||||
val cty.Value
|
||||
|
||||
want tfdiags.Diagnostics
|
||||
}{
|
||||
"nil schema with no ephemeral mark": {
|
||||
nil,
|
||||
cty.UnknownVal(cty.String),
|
||||
nil,
|
||||
},
|
||||
"nil schema with ephemeral mark": {
|
||||
nil,
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("id value"),
|
||||
"secret": cty.StringVal("secret value"),
|
||||
"secret_wo": cty.StringVal("secret value").Mark(marks.Ephemeral),
|
||||
}),
|
||||
tfdiags.Diagnostics{}.Append(
|
||||
tfdiags.AttributeValue(
|
||||
tfdiags.Error,
|
||||
"Ephemeral value used in non-ephemeral context",
|
||||
fmt.Sprintf("Attribute %q is referencing an ephemeral value but ephemeral values can be referenced only by other ephemeral attributes or by write-only ones.", ".secret_wo"),
|
||||
cty.Path{cty.GetAttrStep{Name: "secret_wo"}},
|
||||
),
|
||||
),
|
||||
},
|
||||
"schema is ephemeral": {
|
||||
&configschema.Block{
|
||||
Ephemeral: true,
|
||||
},
|
||||
cty.UnknownVal(cty.String),
|
||||
nil,
|
||||
},
|
||||
"no checks if the value contains no ephemeral": {
|
||||
schema,
|
||||
cty.StringVal("test"),
|
||||
nil,
|
||||
},
|
||||
"write only argument is referencing ephemeral value": {
|
||||
schema,
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("id value"),
|
||||
"secret": cty.StringVal("secret value"),
|
||||
"secret_wo": cty.StringVal("secret value").Mark(marks.Ephemeral),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
"error when an write-only and a non-write-only contain ephemeral": {
|
||||
schema,
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("id value"),
|
||||
"secret": cty.StringVal("secret value").Mark(marks.Ephemeral),
|
||||
"secret_wo": cty.StringVal("secret value").Mark(marks.Ephemeral),
|
||||
}),
|
||||
tfdiags.Diagnostics{}.Append(
|
||||
tfdiags.AttributeValue(
|
||||
tfdiags.Error,
|
||||
"Ephemeral value used in non-ephemeral context",
|
||||
fmt.Sprintf("Attribute %q is referencing an ephemeral value but ephemeral values can be referenced only by other ephemeral attributes or by write-only ones.", ".secret"),
|
||||
cty.Path{cty.GetAttrStep{Name: "secret"}},
|
||||
),
|
||||
),
|
||||
},
|
||||
"find the right DynamicPseudoType attribute": {
|
||||
schema,
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("id value"),
|
||||
"secret_wo": cty.StringVal("secret value").Mark(marks.Ephemeral),
|
||||
"nested_simple": cty.ObjectVal(map[string]cty.Value{
|
||||
"inner_nested_simple": cty.ObjectVal(map[string]cty.Value{
|
||||
"inner_nested_simple_attr": cty.ObjectVal(map[string]cty.Value{
|
||||
"attribute_not_in_schema": cty.StringVal("test val").Mark(marks.Ephemeral),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
"error when attribute is not in the schema": {
|
||||
schema,
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("id value"),
|
||||
"secret_wo": cty.StringVal("secret value").Mark(marks.Ephemeral),
|
||||
"nested_simple": cty.ObjectVal(map[string]cty.Value{
|
||||
"inner_nested_simple": cty.ObjectVal(map[string]cty.Value{
|
||||
"block_not_in_schema": cty.ObjectVal(map[string]cty.Value{
|
||||
"attribute_not_in_schema": cty.StringVal("test val").Mark(marks.Ephemeral),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
tfdiags.Diagnostics{}.Append(
|
||||
tfdiags.AttributeValue(
|
||||
tfdiags.Error,
|
||||
"Ephemeral value used in non-ephemeral context",
|
||||
fmt.Sprintf(
|
||||
`Attribute %q is referencing an ephemeral value but ephemeral values can be referenced only by other ephemeral attributes or by write-only ones.`,
|
||||
".nested_simple.inner_nested_simple.block_not_in_schema.attribute_not_in_schema",
|
||||
),
|
||||
cty.GetAttrPath("nested_simple").GetAttr("inner_nested_simple").GetAttr("block_not_in_schema").GetAttr("attribute_not_in_schema"),
|
||||
),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
lookupAttributeDiag := func(forPath cty.Path, in tfdiags.Diagnostics) tfdiags.Diagnostic {
|
||||
for _, i := range in {
|
||||
p := tfdiags.GetAttribute(i)
|
||||
if p.Equals(forPath) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
diags := validEphemeralReferences(tt.schema, tt.val)
|
||||
if want, got := len(tt.want), len(diags); want != got {
|
||||
t.Errorf("wrong number of diags. want: %d; got: %d", want, got)
|
||||
}
|
||||
for _, d := range diags {
|
||||
attributePath := tfdiags.GetAttribute(d)
|
||||
wantDiag := lookupAttributeDiag(attributePath, tt.want)
|
||||
if wantDiag == nil {
|
||||
t.Errorf("got a diagnostic with a path (%s) that is not expected: %s", attributePath, d)
|
||||
continue
|
||||
}
|
||||
gotDesc := d.Description()
|
||||
wantDesc := wantDiag.Description()
|
||||
if diff := cmp.Diff(wantDesc, gotDesc); diff != "" {
|
||||
t.Errorf("%s: unexpected diff in diagnostic description:\n%s", attributePath, diff)
|
||||
}
|
||||
if want, got := d.Severity(), wantDiag.Severity(); want != got {
|
||||
t.Errorf("%s: wrong severity. want %q; got %q", attributePath, want, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,10 @@ func Contains(val cty.Value, mark valueMark) bool {
|
||||
// OpenTofu.
|
||||
const Sensitive = valueMark("Sensitive")
|
||||
|
||||
// Ephemeral indicates that this value is marked as ephemeral in the context of
|
||||
// OpenTofu.
|
||||
const Ephemeral = valueMark("Ephemeral")
|
||||
|
||||
// TypeType is used to indicate that the value contains a representation of
|
||||
// another value's type. This is part of the implementation of the console-only
|
||||
// `type` function.
|
||||
@@ -226,3 +230,20 @@ func RemoveDeepDeprecated(val cty.Value) cty.Value {
|
||||
val, _ = unmarkDeepWithPathsDeprecated(val)
|
||||
return val
|
||||
}
|
||||
|
||||
// EnsureNoEphemeralMarks checks all the given paths for the Ephemeral mark.
|
||||
// If there is at least one path marked as such, this method will return
|
||||
// an error containing the marked paths.
|
||||
func EnsureNoEphemeralMarks(pvms []cty.PathValueMarks) error {
|
||||
var res []string
|
||||
for _, pvm := range pvms {
|
||||
if _, ok := pvm.Marks[Ephemeral]; ok {
|
||||
res = append(res, tfdiags.FormatCtyPath(pvm.Path))
|
||||
}
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
return fmt.Errorf("ephemeral marks found at the following paths:\n%s", strings.Join(res, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -126,6 +126,9 @@ func NewInstanceInfo(addr addrs.AbsResourceInstance) *InstanceInfo {
|
||||
if addr.Resource.Resource.Mode == addrs.DataResourceMode {
|
||||
id = "data." + id
|
||||
}
|
||||
if addr.Resource.Resource.Mode == addrs.EphemeralResourceMode {
|
||||
panic("ephemeral resources are not meant to be processed by this function. Are you sure that this code should be reused?")
|
||||
}
|
||||
if addr.Resource.Key != addrs.NoKey {
|
||||
switch k := addr.Resource.Key.(type) {
|
||||
case addrs.IntKey:
|
||||
|
||||
@@ -140,6 +140,10 @@ func (r *ResourceAddress) MatchesResourceConfig(path addrs.Module, rc *configs.R
|
||||
if rc.Mode != addrs.DataResourceMode {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
// NOTE: Even though the ephemeral resources are not supported in the legacy form, we want to be sure
|
||||
// that it is not handled as the other type of resources.
|
||||
return false
|
||||
}
|
||||
if r.Type != rc.Type || r.Name != rc.Name {
|
||||
return false
|
||||
@@ -300,6 +304,10 @@ func NewLegacyResourceAddress(addr addrs.AbsResource) *ResourceAddress {
|
||||
case addrs.DataResourceMode:
|
||||
ret.Mode = DataResourceMode
|
||||
default:
|
||||
// This is also covering the unlikely situation when an ephemeral resource will end up in here.
|
||||
// This is not meant to happen. However, since this method is not used anymore, we want it to panic
|
||||
// in case somebody starts using it again in the future. This is to indicate that this is legacy code and that
|
||||
// is not meant to work with new features without putting additional work into it, if ever needed.
|
||||
panic(fmt.Errorf("cannot shim %s to legacy ResourceMode value", addr.Resource.Mode))
|
||||
}
|
||||
|
||||
@@ -338,6 +346,10 @@ func NewLegacyResourceInstanceAddress(addr addrs.AbsResourceInstance) *ResourceA
|
||||
case addrs.DataResourceMode:
|
||||
ret.Mode = DataResourceMode
|
||||
default:
|
||||
// This is also covering the unlikely situation when an ephemeral resource will end up in here.
|
||||
// This is not meant to happen. However, since this method is not used anymore, we want it to panic
|
||||
// in case somebody starts using it again in the future. This is to indicate that this is legacy code and that
|
||||
// is not meant to work with new features without putting additional work into it, if ever needed.
|
||||
panic(fmt.Errorf("cannot shim %s to legacy ResourceMode value", addr.Resource.Resource.Mode))
|
||||
}
|
||||
|
||||
@@ -403,6 +415,8 @@ func (addr *ResourceAddress) AbsResourceInstanceAddr() addrs.AbsResourceInstance
|
||||
case DataResourceMode:
|
||||
ret.Resource.Resource.Mode = addrs.DataResourceMode
|
||||
default:
|
||||
// This case is also covering situations when ephemeral resources are getting here.
|
||||
// This shouldn't be possible, so let this panic.
|
||||
panic(fmt.Errorf("cannot shim %s to addrs.ResourceMode value", addr.Mode))
|
||||
}
|
||||
|
||||
|
||||
@@ -167,6 +167,7 @@ func loadProviderSchemas(schemas map[addrs.Provider]*ProviderSchema, config *con
|
||||
)
|
||||
}
|
||||
}
|
||||
// NOTE: No ephemeral resources schema for the legacy code
|
||||
|
||||
schemas[fqn] = s
|
||||
|
||||
@@ -271,6 +272,8 @@ func (ps *ProviderSchema) SchemaForResourceType(mode addrs.ResourceMode, typeNam
|
||||
case addrs.DataResourceMode:
|
||||
// Data resources don't have schema versions right now, since state is discarded for each refresh
|
||||
return ps.DataSources[typeName], 0
|
||||
case addrs.EphemeralResourceMode:
|
||||
panic("ephemeral resource is not meant to be in the schema for legacy providers")
|
||||
default:
|
||||
// Shouldn't happen, because the above cases are comprehensive.
|
||||
return nil, 0
|
||||
|
||||
@@ -1084,6 +1084,9 @@ func (m *ModuleState) Orphans(c *configs.Module) []addrs.ResourceInstance {
|
||||
for _, r := range c.DataResources {
|
||||
inConfig[r.Addr().String()] = struct{}{}
|
||||
}
|
||||
for _, r := range c.EphemeralResources {
|
||||
log.Printf("[ERROR] ephemeral resources detected in legacy state: %s", r.Addr().String())
|
||||
}
|
||||
}
|
||||
|
||||
var result []addrs.ResourceInstance
|
||||
|
||||
@@ -16,6 +16,10 @@ const (
|
||||
CreateThenDelete Action = '±'
|
||||
Delete Action = '-'
|
||||
Forget Action = '.'
|
||||
Open Action = '⁐'
|
||||
// NOTE: Renew and Close missing on purpose.
|
||||
// Those are not meant to be stored in the plan.
|
||||
// Instead, we have hooks for those to show progress.
|
||||
)
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer -type Action
|
||||
|
||||
@@ -16,6 +16,7 @@ func _() {
|
||||
_ = x[CreateThenDelete-177]
|
||||
_ = x[Delete-45]
|
||||
_ = x[Forget-46]
|
||||
_ = x[Open-8272]
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -24,8 +25,9 @@ const (
|
||||
_Action_name_2 = "DeleteForget"
|
||||
_Action_name_3 = "Update"
|
||||
_Action_name_4 = "CreateThenDelete"
|
||||
_Action_name_5 = "Read"
|
||||
_Action_name_6 = "DeleteThenCreate"
|
||||
_Action_name_5 = "Open"
|
||||
_Action_name_6 = "Read"
|
||||
_Action_name_7 = "DeleteThenCreate"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -45,10 +47,12 @@ func (i Action) String() string {
|
||||
return _Action_name_3
|
||||
case i == 177:
|
||||
return _Action_name_4
|
||||
case i == 8592:
|
||||
case i == 8272:
|
||||
return _Action_name_5
|
||||
case i == 8723:
|
||||
case i == 8592:
|
||||
return _Action_name_6
|
||||
case i == 8723:
|
||||
return _Action_name_7
|
||||
default:
|
||||
return "Action(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
|
||||
@@ -37,8 +37,24 @@ func NewChanges() *Changes {
|
||||
return &Changes{}
|
||||
}
|
||||
|
||||
// BuildChanges is a helper -- primarily intended for tests -- to build a state
|
||||
// using imperative code against the StateSync type while still acting as
|
||||
// an expression of type *State to assign into a containing struct.
|
||||
func BuildChanges(cb func(sync *ChangesSync)) *Changes {
|
||||
c := NewChanges()
|
||||
cb(c.SyncWrapper())
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Changes) Empty() bool {
|
||||
for _, res := range c.Resources {
|
||||
// We ignore Open actions which are specific to ephemeral resources.
|
||||
// A configuration containing ephemeral resources will always have changes planned,
|
||||
// but if there is no other change recorded, there is no need for a prompt
|
||||
// on the user.
|
||||
if res.Action == Open {
|
||||
continue
|
||||
}
|
||||
if res.Action != NoOp || res.Moved() {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -41,6 +41,22 @@ func TestChangesEmpty(t *testing.T) {
|
||||
Action: Update,
|
||||
},
|
||||
},
|
||||
// but an ephemeral resources will not impact the "emptiness" of the plan
|
||||
{
|
||||
Addr: addrs.Resource{
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "woot",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||
PrevRunAddr: addrs.Resource{
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "woot",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||
ChangeSrc: ChangeSrc{
|
||||
Action: Open,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
@@ -119,6 +135,28 @@ func TestChangesEmpty(t *testing.T) {
|
||||
},
|
||||
true,
|
||||
},
|
||||
"ephemeral resource change": {
|
||||
&Changes{
|
||||
Resources: []*ResourceInstanceChangeSrc{
|
||||
{
|
||||
Addr: addrs.Resource{
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "woot",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||
PrevRunAddr: addrs.Resource{
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "woot",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||
ChangeSrc: ChangeSrc{
|
||||
Action: Open,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
|
||||
@@ -88,6 +88,7 @@ const (
|
||||
Action_DELETE_THEN_CREATE Action = 6
|
||||
Action_CREATE_THEN_DELETE Action = 7
|
||||
Action_FORGET Action = 8
|
||||
Action_OPEN Action = 9
|
||||
)
|
||||
|
||||
// Enum value maps for Action.
|
||||
@@ -101,6 +102,7 @@ var (
|
||||
6: "DELETE_THEN_CREATE",
|
||||
7: "CREATE_THEN_DELETE",
|
||||
8: "FORGET",
|
||||
9: "OPEN",
|
||||
}
|
||||
Action_value = map[string]int32{
|
||||
"NOOP": 0,
|
||||
@@ -111,6 +113,7 @@ var (
|
||||
"DELETE_THEN_CREATE": 6,
|
||||
"CREATE_THEN_DELETE": 7,
|
||||
"FORGET": 8,
|
||||
"OPEN": 9,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1520,49 +1523,49 @@ var file_planfile_proto_rawDesc = []byte{
|
||||
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x2a, 0x31, 0x0a, 0x04, 0x4d,
|
||||
0x6f, 0x64, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x10, 0x00, 0x12,
|
||||
0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c,
|
||||
0x52, 0x45, 0x46, 0x52, 0x45, 0x53, 0x48, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x02, 0x2a, 0x7c,
|
||||
0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4f, 0x50,
|
||||
0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x08,
|
||||
0x0a, 0x04, 0x52, 0x45, 0x41, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x50, 0x44, 0x41,
|
||||
0x54, 0x45, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x05,
|
||||
0x12, 0x16, 0x0a, 0x12, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f,
|
||||
0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x52, 0x45, 0x41,
|
||||
0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x07,
|
||||
0x12, 0x0a, 0x0a, 0x06, 0x46, 0x4f, 0x52, 0x47, 0x45, 0x54, 0x10, 0x08, 0x2a, 0xc8, 0x03, 0x0a,
|
||||
0x1c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63,
|
||||
0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x08, 0x0a,
|
||||
0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x50, 0x4c, 0x41,
|
||||
0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x54, 0x41, 0x49, 0x4e, 0x54,
|
||||
0x45, 0x44, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f,
|
||||
0x42, 0x59, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d,
|
||||
0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f,
|
||||
0x43, 0x41, 0x4e, 0x4e, 0x4f, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12,
|
||||
0x25, 0x0a, 0x21, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53,
|
||||
0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x4f,
|
||||
0x4e, 0x46, 0x49, 0x47, 0x10, 0x04, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45,
|
||||
0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x57, 0x52, 0x4f, 0x4e, 0x47, 0x5f, 0x52,
|
||||
0x45, 0x50, 0x45, 0x54, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x05, 0x12, 0x1e, 0x0a, 0x1a, 0x44,
|
||||
0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x4f,
|
||||
0x55, 0x4e, 0x54, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x10, 0x06, 0x12, 0x1b, 0x0a, 0x17, 0x44,
|
||||
0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x45, 0x41,
|
||||
0x43, 0x48, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x07, 0x12, 0x1c, 0x0a, 0x18, 0x44, 0x45, 0x4c, 0x45,
|
||||
0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f,
|
||||
0x44, 0x55, 0x4c, 0x45, 0x10, 0x08, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43,
|
||||
0x45, 0x5f, 0x42, 0x59, 0x5f, 0x54, 0x52, 0x49, 0x47, 0x47, 0x45, 0x52, 0x53, 0x10, 0x09, 0x12,
|
||||
0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f,
|
||||
0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x0a,
|
||||
0x12, 0x23, 0x0a, 0x1f, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45,
|
||||
0x5f, 0x44, 0x45, 0x50, 0x45, 0x4e, 0x44, 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x50, 0x45, 0x4e, 0x44,
|
||||
0x49, 0x4e, 0x47, 0x10, 0x0b, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45,
|
||||
0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x4e, 0x45, 0x53, 0x54,
|
||||
0x45, 0x44, 0x10, 0x0d, 0x12, 0x21, 0x0a, 0x1d, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42,
|
||||
0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, 0x56, 0x45, 0x5f, 0x54,
|
||||
0x41, 0x52, 0x47, 0x45, 0x54, 0x10, 0x0c, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75,
|
||||
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x6f, 0x66, 0x75, 0x2f, 0x6f,
|
||||
0x70, 0x65, 0x6e, 0x74, 0x6f, 0x66, 0x75, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c,
|
||||
0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f,
|
||||
0x70, 0x6c, 0x61, 0x6e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x33,
|
||||
0x52, 0x45, 0x46, 0x52, 0x45, 0x53, 0x48, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x02, 0x2a, 0x86,
|
||||
0x01, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4f,
|
||||
0x50, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12,
|
||||
0x08, 0x0a, 0x04, 0x52, 0x45, 0x41, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x50, 0x44,
|
||||
0x41, 0x54, 0x45, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10,
|
||||
0x05, 0x12, 0x16, 0x0a, 0x12, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e,
|
||||
0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x52, 0x45,
|
||||
0x41, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10,
|
||||
0x07, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x4f, 0x52, 0x47, 0x45, 0x54, 0x10, 0x08, 0x12, 0x08, 0x0a,
|
||||
0x04, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x09, 0x2a, 0xc8, 0x03, 0x0a, 0x1c, 0x52, 0x65, 0x73, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x63, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45,
|
||||
0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45,
|
||||
0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x54, 0x41, 0x49, 0x4e, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12,
|
||||
0x16, 0x0a, 0x12, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x59, 0x5f, 0x52, 0x45,
|
||||
0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x45, 0x50, 0x4c, 0x41,
|
||||
0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x4e, 0x4f,
|
||||
0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12, 0x25, 0x0a, 0x21, 0x44, 0x45,
|
||||
0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f,
|
||||
0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x10,
|
||||
0x04, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41,
|
||||
0x55, 0x53, 0x45, 0x5f, 0x57, 0x52, 0x4f, 0x4e, 0x47, 0x5f, 0x52, 0x45, 0x50, 0x45, 0x54, 0x49,
|
||||
0x54, 0x49, 0x4f, 0x4e, 0x10, 0x05, 0x12, 0x1e, 0x0a, 0x1a, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45,
|
||||
0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x5f, 0x49,
|
||||
0x4e, 0x44, 0x45, 0x58, 0x10, 0x06, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45,
|
||||
0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x45, 0x41, 0x43, 0x48, 0x5f, 0x4b, 0x45,
|
||||
0x59, 0x10, 0x07, 0x12, 0x1c, 0x0a, 0x18, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45,
|
||||
0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, 0x44, 0x55, 0x4c, 0x45, 0x10,
|
||||
0x08, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x59, 0x5f,
|
||||
0x54, 0x52, 0x49, 0x47, 0x47, 0x45, 0x52, 0x53, 0x10, 0x09, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x45,
|
||||
0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49,
|
||||
0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x0a, 0x12, 0x23, 0x0a, 0x1f, 0x52,
|
||||
0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x44, 0x45, 0x50, 0x45,
|
||||
0x4e, 0x44, 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x0b,
|
||||
0x12, 0x1d, 0x0a, 0x19, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45,
|
||||
0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x4e, 0x45, 0x53, 0x54, 0x45, 0x44, 0x10, 0x0d, 0x12,
|
||||
0x21, 0x0a, 0x1d, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53,
|
||||
0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, 0x56, 0x45, 0x5f, 0x54, 0x41, 0x52, 0x47, 0x45, 0x54,
|
||||
0x10, 0x0c, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x6f, 0x66, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x6f,
|
||||
0x66, 0x75, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e,
|
||||
0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -122,6 +122,7 @@ enum Action {
|
||||
DELETE_THEN_CREATE = 6;
|
||||
CREATE_THEN_DELETE = 7;
|
||||
FORGET = 8;
|
||||
OPEN = 9;
|
||||
}
|
||||
|
||||
// Change represents a change made to some object, transforming it from an old
|
||||
|
||||
@@ -73,6 +73,16 @@ func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty
|
||||
return proposedNew(schema, prior, config)
|
||||
}
|
||||
|
||||
// PlannedEphemeralResourceObject is exactly as PlannedDataResourceObject, but we
|
||||
// want to have a different copy of it to emphasize the special handling of
|
||||
// this type of resource.
|
||||
// Ephemeral resources are not stored into the state, so every newly planned value
|
||||
// is based only on the configuration and its schema.
|
||||
func PlannedEphemeralResourceObject(schema *configschema.Block, config cty.Value) cty.Value {
|
||||
prior := cty.UnknownVal(schema.ImpliedType())
|
||||
return proposedNew(schema, prior, config)
|
||||
}
|
||||
|
||||
func proposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value {
|
||||
if config.IsNull() || !config.IsKnown() {
|
||||
// A block config should never be null at this point. The only nullable
|
||||
|
||||
@@ -313,6 +313,9 @@ func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, pla
|
||||
return assertPlannedObjectValid(attrS.NestedType, priorV, configV, plannedV, path)
|
||||
}
|
||||
|
||||
if !configV.IsNull() && plannedV.IsNull() && attrS.WriteOnly {
|
||||
return errs // TODO ephemeral - check other places that might need a validation like this (part of the write-only attributes work)
|
||||
}
|
||||
// If none of the above conditions match, the provider has made an invalid
|
||||
// change to this attribute.
|
||||
if priorV.IsNull() {
|
||||
|
||||
@@ -410,6 +410,8 @@ func changeFromTfplan(rawChange *planproto.Change) (*plans.ChangeSrc, error) {
|
||||
ret.Action = plans.DeleteThenCreate
|
||||
beforeIdx = 0
|
||||
afterIdx = 1
|
||||
case planproto.Action_OPEN:
|
||||
ret.Action = plans.Open
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid change action %s", rawChange.Action)
|
||||
}
|
||||
@@ -819,6 +821,15 @@ func changeToTfplan(change *plans.ChangeSrc) (*planproto.Change, error) {
|
||||
case plans.CreateThenDelete:
|
||||
ret.Action = planproto.Action_CREATE_THEN_DELETE
|
||||
ret.Values = []*planproto.DynamicValue{before, after}
|
||||
case plans.Open:
|
||||
ret.Action = planproto.Action_OPEN
|
||||
// We need to write ephemeral resources to the plan file to be able to build
|
||||
// the apply graph on `tofu apply <planfile>`.
|
||||
// The DiffTransformer needs the changes from the plan to be able to generate
|
||||
// executable resource instance graph nodes so we are adding the ephemeral resources too.
|
||||
// Even though we are writing these, the actual values of the ephemeral *must not*
|
||||
// be written to the plan so set nothing here.
|
||||
ret.Values = []*planproto.DynamicValue{}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid change action %s", change.Action)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package planfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
@@ -173,6 +174,32 @@ func TestTFPlanRoundTrip(t *testing.T) {
|
||||
GeneratedConfig: "resource \\\"test_thing\\\" \\\"importing\\\" {}",
|
||||
},
|
||||
},
|
||||
{
|
||||
Addr: addrs.Resource{
|
||||
Mode: addrs.EphemeralResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "testeph",
|
||||
}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance),
|
||||
PrevRunAddr: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_thing",
|
||||
Name: "testeph",
|
||||
}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance),
|
||||
ProviderAddr: addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
ChangeSrc: plans.ChangeSrc{
|
||||
Action: plans.Open,
|
||||
Before: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("testing"),
|
||||
}), objTy),
|
||||
After: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("testing"),
|
||||
}), objTy),
|
||||
GeneratedConfig: "ephemeral \\\"test_thing\\\" \\\"testeph\\\" {}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
DriftedResources: []*plans.ResourceInstanceChangeSrc{
|
||||
@@ -297,6 +324,14 @@ func TestTFPlanRoundTrip(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
{
|
||||
// nullify the ephemeral values from the initial plan since those must be nil in the plan file
|
||||
i := slices.IndexFunc(plan.Changes.Resources, func(src *plans.ResourceInstanceChangeSrc) bool {
|
||||
return src.Addr.Resource.Resource.Mode == addrs.EphemeralResourceMode
|
||||
})
|
||||
plan.Changes.Resources[i].After = nil
|
||||
plan.Changes.Resources[i].Before = nil
|
||||
}
|
||||
|
||||
newPlan, err := readTfplan(&buf)
|
||||
if err != nil {
|
||||
|
||||
@@ -36,6 +36,7 @@ func ConfigSchemaToProto(b *configschema.Block) *proto.Schema_Block {
|
||||
Required: a.Required,
|
||||
Sensitive: a.Sensitive,
|
||||
Deprecated: a.Deprecated,
|
||||
WriteOnly: a.WriteOnly,
|
||||
}
|
||||
|
||||
ty, err := json.Marshal(a.Type)
|
||||
@@ -98,6 +99,15 @@ func ProtoToProviderSchema(s *proto.Schema) providers.Schema {
|
||||
}
|
||||
}
|
||||
|
||||
// ProtoToEphemeralProviderSchema takes a proto.Schema and converts it to a providers.Schema
|
||||
// marking it as being able to work with ephemeral values.
|
||||
func ProtoToEphemeralProviderSchema(s *proto.Schema) providers.Schema {
|
||||
ret := ProtoToProviderSchema(s)
|
||||
ret.Block.Ephemeral = true
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// ProtoToConfigSchema takes the Schema_Block from a grpc response and converts it
|
||||
// to a tofu *configschema.Block.
|
||||
func ProtoToConfigSchema(b *proto.Schema_Block) *configschema.Block {
|
||||
@@ -119,6 +129,7 @@ func ProtoToConfigSchema(b *proto.Schema_Block) *configschema.Block {
|
||||
Computed: a.Computed,
|
||||
Sensitive: a.Sensitive,
|
||||
Deprecated: a.Deprecated,
|
||||
WriteOnly: a.WriteOnly,
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(a.Type, &attr.Type); err != nil {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user