platform/lambda/stack/resources/resources.go

Summary

Maintainability
A
0 mins
Test Coverage
package resources

import (
    "fmt"
    "strconv"

    "github.com/apex/up"
    "github.com/apex/up/config"
    "github.com/apex/up/internal/util"
    "github.com/aws/aws-sdk-go/service/route53"
)

// Map type.
type Map map[string]interface{}

// Versions is a map of stage to lambda function version.
type Versions map[string]string

// Config for the resource template.
type Config struct {
    // Zones already present in route53. This is used to
    // ensure that existing zones previously set up, or
    // automatically configured when purchasing a domain
    // are not duplicated.
    Zones []*route53.HostedZone

    // Versions map used to maintain the correct lambda
    // function aliases when updating a stack.
    Versions Versions

    *up.Config
}

// New template.
func New(c *Config) map[string]interface{} {
    return Map{
        "AWSTemplateFormatVersion": "2010-09-09",
        "Parameters":               parameters(c),
        "Outputs":                  outputs(c),
        "Resources":                resources(c),
    }
}

// ref of id.
func ref(id string) Map {
    return Map{
        "Ref": id,
    }
}

// get value from named ref.
func get(name, value string) Map {
    return Map{
        "Fn::GetAtt": []string{
            name,
            value,
        },
    }
}

// join strings with delim.
func join(delim string, s ...interface{}) Map {
    return Map{
        "Fn::Join": []interface{}{
            delim,
            s,
        },
    }
}

// stageVariable by name.
func stageVariable(name string) string {
    return fmt.Sprintf("${stageVariables.%s}", name)
}

// lambda ARN for function name.
func lambdaArn(name string) Map {
    return join(":", "arn", "aws", "lambda", ref("AWS::Region"), ref("AWS::AccountId"), "function", ref(name))
}

// lambda ARN for function name with qualifier.
func lambdaArnQualifier(name, qualifier string) Map {
    return join(":", "arn", "aws", "lambda", ref("AWS::Region"), ref("AWS::AccountId"), "function", join(":", ref(name), qualifier))
}

// getZone returns a zone by domain or nil.
func getZone(c *Config, domain string) *route53.HostedZone {
    for _, z := range c.Zones {
        if *z.Name == domain+"." {
            return z
        }
    }
    return nil
}

// dnsZone returns the ref to a new zone, or id to an existing zone.
func dnsZone(c *Config, m Map, domain string) interface{} {
    // already exists
    if z := getZone(c, domain); z != nil {
        return *z.Id
    }

    id := util.Camelcase("dns_zone_%s", domain)

    // already registered for creation
    if m[id] != nil {
        return ref(id)
    }

    // new zone
    m[id] = Map{
        "Type":                "AWS::Route53::HostedZone",
        "DeletionPolicy":      "Retain",
        "UpdateReplacePolicy": "Retain",
        "Properties": Map{
            "Name": domain,
        },
    }

    return ref(id)
}

// api sets up the app resources.
func api(c *Config, m Map) {
    m["Api"] = Map{
        "Type": "AWS::ApiGateway::RestApi",
        "Properties": Map{
            "Name":        ref("Name"),
            "Description": util.ManagedByUp(c.Description),
            "BinaryMediaTypes": []string{
                "*/*",
            },
        },
    }

    integration := Map{
        "Type":                  "AWS_PROXY",
        "IntegrationHttpMethod": "POST",
        "Uri": join("",
            "arn:aws:apigateway:",
            ref("AWS::Region"),
            ":lambda:path/2015-03-31/functions/",
            lambdaArnQualifier("FunctionName", stageVariable("qualifier")),
            "/invocations"),
    }

    m["ApiRootMethod"] = Map{
        "Type": "AWS::ApiGateway::Method",
        "Properties": Map{
            "RestApiId":         ref("Api"),
            "ResourceId":        get("Api", "RootResourceId"),
            "HttpMethod":        "ANY",
            "AuthorizationType": "NONE",
            "Integration":       integration,
        },
    }

    m["ApiProxyResource"] = Map{
        "Type": "AWS::ApiGateway::Resource",
        "Properties": Map{
            "RestApiId": ref("Api"),
            "ParentId":  get("Api", "RootResourceId"),
            "PathPart":  "{proxy+}",
        },
    }

    m["ApiProxyMethod"] = Map{
        "Type": "AWS::ApiGateway::Method",
        "Properties": Map{
            "RestApiId":         ref("Api"),
            "ResourceId":        ref("ApiProxyResource"),
            "HttpMethod":        "ANY",
            "AuthorizationType": "NONE",
            "Integration":       integration,
        },
    }

    stages(c, m)
}

// stages sets up the stage specific resources.
func stages(c *Config, m Map) {
    for _, s := range c.Stages.List() {
        if s.IsRemote() {
            stage(c, s, m)
        }
    }
}

// stage sets up the stage specific resources.
func stage(c *Config, s *config.Stage, m Map) {
    aliasID := stageAlias(c, s, m)
    deploymentID := stageDeployment(c, s, m, aliasID)
    stagePermissions(c, s, m, aliasID)
    stageDomain(c, s, m, deploymentID)
}

