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 `