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

     1  package terraform
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"strings"
     7  
     8  	"github.com/hashicorp/otto/app"
     9  	"github.com/hashicorp/otto/directory"
    10  	"github.com/hashicorp/otto/foundation"
    11  	"github.com/hashicorp/otto/helper/router"
    12  )
    13  
    14  type DeployOptions struct {
    15  	// Dir is the directory where Terraform is run. If this isn't set, it'll
    16  	// default to "#{ctx.Dir}/deploy".
    17  	Dir string
    18  
    19  	// DisableBuild, if true, will not load a build associated with this
    20  	// appfile and attempt to extract the artifact from it. In this case,
    21  	// AritfactExtractors is also useless.
    22  	DisableBuild bool
    23  
    24  	// ArtifactExtractors is a mapping of artifact extractors. The
    25  	// built-in artifact extractors will populate this if a key isn't set.
    26  	ArtifactExtractors map[string]DeployArtifactExtractor
    27  
    28  	// InfraOutputMap is a map to change the key of an infra output
    29  	// to a different key for a Terraform variable. The key of this map
    30  	// is the infra output key, and teh value is the Terraform variable name.
    31  	InfraOutputMap map[string]string
    32  }
    33  
    34  // Deploy can be used as an implementation of app.App.Deploy to handle calling
    35  // out to terraform w/ the configured config to get an app deployed to an
    36  // infrastructure.
    37  //
    38  // This will verify the infrastructure is created and a build is available,
    39  // and use that information to run Terraform. Any edge cases around Terraform
    40  // failures is handled and state storage is automatic as well.
    41  //
    42  // This function implements app.App.Deploy.
    43  func Deploy(opts *DeployOptions) *router.Router {
    44  	return &router.Router{
    45  		Actions: map[string]router.Action{
    46  			"": &router.SimpleAction{
    47  				ExecuteFunc:  opts.actionDeploy,
    48  				SynopsisText: actionDeploySyn,
    49  				HelpText:     strings.TrimSpace(actionDeployHelp),
    50  			},
    51  			"destroy": &router.SimpleAction{
    52  				ExecuteFunc:  opts.actionDestroy,
    53  				SynopsisText: actionDestroySyn,
    54  				HelpText:     strings.TrimSpace(actionDestroyHelp),
    55  			},
    56  			"info": &router.SimpleAction{
    57  				ExecuteFunc:  opts.actionInfo,
    58  				SynopsisText: actionInfoSyn,
    59  				HelpText:     strings.TrimSpace(actionInfoHelp),
    60  			},
    61  		},
    62  	}
    63  }
    64  
    65  func (opts *DeployOptions) actionDeploy(rctx router.Context) error {
    66  	ctx := rctx.(*app.Context)
    67  	project, err := Project(&ctx.Shared)
    68  	if err != nil {
    69  		return err
    70  	}
    71  	vars := make(map[string]string)
    72  
    73  	infra, infraVars, err := opts.lookupInfraVars(ctx)
    74  	if err != nil {
    75  		return err
    76  	}
    77  	if infra == nil {
    78  		return fmt.Errorf(
    79  			"Infrastructure for this application hasn't been built yet.\n" +
    80  				"The deploy step requires this because the target infrastructure\n" +
    81  				"as well as its final properties can affect the deploy process.\n" +
    82  				"Please run `otto infra` to build the underlying infrastructure,\n" +
    83  				"then run `otto deploy` again.")
    84  	}
    85  	for k, v := range infraVars {
    86  		vars[k] = v
    87  	}
    88  
    89  	if !opts.DisableBuild {
    90  		buildVars, err := opts.lookupBuildVars(ctx, infra)
    91  		if err != nil {
    92  			return err
    93  		}
    94  		if buildVars == nil {
    95  			return fmt.Errorf(
    96  				"This application hasn't been built yet. Please run `otto build`\n" +
    97  					"first so that the deploy step has an artifact to deploy.")
    98  		}
    99  		for k, v := range buildVars {
   100  			vars[k] = v
   101  		}
   102  	}
   103  
   104  	// Setup the vars
   105  	if err := foundation.WriteVars(&ctx.Shared); err != nil {
   106  		return fmt.Errorf("Error preparing deploy: %s", err)
   107  	}
   108  
   109  	// Get our old deploy to populate the old state data if we have it.
   110  	// This step is critical to make sure that Terraform remains idempotent
   111  	// and that it handles migrations properly.
   112  	deploy, err := opts.lookupDeploy(ctx)
   113  	if err != nil {
   114  		return err
   115  	}
   116  
   117  	// Run Terraform!
   118  	tf := &Terraform{
   119  		Path:      project.Path(),
   120  		Dir:       opts.tfDir(ctx),
   121  		Ui:        ctx.Ui,
   122  		Variables: vars,
   123  		Directory: ctx.Directory,
   124  		StateId:   deploy.ID,
   125  	}
   126  	if err := tf.Execute("apply"); err != nil {
   127  		deploy.MarkFailed()
   128  		if putErr := ctx.Directory.PutDeploy(deploy); putErr != nil {
   129  			return fmt.Errorf("The deploy failed with err: %s\n\n"+
   130  				"And then there was an error storing it in the directory: %s\n"+
   131  				"This second error is a bug and should be reported.", err, putErr)
   132  		}
   133  
   134  		return terraformError(err)
   135  	}
   136  
   137  	deploy.MarkSuccessful()
   138  	if err := ctx.Directory.PutDeploy(deploy); err != nil {
   139  		return err
   140  	}
   141  	return nil
   142  }
   143  
   144  func (opts *DeployOptions) actionDestroy(rctx router.Context) error {
   145  	ctx := rctx.(*app.Context)
   146  	project, err := Project(&ctx.Shared)
   147  	if err != nil {
   148  		return err
   149  	}
   150  	vars := make(map[string]string)
   151  
   152  	infra, infraVars, err := opts.lookupInfraVars(ctx)
   153  	if err != nil {
   154  		return err
   155  	}
   156  	if infra == nil {
   157  		return fmt.Errorf(
   158  			"Infrastructure for this application hasn't been built yet.\n" +
   159  				"Nothing to destroy.")
   160  	}
   161  	for k, v := range infraVars {
   162  		vars[k] = v
   163  	}
   164  
   165  	if !opts.DisableBuild {
   166  		buildVars, err := opts.lookupBuildVars(ctx, infra)
   167  		if err != nil {
   168  			return err
   169  		}
   170  		if buildVars == nil {
   171  			return fmt.Errorf(
   172  				"This application hasn't been built yet. Nothing to destroy.")
   173  		}
   174  		for k, v := range buildVars {
   175  			vars[k] = v
   176  		}
   177  	}
   178  
   179  	deploy, err := opts.lookupDeploy(ctx)
   180  	if err != nil {
   181  		return err
   182  	}
   183  	if deploy.IsNew() {
   184  		return fmt.Errorf(
   185  			"This application hasn't been deployed yet. Nothing to destroy.")
   186  	}
   187  
   188  	// Get the directory
   189  	// Run Terraform!
   190  	tf := &Terraform{
   191  		Path:      project.Path(),
   192  		Dir:       opts.tfDir(ctx),
   193  		Ui:        ctx.Ui,
   194  		Variables: vars,
   195  		Directory: ctx.Directory,
   196  		StateId:   deploy.ID,
   197  	}
   198  	if err := tf.Execute("destroy", "-force"); err != nil {
   199  		deploy.MarkFailed()
   200  		if putErr := ctx.Directory.PutDeploy(deploy); putErr != nil {
   201  			return fmt.Errorf("The destroy failed with err: %s\n\n"+
   202  				"And then there was an error storing it in the directory: %s\n"+
   203  				"This second error is a bug and should be reported.", err, putErr)
   204  		}
   205  
   206  		return terraformError(err)
   207  	}
   208  
   209  	deploy.MarkGone()
   210  	if err := ctx.Directory.PutDeploy(deploy); err != nil {
   211  		return err
   212  	}
   213  
   214  	return nil
   215  }
   216  
   217  func (opts *DeployOptions) actionInfo(rctx router.Context) error {
   218  	ctx := rctx.(*app.Context)
   219  	project, err := Project(&ctx.Shared)
   220  	if err != nil {
   221  		return err
   222  	}
   223  
   224  	deploy, err := opts.lookupDeploy(ctx)
   225  	if err != nil {
   226  		return err
   227  	}
   228  	if deploy.IsNew() {
   229  		return fmt.Errorf(
   230  			"This application hasn't been deployed yet. Nothing to show.")
   231  	}
   232  
   233  	// Get the directory
   234  	// Run Terraform!
   235  	tf := &Terraform{
   236  		Path:      project.Path(),
   237  		Dir:       opts.tfDir(ctx),
   238  		Ui:        ctx.Ui,
   239  		Directory: ctx.Directory,
   240  		StateId:   deploy.ID,
   241  	}
   242  	args := make([]string, len(ctx.ActionArgs)+1)
   243  	args[0] = "output"
   244  	copy(args[1:], ctx.ActionArgs)
   245  	if err := tf.Execute(args...); err != nil {
   246  		return terraformError(err)
   247  	}
   248  
   249  	return nil
   250  }
   251  
   252  // lookupInfraVars collects information about the result of `otto infra` and
   253  // yields a set of variables that can be used by the deploy to reference
   254  // resources in the infrastructure. It returns `nil` if the infrastructure has
   255  // not been created successfully yet.
   256  func (opts *DeployOptions) lookupInfraVars(
   257  	ctx *app.Context) (*directory.Infra, map[string]string, error) {
   258  	infra, err := ctx.Directory.GetInfra(&directory.Infra{
   259  		Lookup: directory.Lookup{
   260  			Infra: ctx.Appfile.ActiveInfrastructure().Name}})
   261  	if err != nil {
   262  		return nil, nil, err
   263  	}
   264  
   265  	if !infra.IsReady() {
   266  		return nil, nil, nil
   267  	}
   268  
   269  	vars := make(map[string]string)
   270  	for k, v := range infra.Outputs {
   271  		if opts.InfraOutputMap != nil {
   272  			if nk, ok := opts.InfraOutputMap[k]; ok {
   273  				k = nk
   274  			}
   275  		}
   276  		vars[k] = v
   277  	}
   278  	for k, v := range ctx.InfraCreds {
   279  		vars[k] = v
   280  	}
   281  	return infra, vars, nil
   282  }
   283  
   284  // lookupBuildVars collects information about the result of `otto build` and
   285  // yields a set of variables that can be used by the deploy to reference the
   286  // built artifact. It returns nil if `otto build` has not yet been run.
   287  func (opts *DeployOptions) lookupBuildVars(
   288  	ctx *app.Context, infra *directory.Infra) (map[string]string, error) {
   289  	build, err := ctx.Directory.GetBuild(&directory.Build{
   290  		Lookup: directory.Lookup{
   291  			AppID:       ctx.Appfile.ID,
   292  			Infra:       ctx.Tuple.Infra,
   293  			InfraFlavor: ctx.Tuple.InfraFlavor,
   294  		},
   295  	})
   296  	if err != nil {
   297  		return nil, err
   298  	}
   299  	if build == nil {
   300  		return nil, nil
   301  	}
   302  
   303  	// Extract the artifact from the build. We do this based on the
   304  	// infrastructure type.
   305  	if opts.ArtifactExtractors == nil {
   306  		opts.ArtifactExtractors = make(map[string]DeployArtifactExtractor)
   307  	}
   308  	for k, v := range deployArtifactExtractors {
   309  		if _, ok := opts.ArtifactExtractors[k]; !ok {
   310  			opts.ArtifactExtractors[k] = v
   311  		}
   312  	}
   313  	ext, ok := opts.ArtifactExtractors[ctx.Tuple.Infra]
   314  	if !ok {
   315  		return nil, fmt.Errorf(
   316  			"Unknown deployment target infrastructure: %s\n\n"+
   317  				"This app currently doesn't know how to deploy to this infrastructure.\n"+
   318  				"Please report this to the project.",
   319  			ctx.Tuple.Infra)
   320  	}
   321  	return ext(ctx, build, infra)
   322  }
   323  
   324  // lookupDeploy returns any previously deploy made by Otto so we have the state
   325  // necessary to update it.
   326  //
   327  // If we don't have a prior deploy, that is okay, we just create one
   328  // now (with the DeployStateNew to note that we've never deployed). This
   329  // gives us the UUID we can use for the state storage.
   330  func (opts *DeployOptions) lookupDeploy(
   331  	ctx *app.Context) (*directory.Deploy, error) {
   332  	deployLookup := directory.Lookup{
   333  		AppID:       ctx.Appfile.ID,
   334  		Infra:       ctx.Tuple.Infra,
   335  		InfraFlavor: ctx.Tuple.InfraFlavor,
   336  	}
   337  	deploy, err := ctx.Directory.GetDeploy(&directory.Deploy{Lookup: deployLookup})
   338  	if err != nil {
   339  		return nil, err
   340  	}
   341  
   342  	if deploy == nil {
   343  		// If we have no deploy, put in a temporary one
   344  		deploy = &directory.Deploy{Lookup: deployLookup}
   345  		deploy.State = directory.DeployStateNew
   346  
   347  		// Write the temporary deploy so we have an ID to use for the state
   348  		if err := ctx.Directory.PutDeploy(deploy); err != nil {
   349  			return nil, err
   350  		}
   351  	}
   352  
   353  	return deploy, nil
   354  }
   355  
   356  // tfDir returns the appropriate terraform working dir
   357  func (opts *DeployOptions) tfDir(ctx *app.Context) string {
   358  	tfDir := opts.Dir
   359  	if tfDir == "" {
   360  		tfDir = filepath.Join(ctx.Dir, "deploy")
   361  	}
   362  	return tfDir
   363  }
   364  
   365  // terraformError wraps an error from Terraform in a friendlier message.
   366  func terraformError(err error) error {
   367  	return fmt.Errorf(
   368  		"Error running Terraform: %s\n\n"+
   369  			"Terraform usually has helpful error messages. Please read the error\n"+
   370  			"messages above and resolve them. Sometimes simply running `otto deploy`\n"+
   371  			"again will work.",
   372  		err)
   373  }
   374  
   375  // Synopsis text for actions
   376  const (
   377  	actionDeploySyn  = "Deploy the latest built artifact into your infrastructure"
   378  	actionDestroySyn = "Destroy all deployed resources for this application"
   379  	actionInfoSyn    = "Display information about this application's deploy"
   380  )
   381  
   382  // Help text for actions
   383  const actionDeployHelp = `
   384  Usage: otto deploy
   385  
   386    Deploys a built artifact into your infrastructure.
   387  
   388    This command will take the latest built artifact and deploy it into your
   389    infrastructure. Otto will create or replace any necessary resources required
   390    to run your app.
   391  `
   392  
   393  const actionDestroyHelp = `
   394  Usage: otto deploy destroy [-force]
   395  
   396    Destroys any deployed resources associated with this application.
   397  
   398    This command will remove any previously-deployed resources from your
   399    infrastructure. This must be run for all of apps in an infrastructure before
   400    'otto infra destroy' will work.
   401  
   402  	Otto will ask for confirmation to protect against an accidental destroy. You
   403  	can provide the -force flag to skip this check.
   404  `
   405  
   406  const actionInfoHelp = `
   407  Usage: otto deploy info [NAME]
   408  
   409    Displays information about this application's deploy.
   410  
   411    This command will show any variables the deploy has specified as outputs. If
   412    no NAME is specified, all outputs will be listed. If NAME is specified, just
   413    the contents of that output will be printed.
   414  `