opentofu/terraform/graph_config_node_resource.go
2016-11-08 13:59:26 -08:00

533 lines
15 KiB
Go

package terraform
import (
"fmt"
"log"
"strings"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/dot"
)
// GraphNodeCountDependent is implemented by resources for giving only
// the dependencies they have from the "count" field.
type GraphNodeCountDependent interface {
CountDependentOn() []string
}
// GraphNodeConfigResource represents a resource within the config graph.
type GraphNodeConfigResource struct {
Resource *config.Resource
// If set to true, this resource represents a resource
// that will be destroyed in some way.
Destroy bool
// Used during DynamicExpand to target indexes
Targets []ResourceAddress
Path []string
}
func (n *GraphNodeConfigResource) Copy() *GraphNodeConfigResource {
ncr := &GraphNodeConfigResource{
Resource: n.Resource.Copy(),
Destroy: n.Destroy,
Targets: make([]ResourceAddress, 0, len(n.Targets)),
Path: make([]string, 0, len(n.Path)),
}
for _, t := range n.Targets {
ncr.Targets = append(ncr.Targets, *t.Copy())
}
for _, p := range n.Path {
ncr.Path = append(ncr.Path, p)
}
return ncr
}
func (n *GraphNodeConfigResource) ConfigType() GraphNodeConfigType {
return GraphNodeConfigTypeResource
}
func (n *GraphNodeConfigResource) DependableName() []string {
return []string{n.Resource.Id()}
}
// GraphNodeCountDependent impl.
func (n *GraphNodeConfigResource) CountDependentOn() []string {
result := make([]string, 0, len(n.Resource.RawCount.Variables))
for _, v := range n.Resource.RawCount.Variables {
if vn := varNameForVar(v); vn != "" {
result = append(result, vn)
}
}
return result
}
// GraphNodeDependent impl.
func (n *GraphNodeConfigResource) DependentOn() []string {
result := make([]string, len(n.Resource.DependsOn),
(len(n.Resource.RawCount.Variables)+
len(n.Resource.RawConfig.Variables)+
len(n.Resource.DependsOn))*2)
copy(result, n.Resource.DependsOn)
for _, v := range n.Resource.RawCount.Variables {
if vn := varNameForVar(v); vn != "" {
result = append(result, vn)
}
}
for _, v := range n.Resource.RawConfig.Variables {
if vn := varNameForVar(v); vn != "" {
result = append(result, vn)
}
}
for _, p := range n.Resource.Provisioners {
for _, v := range p.ConnInfo.Variables {
if vn := varNameForVar(v); vn != "" && vn != n.Resource.Id() {
result = append(result, vn)
}
}
for _, v := range p.RawConfig.Variables {
if vn := varNameForVar(v); vn != "" && vn != n.Resource.Id() {
result = append(result, vn)
}
}
}
return result
}
// VarWalk calls a callback for all the variables that this resource
// depends on.
func (n *GraphNodeConfigResource) VarWalk(fn func(config.InterpolatedVariable)) {
for _, v := range n.Resource.RawCount.Variables {
fn(v)
}
for _, v := range n.Resource.RawConfig.Variables {
fn(v)
}
for _, p := range n.Resource.Provisioners {
for _, v := range p.ConnInfo.Variables {
fn(v)
}
for _, v := range p.RawConfig.Variables {
fn(v)
}
}
}
func (n *GraphNodeConfigResource) Name() string {
result := n.Resource.Id()
if n.Destroy {
result += " (destroy)"
}
return result
}
// GraphNodeDotter impl.
func (n *GraphNodeConfigResource) DotNode(name string, opts *GraphDotOpts) *dot.Node {
if n.Destroy && !opts.Verbose {
return nil
}
return dot.NewNode(name, map[string]string{
"label": n.Name(),
"shape": "box",
})
}
// GraphNodeFlattenable impl.
func (n *GraphNodeConfigResource) Flatten(p []string) (dag.Vertex, error) {
return &GraphNodeConfigResourceFlat{
GraphNodeConfigResource: n,
PathValue: p,
}, nil
}
// GraphNodeDynamicExpandable impl.
func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
state, lock := ctx.State()
lock.RLock()
defer lock.RUnlock()
// Start creating the steps
steps := make([]GraphTransformer, 0, 5)
// Expand counts.
steps = append(steps, &ResourceCountTransformerOld{
Resource: n.Resource,
Destroy: n.Destroy,
Targets: n.Targets,
})
// Additional destroy modifications.
if n.Destroy {
// If we're destroying a primary or tainted resource, we want to
// expand orphans, which have all the same semantics in a destroy
// as a primary or tainted resource.
steps = append(steps, &OrphanTransformer{
Resource: n.Resource,
State: state,
View: n.Resource.Id(),
})
steps = append(steps, &DeposedTransformer{
State: state,
View: n.Resource.Id(),
})
}
// We always want to apply targeting
steps = append(steps, &TargetsTransformer{
ParsedTargets: n.Targets,
Destroy: n.Destroy,
})
// Always end with the root being added
steps = append(steps, &RootTransformer{})
// Build the graph
b := &BasicGraphBuilder{Steps: steps, Validate: true}
return b.Build(ctx.Path())
}
// GraphNodeAddressable impl.
func (n *GraphNodeConfigResource) ResourceAddress() *ResourceAddress {
return &ResourceAddress{
Path: n.Path[1:],
Index: -1,
InstanceType: TypePrimary,
Name: n.Resource.Name,
Type: n.Resource.Type,
Mode: n.Resource.Mode,
}
}
// GraphNodeTargetable impl.
func (n *GraphNodeConfigResource) SetTargets(targets []ResourceAddress) {
n.Targets = targets
}
// GraphNodeEvalable impl.
func (n *GraphNodeConfigResource) EvalTree() EvalNode {
return &EvalSequence{
Nodes: []EvalNode{
&EvalInterpolate{Config: n.Resource.RawCount},
&EvalOpFilter{
Ops: []walkOperation{walkValidate},
Node: &EvalValidateCount{Resource: n.Resource},
},
&EvalCountFixZeroOneBoundary{Resource: n.Resource},
},
}
}
// GraphNodeProviderConsumer
func (n *GraphNodeConfigResource) ProvidedBy() []string {
return []string{resourceProvider(n.Resource.Type, n.Resource.Provider)}
}
// GraphNodeProvisionerConsumer
func (n *GraphNodeConfigResource) ProvisionedBy() []string {
result := make([]string, len(n.Resource.Provisioners))
for i, p := range n.Resource.Provisioners {
result[i] = p.Type
}
return result
}
// GraphNodeDestroyable
func (n *GraphNodeConfigResource) DestroyNode() GraphNodeDestroy {
// If we're already a destroy node, then don't do anything
if n.Destroy {
return nil
}
result := &graphNodeResourceDestroy{
GraphNodeConfigResource: *n.Copy(),
Original: n,
}
result.Destroy = true
return result
}
// GraphNodeNoopPrunable
func (n *GraphNodeConfigResource) Noop(opts *NoopOpts) bool {
log.Printf("[DEBUG] Checking resource noop: %s", n.Name())
// We don't have any noop optimizations for destroy nodes yet
if n.Destroy {
log.Printf("[DEBUG] Destroy node, not a noop")
return false
}
// If there is no diff, then we aren't a noop since something needs to
// be done (such as a plan). We only check if we're a noop in a diff.
if opts.Diff == nil || opts.Diff.Empty() {
log.Printf("[DEBUG] No diff, not a noop")
return false
}
// If the count has any interpolations, we can't prune this node since
// we need to be sure to evaluate the count so that splat variables work
// later (which need to know the full count).
if len(n.Resource.RawCount.Interpolations) > 0 {
log.Printf("[DEBUG] Count has interpolations, not a noop")
return false
}
// If we have no module diff, we're certainly a noop. This is because
// it means there is a diff, and that the module we're in just isn't
// in it, meaning we're not doing anything.
if opts.ModDiff == nil || opts.ModDiff.Empty() {
log.Printf("[DEBUG] No mod diff, treating resource as a noop")
return true
}
// Grab the ID which is the prefix (in the case count > 0 at some point)
prefix := n.Resource.Id()
// Go through the diff and if there are any with our name on it, keep us
found := false
for k, _ := range opts.ModDiff.Resources {
if strings.HasPrefix(k, prefix) {
log.Printf("[DEBUG] Diff has %s, resource is not a noop", k)
found = true
break
}
}
log.Printf("[DEBUG] Final noop value: %t", !found)
return !found
}
// Same as GraphNodeConfigResource, but for flattening
type GraphNodeConfigResourceFlat struct {
*GraphNodeConfigResource
PathValue []string
}
func (n *GraphNodeConfigResourceFlat) Name() string {
return fmt.Sprintf(
"%s.%s", modulePrefixStr(n.PathValue), n.GraphNodeConfigResource.Name())
}
func (n *GraphNodeConfigResourceFlat) Path() []string {
return n.PathValue
}
func (n *GraphNodeConfigResourceFlat) DependableName() []string {
return modulePrefixList(
n.GraphNodeConfigResource.DependableName(),
modulePrefixStr(n.PathValue))
}
func (n *GraphNodeConfigResourceFlat) DependentOn() []string {
prefix := modulePrefixStr(n.PathValue)
return modulePrefixList(
n.GraphNodeConfigResource.DependentOn(),
prefix)
}
func (n *GraphNodeConfigResourceFlat) ProvidedBy() []string {
prefix := modulePrefixStr(n.PathValue)
return modulePrefixList(
n.GraphNodeConfigResource.ProvidedBy(),
prefix)
}
func (n *GraphNodeConfigResourceFlat) ProvisionedBy() []string {
prefix := modulePrefixStr(n.PathValue)
return modulePrefixList(
n.GraphNodeConfigResource.ProvisionedBy(),
prefix)
}
// GraphNodeDestroyable impl.
func (n *GraphNodeConfigResourceFlat) DestroyNode() GraphNodeDestroy {
// Get our parent destroy node. If we don't have any, just return
raw := n.GraphNodeConfigResource.DestroyNode()
if raw == nil {
return nil
}
node, ok := raw.(*graphNodeResourceDestroy)
if !ok {
panic(fmt.Sprintf("unknown destroy node: %s %T", dag.VertexName(raw), raw))
}
// Otherwise, wrap it so that it gets the proper module treatment.
return &graphNodeResourceDestroyFlat{
graphNodeResourceDestroy: node,
PathValue: n.PathValue,
FlatCreateNode: n,
}
}
type graphNodeResourceDestroyFlat struct {
*graphNodeResourceDestroy
PathValue []string
// Needs to be able to properly yield back a flattened create node to prevent
FlatCreateNode *GraphNodeConfigResourceFlat
}
func (n *graphNodeResourceDestroyFlat) Name() string {
return fmt.Sprintf(
"%s.%s", modulePrefixStr(n.PathValue), n.graphNodeResourceDestroy.Name())
}
func (n *graphNodeResourceDestroyFlat) Path() []string {
return n.PathValue
}
func (n *graphNodeResourceDestroyFlat) CreateNode() dag.Vertex {
return n.FlatCreateNode
}
func (n *graphNodeResourceDestroyFlat) ProvidedBy() []string {
prefix := modulePrefixStr(n.PathValue)
return modulePrefixList(
n.GraphNodeConfigResource.ProvidedBy(),
prefix)
}
// graphNodeResourceDestroy represents the logical destruction of a
// resource. This node doesn't mean it will be destroyed for sure, but
// instead that if a destroy were to happen, it must happen at this point.
type graphNodeResourceDestroy struct {
GraphNodeConfigResource
Original *GraphNodeConfigResource
}
func (n *graphNodeResourceDestroy) CreateBeforeDestroy() bool {
// CBD is enabled if the resource enables it
return n.Original.Resource.Lifecycle.CreateBeforeDestroy && n.Destroy
}
func (n *graphNodeResourceDestroy) CreateNode() dag.Vertex {
return n.Original
}
func (n *graphNodeResourceDestroy) DestroyInclude(d *ModuleDiff, s *ModuleState) bool {
if n.Destroy {
return n.destroyInclude(d, s)
}
return true
}
func (n *graphNodeResourceDestroy) destroyInclude(
d *ModuleDiff, s *ModuleState) bool {
// Get the count, and specifically the raw value of the count
// (with interpolations and all). If the count is NOT a static "1",
// then we keep the destroy node no matter what.
//
// The reasoning for this is complicated and not intuitively obvious,
// but I attempt to explain it below.
//
// The destroy transform works by generating the worst case graph,
// with worst case being the case that every resource already exists
// and needs to be destroy/created (force-new). There is a single important
// edge case where this actually results in a real-life cycle: if a
// create-before-destroy (CBD) resource depends on a non-CBD resource.
// Imagine a EC2 instance "foo" with CBD depending on a security
// group "bar" without CBD, and conceptualize the worst case destroy
// order:
//
// 1.) SG must be destroyed (non-CBD)
// 2.) SG must be created/updated
// 3.) EC2 instance must be created (CBD, requires the SG be made)
// 4.) EC2 instance must be destroyed (requires SG be destroyed)
//
// Except, #1 depends on #4, since the SG can't be destroyed while
// an EC2 instance is using it (AWS API requirements). As you can see,
// this is a real life cycle that can't be automatically reconciled
// except under two conditions:
//
// 1.) SG is also CBD. This doesn't work 100% of the time though
// since the non-CBD resource might not support CBD. To make matters
// worse, the entire transitive closure of dependencies must be
// CBD (if the SG depends on a VPC, you have the same problem).
// 2.) EC2 must not CBD. This can't happen automatically because CBD
// is used as a way to ensure zero (or minimal) downtime Terraform
// applies, and it isn't acceptable for TF to ignore this request,
// since it can result in unexpected downtime.
//
// Therefore, we compromise with this edge case here: if there is
// a static count of "1", we prune the diff to remove cycles during a
// graph optimization path if we don't see the resource in the diff.
// If the count is set to ANYTHING other than a static "1" (variable,
// computed attribute, static number greater than 1), then we keep the
// destroy, since it is required for dynamic graph expansion to find
// orphan count objects.
//
// This isn't ideal logic, but its strictly better without introducing
// new impossibilities. It breaks the cycle in practical cases, and the
// cycle comes back in no cases we've found to be practical, but just
// as the cycle would already exist without this anyways.
count := n.Original.Resource.RawCount
if raw := count.Raw[count.Key]; raw != "1" {
return true
}
// Okay, we're dealing with a static count. There are a few ways
// to include this resource.
prefix := n.Original.Resource.Id()
// If we're present in the diff proper, then keep it. We're looking
// only for resources in the diff that match our resource or a count-index
// of our resource that are marked for destroy.
if d != nil {
for k, v := range d.Resources {
match := k == prefix || strings.HasPrefix(k, prefix+".")
if match && v.GetDestroy() {
return true
}
}
}
// If we're in the state as a primary in any form, then keep it.
// This does a prefix check so it will also catch orphans on count
// decreases to "1".
if s != nil {
for k, v := range s.Resources {
// Ignore exact matches
if k == prefix {
continue
}
// Ignore anything that doesn't have a "." afterwards so that
// we only get our own resource and any counts on it.
if !strings.HasPrefix(k, prefix+".") {
continue
}
// Ignore exact matches and the 0'th index. We only care
// about if there is a decrease in count.
if k == prefix+".0" {
continue
}
if v.Primary != nil {
return true
}
}
// If we're in the state as _both_ "foo" and "foo.0", then
// keep it, since we treat the latter as an orphan.
_, okOne := s.Resources[prefix]
_, okTwo := s.Resources[prefix+".0"]
if okOne && okTwo {
return true
}
}
return false
}