platform/lambda/lambda.go

Summary

Maintainability
D
2 days
Test Coverage
// Package lambda implements the API Gateway & AWS Lambda platform.
package lambda

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "os"
    "strings"
    "sync"
    "time"

    "github.com/apex/log"
    "github.com/apex/log/handlers/discard"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/acm"
    "github.com/aws/aws-sdk-go/service/apigateway"
    "github.com/aws/aws-sdk-go/service/iam"
    "github.com/aws/aws-sdk-go/service/lambda"
    "github.com/aws/aws-sdk-go/service/route53"
    "github.com/aws/aws-sdk-go/service/s3"
    "github.com/aws/aws-sdk-go/service/s3/s3manager"
    "github.com/dchest/uniuri"
    humanize "github.com/dustin/go-humanize"
    "github.com/golang/sync/errgroup"
    "github.com/pkg/errors"

    "github.com/apex/up"
    "github.com/apex/up/config"
    "github.com/apex/up/internal/proxy/bin"
    "github.com/apex/up/internal/shim"
    "github.com/apex/up/internal/util"
    "github.com/apex/up/internal/zip"
    "github.com/apex/up/platform/aws/domains"
    "github.com/apex/up/platform/aws/logs"
    "github.com/apex/up/platform/aws/runtime"
    "github.com/apex/up/platform/event"
    "github.com/apex/up/platform/lambda/stack"
    "github.com/apex/up/platform/lambda/stack/resources"
)

// errFirstDeploy is returned from .deploy() when a function is created.
var errFirstDeploy = errors.New("first deploy")

const (
    // maxCodeSize is the max code size supported by Lambda (250MiB).
    maxCodeSize = 250 << 20
)

// assume policy for the lambda function.
var apiGatewayAssumePolicy = `{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "apigateway.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        },
        {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
    ]
}`

// TODO: aggregate progress report for N regions or distinct progress bars
// TODO: refactor with another region-scoped struct to clean this up

// Platform implementation.
type Platform struct {
    config  *up.Config
    handler string
    zip     *bytes.Buffer
    events  event.Events
}

// New platform.
func New(c *up.Config, events event.Events) *Platform {
    return &Platform{
        config:  c,
        handler: "_proxy.handle",
        events:  events,
    }
}

// Build implementation.
func (p *Platform) Build() error {
    start := time.Now()
    p.zip = new(bytes.Buffer)

    if err := p.injectProxy(); err != nil {
        return errors.Wrap(err, "injecting proxy")
    }
    defer p.removeProxy()

    r, stats, err := zip.Build(".")
    if err != nil {
        return errors.Wrap(err, "zip")
    }

    if _, err := io.Copy(p.zip, r); err != nil {
        return errors.Wrap(err, "copying")
    }

    if err := r.Close(); err != nil {
        return errors.Wrap(err, "closing")
    }

    p.events.Emit("platform.build.zip", event.Fields{
        "files":             stats.FilesAdded,
        "size_uncompressed": stats.SizeUncompressed,
        "size_compressed":   p.zip.Len(),
        "duration":          time.Since(start),
    })

    if stats.SizeUncompressed > maxCodeSize {
        size := humanize.Bytes(uint64(stats.SizeUncompressed))
        max := humanize.Bytes(uint64(maxCodeSize))
        return errors.Errorf("zip contents is %s, exceeding Lambda's limit of %s", size, max)
    }

    return nil
}

// Zip returns the zip reader.
func (p *Platform) Zip() io.Reader {
    return p.zip
}

// Init initializes the runtime.
func (p *Platform) Init(stage string) error {
    return runtime.New(
        p.config,
        runtime.WithLogger(&log.Logger{
            Handler: discard.Default,
        }),
    ).Init(stage)
}

// Deploy implementation.
func (p *Platform) Deploy(d up.Deploy) error {
    regions := p.config.Regions
    var g errgroup.Group

    if err := p.createRole(); err != nil {
        return errors.Wrap(err, "iam")
    }

    for _, r := range regions {
        region := r
        g.Go(func() error {
            version, err := p.deploy(region, d)
            if err == nil {
                goto endpoint
            }

            if err != errFirstDeploy {
                return errors.Wrap(err, region)
            }

            if err := p.CreateStack(region, version); err != nil {
                return errors.Wrap(err, region)
            }

        endpoint:
            url, err := p.URL(region, d.Stage)
            if err != nil {
                return errors.Wrap(err, "fetching url")
            }

            p.events.Emit("platform.deploy.url", event.Fields{
                "url": url,
            })

            return nil
        })
    }

    return g.Wait()
}

