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  }