mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-25 08:21:07 -06:00
terraform: OrphanResourceCountTransformer for orphaning extranneous
instances
This commit is contained in:
parent
97b7915b8f
commit
091264e4ba
@ -29,6 +29,11 @@ func (n *NodePlannableResource) EvalTree() EvalNode {
|
||||
|
||||
// GraphNodeDynamicExpandable
|
||||
func (n *NodePlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
|
||||
// Grab the state which we read
|
||||
state, lock := ctx.State()
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
|
||||
// Expand the resource count which must be available by now from EvalTree
|
||||
count, err := n.Config.Count()
|
||||
if err != nil {
|
||||
@ -39,25 +44,45 @@ func (n *NodePlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
|
||||
concreteResource := func(a *NodeAbstractResource) dag.Vertex {
|
||||
// Add the config and state since we don't do that via transforms
|
||||
a.Config = n.Config
|
||||
a.ResourceState = n.ResourceState
|
||||
|
||||
return &NodePlannableResourceInstance{
|
||||
NodeAbstractResource: a,
|
||||
}
|
||||
}
|
||||
|
||||
// The concrete resource factory we'll use for oprhans
|
||||
concreteResourceOrphan := func(a *NodeAbstractResource) dag.Vertex {
|
||||
return &NodePlannableResourceOrphan{
|
||||
NodeAbstractResource: a,
|
||||
}
|
||||
}
|
||||
|
||||
// Start creating the steps
|
||||
steps := make([]GraphTransformer, 0, 5)
|
||||
steps := []GraphTransformer{
|
||||
// Expand the count.
|
||||
&ResourceCountTransformer{
|
||||
Concrete: concreteResource,
|
||||
Count: count,
|
||||
Addr: n.ResourceAddr(),
|
||||
},
|
||||
|
||||
// Expand counts.
|
||||
steps = append(steps, &ResourceCountTransformer{
|
||||
Concrete: concreteResource,
|
||||
Count: count,
|
||||
Addr: n.ResourceAddr(),
|
||||
})
|
||||
// Add the count orphans
|
||||
&OrphanResourceCountTransformer{
|
||||
Concrete: concreteResourceOrphan,
|
||||
Count: count,
|
||||
Addr: n.ResourceAddr(),
|
||||
State: state,
|
||||
},
|
||||
|
||||
// Always end with the root being added
|
||||
steps = append(steps, &RootTransformer{})
|
||||
// TODO: deposed
|
||||
// TODO: targeting
|
||||
|
||||
// Attach the state
|
||||
&AttachStateTransformer{State: state},
|
||||
|
||||
// Make sure there is a single root
|
||||
&RootTransformer{},
|
||||
}
|
||||
|
||||
// Build the graph
|
||||
b := &BasicGraphBuilder{Steps: steps, Validate: true}
|
||||
|
99
terraform/transform_orphan_count.go
Normal file
99
terraform/transform_orphan_count.go
Normal file
@ -0,0 +1,99 @@
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform/dag"
|
||||
)
|
||||
|
||||
// OrphanResourceCountTransformer is a GraphTransformer that adds orphans
|
||||
// for an expanded count to the graph. The determination of this depends
|
||||
// on the count argument given.
|
||||
//
|
||||
// Orphans are found by comparing the count to what is found in the state.
|
||||
// This tranform assumes that if an element in the state is within the count
|
||||
// bounds given, that it is not an orphan.
|
||||
type OrphanResourceCountTransformer struct {
|
||||
Concrete ConcreteResourceNodeFunc
|
||||
|
||||
Count int // Actual count of the resource
|
||||
Addr *ResourceAddress // Addr of the resource to look for orphans
|
||||
State *State // Full global state
|
||||
}
|
||||
|
||||
func (t *OrphanResourceCountTransformer) Transform(g *Graph) error {
|
||||
log.Printf("[TRACE] OrphanResourceCount: Starting...")
|
||||
|
||||
// Grab the module in the state just for this resource address
|
||||
ms := t.State.ModuleByPath(normalizeModulePath(t.Addr.Path))
|
||||
if ms == nil {
|
||||
// If no state, there can't be orphans
|
||||
return nil
|
||||
}
|
||||
|
||||
// Go through the orphans and add them all to the state
|
||||
for key, _ := range ms.Resources {
|
||||
// Build the address
|
||||
addr, err := parseResourceAddressInternal(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addr.Path = ms.Path[1:]
|
||||
|
||||
// Copy the address for comparison. If we aren't looking at
|
||||
// the same resource, then just ignore it.
|
||||
addrCopy := addr.Copy()
|
||||
addrCopy.Index = -1
|
||||
if !addrCopy.Equals(t.Addr) {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[TRACE] OrphanResourceCount: Checking: %s", addr)
|
||||
|
||||
idx := addr.Index
|
||||
|
||||
// If we have zero and the index here is 0 or 1, then we
|
||||
// change the index to a high number so that we treat it as
|
||||
// an orphan.
|
||||
if t.Count <= 0 && idx <= 0 {
|
||||
idx = t.Count + 1
|
||||
}
|
||||
|
||||
// If we have a count greater than 0 and we're at the zero index,
|
||||
// we do a special case check to see if our state also has a
|
||||
// -1 index value. If so, this is an orphan because our rules are
|
||||
// that if both a -1 and 0 are in the state, the 0 is destroyed.
|
||||
if t.Count > 0 && idx == -1 {
|
||||
key := &ResourceStateKey{
|
||||
Name: addr.Name,
|
||||
Type: addr.Type,
|
||||
Mode: addr.Mode,
|
||||
Index: 0,
|
||||
}
|
||||
|
||||
if _, ok := ms.Resources[key.String()]; ok {
|
||||
// We have a -1 index, too. Make an arbitrarily high
|
||||
// index so that we always mark this as an orphan.
|
||||
log.Printf("[WARN] OrphanResourceCount: %q both -1 and 0 index found, orphaning -1", addr)
|
||||
idx = t.Count + 1
|
||||
}
|
||||
}
|
||||
|
||||
// If the index is within the count bounds, it is not an orphan
|
||||
if idx < t.Count {
|
||||
continue
|
||||
}
|
||||
|
||||
// Build the abstract node and the concrete one
|
||||
abstract := &NodeAbstractResource{Addr: addr}
|
||||
var node dag.Vertex = abstract
|
||||
if f := t.Concrete; f != nil {
|
||||
node = f(abstract)
|
||||
}
|
||||
|
||||
// Add it to the graph
|
||||
g.Add(node)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
312
terraform/transform_orphan_count_test.go
Normal file
312
terraform/transform_orphan_count_test.go
Normal file
@ -0,0 +1,312 @@
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOrphanResourceCountTransformer(t *testing.T) {
|
||||
addr, err := parseResourceAddressInternal("aws_instance.foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
state := &State{
|
||||
Modules: []*ModuleState{
|
||||
&ModuleState{
|
||||
Path: RootModulePath,
|
||||
Resources: map[string]*ResourceState{
|
||||
"aws_instance.web": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
|
||||
"aws_instance.foo": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
|
||||
"aws_instance.foo.2": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
g := Graph{Path: RootModulePath}
|
||||
|
||||
{
|
||||
tf := &OrphanResourceCountTransformer{
|
||||
Concrete: testOrphanResourceConcreteFunc,
|
||||
Count: 1,
|
||||
Addr: addr,
|
||||
State: state,
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
expected := strings.TrimSpace(testTransformOrphanResourceCountBasicStr)
|
||||
if actual != expected {
|
||||
t.Fatalf("bad:\n\n%s", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrphanResourceCountTransformer_zero(t *testing.T) {
|
||||
addr, err := parseResourceAddressInternal("aws_instance.foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
state := &State{
|
||||
Modules: []*ModuleState{
|
||||
&ModuleState{
|
||||
Path: RootModulePath,
|
||||
Resources: map[string]*ResourceState{
|
||||
"aws_instance.web": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
|
||||
"aws_instance.foo": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
|
||||
"aws_instance.foo.2": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
g := Graph{Path: RootModulePath}
|
||||
|
||||
{
|
||||
tf := &OrphanResourceCountTransformer{
|
||||
Concrete: testOrphanResourceConcreteFunc,
|
||||
Count: 0,
|
||||
Addr: addr,
|
||||
State: state,
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
expected := strings.TrimSpace(testTransformOrphanResourceCountZeroStr)
|
||||
if actual != expected {
|
||||
t.Fatalf("bad:\n\n%s", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrphanResourceCountTransformer_oneNoIndex(t *testing.T) {
|
||||
addr, err := parseResourceAddressInternal("aws_instance.foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
state := &State{
|
||||
Modules: []*ModuleState{
|
||||
&ModuleState{
|
||||
Path: RootModulePath,
|
||||
Resources: map[string]*ResourceState{
|
||||
"aws_instance.web": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
|
||||
"aws_instance.foo": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
|
||||
"aws_instance.foo.2": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
g := Graph{Path: RootModulePath}
|
||||
|
||||
{
|
||||
tf := &OrphanResourceCountTransformer{
|
||||
Concrete: testOrphanResourceConcreteFunc,
|
||||
Count: 1,
|
||||
Addr: addr,
|
||||
State: state,
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
expected := strings.TrimSpace(testTransformOrphanResourceCountOneNoIndexStr)
|
||||
if actual != expected {
|
||||
t.Fatalf("bad:\n\n%s", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrphanResourceCountTransformer_oneIndex(t *testing.T) {
|
||||
addr, err := parseResourceAddressInternal("aws_instance.foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
state := &State{
|
||||
Modules: []*ModuleState{
|
||||
&ModuleState{
|
||||
Path: RootModulePath,
|
||||
Resources: map[string]*ResourceState{
|
||||
"aws_instance.web": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
|
||||
"aws_instance.foo.0": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
|
||||
"aws_instance.foo.1": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
g := Graph{Path: RootModulePath}
|
||||
|
||||
{
|
||||
tf := &OrphanResourceCountTransformer{
|
||||
Concrete: testOrphanResourceConcreteFunc,
|
||||
Count: 1,
|
||||
Addr: addr,
|
||||
State: state,
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
expected := strings.TrimSpace(testTransformOrphanResourceCountOneIndexStr)
|
||||
if actual != expected {
|
||||
t.Fatalf("bad:\n\n%s", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrphanResourceCountTransformer_zeroAndNone(t *testing.T) {
|
||||
addr, err := parseResourceAddressInternal("aws_instance.foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
state := &State{
|
||||
Modules: []*ModuleState{
|
||||
&ModuleState{
|
||||
Path: RootModulePath,
|
||||
Resources: map[string]*ResourceState{
|
||||
"aws_instance.web": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
|
||||
"aws_instance.foo": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
|
||||
"aws_instance.foo.0": &ResourceState{
|
||||
Type: "aws_instance",
|
||||
Primary: &InstanceState{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
g := Graph{Path: RootModulePath}
|
||||
|
||||
{
|
||||
tf := &OrphanResourceCountTransformer{
|
||||
Concrete: testOrphanResourceConcreteFunc,
|
||||
Count: 1,
|
||||
Addr: addr,
|
||||
State: state,
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
expected := strings.TrimSpace(testTransformOrphanResourceCountZeroAndNoneStr)
|
||||
if actual != expected {
|
||||
t.Fatalf("bad:\n\n%s", actual)
|
||||
}
|
||||
}
|
||||
|
||||
const testTransformOrphanResourceCountBasicStr = `
|
||||
aws_instance.foo[2] (orphan)
|
||||
`
|
||||
|
||||
const testTransformOrphanResourceCountZeroStr = `
|
||||
aws_instance.foo (orphan)
|
||||
aws_instance.foo[2] (orphan)
|
||||
`
|
||||
|
||||
const testTransformOrphanResourceCountOneNoIndexStr = `
|
||||
aws_instance.foo[2] (orphan)
|
||||
`
|
||||
|
||||
const testTransformOrphanResourceCountOneIndexStr = `
|
||||
aws_instance.foo[1] (orphan)
|
||||
`
|
||||
|
||||
const testTransformOrphanResourceCountZeroAndNoneStr = `
|
||||
aws_instance.foo (orphan)
|
||||
`
|
Loading…
Reference in New Issue
Block a user