// Logs implementation.
func (p *Platform) Logs(c up.LogsConfig) up.Logs {
    g := "/aws/lambda/" + p.config.Name
    return logs.New(g, c)
}

// Domains implementation.
func (p *Platform) Domains() up.Domains {
    return domains.New()
}

// URL returns the stage url.
func (p *Platform) URL(region, stage string) (string, error) {
    s := session.New(aws.NewConfig().WithRegion(region))
    c := apigateway.New(s)

    api, err := p.getAPI(c)
    if err != nil {
        return "", errors.Wrap(err, "fetching api")
    }

    if api == nil {
        return "", errors.Errorf("cannot find the API, looks like you haven't deployed")
    }

    id := fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/%s/", *api.Id, region, stage)
    return id, nil
}

// CreateStack implementation.
func (p *Platform) CreateStack(region, version string) error {
    versions := make(resources.Versions)

    for _, s := range p.config.Stages {
        versions[s.Name] = version
    }

    if err := p.createCerts(); err != nil {
        return errors.Wrap(err, "creating certs")
    }

    zones, err := p.getHostedZone()
    if err != nil {
        return errors.Wrap(err, "fetching zones")
    }

    return stack.New(p.config, p.events, zones, region).Create(versions)
}

// DeleteStack implementation.
func (p *Platform) DeleteStack(region string, wait bool) error {
    versions := resources.Versions{}

    for _, s := range p.config.Stages {
        versions[s.Name] = "1"
    }

    if err := p.createRole(); err != nil {
        return errors.Wrap(err, "creating iam role")
    }

    log.Debug("deleting bucket objects")
    if err := p.deleteBucketObjects(region); err != nil && !util.IsNotFound(err) {
        return errors.Wrap(err, "deleting s3 objects")
    }

    log.Debug("deleting stack")
    if err := stack.New(p.config, p.events, nil, region).Delete(versions, wait); err != nil && !util.IsNotFound(err) {
        return errors.Wrap(err, "deleting stack")
    }

    log.Debug("deleting function")
    if err := p.deleteFunction(region); err != nil && !util.IsNotFound(err) {
        return errors.Wrap(err, "deleting function")
    }

    log.Debug("deleting role")
    if err := p.deleteRole(region); err != nil && !util.IsNotFound(err) {
        return errors.Wrap(err, "deleting function iam role")
    }

    return nil
}

// ShowStack implementation.
func (p *Platform) ShowStack(region string) error {
    return stack.New(p.config, p.events, nil, region).Show()
}

// PlanStack implementation.
func (p *Platform) PlanStack(region string) error {
    versions, err := p.getAliasVersions(region)
    if err != nil {
        return errors.Wrap(err, "fetching alias versions")
    }

    if err := p.createCerts(); err != nil {
        return errors.Wrap(err, "creating certs")
    }

    zones, err := p.getHostedZone()
    if err != nil {
        return errors.Wrap(err, "fetching zones")
    }

    return stack.New(p.config, p.events, zones, region).Plan(versions)
}

// ApplyStack implementation.
func (p *Platform) ApplyStack(region string) error {
    if err := p.createCerts(); err != nil {
        return errors.Wrap(err, "creating certs")
    }

    return stack.New(p.config, p.events, nil, region).Apply()
}

// Exists implementation.
func (p *Platform) Exists(region string) (bool, error) {
    log.Debug("checking if application exists")
    c := lambda.New(session.New(aws.NewConfig().WithRegion(region)))

    _, err := c.GetFunctionConfiguration(&lambda.GetFunctionConfigurationInput{
        FunctionName: &p.config.Name,
    })

    if util.IsNotFound(err) {
        return false, nil
    }

    if err != nil {
        return false, err
    }

    return true, nil
}

