├── .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 | 
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 | 
18 |
19 | ## OSS Features
20 |
21 | Features of the free open-source edition.
22 |
23 | 
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 | 
30 |
31 | [](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 | AWS email delivery can be slow sometimes. Please give it 30-60s. Otherwise, be sure to check your spam folder. 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. 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. 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. 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". 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. 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). 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.
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 |
I didn't receive a sign-in or certificate confirmation email
11 | My application times out or seems slow
16 | I'm seeing 404 Not Found responses
22 | My deployment seems stuck
27 | How do I sign into my team?
32 | Unable to associate certificate error
37 | I'm seeing 403 Forbidden errors in CI
42 |
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 |