github.com/webonyx/up@v0.7.4-0.20180808230834-91b94e551323/platform/lambda/stack/resources/resources.go (about)

     1  package resources
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  
     7  	"github.com/apex/up"
     8  	"github.com/apex/up/config"
     9  	"github.com/apex/up/internal/util"
    10  	"github.com/aws/aws-sdk-go/service/route53"
    11  )
    12  
    13  // Map type.
    14  type Map map[string]interface{}
    15  
    16  // Versions is a map of stage to lambda function version.
    17  type Versions map[string]string
    18  
    19  // Config for the resource template.
    20  type Config struct {
    21  	// Zones already present in route53. This is used to
    22  	// ensure that existing zones previously set up, or
    23  	// automatically configured when purchasing a domain
    24  	// are not duplicated.
    25  	Zones []*route53.HostedZone
    26  
    27  	// Versions map used to maintain the correct lambda
    28  	// function aliases when updating a stack.
    29  	Versions Versions
    30  
    31  	*up.Config
    32  }
    33  
    34  // New template.
    35  func New(c *Config) map[string]interface{} {
    36  	return Map{
    37  		"AWSTemplateFormatVersion": "2010-09-09",
    38  		"Parameters":               parameters(c),
    39  		"Outputs":                  outputs(c),
    40  		"Resources":                resources(c),
    41  	}
    42  }
    43  
    44  // ref of id.
    45  func ref(id string) Map {
    46  	return Map{
    47  		"Ref": id,
    48  	}
    49  }
    50  
    51  // get value from named ref.
    52  func get(name, value string) Map {
    53  	return Map{
    54  		"Fn::GetAtt": []string{
    55  			name,
    56  			value,
    57  		},
    58  	}
    59  }
    60  
    61  // join strings with delim.
    62  func join(delim string, s ...interface{}) Map {
    63  	return Map{
    64  		"Fn::Join": []interface{}{
    65  			delim,
    66  			s,
    67  		},
    68  	}
    69  }
    70  
    71  // stageVariable by name.
    72  func stageVariable(name string) string {
    73  	return fmt.Sprintf("${stageVariables.%s}", name)
    74  }
    75  
    76  // lambda ARN for function name.
    77  func lambdaArn(name string) Map {
    78  	return join(":", "arn", "aws", "lambda", ref("AWS::Region"), ref("AWS::AccountId"), "function", ref(name))
    79  }
    80  
    81  // lambda ARN for function name with qualifier.
    82  func lambdaArnQualifier(name, qualifier string) Map {
    83  	return join(":", "arn", "aws", "lambda", ref("AWS::Region"), ref("AWS::AccountId"), "function", join(":", ref(name), qualifier))
    84  }
    85  
    86  // getZone returns a zone by domain or nil.
    87  func getZone(c *Config, domain string) *route53.HostedZone {
    88  	for _, z := range c.Zones {
    89  		if *z.Name == domain+"." {
    90  			return z
    91  		}
    92  	}
    93  	return nil
    94  }
    95  
    96  // dnsZone returns the ref to a new zone, or id to an existing zone.
    97  func dnsZone(c *Config, m Map, domain string) interface{} {
    98  	// already exists
    99  	if z := getZone(c, domain); z != nil {
   100  		return *z.Id
   101  	}
   102  
   103  	id := util.Camelcase("dns_zone_%s", domain)
   104  
   105  	// already registered for creation
   106  	if m[id] != nil {
   107  		return ref(id)
   108  	}
   109  
   110  	// new zone
   111  	m[id] = Map{
   112  		"Type": "AWS::Route53::HostedZone",
   113  		"Properties": Map{
   114  			"Name": domain,
   115  		},
   116  	}
   117  
   118  	return ref(id)
   119  }
   120  
   121  // api sets up the app resources.
   122  func api(c *Config, m Map) {
   123  	m["Api"] = Map{
   124  		"Type": "AWS::ApiGateway::RestApi",
   125  		"Properties": Map{
   126  			"Name":        ref("Name"),
   127  			"Description": util.ManagedByUp(c.Description),
   128  			"BinaryMediaTypes": []string{
   129  				"*/*",
   130  			},
   131  		},
   132  	}
   133  
   134  	integration := Map{
   135  		"Type":                  "AWS_PROXY",
   136  		"IntegrationHttpMethod": "POST",
   137  		"Uri": join("",
   138  			"arn:aws:apigateway:",
   139  			ref("AWS::Region"),
   140  			":lambda:path/2015-03-31/functions/",
   141  			lambdaArnQualifier("FunctionName", stageVariable("qualifier")),
   142  			"/invocations"),
   143  	}
   144  
   145  	m["ApiRootMethod"] = Map{
   146  		"Type": "AWS::ApiGateway::Method",
   147  		"Properties": Map{
   148  			"RestApiId":         ref("Api"),
   149  			"ResourceId":        get("Api", "RootResourceId"),
   150  			"HttpMethod":        "ANY",
   151  			"AuthorizationType": "NONE",
   152  			"Integration":       integration,
   153  		},
   154  	}
   155  
   156  	m["ApiProxyResource"] = Map{
   157  		"Type": "AWS::ApiGateway::Resource",
   158  		"Properties": Map{
   159  			"RestApiId": ref("Api"),
   160  			"ParentId":  get("Api", "RootResourceId"),
   161  			"PathPart":  "{proxy+}",
   162  		},
   163  	}
   164  
   165  	m["ApiProxyMethod"] = Map{
   166  		"Type": "AWS::ApiGateway::Method",
   167  		"Properties": Map{
   168  			"RestApiId":         ref("Api"),
   169  			"ResourceId":        ref("ApiProxyResource"),
   170  			"HttpMethod":        "ANY",
   171  			"AuthorizationType": "NONE",
   172  			"Integration":       integration,
   173  		},
   174  	}
   175  
   176  	stages(c, m)
   177  }
   178  
   179  // stages sets up the stage specific resources.
   180  func stages(c *Config, m Map) {
   181  	for _, s := range c.Stages.List() {
   182  		if s.IsRemote() {
   183  			stage(c, s, m)
   184  		}
   185  	}
   186  }
   187  
   188  // stage sets up the stage specific resources.
   189  func stage(c *Config, s *config.Stage, m Map) {
   190  	aliasID := stageAlias(c, s, m)
   191  	deploymentID := stageDeployment(c, s, m, aliasID)
   192  	stagePermissions(c, s, m, aliasID)
   193  	stageDomain(c, s, m, deploymentID)
   194  }
   195  
   196  // stageAlias sets up the lambda alias and deployment and returns the alias id.
   197  func stageAlias(c *Config, s *config.Stage, m Map) string {
   198  	id := util.Camelcase("api_function_alias_%s", s.Name)
   199  	version, ok := c.Versions[s.Name]
   200  
   201  	if !ok {
   202  		panic(fmt.Sprintf("stage %q is missing a function version mapping", s.Name))
   203  	}
   204  
   205  	m[id] = Map{
   206  		"Type": "AWS::Lambda::Alias",
   207  		"Properties": Map{
   208  			"Name":            s.Name,
   209  			"Description":     util.ManagedByUp(""),
   210  			"FunctionName":    ref("FunctionName"),
   211  			"FunctionVersion": version,
   212  		},
   213  	}
   214  
   215  	return id
   216  }
   217  
   218  // stagePermissions sets up the lambda:invokeFunction permissions for API Gateway.
   219  func stagePermissions(c *Config, s *config.Stage, m Map, aliasID string) {
   220  	id := util.Camelcase("api_lambda_permission_%s", s.Name)
   221  
   222  	m[id] = Map{
   223  		"Type":      "AWS::Lambda::Permission",
   224  		"DependsOn": aliasID,
   225  		"Properties": Map{
   226  			"Action":       "lambda:invokeFunction",
   227  			"FunctionName": lambdaArnQualifier("FunctionName", s.Name),
   228  			"Principal":    "apigateway.amazonaws.com",
   229  			"SourceArn": join("",
   230  				"arn:aws:execute-api",
   231  				":",
   232  				ref("AWS::Region"),
   233  				":",
   234  				ref("AWS::AccountId"),
   235  				":",
   236  				ref("Api"),
   237  				"/*"),
   238  		},
   239  	}
   240  }
   241  
   242  // stageDeployment sets up the API Gateway deployment.
   243  func stageDeployment(c *Config, s *config.Stage, m Map, aliasID string) string {
   244  	id := util.Camelcase("api_deployment_%s", s.Name)
   245  
   246  	m[id] = Map{
   247  		"Type":      "AWS::ApiGateway::Deployment",
   248  		"DependsOn": []string{"ApiRootMethod", "ApiProxyMethod", aliasID},
   249  		"Properties": Map{
   250  			"RestApiId": ref("Api"),
   251  			"StageName": s.Name,
   252  			"StageDescription": Map{
   253  				"Variables": Map{
   254  					"qualifier": s.Name,
   255  				},
   256  			},
   257  		},
   258  	}
   259  
   260  	return id
   261  }
   262  
   263  // stageDomain sets up a custom domain, dns record and path mapping.
   264  func stageDomain(c *Config, s *config.Stage, m Map, deploymentID string) {
   265  	if s.Domain == "" {
   266  		return
   267  	}
   268  
   269  	id := util.Camelcase("api_domain_%s", s.Name)
   270  
   271  	m[id] = Map{
   272  		"Type": "AWS::ApiGateway::DomainName",
   273  		"Properties": Map{
   274  			"CertificateArn": s.Cert,
   275  			"DomainName":     s.Domain,
   276  		},
   277  	}
   278  
   279  	stagePathMapping(c, s, m, deploymentID, id)
   280  
   281  	if s.Zone != false {
   282  		stageDNSRecord(c, s, m, id)
   283  	}
   284  }
   285  
   286  // stagePathMapping sets up the stage deployment mapping.
   287  func stagePathMapping(c *Config, s *config.Stage, m Map, deploymentID, domainID string) {
   288  	id := util.Camelcase("api_domain_%s_path_mapping", s.Name)
   289  
   290  	m[id] = Map{
   291  		"Type":      "AWS::ApiGateway::BasePathMapping",
   292  		"DependsOn": []string{deploymentID, domainID},
   293  		"Properties": Map{
   294  			"DomainName": s.Domain,
   295  			"BasePath":   util.BasePath(s.Path),
   296  			"RestApiId":  ref("Api"),
   297  			"Stage":      s.Name,
   298  		},
   299  	}
   300  }
   301  
   302  // stageDNSRecord sets up an ALIAS record and zone if necessary for a custom domain.
   303  func stageDNSRecord(c *Config, s *config.Stage, m Map, domainID string) {
   304  	id := util.Camelcase("dns_zone_%s_record_%s", util.Domain(s.Domain), s.Domain)
   305  	zoneName := util.Domain(s.Domain)
   306  
   307  	// explicit .zone was specified
   308  	if s, ok := s.Zone.(string); ok {
   309  		zoneName = s
   310  	}
   311  
   312  	zone := dnsZone(c, m, zoneName)
   313  
   314  	m[id] = Map{
   315  		"Type": "AWS::Route53::RecordSet",
   316  		"Properties": Map{
   317  			"Name":         s.Domain,
   318  			"Type":         "A",
   319  			"Comment":      util.ManagedByUp(""),
   320  			"HostedZoneId": zone,
   321  			"AliasTarget": Map{
   322  				"DNSName":      get(domainID, "DistributionDomainName"),
   323  				"HostedZoneId": "Z2FDTNDATAQYW2",
   324  			},
   325  		},
   326  	}
   327  }
   328  
   329  // dns setups the the user-defined DNS zones and records.
   330  func dns(c *Config, m Map) {
   331  	for _, z := range c.DNS.Zones {
   332  		zone := dnsZone(c, m, z.Name)
   333  
   334  		for _, r := range z.Records {
   335  			id := util.Camelcase("dns_zone_%s_record_%s_%s", z.Name, r.Name, r.Type)
   336  
   337  			m[id] = Map{
   338  				"Type": "AWS::Route53::RecordSet",
   339  				"Properties": Map{
   340  					"Name":            r.Name,
   341  					"Type":            r.Type,
   342  					"TTL":             strconv.Itoa(r.TTL),
   343  					"ResourceRecords": r.Value,
   344  					"HostedZoneId":    zone,
   345  					"Comment":         util.ManagedByUp(""),
   346  				},
   347  			}
   348  		}
   349  	}
   350  }
   351  
   352  // resources of the stack.
   353  func resources(c *Config) Map {
   354  	m := Map{}
   355  	api(c, m)
   356  	dns(c, m)
   357  	alerting(c, m)
   358  	warming(c, m)
   359  	return m
   360  }
   361  
   362  // parameters of the stack.
   363  func parameters(c *Config) Map {
   364  	return Map{
   365  		"Name": Map{
   366  			"Description": "Name of application",
   367  			"Type":        "String",
   368  		},
   369  		"FunctionName": Map{
   370  			"Description": "Name of application function",
   371  			"Type":        "String",
   372  		},
   373  	}
   374  }
   375  
   376  // outputs of the stack.
   377  func outputs(c *Config) Map {
   378  	return Map{
   379  		"ApiName": Map{
   380  			"Description": "API name",
   381  			"Value":       ref("Name"),
   382  		},
   383  		"ApiFunctionName": Map{
   384  			"Description": "API Lambda function name",
   385  			"Value":       ref("FunctionName"),
   386  		},
   387  		"ApiFunctionArn": Map{
   388  			"Description": "API Lambda function ARN",
   389  			"Value":       lambdaArn("FunctionName"),
   390  		},
   391  	}
   392  }