// getAliasVersions returns the function alias versions.
func (p *Platform) getAliasVersions(region string) (resources.Versions, error) {
    var g errgroup.Group
    var mu sync.Mutex

    c := lambda.New(session.New(aws.NewConfig().WithRegion(region)))
    versions := make(resources.Versions)

    log.Debug("fetching aliases")
    for _, s := range p.config.Stages {
        s := s

        g.Go(func() error {
            log.Debugf("fetching %s alias", s.Name)
            version, err := p.getAliasVersion(c, s.Name)

            if util.IsNotFound(err) {
                log.Debugf("%s has no alias, defaulting to staging", s.Name)
                version, err = p.getAliasVersion(c, "staging")
                if err != nil {
                    return errors.Wrap(err, "fetching staging alias")
                }
            }

            if err != nil {
                return errors.Wrapf(err, "fetching %q alias", s.Name)
            }

            log.Debugf("fetched %s alias (%s)", s.Name, version)
            mu.Lock()
            versions[s.Name] = version
            mu.Unlock()

            return nil
        })
    }

    return versions, g.Wait()
}

// getAliasVersion retruns the alias version for a stage.
func (p *Platform) getAliasVersion(c *lambda.Lambda, stage string) (string, error) {
    res, err := c.GetAlias(&lambda.GetAliasInput{
        FunctionName: &p.config.Name,
        Name:         &stage,
    })

    if err != nil {
        return "", err
    }

    return *res.FunctionVersion, nil
}

// getHostedZone returns existing hosted zones.
func (p *Platform) getHostedZone() (zones []*route53.HostedZone, err error) {
    r := route53.New(session.New(aws.NewConfig()))

    log.Debug("fetching hosted zones")
    res, err := r.ListHostedZonesByName(&route53.ListHostedZonesByNameInput{
        MaxItems: aws.String("100"),
    })

    if err != nil {
        return
    }

    zones = res.HostedZones
    return
}

// createCerts creates the certificates if necessary.
//
// We perform this task outside of CloudFormation because
// the certificates currently must be created in the us-east-1
// region. This also gives us a chance to let the user know
// that they have to confirm an email.
func (p *Platform) createCerts() error {
    s := session.New(aws.NewConfig().WithRegion("us-east-1"))
    a := acm.New(s)
    var domains []string

    // existing certs
    log.Debug("fetching existing certs")
    certs, err := getCerts(a)
    if err != nil {
        return errors.Wrap(err, "fetching certs")
    }

    // request certs
    for _, s := range p.config.Stages.List() {
        if s.Domain == "" {
            continue
        }

        certDomains := util.CertDomainNames(s.Domain)

        // see if the cert exists
        log.Debugf("looking up cert for %s", s.Domain)
        arn := getCert(certs, s.Domain)
        if arn != "" {
            log.Debugf("found cert for %s: %s", s.Domain, arn)
            s.Cert = arn
            continue
        }

        option := acm.DomainValidationOption{
            DomainName:       aws.String(certDomains[0]),
            ValidationDomain: aws.String(util.Domain(s.Domain)),
        }

        options := []*acm.DomainValidationOption{
            &option,
        }

        // request the cert
        res, err := a.RequestCertificate(&acm.RequestCertificateInput{
            DomainName:              aws.String(certDomains[0]),
            DomainValidationOptions: options,
            SubjectAlternativeNames: aws.StringSlice(certDomains[1:]),
        })

        if err != nil {
            return errors.Wrapf(err, "requesting cert for %v", certDomains)
        }

        domains = append(domains, certDomains[0])
        s.Cert = *res.CertificateArn
    }

    // no certs needed
    if len(domains) == 0 {
        return nil
    }

    defer p.events.Time("platform.certs.create", event.Fields{
        "domains": domains,
    })()

    // wait for approval
    for range time.Tick(4 * time.Second) {
        res, err := a.ListCertificates(&acm.ListCertificatesInput{
            MaxItems:            aws.Int64(1000),
            CertificateStatuses: aws.StringSlice([]string{acm.CertificateStatusPendingValidation}),
        })

        if err != nil {
            return errors.Wrap(err, "listing")
        }

        if len(res.CertificateSummaryList) == 0 {
            break
        }
    }

    return nil
}

