├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .goreleaser.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── History.md ├── LICENSE ├── Makefile ├── Readme.md ├── assets ├── features-community.png ├── features-pro.png ├── pricing.png ├── screen2.png └── title.png ├── cmd ├── up-proxy │ └── main.go └── up │ └── main.go ├── config ├── backoff.go ├── backoff_test.go ├── config.go ├── config_test.go ├── cors.go ├── dns.go ├── dns_test.go ├── doc.go ├── duration.go ├── duration_test.go ├── environment.go ├── errorpages.go ├── errorpages_test.go ├── hooks.go ├── hooks_test.go ├── lambda.go ├── lambda_test.go ├── logs.go ├── relay.go ├── runtimes.go ├── stages.go ├── stages_test.go ├── static.go └── static_test.go ├── docs ├── 00-introduction.md ├── 01-installation.md ├── 02-aws-credentials.md ├── 03-getting-started.md ├── 04-configuration.md ├── 05-runtimes.md ├── 06-commands.md ├── 07-guides.md ├── 08-troubleshooting.md ├── 09-faq.md └── 10-links.md ├── go.mod ├── go.sum ├── handler ├── handler.go ├── handler_test.go └── testdata │ ├── node-pkg-start │ ├── index.js │ ├── package.json │ └── up.json │ ├── node-pkg │ ├── app.js │ ├── package.json │ └── up.json │ ├── node │ ├── app.js │ └── up.json │ ├── spa │ ├── app.js │ ├── css │ │ ├── bar.css │ │ └── foo.css │ ├── index.html │ └── up.json │ ├── static-redirects │ ├── help │ │ └── ping │ │ │ └── alerts │ │ │ └── index.html │ ├── index.html │ └── up.json │ ├── static-rewrites │ ├── help │ │ └── ping │ │ │ └── alerts.html │ ├── index.html │ └── up.json │ └── static │ ├── index.html │ ├── style.css │ └── up.json ├── http ├── cors │ ├── cors.go │ └── cors_test.go ├── errorpages │ ├── errorpages.go │ ├── errorpages_test.go │ └── testdata │ │ ├── defaults │ │ ├── index.html │ │ └── up.json │ │ └── templates │ │ ├── 404.html │ │ ├── 5xx.html │ │ ├── index.html │ │ └── up.json ├── gzip │ ├── gzip.go │ └── gzip_test.go ├── headers │ ├── headers.go │ ├── headers_test.go │ └── testdata │ │ ├── _headers │ │ ├── index.html │ │ ├── style.css │ │ └── up.json ├── inject │ ├── inject.go │ ├── inject_test.go │ └── testdata │ │ ├── 404.html │ │ ├── index.html │ │ ├── style.css │ │ └── up.json ├── logs │ ├── logs.go │ ├── logs_test.go │ └── testdata │ │ ├── index.html │ │ └── up.json ├── poweredby │ ├── poweredby.go │ ├── poweredby_test.go │ └── testdata │ │ ├── index.html │ │ └── up.json ├── redirects │ ├── redirects.go │ └── redirects_test.go ├── relay │ ├── relay.go │ ├── relay_test.go │ └── testdata │ │ ├── basic │ │ ├── app.js │ │ └── up.json │ │ └── node │ │ ├── package.json │ │ ├── server.js │ │ └── up.json ├── robots │ ├── robots.go │ ├── robots_test.go │ └── testdata │ │ ├── index.html │ │ └── up.json └── static │ ├── static.go │ ├── static_test.go │ └── testdata │ ├── dynamic │ ├── app.js │ ├── public │ │ └── css │ │ │ └── style.css │ └── up.json │ └── static │ ├── index.html │ ├── style.css │ └── up.json ├── install.sh ├── internal ├── account │ ├── account.go │ └── cards.go ├── cli │ ├── app │ │ └── app.go │ ├── build │ │ └── build.go │ ├── config │ │ └── config.go │ ├── deploy │ │ └── deploy.go │ ├── disable-stats │ │ └── disable-stats.go │ ├── docs │ │ └── docs.go │ ├── domains │ │ └── domains.go │ ├── logs │ │ └── logs.go │ ├── metrics │ │ └── metrics.go │ ├── prune │ │ └── prune.go │ ├── root │ │ └── root.go │ ├── run │ │ └── run.go │ ├── stack │ │ └── stack.go │ ├── start │ │ └── start.go │ ├── team │ │ └── team.go │ ├── upgrade │ │ └── upgrade.go │ ├── url │ │ └── url.go │ └── version │ │ └── version.go ├── colors │ └── colors.go ├── errorpage │ ├── errorpage.go │ ├── errorpage_test.go │ ├── template.go │ └── testdata │ │ ├── 200.html │ │ ├── 404.html │ │ ├── 4xx.html │ │ ├── 500.html │ │ ├── error.html │ │ ├── other.html │ │ └── somedir │ │ └── test.html ├── header │ ├── header.go │ └── header_test.go ├── inject │ ├── inject.go │ └── inject_test.go ├── logs │ ├── logs.go │ ├── parser │ │ ├── ast │ │ │ └── ast.go │ │ ├── grammar.peg │ │ ├── grammar.peg.go │ │ ├── parser.go │ │ └── parser_test.go │ ├── text │ │ ├── text.go │ │ └── text_test.go │ └── writer │ │ ├── writer.go │ │ └── writer_test.go ├── metrics │ └── metrics.go ├── progressreader │ └── progressreader.go ├── proxy │ ├── bin │ │ └── bin.go │ ├── event.go │ ├── event_test.go │ ├── lambda.go │ ├── request.go │ ├── request_test.go │ ├── response.go │ └── response_test.go ├── redirect │ ├── redirect.go │ └── redirect_test.go ├── setup │ └── setup.go ├── shim │ ├── index.js │ └── shim.go ├── signal │ └── signal.go ├── stats │ └── stats.go ├── userconfig │ ├── userconfig.go │ └── userconfig_test.go ├── util │ ├── util.go │ └── util_test.go ├── validate │ └── validate.go └── zip │ ├── testdata │ ├── .file │ ├── .upignore │ ├── Readme.md │ ├── bar.js │ ├── foo.js │ └── index.js │ ├── zip.go │ └── zip_test.go ├── platform.go ├── platform ├── aws │ ├── cost │ │ ├── cost.go │ │ ├── cost_test.go │ │ └── domains.go │ ├── domains │ │ └── domains.go │ ├── logs │ │ └── logs.go │ ├── regions │ │ ├── regions.go │ │ └── regions_test.go │ └── runtime │ │ └── runtime.go ├── event │ └── event.go └── lambda │ ├── lambda.go │ ├── lambda_test.go │ ├── metrics.go │ ├── prune.go │ ├── reporter │ └── reporter.go │ └── stack │ ├── resources │ ├── resources.go │ └── resources_test.go │ ├── stack.go │ ├── stack_test.go │ ├── status.go │ └── status_test.go ├── reporter ├── discard │ └── discard.go ├── plain │ └── plain.go ├── reporter.go └── text │ └── text.go └── up.go /.gitattributes: -------------------------------------------------------------------------------- 1 | internal/proxy/bin/bin_assets.go filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: tj -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Before contributing to Up you'll need a few things: 4 | 5 | - Install [Golang 1.11](https://golang.org/dl/) for that Go thing if you don't have it 6 | 7 | The following are optional: 8 | 9 | - Install [pointlander/peg](https://github.com/pointlander/peg) if you're working on the log grammar 10 | - Install [shuLhan/go-bindata](https://github.com/shuLhan/go-bindata) if you need to bake `up-proxy` into `up` 11 | 12 | ## Setup 13 | 14 | Grab Up: 15 | 16 | ``` 17 | $ go get github.com/apex/up 18 | ``` 19 | 20 | Change into the project: 21 | 22 | ``` 23 | $ cd $GOPATH/src/github.com/apex/up 24 | ``` 25 | 26 | ## Testing 27 | 28 | ``` 29 | $ make test 30 | ``` 31 | 32 | ## Layout 33 | 34 | Although Up is not provided as a library it is structured as if it was, for organizational purposes. The project layout is loosely: 35 | 36 | - *.go – Primary API 37 | - [reporter](reporter) – Event based CLI reporting 38 | - [platform](platform) – Platform specifics (AWS Lambda, Azure, Google, etc) 39 | - [internal](internal) – Internal utilities and lower level tooling 40 | - [http](http) – HTTP middleware for up-proxy 41 | - [handler](handler) – HTTP middleware aggregate, effectively the entire proxy 42 | - [docs](docs) – Documentation used to generate the static site 43 | - [config](config) – Configuration structures and validation for `up.json` 44 | - [cmd](cmd) – Commands, where `up` is the CLI and `up-proxy` is serving requests in production 45 | 46 | Note that this is just a first pass, and the code / layout will be refactored. View [Godoc](http://godoc.org/github.com/apex/up) for more details of the internals. 47 | 48 | ## Proxy 49 | 50 | One oddity is that the `up-proxy` is baked into `up`. Yes there's a binary within the binary :) – this is so `up` can inject the proxy before deploying your function to Lambda. 51 | 52 | The proxy accepts AWS Lambda events from API Gateway, translates them to HTTP, and sends a request to your application, then translates it back to an event that API Gateway understands. 53 | 54 | Reverse proxy features such as URL rewriting, gzip compression, script injection, error pages and others are also provided in `up-proxy`. 55 | 56 | ## Roadmap 57 | 58 | Up uses GitHub issue tracking and milestones for its loose roadmap. I highly recommend installing Zenhub (https://www.zenhub.com/) as well, however I primarily organize by milestones and labels for now. 59 | 60 | ## Releases 61 | 62 | Notes for myself: 63 | 64 | - Run `make clean build` if necessary to re-build the proxy 65 | - Run `git changelog` 66 | - Run `git release` 67 | - Run `make release` 68 | - Re-build documentation 69 | - Notes about any backwards compat issues, migration, IAM policy changes 70 | - Adjust schemastore JSON schema if necessary 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ![](assets/title.png) 2 | 3 | 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. 4 | 5 | With Up there's no need to worry about managing or scaling machines, paying for idle servers, worrying about logging infrastructure or alerting. Just deploy your app with `$ up` and you're done! 6 | 7 | Use the free OSS version, or subscribe to [Up Pro](#pro-features) for a small monthly fee for unlimited use within your company, there is no additional cost per team-member or application. Deploy dozens or even hundreds of applications for pennies thanks to AWS Lambda's cost effective nature. 8 | 9 | ## About 10 | 11 | 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. 12 | 13 | Up currently supports Node.js, Golang, Python, Java, Crystal, Clojure 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, isolation, flexibility, and scalability of AWS. 14 | 15 | Check out the [documentation](https://up.docs.apex.sh/) for more instructions and links, or try one of the [examples](https://github.com/apex/up-examples), or chat with us in [Slack](https://chat.apex.sh/). 16 | 17 | ![](assets/screen2.png) 18 | 19 | ## OSS Features 20 | 21 | Features of the free open-source edition. 22 | 23 | ![Open source edition features](assets/features-community.png) 24 | 25 | ## Pro Features 26 | 27 | Up Pro provides additional features for production-ready applications such as encrypted environment variables, error alerting, unlimited team members, unlimited applications, priority [email support](mailto:support@apex.sh), and global deployments for **$19.99/mo USD**. Visit [Subscribing to Up Pro](https://apex.sh/docs/up/guides/#subscribing_to_up_pro) to get started. 28 | 29 | ![Pro edition features](assets/features-pro.png) 30 | 31 | [![](https://gui.apex.sh/component?name=ShadowButton&config=%7B%22text%22%3A%22SUBSCRIBE%22%2C%22color%22%3A%227956EF%22%7D)](https://apex.sh/docs/up/guides/#subscribing_to_up_pro) 32 | 33 | ## Quick Start 34 | 35 | Install Up: 36 | 37 | ``` 38 | $ curl -sf https://up.apex.sh/install | sh 39 | ``` 40 | 41 | Create an `app.js` file: 42 | 43 | ```js 44 | require('http').createServer((req, res) => { 45 | res.end('Hello World\n') 46 | }).listen(process.env.PORT) 47 | ``` 48 | 49 | Deploy the app: 50 | 51 | ``` 52 | $ up 53 | ``` 54 | 55 | Open it in the browser, or copy the url to your clipboard: 56 | 57 | ``` 58 | $ up url -o 59 | $ up url -c 60 | ``` 61 | 62 | 63 | -------------------------------------------------------------------------------- /assets/features-community.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apex/up/66dbf6d5e8362e1bb008213845a6e01f95917f59/assets/features-community.png -------------------------------------------------------------------------------- /assets/features-pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apex/up/66dbf6d5e8362e1bb008213845a6e01f95917f59/assets/features-pro.png -------------------------------------------------------------------------------- /assets/pricing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apex/up/66dbf6d5e8362e1bb008213845a6e01f95917f59/assets/pricing.png -------------------------------------------------------------------------------- /assets/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apex/up/66dbf6d5e8362e1bb008213845a6e01f95917f59/assets/screen2.png -------------------------------------------------------------------------------- /assets/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apex/up/66dbf6d5e8362e1bb008213845a6e01f95917f59/assets/title.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/dns.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/apex/up/internal/validate" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // recordTypes is a list of valid record types. 11 | var recordTypes = []string{ 12 | "ALIAS", 13 | "A", 14 | "AAAA", 15 | "CNAME", 16 | "MX", 17 | "NAPTR", 18 | "NS", 19 | "PTR", 20 | "SOA", 21 | "SPF", 22 | "SRV", 23 | "TXT", 24 | } 25 | 26 | // DNS config. 27 | type DNS struct { 28 | Zones []*Zone `json:"zones"` 29 | } 30 | 31 | // UnmarshalJSON implementation. 32 | func (d *DNS) UnmarshalJSON(b []byte) error { 33 | var zones map[string][]*Record 34 | 35 | if err := json.Unmarshal(b, &zones); err != nil { 36 | return err 37 | } 38 | 39 | for name, records := range zones { 40 | zone := &Zone{Name: name, Records: records} 41 | d.Zones = append(d.Zones, zone) 42 | } 43 | 44 | return nil 45 | } 46 | 47 | // Default implementation. 48 | func (d *DNS) Default() error { 49 | for _, z := range d.Zones { 50 | if err := z.Default(); err != nil { 51 | return errors.Wrapf(err, "zone %s", z.Name) 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | // Validate implementation. 59 | func (d *DNS) Validate() error { 60 | for _, z := range d.Zones { 61 | if err := z.Validate(); err != nil { 62 | return errors.Wrapf(err, "zone %s", z.Name) 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // Zone is a DNS zone. 70 | type Zone struct { 71 | Name string `json:"name"` 72 | Records []*Record `json:"records"` 73 | } 74 | 75 | // Default implementation. 76 | func (z *Zone) Default() error { 77 | for i, r := range z.Records { 78 | if err := r.Default(); err != nil { 79 | return errors.Wrapf(err, "record %d", i) 80 | } 81 | } 82 | 83 | return nil 84 | } 85 | 86 | // Validate implementation. 87 | func (z *Zone) Validate() error { 88 | for i, r := range z.Records { 89 | if err := r.Validate(); err != nil { 90 | return errors.Wrapf(err, "record %d", i) 91 | } 92 | } 93 | 94 | return nil 95 | } 96 | 97 | // Record is a DNS record. 98 | type Record struct { 99 | Name string `json:"name"` 100 | Type string `json:"type"` 101 | TTL int `json:"ttl"` 102 | Value []string `json:"value"` 103 | } 104 | 105 | // Validate implementation. 106 | func (r *Record) Validate() error { 107 | if err := validate.List(r.Type, recordTypes); err != nil { 108 | return errors.Wrap(err, ".type") 109 | } 110 | 111 | if err := validate.RequiredString(r.Name); err != nil { 112 | return errors.Wrap(err, ".name") 113 | } 114 | 115 | if err := validate.RequiredStrings(r.Value); err != nil { 116 | return errors.Wrap(err, ".value") 117 | } 118 | 119 | if err := validate.MinStrings(r.Value, 1); err != nil { 120 | return errors.Wrap(err, ".value") 121 | } 122 | 123 | return nil 124 | } 125 | 126 | // Default implementation. 127 | func (r *Record) Default() error { 128 | if r.TTL == 0 { 129 | r.TTL = 300 130 | } 131 | 132 | return nil 133 | } 134 | -------------------------------------------------------------------------------- /config/dns_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | "sort" 8 | "testing" 9 | 10 | "github.com/tj/assert" 11 | ) 12 | 13 | func ExampleDNS() { 14 | s := `{ 15 | "something.sh": [ 16 | { 17 | "name": "something.com", 18 | "type": "A", 19 | "ttl": 60, 20 | "value": ["35.161.83.243"] 21 | }, 22 | { 23 | "name": "blog.something.com", 24 | "type": "CNAME", 25 | "ttl": 60, 26 | "value": ["34.209.172.67"] 27 | }, 28 | { 29 | "name": "api.something.com", 30 | "type": "A", 31 | "value": ["54.187.185.18"] 32 | } 33 | ] 34 | }` 35 | 36 | var c DNS 37 | 38 | if err := json.Unmarshal([]byte(s), &c); err != nil { 39 | log.Fatalf("error unmarshaling: %s", err) 40 | } 41 | 42 | sort.Slice(c.Zones[0].Records, func(i int, j int) bool { 43 | a := c.Zones[0].Records[i] 44 | b := c.Zones[0].Records[j] 45 | return a.Name > b.Name 46 | }) 47 | 48 | if err := c.Validate(); err != nil { 49 | log.Fatalf("error validating: %s", err) 50 | } 51 | 52 | if err := c.Default(); err != nil { 53 | log.Fatalf("error defaulting: %s", err) 54 | } 55 | 56 | enc := json.NewEncoder(os.Stdout) 57 | enc.SetIndent("", " ") 58 | enc.Encode(c) 59 | // Output: 60 | // { 61 | // "zones": [ 62 | // { 63 | // "name": "something.sh", 64 | // "records": [ 65 | // { 66 | // "name": "something.com", 67 | // "type": "A", 68 | // "ttl": 60, 69 | // "value": [ 70 | // "35.161.83.243" 71 | // ] 72 | // }, 73 | // { 74 | // "name": "blog.something.com", 75 | // "type": "CNAME", 76 | // "ttl": 60, 77 | // "value": [ 78 | // "34.209.172.67" 79 | // ] 80 | // }, 81 | // { 82 | // "name": "api.something.com", 83 | // "type": "A", 84 | // "ttl": 300, 85 | // "value": [ 86 | // "54.187.185.18" 87 | // ] 88 | // } 89 | // ] 90 | // } 91 | // ] 92 | // } 93 | } 94 | 95 | func TestDNS_Validate(t *testing.T) { 96 | t.Run("invalid", func(t *testing.T) { 97 | c := &DNS{ 98 | Zones: []*Zone{ 99 | { 100 | Name: "apex.sh", 101 | Records: []*Record{ 102 | { 103 | Name: "blog.apex.sh", 104 | Type: "CNAME", 105 | }, 106 | }, 107 | }, 108 | }, 109 | } 110 | 111 | assert.EqualError(t, c.Validate(), `zone apex.sh: record 0: .value: must have at least 1 value`) 112 | }) 113 | } 114 | 115 | func TestRecord_Type(t *testing.T) { 116 | t.Run("valid", func(t *testing.T) { 117 | c := &Record{ 118 | Name: "blog.apex.sh", 119 | Type: "A", 120 | Value: []string{"1.1.1.1"}, 121 | } 122 | 123 | assert.NoError(t, c.Validate(), "validate") 124 | }) 125 | 126 | t.Run("invalid", func(t *testing.T) { 127 | c := &Record{ 128 | Name: "blog.apex.sh", 129 | Type: "AAA", 130 | } 131 | 132 | assert.EqualError(t, c.Validate(), `.type: "AAA" is invalid, must be one of: 133 | 134 | • ALIAS 135 | • A 136 | • AAAA 137 | • CNAME 138 | • MX 139 | • NAPTR 140 | • NS 141 | • PTR 142 | • SOA 143 | • SPF 144 | • SRV 145 | • TXT`) 146 | }) 147 | } 148 | 149 | func TestRecord_TTL(t *testing.T) { 150 | c := &Record{Type: "A"} 151 | assert.NoError(t, c.Default(), "default") 152 | assert.Equal(t, 300, c.TTL) 153 | } 154 | 155 | func TestRecord_Value(t *testing.T) { 156 | t.Run("empty", func(t *testing.T) { 157 | c := &Record{ 158 | Name: "blog.apex.sh", 159 | Type: "A", 160 | } 161 | 162 | assert.EqualError(t, c.Validate(), `.value: must have at least 1 value`) 163 | }) 164 | 165 | t.Run("invalid", func(t *testing.T) { 166 | c := &Record{ 167 | Name: "blog.apex.sh", 168 | Type: "A", 169 | Value: []string{"1.1.1.1", ""}, 170 | } 171 | 172 | assert.EqualError(t, c.Validate(), `.value: at index 1: is required`) 173 | }) 174 | } 175 | -------------------------------------------------------------------------------- /config/doc.go: -------------------------------------------------------------------------------- 1 | // Package config provides configuration structures, 2 | // validation, and defaulting for up.json config. 3 | package config 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/environment.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Environment variables. 4 | type Environment map[string]string 5 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/03-getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | slug: getting-started 4 | --- 5 | 6 | The simplest Up application is a single file for the application itself, with zero dependencies, and an `up.json` file which requires only a `name`. 7 | 8 | If the directory does not contain an `up.json` file, the first execution of `up` will prompt you to create it, or you can manually create an `up.json` with some preferences: 9 | 10 | ```json 11 | { 12 | "name": "appname-api", 13 | "profile": "companyname", 14 | "regions": ["us-west-2"] 15 | } 16 | ``` 17 | 18 | Up runs "vanilla" HTTP servers listening on the `PORT` environment variable, which is passed to your program by Up. For example create a new directory with the following `app.js` file: 19 | 20 | ```js 21 | const http = require('http') 22 | const { PORT = 3000 } = process.env 23 | 24 | http.createServer((req, res) => { 25 | res.end('Hello World from Node.js\n') 26 | }).listen(PORT) 27 | ``` 28 | 29 | Deploy it to the staging environment: 30 | 31 | ``` 32 | $ up 33 | ``` 34 | 35 | Open up the URL in your browser: 36 | 37 | ``` 38 | $ up url --open 39 | ``` 40 | 41 | Or test with curl: 42 | 43 | ``` 44 | $ curl `up url` 45 | ``` 46 | 47 | That's it! You've deployed a basic Up application. To view further help for commands use: 48 | 49 | ``` 50 | $ up help 51 | $ up help COMMAND 52 | $ up help COMMAND SUBCOMMAND 53 | ``` 54 | 55 | If you're not a Node.js developer here are some examples in additional languages. 56 | 57 | For Python create `app.py`: 58 | 59 | ```python 60 | from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer 61 | import os 62 | 63 | class myHandler(BaseHTTPRequestHandler): 64 | def do_GET(self): 65 | self.send_response(200) 66 | self.send_header('Content-type','text/html') 67 | self.end_headers() 68 | self.wfile.write("Hello World from Python\n") 69 | return 70 | 71 | server = HTTPServer(('', int(os.environ['PORT'])), myHandler) 72 | server.serve_forever() 73 | ``` 74 | 75 | For Golang create `main.go`: 76 | 77 | ```go 78 | package main 79 | 80 | import ( 81 | "os" 82 | "fmt" 83 | "log" 84 | "net/http" 85 | ) 86 | 87 | func main() { 88 | addr := ":"+os.Getenv("PORT") 89 | http.HandleFunc("/", hello) 90 | log.Fatal(http.ListenAndServe(addr, nil)) 91 | } 92 | 93 | func hello(w http.ResponseWriter, r *http.Request) { 94 | fmt.Fprintln(w, "Hello World from Go") 95 | } 96 | ``` 97 | 98 | Finally for Crystal create `main.cr`: 99 | 100 | ```ruby 101 | require "http/server" 102 | 103 | port = ENV["PORT"].to_i 104 | 105 | server = HTTP::Server.new(port) do |ctx| 106 | ctx.response.content_type = "text/plain" 107 | ctx.response.print "Hello world from Crystal" 108 | end 109 | 110 | server.listen 111 | ``` 112 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/08-troubleshooting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Troubleshooting 3 | menu: Help 4 | slug: troubleshooting 5 | --- 6 | 7 | This section contains self-help troubleshooting information. If you're running into an issue you can't resolve, try the [Slack](https://chat.apex.sh/) chat, or [submit an issue](https://github.com/apex/up). 8 | 9 |
10 | I didn't receive a sign-in or certificate confirmation email 11 |

AWS email delivery can be slow sometimes. Please give it 30-60s. Otherwise, be sure to check your spam folder.

12 |
13 | 14 |
15 | My application times out or seems slow 16 |

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 |
19 | 20 |
21 | I'm seeing 404 Not Found responses 22 |

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 |
24 | 25 |
26 | My deployment seems stuck 27 |

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 |
29 | 30 |
31 | How do I sign into my team? 32 |

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 |
34 | 35 |
36 | Unable to associate certificate error 37 |

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 |
39 | 40 |
41 | I'm seeing 403 Forbidden errors in CI 42 |

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 |
44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /handler/testdata/node-pkg-start/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "something", 3 | "scripts": { 4 | "start": "node index" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /handler/testdata/node-pkg-start/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "logs": { 4 | "enable": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "something" 3 | } 4 | -------------------------------------------------------------------------------- /handler/testdata/node-pkg/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "logs": { 4 | "enable": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /handler/testdata/node/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "logs": { 4 | "enable": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /handler/testdata/spa/app.js: -------------------------------------------------------------------------------- 1 | app js 2 | -------------------------------------------------------------------------------- /handler/testdata/spa/css/bar.css: -------------------------------------------------------------------------------- 1 | bar css 2 | -------------------------------------------------------------------------------- /handler/testdata/spa/css/foo.css: -------------------------------------------------------------------------------- 1 | foo css 2 | -------------------------------------------------------------------------------- /handler/testdata/spa/index.html: -------------------------------------------------------------------------------- 1 | Index 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /handler/testdata/static-redirects/help/ping/alerts/index.html: -------------------------------------------------------------------------------- 1 | Alerting docs 2 | -------------------------------------------------------------------------------- /handler/testdata/static-redirects/index.html: -------------------------------------------------------------------------------- 1 | Hello World 2 | -------------------------------------------------------------------------------- /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/help/ping/alerts.html: -------------------------------------------------------------------------------- 1 | Alerting docs 2 | -------------------------------------------------------------------------------- /handler/testdata/static-rewrites/index.html: -------------------------------------------------------------------------------- 1 | Hello World 2 | -------------------------------------------------------------------------------- /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/static/index.html: -------------------------------------------------------------------------------- 1 | Hello World 2 | -------------------------------------------------------------------------------- /handler/testdata/static/style.css: -------------------------------------------------------------------------------- 1 | body { background: whatever } 2 | -------------------------------------------------------------------------------- /handler/testdata/static/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "type": "static", 4 | "logs": { 5 | "enable": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /http/errorpages/errorpages.go: -------------------------------------------------------------------------------- 1 | // Package errorpages provides default and customizable 2 | // error pages, via error.html, 5xx.html, or 500.html 3 | // for example. 4 | package errorpages 5 | 6 | import ( 7 | "io" 8 | "net/http" 9 | 10 | "github.com/pkg/errors" 11 | accept "github.com/timewasted/go-accept-headers" 12 | 13 | "github.com/apex/up" 14 | "github.com/apex/up/internal/errorpage" 15 | "github.com/apex/up/internal/logs" 16 | "github.com/apex/up/internal/util" 17 | ) 18 | 19 | // log context. 20 | var ctx = logs.Plugin("errorpages") 21 | 22 | // response wrapper. 23 | type response struct { 24 | http.ResponseWriter 25 | config *up.Config 26 | pages errorpage.Pages 27 | header bool 28 | ignore bool 29 | } 30 | 31 | // WriteHeader implementation. 32 | func (r *response) WriteHeader(code int) { 33 | w := r.ResponseWriter 34 | 35 | r.header = true 36 | page := r.pages.Match(code) 37 | 38 | if page == nil { 39 | ctx.Debugf("did not match %d", code) 40 | w.WriteHeader(code) 41 | return 42 | } 43 | 44 | ctx.Debugf("matched %d with %q", code, page.Name) 45 | 46 | data := struct { 47 | StatusText string 48 | StatusCode int 49 | Variables map[string]interface{} 50 | }{ 51 | StatusText: http.StatusText(code), 52 | StatusCode: code, 53 | Variables: r.config.ErrorPages.Variables, 54 | } 55 | 56 | html, err := page.Render(data) 57 | if err != nil { 58 | ctx.WithError(err).Error("rendering error page") 59 | http.Error(w, "Error rendering error page.", http.StatusInternalServerError) 60 | return 61 | } 62 | 63 | r.ignore = true 64 | util.ClearHeader(w.Header()) 65 | w.Header().Set("Vary", "Accept") 66 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 67 | w.WriteHeader(code) 68 | io.WriteString(w, html) 69 | } 70 | 71 | // Write implementation. 72 | func (r *response) Write(b []byte) (int, error) { 73 | if r.ignore { 74 | return len(b), nil 75 | } 76 | 77 | if !r.header { 78 | r.WriteHeader(200) 79 | return r.Write(b) 80 | } 81 | 82 | return r.ResponseWriter.Write(b) 83 | } 84 | 85 | // New error pages handler. 86 | func New(c *up.Config, next http.Handler) (http.Handler, error) { 87 | // disabled 88 | if !c.ErrorPages.Enable { 89 | return next, nil 90 | } 91 | 92 | // load pages 93 | pages, err := errorpage.Load(c.ErrorPages.Dir) 94 | if err != nil { 95 | return nil, errors.Wrap(err, "loading error pages") 96 | } 97 | 98 | h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 99 | mime, _ := accept.Negotiate(r.Header.Get("Accept"), "text/html") 100 | 101 | if mime == "" { 102 | next.ServeHTTP(w, r) 103 | return 104 | } 105 | 106 | res := &response{ResponseWriter: w, pages: pages, config: c} 107 | next.ServeHTTP(res, r) 108 | }) 109 | 110 | return h, nil 111 | } 112 | -------------------------------------------------------------------------------- /http/errorpages/testdata/defaults/index.html: -------------------------------------------------------------------------------- 1 | Index HTML 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/errorpages/testdata/templates/5xx.html: -------------------------------------------------------------------------------- 1 | {{.StatusCode}} – {{.StatusText}} 2 | -------------------------------------------------------------------------------- /http/errorpages/testdata/templates/index.html: -------------------------------------------------------------------------------- 1 | Index HTML 2 | -------------------------------------------------------------------------------- /http/errorpages/testdata/templates/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /http/headers/testdata/_headers: -------------------------------------------------------------------------------- 1 | /*.css 2 | X-Type: css 3 | -------------------------------------------------------------------------------- /http/headers/testdata/index.html: -------------------------------------------------------------------------------- 1 | Index HTML 2 | -------------------------------------------------------------------------------- /http/headers/testdata/style.css: -------------------------------------------------------------------------------- 1 | body { color: red } 2 | -------------------------------------------------------------------------------- /http/headers/testdata/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /http/inject/inject_test.go: -------------------------------------------------------------------------------- 1 | package inject 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "testing" 9 | 10 | "github.com/apex/up" 11 | "github.com/tj/assert" 12 | 13 | "github.com/apex/up/config" 14 | "github.com/apex/up/http/errorpages" 15 | "github.com/apex/up/http/static" 16 | "github.com/apex/up/internal/inject" 17 | ) 18 | 19 | func TestInject(t *testing.T) { 20 | os.Chdir("testdata") 21 | defer os.Chdir("..") 22 | 23 | c := &up.Config{ 24 | Name: "app", 25 | ErrorPages: config.ErrorPages{ 26 | Enable: true, 27 | }, 28 | Inject: inject.Rules{ 29 | "head": []*inject.Rule{ 30 | { 31 | Type: "script", 32 | Value: "/whatever.js", 33 | }, 34 | }, 35 | }, 36 | } 37 | 38 | assert.NoError(t, c.Default(), "default") 39 | assert.NoError(t, c.Validate(), "validate") 40 | 41 | h, err := New(c, static.New(c)) 42 | assert.NoError(t, err, "init") 43 | 44 | h, err = errorpages.New(c, h) 45 | assert.NoError(t, err, "init") 46 | 47 | t.Run("2xx", func(t *testing.T) { 48 | res := httptest.NewRecorder() 49 | req := httptest.NewRequest("GET", "/", nil) 50 | 51 | h.ServeHTTP(res, req) 52 | 53 | html := ` 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ` 64 | 65 | assert.Equal(t, 200, res.Code) 66 | assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) 67 | assert.Equal(t, html, res.Body.String()) 68 | }) 69 | 70 | t.Run("4xx", func(t *testing.T) { 71 | res := httptest.NewRecorder() 72 | req := httptest.NewRequest("GET", "/missing", nil) 73 | 74 | h.ServeHTTP(res, req) 75 | 76 | assert.Equal(t, 404, res.Code) 77 | assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) 78 | assert.Equal(t, "

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 | -------------------------------------------------------------------------------- /http/inject/testdata/404.html: -------------------------------------------------------------------------------- 1 |

Not Found

2 | -------------------------------------------------------------------------------- /http/inject/testdata/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /http/inject/testdata/style.css: -------------------------------------------------------------------------------- 1 | body{} 2 | -------------------------------------------------------------------------------- /http/inject/testdata/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /http/logs/logs.go: -------------------------------------------------------------------------------- 1 | // Package logs provides HTTP request and response logging. 2 | package logs 3 | 4 | import ( 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/apex/log" 10 | 11 | "github.com/apex/up" 12 | "github.com/apex/up/internal/logs" 13 | "github.com/apex/up/internal/util" 14 | ) 15 | 16 | // TODO: optional verbose mode with req/res header etc? 17 | 18 | // log context. 19 | var ctx = logs.Plugin("logs") 20 | 21 | // response wrapper. 22 | type response struct { 23 | http.ResponseWriter 24 | written int 25 | code int 26 | duration time.Duration 27 | } 28 | 29 | // Write implementation. 30 | func (r *response) Write(b []byte) (int, error) { 31 | n, err := r.ResponseWriter.Write(b) 32 | r.written += n 33 | return n, err 34 | } 35 | 36 | // WriteHeader implementation. 37 | func (r *response) WriteHeader(code int) { 38 | r.code = code 39 | r.ResponseWriter.WriteHeader(code) 40 | } 41 | 42 | // New logs handler. 43 | func New(c *up.Config, next http.Handler) (http.Handler, error) { 44 | if c.Logs.Disable { 45 | return next, nil 46 | } 47 | 48 | h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 49 | ctx := logContext(r) 50 | logRequest(ctx, r) 51 | 52 | start := time.Now() 53 | res := &response{ResponseWriter: w, code: 200} 54 | next.ServeHTTP(res, r) 55 | res.duration = time.Since(start) 56 | 57 | logResponse(ctx, res, r) 58 | }) 59 | 60 | return h, nil 61 | } 62 | 63 | // logContext returns the common log context for a request. 64 | func logContext(r *http.Request) log.Interface { 65 | return ctx.WithFields(log.Fields{ 66 | "request_id": r.Header.Get("X-Request-Id"), 67 | "method": r.Method, 68 | "path": r.URL.Path, 69 | "query": r.URL.Query().Encode(), 70 | "ip": r.RemoteAddr, 71 | }) 72 | } 73 | 74 | // logRequest logs the request. 75 | func logRequest(ctx log.Interface, r *http.Request) { 76 | if s := r.Header.Get("Content-Length"); s != "" { 77 | n, err := strconv.Atoi(s) 78 | if err == nil { 79 | ctx = ctx.WithField("size", n) 80 | } 81 | } 82 | 83 | ctx.Info("request") 84 | } 85 | 86 | // logResponse logs the response. 87 | func logResponse(ctx log.Interface, res *response, r *http.Request) { 88 | ctx = ctx.WithFields(log.Fields{ 89 | "duration": util.Milliseconds(res.duration), 90 | "size": res.written, 91 | "status": res.code, 92 | }) 93 | 94 | switch { 95 | case res.code >= 500: 96 | ctx.Error("response") 97 | case res.code >= 400: 98 | ctx.Warn("response") 99 | default: 100 | ctx.Info("response") 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /http/logs/testdata/index.html: -------------------------------------------------------------------------------- 1 | Index HTML 2 | -------------------------------------------------------------------------------- /http/logs/testdata/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api" 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /http/poweredby/testdata/index.html: -------------------------------------------------------------------------------- 1 | Index HTML 2 | -------------------------------------------------------------------------------- /http/poweredby/testdata/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /http/redirects/redirects.go: -------------------------------------------------------------------------------- 1 | // Package redirects provides redirection and URL rewriting. 2 | package redirects 3 | 4 | import ( 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/apex/log" 9 | "github.com/apex/up" 10 | "github.com/apex/up/internal/logs" 11 | "github.com/apex/up/internal/redirect" 12 | ) 13 | 14 | // TODO: tests for popagating 4xx / 5xx, dont mask all these 15 | // TODO: load _redirects relative to .Static.Dir? 16 | // TODO: add list of methods to match on 17 | 18 | // log context. 19 | var ctx = logs.Plugin("redirects") 20 | 21 | type rewrite struct { 22 | http.ResponseWriter 23 | header bool 24 | isNotFound bool 25 | } 26 | 27 | // WriteHeader implementation. 28 | func (r *rewrite) WriteHeader(code int) { 29 | r.header = true 30 | r.isNotFound = code == 404 31 | 32 | if r.isNotFound { 33 | return 34 | } 35 | 36 | r.ResponseWriter.WriteHeader(code) 37 | } 38 | 39 | // Write implementation. 40 | func (r *rewrite) Write(b []byte) (int, error) { 41 | if r.isNotFound { 42 | return len(b), nil 43 | } 44 | 45 | if !r.header { 46 | r.WriteHeader(200) 47 | return r.Write(b) 48 | } 49 | 50 | return r.ResponseWriter.Write(b) 51 | } 52 | 53 | // New redirects handler. 54 | func New(c *up.Config, next http.Handler) (http.Handler, error) { 55 | if len(c.Redirects) == 0 { 56 | return next, nil 57 | } 58 | 59 | rules, err := redirect.Compile(c.Redirects) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 65 | rule := rules.Lookup(r.URL.Path) 66 | 67 | ctx := ctx.WithFields(log.Fields{ 68 | "path": r.URL.Path, 69 | }) 70 | 71 | // pass-through 72 | if rule == nil { 73 | ctx.Debug("no match") 74 | next.ServeHTTP(w, r) 75 | return 76 | } 77 | 78 | // destination path 79 | path := rule.URL(r.URL.Path) 80 | 81 | // forced rewrite 82 | if rule.IsRewrite() && rule.Force { 83 | ctx.WithField("dest", path).Info("forced rewrite") 84 | r.Header.Set("X-Original-Path", r.URL.Path) 85 | r.URL.Path = path 86 | next.ServeHTTP(w, r) 87 | return 88 | } 89 | 90 | // rewrite 91 | if rule.IsRewrite() { 92 | res := &rewrite{ResponseWriter: w} 93 | next.ServeHTTP(res, r) 94 | 95 | if res.isNotFound { 96 | ctx.WithField("dest", path).Info("rewrite") 97 | r.Header.Set("X-Original-Path", r.URL.Path) 98 | r.URL.Path = path 99 | // This hack is necessary for SPAs because the Go 100 | // static file server uses .html to set the correct mime, 101 | // ideally it uses the file's extension or magic number etc. 102 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 103 | next.ServeHTTP(w, r) 104 | } 105 | return 106 | } 107 | 108 | // redirect 109 | ctx.WithField("dest", path).Info("redirect") 110 | w.Header().Set("Location", path) 111 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 112 | w.WriteHeader(rule.Status) 113 | fmt.Fprintln(w, http.StatusText(rule.Status)) 114 | }) 115 | 116 | return h, nil 117 | } 118 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /http/relay/testdata/basic/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /http/relay/testdata/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "node server" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /http/relay/testdata/node/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/robots/testdata/index.html: -------------------------------------------------------------------------------- 1 | Index HTML 2 | -------------------------------------------------------------------------------- /http/robots/testdata/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /http/static/testdata/dynamic/public/css/style.css: -------------------------------------------------------------------------------- 1 | body { background: whatever } 2 | -------------------------------------------------------------------------------- /http/static/testdata/dynamic/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /http/static/testdata/static/index.html: -------------------------------------------------------------------------------- 1 | Index HTML 2 | -------------------------------------------------------------------------------- /http/static/testdata/static/style.css: -------------------------------------------------------------------------------- 1 | body { background: whatever } 2 | -------------------------------------------------------------------------------- /http/static/testdata/static/up.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /internal/cli/root/root.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | 7 | "github.com/apex/log" 8 | "github.com/apex/log/handlers/cli" 9 | "github.com/apex/log/handlers/delta" 10 | "github.com/pkg/errors" 11 | "github.com/tj/kingpin" 12 | 13 | "github.com/apex/up" 14 | "github.com/apex/up/internal/util" 15 | "github.com/apex/up/platform/event" 16 | "github.com/apex/up/platform/lambda" 17 | "github.com/apex/up/reporter" 18 | ) 19 | 20 | // Cmd is the root command. 21 | var Cmd = kingpin.New("up", "") 22 | 23 | // Command registers a command. 24 | var Command = Cmd.Command 25 | 26 | // Init function. 27 | var Init func() (*up.Config, *up.Project, error) 28 | 29 | func init() { 30 | log.SetHandler(cli.Default) 31 | 32 | Cmd.Example(`up`, "Deploy the project to the staging environment.") 33 | Cmd.Example(`up deploy production`, "Deploy the project to the production stage.") 34 | Cmd.Example(`up url`, "Show the staging endpoint url.") 35 | Cmd.Example(`up logs -f`, "Tail project logs.") 36 | Cmd.Example(`up logs 'error or fatal'`, "Show error or fatal level logs.") 37 | Cmd.Example(`up run build`, "Run build command manually.") 38 | Cmd.Example(`up help team`, "Show help and examples for a command.") 39 | Cmd.Example(`up help team members`, "Show help and examples for a sub-command.") 40 | 41 | workdir := Cmd.Flag("chdir", "Change working directory.").Default(".").Short('C').String() 42 | verbose := Cmd.Flag("verbose", "Enable verbose log output.").Short('v').Bool() 43 | format := Cmd.Flag("format", "Output formatter.").Default("text").String() 44 | region := Cmd.Flag("region", "Target region id.").String() 45 | 46 | Cmd.PreAction(func(ctx *kingpin.ParseContext) error { 47 | os.Chdir(*workdir) 48 | 49 | if *verbose { 50 | log.SetHandler(delta.Default) 51 | log.SetLevel(log.DebugLevel) 52 | log.Debugf("up version %s (os: %s, arch: %s)", Cmd.GetVersion(), runtime.GOOS, runtime.GOARCH) 53 | } 54 | 55 | Init = func() (*up.Config, *up.Project, error) { 56 | c, err := up.ReadConfig("up.json") 57 | if err != nil { 58 | return nil, nil, errors.Wrap(err, "reading config") 59 | } 60 | 61 | if *region != "" { 62 | c.Regions = []string{*region} 63 | } 64 | 65 | events := make(event.Events) 66 | p := up.New(c, events).WithPlatform(lambda.New(c, events)) 67 | 68 | switch { 69 | case *verbose: 70 | go reporter.Discard(events) 71 | case *format == "plain" || util.IsCI(): 72 | go reporter.Plain(events) 73 | default: 74 | go reporter.Text(events) 75 | } 76 | 77 | return c, p, nil 78 | } 79 | 80 | return nil 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /internal/cli/start/start.go: -------------------------------------------------------------------------------- 1 | package start 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/apex/log" 10 | "github.com/pkg/browser" 11 | "github.com/pkg/errors" 12 | "github.com/tj/kingpin" 13 | 14 | "github.com/apex/up/handler" 15 | "github.com/apex/up/internal/cli/root" 16 | "github.com/apex/up/internal/logs/text" 17 | "github.com/apex/up/internal/stats" 18 | ) 19 | 20 | func init() { 21 | cmd := root.Command("start", "Start development server.") 22 | cmd.Example(`up start`, "Start development server on port 3000.") 23 | cmd.Example(`up start -o`, "Start development server and open in the browser.") 24 | cmd.Example(`up start --address :5000`, "Start development server on port 5000.") 25 | cmd.Example(`up start -c 'go run main.go'`, "Override proxy command.") 26 | cmd.Example(`up start -oc 'gin --port $PORT'`, "Override proxy command and open in the browser.") 27 | 28 | stage := cmd.Flag("stage", "Target stage name.").Short('s').Default("development").String() 29 | command := cmd.Flag("command", "Proxy command override").Short('c').String() 30 | open := cmd.Flag("open", "Open endpoint in the browser.").Short('o').Bool() 31 | addr := cmd.Flag("address", "Address for server.").Default("localhost:3000").String() 32 | 33 | cmd.Action(func(_ *kingpin.ParseContext) error { 34 | log.SetHandler(text.New(os.Stdout)) 35 | 36 | c, p, err := root.Init() 37 | if err != nil { 38 | return errors.Wrap(err, "initializing") 39 | } 40 | 41 | for k, v := range c.Environment { 42 | os.Setenv(k, v) 43 | } 44 | 45 | stats.Track("Start", map[string]interface{}{ 46 | "address": *addr, 47 | "has_command": *command != "", 48 | }) 49 | 50 | if err := p.Init(*stage); err != nil { 51 | return errors.Wrap(err, "initializing") 52 | } 53 | 54 | if err := c.Override(*stage); err != nil { 55 | return errors.Wrap(err, "overriding") 56 | } 57 | 58 | if s := *command; s != "" { 59 | c.Proxy.Command = s 60 | } 61 | 62 | h, err := handler.FromConfig(c) 63 | if err != nil { 64 | return errors.Wrap(err, "selecting handler") 65 | } 66 | 67 | h, err = handler.New(c, h) 68 | if err != nil { 69 | return errors.Wrap(err, "initializing handler") 70 | } 71 | 72 | if *open { 73 | _, port, _ := net.SplitHostPort(*addr) 74 | browser.OpenURL(fmt.Sprintf("http://localhost:%s", port)) 75 | } 76 | 77 | log.WithField("address", "http://"+*addr).Info("listening") 78 | if err := http.ListenAndServe(*addr, h); err != nil { 79 | return errors.Wrap(err, "binding") 80 | } 81 | 82 | return nil 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | {{.StatusText}} – {{.StatusCode}} 11 | 81 | 82 | 83 |
84 | {{.StatusText}} 85 | {{.StatusCode}} 86 | 87 | {{with .Variables.support_email}} 88 | Please try your request again or contact support. 89 | {{else}} 90 | Please try your request again or contact support. 91 | {{end}} 92 |
93 | 94 | `)) 95 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /internal/errorpage/testdata/error.html: -------------------------------------------------------------------------------- 1 | {{.StatusText}} - {{.StatusCode}}. 2 | -------------------------------------------------------------------------------- /internal/errorpage/testdata/other.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apex/up/66dbf6d5e8362e1bb008213845a6e01f95917f59/internal/errorpage/testdata/other.html -------------------------------------------------------------------------------- /internal/errorpage/testdata/somedir/test.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apex/up/66dbf6d5e8362e1bb008213845a6e01f95917f59/internal/errorpage/testdata/somedir/test.html -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/proxy/response.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "mime" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/apex/up/internal/util" 11 | ) 12 | 13 | // ResponseWriter implements the http.ResponseWriter interface 14 | // in order to support the API Gateway Lambda HTTP "protocol". 15 | type ResponseWriter struct { 16 | out Output 17 | buf bytes.Buffer 18 | header http.Header 19 | wroteHeader bool 20 | } 21 | 22 | // NewResponse returns a new response writer to capture http output. 23 | func NewResponse() *ResponseWriter { 24 | return &ResponseWriter{} 25 | } 26 | 27 | // Header implementation. 28 | func (w *ResponseWriter) Header() http.Header { 29 | if w.header == nil { 30 | w.header = make(http.Header) 31 | } 32 | 33 | return w.header 34 | } 35 | 36 | // Write implementation. 37 | func (w *ResponseWriter) Write(b []byte) (int, error) { 38 | if !w.wroteHeader { 39 | w.WriteHeader(http.StatusOK) 40 | } 41 | 42 | // TODO: HEAD? ignore 43 | 44 | return w.buf.Write(b) 45 | } 46 | 47 | // WriteHeader implementation. 48 | func (w *ResponseWriter) WriteHeader(status int) { 49 | if w.wroteHeader { 50 | return 51 | } 52 | 53 | if w.Header().Get("Content-Type") == "" { 54 | w.Header().Set("Content-Type", "text/plain; charset=utf8") 55 | } 56 | 57 | w.out.StatusCode = status 58 | 59 | h := make(map[string]string) 60 | 61 | // API Gateway does not support multiple set-cookie fields 62 | // so we have to stagger the casing in order to support this. 63 | util.FixMultipleSetCookie(w.Header()) 64 | 65 | for k, v := range w.Header() { 66 | if len(v) > 0 { 67 | h[k] = v[len(v)-1] 68 | } 69 | } 70 | 71 | w.out.Headers = h 72 | w.wroteHeader = true 73 | } 74 | 75 | // End the request. 76 | func (w *ResponseWriter) End() Output { 77 | w.out.IsBase64Encoded = isBinary(w.header) 78 | 79 | if w.out.IsBase64Encoded { 80 | w.out.Body = base64.StdEncoding.EncodeToString(w.buf.Bytes()) 81 | } else { 82 | w.out.Body = w.buf.String() 83 | } 84 | 85 | return w.out 86 | } 87 | 88 | // isBinary returns true if the response reprensents binary. 89 | func isBinary(h http.Header) bool { 90 | if !isTextMime(h.Get("Content-Type")) { 91 | return true 92 | } 93 | 94 | if h.Get("Content-Encoding") == "gzip" { 95 | return true 96 | } 97 | 98 | return false 99 | } 100 | 101 | // isTextMime returns true if the content type represents textual data. 102 | func isTextMime(kind string) bool { 103 | mt, _, err := mime.ParseMediaType(kind) 104 | if err != nil { 105 | return false 106 | } 107 | 108 | if strings.HasPrefix(mt, "text/") { 109 | return true 110 | } 111 | 112 | switch mt { 113 | case "image/svg+xml": 114 | return true 115 | case "application/json": 116 | return true 117 | case "application/xml": 118 | return true 119 | default: 120 | return false 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /internal/proxy/response_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/tj/assert" 8 | ) 9 | 10 | func Test_JSON_isTextMime(t *testing.T) { 11 | assert.Equal(t, isTextMime("application/json"), true) 12 | assert.Equal(t, isTextMime("application/json; charset=utf-8"), true) 13 | assert.Equal(t, isTextMime("Application/JSON"), true) 14 | } 15 | 16 | func Test_XML_isTextMime(t *testing.T) { 17 | assert.Equal(t, isTextMime("application/xml"), true) 18 | assert.Equal(t, isTextMime("application/xml; charset=utf-8"), true) 19 | assert.Equal(t, isTextMime("ApPlicaTion/xMl"), true) 20 | } 21 | 22 | func TestResponseWriter_Header(t *testing.T) { 23 | w := NewResponse() 24 | w.Header().Set("Foo", "bar") 25 | w.Header().Set("Bar", "baz") 26 | 27 | var buf bytes.Buffer 28 | w.header.Write(&buf) 29 | 30 | assert.Equal(t, "Bar: baz\r\nFoo: bar\r\n", buf.String()) 31 | } 32 | 33 | func TestResponseWriter_Write_text(t *testing.T) { 34 | types := []string{ 35 | "text/x-custom", 36 | "text/plain", 37 | "text/plain; charset=utf-8", 38 | "application/json", 39 | "application/json; charset=utf-8", 40 | "application/xml", 41 | "image/svg+xml", 42 | } 43 | 44 | for _, kind := range types { 45 | t.Run(kind, func(t *testing.T) { 46 | w := NewResponse() 47 | w.Header().Set("Content-Type", kind) 48 | w.Write([]byte("hello world\n")) 49 | 50 | e := w.End() 51 | assert.Equal(t, 200, e.StatusCode) 52 | assert.Equal(t, "hello world\n", e.Body) 53 | assert.Equal(t, kind, e.Headers["Content-Type"]) 54 | assert.False(t, e.IsBase64Encoded) 55 | }) 56 | } 57 | } 58 | 59 | func TestResponseWriter_Write_binary(t *testing.T) { 60 | w := NewResponse() 61 | w.Header().Set("Content-Type", "image/png") 62 | w.Write([]byte("data")) 63 | 64 | e := w.End() 65 | assert.Equal(t, 200, e.StatusCode) 66 | assert.Equal(t, "ZGF0YQ==", e.Body) 67 | assert.Equal(t, "image/png", e.Headers["Content-Type"]) 68 | assert.True(t, e.IsBase64Encoded) 69 | } 70 | 71 | func TestResponseWriter_Write_gzip(t *testing.T) { 72 | w := NewResponse() 73 | w.Header().Set("Content-Type", "text/plain") 74 | w.Header().Set("Content-Encoding", "gzip") 75 | w.Write([]byte("data")) 76 | 77 | e := w.End() 78 | assert.Equal(t, 200, e.StatusCode) 79 | assert.Equal(t, "ZGF0YQ==", e.Body) 80 | assert.Equal(t, "text/plain", e.Headers["Content-Type"]) 81 | assert.True(t, e.IsBase64Encoded) 82 | } 83 | 84 | func TestResponseWriter_WriteHeader(t *testing.T) { 85 | w := NewResponse() 86 | w.WriteHeader(404) 87 | w.Write([]byte("Not Found\n")) 88 | 89 | e := w.End() 90 | assert.Equal(t, 404, e.StatusCode) 91 | assert.Equal(t, "Not Found\n", e.Body) 92 | assert.Equal(t, "text/plain; charset=utf8", e.Headers["Content-Type"]) 93 | } 94 | -------------------------------------------------------------------------------- /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.*?)`, -1) 115 | 116 | // placeholders 117 | s = placeholders.ReplaceAllStringFunc(s, func(v string) string { 118 | name := v[1:] 119 | names[name] = true 120 | return fmt.Sprintf(`(?P<%s>[^/]+)`, name) 121 | }) 122 | 123 | // trailing slash 124 | s += `\/?` 125 | 126 | s = fmt.Sprintf(`^%s$`, s) 127 | return regexp.MustCompile(s), names 128 | } 129 | 130 | // compilePattern to a syntax usable by the trie. 131 | func compilePattern(s string) string { 132 | return placeholders.ReplaceAllString(s, "*") 133 | } 134 | 135 | // isDynamic returns true for splats or placeholders. 136 | func isDynamic(s string) bool { 137 | return hasPlaceholder(s) || hasSplat(s) 138 | } 139 | 140 | // hasPlaceholder returns true for placeholders 141 | func hasPlaceholder(s string) bool { 142 | return strings.ContainsRune(s, ':') 143 | } 144 | 145 | // hasSplat returns true for splats. 146 | func hasSplat(s string) bool { 147 | return strings.ContainsRune(s, '*') 148 | } 149 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/testdata/.file: -------------------------------------------------------------------------------- 1 | 👻 2 | -------------------------------------------------------------------------------- /internal/zip/testdata/.upignore: -------------------------------------------------------------------------------- 1 | *.md 2 | -------------------------------------------------------------------------------- /internal/zip/testdata/Readme.md: -------------------------------------------------------------------------------- 1 | Hello World 2 | -------------------------------------------------------------------------------- /internal/zip/testdata/bar.js: -------------------------------------------------------------------------------- 1 | bar 2 | -------------------------------------------------------------------------------- /internal/zip/testdata/foo.js: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /internal/zip/testdata/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 'hello world' 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /platform/aws/regions/regions.go: -------------------------------------------------------------------------------- 1 | // Package regions provides AWS region utilities. 2 | package regions 3 | 4 | import ( 5 | "fmt" 6 | "path/filepath" 7 | ) 8 | 9 | // hostedZoneIDs is a set of hosted zone ids for API Gateway. 10 | var hostedZoneIDs = map[string]string{ 11 | "us-east-2": "ZOJJZC49E0EPZ", 12 | "us-east-1": "Z1UJRXOUMOOFQ8", 13 | "us-west-1": "Z2MUQ32089INYE", 14 | "us-west-2": "Z2OJLYMUO9EFXC", 15 | "ap-east-1": "Z3FD1VL90ND7K5", 16 | "ap-south-1": "Z3VO1THU9YC4UR", 17 | "ap-northeast-3": "Z2YQB5RD63NC85", 18 | "ap-northeast-2": "Z20JF4UZKIW1U8", 19 | "ap-southeast-1": "ZL327KTPIQFUL", 20 | "ap-southeast-2": "Z2RPCDW04V8134", 21 | "ap-northeast-1": "Z1YSHQZHG15GKL", 22 | "ca-central-1": "Z19DQILCV0OWEC", 23 | "eu-central-1": "Z1U9ULNL0V5AJ3", 24 | "eu-west-1": "ZLY8HYME6SFDD", 25 | "eu-west-2": "ZJ5UAJN8Y3Z2Q", 26 | "eu-west-3": "Z3KY65QIEKYHQQ", 27 | "eu-north-1": "Z2YB950C88HT6D", 28 | "sa-east-1": "ZCMLWB8V5SYIT", 29 | } 30 | 31 | // IDs of regions. 32 | var IDs = []string{ 33 | "us-east-2", 34 | "us-east-1", 35 | "us-west-1", 36 | "us-west-2", 37 | "ap-east-1", 38 | "ap-south-1", 39 | "ap-northeast-2", 40 | "ap-southeast-1", 41 | "ap-southeast-2", 42 | "ap-northeast-1", 43 | "ca-central-1", 44 | "eu-central-1", 45 | "eu-west-1", 46 | "eu-west-2", 47 | "eu-west-3", 48 | "eu-north-1", 49 | "sa-east-1", 50 | } 51 | 52 | // Names of regions. 53 | var Names = []string{ 54 | "US East (Ohio)", 55 | "US East (N. Virginia)", 56 | "US West (N. California)", 57 | "US West (Oregon)", 58 | "Asia Pacific (Hong Kong)", 59 | "Asia Pacific (Mumbai)", 60 | "Asia Pacific (Seoul)", 61 | "Asia Pacific (Singapore)", 62 | "Asia Pacific (Sydney)", 63 | "Asia Pacific (Tokyo)", 64 | "Canada (Central)", 65 | "EU (Frankfurt)", 66 | "EU (Ireland)", 67 | "EU (London)", 68 | "EU (Paris)", 69 | "EU (Stockholm)", 70 | "South America (São Paulo)", 71 | } 72 | 73 | // Match returns regions matching the pattern(s) provided. Patterns 74 | // which are not "expanded" are returned as-is. 75 | func Match(regions []string) (v []string) { 76 | for _, pattern := range regions { 77 | matched := false 78 | 79 | for _, id := range IDs { 80 | if ok, _ := filepath.Match(pattern, id); ok { 81 | v = append(v, id) 82 | matched = true 83 | } 84 | } 85 | 86 | if !matched { 87 | v = append(v, pattern) 88 | } 89 | } 90 | 91 | return 92 | } 93 | 94 | // GetIdByName returns a region id by name. 95 | func GetIdByName(name string) string { 96 | for i, n := range Names { 97 | if n == name { 98 | return IDs[i] 99 | } 100 | } 101 | return "" 102 | } 103 | 104 | // GetHostedZoneID returns a hosted zone id by region. 105 | func GetHostedZoneID(region string) string { 106 | id, ok := hostedZoneIDs[region] 107 | if !ok { 108 | panic(fmt.Sprintf("region %q is not yet supported", region)) 109 | } 110 | return id 111 | } 112 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /platform/event/event.go: -------------------------------------------------------------------------------- 1 | // Package event provides an evented mechanism for hooking into platform specifics. 2 | // 3 | // This is necessary as not all platforms have identical capabilities, 4 | // so the reporting output (among other things) may differ 5 | // slightly. 6 | package event 7 | 8 | import ( 9 | "fmt" 10 | "strings" 11 | "time" 12 | 13 | "github.com/apex/log" 14 | ) 15 | 16 | // Events channel. 17 | type Events chan *Event 18 | 19 | // Emit an event. 20 | func (e Events) Emit(name string, fields Fields) { 21 | if !strings.Contains(name, ".event") { 22 | log.Debugf("event %s %v", name, fields) 23 | } 24 | 25 | e <- &Event{ 26 | Name: name, 27 | Fields: fields, 28 | } 29 | } 30 | 31 | // Time an event. 32 | func (e Events) Time(name string, fields Fields) func() { 33 | start := time.Now() 34 | 35 | e.Emit(name, fields) 36 | 37 | return func() { 38 | if fields == nil { 39 | fields = make(Fields) 40 | } 41 | 42 | f := make(Fields) 43 | 44 | for k, v := range fields { 45 | f[k] = v 46 | } 47 | 48 | f["duration"] = time.Since(start) 49 | e.Emit(name+".complete", f) 50 | } 51 | } 52 | 53 | // Fields for an event. 54 | type Fields map[string]interface{} 55 | 56 | // Event is a representation of an operation performed 57 | // by a platform, and is used for reporting. 58 | type Event struct { 59 | Name string 60 | Fields Fields 61 | } 62 | 63 | // Strings value. 64 | func (e *Event) Strings(name string) []string { 65 | v, ok := e.Fields[name].([]string) 66 | if !ok { 67 | panic(fmt.Errorf("%#v field %s is not []string", e, name)) 68 | } 69 | return v 70 | } 71 | 72 | // String value. 73 | func (e *Event) String(name string) string { 74 | v, ok := e.Fields[name].(string) 75 | if !ok { 76 | panic(fmt.Errorf("%#v field %s is not a string", e, name)) 77 | } 78 | return v 79 | } 80 | 81 | // Duration value. 82 | func (e *Event) Duration(name string) time.Duration { 83 | v, ok := e.Fields[name].(time.Duration) 84 | if !ok { 85 | panic(fmt.Errorf("%#v field %s is not a time.Duration", e, name)) 86 | } 87 | return v 88 | } 89 | 90 | // Int64 value. 91 | func (e *Event) Int64(name string) int64 { 92 | v, ok := e.Fields[name].(int64) 93 | if !ok { 94 | panic(fmt.Errorf("%#v field %s is not a int64", e, name)) 95 | } 96 | return v 97 | } 98 | 99 | // Int value. 100 | func (e *Event) Int(name string) int { 101 | v, ok := e.Fields[name].(int) 102 | if !ok { 103 | panic(fmt.Errorf("%#v field %s is not a int", e, name)) 104 | } 105 | return v 106 | } 107 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /platform/lambda/metrics.go: -------------------------------------------------------------------------------- 1 | package lambda 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/aws/session" 8 | "github.com/aws/aws-sdk-go/service/cloudwatch" 9 | "github.com/golang/sync/errgroup" 10 | 11 | "github.com/apex/up/internal/metrics" 12 | "github.com/apex/up/platform/event" 13 | ) 14 | 15 | // TODO: write a higher level pkg in tj/aws 16 | // TODO: move the metrics pkg to tj/aws 17 | 18 | type stat struct { 19 | Namespace string 20 | Name string 21 | Metric string 22 | Stat string 23 | point *cloudwatch.Datapoint 24 | } 25 | 26 | // Value returns the metric value. 27 | func (s *stat) Value() int { 28 | if s.point == nil { 29 | return 0 30 | } 31 | 32 | switch s.Stat { 33 | case "Sum": 34 | return int(*s.point.Sum) 35 | case "Average": 36 | return int(*s.point.Average) 37 | case "Minimum": 38 | return int(*s.point.Minimum) 39 | case "Maximum": 40 | return int(*s.point.Maximum) 41 | default: 42 | return 0 43 | } 44 | } 45 | 46 | // stats to fetch. 47 | var stats = []*stat{ 48 | {"AWS/ApiGateway", "Requests", "Count", "Sum", nil}, 49 | {"AWS/ApiGateway", "Duration min", "Latency", "Minimum", nil}, 50 | {"AWS/ApiGateway", "Duration avg", "Latency", "Average", nil}, 51 | {"AWS/ApiGateway", "Duration max", "Latency", "Maximum", nil}, 52 | {"AWS/Lambda", "Duration sum", "Duration", "Sum", nil}, 53 | {"AWS/ApiGateway", "Errors 4xx", "4XXError", "Sum", nil}, 54 | {"AWS/ApiGateway", "Errors 5xx", "5XXError", "Sum", nil}, 55 | {"AWS/Lambda", "Invocations", "Invocations", "Sum", nil}, 56 | {"AWS/Lambda", "Errors", "Errors", "Sum", nil}, 57 | {"AWS/Lambda", "Throttles", "Throttles", "Sum", nil}, 58 | } 59 | 60 | // ShowMetrics implementation. 61 | func (p *Platform) ShowMetrics(region, stage string, start time.Time) error { 62 | s := session.New(aws.NewConfig().WithRegion(region)) 63 | c := cloudwatch.New(s) 64 | var g errgroup.Group 65 | name := p.config.Name 66 | 67 | d := time.Now().UTC().Sub(start) 68 | 69 | for _, s := range stats { 70 | s := s 71 | g.Go(func() error { 72 | m := metrics.New(). 73 | Namespace(s.Namespace). 74 | TimeRange(time.Now().Add(-d), time.Now()). 75 | Period(int(d.Seconds() * 2)). 76 | Stat(s.Stat). 77 | Metric(s.Metric) 78 | 79 | switch s.Namespace { 80 | case "AWS/ApiGateway": 81 | m = m.Dimension("ApiName", name).Dimension("Stage", stage) 82 | case "AWS/Lambda": 83 | m = m.Dimension("FunctionName", name).Dimension("Resource", name+":"+stage) 84 | } 85 | 86 | res, err := c.GetMetricStatistics(m.Params()) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | if len(res.Datapoints) > 0 { 92 | s.point = res.Datapoints[0] 93 | } 94 | 95 | return nil 96 | }) 97 | } 98 | 99 | if err := g.Wait(); err != nil { 100 | return err 101 | } 102 | 103 | for _, s := range stats { 104 | p.events.Emit("metrics.value", event.Fields{ 105 | "name": s.Name, 106 | "value": s.Value(), 107 | "memory": p.config.Lambda.Memory, 108 | }) 109 | } 110 | 111 | return nil 112 | } 113 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------