github.com/dahs81/otto@v0.2.1-0.20160126165905-6400716cf085/helper/terraform/foundation.go (about)

     1  package terraform
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  
     7  	"github.com/hashicorp/otto/directory"
     8  	"github.com/hashicorp/otto/foundation"
     9  )
    10  
    11  // Foundation is a helper for various operations a foundation must
    12  // perform with Terraform.
    13  type Foundation struct {
    14  	// Dir is the directory where Terraform is run. If this isn't set, it'll
    15  	// default to "#{ctx.Dir}/deploy".
    16  	Dir string
    17  }
    18  
    19  // Infra manages a foundation using Terraform.
    20  //
    21  // This will verify the infrastruction is created and use that information
    22  // to execute Terraform with the options given.
    23  //
    24  // This function implements foundation.Foundation.Infra.
    25  func (f *Foundation) Infra(ctx *foundation.Context) error {
    26  	switch ctx.Action {
    27  	case "":
    28  		if err := f.execute(ctx, "get", "."); err != nil {
    29  			return err
    30  		}
    31  
    32  		return f.execute(ctx, "apply")
    33  	case "destroy":
    34  		if err := f.execute(ctx, "get", "."); err != nil {
    35  			return err
    36  		}
    37  
    38  		return f.execute(ctx, "destroy", "-force")
    39  	default:
    40  		return fmt.Errorf("unknown action: %s", ctx.Action)
    41  	}
    42  }
    43  
    44  func (f *Foundation) execute(ctx *foundation.Context, args ...string) error {
    45  	project, err := Project(&ctx.Shared)
    46  	if err != nil {
    47  		return err
    48  	}
    49  
    50  	appInfra := ctx.Appfile.ActiveInfrastructure()
    51  
    52  	// Foundations themselves are represented as infrastructure in the
    53  	// backend. Let's look that up. If it doesn't exist, we have to create
    54  	// it in order to get our UUID for storing state.
    55  	lookup := directory.Lookup{Infra: appInfra.Type, Foundation: ctx.Tuple.Type}
    56  	foundationInfra, err := ctx.Directory.GetInfra(&directory.Infra{Lookup: lookup})
    57  	if err != nil {
    58  		return fmt.Errorf(
    59  			"Error looking up existing infrastructure data: %s\n\n"+
    60  				"These errors are usually transient and can be fixed by retrying\n"+
    61  				"the command. Additional causes of errors are networking or disk\n"+
    62  				"issues that can be resolved external to Otto.",
    63  			err)
    64  	}
    65  	if foundationInfra == nil {
    66  		// If we don't have an infra, create one
    67  		foundationInfra = &directory.Infra{Lookup: lookup}
    68  		foundationInfra.State = directory.InfraStatePartial
    69  
    70  		// Put the infrastructure so we can get the UUID to use for our state
    71  		if err := ctx.Directory.PutInfra(foundationInfra); err != nil {
    72  			return fmt.Errorf(
    73  				"Error preparing infrastructure: %s\n\n"+
    74  					"These errors are usually transient and can be fixed by retrying\n"+
    75  					"the command. Additional causes of errors are networking or disk\n"+
    76  					"issues that can be resolved external to Otto.",
    77  				err)
    78  		}
    79  	}
    80  
    81  	// Get the infrastructure state. The infrastructure must be
    82  	// created for us to deploy to it.
    83  	infra, err := ctx.Directory.GetInfra(&directory.Infra{
    84  		Lookup: directory.Lookup{Infra: appInfra.Name}})
    85  	if err != nil {
    86  		return err
    87  	}
    88  	if infra == nil || infra.State != directory.InfraStateReady {
    89  		return fmt.Errorf(
    90  			"Infrastructure for this application hasn't been built yet.\n" +
    91  				"Building a foundation requires a target infrastruction to\n" +
    92  				"be built. Please run `otta infra` to build the underlying\n" +
    93  				"infrastructure.")
    94  	}
    95  
    96  	// Construct the variables for Terraform from our queried infra
    97  	vars := make(map[string]string)
    98  	for k, v := range infra.Outputs {
    99  		vars[k] = v
   100  	}
   101  	for k, v := range ctx.InfraCreds {
   102  		vars[k] = v
   103  	}
   104  
   105  	// Get the directory
   106  	tfDir := f.Dir
   107  	if tfDir == "" {
   108  		tfDir = filepath.Join(ctx.Dir, "deploy")
   109  	}
   110  
   111  	// Run Terraform!
   112  	tf := &Terraform{
   113  		Path:      project.Path(),
   114  		Dir:       tfDir,
   115  		Ui:        ctx.Ui,
   116  		Variables: vars,
   117  		Directory: ctx.Directory,
   118  		StateId:   foundationInfra.ID,
   119  	}
   120  	err = tf.Execute(args...)
   121  	if err != nil {
   122  		return fmt.Errorf(
   123  			"Error running Terraform: %s\n\n"+
   124  				"Terraform usually has helpful error messages. Please read the error\n"+
   125  				"messages above and resolve them. Sometimes simply re-running the\n"+
   126  				"command again will work.",
   127  			err)
   128  	}
   129  
   130  	ctx.Ui.Header("Terraform execution complete. Saving results...")
   131  	if err == nil {
   132  		if ctx.Action == "destroy" {
   133  			// If we just destroyed successfully, the infra is now empty.
   134  			foundationInfra.State = directory.InfraStateInvalid
   135  			foundationInfra.Outputs = map[string]string{}
   136  		} else {
   137  			// If an apply was successful, populate the state and outputs.
   138  			foundationInfra.State = directory.InfraStateReady
   139  			foundationInfra.Outputs, err = tf.Outputs()
   140  			if err != nil {
   141  				err = fmt.Errorf("Error reading Terraform outputs: %s", err)
   142  				foundationInfra.State = directory.InfraStatePartial
   143  			}
   144  		}
   145  	}
   146  
   147  	// Save the infrastructure information
   148  	if err := ctx.Directory.PutInfra(foundationInfra); err != nil {
   149  		return fmt.Errorf(
   150  			"Error storing infrastructure data: %s\n\n"+
   151  				"This means that Otto won't be able to know that your infrastructure\n"+
   152  				"was successfully created. Otto tries a few times to save the\n"+
   153  				"infrastructure. At this point in time, Otto doesn't support gracefully\n"+
   154  				"recovering from this error. Your infrastructure is now orphaned from\n"+
   155  				"Otto's management. Please reference the community for help.\n\n"+
   156  				"A future version of Otto will resolve this.",
   157  			err)
   158  	}
   159  
   160  	return err
   161  }