// deploy to the given region.
func (p *Platform) deploy(region string, d up.Deploy) (version string, err error) {
    start := time.Now()

    fields := event.Fields{
        "commit": d.Commit,
        "stage":  d.Stage,
        "region": region,
    }

    p.events.Emit("platform.deploy", fields)

    defer func() {
        fields["duration"] = time.Since(start)
        fields["commit"] = d.Commit
        fields["version"] = version
        p.events.Emit("platform.deploy.complete", fields)
    }()

    ctx := log.WithField("region", region)
    s := session.New(aws.NewConfig().WithRegion(region))
    u := s3manager.NewUploaderWithClient(s3.New(s))
    a := apigateway.New(s)
    c := lambda.New(s)

    ctx.Debug("fetching function config")
    _, err = c.GetFunctionConfiguration(&lambda.GetFunctionConfigurationInput{
        FunctionName: &p.config.Name,
    })

    if util.IsNotFound(err) {
        defer p.events.Time("platform.function.create", fields)
        return p.createFunction(c, a, u, region, d)
    }

    if err != nil {
        return "", errors.Wrap(err, "fetching function config")
    }

    defer p.events.Time("platform.function.update", fields)
    return p.updateFunction(c, a, u, region, d)
}

// createFunction creates the function.
func (p *Platform) createFunction(c *lambda.Lambda, a *apigateway.APIGateway, up *s3manager.Uploader, region string, d up.Deploy) (version string, err error) {
    // ensure bucket exists
    if err := p.createBucket(region); err != nil && !util.IsBucketExists(err) {
        return "", errors.Wrap(err, "creating s3 bucket")
    }

    // upload to s3
    b := aws.String(p.getS3BucketName(region))
    k := aws.String(p.getS3Key(d.Stage))

    log.Debugf("uploading function to bucket %s key %s", *b, *k)
    _, err = up.Upload(&s3manager.UploadInput{
        Bucket:               b,
        Key:                  k,
        Body:                 bytes.NewReader(p.zip.Bytes()),
        ServerSideEncryption: aws.String("aws:kms"),
    })

    if err != nil {
        return "", errors.Wrap(err, "uploading function")
    }

    // load environment
    env, err := p.loadEnvironment(d)
    if err != nil {
        return "", errors.Wrap(err, "loading environment variables")
    }

    // create function
retry:
    log.Debug("creating function")
    res, err := c.CreateFunction(&lambda.CreateFunctionInput{
        FunctionName: &p.config.Name,
        Handler:      &p.handler,
        Runtime:      &p.config.Lambda.Runtime,
        Role:         &p.config.Lambda.Role,
        MemorySize:   aws.Int64(int64(p.config.Lambda.Memory)),
        Timeout:      aws.Int64(int64(p.config.Lambda.Timeout)),
        Publish:      aws.Bool(true),
        Environment:  env,
        Code: &lambda.FunctionCode{
            S3Bucket: b,
            S3Key:    k,
        },
        VpcConfig: p.vpc(),
    })

    // IAM is eventually consistent apparently, so we have to keep retrying
    if isCreatingRole(err) {
        log.Debug("waiting for role to be created")
        time.Sleep(500 * time.Millisecond)
        goto retry
    }

    if err != nil {
        return "", errors.Wrap(err, "creating function")
    }

    return *res.Version, errFirstDeploy
}

