github.com/orangenpresse/up@v0.6.0/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 func (s *Stack) showVersion(stage *config.Stage) error { 400 res, err := s.lambda.GetAlias(&lambda.GetAliasInput{ 401 FunctionName: &s.config.Name, 402 Name: &stage.Name, 403 }) 404 405 if err != nil { 406 return errors.Wrap(err, "fetching alias") 407 } 408 409 s.events.Emit("platform.stack.show.version", event.Fields{ 410 "domain": stage.Domain, 411 "version": *res.FunctionVersion, 412 }) 413 414 return nil 415 } 416 417 // showCloudfront emits events for listing cloudfront end-points. 418 func (s *Stack) showCloudfront(stage *config.Stage) error { 419 if stage.Domain == "" { 420 return nil 421 } 422 423 res, err := s.apigateway.GetDomainName(&apigateway.GetDomainNameInput{ 424 DomainName: &stage.Domain, 425 }) 426 427 if err != nil { 428 return errors.Wrap(err, "getting domain mapping") 429 } 430 431 s.events.Emit("platform.stack.show.domain", event.Fields{ 432 "domain": stage.Domain, 433 "endpoint": *res.DistributionDomainName, 434 }) 435 436 return nil 437 } 438 439 // showNameservers emits events for listing name servers. 440 func (s *Stack) showNameservers(stage *config.Stage) error { 441 if stage.Domain == "" { 442 return nil 443 } 444 445 res, err := s.route53.ListHostedZonesByName(&route53.ListHostedZonesByNameInput{ 446 DNSName: &stage.Domain, 447 MaxItems: aws.String("1"), 448 }) 449 450 if err != nil { 451 return errors.Wrap(err, "listing hosted zone") 452 } 453 454 if len(res.HostedZones) == 0 { 455 return nil 456 } 457 458 z := res.HostedZones[0] 459 if stage.Domain+"." != *z.Name { 460 return nil 461 } 462 463 zone, err := s.route53.GetHostedZone(&route53.GetHostedZoneInput{ 464 Id: z.Id, 465 }) 466 467 if err != nil { 468 return errors.Wrap(err, "fetching hosted zone") 469 } 470 471 var ns []string 472 473 for _, s := range zone.DelegationSet.NameServers { 474 ns = append(ns, *s) 475 } 476 477 s.events.Emit("platform.stack.show.nameservers", event.Fields{ 478 "nameservers": ns, 479 }) 480 481 return nil 482 } 483 484 // getStack returns the stack. 485 func (s *Stack) getStack() (*cloudformation.Stack, error) { 486 res, err := s.client.DescribeStacks(&cloudformation.DescribeStacksInput{ 487 StackName: &s.config.Name, 488 }) 489 490 if err != nil { 491 return nil, err 492 } 493 494 stack := res.Stacks[0] 495 return stack, nil 496 } 497 498 // getLatestEvents returns the latest events for each resource. 499 func (s *Stack) getLatestEvents() (v []*cloudformation.StackEvent, err error) { 500 events, err := s.getEvents() 501 if err != nil { 502 return 503 } 504 505 hit := make(map[string]bool) 506 507 for _, e := range events { 508 id := *e.LogicalResourceId 509 if hit[id] { 510 continue 511 } 512 513 hit[id] = true 514 v = append(v, e) 515 } 516 517 return 518 } 519 520 // getFailedEvents returns failed events. 521 func (s *Stack) getFailedEvents() (v []*cloudformation.StackEvent, err error) { 522 events, err := s.getEvents() 523 if err != nil { 524 return 525 } 526 527 for _, e := range events { 528 if Status(*e.ResourceStatus).State() == Failure { 529 v = append(v, e) 530 } 531 } 532 533 return 534 } 535 536 // getEvents returns events. 537 func (s *Stack) getEvents() (events []*cloudformation.StackEvent, err error) { 538 var next *string 539 540 for { 541 res, err := s.client.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ 542 StackName: &s.config.Name, 543 NextToken: next, 544 }) 545 546 if err != nil { 547 return nil, err 548 } 549 550 events = append(events, res.StackEvents...) 551 552 next = res.NextToken 553 554 if next == nil { 555 break 556 } 557 } 558 559 return 560 } 561 562 // resourceStateFromTemplate returns a map of the logical ids from template t, to status s. 563 func resourceStateFromTemplate(t Map, s Status) map[string]Status { 564 r := t["Resources"].(Map) 565 m := make(map[string]Status) 566 567 for id := range r { 568 m[id] = s 569 } 570 571 return m 572 } 573 574 // TODO: ignore deletes since they're in cleanup phase? 575 576 // resourceStateFromChanges returns a map of statuses from a changeset. 577 func resourceStateFromChanges(changes []*cloudformation.Change) map[string]Status { 578 m := make(map[string]Status) 579 580 for _, c := range changes { 581 var state Status 582 var id string 583 584 if s := c.ResourceChange.PhysicalResourceId; s != nil { 585 id = *s 586 } 587 588 if id == "" { 589 id = *c.ResourceChange.LogicalResourceId 590 } 591 592 switch a := *c.ResourceChange.Action; a { 593 case "Add": 594 state = CreateComplete 595 case "Modify": 596 state = UpdateComplete 597 case "Remove": 598 state = DeleteComplete 599 default: 600 panic(errors.Errorf("unhandled Action %q", a)) 601 } 602 603 m[id] = state 604 } 605 606 return m 607 } 608 609 // resourcesCompleted returns a map of the completed resources. When the resource is not 610 // present in states, it is ignored as no changes are expected. 611 func resourcesCompleted(resources []*cloudformation.StackResource, states map[string]Status) map[string]*cloudformation.StackResource { 612 m := make(map[string]*cloudformation.StackResource) 613 614 for _, r := range resources { 615 var expected Status 616 var id string 617 618 // try physical id first, this is necessary as 619 // replacement of a logical id will cause the id 620 // to appear twice (once for Add once for Remove). 621 if s := r.PhysicalResourceId; s != nil { 622 if _, ok := states[*s]; ok { 623 id = *s 624 } 625 } 626 627 // try logical id 628 if s := *r.LogicalResourceId; id == "" { 629 if _, ok := states[s]; ok { 630 id = s 631 } 632 } 633 634 // expected state 635 if id != "" { 636 expected = states[id] 637 } 638 639 // matched expected state 640 if expected == Status(*r.ResourceStatus) { 641 m[id] = r 642 } 643 } 644 645 return m 646 } 647 648 // isNotFound returns true if the error indicates a missing changeset. 649 func isNotFound(err error) bool { 650 return err != nil && strings.Contains(err.Error(), "ChangeSetNotFound") 651 }