├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── handler ├── testdata │ ├── spa │ │ ├── app.js │ │ ├── index.html │ │ ├── css │ │ │ ├── bar.css │ │ │ └── foo.css │ │ └── up.json │ ├── static │ │ ├── index.html │ │ ├── style.css │ │ └── up.json │ ├── static-redirects │ │ ├── index.html │ │ ├── help │ │ │ └── ping │ │ │ │ └── alerts │ │ │ │ └── index.html │ │ └── up.json │ ├── static-rewrites │ │ ├── index.html │ │ ├── help │ │ │ └── ping │ │ │ │ └── alerts.html │ │ └── up.json │ ├── node-pkg │ │ ├── package.json │ │ ├── up.json │ │ └── app.js │ ├── node │ │ ├── up.json │ │ └── app.js │ └── node-pkg-start │ │ ├── up.json │ │ ├── package.json │ │ └── index.js └── handler.go ├── internal ├── errorpage │ ├── testdata │ │ ├── other.html │ │ ├── somedir │ │ │ └── test.html │ │ ├── 200.html │ │ ├── 404.html │ │ ├── 4xx.html │ │ ├── 500.html │ │ └── error.html │ ├── errorpage_test.go │ ├── template.go │ └── errorpage.go ├── zip │ ├── testdata │ │ ├── .file │ │ ├── bar.js │ │ ├── foo.js │ │ ├── .upignore │ │ ├── Readme.md │ │ └── index.js │ ├── zip_test.go │ └── zip.go ├── shim │ └── shim.go ├── proxy │ ├── bin │ │ └── bin.go │ ├── lambda.go │ ├── request.go │ ├── event.go │ ├── request_test.go │ ├── response_test.go │ └── response.go ├── cli │ ├── app │ │ └── app.go │ ├── version │ │ └── version.go │ ├── disable-stats │ │ └── disable-stats.go │ ├── docs │ │ └── docs.go │ ├── config │ │ └── config.go │ ├── run │ │ └── run.go │ ├── prune │ │ └── prune.go │ ├── metrics │ │ └── metrics.go │ ├── url │ │ └── url.go │ ├── build │ │ └── build.go │ ├── start │ │ └── start.go │ ├── root │ │ └── root.go │ └── logs │ │ └── logs.go ├── logs │ ├── text │ │ └── text_test.go │ ├── logs.go │ ├── writer │ │ ├── writer.go │ │ └── writer_test.go │ └── parser │ │ └── parser.go ├── signal │ └── signal.go ├── progressreader │ └── progressreader.go ├── stats │ └── stats.go ├── colors │ └── colors.go ├── userconfig │ └── userconfig_test.go ├── header │ ├── header.go │ └── header_test.go ├── account │ └── cards.go ├── metrics │ └── metrics.go ├── validate │ └── validate.go ├── setup │ └── setup.go └── redirect │ └── redirect.go ├── http ├── inject │ ├── testdata │ │ ├── style.css │ │ ├── 404.html │ │ ├── up.json │ │ └── index.html │ ├── inject.go │ └── inject_test.go ├── headers │ ├── testdata │ │ ├── index.html │ │ ├── style.css │ │ ├── _headers │ │ └── up.json │ ├── headers_test.go │ └── headers.go ├── logs │ ├── testdata │ │ ├── index.html │ │ └── up.json │ ├── logs_test.go │ └── logs.go ├── robots │ ├── testdata │ │ ├── index.html │ │ └── up.json │ ├── robots.go │ └── robots_test.go ├── poweredby │ ├── testdata │ │ ├── index.html │ │ └── up.json │ ├── poweredby.go │ └── poweredby_test.go ├── static │ ├── testdata │ │ ├── static │ │ │ ├── index.html │ │ │ ├── up.json │ │ │ └── style.css │ │ └── dynamic │ │ │ ├── up.json │ │ │ ├── public │ │ │ └── css │ │ │ │ └── style.css │ │ │ └── app.js │ └── static.go ├── errorpages │ ├── testdata │ │ ├── defaults │ │ │ ├── index.html │ │ │ └── up.json │ │ └── templates │ │ │ ├── index.html │ │ │ ├── 404.html │ │ │ ├── up.json │ │ │ └── 5xx.html │ └── errorpages.go ├── relay │ └── testdata │ │ ├── basic │ │ ├── up.json │ │ └── app.js │ │ └── node │ │ ├── up.json │ │ ├── package.json │ │ └── server.js ├── gzip │ ├── gzip.go │ └── gzip_test.go ├── cors │ └── cors.go └── redirects │ └── redirects.go ├── assets ├── title.png ├── pricing.png ├── screen2.png ├── features-pro.png └── features-community.png ├── .gitattributes ├── config ├── environment.go ├── doc.go ├── errorpages_test.go ├── errorpages.go ├── static_test.go ├── logs.go ├── backoff_test.go ├── static.go ├── duration_test.go ├── duration.go ├── lambda_test.go ├── backoff.go ├── relay.go ├── hooks_test.go ├── cors.go ├── hooks.go ├── lambda.go ├── dns.go ├── stages.go └── dns_test.go ├── .gitignore ├── reporter ├── discard │ └── discard.go ├── reporter.go └── plain │ └── plain.go ├── .goreleaser.yml ├── platform ├── aws │ ├── regions │ │ ├── regions_test.go │ │ └── regions.go │ ├── runtime │ │ └── runtime.go │ ├── cost │ │ ├── cost_test.go │ │ └── cost.go │ ├── domains │ │ └── domains.go │ └── logs │ │ └── logs.go ├── lambda │ ├── reporter │ │ └── reporter.go │ ├── stack │ │ ├── status_test.go │ │ ├── stack_test.go │ │ └── status.go │ ├── prune.go │ ├── lambda_test.go │ └── metrics.go └── event │ └── event.go ├── docs ├── 00-introduction.md ├── 10-links.md ├── 01-installation.md ├── 05-runtimes.md ├── 08-troubleshooting.md ├── 03-getting-started.md └── 02-aws-credentials.md ├── LICENSE ├── Makefile ├── cmd ├── up-proxy │ └── main.go └── up │ └── main.go ├── CONTRIBUTING.md ├── Readme.md ├── CODE_OF_CONDUCT.md └── platform.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: tj -------------------------------------------------------------------------------- /handler/testdata/spa/app.js: -------------------------------------------------------------------------------- 1 | app js 2 | -------------------------------------------------------------------------------- /internal/errorpage/testdata/other.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/zip/testdata/.file: -------------------------------------------------------------------------------- 1 | 👻 2 | -------------------------------------------------------------------------------- /internal/zip/testdata/bar.js: -------------------------------------------------------------------------------- 1 | bar 2 | -------------------------------------------------------------------------------- /internal/zip/testdata/foo.js: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /handler/testdata/spa/index.html: -------------------------------------------------------------------------------- 1 | Index 2 | -------------------------------------------------------------------------------- /http/inject/testdata/style.css: -------------------------------------------------------------------------------- 1 | body{} 2 | -------------------------------------------------------------------------------- /internal/zip/testdata/.upignore: -------------------------------------------------------------------------------- 1 | *.md 2 | -------------------------------------------------------------------------------- /handler/testdata/spa/css/bar.css: -------------------------------------------------------------------------------- 1 | bar css 2 | -------------------------------------------------------------------------------- /handler/testdata/spa/css/foo.css: -------------------------------------------------------------------------------- 1 | foo css 2 | -------------------------------------------------------------------------------- /http/headers/testdata/index.html: -------------------------------------------------------------------------------- 1 | Index HTML 2 | -------------------------------------------------------------------------------- /http/logs/testdata/index.html: -------------------------------------------------------------------------------- 1 | Index HTML 2 | -------------------------------------------------------------------------------- /http/robots/testdata/index.html: -------------------------------------------------------------------------------- 1 | Index HTML 2 | -------------------------------------------------------------------------------- /internal/errorpage/testdata/somedir/test.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/zip/testdata/Readme.md: -------------------------------------------------------------------------------- 1 | Hello World 2 | -------------------------------------------------------------------------------- /handler/testdata/static/index.html: -------------------------------------------------------------------------------- 1 | Hello World 2 | -------------------------------------------------------------------------------- /http/inject/testdata/404.html: -------------------------------------------------------------------------------- 1 |
Not Found
2 | -------------------------------------------------------------------------------- /http/poweredby/testdata/index.html: -------------------------------------------------------------------------------- 1 | Index HTML 2 | -------------------------------------------------------------------------------- /http/static/testdata/static/index.html: -------------------------------------------------------------------------------- 1 | Index HTML 2 | -------------------------------------------------------------------------------- /internal/errorpage/testdata/200.html: -------------------------------------------------------------------------------- 1 | 200 page. 2 | -------------------------------------------------------------------------------- /internal/errorpage/testdata/404.html: -------------------------------------------------------------------------------- 1 | 404 page. 2 | -------------------------------------------------------------------------------- /internal/errorpage/testdata/4xx.html: -------------------------------------------------------------------------------- 1 | 4xx page. 2 | -------------------------------------------------------------------------------- /internal/errorpage/testdata/500.html: -------------------------------------------------------------------------------- 1 | 500 page. 2 | -------------------------------------------------------------------------------- /http/headers/testdata/style.css: -------------------------------------------------------------------------------- 1 | body { color: red } 2 | -------------------------------------------------------------------------------- /http/logs/testdata/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api" 3 | } 4 | -------------------------------------------------------------------------------- /handler/testdata/static-redirects/index.html: -------------------------------------------------------------------------------- 1 | Hello World 2 | -------------------------------------------------------------------------------- /handler/testdata/static-rewrites/index.html: -------------------------------------------------------------------------------- 1 | Hello World 2 | -------------------------------------------------------------------------------- /http/errorpages/testdata/defaults/index.html: -------------------------------------------------------------------------------- 1 | Index HTML 2 | -------------------------------------------------------------------------------- /http/errorpages/testdata/templates/index.html: -------------------------------------------------------------------------------- 1 | Index HTML 2 | -------------------------------------------------------------------------------- /http/headers/testdata/_headers: -------------------------------------------------------------------------------- 1 | /*.css 2 | X-Type: css 3 | -------------------------------------------------------------------------------- /http/headers/testdata/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /http/inject/testdata/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /http/poweredby/testdata/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /http/robots/testdata/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /handler/testdata/static/style.css: -------------------------------------------------------------------------------- 1 | body { background: whatever } 2 | -------------------------------------------------------------------------------- /http/relay/testdata/basic/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /http/relay/testdata/node/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /http/static/testdata/dynamic/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /http/static/testdata/static/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /internal/zip/testdata/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 'hello world' 2 | -------------------------------------------------------------------------------- /handler/testdata/static-rewrites/help/ping/alerts.html: -------------------------------------------------------------------------------- 1 | Alerting docs 2 | -------------------------------------------------------------------------------- /http/errorpages/testdata/defaults/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /http/errorpages/testdata/templates/404.html: -------------------------------------------------------------------------------- 1 | Sorry! Can't find that. 2 | -------------------------------------------------------------------------------- /http/static/testdata/static/style.css: -------------------------------------------------------------------------------- 1 | body { background: whatever } 2 | -------------------------------------------------------------------------------- /assets/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apex/up/HEAD/assets/title.png -------------------------------------------------------------------------------- /handler/testdata/node-pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "something" 3 | } 4 | -------------------------------------------------------------------------------- /http/errorpages/testdata/templates/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /internal/errorpage/testdata/error.html: -------------------------------------------------------------------------------- 1 | {{.StatusText}} - {{.StatusCode}}. 2 | -------------------------------------------------------------------------------- /assets/pricing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apex/up/HEAD/assets/pricing.png -------------------------------------------------------------------------------- /assets/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apex/up/HEAD/assets/screen2.png -------------------------------------------------------------------------------- /handler/testdata/static-redirects/help/ping/alerts/index.html: -------------------------------------------------------------------------------- 1 | Alerting docs 2 | -------------------------------------------------------------------------------- /http/errorpages/testdata/templates/5xx.html: -------------------------------------------------------------------------------- 1 | {{.StatusCode}} – {{.StatusText}} 2 | -------------------------------------------------------------------------------- /http/static/testdata/dynamic/public/css/style.css: -------------------------------------------------------------------------------- 1 | body { background: whatever } 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | internal/proxy/bin/bin_assets.go filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /assets/features-pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apex/up/HEAD/assets/features-pro.png -------------------------------------------------------------------------------- /assets/features-community.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apex/up/HEAD/assets/features-community.png -------------------------------------------------------------------------------- /http/relay/testdata/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "node server" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /handler/testdata/node/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "logs": { 4 | "enable": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /config/environment.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Environment variables. 4 | type Environment map[string]string 5 | -------------------------------------------------------------------------------- /handler/testdata/node-pkg/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "logs": { 4 | "enable": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /handler/testdata/node-pkg-start/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "logs": { 4 | "enable": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /handler/testdata/static/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "type": "static", 4 | "logs": { 5 | "enable": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /config/doc.go: -------------------------------------------------------------------------------- 1 | // Package config provides configuration structures, 2 | // validation, and defaulting for up.json config. 3 | package config 4 | -------------------------------------------------------------------------------- /handler/testdata/node-pkg-start/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "something", 3 | "scripts": { 4 | "start": "node index" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /http/inject/testdata/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /internal/shim/shim.go: -------------------------------------------------------------------------------- 1 | //go:generate go-bindata -modtime 0 -pkg shim . 2 | 3 | // Package shim provides a shim for running arbitrary languages on Lambda. 4 | package shim 5 | -------------------------------------------------------------------------------- /handler/testdata/node-pkg/app.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const { PORT } = process.env 3 | 4 | http.createServer((req, res) => { 5 | res.end('Hello World') 6 | }).listen(PORT) 7 | -------------------------------------------------------------------------------- /handler/testdata/node-pkg-start/index.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const { PORT } = process.env 3 | 4 | http.createServer((req, res) => { 5 | res.end('Hello World') 6 | }).listen(PORT) 7 | -------------------------------------------------------------------------------- /http/relay/testdata/node/server.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const port = parseInt(process.env.PORT, 10) 3 | 4 | http.createServer((req, res) => { 5 | res.end('Node') 6 | }).listen(port) 7 | -------------------------------------------------------------------------------- /internal/proxy/bin/bin.go: -------------------------------------------------------------------------------- 1 | //go:generate sh -c "GOOS=linux GOARCH=amd64 go build -o up-proxy ../../../cmd/up-proxy/main.go" 2 | //go:generate go-bindata -modtime 0 -pkg bin -o bin_assets.go . 3 | 4 | package bin 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | node_modules/ 3 | .shards/ 4 | lib 5 | vendor/ 6 | testing 7 | up-proxy 8 | !cmd/up-proxy 9 | dist 10 | .idea 11 | .vscode 12 | .DS_Store 13 | internal/proxy/bin/bin_assets.go 14 | internal/shim/bindata.go 15 | -------------------------------------------------------------------------------- /handler/testdata/spa/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "type": "static", 4 | "logs": { 5 | "enable": false 6 | }, 7 | "redirects": { 8 | "/*": { 9 | "location": "/", 10 | "status": 200 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /config/errorpages_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tj/assert" 7 | ) 8 | 9 | func TestErrorPages(t *testing.T) { 10 | c := &ErrorPages{} 11 | assert.NoError(t, c.Default(), "default") 12 | assert.Equal(t, ".", c.Dir, "dir") 13 | } 14 | -------------------------------------------------------------------------------- /http/static/testdata/dynamic/app.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const port = process.env.PORT 3 | 4 | http.createServer((req, res) => { 5 | res.setHeader('X-Foo', 'bar') 6 | res.setHeader('Content-Type', 'text/plain; charset=utf-8') 7 | res.end('Hello World') 8 | }).listen(port) 9 | -------------------------------------------------------------------------------- /reporter/discard/discard.go: -------------------------------------------------------------------------------- 1 | // Package discard provides a reporter for discarding events. 2 | package discard 3 | 4 | import "github.com/apex/up/platform/event" 5 | 6 | // Report events. 7 | func Report(events <-chan *event.Event) { 8 | for range events { 9 | // :) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /handler/testdata/static-redirects/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "type": "static", 4 | "logs": { 5 | "enable": false 6 | }, 7 | "redirects": { 8 | "/docs/:product/guides/:guide": { 9 | "location": "/help/:product/:guide", 10 | "status": 302 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /handler/testdata/static-rewrites/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "type": "static", 4 | "logs": { 5 | "enable": false 6 | }, 7 | "redirects": { 8 | "/docs/:product/guides/:guide": { 9 | "location": "/help/:product/:guide.html", 10 | "status": 200 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /handler/testdata/node/app.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const port = process.env.PORT 3 | 4 | http.createServer((req, res) => { 5 | res.setHeader('X-Foo', 'bar') 6 | res.setHeader('Content-Type', 'text/plain; charset=utf-8') 7 | res.end('Hello World') 8 | }).listen(port, '127.0.0.1', _ => { 9 | console.log('listening') 10 | }) 11 | -------------------------------------------------------------------------------- /http/gzip/gzip.go: -------------------------------------------------------------------------------- 1 | // Package gzip provides gzip compression support. 2 | package gzip 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/NYTimes/gziphandler" 8 | 9 | "github.com/apex/up" 10 | ) 11 | 12 | // New gzip handler. 13 | func New(c *up.Config, next http.Handler) http.Handler { 14 | return gziphandler.GzipHandler(next) 15 | } 16 | -------------------------------------------------------------------------------- /reporter/reporter.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "github.com/apex/up/reporter/discard" 5 | "github.com/apex/up/reporter/plain" 6 | "github.com/apex/up/reporter/text" 7 | ) 8 | 9 | var ( 10 | // Discard reporter. 11 | Discard = discard.Report 12 | 13 | // Plain reporter. 14 | Plain = plain.Report 15 | 16 | // Text reporter. 17 | Text = text.Report 18 | ) 19 | -------------------------------------------------------------------------------- /http/poweredby/poweredby.go: -------------------------------------------------------------------------------- 1 | // Package poweredby provides nothing :). 2 | package poweredby 3 | 4 | import ( 5 | "net/http" 6 | ) 7 | 8 | // New powered-by middleware. 9 | func New(name string, next http.Handler) http.Handler { 10 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 11 | w.Header().Set("X-Powered-By", name) 12 | next.ServeHTTP(w, r) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | build: 2 | main: cmd/up/main.go 3 | binary: up 4 | goos: 5 | - darwin 6 | - linux 7 | - windows 8 | - freebsd 9 | - netbsd 10 | - openbsd 11 | goarch: 12 | - amd64 13 | - 386 14 | ignore: 15 | - goos: darwin 16 | goarch: 386 17 | changelog: 18 | sort: asc 19 | filters: 20 | exclude: 21 | - '^docs:' 22 | - '^refactor' 23 | -------------------------------------------------------------------------------- /internal/cli/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/apex/up/internal/cli/root" 8 | 9 | "github.com/apex/up/internal/stats" 10 | ) 11 | 12 | // Run the command. 13 | func Run(version string) error { 14 | defer stats.Client.ConditionalFlush(50, 6*time.Hour) 15 | root.Cmd.Version(version) 16 | _, err := root.Cmd.Parse(os.Args[1:]) 17 | return err 18 | } 19 | -------------------------------------------------------------------------------- /internal/cli/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tj/kingpin" 7 | 8 | "github.com/apex/up/internal/cli/root" 9 | "github.com/apex/up/internal/stats" 10 | ) 11 | 12 | func init() { 13 | cmd := root.Command("version", "Show version.") 14 | cmd.Action(func(_ *kingpin.ParseContext) error { 15 | stats.Track("Show Version", nil) 16 | fmt.Println(root.Cmd.GetVersion()) 17 | return nil 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | * [ ] I am running the latest version. (`up upgrade`) 4 | * [ ] I searched to see if the issue already exists. 5 | * [ ] I inspected the verbose debug output with the `-v, --verbose` flag. 6 | * [ ] Are you an Up Pro subscriber? 7 | 8 | ## Description 9 | 10 | Describe the bug or feature. 11 | 12 | ## Steps to Reproduce 13 | 14 | Describe the steps required to reproduce the issue if applicable. 15 | 16 | ## Slack 17 | 18 | Join us on Slack https://chat.apex.sh/ 19 | -------------------------------------------------------------------------------- /config/errorpages.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // ErrorPages configuration. 4 | type ErrorPages struct { 5 | // Enable error pages. 6 | Enable bool `json:"enable"` 7 | 8 | // Dir containing error pages. 9 | Dir string `json:"dir"` 10 | 11 | // Variables are passed to the template for use. 12 | Variables map[string]interface{} `json:"variables"` 13 | } 14 | 15 | // Default implementation. 16 | func (e *ErrorPages) Default() error { 17 | if e.Dir == "" { 18 | e.Dir = "." 19 | } 20 | 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /config/static_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | ) 9 | 10 | func TestStatic(t *testing.T) { 11 | cwd, _ := os.Getwd() 12 | 13 | table := []struct { 14 | Static 15 | valid bool 16 | }{ 17 | {Static{Dir: cwd}, true}, 18 | {Static{Dir: cwd + "/static_test.go"}, false}, 19 | } 20 | 21 | for _, row := range table { 22 | if row.valid { 23 | assert.NoError(t, row.Validate()) 24 | } else { 25 | assert.Error(t, row.Validate()) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /config/logs.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Logs configuration. 4 | type Logs struct { 5 | // Disable json log output. 6 | Disable bool `json:"disable"` 7 | 8 | // Stdout default log level. 9 | Stdout string `json:"stdout"` 10 | 11 | // Stderr default log level. 12 | Stderr string `json:"stderr"` 13 | } 14 | 15 | // Default implementation. 16 | func (l *Logs) Default() error { 17 | if l.Stdout == "" { 18 | l.Stdout = "info" 19 | } 20 | 21 | if l.Stderr == "" { 22 | l.Stderr = "error" 23 | } 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /http/robots/robots.go: -------------------------------------------------------------------------------- 1 | // Package robots provides a way of dealing with robots exclusion protocol 2 | package robots 3 | 4 | import ( 5 | "net/http" 6 | "os" 7 | 8 | "github.com/apex/up" 9 | ) 10 | 11 | // New robots middleware. 12 | func New(c *up.Config, next http.Handler) http.Handler { 13 | if os.Getenv("UP_STAGE") == "production" { 14 | return next 15 | } 16 | 17 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | w.Header().Set("X-Robots-Tag", "none") 19 | next.ServeHTTP(w, r) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /internal/cli/disable-stats/disable-stats.go: -------------------------------------------------------------------------------- 1 | package disablestats 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/tj/kingpin" 6 | 7 | "github.com/apex/up/internal/cli/root" 8 | "github.com/apex/up/internal/stats" 9 | ) 10 | 11 | func init() { 12 | cmd := root.Command("disable-stats", "Disable anonymized usage stats").Hidden() 13 | cmd.Action(func(_ *kingpin.ParseContext) error { 14 | err := stats.Client.Disable() 15 | if err != nil { 16 | return errors.Wrap(err, "disabling") 17 | } 18 | return nil 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Open an issue and discuss changes before spending time on them, unless the change is trivial or an issue already exists. 2 | 3 | Use "VERB some thing here. Closes #n" to close the relevant issue, where VERB is one of: 4 | 5 | - add 6 | - remove 7 | - change 8 | - refactor 9 | 10 | If the change is documentation related prefix with "docs: ", as these are filtered from the changelog. 11 | 12 | docs: add ~/.aws/config 13 | 14 | Run `dep ensure` if you introduce any new `import`'s so they're included in the ./vendor dir. 15 | -------------------------------------------------------------------------------- /internal/cli/docs/docs.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import ( 4 | "github.com/pkg/browser" 5 | "github.com/tj/kingpin" 6 | 7 | "github.com/apex/up/internal/cli/root" 8 | "github.com/apex/up/internal/stats" 9 | ) 10 | 11 | var url = "https://up.docs.apex.sh" 12 | 13 | func init() { 14 | cmd := root.Command("docs", "Open documentation website in the browser.") 15 | cmd.Example(`up docs`, "Open the documentation site.") 16 | 17 | cmd.Action(func(_ *kingpin.ParseContext) error { 18 | stats.Track("Open Docs", nil) 19 | return browser.OpenURL(url) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /internal/logs/text/text_test.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/apex/log" 11 | ) 12 | 13 | func init() { 14 | log.Now = func() time.Time { 15 | return time.Unix(0, 0) 16 | } 17 | } 18 | 19 | func Test(t *testing.T) { 20 | var buf bytes.Buffer 21 | 22 | log.SetHandler(New(&buf)) 23 | log.WithField("user", "tj").WithField("id", "123").Info("hello") 24 | log.WithField("user", "tj").Info("something broke") 25 | log.WithField("user", "tj").Warn("something kind of broke") 26 | log.WithField("user", "tj").Error("boom") 27 | 28 | io.Copy(os.Stdout, &buf) 29 | } 30 | -------------------------------------------------------------------------------- /config/backoff_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/tj/assert" 8 | ) 9 | 10 | func TestBackoff_Default(t *testing.T) { 11 | a := &Backoff{} 12 | assert.NoError(t, a.Default(), "default") 13 | 14 | b := &Backoff{ 15 | Min: 100, 16 | Max: 500, 17 | Factor: 2, 18 | Attempts: 3, 19 | } 20 | 21 | assert.Equal(t, b, a) 22 | } 23 | 24 | func TestBackoff_Backoff(t *testing.T) { 25 | a := &Backoff{} 26 | assert.NoError(t, a.Default(), "default") 27 | 28 | b := a.Backoff() 29 | assert.Equal(t, time.Millisecond*100, b.Min) 30 | assert.Equal(t, time.Millisecond*500, b.Max) 31 | } 32 | -------------------------------------------------------------------------------- /platform/aws/regions/regions_test.go: -------------------------------------------------------------------------------- 1 | package regions 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tj/assert" 7 | ) 8 | 9 | func TestMatch(t *testing.T) { 10 | t.Run("explicit", func(t *testing.T) { 11 | v := Match([]string{"us-west-2", "us-east-1"}) 12 | assert.Equal(t, []string{"us-west-2", "us-east-1"}, v) 13 | }) 14 | 15 | t.Run("glob all", func(t *testing.T) { 16 | v := Match([]string{"*"}) 17 | assert.Equal(t, IDs, v) 18 | }) 19 | 20 | t.Run("glob some", func(t *testing.T) { 21 | v := Match([]string{"us-west-*", "ca-*"}) 22 | e := []string{"us-west-1", "us-west-2", "ca-central-1"} 23 | assert.Equal(t, e, v) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /config/static.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // Static configuration. 10 | type Static struct { 11 | // Dir containing static files. 12 | Dir string `json:"dir"` 13 | 14 | // Prefix is an optional URL prefix for serving static files. 15 | Prefix string `json:"prefix"` 16 | } 17 | 18 | // Validate implementation. 19 | func (s *Static) Validate() error { 20 | info, err := os.Stat(s.Dir) 21 | 22 | if os.IsNotExist(err) { 23 | return nil 24 | } 25 | 26 | if err != nil { 27 | return errors.Wrap(err, ".dir") 28 | } 29 | 30 | if !info.IsDir() { 31 | return errors.Errorf(".dir %s is not a directory", s.Dir) 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/signal/signal.go: -------------------------------------------------------------------------------- 1 | package signal 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/apex/up/internal/util" 9 | ) 10 | 11 | // close funcs. 12 | var fns []Func 13 | 14 | // Init signals channel 15 | func init() { 16 | s := make(chan os.Signal, 1) 17 | go trap(s) 18 | signal.Notify(s, syscall.SIGINT) 19 | } 20 | 21 | // Func is a close function. 22 | type Func func() error 23 | 24 | // Add registers a close handler func. 25 | func Add(fn Func) { 26 | fns = append(fns, fn) 27 | } 28 | 29 | // trap signals to invoke callbacks and exit. 30 | func trap(ch chan os.Signal) { 31 | <-ch 32 | for _, fn := range fns { 33 | if err := fn(); err != nil { 34 | util.Fatal(err) 35 | } 36 | } 37 | os.Exit(1) 38 | } 39 | -------------------------------------------------------------------------------- /internal/cli/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/apex/up/internal/cli/root" 8 | "github.com/apex/up/internal/stats" 9 | "github.com/pkg/errors" 10 | "github.com/tj/kingpin" 11 | ) 12 | 13 | func init() { 14 | cmd := root.Command("config", "Show configuration after defaults and validation.") 15 | cmd.Example(`up config`, "Show the config.") 16 | 17 | cmd.Action(func(_ *kingpin.ParseContext) error { 18 | c, _, err := root.Init() 19 | if err != nil { 20 | return errors.Wrap(err, "initializing") 21 | } 22 | 23 | stats.Track("Show Config", nil) 24 | 25 | enc := json.NewEncoder(os.Stdout) 26 | enc.SetIndent("", " ") 27 | enc.Encode(c) 28 | 29 | return nil 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /internal/logs/logs.go: -------------------------------------------------------------------------------- 1 | // Package logs provides logging utilities. 2 | package logs 3 | 4 | import ( 5 | "os" 6 | 7 | "github.com/apex/log" 8 | ) 9 | 10 | // Fields returns the global log fields. 11 | func Fields() log.Fields { 12 | f := log.Fields{ 13 | "app": os.Getenv("AWS_LAMBDA_FUNCTION_NAME"), 14 | "region": os.Getenv("AWS_REGION"), 15 | "version": os.Getenv("AWS_LAMBDA_FUNCTION_VERSION"), 16 | "stage": os.Getenv("UP_STAGE"), 17 | } 18 | 19 | if s := os.Getenv("UP_COMMIT"); s != "" { 20 | f["commit"] = s 21 | } 22 | 23 | return f 24 | } 25 | 26 | // Plugin returns a log context for the given plugin name. 27 | func Plugin(name string) log.Interface { 28 | f := Fields() 29 | f["plugin"] = name 30 | return log.WithFields(f) 31 | } 32 | -------------------------------------------------------------------------------- /docs/00-introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | slug: introduction 4 | --- 5 | 6 | Up deploys infinitely scalable serverless apps, APIs, and static websites in seconds, so you can get back to working on what makes your product unique. 7 | 8 | Up focuses on deploying "vanilla" HTTP servers so there's nothing new to learn, just develop with your favorite existing frameworks such as Express, Koa, Django, Golang net/http or others. 9 | 10 | Up currently supports Node.js, Golang, Python, Java, Crystal, and static sites out of the box. Up is platform-agnostic, supporting AWS Lambda and API Gateway as the first targets — you can think of Up as self-hosted Heroku style user experience for a fraction of the price, with the security, flexibility, and scalability of AWS — just `$ up` and you're done! 11 | -------------------------------------------------------------------------------- /http/poweredby/poweredby_test.go: -------------------------------------------------------------------------------- 1 | package poweredby 2 | 3 | import ( 4 | "net/http/httptest" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | "github.com/apex/up" 9 | 10 | "github.com/apex/up/config" 11 | "github.com/apex/up/http/static" 12 | ) 13 | 14 | func TestPoweredby(t *testing.T) { 15 | c := &up.Config{ 16 | Static: config.Static{ 17 | Dir: "testdata", 18 | }, 19 | } 20 | 21 | h := New("up", static.New(c)) 22 | 23 | res := httptest.NewRecorder() 24 | req := httptest.NewRequest("GET", "/", nil) 25 | 26 | h.ServeHTTP(res, req) 27 | 28 | assert.Equal(t, 200, res.Code) 29 | assert.Equal(t, "up", res.Header().Get("X-Powered-By")) 30 | assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) 31 | assert.Equal(t, "Index HTML\n", res.Body.String()) 32 | } 33 | -------------------------------------------------------------------------------- /internal/proxy/lambda.go: -------------------------------------------------------------------------------- 1 | // Package proxy provides API Gateway and Lambda interoperability. 2 | package proxy 3 | 4 | import ( 5 | "encoding/json" 6 | "net/http" 7 | 8 | "github.com/apex/go-apex" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // NewHandler returns an apex.Handler. 13 | func NewHandler(h http.Handler) apex.Handler { 14 | return apex.HandlerFunc(func(event json.RawMessage, ctx *apex.Context) (interface{}, error) { 15 | e := new(Input) 16 | 17 | err := json.Unmarshal(event, e) 18 | if err != nil { 19 | return nil, errors.Wrap(err, "parsing proxy event") 20 | } 21 | 22 | req, err := NewRequest(e) 23 | if err != nil { 24 | return nil, errors.Wrap(err, "creating new request from event") 25 | } 26 | 27 | res := NewResponse() 28 | h.ServeHTTP(res, req) 29 | return res.End(), nil 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /http/cors/cors.go: -------------------------------------------------------------------------------- 1 | // Package cors provides CORS support. 2 | package cors 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/rs/cors" 8 | 9 | "github.com/apex/up" 10 | "github.com/apex/up/config" 11 | ) 12 | 13 | // New CORS handler. 14 | func New(c *up.Config, next http.Handler) http.Handler { 15 | if c.CORS == nil { 16 | return next 17 | } 18 | 19 | return cors.New(options(c.CORS)).Handler(next) 20 | } 21 | 22 | // options returns the canonical options. 23 | func options(c *config.CORS) cors.Options { 24 | return cors.Options{ 25 | AllowedOrigins: c.AllowedOrigins, 26 | AllowedMethods: c.AllowedMethods, 27 | AllowedHeaders: c.AllowedHeaders, 28 | ExposedHeaders: c.ExposedHeaders, 29 | AllowCredentials: c.AllowCredentials, 30 | MaxAge: c.MaxAge, 31 | Debug: c.Debug, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /config/duration_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/tj/assert" 9 | ) 10 | 11 | func TestDuration_UnmarshalJSON(t *testing.T) { 12 | t.Run("numeric seconds", func(t *testing.T) { 13 | s := `{ 14 | "timeout": 5 15 | }` 16 | 17 | var c struct { 18 | Timeout Duration 19 | } 20 | 21 | err := json.Unmarshal([]byte(s), &c) 22 | assert.NoError(t, err, "unmarshal") 23 | 24 | assert.Equal(t, Duration(5*time.Second), c.Timeout) 25 | }) 26 | 27 | t.Run("string duration", func(t *testing.T) { 28 | s := `{ 29 | "timeout": "1.5m" 30 | }` 31 | 32 | var c struct { 33 | Timeout Duration 34 | } 35 | 36 | err := json.Unmarshal([]byte(s), &c) 37 | assert.NoError(t, err, "unmarshal") 38 | 39 | assert.Equal(t, Duration(90*time.Second), c.Timeout) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /platform/aws/runtime/runtime.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/apex/log" 7 | "github.com/apex/up" 8 | ) 9 | 10 | // Runtime implementation. 11 | type Runtime struct { 12 | config *up.Config 13 | log log.Interface 14 | } 15 | 16 | // Option function. 17 | type Option func(*Runtime) 18 | 19 | // New with the given options. 20 | func New(c *up.Config, options ...Option) *Runtime { 21 | var v Runtime 22 | v.config = c 23 | v.log = log.Log 24 | for _, o := range options { 25 | o(&v) 26 | } 27 | return &v 28 | } 29 | 30 | // WithLogger option. 31 | func WithLogger(l log.Interface) Option { 32 | return func(v *Runtime) { 33 | v.log = l 34 | } 35 | } 36 | 37 | // Init implementation. 38 | func (r *Runtime) Init(stage string) error { 39 | os.Setenv("UP_STAGE", stage) 40 | 41 | if os.Getenv("NODE_ENV") == "" { 42 | os.Setenv("NODE_ENV", stage) 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /config/duration.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | // Duration may be specified as numerical seconds or 10 | // as a duration string such as "1.5m". 11 | type Duration time.Duration 12 | 13 | // Seconds returns the duration in seconds. 14 | func (d *Duration) Seconds() float64 { 15 | return float64(time.Duration(*d) / time.Second) 16 | } 17 | 18 | // UnmarshalJSON implementation. 19 | func (d *Duration) UnmarshalJSON(b []byte) error { 20 | if i, err := strconv.ParseInt(string(b), 10, 64); err == nil { 21 | *d = Duration(time.Second * time.Duration(i)) 22 | return nil 23 | } 24 | 25 | v, err := time.ParseDuration(string(bytes.Trim(b, `"`))) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | *d = Duration(v) 31 | return nil 32 | } 33 | 34 | // MarshalJSON implement. 35 | func (d *Duration) MarshalJSON() ([]byte, error) { 36 | return []byte(strconv.Itoa(int(d.Seconds()))), nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/progressreader/progressreader.go: -------------------------------------------------------------------------------- 1 | // Package progressreader provides an io.Reader progress bar. 2 | package progressreader 3 | 4 | import ( 5 | "io" 6 | "sync" 7 | 8 | "github.com/apex/up/internal/util" 9 | "github.com/tj/go-progress" 10 | "github.com/tj/go/term" 11 | ) 12 | 13 | // reader wrapping a progress bar. 14 | type reader struct { 15 | io.ReadCloser 16 | p *progress.Bar 17 | render func(string) 18 | written int 19 | sync.Once 20 | } 21 | 22 | // Read implementation. 23 | func (r *reader) Read(b []byte) (int, error) { 24 | r.Do(term.ClearAll) 25 | n, err := r.ReadCloser.Read(b) 26 | r.written += n 27 | r.p.ValueInt(r.written) 28 | r.render(term.CenterLine(r.p.String())) 29 | return n, err 30 | } 31 | 32 | // New returns a progress bar reader. 33 | func New(size int, r io.ReadCloser) io.ReadCloser { 34 | return &reader{ 35 | ReadCloser: r, 36 | p: util.NewProgressInt(size), 37 | render: term.Renderer(), 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /platform/lambda/reporter/reporter.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import "strings" 4 | 5 | // TODO: move most of reporting here 6 | 7 | // types map. 8 | var types = map[string]string{ 9 | "AWS::CloudFormation::Stack": "Stack", 10 | "AWS::Lambda::Alias": "Lambda alias", 11 | "AWS::Lambda::Permission": "Lambda permission", 12 | "AWS::ApiGateway::RestApi": "API", 13 | "AWS::ApiGateway::Method": "API method", 14 | "AWS::ApiGateway::Deployment": "API deployment", 15 | "AWS::ApiGateway::Resource": "API resource", 16 | "AWS::ApiGateway::DomainName": "API domain", 17 | "AWS::ApiGateway::BasePathMapping": "API mapping", 18 | "AWS::Route53::HostedZone": "DNS zone", 19 | "AWS::Route53::RecordSet": "DNS record", 20 | } 21 | 22 | // ResourceType returns a human-friendly resource type name. 23 | func ResourceType(s string) string { 24 | if types[s] != "" { 25 | return strings.ToLower(types[s]) 26 | } 27 | 28 | return s 29 | } 30 | -------------------------------------------------------------------------------- /config/lambda_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tj/assert" 7 | ) 8 | 9 | func TestLambda(t *testing.T) { 10 | c := &Lambda{} 11 | assert.NoError(t, c.Default(), "default") 12 | assert.Equal(t, 60, c.Timeout, "timeout") 13 | assert.Equal(t, 512, c.Memory, "timeout") 14 | } 15 | 16 | func TestLambda_Policy(t *testing.T) { 17 | t.Run("defaults", func(t *testing.T) { 18 | c := &Lambda{} 19 | assert.NoError(t, c.Default(), "default") 20 | assert.Len(t, c.Policy, 1) 21 | assert.Equal(t, defaultPolicy, c.Policy[0]) 22 | }) 23 | 24 | t.Run("specified", func(t *testing.T) { 25 | c := &Lambda{ 26 | Policy: []IAMPolicyStatement{ 27 | { 28 | "Effect": "Allow", 29 | "Resource": "*", 30 | "Action": []string{ 31 | "s3:List*", 32 | "s3:Get*", 33 | }, 34 | }, 35 | }, 36 | } 37 | 38 | assert.NoError(t, c.Default(), "default") 39 | assert.Len(t, c.Policy, 2) 40 | assert.Equal(t, defaultPolicy, c.Policy[1]) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /internal/cli/run/run.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "github.com/apex/up/internal/cli/root" 5 | "github.com/apex/up/internal/stats" 6 | "github.com/apex/up/internal/util" 7 | "github.com/pkg/errors" 8 | "github.com/tj/kingpin" 9 | ) 10 | 11 | func init() { 12 | cmd := root.Command("run", "Run a hook.") 13 | cmd.Example(`up run build`, "Run build hook.") 14 | cmd.Example(`up run clean`, "Run clean hook.") 15 | 16 | hook := cmd.Arg("hook", "Name of the hook to run.").Required().String() 17 | stage := cmd.Flag("stage", "Target stage name.").Short('s').Default("staging").String() 18 | 19 | cmd.Action(func(_ *kingpin.ParseContext) error { 20 | _, p, err := root.Init() 21 | if err != nil { 22 | return errors.Wrap(err, "initializing") 23 | } 24 | 25 | defer util.Pad()() 26 | 27 | stats.Track("Hook", map[string]interface{}{ 28 | "name": *hook, 29 | }) 30 | 31 | if err := p.Init(*stage); err != nil { 32 | return errors.Wrap(err, "initializing") 33 | } 34 | 35 | return p.RunHook(*hook) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /docs/10-links.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Links 3 | slug: links 4 | --- 5 | 6 | Links to helpful resources such as the Up community, changelog, examples, articles, videos and more. 7 | 8 | - [Changelog](https://github.com/apex/up/blob/master/History.md) for changes 9 | - [GitHub repository](https://github.com/apex/up) 10 | - [GitHub Actions](https://github.com/apex/actions) for continuous deployment 11 | - [@tjholowaychuk](https://twitter.com/tjholowaychuk) on Twitter for updates 12 | - [Example applications](https://github.com/apex/up-examples) for Up in various languages 13 | - [Slack](https://chat.apex.sh/) to chat with apex(1) and up(1) community members 14 | - [Blog](https://blog.apex.sh/) to follow release posts, tips and tricks 15 | - [YouTube](https://www.youtube.com/watch?v=1wnSNj-jmo4&index=1&list=PLbFkWVvnVLnRP-E87Tqe6nYVjOk6461o0) for the Apex Up video playlist 16 | - [Wiki](https://github.com/apex/up/wiki) for article listings, database suggestions, and sample apps 17 | - [Serverless Calculator](http://serverlesscalc.com/) for helping estimate costs for your use-case 18 | -------------------------------------------------------------------------------- /docs/01-installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | slug: setup 4 | --- 5 | 6 | Up is distributed in a binary form and can be installed manually via the [tarball releases](https://github.com/apex/up/releases) or one of the options below. The quickest way to get `up` is to run the following command: 7 | 8 | ``` 9 | $ curl -sf https://up.apex.sh/install | sh 10 | ``` 11 | 12 | By default Up is installed to `/usr/local/bin`, to specify a directory use `BINDIR`, this can be useful in CI where you may not have access to `/usr/local/bin`. Here's an example installing to the current directory: 13 | 14 | ``` 15 | $ curl -sf https://up.apex.sh/install | BINDIR=. sh 16 | ``` 17 | 18 | Verify installation with: 19 | 20 | ``` 21 | $ up version 22 | ``` 23 | 24 | Later when you want to update `up` to the latest version use the following command: 25 | 26 | ``` 27 | $ up upgrade 28 | ``` 29 | 30 | If you hit permission issues, you may need to run the following, as `up` is installed to `/usr/local/bin/up` by default. 31 | 32 | ``` 33 | $ sudo chown -R $(whoami) /usr/local/bin/ 34 | ``` 35 | -------------------------------------------------------------------------------- /internal/stats/stats.go: -------------------------------------------------------------------------------- 1 | // Package stats provides CLI analytics. 2 | package stats 3 | 4 | import ( 5 | "github.com/apex/log" 6 | "github.com/tj/go-cli-analytics" 7 | ) 8 | 9 | // p merged with track calls. 10 | var p = map[string]interface{}{} 11 | 12 | // Client for Segment analytics. 13 | var Client = analytics.New(&analytics.Config{ 14 | WriteKey: "qnvYCHktBBgACBkQ6V4dzh7aFCe8LF8u", 15 | Dir: ".up", 16 | }) 17 | 18 | // Track event `name` with optional `props`. 19 | func Track(name string, props map[string]interface{}) { 20 | if props == nil { 21 | props = map[string]interface{}{} 22 | } 23 | 24 | for k, v := range p { 25 | props[k] = v 26 | } 27 | 28 | log.Debugf("track %q %v", name, props) 29 | Client.Track(name, props) 30 | } 31 | 32 | // SetProperties sets global properties. 33 | func SetProperties(props map[string]interface{}) { 34 | p = props 35 | } 36 | 37 | // Flush stats. 38 | func Flush() { 39 | log.Debug("flushing analytics") 40 | if err := Client.Flush(); err != nil { 41 | log.WithError(err).Debug("flushing analytics") 42 | } 43 | log.Debug("flushing analytics") 44 | } 45 | -------------------------------------------------------------------------------- /internal/zip/zip_test.go: -------------------------------------------------------------------------------- 1 | package zip 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "sort" 10 | "testing" 11 | 12 | "github.com/tj/assert" 13 | ) 14 | 15 | // TODO: better tests 16 | 17 | func TestBuild(t *testing.T) { 18 | os.Chdir("testdata") 19 | defer os.Chdir("..") 20 | 21 | zip, _, err := Build(".") 22 | assert.NoError(t, err) 23 | 24 | out, err := ioutil.TempDir(os.TempDir(), "-up") 25 | assert.NoError(t, err, "tmpdir") 26 | dst := filepath.Join(out, "out.zip") 27 | 28 | f, err := os.Create(dst) 29 | assert.NoError(t, err, "create") 30 | 31 | _, err = io.Copy(f, zip) 32 | assert.NoError(t, err, "copy") 33 | 34 | assert.NoError(t, f.Close(), "close") 35 | 36 | cmd := exec.Command("unzip", "out.zip") 37 | cmd.Dir = out 38 | assert.NoError(t, cmd.Run(), "unzip") 39 | 40 | files, err := ioutil.ReadDir(out) 41 | assert.NoError(t, err, "readdir") 42 | 43 | var names []string 44 | for _, f := range files { 45 | names = append(names, f.Name()) 46 | } 47 | sort.Strings(names) 48 | 49 | assert.Equal(t, []string{"bar.js", "foo.js", "index.js", "out.zip"}, names) 50 | } 51 | -------------------------------------------------------------------------------- /platform/lambda/stack/status_test.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tj/assert" 7 | ) 8 | 9 | func TestStatus_String(t *testing.T) { 10 | assert.Equal(t, "Unknown", Status("").String()) 11 | assert.Equal(t, "Creating", Status("CREATE_IN_PROGRESS").String()) 12 | assert.Equal(t, "Deleting", Status("DELETE_IN_PROGRESS").String()) 13 | assert.Equal(t, "Failed to update", Status("UPDATE_FAILED").String()) 14 | } 15 | 16 | func TestStatus_State(t *testing.T) { 17 | assert.Equal(t, Pending, Status("CREATE_IN_PROGRESS").State()) 18 | assert.Equal(t, Pending, Status("UPDATE_IN_PROGRESS").State()) 19 | assert.Equal(t, Success, Status("CREATE_COMPLETE").State()) 20 | assert.Equal(t, Failure, Status("CREATE_FAILED").State()) 21 | } 22 | 23 | func TestStatus_IsDone(t *testing.T) { 24 | assert.False(t, Status("CREATE_IN_PROGRESS").IsDone()) 25 | assert.False(t, Status("UPDATE_IN_PROGRESS").IsDone()) 26 | assert.True(t, Status("CREATE_COMPLETE").IsDone()) 27 | assert.True(t, Status("UPDATE_COMPLETE").IsDone()) 28 | assert.True(t, Status("DELETE_COMPLETE").IsDone()) 29 | assert.True(t, Status("DELETE_FAILED").IsDone()) 30 | } 31 | -------------------------------------------------------------------------------- /internal/colors/colors.go: -------------------------------------------------------------------------------- 1 | // Package colors provides colors used by the CLI. 2 | package colors 3 | 4 | import ( 5 | color "github.com/aybabtme/rgbterm" 6 | ) 7 | 8 | // Func is a color function. 9 | type Func func(string) string 10 | 11 | // Gray string. 12 | func Gray(s string) string { 13 | return color.FgString(s, 150, 150, 150) 14 | } 15 | 16 | // Blue string. 17 | func Blue(s string) string { 18 | return color.FgString(s, 77, 173, 247) 19 | } 20 | 21 | // Cyan string. 22 | func Cyan(s string) string { 23 | return color.FgString(s, 34, 184, 207) 24 | } 25 | 26 | // Green string. 27 | func Green(s string) string { 28 | return color.FgString(s, 0, 200, 255) 29 | } 30 | 31 | // Red string. 32 | func Red(s string) string { 33 | return color.FgString(s, 194, 37, 92) 34 | } 35 | 36 | // Yellow string. 37 | func Yellow(s string) string { 38 | return color.FgString(s, 252, 196, 25) 39 | } 40 | 41 | // Purple string. 42 | func Purple(s string) string { 43 | return color.FgString(s, 96, 97, 190) 44 | } 45 | 46 | // Bool returns a color func based on the state. 47 | func Bool(ok bool) Func { 48 | if ok { 49 | return Purple 50 | } 51 | 52 | return Red 53 | } 54 | -------------------------------------------------------------------------------- /config/backoff.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/tj/backoff" 7 | ) 8 | 9 | // Backoff config. 10 | type Backoff struct { 11 | // Min time in milliseconds. 12 | Min int `json:"min"` 13 | 14 | // Max time in milliseconds. 15 | Max int `json:"max"` 16 | 17 | // Factor applied for every attempt. 18 | Factor float64 `json:"factor"` 19 | 20 | // Attempts performed before failing. 21 | Attempts int `json:"attempts"` 22 | 23 | // Jitter is applied when true. 24 | Jitter bool `json:"jitter"` 25 | } 26 | 27 | // Default implementation. 28 | func (b *Backoff) Default() error { 29 | if b.Min == 0 { 30 | b.Min = 100 31 | } 32 | 33 | if b.Max == 0 { 34 | b.Max = 500 35 | } 36 | 37 | if b.Factor == 0 { 38 | b.Factor = 2 39 | } 40 | 41 | if b.Attempts == 0 { 42 | b.Attempts = 3 43 | } 44 | 45 | return nil 46 | } 47 | 48 | // Backoff returns the backoff from config. 49 | func (b *Backoff) Backoff() *backoff.Backoff { 50 | return &backoff.Backoff{ 51 | Min: time.Duration(b.Min) * time.Millisecond, 52 | Max: time.Duration(b.Max) * time.Millisecond, 53 | Factor: b.Factor, 54 | Jitter: b.Jitter, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/userconfig/userconfig_test.go: -------------------------------------------------------------------------------- 1 | package userconfig 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/mitchellh/go-homedir" 9 | "github.com/tj/assert" 10 | ) 11 | 12 | func init() { 13 | configDir = ".up-test" 14 | } 15 | 16 | func TestConfig_file(t *testing.T) { 17 | t.Run("load when missing", func(t *testing.T) { 18 | dir, _ := homedir.Dir() 19 | os.RemoveAll(filepath.Join(dir, configDir)) 20 | 21 | c := Config{} 22 | assert.NoError(t, c.Load(), "load") 23 | }) 24 | 25 | t.Run("save", func(t *testing.T) { 26 | c := Config{} 27 | assert.NoError(t, c.Load(), "load") 28 | assert.Equal(t, "", c.Team) 29 | 30 | c.Team = "apex" 31 | assert.NoError(t, c.Save(), "save") 32 | }) 33 | 34 | t.Run("load after save", func(t *testing.T) { 35 | c := Config{} 36 | assert.NoError(t, c.Load(), "save") 37 | assert.Equal(t, "apex", c.Team) 38 | }) 39 | } 40 | 41 | func TestConfig_env(t *testing.T) { 42 | t.Run("load", func(t *testing.T) { 43 | os.Setenv("UP_CONFIG", `{ "team": "tj@apex.sh" }`) 44 | c := Config{} 45 | assert.NoError(t, c.Load(), "load") 46 | assert.Equal(t, "tj@apex.sh", c.Team) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020 TJ Holowaychuk tj@tjholowaychuk.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /internal/cli/prune/prune.go: -------------------------------------------------------------------------------- 1 | package prune 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/tj/kingpin" 6 | 7 | "github.com/apex/up/internal/cli/root" 8 | "github.com/apex/up/internal/stats" 9 | ) 10 | 11 | func init() { 12 | cmd := root.Command("prune", "Prune old S3 deployments of a stage.") 13 | 14 | cmd.Example(`up prune`, "Prune and retain the most recent 30 staging versions.") 15 | cmd.Example(`up prune -s production`, "Prune and retain the most recent 30 production versions.") 16 | cmd.Example(`up prune -s production -r 15`, "Prune and retain the most recent 15 production versions.") 17 | 18 | stage := cmd.Flag("stage", "Target stage name.").Short('s').Default("staging").String() 19 | versions := cmd.Flag("retain", "Number of versions to retain.").Short('r').Default("30").Int() 20 | 21 | cmd.Action(func(_ *kingpin.ParseContext) error { 22 | c, p, err := root.Init() 23 | if err != nil { 24 | return errors.Wrap(err, "initializing") 25 | } 26 | 27 | region := c.Regions[0] 28 | 29 | stats.Track("Prune", map[string]interface{}{ 30 | "versions": *versions, 31 | "stage": *stage, 32 | }) 33 | 34 | return p.Prune(region, *stage, *versions) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /http/relay/testdata/basic/app.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | const qs = require('querystring'); 4 | const port = process.env.PORT; 5 | 6 | let server; 7 | 8 | const routes = {}; 9 | 10 | routes['/echo'] = (req, res) => { 11 | const buffers = [] 12 | req.on('data', b => buffers.push(b)) 13 | req.on('end', _ => { 14 | const body = Buffer.concat(buffers).toString() 15 | res.setHeader('Content-Type', 'application/json') 16 | res.end(JSON.stringify({ 17 | header: req.headers, 18 | url: req.url, 19 | body 20 | }, null, 2)) 21 | }); 22 | }; 23 | 24 | routes['/timeout'] = (req, res) => { 25 | setTimeout(function(){ 26 | res.end('Hello') 27 | }, 50000); 28 | }; 29 | 30 | routes['/throw'] = (req, res) => { 31 | yaynode() 32 | }; 33 | 34 | routes['/exit'] = (req, res) => { 35 | process.exit() 36 | }; 37 | 38 | server = http.createServer((req, res) => { 39 | const r = Object.keys(routes).find(pattern => req.url.indexOf(pattern) === 0); 40 | const handler = r && routes[r]; 41 | if (handler) { 42 | handler(req, res); 43 | return; 44 | } 45 | 46 | res.setHeader('Content-Type', 'text/plain') 47 | res.end('Hello World') 48 | }).listen(port); 49 | -------------------------------------------------------------------------------- /internal/header/header.go: -------------------------------------------------------------------------------- 1 | // Package header provides path-matched header injection rules. 2 | package header 3 | 4 | import ( 5 | "github.com/fanyang01/radix" 6 | ) 7 | 8 | // Fields map. 9 | type Fields map[string]string 10 | 11 | // Rules map of paths to fields. 12 | type Rules map[string]Fields 13 | 14 | // Matcher for header lookup. 15 | type Matcher struct { 16 | t *radix.PatternTrie 17 | } 18 | 19 | // Lookup returns fields for the given path. 20 | func (m *Matcher) Lookup(path string) Fields { 21 | v, ok := m.t.Lookup(path) 22 | if !ok { 23 | return nil 24 | } 25 | 26 | return v.(Fields) 27 | } 28 | 29 | // Compile the given rules to a trie. 30 | func Compile(rules Rules) (*Matcher, error) { 31 | t := radix.NewPatternTrie() 32 | m := &Matcher{t} 33 | 34 | for path, fields := range rules { 35 | t.Add(path, fields) 36 | } 37 | 38 | return m, nil 39 | } 40 | 41 | // Merge returns a new rules set giving precedence to `b`. 42 | func Merge(a, b Rules) Rules { 43 | r := make(Rules) 44 | 45 | for path, fields := range a { 46 | r[path] = fields 47 | } 48 | 49 | for path, fields := range b { 50 | if _, ok := r[path]; !ok { 51 | r[path] = make(Fields) 52 | } 53 | 54 | for name, val := range fields { 55 | r[path][name] = val 56 | } 57 | } 58 | 59 | return r 60 | } 61 | -------------------------------------------------------------------------------- /internal/cli/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/tj/kingpin" 8 | 9 | "github.com/apex/up/internal/cli/root" 10 | "github.com/apex/up/internal/stats" 11 | "github.com/apex/up/internal/util" 12 | ) 13 | 14 | func init() { 15 | cmd := root.Command("metrics", "Show project metrics.") 16 | cmd.Example(`up metrics`, "Show metrics for staging environment.") 17 | cmd.Example(`up metrics -s production`, "Show metrics for production environment.") 18 | 19 | stage := cmd.Flag("stage", "Target stage name.").Short('s').Default("staging").String() 20 | since := cmd.Flag("since", "Show metrics since duration (30s, 5m, 2h, 1h30m, 3d, 1M).").Short('S').Default("1M").String() 21 | 22 | cmd.Action(func(_ *kingpin.ParseContext) error { 23 | c, p, err := root.Init() 24 | if err != nil { 25 | return errors.Wrap(err, "initializing") 26 | } 27 | 28 | s, err := util.ParseDuration(*since) 29 | if err != nil { 30 | return errors.Wrap(err, "parsing --since duration") 31 | } 32 | 33 | region := c.Regions[0] 34 | 35 | stats.Track("Metrics", map[string]interface{}{ 36 | "stage": *stage, 37 | "since": s.Round(time.Second), 38 | }) 39 | 40 | start := time.Now().UTC().Add(-s) 41 | return p.ShowMetrics(region, *stage, start) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /http/logs/logs_test.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/tj/assert" 10 | 11 | "github.com/apex/up" 12 | "github.com/apex/up/config" 13 | "github.com/apex/up/http/static" 14 | ) 15 | 16 | func TestLogs(t *testing.T) { 17 | // TODO: refactor and pass in app name/version/region 18 | 19 | var buf bytes.Buffer 20 | log.SetOutput(&buf) 21 | 22 | c := &up.Config{ 23 | Static: config.Static{ 24 | Dir: "testdata", 25 | }, 26 | } 27 | 28 | h, err := New(c, static.New(c)) 29 | assert.NoError(t, err) 30 | 31 | res := httptest.NewRecorder() 32 | req := httptest.NewRequest("GET", "/?foo=bar", nil) 33 | 34 | h.ServeHTTP(res, req) 35 | 36 | assert.Equal(t, 200, res.Code) 37 | assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) 38 | assert.Equal(t, "Index HTML\n", res.Body.String()) 39 | 40 | s := buf.String() 41 | assert.Contains(t, s, `info response`) 42 | // assert.Contains(t, s, `app_name=api`) 43 | // assert.Contains(t, s, `app_version=5`) 44 | // assert.Contains(t, s, `app_region=us-west-2`) 45 | assert.Contains(t, s, `ip=192.0.2.1:1234`) 46 | assert.Contains(t, s, `method=GET`) 47 | assert.Contains(t, s, `path=/`) 48 | assert.Contains(t, s, `plugin=logs`) 49 | assert.Contains(t, s, `size=11`) 50 | assert.Contains(t, s, `status=200`) 51 | } 52 | -------------------------------------------------------------------------------- /platform/aws/cost/cost_test.go: -------------------------------------------------------------------------------- 1 | package cost 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tj/assert" 7 | ) 8 | 9 | func TestRequests(t *testing.T) { 10 | table := []struct { 11 | requests int 12 | expected float64 13 | }{ 14 | {0, 0.0}, 15 | {1000, 0.001}, 16 | {1000000, 1.0}, 17 | } 18 | 19 | for _, row := range table { 20 | assert.Equal(t, row.expected, Requests(row.requests)) 21 | } 22 | } 23 | 24 | func TestRate(t *testing.T) { 25 | table := []struct { 26 | memory int 27 | expected float64 28 | }{ 29 | {-1, 0.0}, 30 | {0, 0.0}, 31 | {128, 2.08e-7}, 32 | {156, 0.0}, 33 | } 34 | 35 | for _, row := range table { 36 | assert.Equal(t, row.expected, Rate(row.memory)) 37 | } 38 | } 39 | 40 | func TestInvocations(t *testing.T) { 41 | table := []struct { 42 | invocations int 43 | expected float64 44 | }{ 45 | {0, 0.0}, 46 | {1, 2.0e-7}, 47 | {1.0e7, 2.0}, 48 | } 49 | 50 | for _, row := range table { 51 | assert.Equal(t, row.expected, Invocations(row.invocations)) 52 | } 53 | } 54 | 55 | func TestDuration(t *testing.T) { 56 | table := []struct { 57 | duration int 58 | memory int 59 | expected float64 60 | }{ 61 | {0, 128, 0}, 62 | {100000, 256, 4.17e-4}, 63 | {1e8, 1536, 2.501}, 64 | } 65 | 66 | for _, row := range table { 67 | assert.Equal(t, row.expected, Duration(row.duration, row.memory)) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | GO ?= go 3 | 4 | # Build all files. 5 | build: 6 | @echo "==> Building" 7 | @$(GO) generate ./... 8 | .PHONY: build 9 | 10 | # Install from source. 11 | install: 12 | @echo "==> Installing up ${GOPATH}/bin/up" 13 | @$(GO) install ./... 14 | .PHONY: install 15 | 16 | # Run all tests. 17 | test: internal/proxy/bin/bin_assets.go 18 | @$(GO) test -timeout 2m ./... && echo "\n==>\033[32m Ok\033[m\n" 19 | .PHONY: test 20 | 21 | # Run all tests in CI. 22 | test.ci: internal/proxy/bin/bin_assets.go 23 | @$(GO) test -v -timeout 5m ./... && echo "\n==>\033[32m Ok\033[m\n" 24 | .PHONY: test.ci 25 | 26 | internal/proxy/bin/bin_assets.go: 27 | @$(GO) generate ./... 28 | 29 | # Show source statistics. 30 | cloc: 31 | @cloc -exclude-dir=vendor,node_modules . 32 | .PHONY: cloc 33 | 34 | # Release binaries to GitHub. 35 | release: build 36 | @echo "==> Releasing" 37 | @goreleaser -p 1 --rm-dist --config .goreleaser.yml 38 | @echo "==> Complete" 39 | .PHONY: release 40 | 41 | # Show to-do items per file. 42 | todo: 43 | @rg TODO: 44 | .PHONY: todo 45 | 46 | # Show size of imports. 47 | size: 48 | @curl -sL https://gist.githubusercontent.com/tj/04e0965e23da00ca33f101e5b2ed4ed4/raw/9aa16698b2bc606cf911219ea540972edef05c4b/gistfile1.txt | bash 49 | .PHONY: size 50 | 51 | # Clean. 52 | clean: 53 | @rm -fr \ 54 | dist \ 55 | internal/proxy/bin/bin_assets.go \ 56 | internal/shim/bindata.go 57 | .PHONY: clean 58 | -------------------------------------------------------------------------------- /http/robots/robots_test.go: -------------------------------------------------------------------------------- 1 | package robots 2 | 3 | import ( 4 | "net/http/httptest" 5 | "os" 6 | "testing" 7 | 8 | "github.com/apex/up" 9 | "github.com/tj/assert" 10 | 11 | "github.com/apex/up/config" 12 | "github.com/apex/up/http/static" 13 | ) 14 | 15 | func TestRobots(t *testing.T) { 16 | c := &up.Config{ 17 | Static: config.Static{ 18 | Dir: "testdata", 19 | }, 20 | } 21 | 22 | t.Run("should set X-Robots-Tag", func(t *testing.T) { 23 | h := New(c, static.New(c)) 24 | 25 | res := httptest.NewRecorder() 26 | req := httptest.NewRequest("GET", "/", nil) 27 | 28 | h.ServeHTTP(res, req) 29 | 30 | assert.Equal(t, 200, res.Code) 31 | assert.Equal(t, "none", res.Header().Get("X-Robots-Tag")) 32 | assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) 33 | assert.Equal(t, "Index HTML\n", res.Body.String()) 34 | }) 35 | 36 | t.Run("should not set X-Robots-Tag for production stage", func(t *testing.T) { 37 | os.Setenv("UP_STAGE", "production") 38 | defer os.Setenv("UP_STAGE", "") 39 | 40 | h := New(c, static.New(c)) 41 | 42 | res := httptest.NewRecorder() 43 | req := httptest.NewRequest("GET", "/", nil) 44 | 45 | h.ServeHTTP(res, req) 46 | 47 | assert.Equal(t, 200, res.Code) 48 | assert.Equal(t, "", res.Header().Get("X-Robots-Tag")) 49 | assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) 50 | assert.Equal(t, "Index HTML\n", res.Body.String()) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /http/headers/headers_test.go: -------------------------------------------------------------------------------- 1 | package headers 2 | 3 | import ( 4 | "net/http/httptest" 5 | "os" 6 | "testing" 7 | 8 | "github.com/tj/assert" 9 | "github.com/apex/up" 10 | 11 | "github.com/apex/up/http/static" 12 | "github.com/apex/up/internal/header" 13 | ) 14 | 15 | func TestHeaders(t *testing.T) { 16 | os.Chdir("testdata") 17 | defer os.Chdir("..") 18 | 19 | c := &up.Config{ 20 | Headers: header.Rules{ 21 | "/*.css": { 22 | "Cache-Control": "public, max-age=999999", 23 | }, 24 | }, 25 | } 26 | 27 | h, err := New(c, static.New(c)) 28 | assert.NoError(t, err, "init") 29 | 30 | t.Run("mismatch", func(t *testing.T) { 31 | res := httptest.NewRecorder() 32 | req := httptest.NewRequest("GET", "/", nil) 33 | 34 | h.ServeHTTP(res, req) 35 | 36 | assert.Equal(t, 200, res.Code) 37 | assert.Equal(t, "", res.Header().Get("Cache-Control")) 38 | assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) 39 | assert.Equal(t, "Index HTML\n", res.Body.String()) 40 | }) 41 | 42 | t.Run("matched exact", func(t *testing.T) { 43 | res := httptest.NewRecorder() 44 | req := httptest.NewRequest("GET", "/style.css", nil) 45 | 46 | h.ServeHTTP(res, req) 47 | 48 | assert.Equal(t, 200, res.Code) 49 | assert.Equal(t, "public, max-age=999999", res.Header().Get("Cache-Control")) 50 | assert.Equal(t, "css", res.Header().Get("X-Type")) 51 | assert.Equal(t, "text/css; charset=utf-8", res.Header().Get("Content-Type")) 52 | assert.Equal(t, "body { color: red }\n", res.Body.String()) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /config/relay.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | ) 6 | 7 | // Relay config. 8 | type Relay struct { 9 | // Command run to start your server. 10 | Command string `json:"command"` 11 | 12 | // Timeout in seconds to wait for a response. 13 | Timeout int `json:"timeout"` 14 | 15 | // ListenTimeout in seconds when waiting for the app to bind to PORT. 16 | ListenTimeout int `json:"listen_timeout"` 17 | } 18 | 19 | // Default implementation. 20 | func (r *Relay) Default() error { 21 | if r.Command == "" { 22 | r.Command = "./server" 23 | } 24 | 25 | if r.Timeout == 0 { 26 | r.Timeout = 15 27 | } 28 | 29 | if r.ListenTimeout == 0 { 30 | r.ListenTimeout = 15 31 | } 32 | 33 | return nil 34 | } 35 | 36 | // Validate will try to perform sanity checks for this Relay configuration. 37 | func (r *Relay) Validate() error { 38 | if r.Command == "" { 39 | err := errors.New("should not be empty") 40 | return errors.Wrap(err, ".command") 41 | } 42 | 43 | if r.ListenTimeout <= 0 { 44 | err := errors.New("should be greater than 0") 45 | return errors.Wrap(err, ".listen_timeout") 46 | } 47 | 48 | if r.ListenTimeout > 25 { 49 | err := errors.New("should be <= 25") 50 | return errors.Wrap(err, ".listen_timeout") 51 | } 52 | 53 | if r.Timeout > 25 { 54 | err := errors.New("should be <= 25") 55 | return errors.Wrap(err, ".timeout") 56 | } 57 | 58 | return nil 59 | } 60 | 61 | // Override config. 62 | func (r *Relay) Override(c *Config) { 63 | if r.Command != "" { 64 | c.Proxy.Command = r.Command 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /docs/05-runtimes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Runtimes 3 | slug: runtimes 4 | --- 5 | 6 | Up supports a number of interpreted languages, and virtually any language which can be compiled to a binary such as Golang. Up does its best to provide idiomatic and useful out-of-the-box experiences tailored to each language. Currently first-class support is provided for: 7 | 8 | - Golang 9 | - Node.js 10 | - Crystal 11 | - Static sites 12 | 13 | ## Node.js 14 | 15 | When a `package.json` file is detected, Node.js is the assumed runtime. By default `nodejs10.x` is used, see [Lambda Settings](https://apex.sh/docs/up/configuration/#lambda_settings) for details. 16 | 17 | The `build` hook becomes: 18 | 19 | ``` 20 | $ npm run build 21 | ``` 22 | 23 | The server run by the proxy becomes: 24 | 25 | ``` 26 | $ npm start 27 | ``` 28 | 29 | ## Golang 30 | 31 | When a `main.go` file is detected, Golang is the assumed runtime. 32 | 33 | The `build` hook becomes: 34 | 35 | ``` 36 | $ GOOS=linux GOARCH=amd64 go build -o server *.go 37 | ``` 38 | 39 | The `clean` hook becomes: 40 | 41 | ``` 42 | $ rm server 43 | ``` 44 | 45 | ## Crystal 46 | 47 | When a `main.cr` file is detected, Crystal is the assumed runtime. Note that this runtime requires Docker to be installed. 48 | 49 | The `build` hook becomes: 50 | 51 | ``` 52 | $ docker run --rm -v $(pwd):/src -w /src crystallang/crystal crystal build -o server main.cr --release --static 53 | ``` 54 | 55 | The `clean` hook becomes: 56 | 57 | ``` 58 | $ rm server 59 | ``` 60 | 61 | ## Static 62 | 63 | When an `index.html` file is detected the project is assumed to be static. 64 | -------------------------------------------------------------------------------- /config/hooks_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | ) 9 | 10 | func TestHook(t *testing.T) { 11 | t.Run("missing", func(t *testing.T) { 12 | s := []byte(`{}`) 13 | 14 | var c struct { 15 | Build Hook 16 | } 17 | 18 | err := json.Unmarshal(s, &c) 19 | assert.NoError(t, err, "unmarshal") 20 | 21 | assert.Equal(t, Hook(nil), c.Build) 22 | }) 23 | 24 | t.Run("invalid type", func(t *testing.T) { 25 | s := []byte(` 26 | { 27 | "build": 5 28 | } 29 | `) 30 | 31 | var c struct { 32 | Build Hook 33 | } 34 | 35 | err := json.Unmarshal(s, &c) 36 | assert.EqualError(t, err, `hook must be a string or array of strings`) 37 | }) 38 | 39 | t.Run("string", func(t *testing.T) { 40 | s := []byte(` 41 | { 42 | "build": "go build main.go" 43 | } 44 | `) 45 | 46 | var c struct { 47 | Build Hook 48 | } 49 | 50 | err := json.Unmarshal(s, &c) 51 | assert.NoError(t, err, "unmarshal") 52 | 53 | assert.Equal(t, Hook{"go build main.go"}, c.Build) 54 | }) 55 | 56 | t.Run("array", func(t *testing.T) { 57 | s := []byte(` 58 | { 59 | "build": [ 60 | "go build main.go", 61 | "browserify src/index.js > app.js" 62 | ] 63 | } 64 | `) 65 | 66 | var c struct { 67 | Build Hook 68 | } 69 | 70 | err := json.Unmarshal(s, &c) 71 | assert.NoError(t, err, "unmarshal") 72 | 73 | assert.Equal(t, Hook{ 74 | "go build main.go", 75 | "browserify src/index.js > app.js", 76 | }, c.Build) 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /http/static/static.go: -------------------------------------------------------------------------------- 1 | // Package static provides static file serving with HTTP cache support. 2 | package static 3 | 4 | import ( 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/apex/up" 11 | ) 12 | 13 | // New static handler. 14 | func New(c *up.Config) http.Handler { 15 | return http.FileServer(http.Dir(c.Static.Dir)) 16 | } 17 | 18 | // NewDynamic static handler for dynamic apps. 19 | func NewDynamic(c *up.Config, next http.Handler) http.Handler { 20 | prefix := normalizePrefix(c.Static.Prefix) 21 | dir := c.Static.Dir 22 | 23 | if dir == "" { 24 | return next 25 | } 26 | 27 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 28 | var skip bool 29 | path := r.URL.Path 30 | 31 | // prefix 32 | if prefix != "" { 33 | if strings.HasPrefix(path, prefix) { 34 | path = strings.Replace(path, prefix, "/", 1) 35 | } else { 36 | skip = true 37 | } 38 | } 39 | 40 | // convert 41 | path = filepath.FromSlash(path) 42 | 43 | // file exists, serve it 44 | if !skip { 45 | file := filepath.Join(dir, path) 46 | info, err := os.Stat(file) 47 | if !os.IsNotExist(err) && !info.IsDir() { 48 | http.ServeFile(w, r, file) 49 | return 50 | } 51 | } 52 | 53 | // delegate 54 | next.ServeHTTP(w, r) 55 | }) 56 | } 57 | 58 | // normalizePrefix returns a prefix path normalized with leading and trailing "/". 59 | func normalizePrefix(s string) string { 60 | if !strings.HasPrefix(s, "/") { 61 | s = "/" + s 62 | } 63 | 64 | if !strings.HasSuffix(s, "/") { 65 | s = s + "/" 66 | } 67 | 68 | return s 69 | } 70 | -------------------------------------------------------------------------------- /internal/header/header_test.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tj/assert" 7 | ) 8 | 9 | func TestMatcher_Lookup(t *testing.T) { 10 | rules := Rules{ 11 | "*": { 12 | "X-Type": "html", 13 | }, 14 | "*.css": { 15 | "X-Type": "css", 16 | }, 17 | "/docs/alerts": { 18 | "X-Type": "docs alerts", 19 | }, 20 | "/docs/*": { 21 | "X-Type": "docs", 22 | }, 23 | } 24 | 25 | m, err := Compile(rules) 26 | assert.NoError(t, err, "compile") 27 | 28 | assert.Equal(t, Fields{"X-Type": "html"}, m.Lookup("/something")) 29 | assert.Equal(t, Fields{"X-Type": "html"}, m.Lookup("/docs")) 30 | assert.Equal(t, Fields{"X-Type": "docs"}, m.Lookup("/docs/")) 31 | assert.Equal(t, Fields{"X-Type": "css"}, m.Lookup("/style.css")) 32 | assert.Equal(t, Fields{"X-Type": "css"}, m.Lookup("/public/css/style.css")) 33 | assert.Equal(t, Fields{"X-Type": "docs"}, m.Lookup("/docs/checks")) 34 | assert.Equal(t, Fields{"X-Type": "docs alerts"}, m.Lookup("/docs/alerts")) 35 | } 36 | 37 | func TestMerge(t *testing.T) { 38 | rules := Rules{ 39 | "*": { 40 | "X-Type": "html", 41 | "X-Foo": "bar", 42 | }, 43 | "/login": { 44 | "X-Something": "here", 45 | }, 46 | } 47 | 48 | rules = Merge(rules, Rules{ 49 | "*": { 50 | "X-Type": "pdf", 51 | }, 52 | "/admin": { 53 | "X-Something": "here", 54 | }, 55 | }) 56 | 57 | expected := Rules{ 58 | "*": { 59 | "X-Type": "pdf", 60 | "X-Foo": "bar", 61 | }, 62 | "/login": { 63 | "X-Something": "here", 64 | }, 65 | "/admin": { 66 | "X-Something": "here", 67 | }, 68 | } 69 | 70 | assert.Equal(t, expected, rules) 71 | } 72 | -------------------------------------------------------------------------------- /http/inject/inject.go: -------------------------------------------------------------------------------- 1 | // Package inject provides script and style injection. 2 | package inject 3 | 4 | import ( 5 | "bytes" 6 | "io" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/apex/up" 12 | "github.com/apex/up/internal/inject" 13 | ) 14 | 15 | // response wrapper. 16 | type response struct { 17 | http.ResponseWriter 18 | rules inject.Rules 19 | body bytes.Buffer 20 | header bool 21 | ignore bool 22 | code int 23 | } 24 | 25 | // Write implementation. 26 | func (r *response) Write(b []byte) (int, error) { 27 | if !r.header { 28 | r.WriteHeader(200) 29 | return r.Write(b) 30 | } 31 | 32 | return r.body.Write(b) 33 | } 34 | 35 | // WriteHeader implementation. 36 | func (r *response) WriteHeader(code int) { 37 | r.header = true 38 | w := r.ResponseWriter 39 | kind := w.Header().Get("Content-Type") 40 | r.ignore = !strings.HasPrefix(kind, "text/html") || code >= 300 41 | r.code = code 42 | } 43 | 44 | // end injects if necessary. 45 | func (r *response) end() { 46 | w := r.ResponseWriter 47 | 48 | if r.ignore { 49 | w.WriteHeader(r.code) 50 | r.body.WriteTo(w) 51 | return 52 | } 53 | 54 | body := r.rules.Apply(r.body.String()) 55 | w.Header().Set("Content-Length", strconv.Itoa(len(body))) 56 | io.WriteString(w, body) 57 | } 58 | 59 | // New inject handler. 60 | func New(c *up.Config, next http.Handler) (http.Handler, error) { 61 | if len(c.Inject) == 0 { 62 | return next, nil 63 | } 64 | 65 | h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 66 | res := &response{ResponseWriter: w, rules: c.Inject} 67 | next.ServeHTTP(res, r) 68 | res.end() 69 | }) 70 | 71 | return h, nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/account/cards.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "github.com/stripe/stripe-go" 5 | "github.com/tj/survey" 6 | ) 7 | 8 | // Questions. 9 | var questions = []*survey.Question{ 10 | { 11 | Name: "name", 12 | Prompt: &survey.Input{Message: "Name:"}, 13 | Validate: survey.Required, 14 | }, 15 | { 16 | Name: "number", 17 | Prompt: &survey.Input{Message: "Number:"}, 18 | Validate: survey.Required, 19 | }, 20 | { 21 | Name: "cvc", 22 | Prompt: &survey.Input{Message: "CVC:"}, 23 | Validate: survey.Required, 24 | }, 25 | { 26 | Name: "month", 27 | Prompt: &survey.Input{Message: "Expiration month:"}, 28 | Validate: survey.Required, 29 | }, 30 | { 31 | Name: "year", 32 | Prompt: &survey.Input{Message: "Expiration year:"}, 33 | Validate: survey.Required, 34 | }, 35 | { 36 | Name: "address1", 37 | Prompt: &survey.Input{Message: "Street Address:"}, 38 | Validate: survey.Required, 39 | }, 40 | { 41 | Name: "city", 42 | Prompt: &survey.Input{Message: "City:"}, 43 | Validate: survey.Required, 44 | }, 45 | { 46 | Name: "state", 47 | Prompt: &survey.Input{Message: "State:"}, 48 | Validate: survey.Required, 49 | }, 50 | { 51 | Name: "country", 52 | Prompt: &survey.Input{Message: "Country:"}, 53 | Validate: survey.Required, 54 | }, 55 | { 56 | Name: "zip", 57 | Prompt: &survey.Input{Message: "Zip:"}, 58 | Validate: survey.Required, 59 | }, 60 | } 61 | 62 | // PromptForCard displays an interactive form for the user to provide CC details. 63 | func PromptForCard() (card stripe.CardParams, err error) { 64 | err = survey.Ask(questions, &card) 65 | return 66 | } 67 | -------------------------------------------------------------------------------- /cmd/up-proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/apex/go-apex" 8 | "github.com/apex/log" 9 | "github.com/apex/log/handlers/json" 10 | 11 | "github.com/apex/up" 12 | "github.com/apex/up/handler" 13 | "github.com/apex/up/internal/logs" 14 | "github.com/apex/up/internal/proxy" 15 | "github.com/apex/up/internal/util" 16 | "github.com/apex/up/platform/aws/runtime" 17 | ) 18 | 19 | func main() { 20 | start := time.Now() 21 | stage := os.Getenv("UP_STAGE") 22 | 23 | // setup logging 24 | log.SetHandler(json.Default) 25 | if s := os.Getenv("LOG_LEVEL"); s != "" { 26 | log.SetLevelFromString(s) 27 | } 28 | 29 | log.Log = log.WithFields(logs.Fields()) 30 | log.Info("initializing") 31 | 32 | // read config 33 | c, err := up.ReadConfig("up.json") 34 | if err != nil { 35 | log.Fatalf("error reading config: %s", err) 36 | } 37 | 38 | ctx := log.WithFields(log.Fields{ 39 | "name": c.Name, 40 | "type": c.Type, 41 | }) 42 | 43 | // init project 44 | p := runtime.New(c) 45 | 46 | // init runtime 47 | if err := p.Init(stage); err != nil { 48 | ctx.Fatalf("error initializing: %s", err) 49 | } 50 | 51 | // overrides 52 | if err := c.Override(stage); err != nil { 53 | ctx.Fatalf("error overriding: %s", err) 54 | } 55 | 56 | // create handler 57 | h, err := handler.FromConfig(c) 58 | if err != nil { 59 | ctx.Fatalf("error creating handler: %s", err) 60 | } 61 | 62 | // init handler 63 | h, err = handler.New(c, h) 64 | if err != nil { 65 | ctx.Fatalf("error initializing handler: %s", err) 66 | } 67 | 68 | // serve 69 | log.WithField("duration", util.MillisecondsSince(start)).Info("initialized") 70 | apex.Handle(proxy.NewHandler(h)) 71 | } 72 | -------------------------------------------------------------------------------- /config/cors.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // CORS configuration. 4 | type CORS struct { 5 | // AllowedOrigins is a list of origins a cross-domain request can be executed from. 6 | // If the special "*" value is present in the list, all origins will be allowed. 7 | // An origin may contain a wildcard (*) to replace 0 or more characters 8 | // (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty. 9 | // Only one wildcard can be used per origin. 10 | // Default value is ["*"] 11 | AllowedOrigins []string `json:"allowed_origins"` 12 | 13 | // AllowedMethods is a list of methods the client is allowed to use with 14 | // cross-domain requests. Default value is simple methods (GET and POST) 15 | AllowedMethods []string `json:"allowed_methods"` 16 | 17 | // AllowedHeaders is list of non simple headers the client is allowed to use with 18 | // cross-domain requests. 19 | // If the special "*" value is present in the list, all headers will be allowed. 20 | // Default value is [] but "Origin" is always appended to the list. 21 | AllowedHeaders []string `json:"allowed_headers"` 22 | 23 | // ExposedHeaders indicates which headers are safe to expose to the API of a CORS 24 | // API specification 25 | ExposedHeaders []string `json:"exposed_headers"` 26 | 27 | // AllowCredentials indicates whether the request can include user credentials like 28 | // cookies, HTTP authentication or client side SSL certificates. 29 | AllowCredentials bool `json:"allow_credentials"` 30 | 31 | // MaxAge indicates how long (in seconds) the results of a preflight request 32 | // can be cached. 33 | MaxAge int `json:"max_age"` 34 | 35 | // Debugging flag adds additional output to debug server side CORS issues 36 | Debug bool `json:"debug"` 37 | } 38 | -------------------------------------------------------------------------------- /internal/proxy/request.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "net/http" 7 | "net/url" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // NewRequest returns a new http.Request from the given Lambda event. 15 | func NewRequest(e *Input) (*http.Request, error) { 16 | // path 17 | u, err := url.Parse(e.Path) 18 | if err != nil { 19 | return nil, errors.Wrap(err, "parsing path") 20 | } 21 | 22 | // querystring 23 | q := u.Query() 24 | for k, v := range e.QueryStringParameters { 25 | q.Set(k, v) 26 | } 27 | u.RawQuery = q.Encode() 28 | 29 | // base64 encoded body 30 | body := e.Body 31 | if e.IsBase64Encoded { 32 | b, err := base64.StdEncoding.DecodeString(body) 33 | if err != nil { 34 | return nil, errors.Wrap(err, "decoding base64 body") 35 | } 36 | body = string(b) 37 | } 38 | 39 | // new request 40 | req, err := http.NewRequest(e.HTTPMethod, u.String(), strings.NewReader(body)) 41 | if err != nil { 42 | return nil, errors.Wrap(err, "creating request") 43 | } 44 | 45 | // remote addr 46 | req.RemoteAddr = e.RequestContext.Identity.SourceIP 47 | 48 | // header fields 49 | for k, v := range e.Headers { 50 | req.Header.Set(k, v) 51 | } 52 | 53 | // content-length 54 | if req.Header.Get("Content-Length") == "" && body != "" { 55 | req.Header.Set("Content-Length", strconv.Itoa(len(body))) 56 | } 57 | 58 | // custom fields 59 | b, _ := json.Marshal(e.RequestContext) 60 | req.Header.Set("X-Context", string(b)) 61 | req.Header.Set("X-Request-Id", e.RequestContext.RequestID) 62 | req.Header.Set("X-Stage", e.RequestContext.Stage) 63 | 64 | // host 65 | req.URL.Host = req.Header.Get("Host") 66 | req.Host = req.URL.Host 67 | 68 | return req, nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | // Package metrics provides higher level CloudWatch metrics operations. 2 | package metrics 3 | 4 | import ( 5 | "time" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/service/cloudwatch" 9 | ) 10 | 11 | // Metrics helper. 12 | type Metrics struct { 13 | in cloudwatch.GetMetricStatisticsInput 14 | } 15 | 16 | // New metrics. 17 | func New() *Metrics { 18 | return &Metrics{} 19 | } 20 | 21 | // Namespace sets the namespace. 22 | func (m *Metrics) Namespace(name string) *Metrics { 23 | m.in.Namespace = &name 24 | return m 25 | } 26 | 27 | // Metric sets the metric name. 28 | func (m *Metrics) Metric(name string) *Metrics { 29 | m.in.MetricName = &name 30 | return m 31 | } 32 | 33 | // Stats sets the stats. 34 | func (m *Metrics) Stats(names []string) *Metrics { 35 | m.in.Statistics = aws.StringSlice(names) 36 | return m 37 | } 38 | 39 | // Stat adds the stat. 40 | func (m *Metrics) Stat(name string) *Metrics { 41 | m.in.Statistics = append(m.in.Statistics, &name) 42 | return m 43 | } 44 | 45 | // Dimension adds a dimension. 46 | func (m *Metrics) Dimension(name, value string) *Metrics { 47 | m.in.Dimensions = append(m.in.Dimensions, &cloudwatch.Dimension{ 48 | Name: &name, 49 | Value: &value, 50 | }) 51 | 52 | return m 53 | } 54 | 55 | // Period sets the period in seconds. 56 | func (m *Metrics) Period(seconds int) *Metrics { 57 | m.in.Period = aws.Int64(int64(seconds)) 58 | return m 59 | } 60 | 61 | // TimeRange sets the start and time times. 62 | func (m *Metrics) TimeRange(start, end time.Time) *Metrics { 63 | m.in.StartTime = &start 64 | m.in.EndTime = &end 65 | return m 66 | } 67 | 68 | // Params returns the API input. 69 | func (m *Metrics) Params() *cloudwatch.GetMetricStatisticsInput { 70 | return &m.in 71 | } 72 | -------------------------------------------------------------------------------- /http/gzip/gzip_test.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "compress/gzip" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/apex/up" 13 | "github.com/tj/assert" 14 | ) 15 | 16 | var body = strings.Repeat("так", 5000) 17 | 18 | var hello = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | fmt.Fprint(w, body) 20 | }) 21 | 22 | func TestGzip(t *testing.T) { 23 | c, err := up.ParseConfigString(`{ "name": "app" }`) 24 | assert.NoError(t, err, "config") 25 | 26 | h := New(c, hello) 27 | 28 | t.Run("accepts gzip", func(t *testing.T) { 29 | res := httptest.NewRecorder() 30 | req := httptest.NewRequest("GET", "/", nil) 31 | req.Header.Set("Accept-Encoding", "gzip") 32 | 33 | h.ServeHTTP(res, req) 34 | 35 | header := make(http.Header) 36 | header.Add("Content-Type", "text/plain; charset=utf-8") 37 | header.Add("Content-Encoding", "gzip") 38 | header.Add("Vary", "Accept-Encoding") 39 | 40 | assert.Equal(t, 200, res.Code) 41 | assert.Equal(t, header, res.HeaderMap) 42 | 43 | gz, err := gzip.NewReader(res.Body) 44 | assert.NoError(t, err, "reader") 45 | 46 | b, err := ioutil.ReadAll(gz) 47 | assert.NoError(t, err, "reading") 48 | assert.NoError(t, gz.Close(), "close") 49 | 50 | assert.Equal(t, body, string(b)) 51 | }) 52 | 53 | t.Run("accepts identity", func(t *testing.T) { 54 | res := httptest.NewRecorder() 55 | req := httptest.NewRequest("GET", "/", nil) 56 | 57 | h.ServeHTTP(res, req) 58 | 59 | header := make(http.Header) 60 | header.Add("Content-Type", "text/plain; charset=utf-8") 61 | header.Add("Vary", "Accept-Encoding") 62 | 63 | assert.Equal(t, 200, res.Code) 64 | assert.Equal(t, header, res.HeaderMap) 65 | 66 | assert.Equal(t, body, res.Body.String()) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /internal/errorpage/errorpage_test.go: -------------------------------------------------------------------------------- 1 | package errorpage 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | ) 9 | 10 | // load pages from dir. 11 | func load(t testing.TB, dir string) Pages { 12 | dir = filepath.Join("testdata", dir) 13 | pages, err := Load(dir) 14 | assert.NoError(t, err, "load") 15 | return pages 16 | } 17 | 18 | func TestPages_precedence(t *testing.T) { 19 | pages := load(t, ".") 20 | 21 | t.Run("code 500 match exact", func(t *testing.T) { 22 | p := pages.Match(500) 23 | assert.NotNil(t, p, "no match") 24 | 25 | html, err := p.Render(nil) 26 | assert.NoError(t, err) 27 | 28 | assert.Equal(t, "500 page.\n", html) 29 | }) 30 | 31 | t.Run("code 404 match exact", func(t *testing.T) { 32 | p := pages.Match(404) 33 | assert.NotNil(t, p, "no match") 34 | 35 | html, err := p.Render(nil) 36 | assert.NoError(t, err) 37 | 38 | assert.Equal(t, "404 page.\n", html) 39 | }) 40 | 41 | t.Run("code 200 match exact", func(t *testing.T) { 42 | p := pages.Match(200) 43 | assert.NotNil(t, p, "no match") 44 | 45 | html, err := p.Render(nil) 46 | assert.NoError(t, err) 47 | 48 | assert.Equal(t, "200 page.\n", html) 49 | }) 50 | 51 | t.Run("code 403 match range", func(t *testing.T) { 52 | p := pages.Match(403) 53 | assert.NotNil(t, p, "no match") 54 | 55 | html, err := p.Render(nil) 56 | assert.NoError(t, err) 57 | 58 | assert.Equal(t, "4xx page.\n", html) 59 | }) 60 | 61 | t.Run("502 match global", func(t *testing.T) { 62 | p := pages.Match(502) 63 | assert.NotNil(t, p, "no match") 64 | 65 | data := struct { 66 | StatusText string 67 | StatusCode int 68 | }{"Bad Gateway", 502} 69 | 70 | html, err := p.Render(data) 71 | assert.NoError(t, err) 72 | 73 | assert.Equal(t, "Bad Gateway - 502.\n", html) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /reporter/plain/plain.go: -------------------------------------------------------------------------------- 1 | // Package plain provides plain-text reporting for CI. 2 | package plain 3 | 4 | import ( 5 | "fmt" 6 | "time" 7 | 8 | "github.com/dustin/go-humanize" 9 | 10 | "github.com/apex/up/platform/event" 11 | ) 12 | 13 | // Report on events. 14 | func Report(events <-chan *event.Event) { 15 | r := reporter{ 16 | events: events, 17 | } 18 | 19 | r.Start() 20 | } 21 | 22 | // reporter struct. 23 | type reporter struct { 24 | events <-chan *event.Event 25 | } 26 | 27 | // complete log with duration. 28 | func (r *reporter) complete(name, value string, d time.Duration) { 29 | duration := fmt.Sprintf("(%s)", d.Round(time.Millisecond)) 30 | fmt.Printf(" %s %s %s\n", name+":", value, duration) 31 | } 32 | 33 | // log line. 34 | func (r *reporter) log(name, value string) { 35 | fmt.Printf(" %s %s\n", name+":", value) 36 | } 37 | 38 | // error line. 39 | func (r *reporter) error(name, value string) { 40 | fmt.Printf(" %s %s\n", name+":", value) 41 | } 42 | 43 | // Start handling events. 44 | func (r *reporter) Start() { 45 | for e := range r.events { 46 | switch e.Name { 47 | case "account.login.verify": 48 | r.log("verify", "Check your email for a confirmation link") 49 | case "account.login.verified": 50 | r.log("verify", "complete") 51 | case "hook": 52 | r.log("hook", e.String("name")) 53 | case "hook.complete": 54 | r.complete("hook", e.String("name"), e.Duration("duration")) 55 | case "platform.build.zip": 56 | s := fmt.Sprintf("%s files, %s", humanize.Comma(e.Int64("files")), humanize.Bytes(uint64(e.Int("size_compressed")))) 57 | r.complete("build", s, e.Duration("duration")) 58 | case "platform.deploy.complete": 59 | s := "complete" 60 | if v := e.String("version"); v != "" { 61 | s = "version " + v 62 | } 63 | r.complete("deploy", s, e.Duration("duration")) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /platform/lambda/stack/stack_test.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/service/cloudformation" 8 | "github.com/tj/assert" 9 | ) 10 | 11 | func TestResourcesCompleted(t *testing.T) { 12 | resources := []*cloudformation.StackResource{ 13 | { 14 | LogicalResourceId: aws.String("DnsZoneSomethingComRecordApiSomethingCom"), 15 | PhysicalResourceId: aws.String("api.something.com"), 16 | ResourceStatus: aws.String("CREATE_IN_PROGRESS"), 17 | ResourceStatusReason: aws.String("Resource creation Initiated"), 18 | ResourceType: aws.String("AWS::Route53::RecordSet"), 19 | StackId: aws.String("arn:aws:cloudformation:us-west-2:foobarbaz:stack/app/ad3af570-8511-11e7-8832-50d5ca789e4a"), 20 | StackName: aws.String("app"), 21 | }, 22 | { 23 | LogicalResourceId: aws.String("ApiProxyMethod"), 24 | PhysicalResourceId: aws.String("app-ApiProx-33K7PKBL7HNI"), 25 | ResourceStatus: aws.String("CREATE_COMPLETE"), 26 | ResourceType: aws.String("AWS::ApiGateway::Method"), 27 | StackId: aws.String("arn:aws:cloudformation:us-west-2:foobarbaz:stack/app/ad3af570-8511-11e7-8832-50d5ca789e4a"), 28 | StackName: aws.String("app"), 29 | }, 30 | { 31 | LogicalResourceId: aws.String("Another"), 32 | ResourceStatus: aws.String("CREATE_COMPLETE"), 33 | ResourceType: aws.String("AWS::ApiGateway::Method"), 34 | StackId: aws.String("arn:aws:cloudformation:us-west-2:foobarbaz:stack/app/ad3af570-8511-11e7-8832-50d5ca789e4a"), 35 | StackName: aws.String("app"), 36 | }, 37 | } 38 | 39 | states := map[string]Status{ 40 | "DnsZoneSomethingComRecordApiSomethingCom": CreateComplete, 41 | "app-ApiProx-33K7PKBL7HNI": CreateComplete, 42 | } 43 | 44 | c := resourcesCompleted(resources, states) 45 | assert.Len(t, c, 1) 46 | } 47 | -------------------------------------------------------------------------------- /handler/handler.go: -------------------------------------------------------------------------------- 1 | // Package handler provides what is essentially the core of Up's 2 | // reverse proxy, complete with all middleware for handling 3 | // logging, redirectcs, static file serving and so on. 4 | package handler 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/pkg/errors" 10 | 11 | "github.com/apex/up" 12 | "github.com/apex/up/http/cors" 13 | "github.com/apex/up/http/errorpages" 14 | "github.com/apex/up/http/gzip" 15 | "github.com/apex/up/http/headers" 16 | "github.com/apex/up/http/inject" 17 | "github.com/apex/up/http/logs" 18 | "github.com/apex/up/http/poweredby" 19 | "github.com/apex/up/http/redirects" 20 | "github.com/apex/up/http/relay" 21 | "github.com/apex/up/http/robots" 22 | "github.com/apex/up/http/static" 23 | ) 24 | 25 | // FromConfig returns the handler based on user config. 26 | func FromConfig(c *up.Config) (http.Handler, error) { 27 | switch c.Type { 28 | case "server": 29 | return relay.New(c) 30 | case "static": 31 | return static.New(c), nil 32 | default: 33 | return nil, errors.Errorf("unknown .type %q", c.Type) 34 | } 35 | } 36 | 37 | // New handler complete with all Up middleware. 38 | func New(c *up.Config, h http.Handler) (http.Handler, error) { 39 | h = poweredby.New("up", h) 40 | h = robots.New(c, h) 41 | h = static.NewDynamic(c, h) 42 | 43 | h, err := headers.New(c, h) 44 | if err != nil { 45 | return nil, errors.Wrap(err, "headers") 46 | } 47 | 48 | h = cors.New(c, h) 49 | 50 | h, err = errorpages.New(c, h) 51 | if err != nil { 52 | return nil, errors.Wrap(err, "error pages") 53 | } 54 | 55 | h, err = inject.New(c, h) 56 | if err != nil { 57 | return nil, errors.Wrap(err, "inject") 58 | } 59 | 60 | h, err = redirects.New(c, h) 61 | if err != nil { 62 | return nil, errors.Wrap(err, "redirects") 63 | } 64 | 65 | h = gzip.New(c, h) 66 | 67 | h, err = logs.New(c, h) 68 | if err != nil { 69 | return nil, errors.Wrap(err, "logs") 70 | } 71 | 72 | return h, nil 73 | } 74 | -------------------------------------------------------------------------------- /config/hooks.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | // Hook is one or more commands. 9 | type Hook []string 10 | 11 | // Hooks for the project. 12 | type Hooks struct { 13 | Build Hook `json:"build"` 14 | Clean Hook `json:"clean"` 15 | PreBuild Hook `json:"prebuild"` 16 | PostBuild Hook `json:"postbuild"` 17 | PreDeploy Hook `json:"predeploy"` 18 | PostDeploy Hook `json:"postdeploy"` 19 | } 20 | 21 | // Override config. 22 | func (h *Hooks) Override(c *Config) { 23 | if v := h.Build; v != nil { 24 | c.Hooks.Build = v 25 | } 26 | 27 | if v := h.Clean; v != nil { 28 | c.Hooks.Clean = v 29 | } 30 | 31 | if v := h.PreBuild; v != nil { 32 | c.Hooks.PreBuild = v 33 | } 34 | 35 | if v := h.PostBuild; v != nil { 36 | c.Hooks.PostBuild = v 37 | } 38 | 39 | if v := h.PreDeploy; v != nil { 40 | c.Hooks.PreDeploy = v 41 | } 42 | 43 | if v := h.PostDeploy; v != nil { 44 | c.Hooks.PostDeploy = v 45 | } 46 | } 47 | 48 | // Get returns the hook by name or nil. 49 | func (h *Hooks) Get(s string) Hook { 50 | switch s { 51 | case "build": 52 | return h.Build 53 | case "clean": 54 | return h.Clean 55 | case "prebuild": 56 | return h.PreBuild 57 | case "postbuild": 58 | return h.PostBuild 59 | case "predeploy": 60 | return h.PreDeploy 61 | case "postdeploy": 62 | return h.PostDeploy 63 | default: 64 | return nil 65 | } 66 | } 67 | 68 | // UnmarshalJSON implementation. 69 | func (h *Hook) UnmarshalJSON(b []byte) error { 70 | switch b[0] { 71 | case '"': 72 | var s string 73 | if err := json.Unmarshal(b, &s); err != nil { 74 | return err 75 | } 76 | *h = append(*h, s) 77 | return nil 78 | case '[': 79 | return json.Unmarshal(b, (*[]string)(h)) 80 | default: 81 | return errors.New("hook must be a string or array of strings") 82 | } 83 | } 84 | 85 | // IsEmpty returns true if the hook is empty. 86 | func (h *Hook) IsEmpty() bool { 87 | return h == nil || len(*h) == 0 88 | } 89 | -------------------------------------------------------------------------------- /http/headers/headers.go: -------------------------------------------------------------------------------- 1 | // Package headers provides header injection support. 2 | package headers 3 | 4 | import ( 5 | "net/http" 6 | "os" 7 | 8 | "github.com/apex/log" 9 | "github.com/pkg/errors" 10 | hdr "github.com/tj/go-headers" 11 | 12 | "github.com/apex/up" 13 | "github.com/apex/up/internal/header" 14 | ) 15 | 16 | // TODO: document precedence and/or add options 17 | // TODO: maybe allow storing _headers in Static.Dir? 18 | 19 | // filename of headers file. 20 | var filename = "_headers" 21 | 22 | // New headers handler. 23 | func New(c *up.Config, next http.Handler) (http.Handler, error) { 24 | rulesFromFile, err := readFromFile(filename) 25 | if err != nil { 26 | return nil, errors.Wrap(err, "reading header file") 27 | } 28 | 29 | rules, err := header.Compile(header.Merge(rulesFromFile, c.Headers)) 30 | if err != nil { 31 | return nil, errors.Wrap(err, "compiling header") 32 | } 33 | 34 | log.Debugf("header rules from _headers file: %d", len(rulesFromFile)) 35 | log.Debugf("header rules from up.json: %d", len(c.Headers)) 36 | 37 | h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 38 | fields := rules.Lookup(r.URL.Path) 39 | 40 | for k, v := range fields { 41 | w.Header().Set(k, v) 42 | } 43 | 44 | next.ServeHTTP(w, r) 45 | }) 46 | 47 | return h, nil 48 | } 49 | 50 | // readFromFile reads from a Netlify style headers file. 51 | func readFromFile(path string) (header.Rules, error) { 52 | rules := make(header.Rules) 53 | 54 | f, err := os.Open(path) 55 | 56 | if os.IsNotExist(err) { 57 | return nil, nil 58 | } 59 | 60 | if err != nil { 61 | return nil, errors.Wrap(err, "opening headers file") 62 | } 63 | 64 | defer f.Close() 65 | 66 | h, err := hdr.Parse(f) 67 | if err != nil { 68 | return nil, errors.Wrap(err, "parsing") 69 | } 70 | 71 | for path, fields := range h { 72 | rules[path] = make(header.Fields) 73 | for name, vals := range fields { 74 | rules[path][name] = vals[0] 75 | } 76 | } 77 | 78 | return rules, nil 79 | } 80 | -------------------------------------------------------------------------------- /internal/cli/url/url.go: -------------------------------------------------------------------------------- 1 | package url 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pkg/browser" 7 | "github.com/pkg/errors" 8 | "github.com/tj/go/clipboard" 9 | "github.com/tj/kingpin" 10 | 11 | "github.com/apex/up/internal/cli/root" 12 | "github.com/apex/up/internal/stats" 13 | "github.com/apex/up/internal/util" 14 | "github.com/apex/up/internal/validate" 15 | ) 16 | 17 | func init() { 18 | cmd := root.Command("url", "Show, open, or copy a stage endpoint.") 19 | 20 | cmd.Example(`up url`, "Show the staging endpoint.") 21 | cmd.Example(`up url --open`, "Open the staging endpoint in the browser.") 22 | cmd.Example(`up url --copy`, "Copy the staging endpoint to the clipboard.") 23 | cmd.Example(`up url -s production`, "Show the production endpoint.") 24 | cmd.Example(`up url -o -s production`, "Open the production endpoint in the browser.") 25 | cmd.Example(`up url -c -s production`, "Copy the production endpoint to the clipboard.") 26 | 27 | stage := cmd.Flag("stage", "Target stage name.").Short('s').Default("staging").String() 28 | open := cmd.Flag("open", "Open endpoint in the browser.").Short('o').Bool() 29 | copy := cmd.Flag("copy", "Copy endpoint to the clipboard.").Short('c').Bool() 30 | 31 | cmd.Action(func(_ *kingpin.ParseContext) error { 32 | c, p, err := root.Init() 33 | if err != nil { 34 | return errors.Wrap(err, "initializing") 35 | } 36 | 37 | region := c.Regions[0] 38 | 39 | stats.Track("URL", map[string]interface{}{ 40 | "region": region, 41 | "stage": *stage, 42 | "open": *open, 43 | "copy": *copy, 44 | }) 45 | 46 | if err := validate.List(*stage, c.Stages.RemoteNames()); err != nil { 47 | return err 48 | } 49 | 50 | url, err := p.URL(region, *stage) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | switch { 56 | case *open: 57 | browser.OpenURL(url) 58 | case *copy: 59 | clipboard.Write(url) 60 | util.LogPad("Copied to clipboard!") 61 | default: 62 | fmt.Println(url) 63 | } 64 | 65 | return nil 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /internal/validate/validate.go: -------------------------------------------------------------------------------- 1 | // Package validate provides config validation functions. 2 | package validate 3 | 4 | import ( 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // RequiredString validation. 12 | func RequiredString(s string) error { 13 | if strings.TrimSpace(s) == "" { 14 | return errors.New("is required") 15 | } 16 | 17 | return nil 18 | } 19 | 20 | // RequiredStrings validation. 21 | func RequiredStrings(s []string) error { 22 | for i, v := range s { 23 | if err := RequiredString(v); err != nil { 24 | return errors.Wrapf(err, "at index %d", i) 25 | } 26 | } 27 | 28 | return nil 29 | } 30 | 31 | // MinStrings validation. 32 | func MinStrings(s []string, n int) error { 33 | if len(s) < n { 34 | if n == 1 { 35 | return errors.Errorf("must have at least %d value", n) 36 | } 37 | 38 | return errors.Errorf("must have at least %d values", n) 39 | } 40 | 41 | return nil 42 | } 43 | 44 | // name regexp. 45 | var name = regexp.MustCompile(`^[a-z][-a-z0-9]*$`) 46 | 47 | // Name validation. 48 | func Name(s string) error { 49 | if !name.MatchString(s) { 50 | return errors.Errorf("must contain only lowercase alphanumeric characters and '-'") 51 | } 52 | 53 | return nil 54 | } 55 | 56 | // stage regexp. 57 | var stage = regexp.MustCompile(`^[a-zA-Z0-9_]+$`) 58 | 59 | // Stage name validation. 60 | func Stage(s string) error { 61 | if !stage.MatchString(s) { 62 | return errors.Errorf("must contain only alphanumeric characters and '_'") 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // List validation. 69 | func List(s string, list []string) error { 70 | for _, v := range list { 71 | if s == v { 72 | return nil 73 | } 74 | } 75 | 76 | return errors.Errorf("%q is invalid, must be one of:\n\n • %s", s, strings.Join(list, "\n • ")) 77 | } 78 | 79 | // Lists validation. 80 | func Lists(vals, list []string) error { 81 | for _, v := range vals { 82 | if err := List(v, list); err != nil { 83 | return err 84 | } 85 | } 86 | 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /internal/zip/zip.go: -------------------------------------------------------------------------------- 1 | package zip 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | archive "github.com/tj/go-archive" 12 | ) 13 | 14 | var transform = archive.TransformFunc(func(r io.Reader, i os.FileInfo) (io.Reader, os.FileInfo) { 15 | name := strings.Replace(i.Name(), "\\", "/", -1) 16 | 17 | i = archive.Info{ 18 | Name: name, 19 | Size: i.Size(), 20 | Mode: i.Mode() | 0555, 21 | Modified: i.ModTime(), 22 | Dir: i.IsDir(), 23 | }.FileInfo() 24 | 25 | return r, i 26 | }) 27 | 28 | // Build the given `dir`. 29 | func Build(dir string) (io.ReadCloser, *archive.Stats, error) { 30 | upignore, err := read(".upignore") 31 | if err != nil { 32 | return nil, nil, errors.Wrap(err, "reading .upignore") 33 | } 34 | defer upignore.Close() 35 | 36 | r := io.MultiReader( 37 | strings.NewReader(".*\n"), 38 | strings.NewReader("\n!vendor\n!node_modules/**\n!.pypath/**\n"), 39 | upignore, 40 | strings.NewReader("\n!main\n!server\n!_proxy.js\n!up.json\n!pom.xml\n!build.gradle\n!project.clj\ngin-bin\nup\n")) 41 | 42 | filter, err := archive.FilterPatterns(r) 43 | if err != nil { 44 | return nil, nil, errors.Wrap(err, "parsing ignore patterns") 45 | } 46 | 47 | buf := new(bytes.Buffer) 48 | zip := archive.NewZip(buf). 49 | WithFilter(filter). 50 | WithTransform(transform) 51 | 52 | if err := zip.Open(); err != nil { 53 | return nil, nil, errors.Wrap(err, "opening") 54 | } 55 | 56 | if err := zip.AddDir(dir); err != nil { 57 | return nil, nil, errors.Wrap(err, "adding dir") 58 | } 59 | 60 | if err := zip.Close(); err != nil { 61 | return nil, nil, errors.Wrap(err, "closing") 62 | } 63 | 64 | return ioutil.NopCloser(buf), zip.Stats(), nil 65 | } 66 | 67 | // read file. 68 | func read(path string) (io.ReadCloser, error) { 69 | f, err := os.Open(path) 70 | 71 | if os.IsNotExist(err) { 72 | return ioutil.NopCloser(bytes.NewReader(nil)), nil 73 | } 74 | 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | return f, nil 80 | } 81 | -------------------------------------------------------------------------------- /cmd/up/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "runtime" 7 | 8 | "github.com/stripe/stripe-go" 9 | "github.com/tj/go/env" 10 | "github.com/tj/go/term" 11 | 12 | // commands 13 | _ "github.com/apex/up/internal/cli/build" 14 | _ "github.com/apex/up/internal/cli/config" 15 | _ "github.com/apex/up/internal/cli/deploy" 16 | _ "github.com/apex/up/internal/cli/disable-stats" 17 | _ "github.com/apex/up/internal/cli/docs" 18 | _ "github.com/apex/up/internal/cli/domains" 19 | _ "github.com/apex/up/internal/cli/logs" 20 | _ "github.com/apex/up/internal/cli/metrics" 21 | _ "github.com/apex/up/internal/cli/prune" 22 | _ "github.com/apex/up/internal/cli/run" 23 | _ "github.com/apex/up/internal/cli/stack" 24 | _ "github.com/apex/up/internal/cli/start" 25 | _ "github.com/apex/up/internal/cli/team" 26 | _ "github.com/apex/up/internal/cli/upgrade" 27 | _ "github.com/apex/up/internal/cli/url" 28 | _ "github.com/apex/up/internal/cli/version" 29 | 30 | "github.com/apex/up/internal/cli/app" 31 | "github.com/apex/up/internal/signal" 32 | "github.com/apex/up/internal/stats" 33 | "github.com/apex/up/internal/util" 34 | ) 35 | 36 | var version = "master" 37 | 38 | func main() { 39 | signal.Add(reset) 40 | stripe.Key = env.GetDefault("STRIPE_KEY", "pk_live_23pGrHcZ2QpfX525XYmiyzmx") 41 | stripe.LogLevel = 0 42 | 43 | err := run() 44 | 45 | if err == nil { 46 | return 47 | } 48 | 49 | term.ShowCursor() 50 | 51 | switch { 52 | case util.IsNoCredentials(err): 53 | util.Fatal(errors.New("Cannot find credentials, visit https://apex.sh/docs/up/credentials/ for help.")) 54 | default: 55 | util.Fatal(err) 56 | } 57 | } 58 | 59 | // run the cli. 60 | func run() error { 61 | stats.SetProperties(map[string]interface{}{ 62 | "os": runtime.GOOS, 63 | "arch": runtime.GOARCH, 64 | "version": version, 65 | "ci": os.Getenv("CI") == "true" || os.Getenv("CI") == "1", 66 | }) 67 | 68 | return app.Run(version) 69 | } 70 | 71 | // reset cursor. 72 | func reset() error { 73 | term.ShowCursor() 74 | println() 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /config/lambda.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // defaultRuntime is the default runtime. 4 | var defaultRuntime = "nodejs10.x" 5 | 6 | // defaultPolicy is the default function role policy. 7 | var defaultPolicy = IAMPolicyStatement{ 8 | "Effect": "Allow", 9 | "Resource": "*", 10 | "Action": []string{ 11 | "logs:CreateLogGroup", 12 | "logs:CreateLogStream", 13 | "logs:PutLogEvents", 14 | "ssm:GetParametersByPath", 15 | "ec2:CreateNetworkInterface", 16 | "ec2:DescribeNetworkInterfaces", 17 | "ec2:DeleteNetworkInterface", 18 | }, 19 | } 20 | 21 | // IAMPolicyStatement configuration. 22 | type IAMPolicyStatement map[string]interface{} 23 | 24 | // VPC configuration. 25 | type VPC struct { 26 | Subnets []string `json:"subnets"` 27 | SecurityGroups []string `json:"security_groups"` 28 | } 29 | 30 | // Lambda configuration. 31 | type Lambda struct { 32 | // Memory of the function. 33 | Memory int `json:"memory"` 34 | 35 | // Timeout of the function. 36 | Timeout int `json:"timeout"` 37 | 38 | // Role of the function. 39 | Role string `json:"role"` 40 | 41 | // Runtime of the function. 42 | Runtime string `json:"runtime"` 43 | 44 | // Policy of the function role. 45 | Policy []IAMPolicyStatement `json:"policy"` 46 | 47 | // VPC configuration. 48 | VPC *VPC `json:"vpc"` 49 | } 50 | 51 | // Default implementation. 52 | func (l *Lambda) Default() error { 53 | if l.Timeout == 0 { 54 | l.Timeout = 60 55 | } 56 | 57 | if l.Memory == 0 { 58 | l.Memory = 512 59 | } 60 | 61 | if l.Runtime == "" { 62 | l.Runtime = defaultRuntime 63 | } 64 | 65 | l.Policy = append(l.Policy, defaultPolicy) 66 | 67 | return nil 68 | } 69 | 70 | // Validate implementation. 71 | func (l *Lambda) Validate() error { 72 | return nil 73 | } 74 | 75 | // Override config. 76 | func (l *Lambda) Override(c *Config) { 77 | if l.Memory != 0 { 78 | c.Lambda.Memory = l.Memory 79 | } 80 | 81 | if l.Timeout != 0 { 82 | c.Lambda.Timeout = l.Timeout 83 | } 84 | 85 | if l.Role != "" { 86 | c.Lambda.Role = l.Role 87 | } 88 | 89 | if l.VPC != nil { 90 | c.Lambda.VPC = l.VPC 91 | } 92 | 93 | if l.Runtime != "" { 94 | c.Lambda.Runtime = l.Runtime 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /internal/logs/writer/writer.go: -------------------------------------------------------------------------------- 1 | // Package writer provides an io.Writer for capturing 2 | // process output as logs, so that stdout may become 3 | // INFO, and stderr ERROR. 4 | package writer 5 | 6 | import ( 7 | "bufio" 8 | "bytes" 9 | "encoding/json" 10 | 11 | "github.com/apex/log" 12 | "github.com/apex/up/internal/util" 13 | ) 14 | 15 | // Writer struct. 16 | type Writer struct { 17 | log log.Interface 18 | level log.Level 19 | } 20 | 21 | // New writer with the given log level. 22 | func New(l log.Level, ctx log.Interface) *Writer { 23 | return &Writer{ 24 | log: ctx, 25 | level: l, 26 | } 27 | } 28 | 29 | // Write implementation. 30 | func (w *Writer) Write(b []byte) (int, error) { 31 | s := bufio.NewScanner(bytes.NewReader(b)) 32 | 33 | for s.Scan() { 34 | if err := w.write(s.Text()); err != nil { 35 | return 0, err 36 | } 37 | } 38 | 39 | if err := s.Err(); err != nil { 40 | return 0, err 41 | } 42 | 43 | return len(b), nil 44 | } 45 | 46 | // write the line. 47 | func (w *Writer) write(s string) error { 48 | if util.IsJSONLog(s) { 49 | return w.writeJSON(s) 50 | } 51 | 52 | return w.writeText(s) 53 | } 54 | 55 | // writeJSON writes a json log, interpreting it as a log.Entry. 56 | func (w *Writer) writeJSON(s string) error { 57 | // TODO: make this less ugly in apex/log, 58 | // you should be able to write an arbitrary Entry. 59 | var e log.Entry 60 | 61 | if err := json.Unmarshal([]byte(s), &e); err != nil { 62 | return w.writeText(s) 63 | } 64 | 65 | switch e.Level { 66 | case log.DebugLevel: 67 | w.log.WithFields(e.Fields).Debug(e.Message) 68 | case log.InfoLevel: 69 | w.log.WithFields(e.Fields).Info(e.Message) 70 | case log.WarnLevel: 71 | w.log.WithFields(e.Fields).Warn(e.Message) 72 | case log.ErrorLevel: 73 | w.log.WithFields(e.Fields).Error(e.Message) 74 | case log.FatalLevel: 75 | // TODO: FATAL without exit... 76 | w.log.WithFields(e.Fields).Error(e.Message) 77 | } 78 | 79 | return nil 80 | } 81 | 82 | // writeText writes plain text. 83 | func (w *Writer) writeText(s string) error { 84 | switch w.level { 85 | case log.InfoLevel: 86 | w.log.Info(s) 87 | case log.ErrorLevel: 88 | w.log.Error(s) 89 | } 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /internal/logs/writer/writer_test.go: -------------------------------------------------------------------------------- 1 | package writer 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/apex/log" 11 | "github.com/apex/log/handlers/json" 12 | "github.com/tj/assert" 13 | ) 14 | 15 | func init() { 16 | log.Now = func() time.Time { 17 | return time.Unix(0, 0).UTC() 18 | } 19 | } 20 | 21 | func TestWriter_plainTextFlat(t *testing.T) { 22 | var buf bytes.Buffer 23 | 24 | log.SetHandler(json.New(&buf)) 25 | 26 | w := New(log.InfoLevel, log.Log) 27 | 28 | input := `GET / 29 | GET /account 30 | GET /login 31 | POST / 32 | POST /logout 33 | ` 34 | 35 | _, err := io.Copy(w, strings.NewReader(input)) 36 | assert.NoError(t, err, "copy") 37 | 38 | expected := `{"fields":{},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"GET /"} 39 | {"fields":{},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"GET /account"} 40 | {"fields":{},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"GET /login"} 41 | {"fields":{},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"POST /"} 42 | {"fields":{},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"POST /logout"} 43 | ` 44 | 45 | assert.Equal(t, expected, buf.String()) 46 | } 47 | 48 | func TestWriter_json(t *testing.T) { 49 | var buf bytes.Buffer 50 | 51 | log.SetHandler(json.New(&buf)) 52 | 53 | w := New(log.InfoLevel, log.Log) 54 | 55 | input := `{ "level": "info", "message": "request", "fields": { "method": "GET", "path": "/" } } 56 | { "level": "info", "message": "request", "fields": { "method": "GET", "path": "/login" } } 57 | { "level": "info", "message": "request", "fields": { "method": "POST", "path": "/login" } } 58 | ` 59 | 60 | _, err := io.Copy(w, strings.NewReader(input)) 61 | assert.NoError(t, err, "copy") 62 | 63 | expected := `{"fields":{"method":"GET","path":"/"},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"request"} 64 | {"fields":{"method":"GET","path":"/login"},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"request"} 65 | {"fields":{"method":"POST","path":"/login"},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"request"} 66 | ` 67 | 68 | assert.Equal(t, expected, buf.String()) 69 | } 70 | -------------------------------------------------------------------------------- /platform/lambda/prune.go: -------------------------------------------------------------------------------- 1 | package lambda 2 | 3 | import ( 4 | "sort" 5 | "time" 6 | 7 | "github.com/apex/log" 8 | "github.com/apex/up/platform/event" 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | "github.com/aws/aws-sdk-go/service/s3" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // Prune implementation. 16 | func (p *Platform) Prune(region, stage string, versions int) error { 17 | p.events.Emit("prune", nil) 18 | 19 | if err := p.createRole(); err != nil { 20 | return errors.Wrap(err, "creating iam role") 21 | } 22 | 23 | s := s3.New(session.New(aws.NewConfig().WithRegion(region))) 24 | b := aws.String(p.getS3BucketName(region)) 25 | prefix := p.config.Name + "/" + stage + "/" 26 | 27 | params := &s3.ListObjectsInput{ 28 | Bucket: b, 29 | Prefix: &prefix, 30 | } 31 | 32 | start := time.Now() 33 | var objects []*s3.Object 34 | var count int 35 | var size int64 36 | 37 | // fetch objects 38 | err := s.ListObjectsPages(params, func(page *s3.ListObjectsOutput, lastPage bool) bool { 39 | for _, o := range page.Contents { 40 | objects = append(objects, o) 41 | } 42 | return *page.IsTruncated 43 | }) 44 | 45 | if err != nil { 46 | return errors.Wrap(err, "listing s3 objects") 47 | } 48 | 49 | // sort by time descending 50 | sort.Slice(objects, func(i int, j int) bool { 51 | a := objects[i] 52 | b := objects[j] 53 | return (*b).LastModified.Before(*a.LastModified) 54 | }) 55 | 56 | // remove old versions 57 | for i, o := range objects { 58 | ctx := log.WithFields(log.Fields{ 59 | "index": i, 60 | "key": *o.Key, 61 | "size": *o.Size, 62 | "last_modified": *o.LastModified, 63 | }) 64 | 65 | if i < versions { 66 | ctx.Debug("retain") 67 | continue 68 | } 69 | 70 | ctx.Debug("remove") 71 | size += *o.Size 72 | count++ 73 | 74 | _, err := s.DeleteObject(&s3.DeleteObjectInput{ 75 | Bucket: b, 76 | Key: o.Key, 77 | }) 78 | 79 | if err != nil { 80 | return errors.Wrap(err, "removing object") 81 | } 82 | } 83 | 84 | p.events.Emit("prune.complete", event.Fields{ 85 | "duration": time.Since(start), 86 | "size": size, 87 | "count": count, 88 | }) 89 | 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /platform/aws/cost/cost.go: -------------------------------------------------------------------------------- 1 | // Package cost provides utilities for calculating AWS Lambda pricing. 2 | package cost 3 | 4 | // pricePerInvoke is the cost per function invocation. 5 | var pricePerInvoke = 0.0000002 6 | 7 | // pricePerRequestUnit is the cost per api gateway request unit. 8 | var pricePerRequestUnit = 5 9 | 10 | // requestUnit is 5 million requests. 11 | var requestUnit = 5e6 12 | 13 | // memoryConfigurations available. 14 | var memoryConfigurations = map[int]float64{ 15 | 128: 0.000000208, 16 | 192: 0.000000313, 17 | 256: 0.000000417, 18 | 320: 0.000000521, 19 | 384: 0.000000625, 20 | 448: 0.000000729, 21 | 512: 0.000000834, 22 | 576: 0.000000938, 23 | 640: 0.000001042, 24 | 704: 0.000001146, 25 | 768: 0.00000125, 26 | 832: 0.000001354, 27 | 896: 0.000001459, 28 | 960: 0.000001563, 29 | 1024: 0.000001667, 30 | 1088: 0.000001771, 31 | 1152: 0.000001875, 32 | 1216: 0.00000198, 33 | 1280: 0.000002084, 34 | 1344: 0.000002188, 35 | 1408: 0.000002292, 36 | 1472: 0.000002396, 37 | 1536: 0.000002501, 38 | 1600: 0.000002605, 39 | 1664: 0.000002709, 40 | 1728: 0.000002813, 41 | 1792: 0.000002917, 42 | 1856: 0.000003021, 43 | 1920: 0.000003126, 44 | 1984: 0.000003230, 45 | 2048: 0.000003334, 46 | 2112: 0.000003438, 47 | 2176: 0.000003542, 48 | 2240: 0.000003647, 49 | 2304: 0.000003751, 50 | 2368: 0.000003855, 51 | 2432: 0.000003959, 52 | 2496: 0.000004063, 53 | 2560: 0.000004168, 54 | 2624: 0.000004272, 55 | 2688: 0.000004376, 56 | 2752: 0.000004480, 57 | 2816: 0.000004584, 58 | 2880: 0.000004688, 59 | 2944: 0.000004793, 60 | 3008: 0.000004897, 61 | } 62 | 63 | // Requests returns the cost for the given number of http requests. 64 | func Requests(n int) float64 { 65 | return (float64(n) / float64(requestUnit)) * float64(pricePerRequestUnit) 66 | } 67 | 68 | // Rate returns the cost per 100ms for the given `memory` configuration in megabytes. 69 | func Rate(memory int) float64 { 70 | return memoryConfigurations[memory] 71 | } 72 | 73 | // Invocations returns the cost of `n` requests. 74 | func Invocations(n int) float64 { 75 | return pricePerInvoke * float64(n) 76 | } 77 | 78 | // Duration returns the cost of `ms` for the given `memory` configuration in megabytes. 79 | func Duration(ms, memory int) float64 { 80 | return Rate(memory) * (float64(ms) / 100) 81 | } 82 | -------------------------------------------------------------------------------- /internal/proxy/event.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | // Identity is the identity information associated with the request. 4 | type Identity struct { 5 | APIKey string `json:"apiKey"` 6 | AccountID string `json:"accountId"` 7 | UserAgent string `json:"userAgent"` 8 | SourceIP string `json:"sourceIp"` 9 | AccessKey string `json:"accessKey"` 10 | Caller string `json:"caller"` 11 | User string `json:"user"` 12 | UserARN string `json:"userARN"` 13 | CognitoIdentityID string `json:"cognitoIdentityId"` 14 | CognitoIdentityPoolID string `json:"cognitoIdentityPoolId"` 15 | CognitoAuthenticationType string `json:"cognitoAuthenticationType"` 16 | CognitoAuthenticationProvider string `json:"cognitoAuthenticationProvider"` 17 | } 18 | 19 | // RequestContext is the contextual information provided by API Gateway. 20 | type RequestContext struct { 21 | APIID string `json:"apiId"` 22 | ResourceID string `json:"resourceId"` 23 | RequestID string `json:"requestId"` 24 | HTTPMethod string `json:"-"` 25 | ResourcePath string `json:"-"` 26 | AccountID string `json:"accountId"` 27 | Stage string `json:"stage"` 28 | Identity Identity `json:"identity"` 29 | Authorizer map[string]interface{} `json:"authorizer"` 30 | } 31 | 32 | // Input is the input provided by API Gateway. 33 | type Input struct { 34 | HTTPMethod string 35 | Headers map[string]string 36 | Resource string 37 | PathParameters map[string]string 38 | Path string 39 | QueryStringParameters map[string]string 40 | Body string 41 | IsBase64Encoded bool 42 | StageVariables map[string]string 43 | RequestContext RequestContext 44 | } 45 | 46 | // Output is the output expected by API Gateway. 47 | type Output struct { 48 | StatusCode int `json:"statusCode"` 49 | Headers map[string]string `json:"headers,omitempty"` 50 | Body string `json:"body,omitempty"` 51 | IsBase64Encoded bool `json:"isBase64Encoded"` 52 | } 53 | -------------------------------------------------------------------------------- /platform/lambda/lambda_test.go: -------------------------------------------------------------------------------- 1 | package lambda 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/service/acm" 8 | "github.com/tj/assert" 9 | 10 | "github.com/apex/up/config" 11 | "github.com/apex/up/platform/event" 12 | ) 13 | 14 | func TestGetCert(t *testing.T) { 15 | certs := []*acm.CertificateDetail{ 16 | { 17 | DomainName: aws.String("example.com"), 18 | CertificateArn: aws.String("arn:example.com"), 19 | SubjectAlternativeNames: aws.StringSlice([]string{ 20 | "*.example.com", 21 | }), 22 | }, 23 | { 24 | DomainName: aws.String("*.apex.sh"), 25 | CertificateArn: aws.String("arn:*.apex.sh"), 26 | }, 27 | { 28 | DomainName: aws.String("api.example.com"), 29 | CertificateArn: aws.String("arn:api.example.com"), 30 | SubjectAlternativeNames: aws.StringSlice([]string{ 31 | "*.api.example.com", 32 | "something.example.com", 33 | }), 34 | }, 35 | } 36 | 37 | arn := getCert(certs, "example.com") 38 | assert.Equal(t, "arn:example.com", arn) 39 | 40 | arn = getCert(certs, "www.example.com") 41 | assert.Equal(t, "arn:example.com", arn) 42 | 43 | arn = getCert(certs, "api.example.com") 44 | assert.Equal(t, "arn:api.example.com", arn) 45 | 46 | arn = getCert(certs, "apex.sh") 47 | assert.Empty(t, arn) 48 | 49 | arn = getCert(certs, "api.apex.sh") 50 | assert.Equal(t, "arn:*.apex.sh", arn) 51 | 52 | arn = getCert(certs, "v1.api.example.com") 53 | assert.Equal(t, "arn:api.example.com", arn) 54 | 55 | arn = getCert(certs, "something.example.com") 56 | assert.Equal(t, "arn:api.example.com", arn) 57 | 58 | arn = getCert(certs, "staging.v1.api.example.com") 59 | assert.Empty(t, arn) 60 | } 61 | 62 | func TestCreateRole(t *testing.T) { 63 | t.Run("doesn't attempt to create configured role", func(t *testing.T) { 64 | c := &config.Config{ 65 | Lambda: config.Lambda{ 66 | Role: "custom-role-name", 67 | }, 68 | } 69 | events := make(event.Events) 70 | p := New(c, events) 71 | assert.NoError(t, p.createRole(), "createRole") 72 | }) 73 | } 74 | 75 | func TestDeleteRole(t *testing.T) { 76 | t.Run("doesn't attempt to delete configured role", func(t *testing.T) { 77 | c := &config.Config{ 78 | Lambda: config.Lambda{ 79 | Role: "custom-role-name", 80 | }, 81 | } 82 | events := make(event.Events) 83 | p := New(c, events) 84 | assert.NoError(t, p.deleteRole("us-west-2"), "deleteRole") 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /internal/proxy/request_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | 8 | "github.com/tj/assert" 9 | ) 10 | 11 | func TestNewRequest(t *testing.T) { 12 | t.Run("GET", func(t *testing.T) { 13 | var in Input 14 | err := json.Unmarshal([]byte(getEvent), &in) 15 | assert.NoError(t, err, "unmarshal") 16 | 17 | req, err := NewRequest(&in) 18 | assert.NoError(t, err, "new request") 19 | 20 | assert.Equal(t, "GET", req.Method) 21 | assert.Equal(t, "apex-ping.com", req.Host) 22 | assert.Equal(t, "/pets/tobi", req.URL.Path) 23 | assert.Equal(t, "format=json", req.URL.Query().Encode()) 24 | assert.Equal(t, "207.102.57.26", req.RemoteAddr) 25 | }) 26 | 27 | t.Run("POST", func(t *testing.T) { 28 | var in Input 29 | err := json.Unmarshal([]byte(postEvent), &in) 30 | assert.NoError(t, err, "unmarshal") 31 | 32 | req, err := NewRequest(&in) 33 | assert.NoError(t, err, "new request") 34 | 35 | assert.Equal(t, "POST", req.Method) 36 | assert.Equal(t, "apex-ping.com", req.Host) 37 | assert.Equal(t, "/pets/tobi", req.URL.Path) 38 | assert.Equal(t, "", req.URL.Query().Encode()) 39 | assert.Equal(t, "207.102.57.26", req.RemoteAddr) 40 | 41 | b, err := ioutil.ReadAll(req.Body) 42 | assert.NoError(t, err, "read body") 43 | 44 | assert.Equal(t, `{ "name": "Tobi" }`, string(b)) 45 | }) 46 | 47 | t.Run("POST binary", func(t *testing.T) { 48 | var in Input 49 | err := json.Unmarshal([]byte(postEventBinary), &in) 50 | assert.NoError(t, err, "unmarshal") 51 | 52 | req, err := NewRequest(&in) 53 | assert.NoError(t, err, "new request") 54 | 55 | assert.Equal(t, "POST", req.Method) 56 | assert.Equal(t, "/pets/tobi", req.URL.Path) 57 | assert.Equal(t, "", req.URL.Query().Encode()) 58 | assert.Equal(t, "207.102.57.26", req.RemoteAddr) 59 | 60 | b, err := ioutil.ReadAll(req.Body) 61 | assert.NoError(t, err, "read body") 62 | 63 | assert.Equal(t, `Hello World`, string(b)) 64 | }) 65 | 66 | t.Run("Basic Auth", func(t *testing.T) { 67 | var in Input 68 | err := json.Unmarshal([]byte(getEventBasicAuth), &in) 69 | assert.NoError(t, err, "unmarshal") 70 | 71 | req, err := NewRequest(&in) 72 | assert.NoError(t, err, "new request") 73 | 74 | assert.Equal(t, "GET", req.Method) 75 | assert.Equal(t, "/pets/tobi", req.URL.Path) 76 | user, pass, ok := req.BasicAuth() 77 | assert.Equal(t, "tobi", user) 78 | assert.Equal(t, "ferret", pass) 79 | assert.True(t, ok) 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /internal/cli/build/build.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | "sort" 10 | 11 | "github.com/dustin/go-humanize" 12 | "github.com/pkg/errors" 13 | "github.com/tj/go/term" 14 | "github.com/tj/kingpin" 15 | 16 | "github.com/apex/up/internal/cli/root" 17 | "github.com/apex/up/internal/colors" 18 | "github.com/apex/up/internal/stats" 19 | "github.com/apex/up/internal/util" 20 | ) 21 | 22 | func init() { 23 | cmd := root.Command("build", "Build zip file.") 24 | cmd.Example(`up build`, "Build archive and save to ./out.zip") 25 | cmd.Example(`up build > /tmp/out.zip`, "Build archive and output to file via stdout.") 26 | cmd.Example(`up build --size`, "Build archive and list files by size.") 27 | 28 | stage := cmd.Flag("stage", "Target stage name.").Short('s').Default("staging").String() 29 | size := cmd.Flag("size", "Show zip contents size information.").Bool() 30 | 31 | cmd.Action(func(_ *kingpin.ParseContext) error { 32 | defer util.Pad()() 33 | 34 | _, p, err := root.Init() 35 | if err != nil { 36 | return errors.Wrap(err, "initializing") 37 | } 38 | 39 | stats.Track("Build", nil) 40 | 41 | if err := p.Init(*stage); err != nil { 42 | return errors.Wrap(err, "initializing") 43 | } 44 | 45 | if err := p.Build(true); err != nil { 46 | return errors.Wrap(err, "building") 47 | } 48 | 49 | r, err := p.Zip() 50 | if err != nil { 51 | return errors.Wrap(err, "zip") 52 | } 53 | 54 | var out io.Writer 55 | var buf bytes.Buffer 56 | 57 | switch { 58 | default: 59 | out = os.Stdout 60 | case *size: 61 | out = &buf 62 | case term.IsTerminal(os.Stdout.Fd()): 63 | f, err := os.Create("out.zip") 64 | if err != nil { 65 | return errors.Wrap(err, "creating zip") 66 | } 67 | defer f.Close() 68 | out = f 69 | } 70 | 71 | if _, err := io.Copy(out, r); err != nil { 72 | return errors.Wrap(err, "copying") 73 | } 74 | 75 | if *size { 76 | z, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) 77 | if err != nil { 78 | return errors.Wrap(err, "opening zip") 79 | } 80 | 81 | files := z.File 82 | 83 | sort.Slice(files, func(i int, j int) bool { 84 | a := files[i] 85 | b := files[j] 86 | return a.UncompressedSize64 > b.UncompressedSize64 87 | }) 88 | 89 | fmt.Printf("\n") 90 | for _, f := range files { 91 | size := humanize.Bytes(f.UncompressedSize64) 92 | fmt.Printf(" %10s %s\n", size, colors.Purple(f.Name)) 93 | } 94 | } 95 | 96 | return err 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /internal/errorpage/template.go: -------------------------------------------------------------------------------- 1 | package errorpage 2 | 3 | import "html/template" 4 | 5 | // defaultPage is the default error page. 6 | var defaultPage = template.Must(template.New("errorpage").Parse(` 7 | 8 | 9 | 10 |AWS email delivery can be slow sometimes. Please give it 30-60s. Otherwise, be sure to check your spam folder.
12 |Lambda `memory` scales CPU alongside RAM, so if your application is slow to initialize or serve responses, you may want to try `1024` or above. See [Lambda Pricing](https://aws.amazon.com/lambda/pricing/) for options.
17 |Ensure that all of your dependencies are deployed. You may use `up -v` to view what is added or filtered from the deployment or `up build --size` to output the contents of the zip.
18 |By default, Up ignores files which are found in `.upignore`. Use the verbose flag such as `up -v` to see if files have been filtered or `up build --size` to see a list of files within the zip sorted by size. See [Ignoring Files](#configuration.ignoring_files) for more information.
23 |The first deploy also creates resources associated with your project and can take roughly 1-2 minutes. AWS provides limited granularity into the creation progress of these resources, so the progress bar may appear "stuck".
28 |Run `up team login` if you aren't signed in, then run `up team login --team my-team-id` to sign into any teams you're an owner or member of.
33 |If you receive a `Unable to associate certificate` error it is because you have not verified the SSL certificate. Certs for CloudFront when creating a custom domain MUST be in us-east-1, so if you need to manually resend verification emails visit [ACM in US East 1](https://console.aws.amazon.com/acm/home?region=us-east-1).
38 |If you run into "403 Forbidden" errors this is due to GitHub's low rate limit for unauthenticated users, consider creating a [Personal Access Token](https://github.com/settings/tokens) and adding `GITHUB_TOKEN` to your CI.
43 |Not Found
\n", res.Body.String()) 79 | }) 80 | 81 | t.Run("non-html", func(t *testing.T) { 82 | res := httptest.NewRecorder() 83 | req := httptest.NewRequest("GET", "/style.css", nil) 84 | 85 | h.ServeHTTP(res, req) 86 | 87 | assert.Equal(t, 200, res.Code) 88 | assert.Equal(t, "text/css; charset=utf-8", res.Header().Get("Content-Type")) 89 | assert.Equal(t, "body{}\n", res.Body.String()) 90 | }) 91 | 92 | t.Run("write before header", func(t *testing.T) { 93 | s := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 94 | w.Header().Set("Content-Type", "text/html") 95 | io.WriteString(w, "") 96 | io.WriteString(w, "") 97 | io.WriteString(w, "") 98 | io.WriteString(w, "") 99 | io.WriteString(w, "") 100 | io.WriteString(w, "") 101 | }) 102 | 103 | h, err := New(c, s) 104 | assert.NoError(t, err, "initialize") 105 | 106 | res := httptest.NewRecorder() 107 | req := httptest.NewRequest("GET", "/", nil) 108 | 109 | h.ServeHTTP(res, req) 110 | 111 | assert.Equal(t, 200, res.Code) 112 | assert.Equal(t, " \n ", res.Body.String()) 113 | }) 114 | } 115 | -------------------------------------------------------------------------------- /internal/logs/parser/parser.go: -------------------------------------------------------------------------------- 1 | //go:generate peg -inline -switch grammar.peg 2 | 3 | // Package parser provides a parser for Up's 4 | // log query language, abstracting away provider 5 | // specifics. 6 | package parser 7 | 8 | import ( 9 | "strconv" 10 | 11 | "github.com/apex/up/internal/logs/parser/ast" 12 | ) 13 | 14 | // Parse query string. 15 | func Parse(s string) (ast.Node, error) { 16 | p := &parser{Buffer: s} 17 | p.Init() 18 | 19 | if err := p.Parse(); err != nil { 20 | return nil, err 21 | } 22 | 23 | p.Execute() 24 | n := ast.Root{Node: p.stack[0]} 25 | return n, nil 26 | } 27 | 28 | // push node. 29 | func (p *parser) push(n ast.Node) { 30 | p.stack = append(p.stack, n) 31 | } 32 | 33 | // pop node. 34 | func (p *parser) pop() ast.Node { 35 | if len(p.stack) == 0 { 36 | panic("pop: no nodes") 37 | } 38 | 39 | n := p.stack[len(p.stack)-1] 40 | p.stack = p.stack[:len(p.stack)-1] 41 | return n 42 | } 43 | 44 | // AddLevel node. 45 | func (p *parser) AddLevel(s string) { 46 | p.AddField("level") 47 | p.AddString(s) 48 | p.AddBinary(ast.EQ) 49 | p.AddExpr() 50 | } 51 | 52 | // AddExpr node. 53 | func (p *parser) AddExpr() { 54 | p.push(ast.Expr{ 55 | Node: p.pop(), 56 | }) 57 | } 58 | 59 | // AddField node. 60 | func (p *parser) AddField(s string) { 61 | switch s { 62 | case "level", "message", "timestamp": 63 | p.push(ast.Property(s)) 64 | default: 65 | p.push(ast.Field(s)) 66 | } 67 | } 68 | 69 | // AddString node. 70 | func (p *parser) AddString(s string) { 71 | p.push(ast.String(s)) 72 | } 73 | 74 | // AddSubscript node. 75 | func (p *parser) AddSubscript(s string) { 76 | p.push(ast.Subscript{ 77 | Left: p.pop(), 78 | Right: ast.Literal(s), 79 | }) 80 | } 81 | 82 | // AddMember node. 83 | func (p *parser) AddMember(s string) { 84 | p.push(ast.Member{ 85 | Left: p.pop(), 86 | Right: ast.Literal(s), 87 | }) 88 | } 89 | 90 | // SetNumber text. 91 | func (p *parser) SetNumber(s string) { 92 | p.number = s 93 | } 94 | 95 | // AddNumber node. 96 | func (p *parser) AddNumber(unit string) { 97 | f, _ := strconv.ParseFloat(p.number, 64) 98 | p.push(ast.Number{ 99 | Value: f, 100 | Unit: unit, 101 | }) 102 | } 103 | 104 | // AddTuple node. 105 | func (p *parser) AddTuple() { 106 | p.push(ast.Tuple{}) 107 | } 108 | 109 | // AddTupleValue node. 110 | func (p *parser) AddTupleValue() { 111 | v := p.pop() 112 | t := p.pop().(ast.Tuple) 113 | t = append(t, v) 114 | p.push(t) 115 | } 116 | 117 | // AddBinary node. 118 | func (p *parser) AddBinary(op ast.Op) { 119 | p.push(ast.Binary{ 120 | Op: op, 121 | Right: p.pop(), 122 | Left: p.pop(), 123 | }) 124 | } 125 | 126 | // AddStage node. 127 | func (p *parser) AddStage(stage string) { 128 | p.push(ast.Binary{ 129 | Op: ast.EQ, 130 | Left: ast.Field("stage"), 131 | Right: ast.String(stage), 132 | }) 133 | } 134 | 135 | // AddBinaryContains node. 136 | func (p *parser) AddBinaryContains() { 137 | p.push(ast.Binary{ 138 | Op: ast.EQ, 139 | Right: ast.Contains{Node: p.pop()}, 140 | Left: p.pop(), 141 | }) 142 | } 143 | 144 | // AddUnary node. 145 | func (p *parser) AddUnary(op ast.Op) { 146 | p.push(ast.Unary{ 147 | Op: op, 148 | Right: p.pop(), 149 | }) 150 | } 151 | -------------------------------------------------------------------------------- /platform/aws/domains/domains.go: -------------------------------------------------------------------------------- 1 | // Package domains provides domain management for AWS platforms. 2 | package domains 3 | 4 | import ( 5 | "github.com/aws/aws-sdk-go/aws" 6 | "github.com/aws/aws-sdk-go/aws/session" 7 | r "github.com/aws/aws-sdk-go/service/route53domains" 8 | 9 | "github.com/apex/up" 10 | ) 11 | 12 | // Domains implementation. 13 | type Domains struct { 14 | client *r.Route53Domains 15 | } 16 | 17 | // New returns a new domain manager. 18 | func New() *Domains { 19 | return &Domains{ 20 | client: r.New(session.New(aws.NewConfig().WithRegion("us-east-1"))), 21 | } 22 | } 23 | 24 | // List implementation. 25 | func (d *Domains) List() (v []*up.Domain, err error) { 26 | res, err := d.client.ListDomains(&r.ListDomainsInput{ 27 | MaxItems: aws.Int64(100), 28 | }) 29 | 30 | if err != nil { 31 | return 32 | } 33 | 34 | for _, d := range res.Domains { 35 | v = append(v, &up.Domain{ 36 | Name: *d.DomainName, 37 | Expiry: *d.Expiry, 38 | AutoRenew: *d.AutoRenew, 39 | }) 40 | } 41 | 42 | return 43 | } 44 | 45 | // Availability implementation. 46 | func (d *Domains) Availability(domain string) (*up.Domain, error) { 47 | res, err := d.client.CheckDomainAvailability(&r.CheckDomainAvailabilityInput{ 48 | DomainName: &domain, 49 | }) 50 | 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | if *res.Availability == "AVAILABLE" { 56 | return &up.Domain{ 57 | Name: domain, 58 | Available: true, 59 | }, nil 60 | } 61 | 62 | return &up.Domain{ 63 | Name: domain, 64 | Available: false, 65 | }, nil 66 | } 67 | 68 | // Suggestions implementation. 69 | func (d *Domains) Suggestions(domain string) (domains []*up.Domain, err error) { 70 | res, err := d.client.GetDomainSuggestions(&r.GetDomainSuggestionsInput{ 71 | DomainName: &domain, 72 | OnlyAvailable: aws.Bool(true), 73 | SuggestionCount: aws.Int64(15), 74 | }) 75 | 76 | if err != nil { 77 | return 78 | } 79 | 80 | for _, s := range res.SuggestionsList { 81 | domains = append(domains, &up.Domain{ 82 | Name: *s.DomainName, 83 | Available: true, 84 | }) 85 | } 86 | 87 | return 88 | } 89 | 90 | // Purchase implementation. 91 | func (d *Domains) Purchase(domain string, contact up.DomainContact) error { 92 | _, err := d.client.RegisterDomain(&r.RegisterDomainInput{ 93 | DomainName: &domain, 94 | AutoRenew: aws.Bool(true), 95 | DurationInYears: aws.Int64(1), 96 | RegistrantContact: contactDetails(contact), 97 | AdminContact: contactDetails(contact), 98 | TechContact: contactDetails(contact), 99 | }) 100 | 101 | return err 102 | } 103 | 104 | // contactDetails returns route53 contact details. 105 | func contactDetails(c up.DomainContact) *r.ContactDetail { 106 | return &r.ContactDetail{ 107 | AddressLine1: aws.String(c.Address), 108 | City: aws.String(c.City), 109 | State: aws.String(c.State), 110 | ZipCode: aws.String(c.ZipCode), 111 | CountryCode: aws.String(c.CountryCode), 112 | Email: aws.String(c.Email), 113 | PhoneNumber: aws.String(c.PhoneNumber), 114 | FirstName: aws.String(c.FirstName), 115 | LastName: aws.String(c.LastName), 116 | ContactType: aws.String("PERSON"), 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at tj@apex.sh. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /platform/aws/logs/logs.go: -------------------------------------------------------------------------------- 1 | // Package logs provides log management for AWS platforms. 2 | package logs 3 | 4 | import ( 5 | "encoding/json" 6 | "io" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/apex/log" 12 | jsonlog "github.com/apex/log/handlers/json" 13 | "github.com/apex/up" 14 | 15 | "github.com/aws/aws-sdk-go/aws" 16 | "github.com/aws/aws-sdk-go/aws/session" 17 | "github.com/aws/aws-sdk-go/service/cloudwatchlogs" 18 | "github.com/tj/aws/logs" 19 | 20 | "github.com/apex/up/internal/logs/parser" 21 | "github.com/apex/up/internal/logs/text" 22 | "github.com/apex/up/internal/util" 23 | ) 24 | 25 | // Logs implementation. 26 | type Logs struct { 27 | up.LogsConfig 28 | group string 29 | query string 30 | w io.WriteCloser 31 | io.Reader 32 | } 33 | 34 | // New log tailer. 35 | func New(group string, c up.LogsConfig) up.Logs { 36 | r, w := io.Pipe() 37 | 38 | query, err := parseQuery(c.Query) 39 | if err != nil { 40 | w.CloseWithError(err) 41 | } 42 | log.Debugf("query %q", query) 43 | 44 | l := &Logs{ 45 | LogsConfig: c, 46 | query: query, 47 | group: group, 48 | Reader: r, 49 | w: w, 50 | } 51 | 52 | go l.start() 53 | 54 | return l 55 | } 56 | 57 | // start fetching logs. 58 | func (l *Logs) start() { 59 | tailer := logs.New(logs.Config{ 60 | Service: cloudwatchlogs.New(session.New(aws.NewConfig().WithRegion(l.Region))), 61 | StartTime: l.Since, 62 | PollInterval: 2 * time.Second, 63 | Follow: l.Follow, 64 | FilterPattern: l.query, 65 | GroupNames: []string{l.group}, 66 | }) 67 | 68 | var handler log.Handler 69 | 70 | if l.OutputJSON { 71 | handler = jsonlog.New(os.Stdout) 72 | } else { 73 | handler = text.New(os.Stdout).WithExpandedFields(l.Expand) 74 | } 75 | 76 | // TODO: transform to reader of nl-delimited json, move to apex/log? 77 | // TODO: marshal/unmarshal as JSON so that numeric values are always float64... remove util.ToFloat() 78 | for l := range tailer.Start() { 79 | line := strings.TrimSpace(l.Message) 80 | 81 | // json log 82 | if util.IsJSONLog(line) { 83 | var e log.Entry 84 | err := json.Unmarshal([]byte(line), &e) 85 | if err != nil { 86 | log.Fatalf("error parsing json: %s", err) 87 | } 88 | 89 | handler.HandleLog(&e) 90 | continue 91 | } 92 | 93 | // skip START / END logs since they are redundant 94 | if skippable(l.Message) { 95 | continue 96 | } 97 | 98 | // lambda textual logs 99 | handler.HandleLog(&log.Entry{ 100 | Timestamp: l.Timestamp, 101 | Level: log.InfoLevel, 102 | Message: strings.TrimRight(l.Message, " \n"), 103 | }) 104 | } 105 | 106 | // TODO: refactor interface to delegate 107 | if err := tailer.Err(); err != nil { 108 | panic(err) 109 | } 110 | 111 | l.w.Close() 112 | } 113 | 114 | // parseQuery parses and converts the query to a CW friendly syntax. 115 | func parseQuery(s string) (string, error) { 116 | if s == "" { 117 | return s, nil 118 | } 119 | 120 | n, err := parser.Parse(s) 121 | if err != nil { 122 | return "", err 123 | } 124 | 125 | return n.String(), nil 126 | } 127 | 128 | // skippable returns true if the message is skippable. 129 | func skippable(s string) bool { 130 | return strings.Contains(s, "END RequestId") || 131 | strings.Contains(s, "START RequestId") 132 | } 133 | -------------------------------------------------------------------------------- /internal/cli/logs/logs.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "time" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/tj/go/term" 10 | "github.com/tj/kingpin" 11 | 12 | "github.com/apex/up" 13 | "github.com/apex/up/internal/cli/root" 14 | "github.com/apex/up/internal/stats" 15 | "github.com/apex/up/internal/util" 16 | ) 17 | 18 | func init() { 19 | cmd := root.Command("logs", "Show log output.") 20 | cmd.Example(`up logs`, "Show logs from the past hour.") 21 | cmd.Example(`up logs -S 30m`, "Show logs from the past 30 minutes.") 22 | cmd.Example(`up logs -S 5h`, "Show logs from the past 5 hours.") 23 | cmd.Example(`up logs -f`, "Show live log output.") 24 | cmd.Example(`up logs error`, "Show error logs.") 25 | cmd.Example(`up logs 'level != "info"'`, "Show non-info logs.") 26 | cmd.Example(`up logs 'production (warn or error)'`, "Show 4xx and 5xx responses in production.") 27 | cmd.Example(`up logs 'production error method in ("POST", "PUT", "DELETE")'`, "Show production 5xx responses with a POST, PUT, or DELETE method.") 28 | cmd.Example(`up logs 'error or fatal'`, "Show error and fatal logs.") 29 | cmd.Example(`up logs 'message = "user login"'`, "Show logs with a specific message.") 30 | cmd.Example(`up logs 'status = 200 duration > 1.5s'`, "Show 200 responses with latency above 1500ms.") 31 | cmd.Example(`up logs 'size > 100kb'`, "Show responses with bodies larger than 100kb.") 32 | cmd.Example(`up logs 'status >= 400'`, "Show 4xx and 5xx responses.") 33 | cmd.Example(`up logs 'user.email contains "@apex.sh"'`, "Show emails containing @apex.sh.") 34 | cmd.Example(`up logs 'user.email = "*@apex.sh"'`, "Show emails ending with @apex.sh.") 35 | cmd.Example(`up logs 'user.email = "tj@*"'`, "Show emails starting with tj@.") 36 | cmd.Example(`up logs 'method in ("POST", "PUT") ip = "207.*" status = 200 duration >= 50'`, "Show logs with a more complex query.") 37 | cmd.Example(`up logs error | jq`, "Pipe JSON error logs to the jq tool.") 38 | 39 | query := cmd.Arg("query", "Query pattern for filtering logs.").String() 40 | follow := cmd.Flag("follow", "Follow or tail the live logs.").Short('f').Bool() 41 | since := cmd.Flag("since", "Show logs since duration (30s, 5m, 2h, 1h30m, 3d, 1M).").Short('S').Default("1d").String() 42 | expand := cmd.Flag("expand", "Show expanded logs.").Short('e').Bool() 43 | 44 | cmd.Action(func(_ *kingpin.ParseContext) error { 45 | c, p, err := root.Init() 46 | if err != nil { 47 | return errors.Wrap(err, "initializing") 48 | } 49 | 50 | var s time.Duration 51 | 52 | if *since != "" { 53 | s, err = util.ParseDuration(*since) 54 | if err != nil { 55 | return errors.Wrap(err, "parsing --since duration") 56 | } 57 | } 58 | 59 | if *follow { 60 | s = time.Duration(0) 61 | } 62 | 63 | q := *query 64 | 65 | stats.Track("Logs", map[string]interface{}{ 66 | "query": q != "", 67 | "query_length": len(q), 68 | "follow": *follow, 69 | "since": s.Round(time.Second), 70 | "expand": *expand, 71 | }) 72 | 73 | logs := p.Logs(up.LogsConfig{ 74 | Region: c.Regions[0], 75 | Since: time.Now().Add(-s), 76 | Follow: *follow, 77 | Expand: *expand, 78 | Query: q, 79 | OutputJSON: !term.IsTerminal(os.Stdout.Fd()), 80 | }) 81 | 82 | if _, err := io.Copy(os.Stdout, logs); err != nil { 83 | return errors.Wrap(err, "writing logs") 84 | } 85 | 86 | return nil 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /docs/02-aws-credentials.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: AWS Credentials 3 | slug: credentials 4 | --- 5 | 6 | Before using Up you need to first provide your AWS account credentials so that Up is allowed to create resources on your behalf. 7 | 8 | ## AWS credential profiles 9 | 10 | Most AWS tools support the `~/.aws/credentials` file for storing credentials, allowing you to specify `AWS_PROFILE` environment variable so Up knows which one to reference. To read more on configuring these files view [Configuring the AWS CLI](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html). 11 | 12 | Here's an example of `~/.aws/credentials`, where `export AWS_PROFILE=myaccount` would activate these settings. 13 | 14 | ``` 15 | [myaccount] 16 | aws_access_key_id = xxxxxxxx 17 | aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxx 18 | ``` 19 | 20 | ### Best practices 21 | 22 | You may store the profile name in the `up.json` file itself as shown in the following snippet: 23 | 24 | ```json 25 | { 26 | "name": "appname-api", 27 | "profile": "myaccount" 28 | } 29 | ``` 30 | 31 | This is ideal as it ensures you will not accidentally deploy to a different AWS account. 32 | 33 | ## IAM policy for Up CLI 34 | 35 | Below is a policy for [AWS Identity and Access Management](https://aws.amazon.com/iam/) which provides Up access to manage your resources. Note that the policy may change as features are added to Up, so you may have to adjust the policy. 36 | 37 | If you're using Up for a production application it's highly recommended to configure an IAM role and user(s) for your team, restricting the access to the account and its resources. 38 | 39 | ```json 40 | { 41 | "Version": "2012-10-17", 42 | "Statement": [ 43 | { 44 | "Effect": "Allow", 45 | "Action": [ 46 | "acm:*", 47 | "cloudformation:Create*", 48 | "cloudformation:Delete*", 49 | "cloudformation:Describe*", 50 | "cloudformation:ExecuteChangeSet", 51 | "cloudformation:Update*", 52 | "cloudfront:*", 53 | "cloudwatch:*", 54 | "ec2:*", 55 | "ecs:*", 56 | "events:*", 57 | "iam:AttachRolePolicy", 58 | "iam:CreatePolicy", 59 | "iam:CreateRole", 60 | "iam:DeleteRole", 61 | "iam:DeleteRolePolicy", 62 | "iam:GetRole", 63 | "iam:PassRole", 64 | "iam:PutRolePolicy", 65 | "lambda:AddPermission", 66 | "lambda:Create*", 67 | "lambda:Delete*", 68 | "lambda:Get*", 69 | "lambda:InvokeFunction", 70 | "lambda:List*", 71 | "lambda:RemovePermission", 72 | "lambda:Update*", 73 | "logs:Create*", 74 | "logs:Describe*", 75 | "logs:FilterLogEvents", 76 | "logs:Put*", 77 | "logs:Test*", 78 | "route53:*", 79 | "route53domains:*", 80 | "s3:*", 81 | "ssm:*", 82 | "sns:*" 83 | ], 84 | "Resource": "*" 85 | }, 86 | { 87 | "Effect": "Allow", 88 | "Action": "apigateway:*", 89 | "Resource": "arn:aws:apigateway:*::/*" 90 | } 91 | ] 92 | } 93 | ``` 94 | -------------------------------------------------------------------------------- /platform/lambda/stack/status.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/apex/up/internal/colors" 7 | ) 8 | 9 | // status map for humanization. 10 | var statusMap = map[Status]string{ 11 | Unknown: "Unknown", 12 | 13 | CreateInProgress: "Creating", 14 | CreateFailed: "Failed to create", 15 | CreateComplete: "Created", 16 | 17 | DeleteInProgress: "Deleting", 18 | DeleteFailed: "Failed to delete", 19 | DeleteComplete: "Deleted", 20 | DeleteSkipped: "Skipped", 21 | 22 | UpdateInProgress: "Updating", 23 | UpdateFailed: "Failed to update", 24 | UpdateComplete: "Updated", 25 | 26 | UpdateCompleteCleanup: "Update complete cleanup in progress", 27 | UpdateRollbackCompleteCleanup: "Update rollback complete cleanup in progress", 28 | UpdateRollbackInProgress: "Update rollback in progress", 29 | UpdateRollbackComplete: "Update rollback complete", 30 | 31 | RollbackInProgress: "Rolling back", 32 | RollbackFailed: "Failed to rollback", 33 | RollbackComplete: "Rollback complete", 34 | 35 | CreatePending: "Create pending", 36 | Failed: "Failed", 37 | } 38 | 39 | // State represents a generalized stack event state. 40 | type State int 41 | 42 | // States available. 43 | const ( 44 | Success State = iota 45 | Pending 46 | Failure 47 | ) 48 | 49 | // Status represents a stack event status. 50 | type Status string 51 | 52 | // Statuses available. 53 | const ( 54 | Unknown Status = "" 55 | 56 | CreateInProgress = "CREATE_IN_PROGRESS" 57 | CreateFailed = "CREATE_FAILED" 58 | CreateComplete = "CREATE_COMPLETE" 59 | CreatePending = "CREATE_PENDING" 60 | 61 | DeleteInProgress = "DELETE_IN_PROGRESS" 62 | DeleteFailed = "DELETE_FAILED" 63 | DeleteComplete = "DELETE_COMPLETE" 64 | DeleteSkipped = "DELETE_SKIPPED" 65 | 66 | UpdateInProgress = "UPDATE_IN_PROGRESS" 67 | UpdateFailed = "UPDATE_FAILED" 68 | UpdateComplete = "UPDATE_COMPLETE" 69 | 70 | UpdateRollbackInProgress = "UPDATE_ROLLBACK_IN_PROGRESS" 71 | UpdateRollbackComplete = "UPDATE_ROLLBACK_COMPLETE" 72 | UpdateRollbackCompleteCleanup = "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS" 73 | UpdateCompleteCleanup = "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS" 74 | 75 | RollbackInProgress = "ROLLBACK_IN_PROGRESS" 76 | RollbackFailed = "ROLLBACK_FAILED" 77 | RollbackComplete = "ROLLBACK_COMPLETE" 78 | 79 | Failed = "FAILED" 80 | ) 81 | 82 | // String returns the human representation. 83 | func (s Status) String() string { 84 | return statusMap[s] 85 | } 86 | 87 | // IsDone returns true when failed or complete. 88 | func (s Status) IsDone() bool { 89 | return s.State() != Pending 90 | } 91 | 92 | // Color the given string based on the status. 93 | func (s Status) Color(v string) string { 94 | switch s.State() { 95 | case Success: 96 | return colors.Blue(v) 97 | case Pending: 98 | return colors.Yellow(v) 99 | case Failure: 100 | return colors.Red(v) 101 | default: 102 | return v 103 | } 104 | } 105 | 106 | // State returns a generalized state. 107 | func (s Status) State() State { 108 | switch s { 109 | case CreateFailed, UpdateFailed, DeleteFailed, RollbackFailed, Failed, UpdateRollbackCompleteCleanup, UpdateRollbackComplete: 110 | return Failure 111 | case CreateInProgress, UpdateInProgress, DeleteInProgress, RollbackInProgress, CreatePending, UpdateRollbackInProgress: 112 | return Pending 113 | case CreateComplete, UpdateComplete, DeleteComplete, DeleteSkipped, RollbackComplete, UpdateCompleteCleanup: 114 | return Success 115 | default: 116 | panic(fmt.Sprintf("unhandled state %q", string(s))) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /platform.go: -------------------------------------------------------------------------------- 1 | package up 2 | 3 | import ( 4 | "io" 5 | "time" 6 | ) 7 | 8 | // TODO: finalize and finish documentation 9 | 10 | // LogsConfig is configuration for viewing logs. 11 | type LogsConfig struct { 12 | // Region is the target region. 13 | Region string 14 | 15 | // Query is the filter pattern. 16 | Query string 17 | 18 | // Since is used as the starting point when filtering 19 | // historical logs, no logs before this point are returned. 20 | Since time.Time 21 | 22 | // Follow is used to stream new logs. 23 | Follow bool 24 | 25 | // Expand is used to expand logs to a verbose format. 26 | Expand bool 27 | 28 | // OutputJSON is used to output raw json. 29 | OutputJSON bool 30 | } 31 | 32 | // Logs is the interface for viewing platform logs. 33 | type Logs interface { 34 | io.Reader 35 | } 36 | 37 | // Domains is the interface for purchasing and 38 | // managing domains names. 39 | type Domains interface { 40 | Availability(domain string) (*Domain, error) 41 | Suggestions(domain string) ([]*Domain, error) 42 | Purchase(domain string, contact DomainContact) error 43 | List() ([]*Domain, error) 44 | } 45 | 46 | // Deploy config. 47 | type Deploy struct { 48 | Stage string 49 | Commit string 50 | Author string 51 | Build bool 52 | } 53 | 54 | // Platform is the interface for platform integration, 55 | // defining the basic set of functionality required for 56 | // Up applications. 57 | type Platform interface { 58 | // Build the project. 59 | Build() error 60 | 61 | // Deploy to the given stage, to the 62 | // region(s) configured by the user. 63 | Deploy(Deploy) error 64 | 65 | // Logs returns an interface for working 66 | // with logging data. 67 | Logs(LogsConfig) Logs 68 | 69 | // Domains returns an interface for 70 | // managing domain names. 71 | Domains() Domains 72 | 73 | // URL returns the endpoint for the given 74 | // region and stage combination, or an 75 | // empty string. 76 | URL(region, stage string) (string, error) 77 | 78 | // Exists returns true if the application has been created. 79 | Exists(region string) (bool, error) 80 | 81 | CreateStack(region, version string) error 82 | DeleteStack(region string, wait bool) error 83 | ShowStack(region string) error 84 | PlanStack(region string) error 85 | ApplyStack(region string) error 86 | 87 | ShowMetrics(region, stage string, start time.Time) error 88 | } 89 | 90 | // Pruner is the interface used to prune old versions and 91 | // the artifacts associated such as S3 zip files for Lambda. 92 | type Pruner interface { 93 | Prune(region, stage string, versions int) error 94 | } 95 | 96 | // Runtime is the interface used by a platform to support 97 | // runtime operations such as initializing environment 98 | // variables from remote storage. 99 | type Runtime interface { 100 | Init(stage string) error 101 | } 102 | 103 | // Zipper is the interface used by platforms which 104 | // utilize zips for delivery of deployments. 105 | type Zipper interface { 106 | Zip() io.Reader 107 | } 108 | 109 | // Domain is a domain name and its availability. 110 | type Domain struct { 111 | Name string 112 | Available bool 113 | Expiry time.Time 114 | AutoRenew bool 115 | } 116 | 117 | // DomainContact is the domain name contact 118 | // information required for registration. 119 | type DomainContact struct { 120 | Email string 121 | FirstName string 122 | LastName string 123 | CountryCode string 124 | City string 125 | Address string 126 | OrganizationName string 127 | PhoneNumber string 128 | State string 129 | ZipCode string 130 | } 131 | -------------------------------------------------------------------------------- /internal/errorpage/errorpage.go: -------------------------------------------------------------------------------- 1 | // Package errorpage provides error page loading utilities. 2 | package errorpage 3 | 4 | import ( 5 | "bytes" 6 | "html/template" 7 | "io/ioutil" 8 | "path/filepath" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | // Page is a single .html file matching 17 | // one or more status codes. 18 | type Page struct { 19 | Name string 20 | Code int 21 | Range bool 22 | Template *template.Template 23 | } 24 | 25 | // Match returns true if the page matches code. 26 | func (p *Page) Match(code int) bool { 27 | switch { 28 | case p.Code == code: 29 | return true 30 | case p.Range && p.Code == code/100: 31 | return true 32 | case p.Name == "error" && code >= 400: 33 | return true 34 | case p.Name == "default" && code >= 400: 35 | return true 36 | default: 37 | return false 38 | } 39 | } 40 | 41 | // Specificity returns the specificity, where higher is more precise. 42 | func (p *Page) Specificity() int { 43 | switch { 44 | case p.Name == "default": 45 | return 4 46 | case p.Name == "error": 47 | return 3 48 | case p.Range: 49 | return 2 50 | default: 51 | return 1 52 | } 53 | } 54 | 55 | // Render the page. 56 | func (p *Page) Render(data interface{}) (string, error) { 57 | var buf bytes.Buffer 58 | 59 | if err := p.Template.Execute(&buf, data); err != nil { 60 | return "", err 61 | } 62 | 63 | return buf.String(), nil 64 | } 65 | 66 | // Pages is a group of .html files 67 | // matching one or more status codes. 68 | type Pages []Page 69 | 70 | // Match returns the matching page. 71 | func (p Pages) Match(code int) *Page { 72 | for _, page := range p { 73 | if page.Match(code) { 74 | return &page 75 | } 76 | } 77 | 78 | return nil 79 | } 80 | 81 | // Load pages in dir. 82 | func Load(dir string) (pages Pages, err error) { 83 | files, err := ioutil.ReadDir(dir) 84 | if err != nil { 85 | return nil, errors.Wrap(err, "reading dir") 86 | } 87 | 88 | for _, file := range files { 89 | if !isErrorPage(file.Name()) { 90 | continue 91 | } 92 | 93 | path := filepath.Join(dir, file.Name()) 94 | 95 | t, err := template.New(file.Name()).ParseFiles(path) 96 | if err != nil { 97 | return nil, errors.Wrap(err, "parsing template") 98 | } 99 | 100 | name := stripExt(file.Name()) 101 | code, _ := strconv.Atoi(name) 102 | 103 | if isRange(name) { 104 | code = int(name[0] - '0') 105 | } 106 | 107 | page := Page{ 108 | Name: name, 109 | Code: code, 110 | Range: isRange(name), 111 | Template: t, 112 | } 113 | 114 | pages = append(pages, page) 115 | } 116 | 117 | pages = append(pages, Page{ 118 | Name: "default", 119 | Template: defaultPage, 120 | }) 121 | 122 | Sort(pages) 123 | return 124 | } 125 | 126 | // Sort pages by specificity. 127 | func Sort(pages Pages) { 128 | sort.Slice(pages, func(i int, j int) bool { 129 | a := pages[i] 130 | b := pages[j] 131 | return a.Specificity() < b.Specificity() 132 | }) 133 | } 134 | 135 | // isErrorPage returns true if it looks like an error page. 136 | func isErrorPage(path string) bool { 137 | if filepath.Ext(path) != ".html" { 138 | return false 139 | } 140 | 141 | name := stripExt(path) 142 | 143 | if name == "error" { 144 | return true 145 | } 146 | 147 | if isRange(name) { 148 | return true 149 | } 150 | 151 | _, err := strconv.Atoi(name) 152 | return err == nil 153 | } 154 | 155 | // isRange returns true if the name matches xx.s 156 | func isRange(name string) bool { 157 | return strings.HasSuffix(name, "xx") 158 | } 159 | 160 | // stripExt returns path without extname. 161 | func stripExt(path string) string { 162 | return strings.Replace(path, filepath.Ext(path), "", 1) 163 | } 164 | -------------------------------------------------------------------------------- /internal/setup/setup.go: -------------------------------------------------------------------------------- 1 | // Package setup provides up.json initialization. 2 | package setup 3 | 4 | import ( 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "sort" 12 | 13 | "github.com/mitchellh/go-homedir" 14 | "github.com/tj/go/term" 15 | "github.com/tj/survey" 16 | 17 | "github.com/apex/up/internal/util" 18 | "github.com/apex/up/internal/validate" 19 | "github.com/apex/up/platform/aws/regions" 20 | ) 21 | 22 | // ErrNoCredentials is the error returned when no AWS credential profiles are available. 23 | var ErrNoCredentials = errors.New("no credentials") 24 | 25 | // config saved to up.json 26 | type config struct { 27 | Name string `json:"name"` 28 | Profile string `json:"profile"` 29 | Regions []string `json:"regions"` 30 | } 31 | 32 | // questions for the user. 33 | var questions = []*survey.Question{ 34 | { 35 | Name: "name", 36 | Prompt: &survey.Input{ 37 | Message: "Project name:", 38 | Default: defaultName(), 39 | }, 40 | Validate: validateName, 41 | }, 42 | { 43 | Name: "profile", 44 | Prompt: &survey.Select{ 45 | Message: "AWS profile:", 46 | Options: awsProfiles(), 47 | Default: os.Getenv("AWS_PROFILE"), 48 | PageSize: 10, 49 | }, 50 | Validate: survey.Required, 51 | }, 52 | { 53 | Name: "region", 54 | Prompt: &survey.Select{ 55 | Message: "AWS region:", 56 | Options: regions.Names, 57 | Default: defaultRegion(), 58 | PageSize: 15, 59 | }, 60 | Validate: survey.Required, 61 | }, 62 | } 63 | 64 | // Create an up.json file for the user. 65 | func Create() error { 66 | var in struct { 67 | Name string `json:"name"` 68 | Profile string `json:"profile"` 69 | Region string `json:"region"` 70 | } 71 | 72 | if len(awsProfiles()) == 0 { 73 | return ErrNoCredentials 74 | } 75 | 76 | println() 77 | 78 | // confirm 79 | var ok bool 80 | err := survey.AskOne(&survey.Confirm{ 81 | Message: fmt.Sprintf("No up.json found, create a new project?"), 82 | Default: true, 83 | }, &ok, nil) 84 | 85 | if err != nil { 86 | return err 87 | } 88 | 89 | if !ok { 90 | return errors.New("aborted") 91 | } 92 | 93 | // prompt 94 | term.MoveUp(1) 95 | term.ClearLine() 96 | if err := survey.Ask(questions, &in); err != nil { 97 | return err 98 | } 99 | 100 | c := config{ 101 | Name: in.Name, 102 | Profile: in.Profile, 103 | Regions: []string{ 104 | regions.GetIdByName(in.Region), 105 | }, 106 | } 107 | 108 | b, _ := json.MarshalIndent(c, "", " ") 109 | return ioutil.WriteFile("up.json", b, 0644) 110 | } 111 | 112 | // defaultName returns the default app name. 113 | // The name is only inferred if it is valid. 114 | func defaultName() string { 115 | dir, _ := os.Getwd() 116 | name := filepath.Base(dir) 117 | if validate.Name(name) != nil { 118 | return "" 119 | } 120 | return name 121 | } 122 | 123 | // defaultRegion returns the default aws region. 124 | func defaultRegion() string { 125 | if s := os.Getenv("AWS_DEFAULT_REGION"); s != "" { 126 | return s 127 | } 128 | 129 | if s := os.Getenv("AWS_REGION"); s != "" { 130 | return s 131 | } 132 | 133 | return "" 134 | } 135 | 136 | // validateName validates the name prompt. 137 | func validateName(v interface{}) error { 138 | if err := validate.Name(v.(string)); err != nil { 139 | return err 140 | } 141 | 142 | return survey.Required(v) 143 | } 144 | 145 | // awsProfiles returns the AWS profiles found. 146 | func awsProfiles() []string { 147 | path, err := homedir.Expand("~/.aws/credentials") 148 | if err != nil { 149 | return nil 150 | } 151 | 152 | f, err := os.Open(path) 153 | if err != nil { 154 | return nil 155 | } 156 | defer f.Close() 157 | 158 | s, err := util.ParseSections(f) 159 | if err != nil { 160 | return nil 161 | } 162 | 163 | sort.Strings(s) 164 | return s 165 | } 166 | -------------------------------------------------------------------------------- /config/stages.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/apex/up/internal/validate" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // defaultStages is a list of default stage names. 11 | var defaultStages = []string{ 12 | "development", 13 | "staging", 14 | "production", 15 | } 16 | 17 | // Stage config. 18 | type Stage struct { 19 | Domain string `json:"domain"` 20 | Zone interface{} `json:"zone"` 21 | Path string `json:"path"` 22 | Cert string `json:"cert"` 23 | Name string `json:"-"` 24 | StageOverrides 25 | } 26 | 27 | // IsLocal returns true if the stage represents a local environment. 28 | func (s *Stage) IsLocal() bool { 29 | return s.Name == "development" 30 | } 31 | 32 | // IsRemote returns true if the stage represents a remote environment. 33 | func (s *Stage) IsRemote() bool { 34 | return !s.IsLocal() 35 | } 36 | 37 | // Validate implementation. 38 | func (s *Stage) Validate() error { 39 | if err := validate.Stage(s.Name); err != nil { 40 | return errors.Wrap(err, ".name") 41 | } 42 | 43 | switch s.Zone.(type) { 44 | case bool, string: 45 | return nil 46 | default: 47 | return errors.Errorf(".zone is an invalid type, must be string or boolean") 48 | } 49 | } 50 | 51 | // Default implementation. 52 | func (s *Stage) Default() error { 53 | if s.Zone == nil { 54 | s.Zone = true 55 | } 56 | 57 | return nil 58 | } 59 | 60 | // StageOverrides config. 61 | type StageOverrides struct { 62 | Hooks Hooks `json:"hooks"` 63 | Lambda Lambda `json:"lambda"` 64 | Proxy Relay `json:"proxy"` 65 | } 66 | 67 | // Override config. 68 | func (s *StageOverrides) Override(c *Config) { 69 | s.Hooks.Override(c) 70 | s.Lambda.Override(c) 71 | s.Proxy.Override(c) 72 | } 73 | 74 | // Stages config. 75 | type Stages map[string]*Stage 76 | 77 | // Default implementation. 78 | func (s Stages) Default() error { 79 | // defaults 80 | for _, name := range defaultStages { 81 | if _, ok := s[name]; !ok { 82 | s[name] = &Stage{} 83 | } 84 | } 85 | 86 | // assign names 87 | for name, s := range s { 88 | s.Name = name 89 | } 90 | 91 | // defaults 92 | for _, s := range s { 93 | if err := s.Default(); err != nil { 94 | return errors.Wrapf(err, "stage %q", s.Name) 95 | } 96 | } 97 | 98 | return nil 99 | } 100 | 101 | // Validate implementation. 102 | func (s Stages) Validate() error { 103 | for _, s := range s { 104 | if err := s.Validate(); err != nil { 105 | return errors.Wrapf(err, "stage %q", s.Name) 106 | } 107 | } 108 | return nil 109 | } 110 | 111 | // List returns configured stages. 112 | func (s Stages) List() (v []*Stage) { 113 | for _, s := range s { 114 | v = append(v, s) 115 | } 116 | 117 | return 118 | } 119 | 120 | // Domains returns configured domains. 121 | func (s Stages) Domains() (v []string) { 122 | for _, s := range s.List() { 123 | if s.Domain != "" { 124 | v = append(v, s.Domain) 125 | } 126 | } 127 | 128 | return 129 | } 130 | 131 | // Names returns configured stage names. 132 | func (s Stages) Names() (v []string) { 133 | for _, s := range s.List() { 134 | v = append(v, s.Name) 135 | } 136 | 137 | sort.Strings(v) 138 | return 139 | } 140 | 141 | // RemoteNames returns configured remote stage names. 142 | func (s Stages) RemoteNames() (v []string) { 143 | for _, s := range s.List() { 144 | if s.IsRemote() { 145 | v = append(v, s.Name) 146 | } 147 | } 148 | 149 | sort.Strings(v) 150 | return 151 | } 152 | 153 | // GetByDomain returns the stage by domain or nil. 154 | func (s Stages) GetByDomain(domain string) *Stage { 155 | for _, s := range s.List() { 156 | if s.Domain == domain { 157 | return s 158 | } 159 | } 160 | 161 | return nil 162 | } 163 | 164 | // GetByName returns the stage by name or nil. 165 | func (s Stages) GetByName(name string) *Stage { 166 | for _, s := range s.List() { 167 | if s.Name == name { 168 | return s 169 | } 170 | } 171 | 172 | return nil 173 | } 174 | -------------------------------------------------------------------------------- /internal/redirect/redirect.go: -------------------------------------------------------------------------------- 1 | // Package redirect provides compiling and matching 2 | // redirect and rewrite rules. 3 | package redirect 4 | 5 | import ( 6 | "fmt" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/fanyang01/radix" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // placeholders regexp. 15 | var placeholders = regexp.MustCompile(`:(\w+)`) 16 | 17 | // Rule is a single redirect rule. 18 | type Rule struct { 19 | Path string `json:"path"` 20 | Location string `json:"location"` 21 | Status int `json:"status"` 22 | Force bool `json:"force"` 23 | names map[string]bool 24 | dynamic bool 25 | sub string 26 | path *regexp.Regexp 27 | } 28 | 29 | // URL returns the final destination after substitutions from path. 30 | func (r Rule) URL(path string) string { 31 | return r.path.ReplaceAllString(path, r.sub) 32 | } 33 | 34 | // IsDynamic returns true if a splat or placeholder is used. 35 | func (r *Rule) IsDynamic() bool { 36 | return r.dynamic 37 | } 38 | 39 | // IsRewrite returns true if the rule represents a rewrite. 40 | func (r *Rule) IsRewrite() bool { 41 | return r.Status == 200 || r.Status == 0 42 | } 43 | 44 | // Compile the rule. 45 | func (r *Rule) Compile() { 46 | r.path, r.names = compilePath(r.Path) 47 | r.sub = compileSub(r.Path, r.Location, r.names) 48 | r.dynamic = isDynamic(r.Path) 49 | } 50 | 51 | // Rules map of paths to redirects. 52 | type Rules map[string]Rule 53 | 54 | // Matcher for header lookup. 55 | type Matcher struct { 56 | t *radix.PatternTrie 57 | } 58 | 59 | // Lookup returns fields for the given path. 60 | func (m *Matcher) Lookup(path string) *Rule { 61 | v, ok := m.t.Lookup(path) 62 | if !ok { 63 | return nil 64 | } 65 | 66 | r := v.(Rule) 67 | return &r 68 | } 69 | 70 | // Compile the given rules to a trie. 71 | func Compile(rules Rules) (*Matcher, error) { 72 | t := radix.NewPatternTrie() 73 | m := &Matcher{t} 74 | 75 | for path, rule := range rules { 76 | rule.Path = path 77 | rule.Compile() 78 | t.Add(compilePattern(path), rule) 79 | t.Add(compilePattern(path)+"/", rule) 80 | } 81 | 82 | return m, nil 83 | } 84 | 85 | // compileSub returns a substitution string. 86 | func compileSub(path, s string, names map[string]bool) string { 87 | // splat 88 | s = strings.Replace(s, `:splat`, `${splat}`, -1) 89 | 90 | // placeholders 91 | s = placeholders.ReplaceAllStringFunc(s, func(v string) string { 92 | name := v[1:] 93 | 94 | // TODO: refactor to not panic 95 | if !names[name] { 96 | panic(errors.Errorf("placeholder %q is not present in the path pattern %q", v, path)) 97 | } 98 | 99 | return fmt.Sprintf("${%s}", name) 100 | }) 101 | 102 | return s 103 | } 104 | 105 | // compilePath returns a regexp for substitutions and return 106 | // a map of placeholder names for validation. 107 | func compilePath(s string) (*regexp.Regexp, map[string]bool) { 108 | names := make(map[string]bool) 109 | 110 | // escape 111 | s = regexp.QuoteMeta(s) 112 | 113 | // splat 114 | s = strings.Replace(s, `\*`, `(?P