// stageAlias sets up the lambda alias and deployment and returns the alias id.
func stageAlias(c *Config, s *config.Stage, m Map) string {
    id := util.Camelcase("api_function_alias_%s", s.Name)
    version, ok := c.Versions[s.Name]

    if !ok {
        panic(fmt.Sprintf("stage %q is missing a function version mapping", s.Name))
    }

    m[id] = Map{
        "Type": "AWS::Lambda::Alias",
        "Properties": Map{
            "Name":            s.Name,
            "Description":     util.ManagedByUp(""),
            "FunctionName":    ref("FunctionName"),
            "FunctionVersion": version,
        },
    }

    return id
}

// stagePermissions sets up the lambda:invokeFunction permissions for API Gateway.
func stagePermissions(c *Config, s *config.Stage, m Map, aliasID string) {
    id := util.Camelcase("api_lambda_permission_%s", s.Name)

    m[id] = Map{
        "Type":      "AWS::Lambda::Permission",
        "DependsOn": aliasID,
        "Properties": Map{
            "Action":       "lambda:invokeFunction",
            "FunctionName": lambdaArnQualifier("FunctionName", s.Name),
            "Principal":    "apigateway.amazonaws.com",
            "SourceArn": join("",
                "arn:aws:execute-api",
                ":",
                ref("AWS::Region"),
                ":",
                ref("AWS::AccountId"),
                ":",
                ref("Api"),
                "/*"),
        },
    }
}

// stageDeployment sets up the API Gateway deployment.
func stageDeployment(c *Config, s *config.Stage, m Map, aliasID string) string {
    id := util.Camelcase("api_deployment_%s", s.Name)

    m[id] = Map{
        "Type":      "AWS::ApiGateway::Deployment",
        "DependsOn": []string{"ApiRootMethod", "ApiProxyMethod", aliasID},
        "Properties": Map{
            "RestApiId": ref("Api"),
            "StageName": s.Name,
            "StageDescription": Map{
                "Variables": Map{
                    "qualifier": s.Name,
                },
            },
        },
    }

    return id
}

// stageDomain sets up a custom domain, dns record and path mapping.
func stageDomain(c *Config, s *config.Stage, m Map, deploymentID string) {
    if s.Domain == "" {
        return
    }

    id := util.Camelcase("api_domain_%s", s.Name)

    m[id] = Map{
        "Type": "AWS::ApiGateway::DomainName",
        "Properties": Map{
            "CertificateArn": s.Cert,
            "DomainName":     s.Domain,
        },
    }

    stagePathMapping(c, s, m, deploymentID, id)

    if s.Zone != false {
        stageDNSRecord(c, s, m, id)
    }
}

// stagePathMapping sets up the stage deployment mapping.
func stagePathMapping(c *Config, s *config.Stage, m Map, deploymentID, domainID string) {
    id := util.Camelcase("api_domain_%s_path_mapping", s.Name)

    m[id] = Map{
        "Type":      "AWS::ApiGateway::BasePathMapping",
        "DependsOn": []string{deploymentID, domainID},
        "Properties": Map{
            "DomainName": s.Domain,
            "BasePath":   util.BasePath(s.Path),
            "RestApiId":  ref("Api"),
            "Stage":      s.Name,
        },
    }
}

// stageDNSRecord sets up an ALIAS record and zone if necessary for a custom domain.
func stageDNSRecord(c *Config, s *config.Stage, m Map, domainID string) {
    id := util.Camelcase("dns_zone_%s_record_%s", util.Domain(s.Domain), s.Domain)
    zoneName := util.Domain(s.Domain)

    // explicit .zone was specified
    if s, ok := s.Zone.(string); ok {
        zoneName = s
    }

    zone := dnsZone(c, m, zoneName)

    m[id] = Map{
        "Type": "AWS::Route53::RecordSet",
        "Properties": Map{
            "Name":         s.Domain,
            "Type":         "A",
            "Comment":      util.ManagedByUp(""),
            "HostedZoneId": zone,
            "AliasTarget": Map{
                "DNSName":      get(domainID, "DistributionDomainName"),
                "HostedZoneId": "Z2FDTNDATAQYW2",
            },
        },
    }
}

// dns setups the the user-defined DNS zones and records.
func dns(c *Config, m Map) {
    for _, z := range c.DNS.Zones {
        zone := dnsZone(c, m, z.Name)

        for _, r := range z.Records {
            id := util.Camelcase("dns_zone_%s_record_%s_%s", z.Name, r.Name, r.Type)

            m[id] = Map{
                "Type": "AWS::Route53::RecordSet",
                "Properties": Map{
                    "Name":            r.Name,
                    "Type":            r.Type,
                    "TTL":             strconv.Itoa(r.TTL),
                    "ResourceRecords": r.Value,
                    "HostedZoneId":    zone,
                    "Comment":         util.ManagedByUp(""),
                },
            }
        }
    }
}

// resources of the stack.
func resources(c *Config) Map {
    m := Map{}
    api(c, m)
    dns(c, m)
    return m
}

// parameters of the stack.
func parameters(c *Config) Map {
    return Map{
        "Name": Map{
            "Description": "Name of application",
            "Type":        "String",
        },
        "FunctionName": Map{
            "Description": "Name of application function",
            "Type":        "String",
        },
    }
}

// outputs of the stack.
func outputs(c *Config) Map {
    return Map{
        "ApiName": Map{
            "Description": "API name",
            "Value":       ref("Name"),
        },
        "ApiFunctionName": Map{
            "Description": "API Lambda function name",
            "Value":       ref("FunctionName"),
        },
        "ApiFunctionArn": Map{
            "Description": "API Lambda function ARN",
            "Value":       lambdaArn("FunctionName"),
        },
    }
}