Merge pull request #10518 from hashicorp/b-graph

command/graph: work with new graphs
This commit is contained in:
Mitchell Hashimoto 2016-12-05 21:36:33 -08:00 committed by GitHub
commit a4ceb4a772
17 changed files with 272 additions and 144 deletions

View File

@ -20,6 +20,7 @@ func (c *GraphCommand) Run(args []string) int {
var moduleDepth int
var verbose bool
var drawCycles bool
var graphTypeStr string
args = c.Meta.process(args, false)
@ -27,6 +28,7 @@ func (c *GraphCommand) Run(args []string) int {
c.addModuleDepthFlag(cmdFlags, &moduleDepth)
cmdFlags.BoolVar(&verbose, "verbose", false, "verbose")
cmdFlags.BoolVar(&drawCycles, "draw-cycles", false, "draw-cycles")
cmdFlags.StringVar(&graphTypeStr, "type", "", "type")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
@ -48,7 +50,7 @@ func (c *GraphCommand) Run(args []string) int {
}
}
ctx, _, err := c.Context(contextOpts{
ctx, planFile, err := c.Context(contextOpts{
Path: path,
StatePath: "",
})
@ -57,9 +59,25 @@ func (c *GraphCommand) Run(args []string) int {
return 1
}
// Determine the graph type
graphType := terraform.GraphTypePlan
if planFile {
graphType = terraform.GraphTypeApply
}
if graphTypeStr != "" {
v, ok := terraform.GraphTypeMap[graphTypeStr]
if !ok {
c.Ui.Error(fmt.Sprintf("Invalid graph type requested: %s", graphTypeStr))
return 1
}
graphType = v
}
// Skip validation during graph generation - we want to see the graph even if
// it is invalid for some reason.
g, err := ctx.Graph(&terraform.ContextGraphOpts{
g, err := ctx.Graph(graphType, &terraform.ContextGraphOpts{
Verbose: verbose,
Validate: false,
})
@ -87,25 +105,28 @@ func (c *GraphCommand) Help() string {
helpText := `
Usage: terraform graph [options] [DIR]
Outputs the visual dependency graph of Terraform resources according to
Outputs the visual execution graph of Terraform resources according to
configuration files in DIR (or the current directory if omitted).
The graph is outputted in DOT format. The typical program that can
read this format is GraphViz, but many web services are also available
to read this format.
The -type flag can be used to control the type of graph shown. Terraform
creates different graphs for different operations. See the options below
for the list of types supported. The default type is "plan" if a
configuration is given, and "apply" if a plan file is passed as an
argument.
Options:
-draw-cycles Highlight any cycles in the graph with colored edges.
This helps when diagnosing cycle errors.
-draw-cycles Highlight any cycles in the graph with colored edges.
This helps when diagnosing cycle errors.
-module-depth=n The maximum depth to expand modules. By default this is
-1, which will expand resources within all modules.
-no-color If specified, output won't contain any color.
-verbose Generate a verbose, "worst-case" graph, with all nodes
for potential operations in place.
-no-color If specified, output won't contain any color.
-type=plan Type of graph to output. Can be: plan, plan-destroy, apply,
legacy.
`
return strings.TrimSpace(helpText)

View File

@ -80,6 +80,19 @@ func TestGraph_noArgs(t *testing.T) {
func TestGraph_plan(t *testing.T) {
planPath := testPlanFile(t, &terraform.Plan{
Diff: &terraform.Diff{
Modules: []*terraform.ModuleDiff{
&terraform.ModuleDiff{
Path: []string{"root"},
Resources: map[string]*terraform.InstanceDiff{
"test_instance.bar": &terraform.InstanceDiff{
Destroy: true,
},
},
},
},
},
Module: testModule(t, "graph"),
})

View File

@ -85,8 +85,29 @@ func (v *marshalVertex) dot(g *marshalGraph) []byte {
if graphName == "" {
graphName = "root"
}
buf.WriteString(fmt.Sprintf(`"[%s] %s"`, graphName, v.Name))
writeAttrs(&buf, v.Attrs)
name := v.Name
attrs := v.Attrs
if v.graphNodeDotter != nil {
node := v.graphNodeDotter.DotNode(name, nil)
if node == nil {
return []byte{}
}
newAttrs := make(map[string]string)
for k, v := range attrs {
newAttrs[k] = v
}
for k, v := range node.Attrs {
newAttrs[k] = v
}
name = node.Name
attrs = newAttrs
}
buf.WriteString(fmt.Sprintf(`"[%s] %s"`, graphName, name))
writeAttrs(&buf, attrs)
buf.WriteByte('\n')
return buf.Bytes()
@ -145,7 +166,7 @@ func (g *marshalGraph) writeBody(opts *DotOpts, w *indentWriter) {
skip := map[string]bool{}
for _, v := range g.Vertices {
if !v.graphNodeDotter {
if v.graphNodeDotter == nil {
skip[v.ID] = true
continue
}

View File

@ -102,32 +102,24 @@ type marshalVertex struct {
Attrs map[string]string `json:",omitempty"`
// This is to help transition from the old Dot interfaces. We record if the
// node was a GraphNodeDotter here, so we know if it should be included in the
// dot output
graphNodeDotter bool
// node was a GraphNodeDotter here, so we can call it to get attributes.
graphNodeDotter GraphNodeDotter
}
func newMarshalVertex(v Vertex) *marshalVertex {
dn, ok := v.(GraphNodeDotter)
if !ok {
dn = nil
}
return &marshalVertex{
ID: marshalVertexID(v),
Name: VertexName(v),
Attrs: make(map[string]string),
graphNodeDotter: isDotter(v),
graphNodeDotter: dn,
}
}
func isDotter(v Vertex) bool {
dn, isDotter := v.(GraphNodeDotter)
dotOpts := &DotOpts{
Verbose: true,
DrawCycles: true,
}
if isDotter && dn.DotNode("fake", dotOpts) == nil {
isDotter = false
}
return isDotter
}
// vertices is a sort.Interface implementation for sorting vertices by ID
type vertices []*marshalVertex

View File

@ -34,6 +34,27 @@ func TestGraphDot_basic(t *testing.T) {
}
}
func TestGraphDot_attrs(t *testing.T) {
var g Graph
g.Add(&testGraphNodeDotter{
Result: &DotNode{
Name: "foo",
Attrs: map[string]string{"foo": "bar"},
},
})
actual := strings.TrimSpace(string(g.Dot(nil)))
expected := strings.TrimSpace(testGraphDotAttrsStr)
if actual != expected {
t.Fatalf("bad: %s", actual)
}
}
type testGraphNodeDotter struct{ Result *DotNode }
func (n *testGraphNodeDotter) Name() string { return n.Result.Name }
func (n *testGraphNodeDotter) DotNode(string, *DotOpts) *DotNode { return n.Result }
const testGraphDotBasicStr = `digraph {
compound = "true"
newrank = "true"
@ -50,6 +71,14 @@ const testGraphDotEmptyStr = `digraph {
}
}`
const testGraphDotAttrsStr = `digraph {
compound = "true"
newrank = "true"
subgraph "root" {
"[root] foo" [foo = "bar"]
}
}`
func TestGraphJSON_empty(t *testing.T) {
var g Graph
g.Add(1)

View File

@ -189,13 +189,55 @@ func NewContext(opts *ContextOpts) (*Context, error) {
}
type ContextGraphOpts struct {
// If true, validates the graph structure (checks for cycles).
Validate bool
Verbose bool
// Legacy graphs only: won't prune the graph
Verbose bool
}
// Graph returns the graph for this config.
func (c *Context) Graph(g *ContextGraphOpts) (*Graph, error) {
return c.graphBuilder(g).Build(RootModulePath)
// Graph returns the graph used for the given operation type.
//
// The most extensive or complex graph type is GraphTypePlan.
func (c *Context) Graph(typ GraphType, opts *ContextGraphOpts) (*Graph, error) {
if opts == nil {
opts = &ContextGraphOpts{Validate: true}
}
switch typ {
case GraphTypeApply:
return (&ApplyGraphBuilder{
Module: c.module,
Diff: c.diff,
State: c.state,
Providers: c.components.ResourceProviders(),
Provisioners: c.components.ResourceProvisioners(),
Destroy: c.destroy,
Validate: opts.Validate,
}).Build(RootModulePath)
case GraphTypePlan:
return (&PlanGraphBuilder{
Module: c.module,
State: c.state,
Providers: c.components.ResourceProviders(),
Targets: c.targets,
Validate: opts.Validate,
}).Build(RootModulePath)
case GraphTypePlanDestroy:
return (&DestroyPlanGraphBuilder{
Module: c.module,
State: c.state,
Targets: c.targets,
Validate: opts.Validate,
}).Build(RootModulePath)
case GraphTypeLegacy:
return c.graphBuilder(opts).Build(RootModulePath)
}
return nil, fmt.Errorf("unknown graph type: %s", typ)
}
// GraphBuilder returns the GraphBuilder that will be used to create
@ -360,7 +402,7 @@ func (c *Context) Input(mode InputMode) error {
if mode&InputModeProvider != 0 {
// Build the graph
graph, err := c.Graph(&ContextGraphOpts{Validate: true})
graph, err := c.Graph(GraphTypeLegacy, nil)
if err != nil {
return err
}
@ -390,20 +432,11 @@ func (c *Context) Apply() (*State, error) {
X_legacyGraph := experiment.Enabled(experiment.X_legacyGraph)
// Build the graph.
var graph *Graph
var err error
graphType := GraphTypeLegacy
if !X_legacyGraph {
graph, err = (&ApplyGraphBuilder{
Module: c.module,
Diff: c.diff,
State: c.state,
Providers: c.components.ResourceProviders(),
Provisioners: c.components.ResourceProvisioners(),
Destroy: c.destroy,
}).Build(RootModulePath)
} else {
graph, err = c.Graph(&ContextGraphOpts{Validate: true})
graphType = GraphTypeApply
}
graph, err := c.Graph(graphType, nil)
if err != nil {
return nil, err
}
@ -475,26 +508,15 @@ func (c *Context) Plan() (*Plan, error) {
X_legacyGraph := experiment.Enabled(experiment.X_legacyGraph)
// Build the graph.
var graph *Graph
var err error
graphType := GraphTypeLegacy
if !X_legacyGraph {
if c.destroy {
graph, err = (&DestroyPlanGraphBuilder{
Module: c.module,
State: c.state,
Targets: c.targets,
}).Build(RootModulePath)
graphType = GraphTypePlanDestroy
} else {
graph, err = (&PlanGraphBuilder{
Module: c.module,
State: c.state,
Providers: c.components.ResourceProviders(),
Targets: c.targets,
}).Build(RootModulePath)
graphType = GraphTypePlan
}
} else {
graph, err = c.Graph(&ContextGraphOpts{Validate: true})
}
graph, err := c.Graph(graphType, nil)
if err != nil {
return nil, err
}
@ -522,7 +544,7 @@ func (c *Context) Plan() (*Plan, error) {
if X_legacyGraph {
// Now that we have a diff, we can build the exact graph that Apply will use
// and catch any possible cycles during the Plan phase.
if _, err := c.Graph(&ContextGraphOpts{Validate: true}); err != nil {
if _, err := c.Graph(GraphTypeLegacy, nil); err != nil {
return nil, err
}
}
@ -548,7 +570,7 @@ func (c *Context) Refresh() (*State, error) {
c.state = c.state.DeepCopy()
// Build the graph
graph, err := c.Graph(&ContextGraphOpts{Validate: true})
graph, err := c.Graph(GraphTypeLegacy, nil)
if err != nil {
return nil, err
}
@ -619,7 +641,7 @@ func (c *Context) Validate() ([]string, []error) {
// We also validate the graph generated here, but this graph doesn't
// necessarily match the graph that Plan will generate, so we'll validate the
// graph again later after Planning.
graph, err := c.Graph(&ContextGraphOpts{Validate: true})
graph, err := c.Graph(GraphTypeLegacy, nil)
if err != nil {
return nil, []error{err}
}

View File

@ -0,0 +1,26 @@
package terraform
//go:generate stringer -type=GraphType context_graph_type.go
// GraphType is an enum of the type of graph to create with a Context.
// The values of the constants may change so they shouldn't be depended on;
// always use the constant name.
type GraphType byte
const (
GraphTypeInvalid GraphType = 0
GraphTypeLegacy GraphType = iota
GraphTypePlan
GraphTypePlanDestroy
GraphTypeApply
)
// GraphTypeMap is a mapping of human-readable string to GraphType. This
// is useful to use as the mechanism for human input for configurable
// graph types.
var GraphTypeMap = map[string]GraphType{
"apply": GraphTypeApply,
"plan": GraphTypePlan,
"plan-destroy": GraphTypePlanDestroy,
"legacy": GraphTypeLegacy,
}

View File

@ -33,13 +33,16 @@ type ApplyGraphBuilder struct {
// Destroy, if true, represents a pure destroy operation
Destroy bool
// Validate will do structural validation of the graph.
Validate bool
}
// See GraphBuilder
func (b *ApplyGraphBuilder) Build(path []string) (*Graph, error) {
return (&BasicGraphBuilder{
Steps: b.Steps(),
Validate: true,
Validate: b.Validate,
Name: "ApplyGraphBuilder",
}).Build(path)
}
@ -47,10 +50,9 @@ func (b *ApplyGraphBuilder) Build(path []string) (*Graph, error) {
// See GraphBuilder
func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
// Custom factory for creating providers.
providerFactory := func(name string, path []string) GraphNodeProvider {
concreteProvider := func(a *NodeAbstractProvider) dag.Vertex {
return &NodeApplyableProvider{
NameValue: name,
PathValue: path,
NodeAbstractProvider: a,
}
}
@ -87,7 +89,7 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
),
// Create all the providers
&MissingProviderTransformer{Providers: b.Providers, Factory: providerFactory},
&MissingProviderTransformer{Providers: b.Providers, Concrete: concreteProvider},
&ProviderTransformer{},
&DisableProviderTransformer{},
&ParentProviderTransformer{},

View File

@ -19,13 +19,16 @@ type DestroyPlanGraphBuilder struct {
// Targets are resources to target
Targets []string
// Validate will do structural validation of the graph.
Validate bool
}
// See GraphBuilder
func (b *DestroyPlanGraphBuilder) Build(path []string) (*Graph, error) {
return (&BasicGraphBuilder{
Steps: b.Steps(),
Validate: true,
Validate: b.Validate,
Name: "DestroyPlanGraphBuilder",
}).Build(path)
}

View File

@ -2,6 +2,7 @@ package terraform
import (
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/dag"
)
// ImportGraphBuilder implements GraphBuilder and is responsible for building
@ -38,10 +39,9 @@ func (b *ImportGraphBuilder) Steps() []GraphTransformer {
}
// Custom factory for creating providers.
providerFactory := func(name string, path []string) GraphNodeProvider {
concreteProvider := func(a *NodeAbstractProvider) dag.Vertex {
return &NodeApplyableProvider{
NameValue: name,
PathValue: path,
NodeAbstractProvider: a,
}
}
@ -53,7 +53,7 @@ func (b *ImportGraphBuilder) Steps() []GraphTransformer {
&ImportStateTransformer{Targets: b.ImportTargets},
// Provider-related transformations
&MissingProviderTransformer{Providers: b.Providers, Factory: providerFactory},
&MissingProviderTransformer{Providers: b.Providers, Concrete: concreteProvider},
&ProviderTransformer{},
&DisableProviderTransformerOld{},
&PruneProviderTransformer{},

View File

@ -31,13 +31,16 @@ type PlanGraphBuilder struct {
// DisableReduce, if true, will not reduce the graph. Great for testing.
DisableReduce bool
// Validate will do structural validation of the graph.
Validate bool
}
// See GraphBuilder
func (b *PlanGraphBuilder) Build(path []string) (*Graph, error) {
return (&BasicGraphBuilder{
Steps: b.Steps(),
Validate: true,
Validate: b.Validate,
Name: "PlanGraphBuilder",
}).Build(path)
}
@ -45,10 +48,9 @@ func (b *PlanGraphBuilder) Build(path []string) (*Graph, error) {
// See GraphBuilder
func (b *PlanGraphBuilder) Steps() []GraphTransformer {
// Custom factory for creating providers.
providerFactory := func(name string, path []string) GraphNodeProvider {
concreteProvider := func(a *NodeAbstractProvider) dag.Vertex {
return &NodeApplyableProvider{
NameValue: name,
PathValue: path,
NodeAbstractProvider: a,
}
}
@ -95,7 +97,7 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
&TargetsTransformer{Targets: b.Targets},
// Create all the providers
&MissingProviderTransformer{Providers: b.Providers, Factory: providerFactory},
&MissingProviderTransformer{Providers: b.Providers, Concrete: concreteProvider},
&ProviderTransformer{},
&DisableProviderTransformer{},
&ParentProviderTransformer{},

View File

@ -0,0 +1,16 @@
// Code generated by "stringer -type=GraphType context_graph_type.go"; DO NOT EDIT
package terraform
import "fmt"
const _GraphType_name = "GraphTypeInvalidGraphTypeLegacyGraphTypePlanGraphTypePlanDestroyGraphTypeApply"
var _GraphType_index = [...]uint8{0, 16, 31, 44, 64, 78}
func (i GraphType) String() string {
if i >= GraphType(len(_GraphType_index)-1) {
return fmt.Sprintf("GraphType(%d)", i)
}
return _GraphType_name[_GraphType_index[i]:_GraphType_index[i+1]]
}

View File

@ -1,61 +1,8 @@
package terraform
import (
"fmt"
"github.com/hashicorp/terraform/config"
)
// NodeApplyableProvider represents a provider during an apply.
//
// NOTE: There is a lot of logic here that will be shared with non-Apply.
// The plan is to abstract that eventually into an embedded abstract struct.
type NodeApplyableProvider struct {
NameValue string
PathValue []string
Config *config.ProviderConfig
}
func (n *NodeApplyableProvider) Name() string {
result := fmt.Sprintf("provider.%s", n.NameValue)
if len(n.PathValue) > 1 {
result = fmt.Sprintf("%s.%s", modulePrefixStr(n.PathValue), result)
}
return result
}
// GraphNodeSubPath
func (n *NodeApplyableProvider) Path() []string {
return n.PathValue
}
// GraphNodeReferencer
func (n *NodeApplyableProvider) References() []string {
if n.Config == nil {
return nil
}
return ReferencesFromConfig(n.Config.RawConfig)
}
// GraphNodeProvider
func (n *NodeApplyableProvider) ProviderName() string {
return n.NameValue
}
// GraphNodeProvider
func (n *NodeApplyableProvider) ProviderConfig() *config.RawConfig {
if n.Config == nil {
return nil
}
return n.Config.RawConfig
}
// GraphNodeAttachProvider
func (n *NodeApplyableProvider) AttachProvider(c *config.ProviderConfig) {
n.Config = c
*NodeAbstractProvider
}
// GraphNodeEvalable

View File

@ -4,8 +4,13 @@ import (
"fmt"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/dag"
)
// ConcreteProviderNodeFunc is a callback type used to convert an
// abstract provider to a concrete one of some type.
type ConcreteProviderNodeFunc func(*NodeAbstractProvider) dag.Vertex
// NodeAbstractProvider represents a provider that has no associated operations.
// It registers all the common interfaces across operations for providers.
type NodeAbstractProvider struct {
@ -60,3 +65,14 @@ func (n *NodeAbstractProvider) ProviderConfig() *config.RawConfig {
func (n *NodeAbstractProvider) AttachProvider(c *config.ProviderConfig) {
n.Config = c
}
// GraphNodeDotter impl.
func (n *NodeAbstractProvider) DotNode(name string, opts *dag.DotOpts) *dag.DotNode {
return &dag.DotNode{
Name: name,
Attrs: map[string]string{
"label": n.Name(),
"shape": "diamond",
},
}
}

View File

@ -166,3 +166,14 @@ func (n *NodeAbstractResource) AttachResourceState(s *ResourceState) {
func (n *NodeAbstractResource) AttachResourceConfig(c *config.Resource) {
n.Config = c
}
// GraphNodeDotter impl.
func (n *NodeAbstractResource) DotNode(name string, opts *dag.DotOpts) *dag.DotNode {
return &dag.DotNode{
Name: name,
Attrs: map[string]string{
"label": n.Name(),
"shape": "box",
},
}
}

View File

@ -114,15 +114,15 @@ type MissingProviderTransformer struct {
// Providers is the list of providers we support.
Providers []string
// Factory, if set, overrides how the providers are made.
Factory func(name string, path []string) GraphNodeProvider
// Concrete, if set, overrides how the providers are made.
Concrete ConcreteProviderNodeFunc
}
func (t *MissingProviderTransformer) Transform(g *Graph) error {
// Initialize factory
if t.Factory == nil {
t.Factory = func(name string, path []string) GraphNodeProvider {
return &graphNodeProvider{ProviderNameValue: name}
if t.Concrete == nil {
t.Concrete = func(a *NodeAbstractProvider) dag.Vertex {
return &graphNodeProvider{ProviderNameValue: a.NameValue}
}
}
@ -177,7 +177,10 @@ func (t *MissingProviderTransformer) Transform(g *Graph) error {
}
// Add the missing provider node to the graph
v := t.Factory(p, path).(dag.Vertex)
v := t.Concrete(&NodeAbstractProvider{
NameValue: p,
PathValue: path,
}).(dag.Vertex)
if len(path) > 0 {
if fn, ok := v.(GraphNodeFlattenable); ok {
var err error

View File

@ -25,16 +25,20 @@ The graph is outputted in DOT format. The typical program that can
read this format is GraphViz, but many web services are also available
to read this format.
The -type flag can be used to control the type of graph shown. Terraform
creates different graphs for different operations. See the options below
for the list of types supported. The default type is "plan" if a
configuration is given, and "apply" if a plan file is passed as an
argument.
Options:
* `-draw-cycles` - Highlight any cycles in the graph with colored edges.
This helps when diagnosing cycle errors.
* `-module-depth=n` - The maximum depth to expand modules. By default this is
-1, which will expand all modules.
* `-no-color` - If specified, output won't contain any color.
* `-verbose` - Generate a verbose, "worst-case" graph, with all nodes
for potential operations in place.
* `-type=plan` - Type of graph to output. Can be: plan, plan-destroy, apply, legacy.
## Generating Images