// updateFunction updates the function.
func (p *Platform) updateFunction(c *lambda.Lambda, a *apigateway.APIGateway, up *s3manager.Uploader, region string, d up.Deploy) (version string, err error) {
    b := aws.String(p.getS3BucketName(region))
    k := aws.String(p.getS3Key(d.Stage))

    // upload
    log.Debugf("uploading function to bucket %s key %s", *b, *k)
    _, err = up.Upload(&s3manager.UploadInput{
        Bucket:               b,
        Key:                  k,
        Body:                 bytes.NewReader(p.zip.Bytes()),
        ServerSideEncryption: aws.String("aws:kms"),
    })

    // ensure bucket exists
    if util.IsNotFound(err) {
        if err := p.createBucket(region); err != nil {
            return "", errors.Wrap(err, "creating s3 bucket")
        }
        err = nil
    }

    if err != nil {
        return "", errors.Wrap(err, "uploading function")
    }

    // load environment
    env, err := p.loadEnvironment(d)
    if err != nil {
        return "", errors.Wrap(err, "loading environment variables")
    }

    // update function config
    log.Debug("updating function")
    if err := p.isPending(c); err != nil {
        return "", err
    }
    _, err = c.UpdateFunctionConfiguration(&lambda.UpdateFunctionConfigurationInput{
        FunctionName: &p.config.Name,
        Handler:      &p.handler,
        Runtime:      &p.config.Lambda.Runtime,
        Role:         &p.config.Lambda.Role,
        MemorySize:   aws.Int64(int64(p.config.Lambda.Memory)),
        Timeout:      aws.Int64(int64(p.config.Lambda.Timeout)),
        Environment:  env,
        VpcConfig:    p.vpc(),
    })

    if err != nil {
        return "", errors.Wrap(err, "updating function config")
    }

    // update function code
    log.Debug("updating function code")
    if err := p.isPending(c); err != nil {
        return "", err
    }
    res, err := c.UpdateFunctionCode(&lambda.UpdateFunctionCodeInput{
        FunctionName: &p.config.Name,
        Publish:      aws.Bool(true),
        S3Bucket:     b,
        S3Key:        k,
    })

    if err != nil {
        return "", errors.Wrap(err, "updating function code")
    }

    // create stage alias
    if err := p.alias(c, d.Stage, *res.Version); err != nil {
        return "", errors.Wrapf(err, "creating function stage %q alias", d.Stage)
    }

    // create git alias
    if d.Commit != "" {
        if err := p.alias(c, util.EncodeAlias(d.Commit), *res.Version); err != nil {
            return "", errors.Wrapf(err, "creating function git %q alias", d.Commit)
        }
    }

    return *res.Version, nil
}

// isPending implementation.
func (p *Platform) isPending(c *lambda.Lambda) error {
    var attempt int
    maxAttempts := 30       // TODO: ideally max attempts is configurable
    wait := time.Second * 5 // TODO: ideally some backoff

retry:
    attempt++

    log.Debugf("checking if function is pending (attempt %d of %d)", attempt, maxAttempts)
    conf, err := c.GetFunctionConfiguration(&lambda.GetFunctionConfigurationInput{
        FunctionName: &p.config.Name,
    })
    if err != nil {
        return errors.Wrapf(err, "getting function config")
    }

    if *conf.State == "Active" && *conf.LastUpdateStatus != "InProgress" {
        log.Debugf("function is in state %q / %q", *conf.State, *conf.LastUpdateStatus)
        return nil
    }

    if attempt >= maxAttempts {
        log.Debugf("max attempts exceeded")
        return errors.Errorf("function is stuck in the state %q / %q", *conf.State, *conf.LastUpdateStatus)
    }

    log.Debugf("function is in state %q / %q, trying again in %s", *conf.State, *conf.LastUpdateStatus, wait)
    time.Sleep(wait)
    goto retry
}

// vpc returns the vpc configuration or nil.
func (p *Platform) vpc() *lambda.VpcConfig {
    v := p.config.Lambda.VPC
    if v == nil {
        return nil
    }

    return &lambda.VpcConfig{
        SubnetIds:        aws.StringSlice(v.Subnets),
        SecurityGroupIds: aws.StringSlice(v.SecurityGroups),
    }
}

// alias creates or updates an alias.
func (p *Platform) alias(c *lambda.Lambda, alias, version string) error {
    log.Debugf("alias %s to %s", alias, version)
    _, err := c.UpdateAlias(&lambda.UpdateAliasInput{
        FunctionName:    &p.config.Name,
        FunctionVersion: &version,
        Name:            &alias,
        Description:     aws.String(util.ManagedByUp("")),
    })

    if util.IsNotFound(err) {
        _, err = c.CreateAlias(&lambda.CreateAliasInput{
            FunctionName:    &p.config.Name,
            FunctionVersion: &version,
            Name:            &alias,
            Description:     aws.String(util.ManagedByUp("")),
        })
    }

    return err
}

// deleteFunction deletes the lambda function.
func (p *Platform) deleteFunction(region string) error {
    // TODO: sessions all over... refactor
    c := lambda.New(session.New(aws.NewConfig().WithRegion(region)))

    _, err := c.DeleteFunction(&lambda.DeleteFunctionInput{
        FunctionName: &p.config.Name,
    })

    return err
}

// loadEnvironment loads environment variables.
func (p *Platform) loadEnvironment(d up.Deploy) (*lambda.Environment, error) {
    m := aws.StringMap(p.config.Environment)
    m["UP_STAGE"] = &d.Stage
    m["UP_COMMIT"] = &d.Commit
    m["UP_AUTHOR"] = &d.Author
    return &lambda.Environment{
        Variables: m,
    }, nil
}

