github.com/orangenpresse/up@v0.6.0/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  	stageDNSRecord(c, s, m, id)
   281  }
   282  
   283  // stagePathMapping sets up the stage deployment mapping.
   284  func stagePathMapping(c *Config, s *config.Stage, m Map, deploymentID, domainID string) {
   285  	id := util.Camelcase("api_domain_%s_path_mapping", s.Name)
   286  
   287  	m[id] = Map{
   288  		"Type":      "AWS::ApiGateway::BasePathMapping",
   289  		"DependsOn": []string{deploymentID, domainID},
   290  		"Properties": Map{
   291  			"DomainName": s.Domain,
   292  			"BasePath":   util.BasePath(s.Path),
   293  			"RestApiId":  ref("Api"),
   294  			"Stage":      s.Name,
   295  		},
   296  	}
   297  }
   298  
   299  // stageDNSRecord sets up an ALIAS record and zone if necessary for a custom domain.
   300  func stageDNSRecord(c *Config, s *config.Stage, m Map, domainID string) {
   301  	id := util.Camelcase("dns_zone_%s_record_%s", util.Domain(s.Domain), s.Domain)
   302  	zone := dnsZone(c, m, util.Domain(s.Domain))
   303  
   304  	m[id] = Map{
   305  		"Type": "AWS::Route53::RecordSet",
   306  		"Properties": Map{
   307  			"Name":         s.Domain,
   308  			"Type":         "A",
   309  			"Comment":      util.ManagedByUp(""),
   310  			"HostedZoneId": zone,
   311  			"AliasTarget": Map{
   312  				"DNSName":      get(domainID, "DistributionDomainName"),
   313  				"HostedZoneId": "Z2FDTNDATAQYW2",
   314  			},
   315  		},
   316  	}
   317  }
   318  
   319  // dns setups the the user-defined DNS zones and records.
   320  func dns(c *Config, m Map) {
   321  	for _, z := range c.DNS.Zones {
   322  		zone := dnsZone(c, m, z.Name)
   323  
   324  		for _, r := range z.Records {
   325  			id := util.Camelcase("dns_zone_%s_record_%s_%s", z.Name, r.Name, r.Type)
   326  
   327  			m[id] = Map{
   328  				"Type": "AWS::Route53::RecordSet",
   329  				"Properties": Map{
   330  					"Name":            r.Name,
   331  					"Type":            r.Type,
   332  					"TTL":             strconv.Itoa(r.TTL),
   333  					"ResourceRecords": r.Value,
   334  					"HostedZoneId":    zone,
   335  					"Comment":         util.ManagedByUp(""),
   336  				},
   337  			}
   338  		}
   339  	}
   340  }
   341  
   342  // resources of the stack.
   343  func resources(c *Config) Map {
   344  	m := Map{}
   345  	api(c, m)
   346  	dns(c, m)
   347  	return m
   348  }
   349  
   350  // parameters of the stack.
   351  func parameters(c *Config) Map {
   352  	return Map{
   353  		"Name": Map{
   354  			"Description": "Name of application",
   355  			"Type":        "String",
   356  		},
   357  		"FunctionName": Map{
   358  			"Description": "Name of application function",
   359  			"Type":        "String",
   360  		},
   361  	}
   362  }
   363  
   364  // outputs of the stack.
   365  func outputs(c *Config) Map {
   366  	return Map{
   367  		"ApiName": Map{
   368  			"Description": "API name",
   369  			"Value":       ref("Name"),
   370  		},
   371  		"ApiFunctionName": Map{
   372  			"Description": "API Lambda function name",
   373  			"Value":       ref("FunctionName"),
   374  		},
   375  		"ApiFunctionArn": Map{
   376  			"Description": "API Lambda function ARN",
   377  			"Value":       lambdaArn("FunctionName"),
   378  		},
   379  	}
   380  }