github.com/bhameyie/otto@v0.2.1-0.20160406174117-16052efa52ec/helper/terraform/infrastructure.go (about)

     1  package terraform
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/hashicorp/otto/directory"
     8  	"github.com/hashicorp/otto/helper/bindata"
     9  	"github.com/hashicorp/otto/helper/router"
    10  	"github.com/hashicorp/otto/infrastructure"
    11  )
    12  
    13  // Infrastructure implements infrastructure.Infrastructure and is a
    14  // higher level framework for writing infrastructure implementations that
    15  // use Terraform.
    16  //
    17  // This implementation will automatically:
    18  //
    19  //   * Save/restore state files via the directory service
    20  //   * Populate infrastructure data in the directory (w/ Terraform outputs)
    21  //   * Handle many edge case scenarios gracefully
    22  //
    23  type Infrastructure struct {
    24  	// Creds is a function that gathers credentials. See helper/creds
    25  	// for nice helpers for implementing this function.
    26  	CredsFunc func(*infrastructure.Context) (map[string]string, error)
    27  
    28  	// VerifyCreds is a function that verifies credentials are in good
    29  	// working order.
    30  	VerifyCredsFunc func(*infrastructure.Context) error
    31  
    32  	// Bindata is the bindata.Data structure where assets can be found
    33  	// for compilation. The data for the various flavors is expected to
    34  	// live in "data/#{flavor}"
    35  	Bindata *bindata.Data
    36  
    37  	// Variables are additional variables to pass into Terraform.
    38  	Variables map[string]string
    39  }
    40  
    41  func (i *Infrastructure) Creds(ctx *infrastructure.Context) (map[string]string, error) {
    42  	return i.CredsFunc(ctx)
    43  }
    44  
    45  func (i *Infrastructure) VerifyCreds(ctx *infrastructure.Context) error {
    46  	return i.VerifyCredsFunc(ctx)
    47  }
    48  
    49  func (i *Infrastructure) Execute(ctx *infrastructure.Context) error {
    50  	r := &router.Router{
    51  		Actions: map[string]router.Action{
    52  			"": &router.SimpleAction{
    53  				ExecuteFunc:  i.actionApply,
    54  				SynopsisText: infraApplySyn,
    55  				HelpText:     strings.TrimSpace(infraApplyHelp),
    56  			},
    57  			"destroy": &router.SimpleAction{
    58  				ExecuteFunc:  i.actionDestroy,
    59  				SynopsisText: infraDestroySyn,
    60  				HelpText:     strings.TrimSpace(infraDestroyHelp),
    61  			},
    62  			"info": &router.SimpleAction{
    63  				ExecuteFunc:  i.actionInfo,
    64  				SynopsisText: infraInfoSyn,
    65  				HelpText:     strings.TrimSpace(infraInfoHelp),
    66  			},
    67  		},
    68  	}
    69  	return r.Route(ctx)
    70  }
    71  
    72  func (i *Infrastructure) actionDestroy(rctx router.Context) error {
    73  	rctx.UI().Header("Destroying main infrastructure...")
    74  	ctx := rctx.(*infrastructure.Context)
    75  	return i.execute(ctx, "destroy", "-force")
    76  }
    77  
    78  func (i *Infrastructure) actionApply(rctx router.Context) error {
    79  	rctx.UI().Header("Building main infrastructure...")
    80  	ctx := rctx.(*infrastructure.Context)
    81  	return i.execute(ctx, "apply")
    82  }
    83  
    84  func (i *Infrastructure) actionInfo(rctx router.Context) error {
    85  	ctx := rctx.(*infrastructure.Context)
    86  	project, err := Project(&ctx.Shared)
    87  	if err != nil {
    88  		return err
    89  	}
    90  
    91  	lookup := directory.Lookup{Infra: ctx.Infra.Name}
    92  	infra, err := ctx.Directory.GetInfra(&directory.Infra{Lookup: lookup})
    93  	if err != nil {
    94  		return fmt.Errorf(
    95  			"Error looking up existing infrastructure data: %s\n\n"+
    96  				"These errors are usually transient and can be fixed by retrying\n"+
    97  				"the command. Additional causes of errors are networking or disk\n"+
    98  				"issues that can be resolved external to Otto.",
    99  			err)
   100  	}
   101  	if infra == nil {
   102  		return fmt.Errorf("Infrastructure not created. Nothing to display.")
   103  	}
   104  
   105  	tf := &Terraform{
   106  		Path:      project.Path(),
   107  		Dir:       ctx.Dir,
   108  		Ui:        ctx.Ui,
   109  		Directory: ctx.Directory,
   110  		StateId:   infra.ID,
   111  	}
   112  
   113  	// Start the Terraform command
   114  	args := make([]string, len(ctx.ActionArgs)+1)
   115  	args[0] = "output"
   116  	copy(args[1:], ctx.ActionArgs)
   117  	if err := tf.Execute(args...); err != nil {
   118  		return fmt.Errorf("Error running Terraform: %s", err)
   119  	}
   120  	return nil
   121  }
   122  
   123  func (i *Infrastructure) execute(ctx *infrastructure.Context, command ...string) error {
   124  	project, err := Project(&ctx.Shared)
   125  	if err != nil {
   126  		return err
   127  	}
   128  
   129  	// Build the variables
   130  	vars := make(map[string]string)
   131  	for k, v := range ctx.InfraCreds {
   132  		vars[k] = v
   133  	}
   134  	for k, v := range i.Variables {
   135  		vars[k] = v
   136  	}
   137  
   138  	// Setup the lookup information and query the existing infra so we
   139  	// can get our UUID for storing data.
   140  	lookup := directory.Lookup{Infra: ctx.Infra.Name}
   141  	infra, err := ctx.Directory.GetInfra(&directory.Infra{Lookup: lookup})
   142  	if err != nil {
   143  		return fmt.Errorf(
   144  			"Error looking up existing infrastructure data: %s\n\n"+
   145  				"These errors are usually transient and can be fixed by retrying\n"+
   146  				"the command. Additional causes of errors are networking or disk\n"+
   147  				"issues that can be resolved external to Otto.",
   148  			err)
   149  	}
   150  	if infra == nil {
   151  		// If we don't have an infra, create one
   152  		infra = &directory.Infra{Lookup: lookup}
   153  		infra.State = directory.InfraStatePartial
   154  
   155  		// Put the infrastructure so we can get the UUID to use for our state
   156  		if err := ctx.Directory.PutInfra(infra); err != nil {
   157  			return fmt.Errorf(
   158  				"Error preparing infrastructure: %s\n\n"+
   159  					"These errors are usually transient and can be fixed by retrying\n"+
   160  					"the command. Additional causes of errors are networking or disk\n"+
   161  					"issues that can be resolved external to Otto.",
   162  				err)
   163  		}
   164  	}
   165  
   166  	// Build our executor
   167  	tf := &Terraform{
   168  		Path:      project.Path(),
   169  		Dir:       ctx.Dir,
   170  		Ui:        ctx.Ui,
   171  		Variables: vars,
   172  		Directory: ctx.Directory,
   173  		StateId:   infra.ID,
   174  	}
   175  
   176  	ctx.Ui.Header("Executing Terraform to manage infrastructure...")
   177  	ctx.Ui.Message(
   178  		"Raw Terraform output will begin streaming in below. Otto\n" +
   179  			"does not create this output. It is mirrored directly from\n" +
   180  			"Terraform while the infrastructure is being created.\n\n" +
   181  			"Terraform may ask for input. For infrastructure provider\n" +
   182  			"credentials, be sure to enter the same credentials\n" +
   183  			"consistently within the same Otto environment." +
   184  			"\n\n")
   185  
   186  	// Start the Terraform command
   187  	err = tf.Execute(command...)
   188  	if err != nil {
   189  		err = fmt.Errorf("Error running Terraform: %s", err)
   190  		infra.State = directory.InfraStatePartial
   191  	}
   192  
   193  	ctx.Ui.Header("Terraform execution complete. Saving results...")
   194  
   195  	if err == nil {
   196  		if ctx.Action == "destroy" {
   197  			// If we just destroyed successfully, the infra is now empty.
   198  			infra.State = directory.InfraStateInvalid
   199  			infra.Outputs = map[string]string{}
   200  		} else {
   201  			// If an apply was successful, populate the state and outputs.
   202  			infra.State = directory.InfraStateReady
   203  			infra.Outputs, err = tf.Outputs()
   204  			if err != nil {
   205  				err = fmt.Errorf("Error reading Terraform outputs: %s", err)
   206  				infra.State = directory.InfraStatePartial
   207  			}
   208  		}
   209  	}
   210  
   211  	// Save the infrastructure information
   212  	if err := ctx.Directory.PutInfra(infra); err != nil {
   213  		return fmt.Errorf(
   214  			"Error storing infrastructure data: %s\n\n"+
   215  				"This means that Otto won't be able to know that your infrastructure\n"+
   216  				"was successfully created. Otto tries a few times to save the\n"+
   217  				"infrastructure. At this point in time, Otto doesn't support gracefully\n"+
   218  				"recovering from this error. Your infrastructure is now orphaned from\n"+
   219  				"Otto's management. Please reference the community for help.\n\n"+
   220  				"A future version of Otto will resolve this.",
   221  			err)
   222  	}
   223  
   224  	// If there was an error during the process, then return that.
   225  	if err != nil {
   226  		return fmt.Errorf("Error reading Terraform outputs: %s\n\n"+
   227  			"In this case, Otto is unable to consider the infrastructure ready.\n"+
   228  			"Otto won't lose your infrastructure information. You may just need\n"+
   229  			"to run `otto infra` again and it may work. If this problem persists,\n"+
   230  			"please see the error message and consult the community for help.",
   231  			err)
   232  	}
   233  
   234  	return nil
   235  }
   236  
   237  // TODO: test
   238  func (i *Infrastructure) Compile(ctx *infrastructure.Context) (*infrastructure.CompileResult, error) {
   239  	if err := i.Bindata.CopyDir(ctx.Dir, "data/"+ctx.Infra.Flavor); err != nil {
   240  		return nil, err
   241  	}
   242  
   243  	return nil, nil
   244  }
   245  
   246  // TODO: impl and test
   247  func (i *Infrastructure) Flavors() []string {
   248  	return nil
   249  }
   250  
   251  // Synopsis text for actions
   252  const (
   253  	infraApplySyn   = "Create or update infrastructure resources for this application"
   254  	infraDestroySyn = "Destroy infrastructure resources for this application"
   255  	infraInfoSyn    = "Display information about this application's infrastructure"
   256  )
   257  
   258  // Help text for actions
   259  const infraApplyHelp = `
   260  Usage: otto infra
   261  
   262    Creates infrastructure for your application.
   263  
   264    This command will create all the resource required to serve as an
   265    infrastructure for your application.
   266  `
   267  
   268  const infraDestroyHelp = `
   269  Usage: otto infra destroy [-force]
   270  
   271    Destroys all infrastructure resources.
   272  
   273    This command will remove any previously-created infrastructure resources.
   274    Note that any apps with resources deployed into this infrastructure will need
   275    to have 'otto deploy destroy' run before this command will succeed.
   276  
   277  	Otto will ask for confirmation to protect against an accidental destroy. You
   278  	can provide the -force flag to skip this check.
   279  `
   280  
   281  const infraInfoHelp = `
   282  Usage: otto infra info [NAME]
   283  
   284    Displays information about this application's infrastructure.
   285  
   286    This command will show any variables the infrastructure has specified as
   287    outputs. If no NAME is specified, all outputs will be listed. If NAME is
   288    specified, just the contents of that output will be printed.
   289  `