// createRole creates the IAM role unless it is present.
func (p *Platform) createRole() error {
    s := session.New(aws.NewConfig())
    c := iam.New(s)

    name := p.roleName()
    desc := util.ManagedByUp("")

    // role is provided
    if s := p.config.Lambda.Role; s != "" {
        log.Debugf("using role from config %s", s)
        return nil
    }

    log.Debug("checking for role")
    existing, err := c.GetRole(&iam.GetRoleInput{
        RoleName: &name,
    })

    // network or permission error
    if err != nil && !util.IsNotFound(err) {
        return errors.Wrap(err, "fetching role")
    }

    // use the existing role
    if err == nil {
        log.Debug("found existing role")

        if err := p.updateRole(c); err != nil {
            return errors.Wrap(err, "updating role policy")
        }

        p.setRoleARN(*existing.Role.Arn)
        return nil
    }

    log.Debug("creating role")
    role, err := c.CreateRole(&iam.CreateRoleInput{
        RoleName:                 &name,
        Description:              &desc,
        AssumeRolePolicyDocument: &apiGatewayAssumePolicy,
    })

    if err != nil {
        return errors.Wrap(err, "creating role")
    }

    if err := p.updateRole(c); err != nil {
        return errors.Wrap(err, "updating role policy")
    }

    p.setRoleARN(*role.Role.Arn)

    return nil
}

// updateRole updates the IAM role.
func (p *Platform) updateRole(c *iam.IAM) error {
    name := p.roleName()

    policy, err := p.functionPolicy()
    if err != nil {
        return errors.Wrap(err, "creating function policy")
    }

    log.Debug("updating role policy")
    _, err = c.PutRolePolicy(&iam.PutRolePolicyInput{
        PolicyName:     &name,
        RoleName:       &name,
        PolicyDocument: &policy,
    })

    return err
}

// setRoleARN sets the role ARN.
func (p *Platform) setRoleARN(arn string) {
    log.Debugf("set role to %s", arn)
    p.config.Lambda.Role = arn
}

// roleName returns the IAM role name.
func (p *Platform) roleName() string {
    return fmt.Sprintf("%s-function", p.config.Name)
}

// deleteRole deletes the role and policy.
func (p *Platform) deleteRole(region string) error {
    name := fmt.Sprintf("%s-function", p.config.Name)
    c := iam.New(session.New(aws.NewConfig().WithRegion(region)))

    // role is provided
    if s := p.config.Lambda.Role; s != "" {
        log.Debugf("using role from config %s; not deleting", s)
        return nil
    }

    _, err := c.DeleteRolePolicy(&iam.DeleteRolePolicyInput{
        RoleName:   &name,
        PolicyName: &name,
    })

    if err != nil {
        return errors.Wrap(err, "deleting policy")
    }

    _, err = c.DeleteRole(&iam.DeleteRoleInput{
        RoleName: &name,
    })

    if err != nil {
        return errors.Wrap(err, "deleting iam role")
    }

    return nil
}

// createBucket creates the bucket.
func (p *Platform) createBucket(region string) error {
    s := s3.New(session.New(aws.NewConfig().WithRegion(region)))
    n := p.getS3BucketName(region)

    log.WithField("name", n).Debug("creating s3 bucket")
    _, err := s.CreateBucket(&s3.CreateBucketInput{
        Bucket: &n,
    })

    return err
}

// deleteBucketObjects deletes the objects for the app.
func (p *Platform) deleteBucketObjects(region string) error {
    s := s3.New(session.New(aws.NewConfig().WithRegion(region)))
    b := aws.String(p.getS3BucketName(region))
    prefix := p.config.Name + "/"

    params := &s3.ListObjectsInput{
        Bucket: b,
        Prefix: &prefix,
    }

    return s.ListObjectsPages(params, func(page *s3.ListObjectsOutput, lastPage bool) bool {
        for _, c := range page.Contents {
            ctx := log.WithField("key", *c.Key)

            ctx.Debug("deleting object")
            _, err := s.DeleteObject(&s3.DeleteObjectInput{
                Bucket: b,
                Key:    c.Key,
            })

            if err != nil {
                ctx.WithError(err).Warn("deleting object")
            }
        }

        return *page.IsTruncated
    })
}

