platform/lambda/lambda.go
// 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 ""
}