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  }