From 93fbb9ea8f17ecbb7799195011e6414654d93fef Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 1 Jul 2014 10:02:13 -0700 Subject: [PATCH] command/graph --- command/apply.go | 79 +++++----------------------------------------- command/command.go | 68 +++++++++++++++++++++++++++++++++++++++ command/graph.go | 75 +++++++++++++++++++++++++++++++++++++++++++ commands.go | 7 ++++ 4 files changed, 158 insertions(+), 71 deletions(-) create mode 100644 command/command.go create mode 100644 command/graph.go diff --git a/command/apply.go b/command/apply.go index f1ab81cce..4245187d8 100644 --- a/command/apply.go +++ b/command/apply.go @@ -6,7 +6,6 @@ import ( "os" "strings" - "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -54,26 +53,14 @@ func (c *ApplyCommand) Run(args []string) int { // Attempt to read a plan from the path given. This is how we test that // it is a plan or not (kind of jank, but if it quacks like a duck...) - var plan *terraform.Plan - f, err := os.Open(configPath) - if err == nil { - plan, err = terraform.ReadPlan(f) - f.Close() - if err != nil { - // Make sure the plan is nil so that we try to load as - // configuration. - plan = nil - } + planStatePath := statePath + if init { + planStatePath = "" } - - if plan == nil { - // No plan was given, so we're loading from configuration. Generate - // the plan given the configuration. - plan, err = c.configToPlan(tf, init, statePath, configPath) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } + plan, err := PlanArg(configPath, planStatePath, tf) + if err != nil { + c.Ui.Error(err.Error()) + return 1 } state, err := tf.Apply(plan) @@ -83,7 +70,7 @@ func (c *ApplyCommand) Run(args []string) int { } // Write state out to the file - f, err = os.Create(stateOutPath) + f, err := os.Create(stateOutPath) if err == nil { err = terraform.WriteState(state, f) f.Close() @@ -120,53 +107,3 @@ Options: func (c *ApplyCommand) Synopsis() string { return "Builds or changes infrastructure" } - -func (c *ApplyCommand) configToPlan( - tf *terraform.Terraform, - init bool, - statePath string, - configPath string) (*terraform.Plan, error) { - if !init { - if _, err := os.Stat(statePath); err != nil { - return nil, fmt.Errorf( - "There was an error reading the state file. The path\n"+ - "and error are shown below. If you're trying to build a\n"+ - "brand new infrastructure, explicitly pass the '-init'\n"+ - "flag to Terraform to tell it it is okay to build new\n"+ - "state.\n\n"+ - "Path: %s\n"+ - "Error: %s", - statePath, - err) - } - } - - // Load up the state - var state *terraform.State - if !init { - f, err := os.Open(statePath) - if err == nil { - state, err = terraform.ReadState(f) - f.Close() - } - - if err != nil { - return nil, fmt.Errorf("Error loading state: %s", err) - } - } - - config, err := config.Load(configPath) - if err != nil { - return nil, fmt.Errorf("Error loading config: %s", err) - } - - plan, err := tf.Plan(&terraform.PlanOpts{ - Config: config, - State: state, - }) - if err != nil { - return nil, fmt.Errorf("Error running plan: %s", err) - } - - return plan, nil -} diff --git a/command/command.go b/command/command.go new file mode 100644 index 000000000..d42584d23 --- /dev/null +++ b/command/command.go @@ -0,0 +1,68 @@ +package command + +import ( + "fmt" + "os" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/terraform" +) + +func PlanArg( + path string, + statePath string, + tf *terraform.Terraform) (*terraform.Plan, error) { + // First try to just read the plan directly from the path given. + f, err := os.Open(path) + if err == nil { + plan, err := terraform.ReadPlan(f) + f.Close() + if err == nil { + return plan, nil + } + } + + if statePath != "" { + if _, err := os.Stat(statePath); err != nil { + return nil, fmt.Errorf( + "There was an error reading the state file. The path\n"+ + "and error are shown below. If you're trying to build a\n"+ + "brand new infrastructure, explicitly pass the '-init'\n"+ + "flag to Terraform to tell it it is okay to build new\n"+ + "state.\n\n"+ + "Path: %s\n"+ + "Error: %s", + statePath, + err) + } + } + + // Load up the state + var state *terraform.State + if statePath != "" { + f, err := os.Open(statePath) + if err == nil { + state, err = terraform.ReadState(f) + f.Close() + } + + if err != nil { + return nil, fmt.Errorf("Error loading state: %s", err) + } + } + + config, err := config.Load(path) + if err != nil { + return nil, fmt.Errorf("Error loading config: %s", err) + } + + plan, err := tf.Plan(&terraform.PlanOpts{ + Config: config, + State: state, + }) + if err != nil { + return nil, fmt.Errorf("Error running plan: %s", err) + } + + return plan, nil +} diff --git a/command/graph.go b/command/graph.go new file mode 100644 index 000000000..bca522e9d --- /dev/null +++ b/command/graph.go @@ -0,0 +1,75 @@ +package command + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/digraph" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +// GraphCommand is a Command implementation that takes a Terraform +// configuration and outputs the dependency tree in graphical form. +type GraphCommand struct { + TFConfig *terraform.Config + Ui cli.Ui +} + +func (c *GraphCommand) Run(args []string) int { + cmdFlags := flag.NewFlagSet("graph", flag.ContinueOnError) + cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } + if err := cmdFlags.Parse(args); err != nil { + return 1 + } + + args = cmdFlags.Args() + if len(args) != 1 { + c.Ui.Error("The graph command expects one argument.\n") + cmdFlags.Usage() + return 1 + } + + conf, err := config.Load(args[0]) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error loading config: %s", err)) + return 1 + } + + g, err := terraform.Graph(&terraform.GraphOpts{ + Config: conf, + Providers: c.TFConfig.Providers, + }) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error creating graph: %s", err)) + return 1 + } + + nodes := make([]digraph.Node, len(g.Nouns)) + for i, n := range g.Nouns { + nodes[i] = n + } + digraph.GenerateDot(nodes, os.Stdout) + + return 0 +} + +func (c *GraphCommand) Help() string { + helpText := ` +Usage: terraform graph [options] PATH + + Outputs the visual graph of Terraform resources. If the path given is + the path to a configuration, the dependency graph of the resources are + shown. If the path is a plan file, then the dependency graph of the + plan itself is shown. + +` + return strings.TrimSpace(helpText) +} + +func (c *GraphCommand) Synopsis() string { + return "Output visual graph of Terraform resources" +} diff --git a/commands.go b/commands.go index 41d1b1d8c..c00210c52 100644 --- a/commands.go +++ b/commands.go @@ -33,6 +33,13 @@ func init() { }, nil }, + "graph": func() (cli.Command, error) { + return &command.GraphCommand{ + TFConfig: &TFConfig, + Ui: Ui, + }, nil + }, + "plan": func() (cli.Command, error) { return &command.PlanCommand{ TFConfig: &TFConfig,