// getAPI returns the API if present or nil.
func (p *Platform) getAPI(c *apigateway.APIGateway) (api *apigateway.RestApi, err error) {
    name := p.config.Name

    res, err := c.GetRestApis(&apigateway.GetRestApisInput{
        Limit: aws.Int64(500),
    })

    if err != nil {
        return nil, errors.Wrap(err, "fetching apis")
    }

    for _, a := range res.Items {
        if *a.Name == name {
            api = a
        }
    }

    return
}

// injectProxy injects the Go proxy.
func (p *Platform) injectProxy() error {
    log.Debugf("injecting proxy")

    if err := ioutil.WriteFile("main", bin.MustAsset("up-proxy"), 0777); err != nil {
        return errors.Wrap(err, "writing up-proxy")
    }

    if err := ioutil.WriteFile("_proxy.js", shim.MustAsset("index.js"), 0755); err != nil {
        return errors.Wrap(err, "writing _proxy.js")
    }

    return nil
}

// removeProxy removes the Go proxy.
func (p *Platform) removeProxy() error {
    log.Debugf("removing proxy")
    os.Remove("main")
    os.Remove("_proxy.js")
    return nil
}

// getS3Key returns a randomized s3 key.
func (p *Platform) getS3Key(stage string) string {
    ts := time.Now().Unix()
    uid := uniuri.New()
    return fmt.Sprintf("%s/%s/%d-%s.zip", p.config.Name, stage, ts, uid)
}

// getS3BucketName returns the s3 bucket name.
func (p *Platform) getS3BucketName(region string) string {
    return fmt.Sprintf("up-%s-%s", p.getAccountID(), region)
}

// getAccountID returns the AWS account id derived from Lambda role,
// which is currently always present, implicitly or explicitly.
func (p *Platform) getAccountID() string {
    return strings.Split(p.config.Lambda.Role, ":")[4]
}

// functionPolicy returns the IAM function role policy.
func (p *Platform) functionPolicy() (string, error) {
    policy := struct {
        Version   string
        Statement []config.IAMPolicyStatement
    }{
        Version:   "2012-10-17",
        Statement: p.config.Lambda.Policy,
    }

    b, err := json.MarshalIndent(policy, "", "  ")
    if err != nil {
        return "", err
    }

    return string(b), nil
}

// isCreatingRole returns true if the role has not been created.
func isCreatingRole(err error) bool {
    return err != nil && strings.Contains(err.Error(), "role defined for the function cannot be assumed by Lambda")
}

// getCerts returns the certificates available.
func getCerts(a *acm.ACM) (certs []*acm.CertificateDetail, err error) {
    var g errgroup.Group
    var mu sync.Mutex

    res, err := a.ListCertificates(&acm.ListCertificatesInput{
        MaxItems: aws.Int64(1000),
    })

    if err != nil {
        return nil, errors.Wrap(err, "listing")
    }

    for _, c := range res.CertificateSummaryList {
        c := c
        g.Go(func() error {
            res, err := a.DescribeCertificate(&acm.DescribeCertificateInput{
                CertificateArn: c.CertificateArn,
            })

            if err != nil {
                return errors.Wrap(err, "describing")
            }

            mu.Lock()
            certs = append(certs, res.Certificate)
            mu.Unlock()
            return nil
        })
    }

    err = g.Wait()
    return
}

// getCert returns the ARN of a certificate with can satisfy domain,
// favoring more specific certificates, then falling back on wildcards.
func getCert(certs []*acm.CertificateDetail, domain string) string {
    // exact domain
    for _, c := range certs {
        if *c.DomainName == domain {
            return *c.CertificateArn
        }
    }

    // exact alt
    for _, c := range certs {
        for _, a := range c.SubjectAlternativeNames {
            if *a == domain {
                return *c.CertificateArn
            }
        }
    }

    // wildcards
    for _, c := range certs {
        if util.WildcardMatches(*c.DomainName, domain) {
            return *c.CertificateArn
        }

        for _, a := range c.SubjectAlternativeNames {
            if util.WildcardMatches(*a, domain) {
                return *c.CertificateArn
            }
        }
    }

    return ""
}