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 `