github.com/webonyx/up@v0.7.4-0.20180808230834-91b94e551323/platform/lambda/stack/stack.go (about) 1 // Package stack provides CloudFormation stack support. 2 package stack 3 4 import ( 5 "encoding/json" 6 "strings" 7 "time" 8 9 "github.com/aws/aws-sdk-go/aws" 10 "github.com/aws/aws-sdk-go/aws/session" 11 "github.com/aws/aws-sdk-go/service/apigateway" 12 "github.com/aws/aws-sdk-go/service/cloudformation" 13 "github.com/aws/aws-sdk-go/service/lambda" 14 "github.com/aws/aws-sdk-go/service/route53" 15 "github.com/pkg/errors" 16 17 "github.com/apex/log" 18 "github.com/apex/up" 19 "github.com/apex/up/config" 20 "github.com/apex/up/internal/util" 21 "github.com/apex/up/platform/event" 22 "github.com/apex/up/platform/lambda/stack/resources" 23 ) 24 25 // TODO: refactor a lot 26 // TODO: backoff 27 // TODO: profile changeset name and description flags 28 // TODO: flags for changeset name / description 29 30 // defaultChangeset name. 31 var defaultChangeset = "changes" 32 33 // Map type. 34 type Map = resources.Map 35 36 // Stack represents a single CloudFormation stack. 37 type Stack struct { 38 client *cloudformation.CloudFormation 39 lambda *lambda.Lambda 40 route53 *route53.Route53 41 apigateway *apigateway.APIGateway 42 events event.Events 43 zones []*route53.HostedZone 44 config *up.Config 45 } 46 47 // New stack. 48 func New(c *up.Config, events event.Events, zones []*route53.HostedZone, region string) *Stack { 49 sess := session.New(aws.NewConfig().WithRegion(region)) 50 return &Stack{ 51 client: cloudformation.New(sess), 52 lambda: lambda.New(sess), 53 route53: route53.New(sess), 54 apigateway: apigateway.New(sess), 55 events: events, 56 zones: zones, 57 config: c, 58 } 59 } 60 61 // template returns a configured resource template. 62 func (s *Stack) template(versions resources.Versions) Map { 63 return resources.New(&resources.Config{ 64 Config: s.config, 65 Zones: s.zones, 66 Versions: versions, 67 }) 68 } 69 70 // Create the stack. 71 func (s *Stack) Create(versions resources.Versions) error { 72 c := s.config 73 tmpl := s.template(versions) 74 name := c.Name 75 76 b, err := json.MarshalIndent(tmpl, "", " ") 77 if err != nil { 78 return errors.Wrap(err, "marshaling") 79 } 80 81 _, err = s.client.CreateStack(&cloudformation.CreateStackInput{ 82 StackName: &name, 83 TemplateBody: aws.String(string(b)), 84 TimeoutInMinutes: aws.Int64(60), 85 DisableRollback: aws.Bool(true), 86 Capabilities: aws.StringSlice([]string{"CAPABILITY_NAMED_IAM"}), 87 Parameters: []*cloudformation.Parameter{ 88 { 89 ParameterKey: aws.String("Name"), 90 ParameterValue: &name, 91 }, 92 { 93 ParameterKey: aws.String("FunctionName"), 94 ParameterValue: &name, 95 }, 96 }, 97 }) 98 99 if err != nil { 100 return errors.Wrap(err, "creating stack") 101 } 102 103 if err := s.report(resourceStateFromTemplate(tmpl, CreateComplete)); err != nil { 104 return errors.Wrap(err, "reporting") 105 } 106 107 stack, err := s.getStack() 108 if err != nil { 109 return errors.Wrap(err, "fetching stack") 110 } 111 112 status := Status(*stack.StackStatus) 113 if status.State() == Failure { 114 return errors.New(*stack.StackStatusReason) 115 } 116 117 return nil 118 } 119 120 // Delete the stack, optionally waiting for completion. 121 func (s *Stack) Delete(versions resources.Versions, wait bool) error { 122 _, err := s.client.DeleteStack(&cloudformation.DeleteStackInput{ 123 StackName: &s.config.Name, 124 }) 125 126 if err != nil { 127 return errors.Wrap(err, "deleting") 128 } 129 130 if wait { 131 tmpl := s.template(versions) 132 if err := s.report(resourceStateFromTemplate(tmpl, DeleteComplete)); err != nil { 133 return errors.Wrap(err, "reporting") 134 } 135 } 136 137 return nil 138 } 139 140 // Show resources. 141 func (s *Stack) Show() error { 142 defer s.events.Time("platform.stack.show", nil)() 143 144 // show stack status 145 stack, err := s.getStack() 146 if err != nil { 147 return errors.Wrap(err, "fetching stack") 148 } 149 150 s.events.Emit("platform.stack.show.stack", event.Fields{ 151 "stack": stack, 152 }) 153 154 // stages 155 for _, stage := range s.config.Stages.List() { 156 if stage.Domain == "" { 157 continue 158 } 159 160 s.events.Emit("platform.stack.show.stage", event.Fields{ 161 "name": stage.Name, 162 "domain": stage.Domain, 163 }) 164 165 // show cloudfront endpoint 166 if err := s.showCloudfront(stage); err != nil { 167 log.WithError(err).Debug("showing cloudfront") 168 } 169 170 // show function version 171 if err := s.showVersion(stage); err != nil { 172 log.WithError(err).Debug("showing version") 173 } 174 175 // show nameservers 176 if err := s.showNameservers(stage); err != nil { 177 return errors.Wrap(err, "showing nameservers") 178 } 179 } 180 181 // skip events if everything is ok 182 if Status(*stack.StackStatus).State() == Success { 183 return nil 184 } 185 186 // show events 187 s.events.Emit("platform.stack.show.stack.events", nil) 188 189 events, err := s.getFailedEvents() 190 if err != nil { 191 return errors.Wrap(err, "fetching latest events") 192 } 193 194 for _, e := range events { 195 if *e.LogicalResourceId == s.config.Name { 196 continue 197 } 198 199 s.events.Emit("platform.stack.show.stack.event", event.Fields{ 200 "event": e, 201 }) 202 } 203 204 return nil 205 } 206 207 // Plan changes. 208 func (s *Stack) Plan(versions resources.Versions) error { 209 c := s.config 210 tmpl := s.template(versions) 211 name := c.Name 212 213 b, err := json.MarshalIndent(tmpl, "", " ") 214 if err != nil { 215 return errors.Wrap(err, "marshaling") 216 } 217 218 defer s.events.Time("platform.stack.plan", nil) 219 220 log.Debug("deleting changeset") 221 _, err = s.client.DeleteChangeSet(&cloudformation.DeleteChangeSetInput{ 222 StackName: &name, 223 ChangeSetName: &defaultChangeset, 224 }) 225 226 if err != nil { 227 return errors.Wrap(err, "deleting changeset") 228 } 229 230 log.Debug("creating changeset") 231 _, err = s.client.CreateChangeSet(&cloudformation.CreateChangeSetInput{ 232 StackName: &name, 233 ChangeSetName: &defaultChangeset, 234 TemplateBody: aws.String(string(b)), 235 Capabilities: aws.StringSlice([]string{"CAPABILITY_NAMED_IAM"}), 236 ChangeSetType: aws.String("UPDATE"), 237 Description: aws.String("Managed by Up."), 238 Parameters: []*cloudformation.Parameter{ 239 { 240 ParameterKey: aws.String("Name"), 241 ParameterValue: &name, 242 }, 243 { 244 ParameterKey: aws.String("FunctionName"), 245 ParameterValue: &name, 246 }, 247 }, 248 }) 249 250 if err != nil { 251 return errors.Wrap(err, "creating changeset") 252 } 253 254 var next *string 255 256 for { 257 log.Debug("describing changeset") 258 res, err := s.client.DescribeChangeSet(&cloudformation.DescribeChangeSetInput{ 259 StackName: &name, 260 ChangeSetName: &defaultChangeset, 261 NextToken: next, 262 }) 263 264 if err != nil { 265 return errors.Wrap(err, "describing changeset") 266 } 267 268 status := Status(*res.Status) 269 270 if status.State() == Failure { 271 if _, err := s.client.DeleteChangeSet(&cloudformation.DeleteChangeSetInput{ 272 StackName: &name, 273 ChangeSetName: &defaultChangeset, 274 }); err != nil { 275 return errors.Wrap(err, "deleting changeset") 276 } 277 278 return errors.New(*res.StatusReason) 279 } 280 281 if !status.IsDone() { 282 log.Debug("waiting for completion") 283 time.Sleep(750 * time.Millisecond) 284 continue 285 } 286 287 for _, c := range res.Changes { 288 s.events.Emit("platform.stack.plan.change", event.Fields{ 289 "change": c, 290 }) 291 } 292 293 next = res.NextToken 294 295 if next == nil { 296 break 297 } 298 } 299 300 return nil 301 } 302 303 // Apply changes. 304 func (s *Stack) Apply() error { 305 c := s.config 306 name := c.Name 307 308 res, err := s.client.DescribeChangeSet(&cloudformation.DescribeChangeSetInput{ 309 StackName: &name, 310 ChangeSetName: &defaultChangeset, 311 }) 312 313 if isNotFound(err) { 314 return errors.Errorf("changeset does not exist, run `up stack plan` first") 315 } 316 317 if err != nil { 318 return errors.Wrap(err, "describing changeset") 319 } 320 321 defer s.events.Time("platform.stack.apply", event.Fields{ 322 "changes": len(res.Changes), 323 })() 324 325 _, err = s.client.ExecuteChangeSet(&cloudformation.ExecuteChangeSetInput{ 326 StackName: &name, 327 ChangeSetName: &defaultChangeset, 328 }) 329 330 if err != nil { 331 return errors.Wrap(err, "executing changeset") 332 } 333 334 if err := s.report(resourceStateFromChanges(res.Changes)); err != nil { 335 return errors.Wrap(err, "reporting") 336 } 337 338 return nil 339 } 340 341 // report events with a map of desired stats from logical or physical id, 342 // any resources not mapped are ignored as they do not contribute to changes. 343 func (s *Stack) report(states map[string]Status) error { 344 defer s.events.Time("platform.stack.report", event.Fields{ 345 "total": len(states), 346 "complete": 0, 347 })() 348 349 ticker := time.NewTicker(time.Second) 350 defer ticker.Stop() 351 352 for range ticker.C { 353 stack, err := s.getStack() 354 355 if util.IsNotFound(err) { 356 return nil 357 } 358 359 if util.IsThrottled(err) { 360 time.Sleep(3 * time.Second) 361 continue 362 } 363 364 if err != nil { 365 return errors.Wrap(err, "fetching stack") 366 } 367 368 status := Status(*stack.StackStatus) 369 370 if status.IsDone() { 371 return nil 372 } 373 374 res, err := s.client.DescribeStackResources(&cloudformation.DescribeStackResourcesInput{ 375 StackName: &s.config.Name, 376 }) 377 378 if util.IsThrottled(err) { 379 time.Sleep(time.Second * 3) 380 continue 381 } 382 383 if err != nil { 384 return errors.Wrap(err, "describing stack resources") 385 } 386 387 complete := len(resourcesCompleted(res.StackResources, states)) 388 389 s.events.Emit("platform.stack.report.event", event.Fields{ 390 "total": len(states), 391 "complete": complete, 392 }) 393 } 394 395 return nil 396 } 397 398 // showVersion emits events for showing the Lambda version, 399 // and any aliases associated with the version. 400 func (s *Stack) showVersion(stage *config.Stage) error { 401 res, err := s.lambda.GetAlias(&lambda.GetAliasInput{ 402 FunctionName: &s.config.Name, 403 Name: &stage.Name, 404 }) 405 406 if err != nil { 407 return errors.Wrap(err, "fetching alias") 408 } 409 410 s.events.Emit("platform.stack.show.version", event.Fields{ 411 "domain": stage.Domain, 412 "version": *res.FunctionVersion, 413 }) 414 415 return nil 416 } 417 418 // showCloudfront emits events for listing cloudfront end-points. 419 func (s *Stack) showCloudfront(stage *config.Stage) error { 420 if stage.Domain == "" { 421 return nil 422 } 423 424 res, err := s.apigateway.GetDomainName(&apigateway.GetDomainNameInput{ 425 DomainName: &stage.Domain, 426 }) 427 428 if err != nil { 429 return errors.Wrap(err, "getting domain mapping") 430 } 431 432 s.events.Emit("platform.stack.show.domain", event.Fields{ 433 "domain": stage.Domain, 434 "endpoint": *res.DistributionDomainName, 435 }) 436 437 return nil 438 } 439 440 // showNameservers emits events for listing name servers. 441 func (s *Stack) showNameservers(stage *config.Stage) error { 442 if stage.Domain == "" { 443 return nil 444 } 445 446 res, err := s.route53.ListHostedZonesByName(&route53.ListHostedZonesByNameInput{ 447 DNSName: &stage.Domain, 448 MaxItems: aws.String("1"), 449 }) 450 451 if err != nil { 452 return errors.Wrap(err, "listing hosted zone") 453 } 454 455 if len(res.HostedZones) == 0 { 456 return nil 457 } 458 459 z := res.HostedZones[0] 460 if stage.Domain+"." != *z.Name { 461 return nil 462 } 463 464 zone, err := s.route53.GetHostedZone(&route53.GetHostedZoneInput{ 465 Id: z.Id, 466 }) 467 468 if err != nil { 469 return errors.Wrap(err, "fetching hosted zone") 470 } 471 472 var ns []string 473 474 for _, s := range zone.DelegationSet.NameServers { 475 ns = append(ns, *s) 476 } 477 478 s.events.Emit("platform.stack.show.nameservers", event.Fields{ 479 "nameservers": ns, 480 }) 481 482 return nil 483 } 484 485 // getStack returns the stack. 486 func (s *Stack) getStack() (*cloudformation.Stack, error) { 487 res, err := s.client.DescribeStacks(&cloudformation.DescribeStacksInput{ 488 StackName: &s.config.Name, 489 }) 490 491 if err != nil { 492 return nil, err 493 } 494 495 stack := res.Stacks[0] 496 return stack, nil 497 } 498 499 // getLatestEvents returns the latest events for each resource. 500 func (s *Stack) getLatestEvents() (v []*cloudformation.StackEvent, err error) { 501 events, err := s.getEvents() 502 if err != nil { 503 return 504 } 505 506 hit := make(map[string]bool) 507 508 for _, e := range events { 509 id := *e.LogicalResourceId 510 if hit[id] { 511 continue 512 } 513 514 hit[id] = true 515 v = append(v, e) 516 } 517 518 return 519 } 520 521 // getFailedEvents returns failed events. 522 func (s *Stack) getFailedEvents() (v []*cloudformation.StackEvent, err error) { 523 events, err := s.getEvents() 524 if err != nil { 525 return 526 } 527 528 for _, e := range events { 529 if Status(*e.ResourceStatus).State() == Failure { 530 v = append(v, e) 531 } 532 } 533 534 return 535 } 536 537 // getEvents returns events. 538 func (s *Stack) getEvents() (events []*cloudformation.StackEvent, err error) { 539 var next *string 540 541 for { 542 res, err := s.client.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ 543 StackName: &s.config.Name, 544 NextToken: next, 545 }) 546 547 if err != nil { 548 return nil, err 549 } 550 551 events = append(events, res.StackEvents...) 552 553 next = res.NextToken 554 555 if next == nil { 556 break 557 } 558 } 559 560 return 561 } 562 563 // resourceStateFromTemplate returns a map of the logical ids from template t, to status s. 564 func resourceStateFromTemplate(t Map, s Status) map[string]Status { 565 r := t["Resources"].(Map) 566 m := make(map[string]Status) 567 568 for id := range r { 569 m[id] = s 570 } 571 572 return m 573 } 574 575 // TODO: ignore deletes since they're in cleanup phase? 576 577 // resourceStateFromChanges returns a map of statuses from a changeset. 578 func resourceStateFromChanges(changes []*cloudformation.Change) map[string]Status { 579 m := make(map[string]Status) 580 581 for _, c := range changes { 582 var state Status 583 var id string 584 585 if s := c.ResourceChange.PhysicalResourceId; s != nil { 586 id = *s 587 } 588 589 if id == "" { 590 id = *c.ResourceChange.LogicalResourceId 591 } 592 593 switch a := *c.ResourceChange.Action; a { 594 case "Add": 595 state = CreateComplete 596 case "Modify": 597 state = UpdateComplete 598 case "Remove": 599 state = DeleteComplete 600 default: 601 panic(errors.Errorf("unhandled Action %q", a)) 602 } 603 604 m[id] = state 605 } 606 607 return m 608 } 609 610 // resourcesCompleted returns a map of the completed resources. When the resource is not 611 // present in states, it is ignored as no changes are expected. 612 func resourcesCompleted(resources []*cloudformation.StackResource, states map[string]Status) map[string]*cloudformation.StackResource { 613 m := make(map[string]*cloudformation.StackResource) 614 615 for _, r := range resources { 616 var expected Status 617 var id string 618 619 // try physical id first, this is necessary as 620 // replacement of a logical id will cause the id 621 // to appear twice (once for Add once for Remove). 622 if s := r.PhysicalResourceId; s != nil { 623 if _, ok := states[*s]; ok { 624 id = *s 625 } 626 } 627 628 // try logical id 629 if s := *r.LogicalResourceId; id == "" { 630 if _, ok := states[s]; ok { 631 id = s 632 } 633 } 634 635 // expected state 636 if id != "" { 637 expected = states[id] 638 } 639 640 // matched expected state 641 if expected == Status(*r.ResourceStatus) { 642 m[id] = r 643 } 644 } 645 646 return m 647 } 648 649 // isNotFound returns true if the error indicates a missing changeset. 650 func isNotFound(err error) bool { 651 return err != nil && strings.Contains(err.Error(), "ChangeSetNotFound") 652 }