Repository: apex/up Branch: master Commit: 66dbf6d5e836 Files: 230 Total size: 618.2 KB Directory structure: gitextract_3gd4es3i/ ├── .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 ├── cmd/ │ ├── up/ │ │ └── main.go │ └── up-proxy/ │ └── 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/ │ │ ├── app.js │ │ └── up.json │ ├── node-pkg/ │ │ ├── app.js │ │ ├── package.json │ │ └── up.json │ ├── node-pkg-start/ │ │ ├── index.js │ │ ├── package.json │ │ └── up.json │ ├── spa/ │ │ ├── app.js │ │ ├── css/ │ │ │ ├── bar.css │ │ │ └── foo.css │ │ ├── index.html │ │ └── up.json │ ├── static/ │ │ ├── index.html │ │ ├── style.css │ │ └── up.json │ ├── static-redirects/ │ │ ├── help/ │ │ │ └── ping/ │ │ │ └── alerts/ │ │ │ └── index.html │ │ ├── index.html │ │ └── up.json │ └── static-rewrites/ │ ├── help/ │ │ └── ping/ │ │ └── alerts.html │ ├── index.html │ └── 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/ │ ├── 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 ├── platform.go ├── reporter/ │ ├── discard/ │ │ └── discard.go │ ├── plain/ │ │ └── plain.go │ ├── reporter.go │ └── text/ │ └── text.go └── up.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ internal/proxy/bin/bin_assets.go filter=lfs diff=lfs merge=lfs -text ================================================ FILE: .github/FUNDING.yml ================================================ github: tj ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ## Prerequisites * [ ] I am running the latest version. (`up upgrade`) * [ ] I searched to see if the issue already exists. * [ ] I inspected the verbose debug output with the `-v, --verbose` flag. * [ ] Are you an Up Pro subscriber? ## Description Describe the bug or feature. ## Steps to Reproduce Describe the steps required to reproduce the issue if applicable. ## Slack Join us on Slack https://chat.apex.sh/ ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ Open an issue and discuss changes before spending time on them, unless the change is trivial or an issue already exists. Use "VERB some thing here. Closes #n" to close the relevant issue, where VERB is one of: - add - remove - change - refactor If the change is documentation related prefix with "docs: ", as these are filtered from the changelog. docs: add ~/.aws/config Run `dep ensure` if you introduce any new `import`'s so they're included in the ./vendor dir. ================================================ FILE: .gitignore ================================================ .envrc node_modules/ .shards/ lib vendor/ testing up-proxy !cmd/up-proxy dist .idea .vscode .DS_Store internal/proxy/bin/bin_assets.go internal/shim/bindata.go ================================================ FILE: .goreleaser.yml ================================================ build: main: cmd/up/main.go binary: up goos: - darwin - linux - windows - freebsd - netbsd - openbsd goarch: - amd64 - 386 ignore: - goos: darwin goarch: 386 changelog: sort: asc filters: exclude: - '^docs:' - '^refactor' ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge 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. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities 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. 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. ## Scope 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. ## Enforcement 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. 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. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Before contributing to Up you'll need a few things: - Install [Golang 1.11](https://golang.org/dl/) for that Go thing if you don't have it The following are optional: - Install [pointlander/peg](https://github.com/pointlander/peg) if you're working on the log grammar - Install [shuLhan/go-bindata](https://github.com/shuLhan/go-bindata) if you need to bake `up-proxy` into `up` ## Setup Grab Up: ``` $ go get github.com/apex/up ``` Change into the project: ``` $ cd $GOPATH/src/github.com/apex/up ``` ## Testing ``` $ make test ``` ## Layout Although Up is not provided as a library it is structured as if it was, for organizational purposes. The project layout is loosely: - *.go – Primary API - [reporter](reporter) – Event based CLI reporting - [platform](platform) – Platform specifics (AWS Lambda, Azure, Google, etc) - [internal](internal) – Internal utilities and lower level tooling - [http](http) – HTTP middleware for up-proxy - [handler](handler) – HTTP middleware aggregate, effectively the entire proxy - [docs](docs) – Documentation used to generate the static site - [config](config) – Configuration structures and validation for `up.json` - [cmd](cmd) – Commands, where `up` is the CLI and `up-proxy` is serving requests in production 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. ## Proxy 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. 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. Reverse proxy features such as URL rewriting, gzip compression, script injection, error pages and others are also provided in `up-proxy`. ## Roadmap 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. ## Releases Notes for myself: - Run `make clean build` if necessary to re-build the proxy - Run `git changelog` - Run `git release` - Run `make release` - Re-build documentation - Notes about any backwards compat issues, migration, IAM policy changes - Adjust schemastore JSON schema if necessary ================================================ FILE: History.md ================================================ v1.7.1 / 2021-09-27 =================== * fix Lambda state issue by waiting for an Active state. Closes #833 v1.7.0-pro / 2020-10-07 ======================= * add support for tagging resources v1.7.0 / 2020-10-07 =================== * add `lambda.timeout` back, defaulting to 60s. Closes #814 * change LICENSE, commercial use requires a subscription v1.6.2 / 2020-09-23 =================== * Rebuild to decrease the binary filesize bloat v1.6.2-pro / 2020-09-23 ======================= * Rebuild to decrease the binary filesize bloat v1.6.1-pro / 2020-09-23 ======================= * Rebuild the proxy to include X-Up-Timeout v1.6.0-pro / 2020-09-23 ======================= * Rebase v1.6.0 / 2020-09-23 =================== * add support for X-Up-Timeout header field. Closes #815 * change id field to request_id v1.5.2 / 2020-06-08 =================== * add Hong Kong region. Closes #804 * fix `up stack` panic due to missing res.DistributionDomainName. Closes #809 v1.5.1-pro / 2019-12-17 ======================= * Rebase v1.5.1 / 2019-12-17 =================== * fix overriding of `lambda.runtime` v1.5.0-pro / 2019-11-21 ======================= * Rebase v1.5.0 / 2019-11-21 =================== * change error_pages to be disabled by default, use `enable: true` to add them * fix stack delete behavior to not attempt to delete configured lambda roles. (Closes #787) (#788) v1.4.1-pro / 2019-10-23 ======================= * fix: add blacklisting of up-env.json so it cannot be .upignored v1.4.0-pro / 2019-10-23 ======================= * change warming functions to nodejs10.x, existing ones will be fine, as AWS doesn't actually stop these functions, they just discontinue updating/creation * Rebase v1.4.0 / 2019-10-23 =================== * refactor nodejs shim to work on node8 or node10 * change default runtime to nodejs10.x (potentially breaking change, depends on your application). Closes #784 v1.3.0-pro / 2019-05-30 ======================= * add wrapping of env var logs with `logs.disable` check v1.3.0 / 2019-05-30 =================== * add ./vendor to excluded directories by default * refactor: regenerate parser with updated peg * remove discount message, it didn't work v1.2.0 / 2019-04-20 =================== * add 60% coupon v1.2.0-pro / 2019-04-05 ======================= * add regional DNS with latency-based routing * add `--region` flag for every command to override region id v1.1.3-pro / 2019-04-02 ======================= * Rebase v1.1.3 / 2019-04-02 =================== * update tj/aws dependency for duplicate logs fix v1.1.2-pro / 2019-03-29 ======================= * Rebase v1.1.2 / 2019-03-29 =================== * fix: update tj/aws dependency for ThrottlingException logs exception v1.1.1-pro / 2019-03-23 ======================= * update warming function to nodejs8.10 to prevent EOL warning from AWS v1.1.0-pro / 2019-03-04 =================== * add file based environment variables, removing the size restrictions v1.0.0-pro / 2019-02-26 =================== * add regional endpoint support v0.9.1-pro / 2019-01-21 ======================= * add sorting of env vars. Closes #750 v0.9.0-pro / 2018-12-13 ======================= * add Lambda layer support. Closes #743 v0.8.1-pro / 2018-12-11 ======================= * improve `up env export` performance, no longer linear time * fix `up deploys` error when the stage is not deployed. Closes #716 * Rebase v0.8.1 / 2018-12-11 =================== * update tj/aws for bug preventing all logs from being returned. Closes #733 * add deploy --no-build flag for skipping build hooks. Closes #730 * Release v0.8.0-pro v0.8.0-pro / 2018-12-06 ======================= * improve `up env export` performance, no longer linear time * fix `up deploys` error when the stage is not deployed. Closes #716 v0.8.0 / 2018-12-04 =================== * add endpoint URL to the deployment output * add deploy stage to the deployment output * add msg about Up Pro * remove "not info" log example. Closes #724 * fix typo in deploy example. Closes #718 * fix: use crystallang/crystal for Crystal builds (#713) v0.7.8-pro / 2018-09-24 ======================= * add `up env export` command for exposing env vars to shell scripts v0.7.7-pro / 2018-09-17 ======================= * Rebase v0.7.7 / 2018-09-17 =================== * update go-update dependency for copy regression v0.7.5-pro / 2018-09-17 ======================= * Rebase v0.7.5 / 2018-09-17 =================== * update go-update dependency for rename() to copy replacement v0.7.4-pro / 2018-09-16 ======================= * add baked in env vars from SSM, env vars are no longer loaded at runtime. Closes #547 v0.7.4 / 2018-09-16 =================== * add request id to proxy errors * fix missing lambda configurations costs. (#703) * fix order relay errors so timeouts are returned first (leads to better error messages) v0.7.3-pro / 2018-08-08 ======================= * Rebase v0.7.3 / 2018-08-08 =================== * fix crash recovery in lambda, bug was introduced in v0.7.0 * update cors middleware for security when using allow-origin * and allow-credentials v0.7.2-pro / 2018-07-23 ======================= * Rebase v0.7.2 / 2018-07-23 =================== * add vpc stage override support. Closes #689 v0.7.1-pro / 2018-07-12 ======================= * Rebase v0.7.1 / 2018-07-12 =================== * fix initial IAM role creation waiting due to error response change v0.7.0-pro / 2018-07-11 ======================= * Rebase v0.7.0 / 2018-07-11 =================== * add in-flight request timeouts. * remove retries * refactor crash recovery to be more robust v0.6.8-pro / 2018-06-07 ======================= * Rebase v0.6.8 / 2018-06-07 =================== * fix multiple set-cookie API Gateway limitation for real (previous had a bug) v0.6.7-pro / 2018-06-07 ======================= * fix s3 acceleration update with existing S3 buckets * Rebase v0.6.7 / 2018-06-07 =================== * add striping of @owner/repo@ portion of Lerna tags. Closes #670 * fix multiple set-cookies API Gateway limitation with casing hack * fix deployment with empty Git repo * update AWS SDK versions for assuming roles. (#668) v0.6.6-pro / 2018-05-24 ======================= * Rebase v0.6.6 / 2018-05-24 =================== * add vpc support. Closes #281 * fix Crystal build on Linux: PWD => pwd (#664) v0.6.5-pro / 2018-05-16 ======================= * Rebase v0.6.5 / 2018-05-16 =================== * add hidden disable stats command (#659) * add X-Context header field. Closes #657 * fix CORS header fields from being clobbered by error pages. Closes #661 v0.6.4-pro / 2018-05-09 ======================= * add deployment size to `up deploys` output * add asterisk to denote current version in `up deploys` due to rollbacks * Rebase v0.6.4 / 2018-05-09 =================== * add support for customizing the Lambda function IAM role policy. Closes #539 * add support for specifying dns zone, and disabling it. Closes #536 * add support for updating the role policy upon deploy * change default prune retention to 30 versions v0.6.3-pro / 2018-05-02 ======================= * add deployment size to `up deploys` output * add asterisk to denote current version in `up deploys` due to rollbacks * Rebase v0.6.3 / 2018-05-02 =================== * add `--stage` flag to `up build` * add `--stage` flag to `up run` * change logs, metrics, and url commands to use `-s` flag for stage. Closes #371 (BREAKING) v0.6.2-pro / 2018-04-25 ======================= * Rebase v0.6.2 / 2018-04-25 =================== * add up prune `--stage` flag. Closes #647 * add `up` to ignore whitelist by default * remove retries on 5xx. Closes #485 * fix login bug preventing `--email` from overriding the active team email v0.6.1-pro / 2018-04-16 ======================= * Rebase v0.6.1 / 2018-04-16 =================== * add guard against `up stack plan` before `up` * add `prune` command to remove old releases from S3. Closes #322 v0.6.0-pro / 2018-04-10 ======================= * Rebase v0.6.0 / 2018-04-10 =================== * add annual plan subscription option v0.5.17-pro / 2018-04-09 ======================== * Rebase v0.5.14 / 2018-04-09 ==================== * add start command --stage flag. Closes #639 * fix scenario where JSON logs have invalid .level values * refactor: add note about running `up upgrade` after subscribing v0.5.16-pro / 2018-04-07 ======================== * Rebase v0.5.13 / 2018-04-07 ==================== * fix "Error: fetching git commit: " error when Git is missing from the system v0.5.15-pro / 2018-04-03 ======================== * Rebase v0.5.12 / 2018-04-03 ==================== * add support for defining `lambda.runtime` * add robots middleware (#627) * change default runtime to nodejs 8.10 * refactor: remove redundant wrapping of "deploying" message v0.5.11 / 2018-03-19 ==================== * fix: update tj/go for Git signer fix v0.5.12-pro / 2018-03-19 ======================== * Rebase v0.5.10 / 2018-03-19 ==================== * fix: update tj/go for Git subject fix v0.5.11-pro / 2018-03-16 ======================== * refactor: add mapping of Alarm and Subscription for `up stack plan` output * refactor: add .duration to Deploys track call * Rebase v0.5.9 / 2018-03-16 =================== * add support for serving static files with dynamic applications. Closes #174 v0.5.10-pro / 2018-03-15 =================== * add nicer `up rollback` failure message when version does not exist * add git sha and tag support to `up rollback` * add `up deploys` for listing deployments and versions * fix log filter relational and equality operators with strings v0.5.8 / 2018-03-15 =================== * fix log filter relational and equality operators with strings v0.5.7 / 2018-03-15 =================== * add git versioning, used for Pro rollbacks and deployment changelog. Closes #100 v0.5.9-pro / 2018-03-09 ======================= * add stage overrides for lambda warming. Closes #615 v0.5.8-pro / 2018-03-05 ======================= * Rebase v0.5.6 / 2018-03-05 =================== * add support for upgrading in-place up(1). Closes #607 * add CI specific upgrade to avoid progress bar * fix: remove IsNotFound error check, masks the real issue v0.5.7-pro / 2018-03-03 ======================= * Rebase v0.5.5 / 2018-03-03 =================== * fix: improve idempotency of stack deletion * docs: add sns to policy (necesary for Pro's alerting) v0.5.6-pro / 2018-03-02 ======================= * add support for `=` delimited env vars ("FOO=bar") * add support for passing multiple env vars to `up env set` * add support for overriding envs for `up start` (`$ URL=xxx up start`) v0.5.5-pro / 2018-03-01 ======================= * Rebase v0.5.4 / 2018-03-01 =================== * add default `up start` command for Go and Crystal. Closes #581 * add log stage field to all logs, not just request-level * add owner to `up team` output * fix `up metrics` output, should be stage-specific, not global * refactor: add humanized error when the stack (app) does not exist * refactor: add stage name to beginning of log line instead of as a field * refactor: add os/arch to debug logs to aid in support * refactor: add alias upserts when updating (merged from pro) * refactor: remove a redundant "deploying" error wrap * refactor: tweak some error messages * refactor: change perms of up.json to 0644. Closes #601 v0.5.4-pro / 2018-02-23 ======================= * Rebase v0.5.3 / 2018-02-23 =================== * fix log flushing, make it synchronous. Closes #545 * docs: add changelog link * docs: add mention of BINDIR v0.5.3-pro / 2018-02-22 ======================= * add 1s sleep to /_ping endpoint for improved warming concurrency accuracy * add `up env get` command for fetching a value * Rebase v0.5.2 / 2018-02-22 =================== * remove unsetting of `AWS_*` vars for now, reverts #590 fix v0.5.1 / 2018-02-22 =================== * add function version to `up stack` output * change `up team ci` to output base64 encoded config * change UP_CONFIG to attempt base64-decode when not JSON (#594) * fix proxy.command overrides. Closes #597 * fix .profile precedence. Closes #590 v0.5.2-pro / 2018-02-12 ======================= * add active warming support * Rebase v0.5.1-pro / 2018-02-08 ======================= * add `up env` --decrypt flag for emergencies when you need to list v0.5.0-pro / 2018-02-08 ======================= * add nicer env var logging with masking * add custom stage support to `up env` * add message for `up env` when no vars are defined * fix rollbacks using -previous aliases * Rebase v0.5.0 / 2018-02-08 =================== * add custom stage support. Closes #326 * add customer feedback option when unsubscribing * add `up team card change` command for updating the CC * remove sourcing of .gitignore. Closes #557 * remove development as a remote stage (now local only). Closes #563 * refactor: add separator to make log message more obvious * refactor: add hiding of cursor when verifying email * refactor retry labels below s3 uploads (improves performance) * refactor: add nicer output when using `up url -c` v0.4.12-pro / 2018-02-01 ======================== * Rebase v0.4.12 / 2018-02-01 ==================== * add -o, --open to `up start` for opening in the browser * add `logs.{stdout,stderr}` for configuring log levels. Closes #565 * add `-c, --command` flag to `up start`. Closes #564 * fix panic when .domain is missing from a stage, as it is now optional. Closes #567 * docs: add example .upignore for static sites * docs: fix team members rm example. Closes #562 * docs: add "Unable to associate certificate error" to troubleshooting * docs: add gin example v0.4.11-pro / 2018-01-29 ======================== * Rebase v0.4.11 / 2018-01-29 ==================== * add development config overrides to `up start` * add the ability to override .proxy.command at the stage level * docs: mention that the WHOIS contact emails are used * docs: fix link for acm validation * docs: tweak * docs: add guide for hot reloading * docs: remove old "Local Environment Variables" guide section * docs: add gin example for dev command v0.4.10-pro / 2018-01-25 ======================== * Rebase v0.4.10 / 2018-01-25 ==================== * refactor to use a single account/region level S3 bucket, not per-project. Closes #550 * fix base64 encoded json when params are provided v0.4.9-pro / 2018-01-24 ======================= * Rebase v0.4.9 / 2018-01-24 =================== * revert tj/go-update, causing permission issues v0.4.8-pro / 2018-01-24 ======================= * fix validating after overrides v0.4.8 / 2018-01-24 =================== * update tj/go-update for copy instead of rename. Closes #329 * update api client for RemoveMember() json body change * docs: add missing ssm to policy * docs: add note about 404s v0.4.7-pro / 2018-01-19 ======================= * add rollback support * fix upgrade deduplication due to version having -pro suffix v0.4.7 / 2018-01-19 =================== * add optimization of ACM certificate creation. Closes #452 * add `development` Lambda alias. Closes #542 * add start of stage overrides for config. Closes #314 * add support for upgrading to a specific version of Up. Closes #387 * update go-cli-analytics for disabled segment cli logging * refactor handler.New() to accept an http.Handler * refactor logging configuration, delegate isatty check etc * refactor: move internal logs to tj/aws * refactor platform integration quickly v0.4.6-pro / 2018-01-03 ======================= * add rollback support v0.4.5-pro / 2018-01-03 ======================= * add s3 acceleration * fix a log call in runtime v0.4.6 / 2018-01-03 =================== * add support for Clojure with Leiningen (#522) * add coupon price adjustment to `up team` output. Closes #516 * add support for overriding NODE_ENV. Closes #505 * add error for multiple regions, until the feature is complete * add Paris region * change `error_pages` to be enabled by default for text/html requests * refactor `handler.New()` to accept config * refactor signal handling * refactor: update api client * refactor: remove unnecessary code (#517) * refactor login and provide a non-error when you are already signed in * fix s3 buckets, should be scoped to region * fix output flickering before build output * fix: add a ! in front of build.gradle for forced inclusion (#518) v0.4.4-pro / 2017-12-22 ======================= * Rebase v0.4.5 / 2017-12-22 =================== * add new subscribe workflow * add team CRUD and rename `up account` to `up team`. Re #410 * refactor: replace `kingpin.CmdClause` with `kingpin.Cmd` * refactor: use `time.Since` for time difference (#509) * refactor: add "ci" to stats so we can see how often CI is used * refactor: simplify start of plain reporter (#508) * refactor: a typo fix in http/relay (#507) * refactor: drop unnecessary `fmt.Sprintf` in reporter/text (#506) * refactor: simplify personal team check (#500) v0.4.3-pro / 2017-12-19 ======================= * Rebase v0.4.4 / 2017-12-19 =================== * fix `up stack status` scenario before a domain is mapped * refactor: config, simplify unmarshal json of dns. Closes #497 v0.4.2-pro / 2017-12-19 ======================= * Rebase v0.4.3 / 2017-12-19 =================== * refactor: shorten s3 bucket name v0.4.1-pro / 2017-12-19 ======================= * remove 0.0.0 hack for pro upgrade * Rebase v0.4.2 / 2017-12-19 =================== * change to disallow uppercase characters in .name. Closes #498 * refactor: add humanized string for the current version * refactor: add config/backoff.go v0.4.1 / 2017-12-18 =================== * fix upgrades to pro when version matches v0.4.0-pro / 2017-12-18 ======================= * add slack `gif` option * add slack alert support * add initialization of env vars for builds. Closes #458 * add initialization of env vars for deployments. Closes #458 * add initialization of env vars for `up start`. Closes #458 * add `{alerts,actions}_count` to Deploy track * change missing default to `notBreaching` * refactor: add title casing to `up env` output v0.4.0 / 2017-12-18 =================== * add unquoted string literals for log queries * add log string sans-quote literal. Closes #461 * add log message field equality short-hand. Closes #372 * add CI=true check for plain text output. Re #422 * add --format=plain for CI. Closes #422 * add setup workflow for creating up.json and doing the initial deploy. Closes #482, #386 * add `NODE_ENV` population by default * add env vars to `up start` * add s3 deployments. Closes #272 * add cloudfront endpoint to `up stack` output. Closes #459 * change logs to purple (match everything else) * change how expanded log mode looks * remove `--region` flag * fix upgrade messages for OSS -> Pro * fix clearing state in text reporter v0.3.0-pro / 2017-12-03 ======================= * add sms alerting support v0.2.0-pro / 2017-12-03 ======================= * add hosted email alerting for nicer formatting * change alert default `period` to 1m v0.1.11-pro / 2017-11-30 ======================== * add support for listing secrets without last modified user name * fix secrets listing when user ARN is not present. Closes #433 * refactor alerting into new resources sub-pkg * Rebase v0.3.8 / 2017-11-30 =================== * add {pre,post}{build,deploy} hooks * add flushing of logs after [re]start. See #359 * add "w" for week to `ParseDuration()` * refactor: fix Map for now * refactor: use effective domain for CFN id * refactor: add test for existing zone and apex domain * refactor: add test for existing zone * refactor: add test coverage for CFN resources * fix hosted zones for sub-domains. Closes #447 * fix `.type` precedence when runtime files are detected. Closes #436 v0.3.7 / 2017-11-24 =================== * add date formatting for older logs * remove project init from `up account login` * fix timestamps for lambda plain text logs v0.1.10-pro / 2017-11-23 ======================== * add support for listing secrets without last modified user name * fix secrets listing when user ARN is not present. Closes #433 * Rebase v0.3.6 / 2017-11-22 =================== * fix subscription without coupon v0.1.9-pro / 2017-11-21 ======================= * Rebase v0.3.5 / 2017-11-21 =================== * add `stage` field to all log contexts (fixes log filtering against `production`) * fix DNS record logical id collision. Closes #420 * refactor `up stack` output v0.1.8-pro / 2017-11-20 ======================= * add TreatMissingData as ignore by default v0.1.7-pro / 2017-11-20 ======================= * fix email alerting v0.1.6-pro / 2017-11-20 ======================= * add initial alerting support v0.1.5-pro / 2017-11-20 ======================= * fix "development" env support for `up env` * Rebase v0.3.4 / 2017-11-20 =================== * add `up accounts ci` and --copy to help with setting up UP_CONFIG for CI * fix domain verification for ssl certificates. Closes #425 * update tj/kingpin for arg output formatting fix v0.1.4-pro / 2017-11-18 ======================= * Rebase v0.3.3 / 2017-11-18 =================== * fix zip paths on Windows. Closes #418 v0.1.3-pro / 2017-11-18 ======================= * Rebase v0.3.2 / 2017-11-18 =================== * add support for UP_CONFIG from environment * add `up docs` command back for opening documentation in the browser * change logs `--since` default to 1 day * fix intermittent metrics failure. Closes #414 v0.3.1 / 2017-11-15 =================== * add `up account` and sub-commands * add extended duration parsing for `--since` flags. Closes #401 * add log expansion. Closes #399 * add Content-Length request header * add request logs * add pom.xml and build.grade to whitelist which cannot be ignored * change metrics `--since` default to 1 month * refactor: remove .size defaulting of 0 * refactor progress bar with diffing, making it more responsive * fix missing logs when json does not take the shape of a log. Closes #411 v0.1.2-pro / 2017-11-15 ======================= * fix missing logs when json does not take the shape of a log. Closes #411 v0.1.0-pro / 2017-11-15 ======================= * add `env` command v0.3.0 / 2017-10-19 =================== * add listing of NS records in `up stack` output * add changelog exclusion of docs: for goreleaser * add nicer domain registration form * update tj/survey for color changes * update dependencies * refactor: add more properties to deploy track * refactor: tweak cert email output * refactor: exclude Makefile from todo target (#382) * refactor: add stack to ResourceType mapping * refactor reporting for aws types * fix install.deps target * fix case where improper cert is created due to second-level domain (.co.uk). Closes #350 * fix hosted zone regression introduced by e8a33a3 * fix permission issues for static file serving. Closes #385 * docs: add domains command * docs: move policy behind a details element for collapsing * docs: tweak for domain changes v0.2.10 / 2017-10-13 ==================== * add flushing of proxy logs after response. Closes #370 * add periodic flushing of proxy logs for `up start`. Closes #369 * add internal text handler to `up start` v0.2.9 / 2017-10-10 =================== * fix: disable relay keep alive conns, they interact poorly with suspension (#365) v0.2.8 / 2017-10-09 =================== * fix missing body regression v0.2.7 / 2017-10-09 =================== * update go-apex dep * update lambda shim with concurrency support * fix: implement proxy GetBody to allow for re-reading request bodies. Closes #363 * remove .lambda.timeout, replace with .proxy.timeout v0.2.6 / 2017-09-29 =================== * add `proxy.retry` option defaulting to `true` * add UP_STAGE to `up start` * add stage `.path` basepath support * fix install script for Yosemite. Closes #345 v0.2.5 / 2017-09-20 =================== * add more relay logs * docs: refactor * add .proxy.timeout for requests and retries. Closes #335 * refactor: remove a duplicate test * add retrying of 5xx errors for idempotent requests. Closes #214 * docs: change chown to bin only. Closes #337 * docs: add deletion info * docs: add more stage info * docs: add guide for full app * docs: add note about CF provisioning * docs: add stage section * docs: refactor dns section * docs: remove references to `certs` * docs: remove "coming soon" * docs: tweak faq * docs: add vendor mention * update Bowery/prompt dep and fix spacing * fix 404 checksum not found (#331) * docs: add missing package comments * docs: add missing package comments * docs: add note about omitting proxy bin changes v0.2.4 / 2017-09-15 =================== * add custom domain support * add Up version to the -v debug output * add support for JSON log lines, captured and translated to the internal format * add support for indented log lines to be captured as a single message * add sub-process cleanup and grace period. Closes #311 * add `ssm:GetParametersByPath` to the function policy * add UP_STAGE env var. Closes #200 * change default `proxy.listen_timeout` to 15 * fix gzip handling when previously compressed. Closes #328 * fix ignoring of .pypath v0.2.3 / 2017-09-05 =================== * fix rewrite content-type. Closes #304 v0.2.2 / 2017-09-05 =================== * add logging of log query for debugging * add stage shorthands to log grammar. Closes #286 * add bytes / duration units to logging grammar. Closes #283 * add humanization of .size field in logs. Closes #252 * add support for checking domain availability and registration. Closes #159 * add support for multiple hook commands with arrays. Closes #127 * add forced inclusion of ./server * add eu-west-2 to the regions list. Closes #280 * fix ignoring of node_modules dotfiles (removed .bin by accident etc) * fix stage validation, move before building zip * fix support for other authentication schemes. Closes #287 * fix dns record .ttl default * rename .proxy.timeout to .proxy.listen_timeout (BREAKING) * remove `docs` command * remove omission of stage from logs v0.2.1 / 2017-08-25 =================== * fix missing param in Infof log call, outputting `MISSING` v0.2.0 / 2017-08-25 =================== * add hiding of cursor for stack delete and apply * add support for configuring proxy timeout (#273) * add cost to metrics output. Closes #204 * add: ignore dotfiles by default * add nicer formatting for numeric metrics * add build command. Closes #257 * add validation of stage name to `url` and `deploy`. Closes #261 * remove .npmignore support. Closes #270 v0.1.12 / 2017-08-23 ===================== * add some basic formatting to `up stack plan` * rename `up stack show` to `up stack status` * fix hard-coded versions for stack updates v0.1.11 / 2017-08-22 ==================== * add support for regions defined in `~/.aws/config` * add `up stack plan` and `up stack apply` support. Closes #115 * add environment variables to hooks when performing builds etc * fix support for implicit `app.js` when `package.json` is present without a `start` script defined v0.1.10 / 2017-08-15 ==================== * add default of ./server back for when source is omitted (main.go for example) * add `**` .upignore support * add forced inclusion of Up's required files * add support for omitting `node_modules` when using Browserify or Webpack * update go-archive for gitignore parity improvements v0.1.9 / 2017-08-14 =================== * add -modtime 0 * add smaller progress bar for initial stack * revert "add error when a dir does not look like a valid project. Closes #197" * caused an issue if you ignore *.go for example, not robust enough v0.1.8 / 2017-08-14 =================== * add error when a dir does not look like a valid project. Closes #197 * add convenience make targets `install` and `install.deps` * add note about AWS_PROFILE in getting started. Closes #230 * add python projects with a requirements.txt * add install.sh * fix greedy default error page, add option to explicitly enable. Closes #233 * fix exec bit on windows. Closes #225 * fix python overriding of custom command * remove default of ./server * remove "-api" suffix from IAM role (breaking change) * refactor NewLogs() to properly delegate the error instead of panic v0.1.7 / 2017-08-12 =================== * add size of code/zip before attempting deploy. Closes #222 * add better description for --force * change default timeout to 15s from 5s * change default memory from 128 to 512 (Node.js require() is slow) * fix relay timeout (lack of an error) ================================================ FILE: LICENSE ================================================ The MIT License Copyright (c) 2020 TJ Holowaychuk tj@tjholowaychuk.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ GO ?= go # Build all files. build: @echo "==> Building" @$(GO) generate ./... .PHONY: build # Install from source. install: @echo "==> Installing up ${GOPATH}/bin/up" @$(GO) install ./... .PHONY: install # Run all tests. test: internal/proxy/bin/bin_assets.go @$(GO) test -timeout 2m ./... && echo "\n==>\033[32m Ok\033[m\n" .PHONY: test # Run all tests in CI. test.ci: internal/proxy/bin/bin_assets.go @$(GO) test -v -timeout 5m ./... && echo "\n==>\033[32m Ok\033[m\n" .PHONY: test.ci internal/proxy/bin/bin_assets.go: @$(GO) generate ./... # Show source statistics. cloc: @cloc -exclude-dir=vendor,node_modules . .PHONY: cloc # Release binaries to GitHub. release: build @echo "==> Releasing" @goreleaser -p 1 --rm-dist --config .goreleaser.yml @echo "==> Complete" .PHONY: release # Show to-do items per file. todo: @rg TODO: .PHONY: todo # Show size of imports. size: @curl -sL https://gist.githubusercontent.com/tj/04e0965e23da00ca33f101e5b2ed4ed4/raw/9aa16698b2bc606cf911219ea540972edef05c4b/gistfile1.txt | bash .PHONY: size # Clean. clean: @rm -fr \ dist \ internal/proxy/bin/bin_assets.go \ internal/shim/bindata.go .PHONY: clean ================================================ FILE: Readme.md ================================================ ![](assets/title.png) 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. 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! 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. ## About 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. 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. 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/). ![](assets/screen2.png) ## OSS Features Features of the free open-source edition. ![Open source edition features](assets/features-community.png) ## Pro Features 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. ![Pro edition features](assets/features-pro.png) [![](https://gui.apex.sh/component?name=ShadowButton&config=%7B%22text%22%3A%22SUBSCRIBE%22%2C%22color%22%3A%227956EF%22%7D)](https://apex.sh/docs/up/guides/#subscribing_to_up_pro) ## Quick Start Install Up: ``` $ curl -sf https://up.apex.sh/install | sh ``` Create an `app.js` file: ```js require('http').createServer((req, res) => { res.end('Hello World\n') }).listen(process.env.PORT) ``` Deploy the app: ``` $ up ``` Open it in the browser, or copy the url to your clipboard: ``` $ up url -o $ up url -c ``` ================================================ FILE: cmd/up/main.go ================================================ package main import ( "errors" "os" "runtime" "github.com/stripe/stripe-go" "github.com/tj/go/env" "github.com/tj/go/term" // commands _ "github.com/apex/up/internal/cli/build" _ "github.com/apex/up/internal/cli/config" _ "github.com/apex/up/internal/cli/deploy" _ "github.com/apex/up/internal/cli/disable-stats" _ "github.com/apex/up/internal/cli/docs" _ "github.com/apex/up/internal/cli/domains" _ "github.com/apex/up/internal/cli/logs" _ "github.com/apex/up/internal/cli/metrics" _ "github.com/apex/up/internal/cli/prune" _ "github.com/apex/up/internal/cli/run" _ "github.com/apex/up/internal/cli/stack" _ "github.com/apex/up/internal/cli/start" _ "github.com/apex/up/internal/cli/team" _ "github.com/apex/up/internal/cli/upgrade" _ "github.com/apex/up/internal/cli/url" _ "github.com/apex/up/internal/cli/version" "github.com/apex/up/internal/cli/app" "github.com/apex/up/internal/signal" "github.com/apex/up/internal/stats" "github.com/apex/up/internal/util" ) var version = "master" func main() { signal.Add(reset) stripe.Key = env.GetDefault("STRIPE_KEY", "pk_live_23pGrHcZ2QpfX525XYmiyzmx") stripe.LogLevel = 0 err := run() if err == nil { return } term.ShowCursor() switch { case util.IsNoCredentials(err): util.Fatal(errors.New("Cannot find credentials, visit https://apex.sh/docs/up/credentials/ for help.")) default: util.Fatal(err) } } // run the cli. func run() error { stats.SetProperties(map[string]interface{}{ "os": runtime.GOOS, "arch": runtime.GOARCH, "version": version, "ci": os.Getenv("CI") == "true" || os.Getenv("CI") == "1", }) return app.Run(version) } // reset cursor. func reset() error { term.ShowCursor() println() return nil } ================================================ FILE: cmd/up-proxy/main.go ================================================ package main import ( "os" "time" "github.com/apex/go-apex" "github.com/apex/log" "github.com/apex/log/handlers/json" "github.com/apex/up" "github.com/apex/up/handler" "github.com/apex/up/internal/logs" "github.com/apex/up/internal/proxy" "github.com/apex/up/internal/util" "github.com/apex/up/platform/aws/runtime" ) func main() { start := time.Now() stage := os.Getenv("UP_STAGE") // setup logging log.SetHandler(json.Default) if s := os.Getenv("LOG_LEVEL"); s != "" { log.SetLevelFromString(s) } log.Log = log.WithFields(logs.Fields()) log.Info("initializing") // read config c, err := up.ReadConfig("up.json") if err != nil { log.Fatalf("error reading config: %s", err) } ctx := log.WithFields(log.Fields{ "name": c.Name, "type": c.Type, }) // init project p := runtime.New(c) // init runtime if err := p.Init(stage); err != nil { ctx.Fatalf("error initializing: %s", err) } // overrides if err := c.Override(stage); err != nil { ctx.Fatalf("error overriding: %s", err) } // create handler h, err := handler.FromConfig(c) if err != nil { ctx.Fatalf("error creating handler: %s", err) } // init handler h, err = handler.New(c, h) if err != nil { ctx.Fatalf("error initializing handler: %s", err) } // serve log.WithField("duration", util.MillisecondsSince(start)).Info("initialized") apex.Handle(proxy.NewHandler(h)) } ================================================ FILE: config/backoff.go ================================================ package config import ( "time" "github.com/tj/backoff" ) // Backoff config. type Backoff struct { // Min time in milliseconds. Min int `json:"min"` // Max time in milliseconds. Max int `json:"max"` // Factor applied for every attempt. Factor float64 `json:"factor"` // Attempts performed before failing. Attempts int `json:"attempts"` // Jitter is applied when true. Jitter bool `json:"jitter"` } // Default implementation. func (b *Backoff) Default() error { if b.Min == 0 { b.Min = 100 } if b.Max == 0 { b.Max = 500 } if b.Factor == 0 { b.Factor = 2 } if b.Attempts == 0 { b.Attempts = 3 } return nil } // Backoff returns the backoff from config. func (b *Backoff) Backoff() *backoff.Backoff { return &backoff.Backoff{ Min: time.Duration(b.Min) * time.Millisecond, Max: time.Duration(b.Max) * time.Millisecond, Factor: b.Factor, Jitter: b.Jitter, } } ================================================ FILE: config/backoff_test.go ================================================ package config import ( "testing" "time" "github.com/tj/assert" ) func TestBackoff_Default(t *testing.T) { a := &Backoff{} assert.NoError(t, a.Default(), "default") b := &Backoff{ Min: 100, Max: 500, Factor: 2, Attempts: 3, } assert.Equal(t, b, a) } func TestBackoff_Backoff(t *testing.T) { a := &Backoff{} assert.NoError(t, a.Default(), "default") b := a.Backoff() assert.Equal(t, time.Millisecond*100, b.Min) assert.Equal(t, time.Millisecond*500, b.Max) } ================================================ FILE: config/config.go ================================================ package config import ( "encoding/json" "io/ioutil" "os" "github.com/apex/log" "github.com/pkg/errors" "github.com/apex/up/internal/header" "github.com/apex/up/internal/inject" "github.com/apex/up/internal/redirect" "github.com/apex/up/internal/validate" "github.com/apex/up/platform/aws/regions" "github.com/aws/aws-sdk-go/aws/session" ) // defaulter is the interface that provides config defaulting. type defaulter interface { Default() error } // validator is the interface that provides config validation. type validator interface { Validate() error } // Config for the project. type Config struct { Name string `json:"name"` Description string `json:"description"` Type string `json:"type"` Headers header.Rules `json:"headers"` Redirects redirect.Rules `json:"redirects"` Hooks Hooks `json:"hooks"` Environment Environment `json:"environment"` Regions []string `json:"regions"` Profile string `json:"profile"` Inject inject.Rules `json:"inject"` Lambda Lambda `json:"lambda"` CORS *CORS `json:"cors"` ErrorPages ErrorPages `json:"error_pages"` Proxy Relay `json:"proxy"` Static Static `json:"static"` Logs Logs `json:"logs"` Stages Stages `json:"stages"` DNS DNS `json:"dns"` } // Validate implementation. func (c *Config) Validate() error { if err := validate.RequiredString(c.Name); err != nil { return errors.Wrap(err, ".name") } if err := validate.Name(c.Name); err != nil { return errors.Wrapf(err, ".name %q", c.Name) } if err := validate.List(c.Type, []string{"static", "server"}); err != nil { return errors.Wrap(err, ".type") } if err := validate.Lists(c.Regions, regions.IDs); err != nil { return errors.Wrap(err, ".regions") } if err := c.DNS.Validate(); err != nil { return errors.Wrap(err, ".dns") } if err := c.Static.Validate(); err != nil { return errors.Wrap(err, ".static") } if err := c.Inject.Validate(); err != nil { return errors.Wrap(err, ".inject") } if err := c.Lambda.Validate(); err != nil { return errors.Wrap(err, ".lambda") } if err := c.Proxy.Validate(); err != nil { return errors.Wrap(err, ".proxy") } if err := c.Stages.Validate(); err != nil { return errors.Wrap(err, ".stages") } if len(c.Regions) > 1 { return errors.New("multiple regions is not yet supported, see https://github.com/apex/up/issues/134") } return nil } // Default implementation. func (c *Config) Default() error { if c.Stages == nil { c.Stages = make(Stages) } // we default stages here before others simply to // initialize the default stages such as "development" // allowing runtime inference to default values. if err := c.Stages.Default(); err != nil { return errors.Wrap(err, ".stages") } // TODO: hack, move to the instantiation of aws clients if c.Profile != "" { setProfile(c.Profile) } // default type to server if c.Type == "" { c.Type = "server" } // runtime defaults if c.Type != "static" { runtime := inferRuntime() log.WithField("type", runtime).Debug("inferred runtime") if err := runtimeConfig(runtime, c); err != nil { return errors.Wrap(err, "runtime") } } // default .regions if err := c.defaultRegions(); err != nil { return errors.Wrap(err, ".region") } // region globbing c.Regions = regions.Match(c.Regions) // default .proxy if err := c.Proxy.Default(); err != nil { return errors.Wrap(err, ".proxy") } // default .lambda if err := c.Lambda.Default(); err != nil { return errors.Wrap(err, ".lambda") } // default .dns if err := c.DNS.Default(); err != nil { return errors.Wrap(err, ".dns") } // default .logs if err := c.Logs.Default(); err != nil { return errors.Wrap(err, ".logs") } // default .inject if err := c.Inject.Default(); err != nil { return errors.Wrap(err, ".inject") } // default .error_pages if err := c.ErrorPages.Default(); err != nil { return errors.Wrap(err, ".error_pages") } // default .stages if err := c.Stages.Default(); err != nil { return errors.Wrap(err, ".stages") } return nil } // Override with stage config if present, and re-validate. func (c *Config) Override(stage string) error { s := c.Stages.GetByName(stage) if s == nil { return nil } s.Override(c) return c.Validate() } // defaultRegions checks AWS_REGION and falls back on us-west-2. func (c *Config) defaultRegions() error { if len(c.Regions) != 0 { log.Debugf("%d regions from config", len(c.Regions)) return nil } s, err := session.NewSessionWithOptions(session.Options{ SharedConfigState: session.SharedConfigEnable, }) if err != nil { return errors.Wrap(err, "creating session") } if r := *s.Config.Region; r != "" { log.Debugf("region from aws shared config %q", r) c.Regions = append(c.Regions, r) return nil } r := "us-west-2" log.Debugf("region defaulted to %q", r) c.Regions = append(c.Regions, r) return nil } // ParseConfig returns config from JSON bytes. func ParseConfig(b []byte) (*Config, error) { c := &Config{} if err := json.Unmarshal(b, c); err != nil { return nil, errors.Wrap(err, "parsing json") } if err := c.Default(); err != nil { return nil, errors.Wrap(err, "defaulting") } if err := c.Validate(); err != nil { return nil, errors.Wrap(err, "validating") } return c, nil } // ParseConfigString returns config from JSON string. func ParseConfigString(s string) (*Config, error) { return ParseConfig([]byte(s)) } // MustParseConfigString returns config from JSON string. func MustParseConfigString(s string) *Config { c, err := ParseConfigString(s) if err != nil { panic(err) } return c } // ReadConfig reads the configuration from `path`. func ReadConfig(path string) (*Config, error) { b, err := ioutil.ReadFile(path) if err != nil { return nil, err } return ParseConfig(b) } // setProfile sets the AWS_PROFILE. func setProfile(name string) { os.Setenv("AWS_PROFILE", name) } ================================================ FILE: config/config_test.go ================================================ package config import ( "io/ioutil" "os" "testing" "github.com/tj/assert" ) func TestConfig_Name(t *testing.T) { t.Run("valid", func(t *testing.T) { c := Config{ Name: "my-app123", } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") }) t.Run("invalid", func(t *testing.T) { c := Config{ Name: "my app", } assert.NoError(t, c.Default(), "default") assert.EqualError(t, c.Validate(), `.name "my app": must contain only lowercase alphanumeric characters and '-'`) }) t.Run("invalid", func(t *testing.T) { c := Config{ Name: "MYAPP", } assert.NoError(t, c.Default(), "default") assert.EqualError(t, c.Validate(), `.name "MYAPP": must contain only lowercase alphanumeric characters and '-'`) }) } func TestConfig_Type(t *testing.T) { t.Run("default", func(t *testing.T) { c := Config{ Name: "api", } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") assert.Equal(t, "server", c.Type) }) t.Run("valid", func(t *testing.T) { c := Config{ Name: "api", Type: "server", } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") }) t.Run("invalid", func(t *testing.T) { c := Config{ Name: "api", Type: "something", } assert.NoError(t, c.Default(), "default") assert.EqualError(t, c.Validate(), `.type: "something" is invalid, must be one of: • static • server`) }) } func TestConfig_Regions(t *testing.T) { t.Skip() t.Run("valid multiple", func(t *testing.T) { c := Config{ Name: "api", Type: "server", Regions: []string{"us-west-2", "us-east-1"}, } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") }) t.Run("valid multiple", func(t *testing.T) { c := Config{ Name: "api", Type: "server", Regions: []string{"us-west-2", "us-east-1"}, } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") }) t.Run("valid globbing", func(t *testing.T) { c := Config{ Name: "api", Type: "server", Regions: []string{"us-*", "us-east-1", "ca-central-*"}, } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") assert.Equal(t, []string{"us-east-2", "us-east-1", "us-west-1", "us-west-2", "us-east-1", "ca-central-1"}, c.Regions) }) t.Run("invalid globbing", func(t *testing.T) { c := Config{ Name: "api", Type: "server", Regions: []string{"uss-*"}, } assert.NoError(t, c.Default(), "default") assert.EqualError(t, c.Validate(), `.regions: "uss-*" is invalid, must be one of: • us-east-2 • us-east-1 • us-west-1 • us-west-2 • ap-south-1 • ap-northeast-2 • ap-southeast-1 • ap-southeast-2 • ap-northeast-1 • ca-central-1 • eu-central-1 • eu-west-1 • eu-west-2 • eu-west-3 • sa-east-1`) }) t.Run("invalid", func(t *testing.T) { c := Config{ Name: "api", Type: "server", Regions: []string{"us-west-1", "us-west-9"}, } assert.NoError(t, c.Default(), "default") assert.EqualError(t, c.Validate(), `.regions: "us-west-9" is invalid, must be one of: • us-east-2 • us-east-1 • us-west-1 • us-west-2 • ap-south-1 • ap-northeast-2 • ap-southeast-1 • ap-southeast-2 • ap-northeast-1 • ca-central-1 • eu-central-1 • eu-west-1 • eu-west-2 • eu-west-3 • sa-east-1`) }) } func TestConfig_defaultRegions(t *testing.T) { t.Run("regions from config", func(t *testing.T) { regions := []string{"us-east-1"} c := Config{ Name: "api", Type: "server", Regions: regions, } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.defaultRegions(), "defaultRegions") assert.Equal(t, 1, len(c.Regions), "regions should have length 2") assert.Equal(t, regions, c.Regions, "should read regions from config") assert.NoError(t, c.Validate(), "validate") }) t.Run("regions from AWS_REGION", func(t *testing.T) { region := "sa-east-1" os.Setenv("AWS_REGION", region) defer os.Setenv("AWS_REGION", "") c := Config{ Name: "api", Type: "server", } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.defaultRegions(), "defaultRegions") assert.Equal(t, 1, len(c.Regions), "regions should have length 1") assert.Equal(t, region, c.Regions[0], "should read regions from AWS_REGION") assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") }) t.Run("regions from AWS_DEFAULT_REGION", func(t *testing.T) { region := "sa-east-1" os.Setenv("AWS_DEFAULT_REGION", region) defer os.Setenv("AWS_DEFAULT_REGION", "") c := Config{ Name: "api", Type: "server", } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.defaultRegions(), "defaultRegions") assert.Equal(t, 1, len(c.Regions), "regions should have length 1") assert.Equal(t, region, c.Regions[0], "should read regions from AWS_DEFAULT_REGION") assert.NoError(t, c.Validate(), "validate") }) t.Run("regions from shared config with default profile", func(t *testing.T) { content := ` [default] region = sa-east-1 output = json [profile another-profile] region = ap-southeast-2 output = json` tmpfile, err := ioutil.TempFile("", "config") assert.NoError(t, err) defer os.Remove(tmpfile.Name()) _, err = tmpfile.WriteString(content) assert.NoError(t, err) os.Setenv("AWS_CONFIG_FILE", tmpfile.Name()) defer os.Setenv("AWS_CONFIG_FILE", "") c := Config{ Name: "api", Type: "server", } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.defaultRegions(), "defaultRegions") assert.Equal(t, 1, len(c.Regions), "regions should have length 1") assert.Equal(t, "sa-east-1", c.Regions[0], "should read regions from shared config with default profile") assert.NoError(t, c.Validate(), "validate") }) t.Run("regions from shared config with AWS_PROFILE profile", func(t *testing.T) { content := ` [default] region = sa-east-1 output = json [profile another-profile] region = ap-southeast-2 output = json` tmpfile, err := ioutil.TempFile("", "config") assert.NoError(t, err) defer os.Remove(tmpfile.Name()) _, err = tmpfile.WriteString(content) assert.NoError(t, err) os.Setenv("AWS_CONFIG_FILE", tmpfile.Name()) defer os.Setenv("AWS_CONFIG_FILE", "") os.Setenv("AWS_PROFILE", "another-profile") defer os.Setenv("AWS_PROFILE", "") c := Config{ Name: "api", Type: "server", } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.defaultRegions(), "defaultRegions") assert.Equal(t, 1, len(c.Regions), "regions should have length 1") assert.Equal(t, "ap-southeast-2", c.Regions[0], "should read regions from shared config with AWS_PROFILE profile") assert.NoError(t, c.Validate(), "validate") }) t.Run("default region must be us-west-2", func(t *testing.T) { // Make sure we aren't reading AWS config file os.Setenv("AWS_CONFIG_FILE", "does-not-exist") c := Config{ Name: "api", Type: "server", } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.defaultRegions(), "defaultRegions") assert.Equal(t, 1, len(c.Regions), "regions should have length 1") assert.Equal(t, "us-west-2", c.Regions[0], "default region must be us-west-2") assert.NoError(t, c.Validate(), "validate") }) } ================================================ FILE: config/cors.go ================================================ package config // CORS configuration. type CORS struct { // AllowedOrigins is a list of origins a cross-domain request can be executed from. // If the special "*" value is present in the list, all origins will be allowed. // An origin may contain a wildcard (*) to replace 0 or more characters // (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty. // Only one wildcard can be used per origin. // Default value is ["*"] AllowedOrigins []string `json:"allowed_origins"` // AllowedMethods is a list of methods the client is allowed to use with // cross-domain requests. Default value is simple methods (GET and POST) AllowedMethods []string `json:"allowed_methods"` // AllowedHeaders is list of non simple headers the client is allowed to use with // cross-domain requests. // If the special "*" value is present in the list, all headers will be allowed. // Default value is [] but "Origin" is always appended to the list. AllowedHeaders []string `json:"allowed_headers"` // ExposedHeaders indicates which headers are safe to expose to the API of a CORS // API specification ExposedHeaders []string `json:"exposed_headers"` // AllowCredentials indicates whether the request can include user credentials like // cookies, HTTP authentication or client side SSL certificates. AllowCredentials bool `json:"allow_credentials"` // MaxAge indicates how long (in seconds) the results of a preflight request // can be cached. MaxAge int `json:"max_age"` // Debugging flag adds additional output to debug server side CORS issues Debug bool `json:"debug"` } ================================================ FILE: config/dns.go ================================================ package config import ( "encoding/json" "github.com/apex/up/internal/validate" "github.com/pkg/errors" ) // recordTypes is a list of valid record types. var recordTypes = []string{ "ALIAS", "A", "AAAA", "CNAME", "MX", "NAPTR", "NS", "PTR", "SOA", "SPF", "SRV", "TXT", } // DNS config. type DNS struct { Zones []*Zone `json:"zones"` } // UnmarshalJSON implementation. func (d *DNS) UnmarshalJSON(b []byte) error { var zones map[string][]*Record if err := json.Unmarshal(b, &zones); err != nil { return err } for name, records := range zones { zone := &Zone{Name: name, Records: records} d.Zones = append(d.Zones, zone) } return nil } // Default implementation. func (d *DNS) Default() error { for _, z := range d.Zones { if err := z.Default(); err != nil { return errors.Wrapf(err, "zone %s", z.Name) } } return nil } // Validate implementation. func (d *DNS) Validate() error { for _, z := range d.Zones { if err := z.Validate(); err != nil { return errors.Wrapf(err, "zone %s", z.Name) } } return nil } // Zone is a DNS zone. type Zone struct { Name string `json:"name"` Records []*Record `json:"records"` } // Default implementation. func (z *Zone) Default() error { for i, r := range z.Records { if err := r.Default(); err != nil { return errors.Wrapf(err, "record %d", i) } } return nil } // Validate implementation. func (z *Zone) Validate() error { for i, r := range z.Records { if err := r.Validate(); err != nil { return errors.Wrapf(err, "record %d", i) } } return nil } // Record is a DNS record. type Record struct { Name string `json:"name"` Type string `json:"type"` TTL int `json:"ttl"` Value []string `json:"value"` } // Validate implementation. func (r *Record) Validate() error { if err := validate.List(r.Type, recordTypes); err != nil { return errors.Wrap(err, ".type") } if err := validate.RequiredString(r.Name); err != nil { return errors.Wrap(err, ".name") } if err := validate.RequiredStrings(r.Value); err != nil { return errors.Wrap(err, ".value") } if err := validate.MinStrings(r.Value, 1); err != nil { return errors.Wrap(err, ".value") } return nil } // Default implementation. func (r *Record) Default() error { if r.TTL == 0 { r.TTL = 300 } return nil } ================================================ FILE: config/dns_test.go ================================================ package config import ( "encoding/json" "log" "os" "sort" "testing" "github.com/tj/assert" ) func ExampleDNS() { s := `{ "something.sh": [ { "name": "something.com", "type": "A", "ttl": 60, "value": ["35.161.83.243"] }, { "name": "blog.something.com", "type": "CNAME", "ttl": 60, "value": ["34.209.172.67"] }, { "name": "api.something.com", "type": "A", "value": ["54.187.185.18"] } ] }` var c DNS if err := json.Unmarshal([]byte(s), &c); err != nil { log.Fatalf("error unmarshaling: %s", err) } sort.Slice(c.Zones[0].Records, func(i int, j int) bool { a := c.Zones[0].Records[i] b := c.Zones[0].Records[j] return a.Name > b.Name }) if err := c.Validate(); err != nil { log.Fatalf("error validating: %s", err) } if err := c.Default(); err != nil { log.Fatalf("error defaulting: %s", err) } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") enc.Encode(c) // Output: // { // "zones": [ // { // "name": "something.sh", // "records": [ // { // "name": "something.com", // "type": "A", // "ttl": 60, // "value": [ // "35.161.83.243" // ] // }, // { // "name": "blog.something.com", // "type": "CNAME", // "ttl": 60, // "value": [ // "34.209.172.67" // ] // }, // { // "name": "api.something.com", // "type": "A", // "ttl": 300, // "value": [ // "54.187.185.18" // ] // } // ] // } // ] // } } func TestDNS_Validate(t *testing.T) { t.Run("invalid", func(t *testing.T) { c := &DNS{ Zones: []*Zone{ { Name: "apex.sh", Records: []*Record{ { Name: "blog.apex.sh", Type: "CNAME", }, }, }, }, } assert.EqualError(t, c.Validate(), `zone apex.sh: record 0: .value: must have at least 1 value`) }) } func TestRecord_Type(t *testing.T) { t.Run("valid", func(t *testing.T) { c := &Record{ Name: "blog.apex.sh", Type: "A", Value: []string{"1.1.1.1"}, } assert.NoError(t, c.Validate(), "validate") }) t.Run("invalid", func(t *testing.T) { c := &Record{ Name: "blog.apex.sh", Type: "AAA", } assert.EqualError(t, c.Validate(), `.type: "AAA" is invalid, must be one of: • ALIAS • A • AAAA • CNAME • MX • NAPTR • NS • PTR • SOA • SPF • SRV • TXT`) }) } func TestRecord_TTL(t *testing.T) { c := &Record{Type: "A"} assert.NoError(t, c.Default(), "default") assert.Equal(t, 300, c.TTL) } func TestRecord_Value(t *testing.T) { t.Run("empty", func(t *testing.T) { c := &Record{ Name: "blog.apex.sh", Type: "A", } assert.EqualError(t, c.Validate(), `.value: must have at least 1 value`) }) t.Run("invalid", func(t *testing.T) { c := &Record{ Name: "blog.apex.sh", Type: "A", Value: []string{"1.1.1.1", ""}, } assert.EqualError(t, c.Validate(), `.value: at index 1: is required`) }) } ================================================ FILE: config/doc.go ================================================ // Package config provides configuration structures, // validation, and defaulting for up.json config. package config ================================================ FILE: config/duration.go ================================================ package config import ( "bytes" "strconv" "time" ) // Duration may be specified as numerical seconds or // as a duration string such as "1.5m". type Duration time.Duration // Seconds returns the duration in seconds. func (d *Duration) Seconds() float64 { return float64(time.Duration(*d) / time.Second) } // UnmarshalJSON implementation. func (d *Duration) UnmarshalJSON(b []byte) error { if i, err := strconv.ParseInt(string(b), 10, 64); err == nil { *d = Duration(time.Second * time.Duration(i)) return nil } v, err := time.ParseDuration(string(bytes.Trim(b, `"`))) if err != nil { return err } *d = Duration(v) return nil } // MarshalJSON implement. func (d *Duration) MarshalJSON() ([]byte, error) { return []byte(strconv.Itoa(int(d.Seconds()))), nil } ================================================ FILE: config/duration_test.go ================================================ package config import ( "encoding/json" "testing" "time" "github.com/tj/assert" ) func TestDuration_UnmarshalJSON(t *testing.T) { t.Run("numeric seconds", func(t *testing.T) { s := `{ "timeout": 5 }` var c struct { Timeout Duration } err := json.Unmarshal([]byte(s), &c) assert.NoError(t, err, "unmarshal") assert.Equal(t, Duration(5*time.Second), c.Timeout) }) t.Run("string duration", func(t *testing.T) { s := `{ "timeout": "1.5m" }` var c struct { Timeout Duration } err := json.Unmarshal([]byte(s), &c) assert.NoError(t, err, "unmarshal") assert.Equal(t, Duration(90*time.Second), c.Timeout) }) } ================================================ FILE: config/environment.go ================================================ package config // Environment variables. type Environment map[string]string ================================================ FILE: config/errorpages.go ================================================ package config // ErrorPages configuration. type ErrorPages struct { // Enable error pages. Enable bool `json:"enable"` // Dir containing error pages. Dir string `json:"dir"` // Variables are passed to the template for use. Variables map[string]interface{} `json:"variables"` } // Default implementation. func (e *ErrorPages) Default() error { if e.Dir == "" { e.Dir = "." } return nil } ================================================ FILE: config/errorpages_test.go ================================================ package config import ( "testing" "github.com/tj/assert" ) func TestErrorPages(t *testing.T) { c := &ErrorPages{} assert.NoError(t, c.Default(), "default") assert.Equal(t, ".", c.Dir, "dir") } ================================================ FILE: config/hooks.go ================================================ package config import ( "encoding/json" "errors" ) // Hook is one or more commands. type Hook []string // Hooks for the project. type Hooks struct { Build Hook `json:"build"` Clean Hook `json:"clean"` PreBuild Hook `json:"prebuild"` PostBuild Hook `json:"postbuild"` PreDeploy Hook `json:"predeploy"` PostDeploy Hook `json:"postdeploy"` } // Override config. func (h *Hooks) Override(c *Config) { if v := h.Build; v != nil { c.Hooks.Build = v } if v := h.Clean; v != nil { c.Hooks.Clean = v } if v := h.PreBuild; v != nil { c.Hooks.PreBuild = v } if v := h.PostBuild; v != nil { c.Hooks.PostBuild = v } if v := h.PreDeploy; v != nil { c.Hooks.PreDeploy = v } if v := h.PostDeploy; v != nil { c.Hooks.PostDeploy = v } } // Get returns the hook by name or nil. func (h *Hooks) Get(s string) Hook { switch s { case "build": return h.Build case "clean": return h.Clean case "prebuild": return h.PreBuild case "postbuild": return h.PostBuild case "predeploy": return h.PreDeploy case "postdeploy": return h.PostDeploy default: return nil } } // UnmarshalJSON implementation. func (h *Hook) UnmarshalJSON(b []byte) error { switch b[0] { case '"': var s string if err := json.Unmarshal(b, &s); err != nil { return err } *h = append(*h, s) return nil case '[': return json.Unmarshal(b, (*[]string)(h)) default: return errors.New("hook must be a string or array of strings") } } // IsEmpty returns true if the hook is empty. func (h *Hook) IsEmpty() bool { return h == nil || len(*h) == 0 } ================================================ FILE: config/hooks_test.go ================================================ package config import ( "encoding/json" "testing" "github.com/tj/assert" ) func TestHook(t *testing.T) { t.Run("missing", func(t *testing.T) { s := []byte(`{}`) var c struct { Build Hook } err := json.Unmarshal(s, &c) assert.NoError(t, err, "unmarshal") assert.Equal(t, Hook(nil), c.Build) }) t.Run("invalid type", func(t *testing.T) { s := []byte(` { "build": 5 } `) var c struct { Build Hook } err := json.Unmarshal(s, &c) assert.EqualError(t, err, `hook must be a string or array of strings`) }) t.Run("string", func(t *testing.T) { s := []byte(` { "build": "go build main.go" } `) var c struct { Build Hook } err := json.Unmarshal(s, &c) assert.NoError(t, err, "unmarshal") assert.Equal(t, Hook{"go build main.go"}, c.Build) }) t.Run("array", func(t *testing.T) { s := []byte(` { "build": [ "go build main.go", "browserify src/index.js > app.js" ] } `) var c struct { Build Hook } err := json.Unmarshal(s, &c) assert.NoError(t, err, "unmarshal") assert.Equal(t, Hook{ "go build main.go", "browserify src/index.js > app.js", }, c.Build) }) } ================================================ FILE: config/lambda.go ================================================ package config // defaultRuntime is the default runtime. var defaultRuntime = "nodejs10.x" // defaultPolicy is the default function role policy. var defaultPolicy = IAMPolicyStatement{ "Effect": "Allow", "Resource": "*", "Action": []string{ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "ssm:GetParametersByPath", "ec2:CreateNetworkInterface", "ec2:DescribeNetworkInterfaces", "ec2:DeleteNetworkInterface", }, } // IAMPolicyStatement configuration. type IAMPolicyStatement map[string]interface{} // VPC configuration. type VPC struct { Subnets []string `json:"subnets"` SecurityGroups []string `json:"security_groups"` } // Lambda configuration. type Lambda struct { // Memory of the function. Memory int `json:"memory"` // Timeout of the function. Timeout int `json:"timeout"` // Role of the function. Role string `json:"role"` // Runtime of the function. Runtime string `json:"runtime"` // Policy of the function role. Policy []IAMPolicyStatement `json:"policy"` // VPC configuration. VPC *VPC `json:"vpc"` } // Default implementation. func (l *Lambda) Default() error { if l.Timeout == 0 { l.Timeout = 60 } if l.Memory == 0 { l.Memory = 512 } if l.Runtime == "" { l.Runtime = defaultRuntime } l.Policy = append(l.Policy, defaultPolicy) return nil } // Validate implementation. func (l *Lambda) Validate() error { return nil } // Override config. func (l *Lambda) Override(c *Config) { if l.Memory != 0 { c.Lambda.Memory = l.Memory } if l.Timeout != 0 { c.Lambda.Timeout = l.Timeout } if l.Role != "" { c.Lambda.Role = l.Role } if l.VPC != nil { c.Lambda.VPC = l.VPC } if l.Runtime != "" { c.Lambda.Runtime = l.Runtime } } ================================================ FILE: config/lambda_test.go ================================================ package config import ( "testing" "github.com/tj/assert" ) func TestLambda(t *testing.T) { c := &Lambda{} assert.NoError(t, c.Default(), "default") assert.Equal(t, 60, c.Timeout, "timeout") assert.Equal(t, 512, c.Memory, "timeout") } func TestLambda_Policy(t *testing.T) { t.Run("defaults", func(t *testing.T) { c := &Lambda{} assert.NoError(t, c.Default(), "default") assert.Len(t, c.Policy, 1) assert.Equal(t, defaultPolicy, c.Policy[0]) }) t.Run("specified", func(t *testing.T) { c := &Lambda{ Policy: []IAMPolicyStatement{ { "Effect": "Allow", "Resource": "*", "Action": []string{ "s3:List*", "s3:Get*", }, }, }, } assert.NoError(t, c.Default(), "default") assert.Len(t, c.Policy, 2) assert.Equal(t, defaultPolicy, c.Policy[1]) }) } ================================================ FILE: config/logs.go ================================================ package config // Logs configuration. type Logs struct { // Disable json log output. Disable bool `json:"disable"` // Stdout default log level. Stdout string `json:"stdout"` // Stderr default log level. Stderr string `json:"stderr"` } // Default implementation. func (l *Logs) Default() error { if l.Stdout == "" { l.Stdout = "info" } if l.Stderr == "" { l.Stderr = "error" } return nil } ================================================ FILE: config/relay.go ================================================ package config import ( "github.com/pkg/errors" ) // Relay config. type Relay struct { // Command run to start your server. Command string `json:"command"` // Timeout in seconds to wait for a response. Timeout int `json:"timeout"` // ListenTimeout in seconds when waiting for the app to bind to PORT. ListenTimeout int `json:"listen_timeout"` } // Default implementation. func (r *Relay) Default() error { if r.Command == "" { r.Command = "./server" } if r.Timeout == 0 { r.Timeout = 15 } if r.ListenTimeout == 0 { r.ListenTimeout = 15 } return nil } // Validate will try to perform sanity checks for this Relay configuration. func (r *Relay) Validate() error { if r.Command == "" { err := errors.New("should not be empty") return errors.Wrap(err, ".command") } if r.ListenTimeout <= 0 { err := errors.New("should be greater than 0") return errors.Wrap(err, ".listen_timeout") } if r.ListenTimeout > 25 { err := errors.New("should be <= 25") return errors.Wrap(err, ".listen_timeout") } if r.Timeout > 25 { err := errors.New("should be <= 25") return errors.Wrap(err, ".timeout") } return nil } // Override config. func (r *Relay) Override(c *Config) { if r.Command != "" { c.Proxy.Command = r.Command } } ================================================ FILE: config/runtimes.go ================================================ package config import ( "os" "github.com/apex/up/internal/util" "github.com/pkg/errors" ) // Runtime is an app runtime. type Runtime string // Runtimes available. const ( RuntimeUnknown Runtime = "unknown" RuntimeGo = "go" RuntimeNode = "node" RuntimeClojure = "clojure" RuntimeCrystal = "crystal" RuntimePython = "python" RuntimeStatic = "static" RuntimeJavaMaven = "java maven" RuntimeJavaGradle = "java gradle" ) // inferRuntime returns the runtime based on files present in the CWD. func inferRuntime() Runtime { switch { case util.Exists("main.go"): return RuntimeGo case util.Exists("main.cr"): return RuntimeCrystal case util.Exists("package.json"): return RuntimeNode case util.Exists("app.js"): return RuntimeNode case util.Exists("project.clj"): return RuntimeClojure case util.Exists("pom.xml"): return RuntimeJavaMaven case util.Exists("build.gradle"): return RuntimeJavaGradle case util.Exists("app.py"): return RuntimePython case util.Exists("index.html"): return RuntimeStatic default: return RuntimeUnknown } } // runtimeConfig performs config inferences based on what Up thinks the runtime is. func runtimeConfig(runtime Runtime, c *Config) error { switch runtime { case RuntimeGo: golang(c) case RuntimeClojure: clojureLein(c) case RuntimeJavaMaven: javaMaven(c) case RuntimeJavaGradle: javaGradle(c) case RuntimeCrystal: crystal(c) case RuntimePython: python(c) case RuntimeStatic: c.Type = "static" case RuntimeNode: if err := nodejs(c); err != nil { return err } } return nil } // golang config. func golang(c *Config) { if c.Hooks.Build.IsEmpty() { c.Hooks.Build = Hook{`GOOS=linux GOARCH=amd64 go build -o server *.go`} } if c.Hooks.Clean.IsEmpty() { c.Hooks.Clean = Hook{`rm server`} } if s := c.Stages.GetByName("development"); s != nil { if s.Proxy.Command == "" { s.Proxy.Command = "go run *.go" } } } // java gradle config. func javaGradle(c *Config) { if c.Proxy.Command == "" { c.Proxy.Command = "java -jar server.jar" } if c.Hooks.Build.IsEmpty() { // assumes build results in a shaded jar named server.jar if util.Exists("gradlew") { c.Hooks.Build = Hook{`./gradlew clean build && cp build/libs/server.jar .`} } else { c.Hooks.Build = Hook{`gradle clean build && cp build/libs/server.jar .`} } } if c.Hooks.Clean.IsEmpty() { c.Hooks.Clean = Hook{`rm server.jar && gradle clean`} } } // java maven config. func javaMaven(c *Config) { if c.Proxy.Command == "" { c.Proxy.Command = "java -jar server.jar" } if c.Hooks.Build.IsEmpty() { // assumes package results in a shaded jar named server.jar if util.Exists("mvnw") { c.Hooks.Build = Hook{`./mvnw clean package && cp target/server.jar .`} } else { c.Hooks.Build = Hook{`mvn clean package && cp target/server.jar .`} } } if c.Hooks.Clean.IsEmpty() { c.Hooks.Clean = Hook{`rm server.jar && mvn clean`} } } // clojure lein config. func clojureLein(c *Config) { if c.Proxy.Command == "" { c.Proxy.Command = "java -jar server.jar" } if c.Hooks.Build.IsEmpty() { // assumes package results in a shaded jar named server.jar c.Hooks.Build = Hook{`lein uberjar && cp target/*-standalone.jar server.jar`} } if c.Hooks.Clean.IsEmpty() { c.Hooks.Clean = Hook{`lein clean && rm server.jar`} } } // crystal config. func crystal(c *Config) { if c.Hooks.Build.IsEmpty() { c.Hooks.Build = Hook{`docker run --rm -v $(pwd):/src -w /src crystallang/crystal crystal build -o server main.cr --release --static`} } if c.Hooks.Clean.IsEmpty() { c.Hooks.Clean = Hook{`rm server`} } if s := c.Stages.GetByName("development"); s != nil { if s.Proxy.Command == "" { s.Proxy.Command = "crystal run main.cr" } } } // nodejs config. func nodejs(c *Config) error { var pkg struct { Scripts struct { Start string `json:"start"` Build string `json:"build"` } `json:"scripts"` } // read package.json if err := util.ReadFileJSON("package.json", &pkg); err != nil && !os.IsNotExist(errors.Cause(err)) { return err } // use "start" script unless explicitly defined in up.json if c.Proxy.Command == "" { if s := pkg.Scripts.Start; s == "" { c.Proxy.Command = `node app.js` } else { c.Proxy.Command = s } } // use "build" script unless explicitly defined in up.json if c.Hooks.Build.IsEmpty() { c.Hooks.Build = Hook{pkg.Scripts.Build} } return nil } // python config. func python(c *Config) { if c.Proxy.Command == "" { c.Proxy.Command = "python app.py" } // Only add build & clean hooks if a requirements.txt exists if !util.Exists("requirements.txt") { return } // Set PYTHONPATH env if c.Environment == nil { c.Environment = Environment{} } c.Environment["PYTHONPATH"] = ".pypath/" // Copy libraries into .pypath/ if c.Hooks.Build.IsEmpty() { c.Hooks.Build = Hook{`mkdir -p .pypath/ && pip install -r requirements.txt -t .pypath/`} } // Clean .pypath/ if c.Hooks.Clean.IsEmpty() { c.Hooks.Clean = Hook{`rm -r .pypath/`} } } ================================================ FILE: config/stages.go ================================================ package config import ( "sort" "github.com/apex/up/internal/validate" "github.com/pkg/errors" ) // defaultStages is a list of default stage names. var defaultStages = []string{ "development", "staging", "production", } // Stage config. type Stage struct { Domain string `json:"domain"` Zone interface{} `json:"zone"` Path string `json:"path"` Cert string `json:"cert"` Name string `json:"-"` StageOverrides } // IsLocal returns true if the stage represents a local environment. func (s *Stage) IsLocal() bool { return s.Name == "development" } // IsRemote returns true if the stage represents a remote environment. func (s *Stage) IsRemote() bool { return !s.IsLocal() } // Validate implementation. func (s *Stage) Validate() error { if err := validate.Stage(s.Name); err != nil { return errors.Wrap(err, ".name") } switch s.Zone.(type) { case bool, string: return nil default: return errors.Errorf(".zone is an invalid type, must be string or boolean") } } // Default implementation. func (s *Stage) Default() error { if s.Zone == nil { s.Zone = true } return nil } // StageOverrides config. type StageOverrides struct { Hooks Hooks `json:"hooks"` Lambda Lambda `json:"lambda"` Proxy Relay `json:"proxy"` } // Override config. func (s *StageOverrides) Override(c *Config) { s.Hooks.Override(c) s.Lambda.Override(c) s.Proxy.Override(c) } // Stages config. type Stages map[string]*Stage // Default implementation. func (s Stages) Default() error { // defaults for _, name := range defaultStages { if _, ok := s[name]; !ok { s[name] = &Stage{} } } // assign names for name, s := range s { s.Name = name } // defaults for _, s := range s { if err := s.Default(); err != nil { return errors.Wrapf(err, "stage %q", s.Name) } } return nil } // Validate implementation. func (s Stages) Validate() error { for _, s := range s { if err := s.Validate(); err != nil { return errors.Wrapf(err, "stage %q", s.Name) } } return nil } // List returns configured stages. func (s Stages) List() (v []*Stage) { for _, s := range s { v = append(v, s) } return } // Domains returns configured domains. func (s Stages) Domains() (v []string) { for _, s := range s.List() { if s.Domain != "" { v = append(v, s.Domain) } } return } // Names returns configured stage names. func (s Stages) Names() (v []string) { for _, s := range s.List() { v = append(v, s.Name) } sort.Strings(v) return } // RemoteNames returns configured remote stage names. func (s Stages) RemoteNames() (v []string) { for _, s := range s.List() { if s.IsRemote() { v = append(v, s.Name) } } sort.Strings(v) return } // GetByDomain returns the stage by domain or nil. func (s Stages) GetByDomain(domain string) *Stage { for _, s := range s.List() { if s.Domain == domain { return s } } return nil } // GetByName returns the stage by name or nil. func (s Stages) GetByName(name string) *Stage { for _, s := range s.List() { if s.Name == name { return s } } return nil } ================================================ FILE: config/stages_test.go ================================================ package config import ( "testing" "github.com/tj/assert" ) func TestStage_Override(t *testing.T) { c, err := ParseConfigString(`{ "name": "app", "regions": ["us-west-2"], "lambda": { "memory": 128 }, "hooks": { "build": "parcel index.html -o build", "clean": "rm -fr build" }, "proxy": { "command": "node app.js" }, "stages": { "production": { "lambda": { "memory": 1024 }, "hooks": { "build": "parcel index.html -o build --production" } }, "staging": { "proxy": { "command": "node app.js --foo=bar" } } } }`) assert.NoError(t, err, "parse") assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") assert.NoError(t, c.Override("production"), "override") assert.Equal(t, 1024, c.Lambda.Memory) assert.Equal(t, Hook{`parcel index.html -o build --production`}, c.Hooks.Build) assert.Equal(t, `node app.js`, c.Proxy.Command) assert.NoError(t, c.Override("staging"), "override") assert.Equal(t, `node app.js --foo=bar`, c.Proxy.Command) } func TestStages_Default(t *testing.T) { t.Run("no custom stages", func(t *testing.T) { s := Stages{} assert.NoError(t, s.Default(), "default") assert.NoError(t, s.Validate(), "validate") assert.Len(t, s, 3) assert.Equal(t, "staging", s["staging"].Name) assert.Equal(t, "production", s["production"].Name) }) t.Run("custom stages", func(t *testing.T) { s := Stages{ "beta": &Stage{}, } assert.NoError(t, s.Default(), "default") assert.NoError(t, s.Validate(), "validate") assert.Len(t, s, 4) assert.Equal(t, "beta", s["beta"].Name) assert.Equal(t, true, s["beta"].Zone) }) } func TestStages_Validate(t *testing.T) { t.Run("no stages", func(t *testing.T) { s := Stages{} assert.NoError(t, s.Validate(), "validate") }) t.Run("some stages", func(t *testing.T) { s := Stages{ "staging": &Stage{ Domain: "gh-polls-stage.com", }, "production": &Stage{ Domain: "gh-polls.com", }, } assert.NoError(t, s.Default(), "default") assert.NoError(t, s.Validate(), "validate") assert.Equal(t, "staging", s["staging"].Name) assert.Equal(t, "production", s["production"].Name) }) t.Run("valid zone boolean", func(t *testing.T) { s := Stages{ "production": &Stage{ Domain: "gh-polls.com", Zone: false, }, } assert.NoError(t, s.Default(), "default") assert.NoError(t, s.Validate(), "validate") }) t.Run("valid zone string", func(t *testing.T) { s := Stages{ "production": &Stage{ Domain: "api.gh-polls.com", Zone: "api.gh-polls.com", }, } assert.NoError(t, s.Default(), "default") assert.NoError(t, s.Validate(), "validate") }) t.Run("invalid zone type", func(t *testing.T) { s := Stages{ "production": &Stage{ Domain: "api.gh-polls.com", Zone: 123, }, } assert.NoError(t, s.Default(), "default") assert.EqualError(t, s.Validate(), `stage "production": .zone is an invalid type, must be string or boolean`) }) } func TestStages_List(t *testing.T) { stage := &Stage{ Domain: "gh-polls-stage.com", } prod := &Stage{ Domain: "gh-polls.com", } s := Stages{ "staging": stage, "production": prod, } list := []*Stage{ stage, prod, } stages := s.List() assert.Equal(t, list, stages) } func TestStages_GetByDomain(t *testing.T) { stage := &Stage{ Domain: "gh-polls-stage.com", } prod := &Stage{ Domain: "gh-polls.com", } s := Stages{ "staging": stage, "production": prod, } assert.Equal(t, prod, s.GetByDomain("gh-polls.com")) } ================================================ FILE: config/static.go ================================================ package config import ( "os" "github.com/pkg/errors" ) // Static configuration. type Static struct { // Dir containing static files. Dir string `json:"dir"` // Prefix is an optional URL prefix for serving static files. Prefix string `json:"prefix"` } // Validate implementation. func (s *Static) Validate() error { info, err := os.Stat(s.Dir) if os.IsNotExist(err) { return nil } if err != nil { return errors.Wrap(err, ".dir") } if !info.IsDir() { return errors.Errorf(".dir %s is not a directory", s.Dir) } return nil } ================================================ FILE: config/static_test.go ================================================ package config import ( "os" "testing" "github.com/tj/assert" ) func TestStatic(t *testing.T) { cwd, _ := os.Getwd() table := []struct { Static valid bool }{ {Static{Dir: cwd}, true}, {Static{Dir: cwd + "/static_test.go"}, false}, } for _, row := range table { if row.valid { assert.NoError(t, row.Validate()) } else { assert.Error(t, row.Validate()) } } } ================================================ FILE: docs/00-introduction.md ================================================ --- title: Introduction slug: introduction --- 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. 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. 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! ================================================ FILE: docs/01-installation.md ================================================ --- title: Installation slug: setup --- 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: ``` $ curl -sf https://up.apex.sh/install | sh ``` 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: ``` $ curl -sf https://up.apex.sh/install | BINDIR=. sh ``` Verify installation with: ``` $ up version ``` Later when you want to update `up` to the latest version use the following command: ``` $ up upgrade ``` If you hit permission issues, you may need to run the following, as `up` is installed to `/usr/local/bin/up` by default. ``` $ sudo chown -R $(whoami) /usr/local/bin/ ``` ================================================ FILE: docs/02-aws-credentials.md ================================================ --- title: AWS Credentials slug: credentials --- Before using Up you need to first provide your AWS account credentials so that Up is allowed to create resources on your behalf. ## AWS credential profiles 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). Here's an example of `~/.aws/credentials`, where `export AWS_PROFILE=myaccount` would activate these settings. ``` [myaccount] aws_access_key_id = xxxxxxxx aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxx ``` ### Best practices You may store the profile name in the `up.json` file itself as shown in the following snippet: ```json { "name": "appname-api", "profile": "myaccount" } ``` This is ideal as it ensures you will not accidentally deploy to a different AWS account. ## IAM policy for Up CLI 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. 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. ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "acm:*", "cloudformation:Create*", "cloudformation:Delete*", "cloudformation:Describe*", "cloudformation:ExecuteChangeSet", "cloudformation:Update*", "cloudfront:*", "cloudwatch:*", "ec2:*", "ecs:*", "events:*", "iam:AttachRolePolicy", "iam:CreatePolicy", "iam:CreateRole", "iam:DeleteRole", "iam:DeleteRolePolicy", "iam:GetRole", "iam:PassRole", "iam:PutRolePolicy", "lambda:AddPermission", "lambda:Create*", "lambda:Delete*", "lambda:Get*", "lambda:InvokeFunction", "lambda:List*", "lambda:RemovePermission", "lambda:Update*", "logs:Create*", "logs:Describe*", "logs:FilterLogEvents", "logs:Put*", "logs:Test*", "route53:*", "route53domains:*", "s3:*", "ssm:*", "sns:*" ], "Resource": "*" }, { "Effect": "Allow", "Action": "apigateway:*", "Resource": "arn:aws:apigateway:*::/*" } ] } ``` ================================================ FILE: docs/03-getting-started.md ================================================ --- title: Getting Started slug: getting-started --- 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`. 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: ```json { "name": "appname-api", "profile": "companyname", "regions": ["us-west-2"] } ``` 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: ```js const http = require('http') const { PORT = 3000 } = process.env http.createServer((req, res) => { res.end('Hello World from Node.js\n') }).listen(PORT) ``` Deploy it to the staging environment: ``` $ up ``` Open up the URL in your browser: ``` $ up url --open ``` Or test with curl: ``` $ curl `up url` ``` That's it! You've deployed a basic Up application. To view further help for commands use: ``` $ up help $ up help COMMAND $ up help COMMAND SUBCOMMAND ``` If you're not a Node.js developer here are some examples in additional languages. For Python create `app.py`: ```python from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer import os class myHandler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header('Content-type','text/html') self.end_headers() self.wfile.write("Hello World from Python\n") return server = HTTPServer(('', int(os.environ['PORT'])), myHandler) server.serve_forever() ``` For Golang create `main.go`: ```go package main import ( "os" "fmt" "log" "net/http" ) func main() { addr := ":"+os.Getenv("PORT") http.HandleFunc("/", hello) log.Fatal(http.ListenAndServe(addr, nil)) } func hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello World from Go") } ``` Finally for Crystal create `main.cr`: ```ruby require "http/server" port = ENV["PORT"].to_i server = HTTP::Server.new(port) do |ctx| ctx.response.content_type = "text/plain" ctx.response.print "Hello world from Crystal" end server.listen ``` ================================================ FILE: docs/04-configuration.md ================================================ --- title: Configuration slug: configuration --- Configuration for your app lives in the `up.json` within your project's directory. This section details each of the options available. ## Name The name of the application, which is used to name resources such as the Lambda function or API Gateway. ```json { "name": "api" } ``` ## Profile The `profile` property is equivalent to setting `AWS_PROFILE` for referencing AWS credentials in the `~/.aws` directory. Use of this property is preferred as it prevents accidents with environment variables. ```json { "profile": "someapp" } ``` ## Regions You may specify a target region for deployments using the `regions` array. By default "us-west-2" is used unless the `AWS_REGION` environment variable is defined. Note: Currently only a single region is supported, however you can deploy to many regions one at a time for global deploys, see https://medium.com/@tjholowaychuk/global-serverless-apps-with-aws-lambda-api-gateway-4642ef1f221d for details. A single region: ```json { "regions": ["us-west-2"] } ``` Currently Lambda supports the following regions: - **us-east-2** – US East (Ohio) - **us-east-1** – US East (N. Virginia) - **us-west-1** – US West (N. California) - **us-west-2** – US West (Oregon) - **ap-northeast-2** – Asia Pacific (Seoul) - **ap-south-1** – Asia Pacific (Mumbai) - **ap-southeast-1** – Asia Pacific (Singapore) - **ap-southeast-2** – Asia Pacific (Sydney) - **ap-northeast-1** – Asia Pacific (Tokyo) - **ca-central-1** – Canada (Central) - **eu-central-1** – EU (Frankfurt) - **eu-west-1** – EU (Ireland) - **eu-west-2** – EU (London) - **eu-west-3** – EU (Paris) - **eu-north-1** – EU (Stockholm) - **sa-east-1** – South America (São Paulo) ## Lambda settings The following Lambda-specific settings are available: - `role` – IAM role ARN, defaulting to the one Up creates for you - `memory` – Function memory in mb (Default `512`, Min `128`, Max `3008`) - `policy` – IAM function policy statement(s) - `runtime` — Lambda function runtime. (Default `nodejs10.x`) - `vpc` - VPC subnets and security groups For example: ```json { "name": "api", "lambda": { "memory": 512, "runtime": "nodejs8.10", "vpc": { "subnets": [ "subnet-aaaaaaa", "subnet-bbbbbbb", "subnet-ccccccc", ], "security_groups": [ "sg-xxxxxxx" ] } } } ``` The Lambda `memory` setting also scales the CPU, if your app is slow, or for cases such as larger Node applications with many `require()`s you may need to increase this value. View the [Lambda Pricing](https://aws.amazon.com/lambda/pricing/) page for more information regarding the `memory` setting. Using Up Pro in a VPC requires access to the that the AWS SSM Parameter Store API for environment variables, otherwise the app may appear to "hang" and timeout when loading secrets. Removing VPC configuration must currently be done in the AWS console. Note: Changes to Lambda configuration do not require a `up stack apply`, just deploy and these changes are picked up! ### IAM policy Up uses IAM policies to grant access to resources within your AWS account such as DynamoDB or S3. To add additional permissions add one or more IAM policy statements to the `policy` array, in the following example we permit DynamoDB item reading, updating, and deleting. ```json { "name": "myapp", "lambda": { "memory": 1024, "policy": [ { "Effect": "Allow", "Resource": "*", "Action": [ "dynamodb:Get*", "dynamodb:List*", "dynamodb:PutItem", "dynamodb:DeleteItem" ] } ] } } ``` Deploy to update the IAM function role permissions. ## Hook scripts Up provides "hooks" which are commands invoked at certain points within the deployment workflow for automating builds, linting and so on. The following hooks are available: - `prebuild` – Run before building - `build` – Run before building. Overrides inferred build command(s) - `postbuild` – Run after building - `predeploy` – Run before deploying - `postdeploy` – Run after deploying - `clean` – Run after a deploy to clean up artifacts. Overrides inferred clean command(s) Here's an example using Browserify to bundle a Node application. Use the `-v` verbose log flag to see how long each hook takes. ```json { "name": "app", "hooks": { "build": "browserify --node app.js > server.js", "clean": "rm server.js" } } ``` Up performs runtime inference to discover what kind of application you're using, and does its best to provide helpful defaults – see the [Runtimes](#runtimes) section. Multiple commands are provided by using arrays, and are run in separate shells: ```json { "name": "app", "hooks": { "build": [ "mkdir -p build", "cp -fr static build", "browserify --node index.js > build/client.js" ], "clean": "rm -fr build" } } ``` To get a better idea of when hooks run, and how long the command(s) take, you may want to deploy with `-v` for verbose debug logs. ## Static file serving Up ships with a robust static file server, to enable it specify the app `type` as `"static"`. ```json { "type": "static" } ``` By default the current directory (`.`) is served, however you can change this using the `dir` setting. The following configuration restricts only serving of files in `./public/*`, any attempts to read files from outside of this root directory will fail. ```json { "name": "app", "type": "static", "static": { "dir": "public" } } ``` Note that `static.dir` only tells Up which directory to serve – it does not exclude other files from the deployment – see [Ignoring Files](#ignoring_files). For example you may want an `.upignore` containing: ``` * !public/** ``` Note: Files are currently served from AWS Lambda as well, so there is a 6MB restriction on the file size. ### Dynamic applications If your project is not strictly static, for example a Node.js web app, you may omit `type` and add static file serving simply by defining `static` as shown below. With this setup Up will serve the file if it exists, before passing control to your application. ```json { "name": "app", "static": { "dir": "public" } } ``` By default there is no prefix, so `GET /index.css` will resolve to `./public/index.css`, however, you may specify a prefix such as "/static/" for `GET /static/index.css` to ensure static files never conflict with your app's routes: ```json { "name": "app", "static": { "dir": "public", "prefix": "/static/" } } ``` Note: Static file serving for dynamic apps does not automatically resolve `index.html` files. The presence of a file is checked before passing control to your application. ## Environment variables The `environment` object may be used for plain-text environment variables. Note that these are not encrypted, and are stored in up.json which is typically committed to GIT, so do not store secrets here. ```json { "name": "api", "environment": { "API_FEATURE_FOO": "1", "API_FEATURE_BAR": "0" } } ``` These become available to you via `process.env.API_FEATURES_FOO`, `os.Getenv("API_FEATURES_FOO")` or similar in your language of choice. The following environment variables are provided by Up: - `PORT` – port number such as "3000" - `UP_STAGE` – stage name such as "staging" or "production" ## Header injection The `headers` object allows you to map HTTP header fields to paths. The most specific pattern takes precedence. Here's an example of two header fields specified for `/*` and `/*.css`: ```json { "name": "app", "type": "static", "headers": { "/*": { "X-Something": "I am applied to everything" }, "/*.css": { "X-Something-Else": "I am applied to styles" } } } ``` Requesting `GET /` will match the first pattern, injecting `X-Something`: ``` HTTP/1.1 200 OK Accept-Ranges: bytes Content-Length: 200 Content-Type: text/html; charset=utf-8 Last-Modified: Fri, 21 Jul 2017 20:42:51 GMT X-Powered-By: up X-Something: I am applied to everything Date: Mon, 31 Jul 2017 20:49:33 GMT ``` Requesting `GET /style.css` will match the second, more specific pattern, injecting `X-Something-Else`: ```json HTTP/1.1 200 OK Accept-Ranges: bytes Content-Length: 50 Content-Type: text/css; charset=utf-8 Last-Modified: Fri, 21 Jul 2017 20:42:51 GMT X-Powered-By: up X-Something-Else: I am applied to styles Date: Mon, 31 Jul 2017 20:49:35 GMT ``` ## Error pages When enabled Up will serve a minimalistic error page for requests accepting `text/html`. The following settings are available: - `enable` — enable the error page feature - `dir` — the directory where the error pages are located - `variables` — vars available to the pages The default template's `color` and optionally provide a `support_email` to allow customers to contact your support team, for example: ```json { "name": "site", "type": "static", "error_pages": { "enable": true, "variables": { "support_email": "support@apex.sh", "color": "#228ae6" } } } ``` If you'd like to provide custom templates you may create one or more of the following files. The most specific file takes precedence. - `error.html` – Matches any 4xx or 5xx - `5xx.html` – Matches any 5xx error - `4xx.html` – Matches any 4xx error - `CODE.html` – Matches a specific code such as 404.html Variables specified via `variables`, as well as `.StatusText` and `.StatusCode` may be used in the template. ```html {{.StatusText}} - {{.StatusCode}}

{{.StatusText}}

{{with .Variables.support_email}} Please try your request again or contact support. {{else}} Please try your request again or contact support. {{end}} ``` ## Script injection Scripts, styles, and other tags may be injected to HTML pages before the closing `` tag or closing `` tag. In the following example the `` is injected to the head, as well as the inlining the `scripts/config.js` file. A `` is then injected into the body. ```json { "name": "site", "type": "static", "inject": { "head": [ { "type": "style", "value": "/style.css" }, { "type": "inline script", "file": "scripts/config.js" } ], "body": [ { "type": "script", "value": "/app.js" } ] } } ``` Currently you may specify the following types: - `literal` – A literal string - `comment` – An html comment - `style` – A style `href` - `script` – A script `src` - `inline style` – An inline style - `inline script` – An inline script - `google analytics` – Google Analytics snippet with API key - `segment` – Segment snippet with API key All of these require a `value`, which sets the `src`, `href`, or inline content. Optionally you can populate `value` via a `file` path to a local file on disk, this is typically more convenient for inline scripts or styles. For example: - `{ "type": "literal", "value": "" }` - `{ "type": "comment", "value": "Just a boring comment" }` - `{ "type": "script", "value": "/feedback.js" }` - `{ "type": "style", "value": "/feedback.css" }` - `{ "type": "inline script", "file": "/feedback.js" }` - `{ "type": "inline style", "file": "/feedback.css" }` - `{ "type": "script", "value": "var config = {};" }` - `{ "type": "google analytics", "value": "API_KEY" }` - `{ "type": "segment", "value": "API_KEY" }` ## Redirects and rewrites Up supports redirects and URL rewriting via the `redirects` object, which maps path patterns to a new location. If `status` is omitted (or 200) then it is a rewrite, otherwise it is a redirect. ```json { "name": "app", "type": "static", "redirects": { "/blog": { "location": "https://blog.apex.sh/", "status": 301 }, "/docs/:section/guides/:guide": { "location": "/help/:section/:guide", "status": 302 }, "/store/*": { "location": "/shop/:splat" } } } ``` In the previous example `/blog` will redirect to a different site, while `/docs/ping/guides/alerting` will redirect to `/help/ping/alerting`. Finally `/store/ferrets` and nested paths such as `/store/ferrets/tobi` will redirect to `/shop/ferrets/tobi` and so on. A common use-case for rewrites is for SPAs or Single Page Apps, where you want to serve the `index.html` file regardless of the path. The other common requirement for SPAs is that you of course can serve scripts and styles, so by default if a file is found, it will not be rewritten to `location`. ```json { "name": "app", "type": "static", "redirects": { "/*": { "location": "/", "status": 200 } } } ``` If you wish to force the rewrite regardless of a file existing, set `force` to `true` as shown here: ```json { "name": "app", "type": "static", "redirects": { "/*": { "location": "/", "status": 200, "force": true } } } ``` More specific target paths take precedence over those which are less specific, for example `/blog` will win over and `/*`. ## Cross-Origin Resource Sharing CORS is a mechanism which allows requests originating from a different host to make requests to your API. Several options are available to restrict this access, if the defaults are appropriate simply enable it as shown below. ```json { "cors": { "enable": true } } ``` Suppose you have `https://api.myapp.com`, you may want to customize `cors` to permit access only from `https://myapp.com` so that other sites cannot call your API directly. ```json { "cors": { "allowed_origins": ["https://myapp.com"], "allowed_methods": ["HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"], "allowed_headers": ["*"], "allow_credentials": true } } ``` - `allowed_origins` – A list of origins a cross-domain request can be executed from. Use `*` to allow any origin, or a wildcard such as `http://*.domain.com` (Default: `["*"]`) - `allowed_methods` – A list of methods the client is allowed to use with cross-domain requests. (Default: `["HEAD", "GET", "POST"]`) - `allowed_headers` – A list of headers the client is allowed to use with cross-domain requests. If the special `*` value is present in the list, all headers will be allowed. (Default: `["Origin", "Accept", "Content-Type", "X-Requested-With"]`) - `exposed_headers` – A list of headers which are safe to expose to the API of a CORS response. - `max_age` – A number indicating how long (in seconds) the results of a preflight request can be cached. - `allow_credentials` – A boolean indicating whether the request can include user credentials such as cookies, HTTP authentication or client side SSL certificates. (Default: `true`) - `debug` - A boolean which will output debug logs (Default: `false`) Here's an example performing a GraphQL query with `fetch()`, note that `Accept` is set to accept only JSON: ```js const body = JSON.stringify({ query: `query { pet(id: 2) { name } }` }) const res = await fetch('https://myapp.com', { headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, method: 'POST', body }) ``` Note: You do not need to run `up stack plan` for CORS settings, simply redeploy the stage. ## Reverse proxy Up acts as a reverse proxy in front of your server, this is how CORS, redirection, script injection and other middleware style features are provided. The following settings are available: - `command` – Command run through the shell to start your server (Default `./server`) - When `package.json` is detected `npm start` is used - When `app.js` is detected `node app.js` is used - `timeout` – Timeout in seconds per request (Default `15`, Max `25`) - `listen_timeout` – Timeout in seconds Up will wait for your app to boot and listen on `PORT` (Default `15`, Max `25`) ```json { "proxy": { "command": "node app.js", "timeout": 10, "listen_timeout": 5 } } ``` Lambda's function timeout is implied from the `.proxy.timeout` setting. ### Crash recovery Another benefit of using Up as a reverse proxy is performing crash recovery. Up will attempt to restart your application if the process crashes to continue serving subsequent requests. ## DNS zones & records Up allows you to configure DNS zones and records. One or more zones may be provided as keys in the `dns` object ("myapp.com" here), with a number of records defined within it. ```json { "name": "gh-polls", "dns": { "gh-polls.com": [ { "name": "app.gh-polls.com", "type": "CNAME", "value": ["gh-polls.netlify.com"] } ] } } ``` The record `type` must be one of: - A - AAAA - CNAME - MX - NAPTR - NS - PTR - SOA - SPF - SRV - TXT ## Stages Up supports the concept of "stages" for configuration, such as mapping of custom domains, or tuneing the size of Lambda function to use. By default the following stages are defined: - `development` — local development environment - `staging` — remote environment for staging new features or releases - `production` — remote environment for production To create a new stage, first add it to your configuration, in this case we'll call it "beta": ```json { "name": "app", "lambda": { "memory": 128 }, "stages": { "beta": { } } } ``` Now you'll need to plan your stack changes, which will set up a new API Gateway and permissions: ``` $ up stack plan Add api deployment id: ApiDeploymentBeta Add lambda permission id: ApiLambdaPermissionBeta ``` Apply those changes: ``` $ up stack apply ``` Now you can deploy to your new stage by passing the name `beta` and open the end-point in the browser: ``` $ up beta $ up url -o beta ``` To delete a stage, simply remove it from the `up.json` configuration and run `up stack plan` again, and `up stack apply` after reviewing the changes. You may of course assign a custom domain to these stages as well, let's take a look at that next! ## Stages & custom domains By defining a stage and its `domain`, Up knows it will need to create a free SSL certificate—`gh-polls.com` in the following example—setup the DNS records, and map the domain to API Gateway. SSL certificates are managed via [AWS ACM](https://aws.amazon.com/certificate-manager/) which automatically renew for you, there's no additional work or cost associated with them. ```json { "stages": { "production": { "domain": "gh-polls.com" } } } ``` Here's another example mapping each stage to a domain, note that the domains do not need to be related, you could use `stage-gh-polls.com` for example. ```json { "stages": { "production": { "domain": "gh-polls.com" }, "staging": { "domain": "stage.gh-polls.com" } } } ``` You may also provide an optional base path, for example to prefix your API with `/v1`. Note that currently your application will still receive "/v1" in its request path, for example Node's `req.url` will be "/v1/users" instead of "/users". ```json { "stages": { "production": { "domain": "api.gh-polls.com", "path": "/v1" } } } ``` Plan the changes via `up stack plan` and `up stack apply` to perform the changes. You may [purchase domains](#guides.development_to_production_workflow.purchasing_a_domain) from the command-line, or map custom domains from other registrars. Up uses Route53 to purchase domains using your AWS account credit card. See `up help domains`. Note: CloudFront can take up to ~40 minutes to distribute this configuration the first time, so grab a coffee while these changes are applied. Also note that ACM certificates are always created in the Virginia (us-east-1) region due to how API Gateway interoperates with CloudFront. ### DNS zones By default when you specify a stage `domain` — such as "api.example.com" — a DNS zone is created in Route53 for the top level domain "example.com", and an ALIAS record "api.example.com" is added to this zone. If you're using external DNS and wish to omit the zone entirely you can disable it with the `zone` property: ```json { "stages": { "production": { "domain": "gh-polls.com", "zone": false } } } ``` You may also explicitly specify the zone by providing a string. In the following example an "api.gh-polls.com" zone will be created, instead of putting the record in "gh-polls.com". ```json { "stages": { "production": { "domain": "api.gh-polls.com", "zone": "api.gh-polls.com" } } } ``` ## Stage overrides Up allows some configuration properties to be overridden at the stage level. The following example illustrates how you can tune lambda memory and hooks per-stage. ```json { "name": "app", "hooks": { "build": "parcel index.html --no-minify -o build", "clean": "rm -fr build" }, "stages": { "production": { "hooks": { "build": "parcel index.html -o build" }, "lambda": { "memory": 1024 } } } } ``` Currently the following properties may be specified at the stage level: - `hooks` - `lambda` - `proxy.command` For example you may want to override `proxy.command` for development, which is the env `up start` uses. In the following example [gin](https://github.com/codegangsta/gin) is used for hot reloading of Go programs: ```json { "name": "app", "stages": { "development": { "proxy": { "command": "gin --port $PORT" } } } } ``` ## Logs By default Up treats stdout as `info` level logs, and stderr as `error` level. If your logger uses stderr, such as Node's `debug()` module and you'd like to change this behaviour you may override these levels: ```json { "name": "app", "environment": { "DEBUG": "myapp" }, "logs": { "stdout": "info", "stderr": "info" } } ``` You can disable Up's logs entirely by using the "disable" option: ```json { "logs": { "disable": true } } ``` ## Ignoring files Up supports gitignore style pattern matching for omitting files from deployment via the `.upignore` file. An example `.upignore` to omit markdown and `.go` source files might look like this: ``` *.md *.go ``` ### Negation By default dotfiles are ignored, if you wish to include them, you may use `!` to negate a pattern in `.upignore`: ``` !.myfile ``` Another use-case for negation is to ignore everything and explicitly include a number of files instead, to be more specific: ``` * !app.js !package.json !node_modules/** !src/** ``` ### Inspecting To get a better idea of which files are being filtered or added, use `up -v` when deploying, and you may also find it useful to `grep` in some cases: ``` $ up -v 2>&1 | grep filtered DEBU filtered .babelrc – 25 DEBU filtered .git – 408 DEBU filtered .gitignore – 13 DEBU filtered node_modules/ansi-regex/readme.md – 1749 DEBU filtered node_modules/ansi-styles/readme.md – 1448 DEBU filtered node_modules/binary-extensions/readme.md – 751 DEBU filtered node_modules/chalk/readme.md – 6136 ``` You may also wish to use `up build --size` to view the largest files within the zip. ### Pattern matching Note that patterns are matched much like `.gitignore`, so if you have the following `.upignore` contents even `node_modules/debug/src/index.js` will be ignored since it contains `src`. ``` src ``` You can be more specific with a leading `./`: ``` ./src ``` Files can be matched recursively using `**`, for example ignoring everything except the files in `dist`: ``` * !dist/** ``` ================================================ FILE: docs/05-runtimes.md ================================================ --- title: Runtimes slug: runtimes --- 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: - Golang - Node.js - Crystal - Static sites ## Node.js 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. The `build` hook becomes: ``` $ npm run build ``` The server run by the proxy becomes: ``` $ npm start ``` ## Golang When a `main.go` file is detected, Golang is the assumed runtime. The `build` hook becomes: ``` $ GOOS=linux GOARCH=amd64 go build -o server *.go ``` The `clean` hook becomes: ``` $ rm server ``` ## Crystal When a `main.cr` file is detected, Crystal is the assumed runtime. Note that this runtime requires Docker to be installed. The `build` hook becomes: ``` $ docker run --rm -v $(pwd):/src -w /src crystallang/crystal crystal build -o server main.cr --release --static ``` The `clean` hook becomes: ``` $ rm server ``` ## Static When an `index.html` file is detected the project is assumed to be static. ================================================ FILE: docs/06-commands.md ================================================ --- title: Commands slug: commands --- Up provides the `up` command-line program, used to deploy the app, and manage associated resources such as domains and SSL certificates, as well as operational tasks like viewing logs. To view details for a command at any time use `up help`, `up help `, for example `up help team members add`. ``` Usage: up [] [ ...] Flags: -h, --help Output usage information. -C, --chdir="." Change working directory. -v, --verbose Enable verbose log output. --format="text" Output formatter. --version Show application version. Commands: help Show help for a command. build Build zip file. config Show configuration after defaults and validation. deploy Deploy the project. docs Open documentation website in the browser. domains ls List purchased domains. domains check Check availability of a domain. domains buy Purchase a domain. env ls List variables. env add Add a variable. env rm Remove a variable. logs Show log output. metrics Show project metrics. rollback Rollback to a previous deployment. prune Prune old S3 deployments of a stage. run Run a hook. stack plan Plan configuration changes. stack apply Apply configuration changes. stack delete Delete configured resources. stack status Show status of resources. start Start development server. team status Status of your account. team switch Switch active team. team login Sign in to your account. team logout Sign out of your account. team members add Add invites a team member. team members rm Remove a member or invite. team members ls List team members and invites. team subscribe Subscribe to the Pro plan. team unsubscribe Unsubscribe from the Pro plan. team card change Change the default card. team ci Credentials for CI. team add Add a new team. upgrade Install the latest or specified version of Up. url Show, open, or copy a stage endpoint. version Show version. Examples: Deploy the project to the staging environment. $ up Deploy the project to the production stage. $ up deploy production Show the staging endpoint url. $ up url Tail project logs. $ up logs -f Show error or fatal level logs. $ up logs 'error or fatal' Run build command manually. $ up run build Show help and examples for a command. $ up help team Show help and examples for a sub-command. $ up help team members ``` ## Deploy Deploy the project, by default to the "staging" stage. Note that running `up` and `up deploy` are identical, as it is the default command. ``` Usage: up deploy [] Flags: -h, --help Output usage information. -C, --chdir="." Change working directory. -v, --verbose Enable verbose log output. --format="text" Output formatter. --version Show application version. Args: [] Target stage name. ``` ### Examples Deploy the project to the staging stage. ``` $ up ``` Deploy the project to the staging stage, this is the same as running `up` without arguments. ``` $ up deploy ``` Deploy the project to the production stage. ``` $ up deploy production ``` Note that since `deploy` is the default command the following is also valid: ``` $ up production ``` ## Config Validate and output configuration with defaults applied. ``` $ up config ``` ```json { "name": "app", "description": "", "type": "server", "headers": null, "redirects": null, "hooks": { "build": "GOOS=linux GOARCH=amd64 go build -o server *.go", "clean": "rm server" }, "environment": null, "regions": [ "us-west-2" ], "inject": null, "lambda": { "role": "arn:aws:iam::ACCOUNT:role/lambda_function", "memory": 128, "timeout": 5 }, "cors": null, "error_pages": { "dir": ".", "variables": null }, "proxy": { "command": "./server", "backoff": { "min": 100, "max": 500, "factor": 2, "attempts": 3, "jitter": false } }, "static": { "dir": "." }, "logs": { "disable": false }, "dns": { "zones": null } } ... ``` ## Logs Show or tail log output with optional query for filtering. When viewing or tailing logs, you are viewing them from _all_ stages, see the examples below to filter on a stage name. ``` Usage: up logs [] [] Flags: -h, --help Output usage information. -C, --chdir="." Change working directory. -v, --verbose Enable verbose log output. --format="text" Output formatter. --version Show application version. -f, --follow Follow or tail the live logs. -S, --since="1d" Show logs since duration (30s, 5m, 2h, 1h30m, 3d, 1M). -e, --expand Show expanded logs. Args: [] Query pattern for filtering logs. ``` ### Expanded output Use the `-e` or `--expand` flag to expand log fields: ``` $ up -e 'path = "/static/*"' 1:36:34pm INFO request id: 8ff53267-c33a-11e7-9685-15d48d102ae9 ip: 70.66.179.182 method: GET path: /static/3.jpg stage: production version: 5 1:36:34pm INFO response duration: 1ms id: 8ff53267-c33a-11e7-9685-15d48d102ae9 ip: 70.66.179.182 method: GET path: /static/3.jpg size: 0 B stage: production status: 304 version: 5 1:36:34pm INFO request id: 8ff4bd57-c33a-11e7-bf4b-4f0d97c427c5 ip: 70.66.179.182 method: GET path: /static/1.png stage: production version: 5 ``` ### JSON output When stdout is not a terminal Up will output the logs as JSON, which can be useful for further processing with tools such as [jq](https://stedolan.github.io/jq/). In this contrived example the last 5 hours of production errors are piped to `jq` to produce a CSV of HTTP methods to IP address. ``` $ up logs -s 5h 'production error' | jq -r '.|[.fields.method,.fields.ip]|@csv' ``` Yielding: ``` "GET","207.194.34.24" "GET","207.194.34.24" "GET","207.194.34.24" ``` ### Examples Show logs from the past day. ``` $ up logs ``` Show logs from the past 45 minutes. ``` $ up -S 45m logs ``` Show logs from the past 12 hours. ``` $ up -S 12h logs ``` Show live log output. ``` $ up logs -f ``` Show live logs from production only. ``` $ up logs -f production ``` Show live error logs from production only. ``` $ up logs -f 'production error' ``` Show error logs, which include 5xx responses. ``` $ up logs error ``` Show error and warning logs, which include 4xx and 5xx responses. ``` $ up logs 'warn or error' ``` Show logs with a specific message. ``` $ up logs 'message = "user login" method = "GET"' ``` Show logs with a specific message with implicit `=`: ``` $ up logs '"user login" method = "GET"' ``` Show responses with latency above 15ms. ``` $ up logs 'duration > 15' ``` Show 4xx and 5xx responses in production ``` $ up logs 'production (warn or error)' ``` Show production 5xx responses with a POST, PUT, or DELETE method. ``` $ up logs 'production error method in ("POST", "PUT", "DELETE") ``` Show 200 responses with latency above 1500ms. ``` $ up logs 'status = 200 duration > 1.5s' ``` Show responses with bodies larger than 100kb. ``` $ up logs 'size > 100kb' ``` Show 4xx and 5xx responses. ``` $ up logs 'status >= 400' ``` Show emails containing @apex.sh. ``` $ up logs 'user.email contains "@apex.sh"' ``` Show emails ending with @apex.sh. ``` $ up logs 'user.email = "*@apex.sh"' ``` Show emails starting with tj@. ``` $ up logs 'user.email = "tj@*"' ``` Show logs with a more complex query. ``` $ up logs 'method in ("POST", "PUT") ip = "207.*" status = 200 duration >= 50' ``` ## URL Show, open, or copy a stage endpoint. ``` Usage: up url [] Flags: -h, --help Output usage information. -C, --chdir="." Change working directory. -v, --verbose Enable verbose log output. --format="text" Output formatter. --version Show application version. -s, --stage="staging" Target stage name. -o, --open Open endpoint in the browser. -c, --copy Copy endpoint to the clipboard. ``` ### Examples Show the staging endpoint. ``` $ up url ``` Open the staging endpoint in the browser. ``` $ up url --open ``` Copy the staging endpoint to the clipboard. ``` $ up url --copy ``` Show the production endpoint. ``` $ up url -s production ``` Open the production endpoint in the browser. ``` $ up url -o -s production ``` Copy the production endpoint to the clipboard. ``` $ up url -c -s production ``` ## Metrics Show project metrics and estimated cost breakdown for requests, invocation count and the time spent for Lambda invocations. ``` Usage: up metrics [] Flags: -h, --help Output usage information. -C, --chdir="." Change working directory. -v, --verbose Enable verbose log output. --format="text" Output formatter. --version Show application version. -s, --stage="staging" Target stage name. -S, --since="1M" Show metrics since duration (30s, 5m, 2h, 1h30m, 3d, 1M). ``` For example: ``` $ up metrics -s production -S 15d Requests: 13,653 ($0.01) Duration min: 0ms Duration avg: 48ms Duration max: 15329ms Duration sum: 3m6.611s ($0.00) Errors 4xx: 1,203 Errors 5xx: 2 Invocations: 12,787 ($0.00) Errors: 0 Throttles: 0 ``` ## Start Start development server. The development server runs the same proxy that is used in production for serving, so you can test a static site or application locally with the same feature-set. See [Stage Overrides](https://up.docs.apex.sh/#configuration.stage_overrides) for an example of overriding the proxy command per-stage, especially useful in development. Up Pro supports environment variables, and these will be loaded with `up start`, and variables mapped to the "development" stage will take precedence. For example: ``` $ up env set NAME Tobi $ up start # app has NAME available as Tobi $ up env set NAME Loki -s development $ up start # app has NAME available Loki ``` The `UP_STAGE` and `NODE_ENV` environment variables will be set to "development" automatically. ``` Usage: up start [] Flags: -h, --help Output usage information. -C, --chdir="." Change working directory. -v, --verbose Enable verbose log output. --format="text" Output formatter. --version Show application version. -c, --command=COMMAND Proxy command override -o, --open Open endpoint in the browser. --address=":3000" Address for server. ``` ### Examples Start development server on port 3000. ``` $ up start ``` Start development server on port 5000. ``` $ up start --address :5000 ``` Override proxy command. Note that the server created must listen on `PORT`, which is why `--port $PORT` is required for the [gin](https://github.com/codegangsta/gin) example. ``` $ up start -c 'go run main.go' $ up start -c 'gin --port $PORT' $ up start -c 'node --some-flag app.js' $ up start -c 'parcel' ``` ## Domains Manage domain names, and purchase them from AWS Route53 as the registrar. ``` Usage: up domains [ ...] Flags: -h, --help Output usage information. -C, --chdir="." Change working directory. -v, --verbose Enable verbose log output. --version Show application version. Subcommands: domains list List purchased domains. domains check Check availability of a domain. domains buy Purchase a domain. ``` ### Examples List purchased domains. ``` $ up domains ``` Check availability of a domain. ``` $ up domains check example.com ``` Purchase a domain (with interactive form). ``` $ up domains buy ``` ## Stack Stack resource management. The stack is essentially all of the resources powering your app, which is configured by Up on the first deploy. At any time if you'd like to delete the application simply run `$ up stack delete`. To view the status and potential errors use `$ up stack`. ``` Usage: up stack [ ...] Flags: -h, --help Output usage information. -C, --chdir="." Change working directory. -v, --verbose Enable verbose log output. --version Show application version. Subcommands: stack plan Plan configuration changes. stack apply Apply configuration changes. stack delete Delete configured resources. stack status Show status of resources. ``` ### Examples Show status of the stack resources and nameservers. ``` $ up stack ``` Show resource changes. ``` $ up stack plan ``` Apply resource changes. ``` $ up stack apply ``` Delete the stack resources. ``` $ up stack delete ``` ## Build Build zip file, typically only helpful for inspecting its contents. If you're interested in seeing what files are causing bloat, use the `--size` flag to list files by size descending. ``` Usage: up build [] Flags: -h, --help Output usage information. -C, --chdir="." Change working directory. -v, --verbose Enable verbose log output. --format="text" Output formatter. --version Show application version. -s, --stage="staging" Target stage name. --size Show zip contents size information. ``` ### Examples Build archive and save to ./out.zip ``` $ up build ``` Build archive and output to file via stdout. ``` $ up build > /tmp/out.zip ``` Build archive list files by size. ``` $ up build --size ``` Build archive and list size without creating out.zip. ``` $ up build --size > /dev/null ``` ## Team Manage team members, plans, and billing. ``` Usage: up team [ ...] Flags: -h, --help Output usage information. -C, --chdir="." Change working directory. -v, --verbose Enable verbose log output. --format="text" Output formatter. --version Show application version. Subcommands: team status Status of your account. team switch Switch active team. team login Sign in to your account. team logout Sign out of your account. team members add Add invites a team member. team members rm Remove a member or invite. team members ls List team members and invites. team subscribe Subscribe to the Pro plan. team unsubscribe Unsubscribe from the Pro plan. team card change Change the default card. team ci Credentials for CI. team add Add a new team. ``` ### Examples Show active team and subscription status. ``` $ up team ``` Switch teams interactively. ``` $ up team switch ``` Sign in or create account with interactive prompt. ``` $ up team login ``` Sign in to a team. ``` $ up team login --email tj@example.com --team apex-software ``` Add a new team and automatically switch to the team. ``` $ up team add "Apex Software" ``` Subscribe to the Pro plan. ``` $ up team subscribe ``` Invite a team member to your active team. ``` $ up team members add asya@example.com ``` ## Upgrade Install the latest or specified version of Up. The OSS and Pro versions have independent semver, as bugfixes and features for one may not be relevant to the other. If you're an Up Pro subscriber, `up upgrade` will _always_ install Up Pro, even when `--target` is specified, there is no need to specify that you want the Pro version. ``` Usage: up upgrade [] Flags: -h, --help Output usage information. -C, --chdir="." Change working directory. -v, --verbose Enable verbose log output. --format="text" Output formatter. --version Show application version. -t, --target=TARGET Target version for upgrade. ``` ### Examples Upgrade to the latest version available. ``` $ up upgrade ``` Upgrade to the specified version. ``` $ up upgrade -t 0.4.4 ``` ## Prune Prune old S3 deployments of a stage. ``` Usage: up prune [] Flags: -h, --help Output usage information. -C, --chdir="." Change working directory. -v, --verbose Enable verbose log output. --format="text" Output formatter. --version Show application version. -s, --stage="staging" Target stage name. -r, --retain=30 Number of versions to retain. ``` ### Examples Prune and retain the most recent 30 staging versions. ``` $ up prune ``` Prune and retain the most recent 30 production versions. ``` $ up prune -s production ``` Prune and retain the most recent 15 production versions. ``` $ up prune -s production -r 15 ``` ================================================ FILE: docs/07-guides.md ================================================ --- title: Guides slug: guides --- ## Subscribing to Up Pro Up Pro provides additional features which are not available in the open-source version, such as encrypted environment variables, alerting support and more. First sign into the platform with the following command – you'll receive an email for confirmation. ``` $ up team login email: tj@apex.sh ⠋ verify: Check your email for a confirmation link ``` Click the link in your email and you're signed in! If you're using Up Pro with one or more organizations, you should create a team to manage team members and subscriptions independently. If you plan on using Up Pro for personal use you may skip this step. ``` $ up team add "My Company" ``` Next you'll need to subscribe! You'll be asked for an optional coupon, credit card information – which never touches our servers, only Stripe via HTTPS – and finally a subscription confirmation. ``` $ up team subscribe ``` Now you and your team members may upgrade to the latest version of Up Pro, instead of the open-source distribution: ``` $ up upgrade ``` To view the status of your account at any time run the following: ``` $ up team team: apex subscription: Up Pro amount: $10.00/mo USD created: December 22, 2017 ``` To switch to another team run the following and select the active team. ``` $ up team switch ❯ apex tj@apex.sh ``` ## Inviting team members To invite members use the following command: ``` $ up team members add tobi@apex.sh $ up team members add loki@apex.sh $ up team members add jane@apex.sh ``` At any time you can view invites and members: ``` $ up team members team: apex Members • tj@apex.sh • tobi@apex.sh • loki@apex.sh Invites • jane@apex.sh ``` Your team members will receive an email with installation instructions, where they run the following to sign in – with your team id of course. ``` $ up team login --email tobi@apex.sh --team apex ``` ## Development to production workflow This section guides you through taking a small application from development, to production, complete with purchasing and mapping a custom domain. ### Deploying First create `app.js` in an empty directory with the following Node.js app. Note that it must listen on __PORT__ which is passed by Up. ```js const http = require('http') const { PORT = 3000 } = process.env http.createServer((req, res) => { res.end('Hello World\n') }).listen(PORT) ``` Next you should give your application a name and start configuring. The `profile` name should correspond with the name in `~/.aws/credentials` so that Up knows which AWS account to deploy to, and which credentials to use. ```json { "name": "up-example", "profile": "up-tobi" } ``` Run `up` to deploy the application. ``` $ up build: 5 files, 3.9 MB (358ms) deploy: complete (14.376s) stack: complete (1m12.086s) ``` Test with `curl` to ensure everything is working: ``` $ curl `up url` Hello World ``` ### Purchasing a domain Domains can be mapped from existing services, or purchased directly from AWS via Route53. First check if the domain you'd like is available: ``` $ up domains check up.com Domain up.com is unavailable Suggestions: theupwards.com $12.00 USD upwardonline.com $12.00 USD myupwards.com $12.00 USD theastir.com $12.00 USD astironline.com $12.00 USD myastir.com $12.00 USD myupward.net $11.00 USD cleanup.tv $32.00 USD myup.tv $32.00 USD itup.tv $32.00 USD newup.tv $32.00 USD thedown.net $11.00 USD theupward.net $11.00 USD upwardsonline.net $11.00 USD ``` Oh no up.com is taken! Try another: ``` $ up domains check up-example.com Domain up-example.com is available for $12.00 USD ``` Purchase it with the following command and fill out the details required by the registrar: ``` $ up domains buy Domain: up-example.com First name: TJ Last name: Holowaychuk Email: tj@apex.sh Phone: +1.2501007000 Country code: CA City: Victoria State or province: BC Zip code: X9X 9X9 Address: Some address here ``` It can take a few minutes for AWS to finalize the purchase after which you should receive an email. Then you'll see it in the `up domains` output along with the automatic renewal time. ``` $ up domains gh-polls.com renews Aug 28 17:17:58 up-example.com renews Sep 19 19:40:50 ``` By default domains purchased with Up have privacy protection enabled, hiding your contact information from [WHOIS](https://en.wikipedia.org/wiki/WHOIS). ### Deploying to Stages Before deploying to the staging and production stages, first tweak the application a little to include the `UP_STAGE` environment variable: ```js const http = require('http') const { PORT = 3000, UP_STAGE } = process.env http.createServer((req, res) => { res.end('Hello World from ' + UP_STAGE) }).listen(PORT) ``` Now deploy to staging and production. Note that `up` is an alias of `up deploy staging`. ``` $ up $ up deploy production ``` Open both in the browser: ``` $ up url -o $ up url -s production -o ``` You should see "Hello World from production" and "Hello World from staging". ### Mapping custom domains to stages Now that you have an application deployed, you probably want a fancy custom domain for it right? You can map these using the `stages` and `domain` properties. Here we let Up know that we want `up-example.com` for production and `stage.up-example.com` for staging. ```json { "name": "up-example", "profile": "up-tobi", "stages": { "staging": { "domain": "stage.up-example.com" }, "production": { "domain": "up-example.com" } } } ``` Note that you could map staging to a domain like `staging-myapp.com`, it does not have to be a sub-domain of your production domain. Now when you run `up stack plan` to preview changes to your resources, it will prompt you to verify the Let's Encrypt certificate emails that AWS sends. ``` $ up stack plan domains: Check your email for certificate approval ⠧ confirm: up-example.com ``` AWS requires email verification to prove you own the domain. AWS sends an email to the 3 contact addresses listed in WHOIS when you registered the domain, and to the following 5 common system addresses for your domain: - administrator@your_domain_name - hostmaster@your_domain_name - postmaster@your_domain_name - webmaster@your_domain_name - admin@your_domain_name See [Validate Domain Ownership](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-email.html) for more information. After clicking "I Approve" in one of the emails, the output will resume and you'll see some new resources Up will be creating. ``` Add AWS::ApiGateway::DomainName id: ApiDomainDevelopment Add AWS::ApiGateway::BasePathMapping id: ApiDomainDevelopmentPathMapping Add AWS::ApiGateway::DomainName id: ApiDomainProduction Add AWS::ApiGateway::BasePathMapping id: ApiDomainProductionPathMapping Add AWS::Route53::RecordSet id: DnsZoneDevUpExampleComRecordDevUpExampleCom Add AWS::Route53::RecordSet id: DnsZoneUpExampleComRecordUpExampleCom ``` If you're curious, now that Up knows you want to map the domain(s), it will create: - Registers ACM free SSL certificate(s) for your domain(s) - CloudFront distribution for the API Gateway - API Gateway stage mapping - Route53 DNS zone and record(s) mapping to the CloudFront distribution Now apply these changes: ``` $ up stack apply ``` After the changes have been applied, it can take roughly 10-40 minutes for CloudFront to distribute the configuration and SSL certificate globally, so until then our up-example.com domain won't work. Once available https://up-example.com will always point to production via `up deploy production`, and https://stage.up-example.com/ will point to the latest deployment via `up`. ### Mapping domains from external registrars If you purchased a domain via `up domains buy` you can skip this step, however if you used an external registrar such as Godaddy you will need to delegate to AWS for DNS management. To do this you'll need to sign in to your registrar's site, and configure the nameservers. To figure out what values to use for the nameservers, run `up stack`, which outputs the NS records for the apex (top-level) domains of your application. ``` $ up stack Staging domain: stage.up-example.com endpoint: d2od0udp1p8bru.cloudfront.net Production domain: up-example.com endpoint: d72wsqljqg5cy.cloudfront.net nameservers: • ns-1495.awsdns-58.org • ns-103.awsdns-12.com • ns-1670.awsdns-16.co.uk • ns-659.awsdns-18.net ``` Save those four values in your registrar's interface, and you should be good to go! Note that altering DNS records can take some time to propagate. ### Mapping with third-party DNS If you manage DNS with a third-party such as Cloudflare, and wish to use Up only for deployment you will need to manually edit or add DNS records. For example if your top-level domain `sloths.com` is managed by Cloudflare and you'd like point `api.sloths.com` to your app, you should first add it to your `up.json`: ```json { "name": "sloths" "stages": { "production": { "domain": "api.sloths.com" } } } ``` Next you will need to `up stack plan` and `up stack apply`, this will set up a CloudFront end-point for the application. To view the endpoint information, run `up stack`: ``` $ up stack Production domain: api.sloths.com endpoint: d72wsqljqg5cy.cloudfront.net ``` In your DNS provider – Cloudflare in this example – you should create a `CNAME` record pointing to the production `endpoint`. Make sure that the `domain` you use matches the domain in Cloudflare. ### Stack changes The "stack" is all of the resources associated with your app. You plan changes via `up stack plan` and perform them with `up stack apply`. Suppose you wanted to map the "staging" stage, you would first add it to `up.json`: ```json { "name": "up-example", "profile": "up-tobi", "stages": { "staging": { "domain": "stage.up-example.com" }, "production": { "domain": "up-example.com" } } } ``` Then run: ``` $ up stack plan ``` Review the output, it should be all "Add"s in this case, then apply: ``` $ up stack apply ``` ### Deleting the application After you're done messing around, you may want to remove all the resources and the app itself. To do so simply run: ``` $ up stack delete ``` ## Deploying applications from continuous integration Up makes it easy to deploy your applications from CI, thanks to its Go binaries you can install Up in seconds in any CI provider such as Travis, Circle, Semaphore among others. ### Environment variables The first step is to set up environment variables so that you have access to your AWS account. You can get these values from `cat ~/.aws/credentials`: - `AWS_ACCESS_KEY_ID` – AWS access key - `AWS_SECRET_ACCESS_KEY` – AWS secret key If using running Up Pro you'll need your Up credentials in order to access Up Pro via the `up upgrade` command. To obtain this run `up team ci` or `up team ci --copy` to copy it directly to your clipboard, then paste this as the env var's value. - `UP_CONFIG` – Up configuration as base64-encoded JSON 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 the following variable to your CI: - `GITHUB_TOKEN` — Github personal access token ### Commands You may install Up in the current working directory, and deploy to production with the following commands, omitting the `up upgrade` if you are not an Up Pro subscriber. ``` $ curl -sf https://up.apex.sh/install | BINDIR=. sh $ ./up upgrade $ ./up production ``` Or if you prefer installing globally within `PATH`: ``` $ sudo chown -R $(whoami) /usr/local/bin $ curl -sf https://up.apex.sh/install | sh $ up upgrade $ up production ``` ## Mastering logging This section describes how you can log from your application in a way that Up will recognize. In the future Up will support forwarding your logs to services such as Loggly, Papertrail or ELK. ### Plain text The first option is plain-text logs to stdout or stderr. Currently writes to stderr are considered ERROR-level logs, and stdout becomes INFO. Writing plain-text logs is simple, for example with Node.js: ```js console.log('User signed in') console.error('Failed to sign in: %s', err) ``` Would be collected as: ``` INFO: User signed in ERROR: Failed to sign in: something broke ``` ### JSON structured logs The second option is structured logging with JSON events, which is preferred as it allows you to query against specific fields and treat logs like events. JSON logs require a `level` and `message` field: ```js console.log(`{ "level": "info", "message": "User login" }`) ``` Would be collected as: ``` INFO: User login ``` The `message` field should typically contain no dynamic content, such as user names or emails, these can be provided as fields: ```js console.log(`{ "level": "info", "message": "User login", "fields": { "name": "Tobi", "email": "tobi@apex.sh" } }`) ``` Would be collected as: ``` INFO: User login name=Tobi email=tobi@apex.sh ``` Allowing you to perform queries such as: ``` $ up logs 'message = "User login" name = "Tobi"' ``` Or: ``` $ up logs 'name = "Tobi" or email = "tobi@*"' ``` Here's a simple JavaScript logger for reference. All you need to do is output some JSON to stdout and Up will handle the rest! ```js function log(level, message, fields = {}) { const entry = { level, message, fields } console.log(JSON.stringify(entry)) } ``` For example, with the Go [apex/log](https://github.com/apex/log) package you'd use the `json` handler, which outputs this format. ## Log query language Up supports a comprehensive query language, allowing you to perform complex filters against structured data, supporting operators, equality, substring tests and so on. This section details the options available when querying. ### AND operator The `and` operator is implied, and entirely optional to specify, since this is the common case. Suppose you have the following example query to show only production errors from a the specified IP address. ``` production error ip = "207.194.32.30" ``` The parser will inject `and`, effectively compiling to: ``` production and error and ip = "207.194.38.50" ``` ### Or operator There is of course also an `or` operator, for example showing warnings or errors. ``` production (warn or error) ``` These may of course be nested as you require: ``` (production or staging) (warn or error) method = "GET" ``` ### Equality operators The `=` and `!=` equality operators allow you to filter on the contents of a field. Here `=` is used to show only GET requests: ``` method = "GET" ``` Or for example `!=` may be used to show anything except GET: ``` method != "GET" ``` ### Relational operators The `>`, `>=`, `<`, and `<=` relational operators are useful for comparing numeric values, for example response status codes: ``` status >= 200 status < 300 ``` ### Stages Currently all development, staging, and production logs are all stored in the same location, however you may filter to find exactly what you need. The keywords `production`, `staging`, and `development` expand to: ``` stage = "production" ``` For example, filtering on slow production responses: ``` production duration >= 1s ``` Is the same as: ``` stage = "production" duration >= 1s ``` ### Severity levels Up provides request level logging with severity levels applied automatically. For example, a 5xx response is an ERROR level, while 4xx is a WARN, and 3xx or 2xx are the INFO level. This means that instead of using the following for showing production errors: ``` production status >= 500 ``` You may use: ``` production error ``` ### In Operator The `in` operator checks for the presence of a field within the set provided. For example, showing only POST, PUT and PATCH requests: ``` method in ("POST", "PUT", "PATCH") ``` ### Units The log grammar supports units for bytes and durations. For example, showing responses larger than 56kb: ``` size > 56kb ``` Or showing responses longer than 1500ms: ``` duration > 1.5s ``` Byte units are: - `b` bytes (`123b` or `123` are equivalent) - `kb` bytes (`5kb`, `128kb`) - `mb` bytes (`5mb`, `15.5mb`) Duration units are: - `ms` milliseconds (`100ms` or `100` are equivalent) - `s` seconds (`1.5s`, `5s`) ### Substring matches When filtering on strings, such as the log message, you may use the `*` character for substring matches. For example if you want to show logs with a remote ip prefix of `207.`: ``` ip = "207.*" ``` Or a message containing the word "login": ``` message = "*login*" ``` There is also a special keyword for this case: ``` message contains "login" ``` ## Hot reloading in development The `up start` command uses your `proxy.command` by default, which may be inferred based on your application type, such as `node app.js` for Node.js or `./server` for Golang. You may alter this command for `up start` with the development environment. For example with Golang you may want `go run main.go`, or hot reloading with [gin](https://github.com/codegangsta/gin) as shown here: ```json { "name": "app", "stages": { "development": { "proxy": { "command": "gin --port $PORT" } } } } ``` Note that the server must always listen on `PORT` which is provided by `up start`. ## Accessing lambda context Traditional AWS Lambda functions provided a context object which contains runtime information such as API Gateway user identity. This information is exposed as JSON in the `X-Context` header field in Up as shown here: ```js const http = require('http') const { PORT } = process.env const app = http.createServer((req, res) => { const ctx = JSON.parse(req.headers['x-context']) res.end(JSON.stringify(ctx, null, 2)) }) console.log('Starting app on %s', PORT) app.listen(PORT) ``` Output will be similar to the following. Visit the [AWS Documentation](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html) for details. ```json { "apiId": "g4yn392afg", "resourceId": "ez0z8areob", "requestId": "d8314ef1-5543-11e8-a925-21fa0dd01c37", "accountId": "337344593553", "stage": "staging", "identity": { "apiKey": "", "accountId": "", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36", "sourceIp": "64.110.31.100", "accessKey": "", "caller": "", "user": "", "userARN": "", "cognitoIdentityId": "", "cognitoIdentityPoolId": "", "cognitoAuthenticationType": "", "cognitoAuthenticationProvider": "" }, "authorizer": null } ``` ================================================ FILE: docs/08-troubleshooting.md ================================================ --- title: Troubleshooting menu: Help slug: troubleshooting --- 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).
I didn't receive a sign-in or certificate confirmation email

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

My application times out or seems slow

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.

I'm seeing 404 Not Found responses

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.

My deployment seems stuck

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".

How do I sign into my team?

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.

Unable to associate certificate error

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).

I'm seeing 403 Forbidden errors in CI

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.

================================================ FILE: docs/09-faq.md ================================================ --- title: Questions menu: FAQ slug: faq ---
Is this a hosted service?

There are currently no plans for a hosted version. Up lets you deploy applications to your own AWS account for isolation, security, and longevity — don't worry about a startup going out of business.

How much does Up Pro cost?

Up's subscription fee is currently $20/mo USD. When subscribed, your team has access to Up Pro updates until the subscription is cancelled. When cancelled, you receive access until the end of your billing period.

For the subscription, you get unlimited access within your organization; there is no additional fee per team member or "seat". You can deploy any number of applications as you wish.

Note that AWS charges for use of its resources. However, most small to medium applications will fit within the AWS free tier.

What platforms does Up support?

Currently AWS via API Gateway and Lambda are supported, this is the focus until Up is nearing feature completion, after which additional providers such as GCP and Azure will be added.

How is this different than other serverless frameworks?

Most of the AWS Lambda based tools are function-oriented, while Up abstracts this away entirely. Up does not use framework "shims", the servers that you run using Up are regular HTTP servers and require no code changes for Lambda compatibility.

Up keeps your apps and APIs portable, makes testing them locally easier, and prevents vendor lock-in. The Lambda support for Up is simply an implementation detail, you are not coupled to API Gateway or Lambda. Up uses the API Gateway proxy mode to send all requests (regardless of path or method) to your application.

If you're looking to manage function-level event processing pipelines, Apex or Serverless are likely better candidates, however if you're creating applications, apis, micro services, or websites, Up is built for you.

Why run HTTP servers in Lambda?

You might be thinking this defeats the purpose of Lambda, however most people just want to use the tools they know and love. Up lets you be productive developing locally as you normally would, Lambda for hosting is only an implementation detail.

With Up you can use any Python, Node, Go, or Java framework you'd normally use to develop, and deploy with a single command, while maintaining the cost effectiveness, self-healing, and scaling capabilities of Lambda.

How much does it cost to run an application?

AWS API Gateway provides 1 million free requests per month, so there's a good chance you won't have to pay anything at all. Beyond that view the AWS Pricing for more information.

How well does it scale?

Up scales to fit your traffic on-demand, you don't have to do anything beyond deploying your code. There's no restriction on the number of concurrent instances, apps, custom domains and so on.

How much latency does Up's reverse proxy introduce?

With a 512mb Lambda function Up introduces an average of around 500µs (microseconds) per request.

Can I remove the /staging and /production paths?

Up uses AWS API Gateway, which imposes the stage base paths. Currently there is no way to remove them, however when you use custom domains these paths are not present.

Do the servers stay active while idle?

This depends on the platform, and with Lambda being the initial platform provided the current answer is no, the server(s) are frozen when inactive and are otherwise "stateless".

Typically relying on background work in-process is an anti-pattern as it does not scale. Lambda functions combined with CloudWatch scheduled events for example are a good way to handle this kind of work, if you're looking for a scalable alternative.

What databases can I use?

You're not limited to databases from any given platform, such as AWS Provided that the database host provides authentication, you can use anything. See the Wiki for a list of managed & serverless database solutions.

Why is Up licensed as GPLv3?

Up is licensed in such a way that myself as an independent developer can continue to improve the product and provide support. Commercial customers receive access to a premium version of Up with additional features, priority support for bugfixes, and of course knowing that the project will stick around! Up saves your team countless hours maintaining infrastructure and custom tooling, so you can get back to what makes your company and products unique.

Can I donate?

Yes you can! Head over to the OpenCollective page. Any donations are greatly appreciated and help me focus more on Up's implementation, documentation, and examples. If you're using the free OSS version for personal or commercial use please consider giving back, even a few bucks buys a coffee :).

================================================ FILE: docs/10-links.md ================================================ --- title: Links slug: links --- Links to helpful resources such as the Up community, changelog, examples, articles, videos and more. - [Changelog](https://github.com/apex/up/blob/master/History.md) for changes - [GitHub repository](https://github.com/apex/up) - [GitHub Actions](https://github.com/apex/actions) for continuous deployment - [@tjholowaychuk](https://twitter.com/tjholowaychuk) on Twitter for updates - [Example applications](https://github.com/apex/up-examples) for Up in various languages - [Slack](https://chat.apex.sh/) to chat with apex(1) and up(1) community members - [Blog](https://blog.apex.sh/) to follow release posts, tips and tricks - [YouTube](https://www.youtube.com/watch?v=1wnSNj-jmo4&index=1&list=PLbFkWVvnVLnRP-E87Tqe6nYVjOk6461o0) for the Apex Up video playlist - [Wiki](https://github.com/apex/up/wiki) for article listings, database suggestions, and sample apps - [Serverless Calculator](http://serverlesscalc.com/) for helping estimate costs for your use-case ================================================ FILE: go.mod ================================================ module github.com/apex/up require ( github.com/NYTimes/gziphandler v0.0.0-20170916004738-97ae7fbaf816 github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 // indirect github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 // indirect github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect github.com/apex/go-apex v1.0.0 github.com/apex/log v1.3.0 github.com/armon/go-radix v1.0.0 // indirect github.com/atotto/clipboard v0.1.2 // indirect github.com/aws/aws-sdk-go v1.31.9 github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2 // indirect github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37 // indirect github.com/c4milo/unpackit v0.0.0-20170704181138-4ed373e9ef1c // indirect github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 github.com/denormal/go-gitignore v0.0.0-20170315120618-40de3d33f668 // indirect github.com/dsnet/compress v0.0.0-20171208185109-cc9eb1d7ad76 // indirect github.com/dustin/go-humanize v0.0.0-20171012181109-77ed807830b4 github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 github.com/fanyang01/radix v0.0.0-20160415095728-e1747dd9eeac github.com/fatih/color v1.7.0 // indirect github.com/golang/sync v0.0.0-20170927054112-8e0aa688b654 github.com/google/go-github v14.0.0+incompatible // indirect github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect github.com/gosuri/uilive v0.0.0-20170323041506-ac356e6e42cd // indirect github.com/gosuri/uiprogress v0.0.0-20170224063937-d0567a9d84a1 // indirect github.com/hashicorp/go-uuid v0.0.0-20160717022140-64130c7a86d7 // indirect github.com/hooklift/assert v0.0.0-20170704181755-9d1defd6d214 // indirect github.com/jehiah/go-strftime v0.0.0-20151206194810-2efbe75097a5 // indirect github.com/klauspost/compress v1.2.1 // indirect github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5 // indirect github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6 // indirect github.com/klauspost/pgzip v0.0.0-20170402124221-0bf5dcad4ada // indirect github.com/mattn/go-isatty v0.0.9 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 github.com/pascaldekloe/name v0.0.0-20170812100307-81013e77fe79 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/cors v0.0.0-20180726230524-02026070ea74 github.com/segmentio/analytics-go v0.0.0-20160426181448-2d840d861c32 // indirect github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c // indirect github.com/segmentio/go-snakecase v1.0.0 github.com/sergi/go-diff v1.0.0 // indirect github.com/stripe/stripe-go v28.5.0+incompatible github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09 github.com/tj/assert v0.0.1 github.com/tj/aws v0.1.4 github.com/tj/backoff v1.0.0 github.com/tj/go v1.8.6 github.com/tj/go-archive v1.0.2 github.com/tj/go-cli-analytics v1.0.0 github.com/tj/go-headers v0.0.0-20170630155323-711a635412ca github.com/tj/go-progress v0.0.0-20171031175334-333acdb6fe9f github.com/tj/go-spin v1.1.0 github.com/tj/go-update v2.2.4+incompatible github.com/tj/kingpin v2.5.0+incompatible github.com/tj/survey v2.0.6+incompatible github.com/ulikunitz/xz v0.5.4 // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect golang.org/x/net v0.0.0-20200202094626-16171245cfb2 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect gopkg.in/yaml.v3 v3.0.0-20200602174320-3e3e88ca92fa // indirect ) go 1.13 ================================================ FILE: go.sum ================================================ github.com/NYTimes/gziphandler v0.0.0-20170916004738-97ae7fbaf816 h1:wBaYm5ra+p6jEKkg9G3tCimdOwp/dcCPSfkePrWoc6w= github.com/NYTimes/gziphandler v0.0.0-20170916004738-97ae7fbaf816/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/apex/go-apex v1.0.0 h1:Em8+vo4WXEQp7GfNDTr35HRnE5sFYcRpkTODpVjU39A= github.com/apex/go-apex v1.0.0/go.mod h1:Hy8WsL4dnQc/bYBxElRQ7xHXLNBAqz0BVxUhHiGwKLA= github.com/apex/log v1.1.0 h1:J5rld6WVFi6NxA6m8GJ1LJqu3+GiTFIt3mYv27gdQWI= github.com/apex/log v1.1.0/go.mod h1:yA770aXIDQrhVOIGurT/pVdfCpSq1GQV/auzMN5fzvY= github.com/apex/log v1.3.0 h1:1fyfbPvUwD10nMoh3hY6MXzvZShJQn9/ck7ATgAt5pA= github.com/apex/log v1.3.0/go.mod h1:jd8Vpsr46WAe3EZSQ/IUMs2qQD/GOycT5rPWCO1yGcs= github.com/apex/logs v0.0.4/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/atotto/clipboard v0.0.0-20160219034421-bb272b845f11 h1:Gmm0NreNeu4FgEzX1Ht6ceAf5/wOBG+Gi625BEy9d40= github.com/atotto/clipboard v0.0.0-20160219034421-bb272b845f11/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go v1.19.1 h1:8kOP0/XGJwXIFlYoD1DAtA39cAjc15Iv/QiDMKitD9U= github.com/aws/aws-sdk-go v1.19.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.31.9 h1:n+b34ydVfgC30j0Qm69yaapmjejQPW2BoDBX7Uy/tLI= github.com/aws/aws-sdk-go v1.31.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2 h1:1B/+1BcRhOMG1KH/YhNIU8OppSWk5d/NGyfRla88CuY= github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= github.com/buger/goterm v0.0.0-20170918171949-d443b9114f9c h1:p1/ndMSoqqRp8tN/HJuqYNt1IlBzDbug0JYTJAktrtg= github.com/buger/goterm v0.0.0-20170918171949-d443b9114f9c/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U= github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37 h1:uxxtrnACqI9zK4ENDMf0WpXfUsHP5V8liuq5QdgDISU= github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U= github.com/c4milo/unpackit v0.0.0-20170704181138-4ed373e9ef1c h1:aprLqMn7gSPT+vdDSl+/E6NLEuArwD/J7IWd8bJt5lQ= github.com/c4milo/unpackit v0.0.0-20170704181138-4ed373e9ef1c/go.mod h1:Ie6SubJv/NTO9Q0UBH0QCl3Ve50lu9hjbi5YJUw03TE= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU= github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= github.com/denormal/go-gitignore v0.0.0-20170315120618-40de3d33f668 h1:JJc1wCJD+mjauA6uWGKHgwwpoqnuHsFRCVnGDQuyAbI= github.com/denormal/go-gitignore v0.0.0-20170315120618-40de3d33f668/go.mod h1:C/+sI4IFnEpCn6VQ3GIPEp+FrQnQw+YQP3+n+GdGq7o= github.com/dsnet/compress v0.0.0-20171208185109-cc9eb1d7ad76 h1:eX+pdPPlD279OWgdx7f6KqIRSONuK7egk+jDx7OM3Ac= github.com/dsnet/compress v0.0.0-20171208185109-cc9eb1d7ad76/go.mod h1:KjxHHirfLaw19iGT70HvVjHQsL1vq1SRQB4yOsAfy2s= github.com/dustin/go-humanize v0.0.0-20171012181109-77ed807830b4 h1:I4YDfvHXPYd8OWal9f4CgxbqNH2Bbcqk6wuLTy+ieww= github.com/dustin/go-humanize v0.0.0-20171012181109-77ed807830b4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 h1:wWke/RUCl7VRjQhwPlR/v0glZXNYzBHdNUzf/Am2Nmg= github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9/go.mod h1:uPmAp6Sws4L7+Q/OokbWDAK1ibXYhB3PXFP1kol5hPg= github.com/fanyang01/radix v0.0.0-20160415095728-e1747dd9eeac h1:fYRW78xH/NepMB5++0kO74kXra7McyBQKsnWlRZyhaQ= github.com/fanyang01/radix v0.0.0-20160415095728-e1747dd9eeac/go.mod h1:PSQWLj5d94M4cGhTeXFExqzf4B98d1Zlxtwf5wL1Vmc= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/sync v0.0.0-20170927054112-8e0aa688b654 h1:bzR1is7uS8E5I4vAF7Qu2TaSDoYqneMomTnAuesaMHw= github.com/golang/sync v0.0.0-20170927054112-8e0aa688b654/go.mod h1:YCHYtYb9c8Q7XgYVYjmJBPtFPKx5QvOcPxHZWjldabE= github.com/google/go-github v14.0.0+incompatible h1:IH7XxuaXbLVh4iwPks5+jmKZXElyvAf+5K1108Ku8fU= github.com/google/go-github v14.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gosuri/uilive v0.0.0-20170323041506-ac356e6e42cd h1:1e+0Z+T4t1mKL5xxvxXh5FkjuiToQGKreCobLu7lR3Y= github.com/gosuri/uilive v0.0.0-20170323041506-ac356e6e42cd/go.mod h1:qkLSc0A5EXSP6B04TrN4oQoxqFI7A8XvoXSlJi8cwk8= github.com/gosuri/uiprogress v0.0.0-20170224063937-d0567a9d84a1 h1:4iPLwzjiWGBQnYdtKbg/JNlGlEEvklrrMdjypdA1LKQ= github.com/gosuri/uiprogress v0.0.0-20170224063937-d0567a9d84a1/go.mod h1:C1RTYn4Sc7iEyf6j8ft5dyoZ4212h8G1ol9QQluh5+0= github.com/hashicorp/go-uuid v0.0.0-20160717022140-64130c7a86d7 h1:w8czuEDeFZo7CdZ2APLO24tTbjzO4cNXTfCkBdtU+wg= github.com/hashicorp/go-uuid v0.0.0-20160717022140-64130c7a86d7/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hooklift/assert v0.0.0-20170704181755-9d1defd6d214 h1:WgfvpuKg42WVLkxNwzfFraXkTXPK36bMqXvMFN67clI= github.com/hooklift/assert v0.0.0-20170704181755-9d1defd6d214/go.mod h1:kj6hFWqfwSjFjLnYW5PK1DoxZ4O0uapwHRmd9jhln4E= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jehiah/go-strftime v0.0.0-20151206194810-2efbe75097a5 h1:E1bpycfzgfdJWK32+GOJDYVrep2fbX6cN6tYiXd+CGY= github.com/jehiah/go-strftime v0.0.0-20151206194810-2efbe75097a5/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= github.com/klauspost/compress v1.2.1 h1:z1Ra6IKoPtIeVA8GV0SCQhuo6T4EBjlL9VwonZ8NYBo= github.com/klauspost/compress v1.2.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5 h1:2U0HzY8BJ8hVwDKIzp7y4voR9CX/nvcfymLmg2UiOio= github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6 h1:KAZ1BW2TCmT6PRihDPpocIy1QTtsAsrx6TneU/4+CMg= github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= github.com/klauspost/pgzip v0.0.0-20170402124221-0bf5dcad4ada h1:ZHhgRyr+9LYwfuWChpSTCCe/07V26LEElTKUXj+2fAg= github.com/klauspost/pgzip v0.0.0-20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 h1:eQox4Rh4ewJF+mqYPxCkmBAirRnPaHEB26UkNuPyjlk= github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pascaldekloe/name v0.0.0-20170812100307-81013e77fe79 h1:01H24QTjrY56mVV6Ms4bbkT+z36nC+c133Cvxe391vQ= github.com/pascaldekloe/name v0.0.0-20170812100307-81013e77fe79/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ= github.com/pkg/browser v0.0.0-20170505125900-c90ca0c84f15 h1:mrI+6Ae64Wjt+uahGe5we/sPS1sXjvfT3YjtawAVgps= github.com/pkg/browser v0.0.0-20170505125900-c90ca0c84f15/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rs/cors v0.0.0-20180726230524-02026070ea74 h1:2UqoDqa4MGBAM2/DsHlZEvzpubV5s4jcMi8tXkLtSvE= github.com/rs/cors v0.0.0-20180726230524-02026070ea74/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/segmentio/analytics-go v0.0.0-20160426181448-2d840d861c32 h1:+0sDBHuIsUlerfNGmggprc/aCAFQ5ZvPReQOHHTVZUs= github.com/segmentio/analytics-go v0.0.0-20160426181448-2d840d861c32/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48= github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c h1:rsRTAcCR5CeNLkvgBVSjQoDGRRt6kggsE6XYBqCv2KQ= github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c/go.mod h1:kJ9mm9YmoWSkk+oQ+5Cj8DEoRCX2JT6As4kEtIIOp1M= github.com/segmentio/go-snakecase v1.0.0 h1:FSeHpP0sBL3O+MCpxvQZrS5a51WAki6gposZuwVE9L4= github.com/segmentio/go-snakecase v1.0.0/go.mod h1:jk1miR5MS7Na32PZUykG89Arm+1BUSYhuGR6b7+hJto= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.1.4 h1:ToftOQTytwshuOSj6bDSolVUa3GINfJP/fg3OkkOzQQ= github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho= github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stripe/stripe-go v28.5.0+incompatible h1:YBJrol3D+WNmmpRIfot0bT/3FGWF8to9306nTRmpZD8= github.com/stripe/stripe-go v28.5.0+incompatible/go.mod h1:A1dQZmO/QypXmsL0T8axYZkSN/uA/T/A64pfKdBAMiY= github.com/stripe/stripe-go v70.15.0+incompatible h1:hNML7M1zx8RgtepEMlxyu/FpVPrP7KZm1gPFQquJQvM= github.com/stripe/stripe-go v70.15.0+incompatible/go.mod h1:A1dQZmO/QypXmsL0T8axYZkSN/uA/T/A64pfKdBAMiY= github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09 h1:QVxbx5l/0pzciWYOynixQMtUhPYC3YKD6EcUlOsgGqw= github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09/go.mod h1:Uy/Rnv5WKuOO+PuDhuYLEpUiiKIZtss3z519uk67aF0= github.com/tj/assert v0.0.0-20170216210512-748ebc778a69 h1:yjFMw2bnmM1YJSZn/aHiJ2yYZSBVL4oB8TC/+Y2pvIg= github.com/tj/assert v0.0.0-20170216210512-748ebc778a69/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/assert v0.0.1 h1:T7ozLNagrCCKl3wc+a706ztUCn/D6WHCJtkyvqYG+kQ= github.com/tj/assert v0.0.1/go.mod h1:lsg+GHQ0XplTcWKGxFLf/XPcPxWO8x2ut5jminoR2rA= github.com/tj/aws v0.1.4 h1:DZJArmjug0N7EvQxSs8W3gIDz1UzF8PeEE2MbTPhA80= github.com/tj/aws v0.1.4/go.mod h1:kG2Lk+22zcwmIKGMfSCcKDrqbVklm6AqVwQ/3BwbFec= github.com/tj/backoff v1.0.0 h1:Zi5jr//0f/ZtFXzXA2sx4pd/n3KTMK6b3xO2D1y50I4= github.com/tj/backoff v1.0.0/go.mod h1:opbA1vtO/vdNR4wI5izntg38viykxrVNMku6WVSWU/k= github.com/tj/go v1.8.5 h1:QbRCjQp1v308lsdHe+x/XYJ2U+WdSxY9tWiduvOFsPU= github.com/tj/go v1.8.5/go.mod h1:iDIwBG1ZkyeGIOBZLZQfpIztHr5m0gG+YGXrKaUC4yE= github.com/tj/go v1.8.6 h1:HZ+XV+wB4vqN5y5VLoZqYUuUJTBF+2kblBru7aUa44E= github.com/tj/go v1.8.6/go.mod h1:iDIwBG1ZkyeGIOBZLZQfpIztHr5m0gG+YGXrKaUC4yE= github.com/tj/go v1.8.7 h1:a7M1Xo+QKmlUHEzZj2LX0LHqkh7/LpOa6Or8luBvY/c= github.com/tj/go v1.8.7/go.mod h1:88DQADQo0c0fHmWNcr88pIGUHlV5du8aGtON+S1jr5A= github.com/tj/go-archive v1.0.2 h1:qrtC9BD1rrxuaUAD3sgzaqkt98YeGelE2aJAwZlFL24= github.com/tj/go-archive v1.0.2/go.mod h1:qqEdZXPGCYTFmpsGAGlTxUIv5fWK8NBEdvzU7NV3fJo= github.com/tj/go-cli-analytics v1.0.0 h1:aOf/mTKoyMSy8VnvLOTNPDwImjxT7mNC7ivmpTmRtdg= github.com/tj/go-cli-analytics v1.0.0/go.mod h1:Iwh92wE6jdgYylSZpfFmEoDtB812G235Nq/FtBYmv3E= github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= github.com/tj/go-headers v0.0.0-20170630155323-711a635412ca h1:xcArqZ1PrVz/YH9AnEd8vCuEVY4vGKqeC6zFNzS56Mg= github.com/tj/go-headers v0.0.0-20170630155323-711a635412ca/go.mod h1:TplXCeOi+Y4CQVdB8KFG4+EtmxgxQaauYO/h3cASuZA= github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= github.com/tj/go-progress v0.0.0-20171031175334-333acdb6fe9f h1:adrNJejn+15T3PPv/M2scCcFcc9WzCZYdLw25nLl76Y= github.com/tj/go-progress v0.0.0-20171031175334-333acdb6fe9f/go.mod h1:abH8hpo1+c7MbAa0ZCKvvGOgowFNgaoRQEcY0vsRTh4= github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds= github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= github.com/tj/go-update v2.2.4+incompatible h1:7Rkw5ZyRSFb3QyEWM7sHCy9rCy1/r66elkOyGlfnZFc= github.com/tj/go-update v2.2.4+incompatible/go.mod h1:waFwwyiAhGey2e+dNoYQ/iLhIcFqhCW7zL/+vDU1WLo= github.com/tj/kingpin v2.5.0+incompatible h1:nZWdCABGeebLFX5Ha/rYqxgEQpSXYWh5N9Dx2sGR0Bs= github.com/tj/kingpin v2.5.0+incompatible/go.mod h1:/babRmtQneL+pp+Yi24s2gukswokaKCR4gfjGbnjHBk= github.com/tj/survey v2.0.6+incompatible h1:tVgc1+kmYX9R0CEoHaTczapjdc4GaJla0VAB7O+w1So= github.com/tj/survey v2.0.6+incompatible/go.mod h1:vLPzQYAOKWgXqr5jV9luQXJuoXKHOg0ltn5FEw1Nz0c= github.com/ulikunitz/xz v0.5.4 h1:zATC2OoZ8H1TZll3FpbX+ikwmadbO699PE06cIkm9oU= github.com/ulikunitz/xz v0.5.4/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.0.0-20171027103834-c73622c77280 h1:TFSo8RGq2v9crRl/RW0EH71y1kdSjqeCxljzuDsD+oA= golang.org/x/net v0.0.0-20171027103834-c73622c77280/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20171031081856-95c657629925 h1:nCH33NboKIsT4HoXBsXTWX8ul303HxWgkc5s2Ezwacg= golang.org/x/sys v0.0.0-20171031081856-95c657629925/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20171102192421-88f656faf3f3 h1:TtrmcC9vFAjk6IwmXFdqQovdiZxrqQycAYaeCHauPKU= golang.org/x/text v0.0.0-20171102192421-88f656faf3f3/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200602174320-3e3e88ca92fa h1:5lGs+2OAqZvyIo1XjvoyXoDb8g6k9uAg2WTflQT/yl8= gopkg.in/yaml.v3 v3.0.0-20200602174320-3e3e88ca92fa/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: handler/handler.go ================================================ // Package handler provides what is essentially the core of Up's // reverse proxy, complete with all middleware for handling // logging, redirectcs, static file serving and so on. package handler import ( "net/http" "github.com/pkg/errors" "github.com/apex/up" "github.com/apex/up/http/cors" "github.com/apex/up/http/errorpages" "github.com/apex/up/http/gzip" "github.com/apex/up/http/headers" "github.com/apex/up/http/inject" "github.com/apex/up/http/logs" "github.com/apex/up/http/poweredby" "github.com/apex/up/http/redirects" "github.com/apex/up/http/relay" "github.com/apex/up/http/robots" "github.com/apex/up/http/static" ) // FromConfig returns the handler based on user config. func FromConfig(c *up.Config) (http.Handler, error) { switch c.Type { case "server": return relay.New(c) case "static": return static.New(c), nil default: return nil, errors.Errorf("unknown .type %q", c.Type) } } // New handler complete with all Up middleware. func New(c *up.Config, h http.Handler) (http.Handler, error) { h = poweredby.New("up", h) h = robots.New(c, h) h = static.NewDynamic(c, h) h, err := headers.New(c, h) if err != nil { return nil, errors.Wrap(err, "headers") } h = cors.New(c, h) h, err = errorpages.New(c, h) if err != nil { return nil, errors.Wrap(err, "error pages") } h, err = inject.New(c, h) if err != nil { return nil, errors.Wrap(err, "inject") } h, err = redirects.New(c, h) if err != nil { return nil, errors.Wrap(err, "redirects") } h = gzip.New(c, h) h, err = logs.New(c, h) if err != nil { return nil, errors.Wrap(err, "logs") } return h, nil } ================================================ FILE: handler/handler_test.go ================================================ package handler import ( "net/http" "net/http/httptest" "os" "testing" "github.com/apex/up" "github.com/tj/assert" ) func newHandler(t testing.TB, c *up.Config) http.Handler { h, err := FromConfig(c) assert.NoError(t, err, "FromConfig") h, err = New(c, h) assert.NoError(t, err, "New") return h } func TestNode(t *testing.T) { os.Chdir("testdata/node") defer os.Chdir("../..") c, err := up.ReadConfig("up.json") assert.NoError(t, err, "read config") h := newHandler(t, c) res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) h.ServeHTTP(res, req) actual := res.Header() assert.NotEmpty(t, actual.Get("Date"), "date") actual.Del("Date") header := make(http.Header) header.Add("X-Powered-By", "up") header.Add("X-Robots-Tag", "none") header.Add("X-Foo", "bar") header.Add("Content-Length", "11") header.Add("Content-Type", "text/plain; charset=utf-8") header.Add("Vary", "Accept-Encoding") assert.Equal(t, header, actual) } func TestStatic(t *testing.T) { os.Chdir("testdata/static") defer os.Chdir("../..") c, err := up.ReadConfig("up.json") assert.NoError(t, err, "read config") h := newHandler(t, c) res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) h.ServeHTTP(res, req) actual := res.Header() assert.NotEmpty(t, actual.Get("Last-Modified"), "last-modified") actual.Del("Last-Modified") header := make(http.Header) header.Add("X-Powered-By", "up") header.Add("X-Robots-Tag", "none") header.Add("Content-Length", "12") header.Add("Content-Type", "text/html; charset=utf-8") header.Add("Accept-Ranges", "bytes") header.Add("Vary", "Accept-Encoding") assert.Equal(t, header, actual) } func TestNodeWithPackage(t *testing.T) { os.Chdir("testdata/node-pkg") defer os.Chdir("../..") c, err := up.ReadConfig("up.json") assert.NoError(t, err, "read config") h := newHandler(t, c) res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) h.ServeHTTP(res, req) assert.Equal(t, "Hello World", res.Body.String()) } func TestNodeWithPackageStart(t *testing.T) { os.Chdir("testdata/node-pkg-start") defer os.Chdir("../..") c, err := up.ReadConfig("up.json") assert.NoError(t, err, "read config") h := newHandler(t, c) res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) h.ServeHTTP(res, req) assert.Equal(t, "Hello World", res.Body.String()) } func TestHandler_conditionalGet(t *testing.T) { os.Chdir("testdata/static") defer os.Chdir("../..") c, err := up.ReadConfig("up.json") assert.NoError(t, err, "read config") h := newHandler(t, c) res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/style.css", nil) req.Header.Set("If-Modified-Since", "Thu, 27 Jul 2030 03:30:31 GMT") h.ServeHTTP(res, req) assert.Equal(t, 304, res.Code) assert.Equal(t, "", res.Header().Get("Content-Length")) assert.Equal(t, "", res.Body.String()) } func TestHandler_rewrite(t *testing.T) { os.Chdir("testdata/static-rewrites") defer os.Chdir("../..") c, err := up.ReadConfig("up.json") assert.NoError(t, err, "read config") h := newHandler(t, c) res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/docs/ping/guides/alerts", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "14", res.Header().Get("Content-Length")) assert.Equal(t, "Alerting docs\n", res.Body.String()) } func TestHandler_redirect(t *testing.T) { os.Chdir("testdata/static-redirects") defer os.Chdir("../..") c, err := up.ReadConfig("up.json") assert.NoError(t, err, "read config") h := newHandler(t, c) res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/docs/ping/guides/alerts/", nil) h.ServeHTTP(res, req) assert.Equal(t, "/help/ping/alerts", res.Header().Get("Location")) assert.Equal(t, 302, res.Code) assert.Equal(t, "Found\n", res.Body.String()) } func TestHandler_spa(t *testing.T) { os.Chdir("testdata/spa") defer os.Chdir("../..") c, err := up.ReadConfig("up.json") assert.NoError(t, err, "read config") h := newHandler(t, c) t.Run("index", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "Index\n", res.Body.String()) }) t.Run("redirect", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/index.html", nil) h.ServeHTTP(res, req) assert.Equal(t, 301, res.Code) }) t.Run("file does not exist", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/something/here", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "Index\n", res.Body.String()) }) t.Run("file exists", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/app.js", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "app js\n", res.Body.String()) }) t.Run("file exists nested", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/css/bar.css", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "bar css\n", res.Body.String()) }) } ================================================ FILE: handler/testdata/node/app.js ================================================ const http = require('http') const port = process.env.PORT http.createServer((req, res) => { res.setHeader('X-Foo', 'bar') res.setHeader('Content-Type', 'text/plain; charset=utf-8') res.end('Hello World') }).listen(port, '127.0.0.1', _ => { console.log('listening') }) ================================================ FILE: handler/testdata/node/up.json ================================================ { "name": "app", "logs": { "enable": false } } ================================================ FILE: handler/testdata/node-pkg/app.js ================================================ const http = require('http') const { PORT } = process.env http.createServer((req, res) => { res.end('Hello World') }).listen(PORT) ================================================ FILE: handler/testdata/node-pkg/package.json ================================================ { "name": "something" } ================================================ FILE: handler/testdata/node-pkg/up.json ================================================ { "name": "app", "logs": { "enable": false } } ================================================ FILE: handler/testdata/node-pkg-start/index.js ================================================ const http = require('http') const { PORT } = process.env http.createServer((req, res) => { res.end('Hello World') }).listen(PORT) ================================================ FILE: handler/testdata/node-pkg-start/package.json ================================================ { "name": "something", "scripts": { "start": "node index" } } ================================================ FILE: handler/testdata/node-pkg-start/up.json ================================================ { "name": "app", "logs": { "enable": false } } ================================================ FILE: handler/testdata/spa/app.js ================================================ app js ================================================ FILE: handler/testdata/spa/css/bar.css ================================================ bar css ================================================ FILE: handler/testdata/spa/css/foo.css ================================================ foo css ================================================ FILE: handler/testdata/spa/index.html ================================================ Index ================================================ FILE: handler/testdata/spa/up.json ================================================ { "name": "app", "type": "static", "logs": { "enable": false }, "redirects": { "/*": { "location": "/", "status": 200 } } } ================================================ FILE: handler/testdata/static/index.html ================================================ Hello World ================================================ FILE: handler/testdata/static/style.css ================================================ body { background: whatever } ================================================ FILE: handler/testdata/static/up.json ================================================ { "name": "app", "type": "static", "logs": { "enable": false } } ================================================ FILE: handler/testdata/static-redirects/help/ping/alerts/index.html ================================================ Alerting docs ================================================ FILE: handler/testdata/static-redirects/index.html ================================================ Hello World ================================================ FILE: handler/testdata/static-redirects/up.json ================================================ { "name": "app", "type": "static", "logs": { "enable": false }, "redirects": { "/docs/:product/guides/:guide": { "location": "/help/:product/:guide", "status": 302 } } } ================================================ FILE: handler/testdata/static-rewrites/help/ping/alerts.html ================================================ Alerting docs ================================================ FILE: handler/testdata/static-rewrites/index.html ================================================ Hello World ================================================ FILE: handler/testdata/static-rewrites/up.json ================================================ { "name": "app", "type": "static", "logs": { "enable": false }, "redirects": { "/docs/:product/guides/:guide": { "location": "/help/:product/:guide.html", "status": 200 } } } ================================================ FILE: http/cors/cors.go ================================================ // Package cors provides CORS support. package cors import ( "net/http" "github.com/rs/cors" "github.com/apex/up" "github.com/apex/up/config" ) // New CORS handler. func New(c *up.Config, next http.Handler) http.Handler { if c.CORS == nil { return next } return cors.New(options(c.CORS)).Handler(next) } // options returns the canonical options. func options(c *config.CORS) cors.Options { return cors.Options{ AllowedOrigins: c.AllowedOrigins, AllowedMethods: c.AllowedMethods, AllowedHeaders: c.AllowedHeaders, ExposedHeaders: c.ExposedHeaders, AllowCredentials: c.AllowCredentials, MaxAge: c.MaxAge, Debug: c.Debug, } } ================================================ FILE: http/cors/cors_test.go ================================================ package cors import ( "fmt" "net/http" "net/http/httptest" "testing" "github.com/apex/up" "github.com/tj/assert" ) var hello = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hello World") }) func TestCORS_disabled(t *testing.T) { c, err := up.ParseConfigString(`{ "name": "app" }`) assert.NoError(t, err, "config") h := New(c, hello) t.Run("GET", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) req.Header.Set("Origin", "https://example.com") h.ServeHTTP(res, req) header := make(http.Header) header.Add("Content-Type", "text/plain; charset=utf-8") assert.Equal(t, 200, res.Code) assert.Equal(t, header, res.HeaderMap) assert.Equal(t, "Hello World", res.Body.String()) }) } func TestCORS_defaults(t *testing.T) { c, err := up.ParseConfigString(`{ "name": "app", "cors": {} }`) assert.NoError(t, err, "config") h := New(c, hello) t.Run("GET", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) req.Header.Set("Origin", "https://example.com") h.ServeHTTP(res, req) header := make(http.Header) header.Add("Content-Type", "text/plain; charset=utf-8") header.Add("Vary", "Origin") header.Add("Access-Control-Allow-Origin", "*") assert.Equal(t, 200, res.Code) assert.Equal(t, header, res.HeaderMap) assert.Equal(t, "Hello World", res.Body.String()) }) t.Run("OPTIONS", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("OPTIONS", "/", nil) req.Header.Set("Access-Control-Request-Method", "POST") req.Header.Set("Origin", "https://example.com") req.Header.Set("Access-Control-Request-Headers", "Content-Type") h.ServeHTTP(res, req) header := make(http.Header) header.Add("Vary", "Origin") header.Add("Vary", "Access-Control-Request-Method") header.Add("Vary", "Access-Control-Request-Headers") header.Add("Access-Control-Allow-Methods", "POST") header.Add("Access-Control-Allow-Headers", "Content-Type") header.Add("Access-Control-Allow-Origin", "*") assert.Equal(t, 200, res.Code) assert.Equal(t, header, res.HeaderMap) assert.Equal(t, "", res.Body.String()) }) } func TestCORS_options(t *testing.T) { c := up.MustParseConfigString(`{ "name": "app", "cors": { "allowed_origins": ["https://apex.sh"], "allowed_methods": ["GET"], "allow_credentials": true, "max_age": 86400 } }`) h := New(c, hello) t.Run("GET", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) req.Header.Set("Origin", "https://example.com") h.ServeHTTP(res, req) header := make(http.Header) header.Add("Content-Type", "text/plain; charset=utf-8") header.Add("Vary", "Origin") assert.Equal(t, 200, res.Code) assert.Equal(t, header, res.HeaderMap) assert.Equal(t, "Hello World", res.Body.String()) }) t.Run("OPTIONS", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("OPTIONS", "/", nil) req.Header.Set("Access-Control-Request-Method", "POST") req.Header.Set("Origin", "https://example.com") req.Header.Set("Access-Control-Request-Headers", "Content-Type") h.ServeHTTP(res, req) header := make(http.Header) header.Add("Vary", "Origin") header.Add("Vary", "Access-Control-Request-Method") header.Add("Vary", "Access-Control-Request-Headers") assert.Equal(t, 200, res.Code) assert.Equal(t, header, res.HeaderMap) assert.Equal(t, "", res.Body.String()) }) } ================================================ FILE: http/errorpages/errorpages.go ================================================ // Package errorpages provides default and customizable // error pages, via error.html, 5xx.html, or 500.html // for example. package errorpages import ( "io" "net/http" "github.com/pkg/errors" accept "github.com/timewasted/go-accept-headers" "github.com/apex/up" "github.com/apex/up/internal/errorpage" "github.com/apex/up/internal/logs" "github.com/apex/up/internal/util" ) // log context. var ctx = logs.Plugin("errorpages") // response wrapper. type response struct { http.ResponseWriter config *up.Config pages errorpage.Pages header bool ignore bool } // WriteHeader implementation. func (r *response) WriteHeader(code int) { w := r.ResponseWriter r.header = true page := r.pages.Match(code) if page == nil { ctx.Debugf("did not match %d", code) w.WriteHeader(code) return } ctx.Debugf("matched %d with %q", code, page.Name) data := struct { StatusText string StatusCode int Variables map[string]interface{} }{ StatusText: http.StatusText(code), StatusCode: code, Variables: r.config.ErrorPages.Variables, } html, err := page.Render(data) if err != nil { ctx.WithError(err).Error("rendering error page") http.Error(w, "Error rendering error page.", http.StatusInternalServerError) return } r.ignore = true util.ClearHeader(w.Header()) w.Header().Set("Vary", "Accept") w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(code) io.WriteString(w, html) } // Write implementation. func (r *response) Write(b []byte) (int, error) { if r.ignore { return len(b), nil } if !r.header { r.WriteHeader(200) return r.Write(b) } return r.ResponseWriter.Write(b) } // New error pages handler. func New(c *up.Config, next http.Handler) (http.Handler, error) { // disabled if !c.ErrorPages.Enable { return next, nil } // load pages pages, err := errorpage.Load(c.ErrorPages.Dir) if err != nil { return nil, errors.Wrap(err, "loading error pages") } h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mime, _ := accept.Negotiate(r.Header.Get("Accept"), "text/html") if mime == "" { next.ServeHTTP(w, r) return } res := &response{ResponseWriter: w, pages: pages, config: c} next.ServeHTTP(res, r) }) return h, nil } ================================================ FILE: http/errorpages/errorpages_test.go ================================================ package errorpages import ( "fmt" "io" "net/http" "net/http/httptest" "os" "testing" "github.com/tj/assert" "github.com/apex/up" "github.com/apex/up/config" ) var server = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/404" { w.Header().Set("ETag", "something") http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } if r.URL.Path == "/400" { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } if r.URL.Path == "/400/json" { w.WriteHeader(400) w.Header().Set("Content-Type", "application/json") io.WriteString(w, `{ "error": "bad_request" }`) return } if r.URL.Path == "/500" { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } w.Header().Set("ETag", "something") w.Header().Set("Content-Type", "text/plain") fmt.Fprintf(w, "Hello") fmt.Fprintf(w, " ") fmt.Fprintf(w, "World") }) func TestErrors_templates(t *testing.T) { os.Chdir("testdata/templates") defer os.Chdir("../..") c := &up.Config{ Name: "app", ErrorPages: config.ErrorPages{ Enable: true, }, } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") test(t, c) } func TestErrors_dir(t *testing.T) { c := &up.Config{ Name: "app", ErrorPages: config.ErrorPages{ Dir: "testdata/templates", Enable: true, }, } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") test(t, c) } func TestErrors_defaults(t *testing.T) { os.Chdir("testdata/defaults") defer os.Chdir("../..") c := &up.Config{ Name: "app", ErrorPages: config.ErrorPages{ Enable: true, }, } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") h, err := New(c, server) assert.NoError(t, err, "init") t.Run("200", nonError(h)) t.Run("accepts text/html", acceptsHTML(h)) t.Run("accepts text/*", acceptsText(h)) t.Run("does not accept html", doesNotAcceptHTML(h)) } func TestErrors_disabled(t *testing.T) { c := &up.Config{ Name: "app", } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") h, err := New(c, server) assert.NoError(t, err, "init") t.Run("200", nonError(h)) t.Run("error", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/404", nil) h.ServeHTTP(res, req) assert.Equal(t, 404, res.Code) assert.Equal(t, "text/plain; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "Not Found\n", res.Body.String()) }) t.Run("json error", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/400/json", nil) req.Header.Set("Accept", "application/json") h.ServeHTTP(res, req) assert.Equal(t, 400, res.Code) assert.Equal(t, "application/json", res.Header().Get("Content-Type")) assert.Equal(t, `{ "error": "bad_request" }`, res.Body.String()) }) } func test(t *testing.T, c *up.Config) { h, err := New(c, server) assert.NoError(t, err, "init") t.Run("200", nonError(h)) t.Run("accepts text/html", acceptsHTML(h)) t.Run("accepts text/*", acceptsText(h)) t.Run("does not accept html", doesNotAcceptHTML(h)) t.Run("exact", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/404", nil) h.ServeHTTP(res, req) assert.Equal(t, 404, res.Code) assert.Equal(t, "", res.Header().Get("ETag")) assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "Sorry! Can't find that.\n", res.Body.String()) }) t.Run("range", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/500", nil) h.ServeHTTP(res, req) assert.Equal(t, 500, res.Code) assert.Equal(t, "Accept", res.Header().Get("Vary")) assert.Equal(t, "", res.Header().Get("ETag")) assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "500 – Internal Server Error\n", res.Body.String()) }) } func nonError(h http.Handler) func(t *testing.T) { return func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "something", res.Header().Get("ETag")) assert.Equal(t, "text/plain", res.Header().Get("Content-Type")) assert.Equal(t, "Hello World", res.Body.String()) } } func acceptsHTML(h http.Handler) func(t *testing.T) { return func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/400", nil) req.Header.Set("Accept", "text/html") h.ServeHTTP(res, req) assert.Equal(t, 400, res.Code) assert.Equal(t, "Accept", res.Header().Get("Vary")) assert.Equal(t, "", res.Header().Get("ETag")) assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) assert.Contains(t, res.Body.String(), "Bad Request – 400", "title") assert.Contains(t, res.Body.String(), `Bad Request`, "status text") assert.Contains(t, res.Body.String(), `400`, "status code") } } func acceptsText(h http.Handler) func(t *testing.T) { return func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/400", nil) req.Header.Set("Accept", "text/*") h.ServeHTTP(res, req) assert.Equal(t, 400, res.Code) assert.Equal(t, "Accept", res.Header().Get("Vary")) assert.Equal(t, "", res.Header().Get("ETag")) assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) assert.Contains(t, res.Body.String(), "Bad Request – 400", "title") assert.Contains(t, res.Body.String(), `Bad Request`, "status text") assert.Contains(t, res.Body.String(), `400`, "status code") } } func doesNotAcceptHTML(h http.Handler) func(t *testing.T) { return func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/400/json", nil) req.Header.Set("Accept", "application/json") h.ServeHTTP(res, req) assert.Equal(t, 400, res.Code) assert.Equal(t, "application/json", res.Header().Get("Content-Type")) assert.Equal(t, `{ "error": "bad_request" }`, res.Body.String()) } } ================================================ FILE: http/errorpages/testdata/defaults/index.html ================================================ Index HTML ================================================ FILE: http/errorpages/testdata/defaults/up.json ================================================ { "name": "app" } ================================================ FILE: http/errorpages/testdata/templates/404.html ================================================ Sorry! Can't find that. ================================================ FILE: http/errorpages/testdata/templates/5xx.html ================================================ {{.StatusCode}} – {{.StatusText}} ================================================ FILE: http/errorpages/testdata/templates/index.html ================================================ Index HTML ================================================ FILE: http/errorpages/testdata/templates/up.json ================================================ { "name": "app" } ================================================ FILE: http/gzip/gzip.go ================================================ // Package gzip provides gzip compression support. package gzip import ( "net/http" "github.com/NYTimes/gziphandler" "github.com/apex/up" ) // New gzip handler. func New(c *up.Config, next http.Handler) http.Handler { return gziphandler.GzipHandler(next) } ================================================ FILE: http/gzip/gzip_test.go ================================================ package gzip import ( "compress/gzip" "fmt" "io/ioutil" "net/http" "net/http/httptest" "strings" "testing" "github.com/apex/up" "github.com/tj/assert" ) var body = strings.Repeat("так", 5000) var hello = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, body) }) func TestGzip(t *testing.T) { c, err := up.ParseConfigString(`{ "name": "app" }`) assert.NoError(t, err, "config") h := New(c, hello) t.Run("accepts gzip", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) req.Header.Set("Accept-Encoding", "gzip") h.ServeHTTP(res, req) header := make(http.Header) header.Add("Content-Type", "text/plain; charset=utf-8") header.Add("Content-Encoding", "gzip") header.Add("Vary", "Accept-Encoding") assert.Equal(t, 200, res.Code) assert.Equal(t, header, res.HeaderMap) gz, err := gzip.NewReader(res.Body) assert.NoError(t, err, "reader") b, err := ioutil.ReadAll(gz) assert.NoError(t, err, "reading") assert.NoError(t, gz.Close(), "close") assert.Equal(t, body, string(b)) }) t.Run("accepts identity", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) h.ServeHTTP(res, req) header := make(http.Header) header.Add("Content-Type", "text/plain; charset=utf-8") header.Add("Vary", "Accept-Encoding") assert.Equal(t, 200, res.Code) assert.Equal(t, header, res.HeaderMap) assert.Equal(t, body, res.Body.String()) }) } ================================================ FILE: http/headers/headers.go ================================================ // Package headers provides header injection support. package headers import ( "net/http" "os" "github.com/apex/log" "github.com/pkg/errors" hdr "github.com/tj/go-headers" "github.com/apex/up" "github.com/apex/up/internal/header" ) // TODO: document precedence and/or add options // TODO: maybe allow storing _headers in Static.Dir? // filename of headers file. var filename = "_headers" // New headers handler. func New(c *up.Config, next http.Handler) (http.Handler, error) { rulesFromFile, err := readFromFile(filename) if err != nil { return nil, errors.Wrap(err, "reading header file") } rules, err := header.Compile(header.Merge(rulesFromFile, c.Headers)) if err != nil { return nil, errors.Wrap(err, "compiling header") } log.Debugf("header rules from _headers file: %d", len(rulesFromFile)) log.Debugf("header rules from up.json: %d", len(c.Headers)) h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fields := rules.Lookup(r.URL.Path) for k, v := range fields { w.Header().Set(k, v) } next.ServeHTTP(w, r) }) return h, nil } // readFromFile reads from a Netlify style headers file. func readFromFile(path string) (header.Rules, error) { rules := make(header.Rules) f, err := os.Open(path) if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, errors.Wrap(err, "opening headers file") } defer f.Close() h, err := hdr.Parse(f) if err != nil { return nil, errors.Wrap(err, "parsing") } for path, fields := range h { rules[path] = make(header.Fields) for name, vals := range fields { rules[path][name] = vals[0] } } return rules, nil } ================================================ FILE: http/headers/headers_test.go ================================================ package headers import ( "net/http/httptest" "os" "testing" "github.com/tj/assert" "github.com/apex/up" "github.com/apex/up/http/static" "github.com/apex/up/internal/header" ) func TestHeaders(t *testing.T) { os.Chdir("testdata") defer os.Chdir("..") c := &up.Config{ Headers: header.Rules{ "/*.css": { "Cache-Control": "public, max-age=999999", }, }, } h, err := New(c, static.New(c)) assert.NoError(t, err, "init") t.Run("mismatch", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "", res.Header().Get("Cache-Control")) assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "Index HTML\n", res.Body.String()) }) t.Run("matched exact", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/style.css", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "public, max-age=999999", res.Header().Get("Cache-Control")) assert.Equal(t, "css", res.Header().Get("X-Type")) assert.Equal(t, "text/css; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "body { color: red }\n", res.Body.String()) }) } ================================================ FILE: http/headers/testdata/_headers ================================================ /*.css X-Type: css ================================================ FILE: http/headers/testdata/index.html ================================================ Index HTML ================================================ FILE: http/headers/testdata/style.css ================================================ body { color: red } ================================================ FILE: http/headers/testdata/up.json ================================================ { "name": "app" } ================================================ FILE: http/inject/inject.go ================================================ // Package inject provides script and style injection. package inject import ( "bytes" "io" "net/http" "strconv" "strings" "github.com/apex/up" "github.com/apex/up/internal/inject" ) // response wrapper. type response struct { http.ResponseWriter rules inject.Rules body bytes.Buffer header bool ignore bool code int } // Write implementation. func (r *response) Write(b []byte) (int, error) { if !r.header { r.WriteHeader(200) return r.Write(b) } return r.body.Write(b) } // WriteHeader implementation. func (r *response) WriteHeader(code int) { r.header = true w := r.ResponseWriter kind := w.Header().Get("Content-Type") r.ignore = !strings.HasPrefix(kind, "text/html") || code >= 300 r.code = code } // end injects if necessary. func (r *response) end() { w := r.ResponseWriter if r.ignore { w.WriteHeader(r.code) r.body.WriteTo(w) return } body := r.rules.Apply(r.body.String()) w.Header().Set("Content-Length", strconv.Itoa(len(body))) io.WriteString(w, body) } // New inject handler. func New(c *up.Config, next http.Handler) (http.Handler, error) { if len(c.Inject) == 0 { return next, nil } h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { res := &response{ResponseWriter: w, rules: c.Inject} next.ServeHTTP(res, r) res.end() }) return h, nil } ================================================ FILE: http/inject/inject_test.go ================================================ package inject import ( "io" "net/http" "net/http/httptest" "os" "testing" "github.com/apex/up" "github.com/tj/assert" "github.com/apex/up/config" "github.com/apex/up/http/errorpages" "github.com/apex/up/http/static" "github.com/apex/up/internal/inject" ) func TestInject(t *testing.T) { os.Chdir("testdata") defer os.Chdir("..") c := &up.Config{ Name: "app", ErrorPages: config.ErrorPages{ Enable: true, }, Inject: inject.Rules{ "head": []*inject.Rule{ { Type: "script", Value: "/whatever.js", }, }, }, } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") h, err := New(c, static.New(c)) assert.NoError(t, err, "init") h, err = errorpages.New(c, h) assert.NoError(t, err, "init") t.Run("2xx", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) h.ServeHTTP(res, req) html := ` ` assert.Equal(t, 200, res.Code) assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, html, res.Body.String()) }) t.Run("4xx", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/missing", nil) h.ServeHTTP(res, req) assert.Equal(t, 404, res.Code) assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "

Not Found

\n", res.Body.String()) }) t.Run("non-html", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/style.css", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "text/css; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "body{}\n", res.Body.String()) }) t.Run("write before header", func(t *testing.T) { s := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") io.WriteString(w, "") io.WriteString(w, "") io.WriteString(w, "") io.WriteString(w, "") io.WriteString(w, "") io.WriteString(w, "") }) h, err := New(c, s) assert.NoError(t, err, "initialize") res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, " \n ", res.Body.String()) }) } ================================================ FILE: http/inject/testdata/404.html ================================================

Not Found

================================================ FILE: http/inject/testdata/index.html ================================================ ================================================ FILE: http/inject/testdata/style.css ================================================ body{} ================================================ FILE: http/inject/testdata/up.json ================================================ { "name": "app" } ================================================ FILE: http/logs/logs.go ================================================ // Package logs provides HTTP request and response logging. package logs import ( "net/http" "strconv" "time" "github.com/apex/log" "github.com/apex/up" "github.com/apex/up/internal/logs" "github.com/apex/up/internal/util" ) // TODO: optional verbose mode with req/res header etc? // log context. var ctx = logs.Plugin("logs") // response wrapper. type response struct { http.ResponseWriter written int code int duration time.Duration } // Write implementation. func (r *response) Write(b []byte) (int, error) { n, err := r.ResponseWriter.Write(b) r.written += n return n, err } // WriteHeader implementation. func (r *response) WriteHeader(code int) { r.code = code r.ResponseWriter.WriteHeader(code) } // New logs handler. func New(c *up.Config, next http.Handler) (http.Handler, error) { if c.Logs.Disable { return next, nil } h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := logContext(r) logRequest(ctx, r) start := time.Now() res := &response{ResponseWriter: w, code: 200} next.ServeHTTP(res, r) res.duration = time.Since(start) logResponse(ctx, res, r) }) return h, nil } // logContext returns the common log context for a request. func logContext(r *http.Request) log.Interface { return ctx.WithFields(log.Fields{ "request_id": r.Header.Get("X-Request-Id"), "method": r.Method, "path": r.URL.Path, "query": r.URL.Query().Encode(), "ip": r.RemoteAddr, }) } // logRequest logs the request. func logRequest(ctx log.Interface, r *http.Request) { if s := r.Header.Get("Content-Length"); s != "" { n, err := strconv.Atoi(s) if err == nil { ctx = ctx.WithField("size", n) } } ctx.Info("request") } // logResponse logs the response. func logResponse(ctx log.Interface, res *response, r *http.Request) { ctx = ctx.WithFields(log.Fields{ "duration": util.Milliseconds(res.duration), "size": res.written, "status": res.code, }) switch { case res.code >= 500: ctx.Error("response") case res.code >= 400: ctx.Warn("response") default: ctx.Info("response") } } ================================================ FILE: http/logs/logs_test.go ================================================ package logs import ( "bytes" "log" "net/http/httptest" "testing" "github.com/tj/assert" "github.com/apex/up" "github.com/apex/up/config" "github.com/apex/up/http/static" ) func TestLogs(t *testing.T) { // TODO: refactor and pass in app name/version/region var buf bytes.Buffer log.SetOutput(&buf) c := &up.Config{ Static: config.Static{ Dir: "testdata", }, } h, err := New(c, static.New(c)) assert.NoError(t, err) res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/?foo=bar", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "Index HTML\n", res.Body.String()) s := buf.String() assert.Contains(t, s, `info response`) // assert.Contains(t, s, `app_name=api`) // assert.Contains(t, s, `app_version=5`) // assert.Contains(t, s, `app_region=us-west-2`) assert.Contains(t, s, `ip=192.0.2.1:1234`) assert.Contains(t, s, `method=GET`) assert.Contains(t, s, `path=/`) assert.Contains(t, s, `plugin=logs`) assert.Contains(t, s, `size=11`) assert.Contains(t, s, `status=200`) } ================================================ FILE: http/logs/testdata/index.html ================================================ Index HTML ================================================ FILE: http/logs/testdata/up.json ================================================ { "name": "api" } ================================================ FILE: http/poweredby/poweredby.go ================================================ // Package poweredby provides nothing :). package poweredby import ( "net/http" ) // New powered-by middleware. func New(name string, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Powered-By", name) next.ServeHTTP(w, r) }) } ================================================ FILE: http/poweredby/poweredby_test.go ================================================ package poweredby import ( "net/http/httptest" "testing" "github.com/tj/assert" "github.com/apex/up" "github.com/apex/up/config" "github.com/apex/up/http/static" ) func TestPoweredby(t *testing.T) { c := &up.Config{ Static: config.Static{ Dir: "testdata", }, } h := New("up", static.New(c)) res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "up", res.Header().Get("X-Powered-By")) assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "Index HTML\n", res.Body.String()) } ================================================ FILE: http/poweredby/testdata/index.html ================================================ Index HTML ================================================ FILE: http/poweredby/testdata/up.json ================================================ { "name": "app" } ================================================ FILE: http/redirects/redirects.go ================================================ // Package redirects provides redirection and URL rewriting. package redirects import ( "fmt" "net/http" "github.com/apex/log" "github.com/apex/up" "github.com/apex/up/internal/logs" "github.com/apex/up/internal/redirect" ) // TODO: tests for popagating 4xx / 5xx, dont mask all these // TODO: load _redirects relative to .Static.Dir? // TODO: add list of methods to match on // log context. var ctx = logs.Plugin("redirects") type rewrite struct { http.ResponseWriter header bool isNotFound bool } // WriteHeader implementation. func (r *rewrite) WriteHeader(code int) { r.header = true r.isNotFound = code == 404 if r.isNotFound { return } r.ResponseWriter.WriteHeader(code) } // Write implementation. func (r *rewrite) Write(b []byte) (int, error) { if r.isNotFound { return len(b), nil } if !r.header { r.WriteHeader(200) return r.Write(b) } return r.ResponseWriter.Write(b) } // New redirects handler. func New(c *up.Config, next http.Handler) (http.Handler, error) { if len(c.Redirects) == 0 { return next, nil } rules, err := redirect.Compile(c.Redirects) if err != nil { return nil, err } h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rule := rules.Lookup(r.URL.Path) ctx := ctx.WithFields(log.Fields{ "path": r.URL.Path, }) // pass-through if rule == nil { ctx.Debug("no match") next.ServeHTTP(w, r) return } // destination path path := rule.URL(r.URL.Path) // forced rewrite if rule.IsRewrite() && rule.Force { ctx.WithField("dest", path).Info("forced rewrite") r.Header.Set("X-Original-Path", r.URL.Path) r.URL.Path = path next.ServeHTTP(w, r) return } // rewrite if rule.IsRewrite() { res := &rewrite{ResponseWriter: w} next.ServeHTTP(res, r) if res.isNotFound { ctx.WithField("dest", path).Info("rewrite") r.Header.Set("X-Original-Path", r.URL.Path) r.URL.Path = path // This hack is necessary for SPAs because the Go // static file server uses .html to set the correct mime, // ideally it uses the file's extension or magic number etc. w.Header().Set("Content-Type", "text/html; charset=utf-8") next.ServeHTTP(w, r) } return } // redirect ctx.WithField("dest", path).Info("redirect") w.Header().Set("Location", path) w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(rule.Status) fmt.Fprintln(w, http.StatusText(rule.Status)) }) return h, nil } ================================================ FILE: http/redirects/redirects_test.go ================================================ package redirects import ( "fmt" "net/http" "net/http/httptest" "os" "strings" "testing" "github.com/apex/up" "github.com/tj/assert" "github.com/apex/up/internal/redirect" ) func TestRedirects(t *testing.T) { t.Run("from config", func(t *testing.T) { c := &up.Config{ Redirects: redirect.Rules{ "/blog": { Location: "https://blog.apex.sh", Status: 301, }, "/enterprise": { Location: "/docs/enterprise", Status: 302, }, "/api": { Location: "/api/v1", Status: 200, }, "/products": { Location: "/store", Status: 301, }, "/app/*": { Location: "/", }, "/app/login": { Location: "https://app.apex.sh", Status: 301, }, "/documentation/:product/guides/:guide": { Location: "/docs/:product/:guide", Status: 200, }, "/shop/:brand": { Location: "/products/:brand", Status: 301, }, "/settings/*": { Location: "/admin/:splat", Status: 200, Force: true, }, }, } handle := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/": fmt.Fprintln(w, "Index") case r.URL.Path == "/api/v1": fmt.Fprintln(w, "API V1") case r.URL.Path == "/products": fmt.Fprintln(w, "products") case strings.Contains(r.URL.Path, "/docs"): fmt.Fprintf(w, "docs %s", r.URL.Path) case strings.HasPrefix(r.URL.Path, "/brand"): fmt.Fprintf(w, "shop %s", r.URL.Path) case strings.HasPrefix(r.URL.Path, "/setting"): fmt.Fprintf(w, "settings %s", r.URL.Path) case strings.HasPrefix(r.URL.Path, "/admin"): fmt.Fprintf(w, "admin %s", r.URL.Path) default: http.NotFound(w, r) } }) h, err := New(c, handle) assert.NoError(t, err, "init") test(t, h) }) } func test(t *testing.T, h http.Handler) { os.Chdir("testdata") defer os.Chdir("..") t.Run("mismatch", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "Index\n", res.Body.String()) }) t.Run("exact match", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/blog", nil) h.ServeHTTP(res, req) assert.Equal(t, 301, res.Code) assert.Equal(t, "text/plain; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "Moved Permanently\n", res.Body.String()) }) t.Run("exact match status", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/enterprise", nil) h.ServeHTTP(res, req) assert.Equal(t, 302, res.Code) assert.Equal(t, "/docs/enterprise", res.Header().Get("Location")) assert.Equal(t, "text/plain; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "Found\n", res.Body.String()) }) t.Run("exact match rewrite", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "/api", req.Header.Get("X-Original-Path")) assert.Empty(t, res.Header().Get("Location"), "location") assert.Equal(t, "API V1\n", res.Body.String()) }) t.Run("shadowed exact", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/products", nil) h.ServeHTTP(res, req) assert.Equal(t, 301, res.Code) assert.Equal(t, "/store", res.Header().Get("Location")) assert.Equal(t, "Moved Permanently\n", res.Body.String()) }) t.Run("shadowed dynamic", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/app/contact", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "Index\n", res.Body.String()) }) t.Run("match precedence", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/app/login", nil) h.ServeHTTP(res, req) assert.Equal(t, 301, res.Code) assert.Equal(t, "https://app.apex.sh", res.Header().Get("Location")) assert.Equal(t, "Moved Permanently\n", res.Body.String()) }) t.Run("rewrite with placeholders", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/documentation/ping/guides/alerting", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "docs /docs/ping/alerting", res.Body.String()) }) t.Run("redirect with placeholders", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/shop/apple", nil) h.ServeHTTP(res, req) assert.Equal(t, 301, res.Code) assert.Equal(t, "Moved Permanently\n", res.Body.String()) }) t.Run("forced rewrite", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/settings/login", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "admin /admin/login", res.Body.String()) }) } ================================================ FILE: http/relay/relay.go ================================================ // Package relay provides a reverse proxy which // relays requests to your "vanilla" HTTP server, // and supports crash recovery. package relay import ( "fmt" "net" "net/http" "net/http/httputil" "net/url" "os" "os/exec" "strconv" "sync" "time" "github.com/apex/log" "github.com/facebookgo/freeport" "github.com/pkg/errors" "github.com/apex/up" "github.com/apex/up/internal/logs" "github.com/apex/up/internal/logs/writer" "github.com/apex/up/internal/util" ) // log context. var ctx = logs.Plugin("relay") // Proxy is a reverse proxy and sub-process monitor // for ensuring your web server is running. type Proxy struct { config *up.Config // transport used for the reverse proxy. transport http.RoundTripper // stdout is the log writer for structured logging output. stdout *writer.Writer // stderr is the log writer for structured logging output. stderr *writer.Writer mu sync.Mutex // restarts is the restart count. restarts int // url is the active application url. url *url.URL // ReverseProxy is the reverse proxy making the requests to the app. *httputil.ReverseProxy // cmd is the current child process of the app. cmd *exec.Cmd } // New proxy. // // We want to buffer the cleanup channel so that we can bound the // number of concurrent processes executing, and prevent exhausting // the ulimits of the host OS. func New(c *up.Config) (http.Handler, error) { stdout, err := log.ParseLevel(c.Logs.Stdout) if err != nil { return nil, errors.Wrap(err, "invalid stdout log level") } stderr, err := log.ParseLevel(c.Logs.Stderr) if err != nil { return nil, errors.Wrap(err, "invalid stderr log level") } timeout := time.Duration(c.Proxy.Timeout) * time.Second p := &Proxy{ config: c, stdout: writer.New(stdout, ctx), stderr: writer.New(stderr, ctx), transport: newTransport(timeout), } if err := p.Start(); err != nil { return nil, err } return p, nil } // Start the server. func (p *Proxy) Start() error { if err := p.startServer(); err != nil { return err } p.ReverseProxy = httputil.NewSingleHostReverseProxy(p.url) p.ReverseProxy.Transport = p start := time.Now() timeout := time.Duration(p.config.Proxy.ListenTimeout) * time.Second ctx.Info("waiting for app to listen on PORT") if err := util.WaitForListen(p.url, timeout); err != nil { return errors.Wrapf(err, "waiting for %s to be in listening state", p.url.String()) } ctx.WithField("duration", util.MillisecondsSince(start)).Info("app listening") return nil } // Restart the server. func (p *Proxy) Restart() error { p.mu.Lock() defer p.mu.Unlock() ctx.Warn("restarting") p.restarts++ if p.cmd != nil { if err := p.cmd.Process.Kill(); err != nil { ctx.WithError(err).Error("killing application process") } } if err := p.Start(); err != nil { return err } ctx.WithField("restarts", p.restarts).Warn("restarted") return nil } // RoundTrip implementation. func (p *Proxy) RoundTrip(r *http.Request) (*http.Response, error) { id := r.Header.Get("X-Request-Id") ctx = ctx.WithField("id", id) transport := p.transport // timeout header if s := r.Header.Get("X-Up-Timeout"); s != "" { if n, err := strconv.ParseInt(s, 10, 64); err == nil { transport = newTransport(time.Duration(n) * time.Second) } } res, err := transport.RoundTrip(r) // timeout error if e, ok := err.(net.Error); ok && e.Timeout() { ctx.WithError(err).Warn("request timeout") return res, err } // temporary error if e, ok := err.(net.Error); ok && e.Temporary() { ctx.WithError(err).Warn("request temporary error") return res, err } // network error if err != nil { ctx.WithError(err).Error("request network error") if err := p.Restart(); err != nil { ctx.WithError(err).Error("restarting") } } return res, err } // environment returns the server env variables. func (p *Proxy) environment() []string { return []string{ env("PORT", p.url.Port()), env("UP_RESTARTS", p.restarts), } } // startServer the server on a free port. func (p *Proxy) startServer() error { port, err := freeport.Get() if err != nil { return errors.Wrap(err, "getting free port") } target, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", port)) if err != nil { return errors.Wrap(err, "parsing url") } p.url = target ctx.WithField("command", p.config.Proxy.Command).WithField("PORT", port).Info("starting app") p.cmd = p.command(p.config.Proxy.Command, p.environment()) if err := p.cmd.Start(); err != nil { return errors.Wrap(err, "running command") } ctx.Info("started app") return nil } // command returns the command for spawning a server. func (p *Proxy) command(s string, env []string) *exec.Cmd { cmd := exec.Command("sh", "-c", s) cmd.Stdout = p.stdout cmd.Stderr = p.stderr cmd.Env = append(os.Environ(), append(env, "PATH=node_modules/.bin:"+os.Getenv("PATH"))...) return cmd } // newTransport returns a new http.Transport with the given timeout. func newTransport(timeout time.Duration) *http.Transport { return &http.Transport{ DialContext: (&net.Dialer{ Timeout: 2 * time.Second, KeepAlive: 2 * time.Second, DualStack: true, }).DialContext, ResponseHeaderTimeout: timeout, DisableKeepAlives: true, } } // env returns an environment variable. func env(name string, val interface{}) string { return fmt.Sprintf("%s=%v", name, val) } ================================================ FILE: http/relay/relay_test.go ================================================ package relay import ( "net/http" "net/http/httptest" "os" "strings" "testing" "time" "github.com/tj/assert" "github.com/apex/up" "github.com/apex/up/config" "github.com/apex/up/internal/util" ) func skipCI(t testing.TB) { if util.IsCI() { t.SkipNow() } } func TestRelay(t *testing.T) { os.Chdir("testdata/basic") defer os.Chdir("../..") c := &up.Config{ Proxy: config.Relay{ Timeout: 2, ListenTimeout: 2, }, } assert.NoError(t, c.Default(), "default") var h http.Handler newHandler := func(t *testing.T) { v, err := New(c) assert.NoError(t, err, "init") h = v } t.Run("GET simple", func(t *testing.T) { newHandler(t) res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/hello", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "text/plain", res.Header().Get("Content-Type")) assert.Equal(t, "Hello World", res.Body.String()) }) t.Run("GET encoded path", func(t *testing.T) { newHandler(t) res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/echo/01BM82CJ9K1WK6EFJX8C1R4YH7/foo%20%25%20bar%20&%20baz%20=%20raz", nil) req.Header.Set("Host", "example.com") req.Header.Set("User-Agent", "tobi") h.ServeHTTP(res, req) body := `{ "header": { "host": "example.com", "user-agent": "tobi", "x-forwarded-for": "192.0.2.1", "accept-encoding": "gzip", "connection": "close" }, "url": "/echo/01BM82CJ9K1WK6EFJX8C1R4YH7/foo%20%25%20bar%20&%20baz%20=%20raz", "body": "" }` assert.Equal(t, 200, res.Code) assert.Equal(t, "application/json", res.Header().Get("Content-Type")) assertString(t, body, res.Body.String()) }) t.Run("POST simple", func(t *testing.T) { newHandler(t) res := httptest.NewRecorder() req := httptest.NewRequest("POST", "/echo/something", strings.NewReader("Some body here")) h.ServeHTTP(res, req) body := `{ "header": { "host": "example.com", "content-length": "14", "x-forwarded-for": "192.0.2.1", "accept-encoding": "gzip", "connection": "close" }, "url": "/echo/something", "body": "Some body here" }` assert.Equal(t, 200, res.Code) assert.Equal(t, "application/json", res.Header().Get("Content-Type")) assertString(t, body, res.Body.String()) }) t.Run("crash", func(t *testing.T) { newHandler(t) // first res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/throw", nil) h.ServeHTTP(res, req) assert.Equal(t, 502, res.Code) assertString(t, "", res.Body.String()) // wait for restart time.Sleep(time.Second) // second res = httptest.NewRecorder() req = httptest.NewRequest("GET", "/hello", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assertString(t, "Hello World", res.Body.String()) }) t.Run("timeout", func(t *testing.T) { newHandler(t) // first res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/timeout", nil) h.ServeHTTP(res, req) assert.Equal(t, 502, res.Code) assertString(t, "", res.Body.String()) // second res = httptest.NewRecorder() req = httptest.NewRequest("GET", "/hello", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assertString(t, "Hello World", res.Body.String()) }) t.Run("timeout header field", func(t *testing.T) { newHandler(t) // first start := time.Now() res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/timeout", nil) req.Header.Set("X-Up-Timeout", "1") h.ServeHTTP(res, req) assert.True(t, time.Since(start) < time.Second*2) assert.Equal(t, 502, res.Code) assertString(t, "", res.Body.String()) // second res = httptest.NewRecorder() req = httptest.NewRequest("GET", "/hello", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assertString(t, "Hello World", res.Body.String()) }) } func assertString(t testing.TB, want, got string) { t.Helper() if want != got { t.Fatalf("\nwant:\n\n%s\n\ngot:\n\n%s\n", want, got) } } ================================================ FILE: http/relay/testdata/basic/app.js ================================================ const http = require('http'); const url = require('url'); const qs = require('querystring'); const port = process.env.PORT; let server; const routes = {}; routes['/echo'] = (req, res) => { const buffers = [] req.on('data', b => buffers.push(b)) req.on('end', _ => { const body = Buffer.concat(buffers).toString() res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ header: req.headers, url: req.url, body }, null, 2)) }); }; routes['/timeout'] = (req, res) => { setTimeout(function(){ res.end('Hello') }, 50000); }; routes['/throw'] = (req, res) => { yaynode() }; routes['/exit'] = (req, res) => { process.exit() }; server = http.createServer((req, res) => { const r = Object.keys(routes).find(pattern => req.url.indexOf(pattern) === 0); const handler = r && routes[r]; if (handler) { handler(req, res); return; } res.setHeader('Content-Type', 'text/plain') res.end('Hello World') }).listen(port); ================================================ FILE: http/relay/testdata/basic/up.json ================================================ { "name": "app" } ================================================ FILE: http/relay/testdata/node/package.json ================================================ { "scripts": { "start": "node server" } } ================================================ FILE: http/relay/testdata/node/server.js ================================================ const http = require('http') const port = parseInt(process.env.PORT, 10) http.createServer((req, res) => { res.end('Node') }).listen(port) ================================================ FILE: http/relay/testdata/node/up.json ================================================ { "name": "app" } ================================================ FILE: http/robots/robots.go ================================================ // Package robots provides a way of dealing with robots exclusion protocol package robots import ( "net/http" "os" "github.com/apex/up" ) // New robots middleware. func New(c *up.Config, next http.Handler) http.Handler { if os.Getenv("UP_STAGE") == "production" { return next } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Robots-Tag", "none") next.ServeHTTP(w, r) }) } ================================================ FILE: http/robots/robots_test.go ================================================ package robots import ( "net/http/httptest" "os" "testing" "github.com/apex/up" "github.com/tj/assert" "github.com/apex/up/config" "github.com/apex/up/http/static" ) func TestRobots(t *testing.T) { c := &up.Config{ Static: config.Static{ Dir: "testdata", }, } t.Run("should set X-Robots-Tag", func(t *testing.T) { h := New(c, static.New(c)) res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "none", res.Header().Get("X-Robots-Tag")) assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "Index HTML\n", res.Body.String()) }) t.Run("should not set X-Robots-Tag for production stage", func(t *testing.T) { os.Setenv("UP_STAGE", "production") defer os.Setenv("UP_STAGE", "") h := New(c, static.New(c)) res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "", res.Header().Get("X-Robots-Tag")) assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "Index HTML\n", res.Body.String()) }) } ================================================ FILE: http/robots/testdata/index.html ================================================ Index HTML ================================================ FILE: http/robots/testdata/up.json ================================================ { "name": "app" } ================================================ FILE: http/static/static.go ================================================ // Package static provides static file serving with HTTP cache support. package static import ( "net/http" "os" "path/filepath" "strings" "github.com/apex/up" ) // New static handler. func New(c *up.Config) http.Handler { return http.FileServer(http.Dir(c.Static.Dir)) } // NewDynamic static handler for dynamic apps. func NewDynamic(c *up.Config, next http.Handler) http.Handler { prefix := normalizePrefix(c.Static.Prefix) dir := c.Static.Dir if dir == "" { return next } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var skip bool path := r.URL.Path // prefix if prefix != "" { if strings.HasPrefix(path, prefix) { path = strings.Replace(path, prefix, "/", 1) } else { skip = true } } // convert path = filepath.FromSlash(path) // file exists, serve it if !skip { file := filepath.Join(dir, path) info, err := os.Stat(file) if !os.IsNotExist(err) && !info.IsDir() { http.ServeFile(w, r, file) return } } // delegate next.ServeHTTP(w, r) }) } // normalizePrefix returns a prefix path normalized with leading and trailing "/". func normalizePrefix(s string) string { if !strings.HasPrefix(s, "/") { s = "/" + s } if !strings.HasSuffix(s, "/") { s = s + "/" } return s } ================================================ FILE: http/static/static_test.go ================================================ package static import ( "fmt" "net/http" "net/http/httptest" "os" "testing" "github.com/apex/up" "github.com/apex/up/config" "github.com/tj/assert" ) func TestStatic_defaults(t *testing.T) { os.Chdir("testdata/static") defer os.Chdir("../..") c := &up.Config{Name: "app", Type: "static"} assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") test(t, c) } func TestStatic_dir(t *testing.T) { c := &up.Config{ Name: "app", Type: "static", Static: config.Static{ Dir: "testdata/static", }, } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") test(t, c) } func test(t *testing.T, c *up.Config) { h := New(c) t.Run("index.html", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "text/html; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "Index HTML\n", res.Body.String()) }) t.Run("file", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/style.css", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "text/css; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "body { background: whatever }\n", res.Body.String()) }) t.Run("missing", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/notfound", nil) h.ServeHTTP(res, req) assert.Equal(t, 404, res.Code) assert.Equal(t, "text/plain; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "404 page not found\n", res.Body.String()) }) t.Run("conditional get", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/style.css", nil) req.Header.Set("If-Modified-Since", "Thu, 27 Jul 2030 03:30:31 GMT") h.ServeHTTP(res, req) assert.Equal(t, 304, res.Code) assert.Equal(t, "", res.Header().Get("Content-Length")) assert.Equal(t, "", res.Body.String()) }) } func TestStatic_dynamic(t *testing.T) { c := &up.Config{ Name: "app", Static: config.Static{ Dir: "testdata/dynamic/public", }, } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") h := NewDynamic(c, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, ":)") })) t.Run("file", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/css/style.css", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "text/css; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "body { background: whatever }\n", res.Body.String()) }) t.Run("missing", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/notfound", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "text/plain; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, ":)\n", res.Body.String()) }) } func TestStatic_dynamicPrefix(t *testing.T) { c := &up.Config{ Name: "app", Static: config.Static{ Dir: "testdata/dynamic/public", Prefix: "/public", }, } assert.NoError(t, c.Default(), "default") assert.NoError(t, c.Validate(), "validate") h := NewDynamic(c, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, ":)") })) t.Run("/", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/index.html", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, ":)\n", res.Body.String()) }) t.Run("file", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/public/css/style.css", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, "text/css; charset=utf-8", res.Header().Get("Content-Type")) assert.Equal(t, "body { background: whatever }\n", res.Body.String()) }) t.Run("missing", func(t *testing.T) { res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/public/notfound", nil) h.ServeHTTP(res, req) assert.Equal(t, 200, res.Code) assert.Equal(t, ":)\n", res.Body.String()) }) } func TestNormalizePrefix(t *testing.T) { assert.Equal(t, `/public/`, normalizePrefix(`public`)) assert.Equal(t, `/public/`, normalizePrefix(`public/`)) assert.Equal(t, `/public/`, normalizePrefix(`/public`)) assert.Equal(t, `/public/`, normalizePrefix(`/public/`)) } ================================================ FILE: http/static/testdata/dynamic/app.js ================================================ const http = require('http') const port = process.env.PORT http.createServer((req, res) => { res.setHeader('X-Foo', 'bar') res.setHeader('Content-Type', 'text/plain; charset=utf-8') res.end('Hello World') }).listen(port) ================================================ FILE: http/static/testdata/dynamic/public/css/style.css ================================================ body { background: whatever } ================================================ FILE: http/static/testdata/dynamic/up.json ================================================ { "name": "app" } ================================================ FILE: http/static/testdata/static/index.html ================================================ Index HTML ================================================ FILE: http/static/testdata/static/style.css ================================================ body { background: whatever } ================================================ FILE: http/static/testdata/static/up.json ================================================ { "name": "app" } ================================================ FILE: install.sh ================================================ #!/bin/sh set -e # Code generated by godownloader. DO NOT EDIT. # usage() { this=$1 cat </dev/null } uname_os() { os=$(uname -s | tr '[:upper:]' '[:lower:]') echo "$os" } uname_arch() { arch=$(uname -m) case $arch in x86_64) arch="amd64" ;; x86) arch="386" ;; i686) arch="386" ;; i386) arch="386" ;; aarch64) arch="arm64" ;; armv5*) arch="arm5" ;; armv6*) arch="arm6" ;; armv7*) arch="arm7" ;; esac echo ${arch} } uname_os_check() { os=$(uname_os) case "$os" in darwin) return 0 ;; dragonfly) return 0 ;; freebsd) return 0 ;; linux) return 0 ;; android) return 0 ;; nacl) return 0 ;; netbsd) return 0 ;; openbsd) return 0 ;; plan9) return 0 ;; solaris) return 0 ;; windows) return 0 ;; esac echo "$0: uname_os_check: internal error '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" return 1 } uname_arch_check() { arch=$(uname_arch) case "$arch" in 386) return 0 ;; amd64) return 0 ;; arm64) return 0 ;; armv5) return 0 ;; armv6) return 0 ;; armv7) return 0 ;; ppc64) return 0 ;; ppc64le) return 0 ;; mips) return 0 ;; mipsle) return 0 ;; mips64) return 0 ;; mips64le) return 0 ;; s390x) return 0 ;; amd64p32) return 0 ;; esac echo "$0: uname_arch_check: internal error '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" return 1 } untar() { tarball=$1 case "${tarball}" in *.tar.gz | *.tgz) tar -xzf "${tarball}" ;; *.tar) tar -xf "${tarball}" ;; *.zip) unzip "${tarball}" ;; *) echo "Unknown archive format for ${tarball}" return 1 ;; esac } mktmpdir() { test -z "$TMPDIR" && TMPDIR="$(mktemp -d)" mkdir -p "${TMPDIR}" echo "${TMPDIR}" } http_download() { local_file=$1 source_url=$2 header=$3 headerflag='' destflag='' if is_command curl; then cmd='curl --fail -sSL' destflag='-o' headerflag='-H' elif is_command wget; then cmd='wget -q' destflag='-O' headerflag='--header' else echo "http_download: unable to find wget or curl" return 1 fi if [ -z "$header" ]; then $cmd $destflag "$local_file" "$source_url" else $cmd $headerflag "$header" $destflag "$local_file" "$source_url" fi } github_api() { local_file=$1 source_url=$2 header="" case "$source_url" in https://api.github.com*) test -z "$GITHUB_TOKEN" || header="Authorization: token $GITHUB_TOKEN" ;; esac http_download "$local_file" "$source_url" "$header" } github_last_release() { owner_repo=$1 giturl="https://api.github.com/repos/${owner_repo}/releases/latest" html=$(github_api - "$giturl") version=$(echo "$html" | tr ',' '\n' | grep -m 1 "\"tag_name\":" | cut -f4 -d'"') test -z "$version" && return 1 echo "$version" } hash_sha256() { TARGET=${1:-/dev/stdin} if is_command gsha256sum; then hash=$(gsha256sum "$TARGET") || return 1 echo "$hash" | cut -d ' ' -f 1 elif is_command sha256sum; then hash=$(sha256sum "$TARGET") || return 1 echo "$hash" | cut -d ' ' -f 1 elif is_command shasum; then hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 echo "$hash" | cut -d ' ' -f 1 elif is_command openssl; then hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 echo "$hash" | cut -d ' ' -f a else echo "hash_sha256: unable to find command to compute sha-256 hash" return 1 fi } hash_sha256_verify() { TARGET=$1 checksums=$2 if [ -z "$checksums" ]; then echo "hash_sha256_verify: checksum file not specified in arg2" return 1 fi BASENAME=${TARGET##*/} want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) if [ -z "$want" ]; then echo "hash_sha256_verify: unable to find checksum for '${TARGET}' in '${checksums}'" return 1 fi got=$(hash_sha256 "$TARGET") if [ "$want" != "$got" ]; then echo "hash_sha256_verify: checksum for '$TARGET' did not verify ${want} vs $got" return 1 fi } cat /dev/null <= 400 { b, _ := ioutil.ReadAll(res.Body) res.Body.Close() return nil, &Error{ Message: strings.TrimSpace(string(b)), Status: res.StatusCode, } } return res, nil } ================================================ FILE: internal/account/cards.go ================================================ package account import ( "github.com/stripe/stripe-go" "github.com/tj/survey" ) // Questions. var questions = []*survey.Question{ { Name: "name", Prompt: &survey.Input{Message: "Name:"}, Validate: survey.Required, }, { Name: "number", Prompt: &survey.Input{Message: "Number:"}, Validate: survey.Required, }, { Name: "cvc", Prompt: &survey.Input{Message: "CVC:"}, Validate: survey.Required, }, { Name: "month", Prompt: &survey.Input{Message: "Expiration month:"}, Validate: survey.Required, }, { Name: "year", Prompt: &survey.Input{Message: "Expiration year:"}, Validate: survey.Required, }, { Name: "address1", Prompt: &survey.Input{Message: "Street Address:"}, Validate: survey.Required, }, { Name: "city", Prompt: &survey.Input{Message: "City:"}, Validate: survey.Required, }, { Name: "state", Prompt: &survey.Input{Message: "State:"}, Validate: survey.Required, }, { Name: "country", Prompt: &survey.Input{Message: "Country:"}, Validate: survey.Required, }, { Name: "zip", Prompt: &survey.Input{Message: "Zip:"}, Validate: survey.Required, }, } // PromptForCard displays an interactive form for the user to provide CC details. func PromptForCard() (card stripe.CardParams, err error) { err = survey.Ask(questions, &card) return } ================================================ FILE: internal/cli/app/app.go ================================================ package app import ( "os" "time" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/stats" ) // Run the command. func Run(version string) error { defer stats.Client.ConditionalFlush(50, 6*time.Hour) root.Cmd.Version(version) _, err := root.Cmd.Parse(os.Args[1:]) return err } ================================================ FILE: internal/cli/build/build.go ================================================ package build import ( "archive/zip" "bytes" "fmt" "io" "os" "sort" "github.com/dustin/go-humanize" "github.com/pkg/errors" "github.com/tj/go/term" "github.com/tj/kingpin" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/colors" "github.com/apex/up/internal/stats" "github.com/apex/up/internal/util" ) func init() { cmd := root.Command("build", "Build zip file.") cmd.Example(`up build`, "Build archive and save to ./out.zip") cmd.Example(`up build > /tmp/out.zip`, "Build archive and output to file via stdout.") cmd.Example(`up build --size`, "Build archive and list files by size.") stage := cmd.Flag("stage", "Target stage name.").Short('s').Default("staging").String() size := cmd.Flag("size", "Show zip contents size information.").Bool() cmd.Action(func(_ *kingpin.ParseContext) error { defer util.Pad()() _, p, err := root.Init() if err != nil { return errors.Wrap(err, "initializing") } stats.Track("Build", nil) if err := p.Init(*stage); err != nil { return errors.Wrap(err, "initializing") } if err := p.Build(true); err != nil { return errors.Wrap(err, "building") } r, err := p.Zip() if err != nil { return errors.Wrap(err, "zip") } var out io.Writer var buf bytes.Buffer switch { default: out = os.Stdout case *size: out = &buf case term.IsTerminal(os.Stdout.Fd()): f, err := os.Create("out.zip") if err != nil { return errors.Wrap(err, "creating zip") } defer f.Close() out = f } if _, err := io.Copy(out, r); err != nil { return errors.Wrap(err, "copying") } if *size { z, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) if err != nil { return errors.Wrap(err, "opening zip") } files := z.File sort.Slice(files, func(i int, j int) bool { a := files[i] b := files[j] return a.UncompressedSize64 > b.UncompressedSize64 }) fmt.Printf("\n") for _, f := range files { size := humanize.Bytes(f.UncompressedSize64) fmt.Printf(" %10s %s\n", size, colors.Purple(f.Name)) } } return err }) } ================================================ FILE: internal/cli/config/config.go ================================================ package config import ( "encoding/json" "os" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/stats" "github.com/pkg/errors" "github.com/tj/kingpin" ) func init() { cmd := root.Command("config", "Show configuration after defaults and validation.") cmd.Example(`up config`, "Show the config.") cmd.Action(func(_ *kingpin.ParseContext) error { c, _, err := root.Init() if err != nil { return errors.Wrap(err, "initializing") } stats.Track("Show Config", nil) enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") enc.Encode(c) return nil }) } ================================================ FILE: internal/cli/deploy/deploy.go ================================================ package deploy import ( "os" "time" "github.com/pkg/errors" "github.com/tj/go/git" "github.com/tj/go/term" "github.com/tj/kingpin" "github.com/apex/up" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/setup" "github.com/apex/up/internal/stats" "github.com/apex/up/internal/util" "github.com/apex/up/internal/validate" ) func init() { cmd := root.Command("deploy", "Deploy the project.").Default() stage := cmd.Arg("stage", "Target stage name.").Default("staging").String() noBuild := cmd.Flag("no-build", "Disable build related hooks.").Bool() cmd.Example(`up deploy`, "Deploy to the staging environment.") cmd.Example(`up deploy production`, "Deploy to the production environment.") cmd.Example(`up deploy --no-build`, "Skip build hooks, useful in CI when a separate build step is used.") cmd.Action(func(_ *kingpin.ParseContext) error { return deploy(*stage, !*noBuild) }) } func deploy(stage string, build bool) error { retry: c, p, err := root.Init() // missing up.json non-interactive if isMissingConfig(err) && !term.IsTerminal(os.Stdin.Fd()) { return errors.New("Cannot find ./up.json configuration file.") } // missing up.json interactive if isMissingConfig(err) { err := setup.Create() if err == setup.ErrNoCredentials { return errors.New("Cannot find credentials, visit https://apex.sh/docs/up/credentials/ for help.") } if err != nil { return errors.Wrap(err, "setup") } util.Log("Deploying the project and creating resources.") goto retry } // unrelated error if err != nil { return errors.Wrap(err, "initializing") } // validate stage name if err := validate.List(stage, c.Stages.RemoteNames()); err != nil { return err } // stage overrides if err := c.Override(stage); err != nil { return errors.Wrap(err, "overriding") } // git information commit, err := getCommit() if err != nil { return errors.Wrap(err, "fetching git commit") } defer util.Pad()() start := time.Now() if err := p.Init(stage); err != nil { return errors.Wrap(err, "initializing") } if err := p.Deploy(up.Deploy{ Stage: stage, Commit: util.StripLerna(commit.Describe()), Author: commit.Author.Name, Build: build, }); err != nil { return err } stats.Track("Deploy", map[string]interface{}{ "duration": util.MillisecondsSince(start), "type": c.Type, "regions": c.Regions, "stage": stage, "proxy_timeout": c.Proxy.Timeout, "header_rules_count": len(c.Headers), "redirect_rules_count": len(c.Redirects), "inject_rules_count": len(c.Inject), "environment_count": len(c.Environment), "dns_zone_count": len(c.DNS.Zones), "stage_count": len(c.Stages.List()), "stage_domain_count": len(c.Stages.Domains()), "lambda_memory": c.Lambda.Memory, "has_cors": c.CORS != nil, "has_logs": !c.Logs.Disable, "has_profile": c.Profile != "", "has_error_pages": c.ErrorPages.Enable, "app_name_hash": util.Md5(c.Name), "is_git": commit.Author.Name != "", }) stats.Flush() return nil } // isMissingConfig returns true if the error represents a missing up.json. func isMissingConfig(err error) bool { err = errors.Cause(err) e, ok := err.(*os.PathError) return ok && e.Path == "up.json" } // getCommit returns the git information when available. func getCommit() (git.Commit, error) { c, err := git.GetCommit(".", "HEAD") if err != nil && !isIgnorable(err) { return git.Commit{}, err } if c == nil { return git.Commit{}, nil } return *c, nil } // isIgnorable returns true if the GIT error is ignorable. func isIgnorable(err error) bool { switch err { case git.ErrLookup, git.ErrNoRepo, git.ErrDirty: return true default: return false } } ================================================ FILE: internal/cli/disable-stats/disable-stats.go ================================================ package disablestats import ( "github.com/pkg/errors" "github.com/tj/kingpin" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/stats" ) func init() { cmd := root.Command("disable-stats", "Disable anonymized usage stats").Hidden() cmd.Action(func(_ *kingpin.ParseContext) error { err := stats.Client.Disable() if err != nil { return errors.Wrap(err, "disabling") } return nil }) } ================================================ FILE: internal/cli/docs/docs.go ================================================ package docs import ( "github.com/pkg/browser" "github.com/tj/kingpin" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/stats" ) var url = "https://up.docs.apex.sh" func init() { cmd := root.Command("docs", "Open documentation website in the browser.") cmd.Example(`up docs`, "Open the documentation site.") cmd.Action(func(_ *kingpin.ParseContext) error { stats.Track("Open Docs", nil) return browser.OpenURL(url) }) } ================================================ FILE: internal/cli/domains/domains.go ================================================ package domains import ( "fmt" "strings" "time" "github.com/pkg/errors" "github.com/tj/kingpin" "github.com/tj/survey" "github.com/apex/up" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/colors" "github.com/apex/up/internal/stats" "github.com/apex/up/internal/util" "github.com/apex/up/platform/aws/cost" ) // TODO: add ability to move up/down lines more like a form // TODO: add polling of registration status (it's async) // TODO: auto-fill these details from AWS account? func init() { cmd := root.Command("domains", "Manage domain names.") cmd.Example(`up domains`, "List purchased domains.") cmd.Example(`up domains check example.com`, "Check availability of a domain.") cmd.Example(`up domains buy`, "Purchase a domain.") list(cmd) check(cmd) buy(cmd) } // USD. var usd = colors.Gray("USD") // Questions. var questions = []*survey.Question{ { Name: "email", Prompt: &survey.Input{Message: "Email:"}, Validate: validateEmail, }, { Name: "firstname", Prompt: &survey.Input{Message: "First name:"}, Validate: survey.Required, }, { Name: "lastname", Prompt: &survey.Input{Message: "Last name:"}, Validate: survey.Required, }, { Name: "countrycode", Prompt: &survey.Input{Message: "Country code:"}, Validate: validateCountryCode, }, { Name: "city", Prompt: &survey.Input{Message: "City:"}, Validate: survey.Required, }, { Name: "address", Prompt: &survey.Input{Message: "Address:"}, Validate: survey.Required, }, { Name: "phonenumber", Prompt: &survey.Input{Message: "Phone:"}, Validate: validatePhoneNumber, }, { Name: "state", Prompt: &survey.Input{Message: "State:"}, Validate: survey.Required, }, { Name: "zipcode", Prompt: &survey.Input{Message: "Zip code:"}, Validate: survey.Required, }, } // buy a domain. func buy(cmd *kingpin.Cmd) { c := cmd.Command("buy", "Purchase a domain.") c.Action(func(_ *kingpin.ParseContext) error { defer util.Pad()() _, p, err := root.Init() if err != nil { return errors.Wrap(err, "initializing") } var domain string survey.AskOne(&survey.Input{ Message: "Domain:", }, &domain, survey.Required) var contact up.DomainContact if err := survey.Ask(questions, &contact); err != nil { return errors.Wrap(err, "prompting") } domains := p.Domains() if err := domains.Purchase(domain, contact); err != nil { return errors.Wrap(err, "purchasing") } return nil }) } // check domain availability. func check(cmd *kingpin.Cmd) { c := cmd.Command("check", "Check availability of a domain.") domain := c.Arg("domain", "Domain name.").Required().String() c.Action(func(_ *kingpin.ParseContext) error { defer util.Pad()() _, p, err := root.Init() if err != nil { return errors.Wrap(err, "initializing") } stats.Track("Check Domain Availability", nil) domains := p.Domains() d, err := domains.Availability(*domain) if err != nil { return errors.Wrap(err, "fetching availability") } state := fmt.Sprintf("Domain %s is unavailable", d.Name) if d.Available { state = fmt.Sprintf("Domain %s is available for %s %s", d.Name, cost.Domain(d.Name), usd) } fmt.Printf(" %s\n", colors.Bool(d.Available)(state)) if !d.Available { fmt.Printf("\n Suggestions:\n") suggestions, err := domains.Suggestions(*domain) if err != nil { return errors.Wrap(err, "fetching suggestions") } fmt.Printf("\n") for _, d := range suggestions { price := cost.Domain(d.Name) fmt.Printf(" %-40s %s %s\n", colors.Purple(d.Name), price, usd) } } return nil }) } // list domains purchased. func list(cmd *kingpin.Cmd) { c := cmd.Command("ls", "List purchased domains.").Alias("list").Default() c.Action(func(_ *kingpin.ParseContext) error { defer util.Pad()() _, p, err := root.Init() if err != nil { return errors.Wrap(err, "initializing") } stats.Track("List Domains", nil) domains, err := p.Domains().List() if err != nil { return errors.Wrap(err, "listing domains") } for _, d := range domains { s := "expires" if d.AutoRenew { s = "renews" } util.LogName(d.Name, "%s %s", s, d.Expiry.Format(time.Stamp)) } return nil }) } // validateEmail returns an error if the input does not look like an email. func validateEmail(v interface{}) error { s := v.(string) i := strings.LastIndex(s, "@") if s == "" { return errors.New("Email is required.") } if i == -1 { return errors.New("Email is missing '@'.") } if i == len(s)-1 { return errors.New("Email is missing domain.") } return nil } // validateCountryCode returns an error if the input does not look like a valid country code. func validateCountryCode(v interface{}) error { s := v.(string) if s == "" { return errors.New("Country code is required.") } if len(s) != 2 { return errors.New("Country codes must consist of two uppercase letters, such as CA or AU.") } return nil } // validatePhoneNumber returns an error if the input does not look like a valid phone number. func validatePhoneNumber(v interface{}) error { s := v.(string) if s == "" { return errors.New("Phone number is required.") } if !strings.HasPrefix(s, "+") { return errors.New("Phone number must contain the country code, for example +1.2223334444 for Canada.") } return nil } ================================================ FILE: internal/cli/logs/logs.go ================================================ package logs import ( "io" "os" "time" "github.com/pkg/errors" "github.com/tj/go/term" "github.com/tj/kingpin" "github.com/apex/up" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/stats" "github.com/apex/up/internal/util" ) func init() { cmd := root.Command("logs", "Show log output.") cmd.Example(`up logs`, "Show logs from the past hour.") cmd.Example(`up logs -S 30m`, "Show logs from the past 30 minutes.") cmd.Example(`up logs -S 5h`, "Show logs from the past 5 hours.") cmd.Example(`up logs -f`, "Show live log output.") cmd.Example(`up logs error`, "Show error logs.") cmd.Example(`up logs 'level != "info"'`, "Show non-info logs.") cmd.Example(`up logs 'production (warn or error)'`, "Show 4xx and 5xx responses in production.") cmd.Example(`up logs 'production error method in ("POST", "PUT", "DELETE")'`, "Show production 5xx responses with a POST, PUT, or DELETE method.") cmd.Example(`up logs 'error or fatal'`, "Show error and fatal logs.") cmd.Example(`up logs 'message = "user login"'`, "Show logs with a specific message.") cmd.Example(`up logs 'status = 200 duration > 1.5s'`, "Show 200 responses with latency above 1500ms.") cmd.Example(`up logs 'size > 100kb'`, "Show responses with bodies larger than 100kb.") cmd.Example(`up logs 'status >= 400'`, "Show 4xx and 5xx responses.") cmd.Example(`up logs 'user.email contains "@apex.sh"'`, "Show emails containing @apex.sh.") cmd.Example(`up logs 'user.email = "*@apex.sh"'`, "Show emails ending with @apex.sh.") cmd.Example(`up logs 'user.email = "tj@*"'`, "Show emails starting with tj@.") cmd.Example(`up logs 'method in ("POST", "PUT") ip = "207.*" status = 200 duration >= 50'`, "Show logs with a more complex query.") cmd.Example(`up logs error | jq`, "Pipe JSON error logs to the jq tool.") query := cmd.Arg("query", "Query pattern for filtering logs.").String() follow := cmd.Flag("follow", "Follow or tail the live logs.").Short('f').Bool() since := cmd.Flag("since", "Show logs since duration (30s, 5m, 2h, 1h30m, 3d, 1M).").Short('S').Default("1d").String() expand := cmd.Flag("expand", "Show expanded logs.").Short('e').Bool() cmd.Action(func(_ *kingpin.ParseContext) error { c, p, err := root.Init() if err != nil { return errors.Wrap(err, "initializing") } var s time.Duration if *since != "" { s, err = util.ParseDuration(*since) if err != nil { return errors.Wrap(err, "parsing --since duration") } } if *follow { s = time.Duration(0) } q := *query stats.Track("Logs", map[string]interface{}{ "query": q != "", "query_length": len(q), "follow": *follow, "since": s.Round(time.Second), "expand": *expand, }) logs := p.Logs(up.LogsConfig{ Region: c.Regions[0], Since: time.Now().Add(-s), Follow: *follow, Expand: *expand, Query: q, OutputJSON: !term.IsTerminal(os.Stdout.Fd()), }) if _, err := io.Copy(os.Stdout, logs); err != nil { return errors.Wrap(err, "writing logs") } return nil }) } ================================================ FILE: internal/cli/metrics/metrics.go ================================================ package metrics import ( "time" "github.com/pkg/errors" "github.com/tj/kingpin" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/stats" "github.com/apex/up/internal/util" ) func init() { cmd := root.Command("metrics", "Show project metrics.") cmd.Example(`up metrics`, "Show metrics for staging environment.") cmd.Example(`up metrics -s production`, "Show metrics for production environment.") stage := cmd.Flag("stage", "Target stage name.").Short('s').Default("staging").String() since := cmd.Flag("since", "Show metrics since duration (30s, 5m, 2h, 1h30m, 3d, 1M).").Short('S').Default("1M").String() cmd.Action(func(_ *kingpin.ParseContext) error { c, p, err := root.Init() if err != nil { return errors.Wrap(err, "initializing") } s, err := util.ParseDuration(*since) if err != nil { return errors.Wrap(err, "parsing --since duration") } region := c.Regions[0] stats.Track("Metrics", map[string]interface{}{ "stage": *stage, "since": s.Round(time.Second), }) start := time.Now().UTC().Add(-s) return p.ShowMetrics(region, *stage, start) }) } ================================================ FILE: internal/cli/prune/prune.go ================================================ package prune import ( "github.com/pkg/errors" "github.com/tj/kingpin" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/stats" ) func init() { cmd := root.Command("prune", "Prune old S3 deployments of a stage.") cmd.Example(`up prune`, "Prune and retain the most recent 30 staging versions.") cmd.Example(`up prune -s production`, "Prune and retain the most recent 30 production versions.") cmd.Example(`up prune -s production -r 15`, "Prune and retain the most recent 15 production versions.") stage := cmd.Flag("stage", "Target stage name.").Short('s').Default("staging").String() versions := cmd.Flag("retain", "Number of versions to retain.").Short('r').Default("30").Int() cmd.Action(func(_ *kingpin.ParseContext) error { c, p, err := root.Init() if err != nil { return errors.Wrap(err, "initializing") } region := c.Regions[0] stats.Track("Prune", map[string]interface{}{ "versions": *versions, "stage": *stage, }) return p.Prune(region, *stage, *versions) }) } ================================================ FILE: internal/cli/root/root.go ================================================ package root import ( "os" "runtime" "github.com/apex/log" "github.com/apex/log/handlers/cli" "github.com/apex/log/handlers/delta" "github.com/pkg/errors" "github.com/tj/kingpin" "github.com/apex/up" "github.com/apex/up/internal/util" "github.com/apex/up/platform/event" "github.com/apex/up/platform/lambda" "github.com/apex/up/reporter" ) // Cmd is the root command. var Cmd = kingpin.New("up", "") // Command registers a command. var Command = Cmd.Command // Init function. var Init func() (*up.Config, *up.Project, error) func init() { log.SetHandler(cli.Default) Cmd.Example(`up`, "Deploy the project to the staging environment.") Cmd.Example(`up deploy production`, "Deploy the project to the production stage.") Cmd.Example(`up url`, "Show the staging endpoint url.") Cmd.Example(`up logs -f`, "Tail project logs.") Cmd.Example(`up logs 'error or fatal'`, "Show error or fatal level logs.") Cmd.Example(`up run build`, "Run build command manually.") Cmd.Example(`up help team`, "Show help and examples for a command.") Cmd.Example(`up help team members`, "Show help and examples for a sub-command.") workdir := Cmd.Flag("chdir", "Change working directory.").Default(".").Short('C').String() verbose := Cmd.Flag("verbose", "Enable verbose log output.").Short('v').Bool() format := Cmd.Flag("format", "Output formatter.").Default("text").String() region := Cmd.Flag("region", "Target region id.").String() Cmd.PreAction(func(ctx *kingpin.ParseContext) error { os.Chdir(*workdir) if *verbose { log.SetHandler(delta.Default) log.SetLevel(log.DebugLevel) log.Debugf("up version %s (os: %s, arch: %s)", Cmd.GetVersion(), runtime.GOOS, runtime.GOARCH) } Init = func() (*up.Config, *up.Project, error) { c, err := up.ReadConfig("up.json") if err != nil { return nil, nil, errors.Wrap(err, "reading config") } if *region != "" { c.Regions = []string{*region} } events := make(event.Events) p := up.New(c, events).WithPlatform(lambda.New(c, events)) switch { case *verbose: go reporter.Discard(events) case *format == "plain" || util.IsCI(): go reporter.Plain(events) default: go reporter.Text(events) } return c, p, nil } return nil }) } ================================================ FILE: internal/cli/run/run.go ================================================ package run import ( "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/stats" "github.com/apex/up/internal/util" "github.com/pkg/errors" "github.com/tj/kingpin" ) func init() { cmd := root.Command("run", "Run a hook.") cmd.Example(`up run build`, "Run build hook.") cmd.Example(`up run clean`, "Run clean hook.") hook := cmd.Arg("hook", "Name of the hook to run.").Required().String() stage := cmd.Flag("stage", "Target stage name.").Short('s').Default("staging").String() cmd.Action(func(_ *kingpin.ParseContext) error { _, p, err := root.Init() if err != nil { return errors.Wrap(err, "initializing") } defer util.Pad()() stats.Track("Hook", map[string]interface{}{ "name": *hook, }) if err := p.Init(*stage); err != nil { return errors.Wrap(err, "initializing") } return p.RunHook(*hook) }) } ================================================ FILE: internal/cli/stack/stack.go ================================================ package stack import ( "fmt" "github.com/pkg/errors" "github.com/tj/kingpin" "github.com/tj/survey" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/stats" "github.com/apex/up/internal/util" ) func init() { cmd := root.Command("stack", "Stack resource management.") cmd.Example(`up stack`, "Show status of the stack resources.") cmd.Example(`up stack plan`, "Show resource changes.") cmd.Example(`up stack apply`, "Apply resource changes.") cmd.Example(`up stack delete`, "Delete the stack resources.") plan(cmd) apply(cmd) delete(cmd) status(cmd) } // plan changes. func plan(cmd *kingpin.Cmd) { c := cmd.Command("plan", "Plan configuration changes.") c.Example(`up stack plan`, "Show changes planned.") c.Action(func(_ *kingpin.ParseContext) error { c, p, err := root.Init() if err != nil { return errors.Wrap(err, "initializing") } stats.Track("Plan Stack", nil) region := c.Regions[0] ok, err := p.Exists(region) if err != nil { return errors.Wrap(err, "checking if app exists") } if !ok { return errors.New("Application does not exist, please run `$ up` initially to create it.") } // TODO: multi-region return p.PlanStack(region) }) } // apply changes. func apply(cmd *kingpin.Cmd) { c := cmd.Command("apply", "Apply configuration changes.") c.Example(`up stack apply`, "Apply the changes of the previous plan.") c.Action(func(_ *kingpin.ParseContext) error { c, p, err := root.Init() if err != nil { return errors.Wrap(err, "initializing") } stats.Track("Apply Stack", map[string]interface{}{ "dns_zone_count": len(c.DNS.Zones), "stage_count": len(c.Stages.List()), "stage_domain_count": len(c.Stages.Domains()), }) // TODO: multi-region return p.ApplyStack(c.Regions[0]) }) } // delete resources. func delete(cmd *kingpin.Cmd) { c := cmd.Command("delete", "Delete configured resources.") c.Example(`up stack delete`, "Delete stack with confirmation prompt.") c.Example(`up stack delete --force`, "Delete stack without confirmation prompt.") c.Example(`up stack delete --async`, "Don't wait for deletion to complete.") c.Example(`up stack delete -fa`, "Force asynchronous deletion.") force := c.Flag("force", "Skip the confirmation prompt.").Short('f').Bool() async := c.Flag("async", "Perform deletion asynchronously.").Short('a').Bool() c.Action(func(_ *kingpin.ParseContext) error { c, p, err := root.Init() if err != nil { return errors.Wrap(err, "initializing") } wait := !*async defer util.Pad()() stats.Track("Delete Stack", map[string]interface{}{ "force": *force, "wait": wait, }) if *force { // TODO: multi-region return p.DeleteStack(c.Regions[0], wait) } prompt := &survey.Confirm{ Message: fmt.Sprintf("Really destroy stack %q?", c.Name), } var ok bool if err := survey.AskOne(prompt, &ok, nil); err != nil { return err } if !ok { util.LogPad("Aborted") return nil } return p.DeleteStack(c.Regions[0], wait) }) } // status of the stack. func status(cmd *kingpin.Cmd) { c := cmd.Command("status", "Show status of resources.").Default() c.Action(func(_ *kingpin.ParseContext) error { c, p, err := root.Init() if err != nil { return errors.Wrap(err, "initializing") } stats.Track("Show Stack", nil) // TODO: multi-region return p.ShowStack(c.Regions[0]) }) } ================================================ FILE: internal/cli/start/start.go ================================================ package start import ( "fmt" "net" "net/http" "os" "github.com/apex/log" "github.com/pkg/browser" "github.com/pkg/errors" "github.com/tj/kingpin" "github.com/apex/up/handler" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/logs/text" "github.com/apex/up/internal/stats" ) func init() { cmd := root.Command("start", "Start development server.") cmd.Example(`up start`, "Start development server on port 3000.") cmd.Example(`up start -o`, "Start development server and open in the browser.") cmd.Example(`up start --address :5000`, "Start development server on port 5000.") cmd.Example(`up start -c 'go run main.go'`, "Override proxy command.") cmd.Example(`up start -oc 'gin --port $PORT'`, "Override proxy command and open in the browser.") stage := cmd.Flag("stage", "Target stage name.").Short('s').Default("development").String() command := cmd.Flag("command", "Proxy command override").Short('c').String() open := cmd.Flag("open", "Open endpoint in the browser.").Short('o').Bool() addr := cmd.Flag("address", "Address for server.").Default("localhost:3000").String() cmd.Action(func(_ *kingpin.ParseContext) error { log.SetHandler(text.New(os.Stdout)) c, p, err := root.Init() if err != nil { return errors.Wrap(err, "initializing") } for k, v := range c.Environment { os.Setenv(k, v) } stats.Track("Start", map[string]interface{}{ "address": *addr, "has_command": *command != "", }) if err := p.Init(*stage); err != nil { return errors.Wrap(err, "initializing") } if err := c.Override(*stage); err != nil { return errors.Wrap(err, "overriding") } if s := *command; s != "" { c.Proxy.Command = s } h, err := handler.FromConfig(c) if err != nil { return errors.Wrap(err, "selecting handler") } h, err = handler.New(c, h) if err != nil { return errors.Wrap(err, "initializing handler") } if *open { _, port, _ := net.SplitHostPort(*addr) browser.OpenURL(fmt.Sprintf("http://localhost:%s", port)) } log.WithField("address", "http://"+*addr).Info("listening") if err := http.ListenAndServe(*addr, h); err != nil { return errors.Wrap(err, "binding") } return nil }) } ================================================ FILE: internal/cli/team/team.go ================================================ package team import ( "context" "encoding/base64" "encoding/json" "fmt" "sort" "strings" "time" "github.com/pkg/errors" "github.com/segmentio/go-snakecase" "github.com/stripe/stripe-go" "github.com/stripe/stripe-go/token" "github.com/tj/go/clipboard" "github.com/tj/go/env" "github.com/tj/go/http/request" "github.com/tj/go/term" "github.com/tj/kingpin" "github.com/tj/survey" "github.com/apex/log" "github.com/apex/up/internal/account" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/colors" "github.com/apex/up/internal/stats" "github.com/apex/up/internal/userconfig" "github.com/apex/up/internal/util" "github.com/apex/up/platform/event" "github.com/apex/up/reporter" ) // api endpoint. var api = env.GetDefault("APEX_TEAMS_API", "https://teams.apex.sh") // api client. var a = account.New(api) // plan amounts. var amounts = map[string]int{ "monthly": 2000, "annually": 21600, } // plan amount select options. var amountOptions = map[string]string{ "Monthly at $20.00 USD": "monthly", "Annually at $216.00 USD": "annually", } func init() { cmd := root.Command("team", "Manage team members, plans, and billing.") cmd.Example(`up team`, "Show active team and subscription status.") cmd.Example(`up team login`, "Sign in or create account with interactive prompt.") cmd.Example(`up team login --email tj@example.com --team apex-software`, "Sign in to a team.") cmd.Example(`up team add "Apex Software"`, "Add a new team.") cmd.Example(`up team subscribe`, "Subscribe to the Pro plan.") cmd.Example(`up team members add asya@example.com`, "Invite a team member to your active team.") cmd.Example(`up team members rm tobi@example.com`, "Remove a team member from your active team.") cmd.Example(`up team card change`, "Change the default credit card.") cmd.Example(`up team switch`, "Switch teams interactively.") status(cmd) switchTeam(cmd) login(cmd) logout(cmd) members(cmd) subscribe(cmd) unsubscribe(cmd) card(cmd) copy(cmd) add(cmd) } // add command. func add(cmd *kingpin.Cmd) { c := cmd.Command("add", "Add a new team.") name := c.Arg("name", "Name of the team.").Required().String() c.Action(func(_ *kingpin.ParseContext) error { var config userconfig.Config if err := config.Load(); err != nil { return errors.Wrap(err, "loading config") } if !config.Authenticated() { return errors.New("Must sign in to create a new team.") } team := strings.Replace(snakecase.Snakecase(*name), "_", "-", -1) stats.Track("Add Team", map[string]interface{}{ "team": team, "name": name, }) t := config.GetActiveTeam() if err := a.AddTeam(t.Token, team, *name); err != nil { return errors.Wrap(err, "creating team") } defer util.Pad()() util.Log("Created team %s with id %s", *name, team) code, err := a.LoginWithToken(t.Token, t.Email, team) if err != nil { return errors.Wrap(err, "login") } // access key ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() token, err := a.PollAccessToken(ctx, t.Email, team, code) if err != nil { return errors.Wrap(err, "getting access token") } err = userconfig.Alter(func(c *userconfig.Config) { c.Team = team c.AddTeam(&userconfig.Team{ Token: token, ID: team, Email: t.Email, }) }) if err != nil { return errors.Wrap(err, "config") } util.Log("%s is now the active team.\n", *name) util.Log("Use `up team switch` to change teams.") return nil }) } // copy commands. func copy(cmd *kingpin.Cmd) { c := cmd.Command("ci", "Credentials for CI.") copy := c.Flag("copy", "Credentials to the clipboard.").Short('c').Bool() c.Action(func(_ *kingpin.ParseContext) error { var config userconfig.Config if err := config.Load(); err != nil { return errors.Wrap(err, "loading") } stats.Track("Copy Credentials", map[string]interface{}{ "copy": *copy, }) b, err := json.Marshal(config) if err != nil { return errors.Wrap(err, "marshaling") } s := base64.StdEncoding.EncodeToString(b) if *copy { clipboard.Write(s) fmt.Println("Copied to clipboard!") return nil } fmt.Printf("%s\n", s) return nil }) } // status of account. func status(cmd *kingpin.Cmd) { c := cmd.Command("status", "Status of your account.").Default() c.Action(func(_ *kingpin.ParseContext) error { var config userconfig.Config if err := config.Load(); err != nil { return errors.Wrap(err, "loading config") } defer util.Pad()() stats.Track("Account Status", nil) if !config.Authenticated() { util.LogName("status", "Signed out") return nil } t := config.GetActiveTeam() defer util.Pad()() util.LogName("team", t.ID) team, err := a.GetTeam(t.Token) if err != nil { return errors.Wrap(err, "fetching team") } plans, err := a.GetPlans(t.Token) if err != nil { return errors.Wrap(err, "fetching plans") } if len(plans) == 0 { util.LogName("subscription", "none") return nil } if c := team.Card; c != nil { util.LogName("card", "%s ending with %s", c.Brand, c.LastFour) } // TODO: filter on plan type (later may be other products) p := plans[0] if d := p.Discount; d != nil { p.Amount = d.Coupon.Discount(p.Amount) util.LogName("coupon", d.Coupon.ID) } util.LogName("amount", "%s USD per %s", currency(p.Amount), p.Interval) util.LogName("owner", team.Owner) util.LogName("created", p.CreatedAt.Format("January 2, 2006")) if p.Canceled { util.LogName("canceled", p.CanceledAt.Format("January 2, 2006")) } return nil }) } // switchTeam team. func switchTeam(cmd *kingpin.Cmd) { c := cmd.Command("switch", "Switch active team.") c.Example(`up team switch`, "Switch teams interactively.") c.Action(func(_ *kingpin.ParseContext) error { defer util.Pad()() var config userconfig.Config if err := config.Load(); err != nil { return errors.Wrap(err, "loading user config") } var options []string for _, t := range config.GetTeams() { options = append(options, t.ID) } sort.Strings(options) var team string prompt := &survey.Select{ Message: "", Options: options, Default: config.Team, } if err := survey.AskOne(prompt, &team, survey.Required); err != nil { return err } stats.Track("Switch Team", nil) err := userconfig.Alter(func(c *userconfig.Config) { c.Team = team }) if err != nil { return errors.Wrap(err, "saving config") } return nil }) } // login user. func login(cmd *kingpin.Cmd) { c := cmd.Command("login", "Sign in to your account.") c.Example(`up team login`, "Sign in or create account with interactive prompt.") c.Example(`up team login --team apex-software`, "Sign in to a team using your existing email.") c.Example(`up team login --email tj@example.com --team apex-software`, "Sign in to a team with email.") email := c.Flag("email", "Email address.").String() team := c.Flag("team", "Team id.").String() c.Action(func(_ *kingpin.ParseContext) error { var config userconfig.Config if err := config.Load(); err != nil { return errors.Wrap(err, "loading user config") } defer util.Pad()() stats.Track("Login", map[string]interface{}{ "team_count": len(config.GetTeams()), "has_email": *email != "", "has_team": *team != "", }) t := config.GetActiveTeam() // both team and email are specified, // so we want to disregard the active team // entirely and sign in using these creds. if *email != "" && *team != "" { t = nil } // ensure we have an email if *email == "" { if t == nil { if err := prompt("email:", email); err != nil { return err } } else { *email = t.Email } } // ensure we have a team if already signed-in, // this lets the user specify only --team xxx to // join a team they were invited to, or add one // which they own. if t != nil && *team == "" { util.Log("Already signed in as %s on team %s.", t.Email, t.ID) util.Log("Use `up team login --team ` to join a team.") return nil } // events events := make(event.Events) go reporter.Text(events) events.Emit("account.login.verify", nil) l := log.WithFields(log.Fields{ "email": *email, "team": *team, }) // authenticate var code string var err error if t == nil { l.Debug("login without token") code, err = a.Login(*email, *team) } else { l.Debug("login with token") code, err = a.LoginWithToken(t.Token, *email, *team) } if err != nil { return errors.Wrap(err, "login") } // personal team if *team == "" { team = email } // access key ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() l.WithField("team", *team).Debug("poll for access token") token, err := a.PollAccessToken(ctx, *email, *team, code) if err != nil { return errors.Wrap(err, "getting access token") } events.Emit("account.login.verified", nil) err = userconfig.Alter(func(c *userconfig.Config) { c.Team = *team c.AddTeam(&userconfig.Team{ Token: token, ID: *team, Email: *email, }) }) if err != nil { return errors.Wrap(err, "config") } return nil }) } // logout user. func logout(cmd *kingpin.Cmd) { c := cmd.Command("logout", "Sign out of your account.") c.Action(func(_ *kingpin.ParseContext) error { stats.Track("Logout", nil) var config userconfig.Config if err := config.Save(); err != nil { return errors.Wrap(err, "saving") } util.LogPad("Signed out") return nil }) } // subscribe to plan. func subscribe(cmd *kingpin.Cmd) { c := cmd.Command("subscribe", "Subscribe to the Pro plan.") c.Action(func(_ *kingpin.ParseContext) error { t, err := userconfig.Require() if err != nil { return err } defer util.Pad()() // plan util.LogTitle("Subscription") util.Log("Choose a monthly billing period, or 10%% off annually.") println() var interval string err = survey.AskOne(&survey.Select{ Message: "Plan:", Options: keys(amountOptions), }, &interval, survey.Required) // amount interval = amountOptions[interval] amount := amounts[interval] util.LogTitle("Coupon") util.Log("Enter a coupon, or press enter to skip this step") util.Log("and move on to adding a credit card.") println() // coupon var couponID string err = survey.AskOne(&survey.Input{ Message: "Coupon:", }, &couponID, nil) if err != nil { return err } // coupon if strings.TrimSpace(couponID) == "" { util.LogClear("No coupon provided") } else { coupon, err := a.GetCoupon(couponID) if err != nil && !request.IsNotFound(err) { return errors.Wrap(err, "fetching coupon") } if coupon == nil { util.LogClear("Coupon is invalid") } else { amount = coupon.Discount(amount) msg := colors.Gray(fmt.Sprintf("%s — now %s %s", coupon.Description(), currency(amount), interval)) util.LogClear("Savings: %s", msg) } } // add card util.LogTitle("Credit Card") util.Log("First add your credit card details, these are transferred") util.Log("directly to Stripe over HTTPS and never touch our servers.") println() card, err := account.PromptForCard() if err != nil { return errors.Wrap(err, "prompting for card") } tok, err := token.New(&stripe.TokenParams{ Card: &card, }) if err != nil { return errors.Wrap(err, "requesting card token") } if err := a.AddCard(t.Token, tok.ID); err != nil { return errors.Wrap(err, "adding card") } util.LogTitle("Confirm") // confirm var ok bool err = survey.AskOne(&survey.Confirm{ Message: fmt.Sprintf("Subscribe to Up Pro for %s USD %s?", currency(amount), interval), }, &ok, nil) if err != nil { return err } if !ok { util.LogPad("Aborted") stats.Track("Abort Subscription", nil) return nil } stats.Track("Subscribe", map[string]interface{}{ "coupon": couponID, "interval": interval, }) if err := a.AddPlan(t.Token, "up", interval, couponID); err != nil { return errors.Wrap(err, "subscribing") } util.LogClear("Thanks for subscribing! Run `up upgrade` to install the Pro release.") return nil }) } // unsubscribe from plan. func unsubscribe(cmd *kingpin.Cmd) { c := cmd.Command("unsubscribe", "Unsubscribe from the Pro plan.") c.Action(func(_ *kingpin.ParseContext) error { config, err := userconfig.Require() if err != nil { return err } defer util.Pad()() // confirm var ok bool err = survey.AskOne(&survey.Confirm{ Message: "Are you sure you want to unsubscribe?", }, &ok, nil) if err != nil { return err } if !ok { util.LogPad("Aborted") return nil } term.MoveUp(1) term.ClearLine() msg, err := feedback() if err != nil { util.LogPad("Aborted") return nil } if strings.TrimSpace(msg) != "" { util.Log("Thanks for the feedback!") _ = a.AddFeedback(config.Token, msg) } stats.Track("Unsubscribe", nil) if err := a.RemovePlan(config.Token, "up"); err != nil { return errors.Wrap(err, "unsubscribing") } util.LogClear("Unsubscribed!") return nil }) } // members commands. func members(cmd *kingpin.Cmd) { c := cmd.Command("members", "Member management.") addMember(c) removeMember(c) listMembers(c) } // addMember command. func addMember(cmd *kingpin.Cmd) { c := cmd.Command("add", "Add invites a team member.") c.Example(`up team members add asya@apex.sh`, "Invite a team member to the active team.") email := c.Arg("email", "Email address.").Required().String() c.Action(func(_ *kingpin.ParseContext) error { t, err := userconfig.Require() if err != nil { return err } stats.Track("Add Member", map[string]interface{}{ "team": t.ID, "email": *email, }) if err := a.AddInvite(t.Token, *email); err != nil { return errors.Wrap(err, "adding invite") } util.LogPad("Invited %s to team %s", *email, t.ID) return nil }) } // removeMember command. func removeMember(cmd *kingpin.Cmd) { c := cmd.Command("rm", "Remove a member or invite.").Alias("remove") c.Example(`up team members rm tobi@apex.sh`, "Remove a team member or invite from the active team.") email := c.Arg("email", "Email address.").Required().String() c.Action(func(_ *kingpin.ParseContext) error { t, err := userconfig.Require() if err != nil { return err } stats.Track("Remove Member", map[string]interface{}{ "team": t.ID, "email": *email, }) if err := a.RemoveMember(t.Token, *email); err != nil { return errors.Wrap(err, "removing member") } util.LogPad("Removed %s from team %s", *email, t.ID) return nil }) } // list members func listMembers(cmd *kingpin.Cmd) { c := cmd.Command("ls", "List team members and invites.").Alias("list").Default() c.Action(func(_ *kingpin.ParseContext) error { t, err := userconfig.Require() if err != nil { return err } stats.Track("List Members", map[string]interface{}{ "team": t.ID, }) team, err := a.GetTeam(t.Token) if err != nil { return errors.Wrap(err, "fetching team") } defer util.Pad()() util.LogName("team", t.ID) if len(team.Members) > 0 { util.LogTitle("Members") for _, u := range team.Members { util.LogListItem(u.Email) } } if len(team.Invites) > 0 { util.LogTitle("Invites") for _, email := range team.Invites { util.LogListItem(email) } } return nil }) } // card commands. func card(cmd *kingpin.Cmd) { c := cmd.Command("card", "Card management.") changeCard(c) } // changeCard command. func changeCard(cmd *kingpin.Cmd) { c := cmd.Command("change", "Change the default card.") c.Action(func(_ *kingpin.ParseContext) error { t, err := userconfig.Require() if err != nil { return err } defer util.Pad()() util.LogTitle("Credit Card") util.Log("Enter new credit card details to replace the existing card.") println() card, err := account.PromptForCard() if err != nil { return errors.Wrap(err, "prompting for card") } tok, err := token.New(&stripe.TokenParams{ Card: &card, }) if err != nil { return errors.Wrap(err, "requesting card token") } if err := a.AddCard(t.Token, tok.ID); err != nil { return errors.Wrap(err, "adding card") } println() util.Log("Updated") return nil }) } // prompt helper. func prompt(name string, s *string) error { prompt := &survey.Input{Message: name} return survey.AskOne(prompt, s, survey.Required) } // feedback prompt helper. func feedback() (string, error) { var s string prompt := &survey.Input{Message: "Have any feedback? (optional)"} if err := survey.AskOne(prompt, &s, nil); err != nil { return "", err } return s, nil } // currency returns formatted currency. func currency(n int) string { return fmt.Sprintf("$%0.2f", float64(n)/100) } // keys returns the keys of a string map. func keys(m map[string]string) (v []string) { for k := range m { v = append(v, k) } sort.Slice(v, func(i int, j int) bool { return v[i] > v[j] }) return } ================================================ FILE: internal/cli/upgrade/upgrade.go ================================================ package upgrade import ( "os" "os/exec" "path/filepath" "runtime" "strings" "time" "github.com/pkg/errors" "github.com/tj/go-update" "github.com/tj/go-update/stores/apex" "github.com/tj/go-update/stores/github" "github.com/tj/go/env" "github.com/tj/go/http/request" "github.com/tj/go/term" "github.com/tj/kingpin" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/progressreader" "github.com/apex/up/internal/stats" "github.com/apex/up/internal/userconfig" "github.com/apex/up/internal/util" ) var releasesAPI = env.GetDefault("APEX_RELEASES_API", "https://releases.apex.sh") func init() { cmd := root.Command("upgrade", "Install the latest or specified version of Up.") cmd.Example(`up upgrade`, "Upgrade to the latest version available.") cmd.Example(`up upgrade -t 0.4.4`, "Upgrade to the specified version.") target := cmd.Flag("target", "Target version for upgrade.").Short('t').String() cmd.Action(func(_ *kingpin.ParseContext) error { version := root.Cmd.GetVersion() start := time.Now() term.HideCursor() defer term.ShowCursor() var config userconfig.Config if err := config.Load(); err != nil { return errors.Wrap(err, "loading user config") } // open-source edition p := &update.Manager{ Command: "up", Store: &github.Store{ Owner: "apex", Repo: "up", Version: version, }, } // commercial edition if t := config.GetActiveTeam(); t != nil { // we pass 0.0.0 here beause the OSS // binary should always upgrade to Pro // regardless of versions matching. p.Store = &apex.Store{ URL: releasesAPI, Product: "up", Version: "0.0.0", Plan: "pro", AccessKey: t.Token, } } // fetch latest or specified release r, err := getLatestOrSpecified(p, *target) if err != nil { return errors.Wrap(err, "fetching latest release") } // no updates if r == nil { util.LogPad("No updates available, you're good :)") return nil } // find the tarball for this system a := r.FindTarball(runtime.GOOS, runtime.GOARCH) if a == nil { return errors.Errorf("failed to find a binary for %s %s", runtime.GOOS, runtime.GOARCH) } // download tarball to a tmp dir var tarball string if util.IsCI() { tarball, err = a.Download() if err != nil { return errors.Wrap(err, "downloading tarball") } } else { tarball, err = a.DownloadProxy(progressreader.New) if err != nil { return errors.Wrap(err, "downloading tarball") } } // determine path path, err := exec.LookPath(os.Args[0]) if err != nil { return errors.Wrap(err, "looking up executable path") } dst := filepath.Dir(path) // install it if err := p.InstallTo(tarball, dst); err != nil { return errors.Wrap(err, "installing") } term.ClearAll() if strings.Contains(a.URL, "up/pro") { util.LogPad("Updated %s to %s Pro", versionName(version), r.Version) } else { util.LogPad("Updated %s to %s OSS", versionName(version), r.Version) } stats.Track("Upgrade", map[string]interface{}{ "from": version, "to": r.Version, "duration": time.Since(start).Round(time.Millisecond), }) return nil }) } // getLatestOrSpecified returns the latest or specified release. func getLatestOrSpecified(s update.Store, version string) (*update.Release, error) { if version == "" { return getLatest(s) } return s.GetRelease(version) } // getLatest returns the latest release, error, or nil when there is none. func getLatest(s update.Store) (*update.Release, error) { releases, err := s.LatestReleases() if request.IsClient(err) { return nil, errors.Wrap(err, "You're not subscribed to Up Pro") } if err != nil { return nil, errors.Wrap(err, "fetching releases") } if len(releases) == 0 { return nil, nil } return releases[0], nil } // versionName returns the humanized version name. func versionName(s string) string { if strings.Contains(s, "-pro") { return strings.Replace(s, "-pro", "", 1) + " Pro" } return s + " OSS" } ================================================ FILE: internal/cli/url/url.go ================================================ package url import ( "fmt" "github.com/pkg/browser" "github.com/pkg/errors" "github.com/tj/go/clipboard" "github.com/tj/kingpin" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/stats" "github.com/apex/up/internal/util" "github.com/apex/up/internal/validate" ) func init() { cmd := root.Command("url", "Show, open, or copy a stage endpoint.") cmd.Example(`up url`, "Show the staging endpoint.") cmd.Example(`up url --open`, "Open the staging endpoint in the browser.") cmd.Example(`up url --copy`, "Copy the staging endpoint to the clipboard.") cmd.Example(`up url -s production`, "Show the production endpoint.") cmd.Example(`up url -o -s production`, "Open the production endpoint in the browser.") cmd.Example(`up url -c -s production`, "Copy the production endpoint to the clipboard.") stage := cmd.Flag("stage", "Target stage name.").Short('s').Default("staging").String() open := cmd.Flag("open", "Open endpoint in the browser.").Short('o').Bool() copy := cmd.Flag("copy", "Copy endpoint to the clipboard.").Short('c').Bool() cmd.Action(func(_ *kingpin.ParseContext) error { c, p, err := root.Init() if err != nil { return errors.Wrap(err, "initializing") } region := c.Regions[0] stats.Track("URL", map[string]interface{}{ "region": region, "stage": *stage, "open": *open, "copy": *copy, }) if err := validate.List(*stage, c.Stages.RemoteNames()); err != nil { return err } url, err := p.URL(region, *stage) if err != nil { return err } switch { case *open: browser.OpenURL(url) case *copy: clipboard.Write(url) util.LogPad("Copied to clipboard!") default: fmt.Println(url) } return nil }) } ================================================ FILE: internal/cli/version/version.go ================================================ package version import ( "fmt" "github.com/tj/kingpin" "github.com/apex/up/internal/cli/root" "github.com/apex/up/internal/stats" ) func init() { cmd := root.Command("version", "Show version.") cmd.Action(func(_ *kingpin.ParseContext) error { stats.Track("Show Version", nil) fmt.Println(root.Cmd.GetVersion()) return nil }) } ================================================ FILE: internal/colors/colors.go ================================================ // Package colors provides colors used by the CLI. package colors import ( color "github.com/aybabtme/rgbterm" ) // Func is a color function. type Func func(string) string // Gray string. func Gray(s string) string { return color.FgString(s, 150, 150, 150) } // Blue string. func Blue(s string) string { return color.FgString(s, 77, 173, 247) } // Cyan string. func Cyan(s string) string { return color.FgString(s, 34, 184, 207) } // Green string. func Green(s string) string { return color.FgString(s, 0, 200, 255) } // Red string. func Red(s string) string { return color.FgString(s, 194, 37, 92) } // Yellow string. func Yellow(s string) string { return color.FgString(s, 252, 196, 25) } // Purple string. func Purple(s string) string { return color.FgString(s, 96, 97, 190) } // Bool returns a color func based on the state. func Bool(ok bool) Func { if ok { return Purple } return Red } ================================================ FILE: internal/errorpage/errorpage.go ================================================ // Package errorpage provides error page loading utilities. package errorpage import ( "bytes" "html/template" "io/ioutil" "path/filepath" "sort" "strconv" "strings" "github.com/pkg/errors" ) // Page is a single .html file matching // one or more status codes. type Page struct { Name string Code int Range bool Template *template.Template } // Match returns true if the page matches code. func (p *Page) Match(code int) bool { switch { case p.Code == code: return true case p.Range && p.Code == code/100: return true case p.Name == "error" && code >= 400: return true case p.Name == "default" && code >= 400: return true default: return false } } // Specificity returns the specificity, where higher is more precise. func (p *Page) Specificity() int { switch { case p.Name == "default": return 4 case p.Name == "error": return 3 case p.Range: return 2 default: return 1 } } // Render the page. func (p *Page) Render(data interface{}) (string, error) { var buf bytes.Buffer if err := p.Template.Execute(&buf, data); err != nil { return "", err } return buf.String(), nil } // Pages is a group of .html files // matching one or more status codes. type Pages []Page // Match returns the matching page. func (p Pages) Match(code int) *Page { for _, page := range p { if page.Match(code) { return &page } } return nil } // Load pages in dir. func Load(dir string) (pages Pages, err error) { files, err := ioutil.ReadDir(dir) if err != nil { return nil, errors.Wrap(err, "reading dir") } for _, file := range files { if !isErrorPage(file.Name()) { continue } path := filepath.Join(dir, file.Name()) t, err := template.New(file.Name()).ParseFiles(path) if err != nil { return nil, errors.Wrap(err, "parsing template") } name := stripExt(file.Name()) code, _ := strconv.Atoi(name) if isRange(name) { code = int(name[0] - '0') } page := Page{ Name: name, Code: code, Range: isRange(name), Template: t, } pages = append(pages, page) } pages = append(pages, Page{ Name: "default", Template: defaultPage, }) Sort(pages) return } // Sort pages by specificity. func Sort(pages Pages) { sort.Slice(pages, func(i int, j int) bool { a := pages[i] b := pages[j] return a.Specificity() < b.Specificity() }) } // isErrorPage returns true if it looks like an error page. func isErrorPage(path string) bool { if filepath.Ext(path) != ".html" { return false } name := stripExt(path) if name == "error" { return true } if isRange(name) { return true } _, err := strconv.Atoi(name) return err == nil } // isRange returns true if the name matches xx.s func isRange(name string) bool { return strings.HasSuffix(name, "xx") } // stripExt returns path without extname. func stripExt(path string) string { return strings.Replace(path, filepath.Ext(path), "", 1) } ================================================ FILE: internal/errorpage/errorpage_test.go ================================================ package errorpage import ( "path/filepath" "testing" "github.com/tj/assert" ) // load pages from dir. func load(t testing.TB, dir string) Pages { dir = filepath.Join("testdata", dir) pages, err := Load(dir) assert.NoError(t, err, "load") return pages } func TestPages_precedence(t *testing.T) { pages := load(t, ".") t.Run("code 500 match exact", func(t *testing.T) { p := pages.Match(500) assert.NotNil(t, p, "no match") html, err := p.Render(nil) assert.NoError(t, err) assert.Equal(t, "500 page.\n", html) }) t.Run("code 404 match exact", func(t *testing.T) { p := pages.Match(404) assert.NotNil(t, p, "no match") html, err := p.Render(nil) assert.NoError(t, err) assert.Equal(t, "404 page.\n", html) }) t.Run("code 200 match exact", func(t *testing.T) { p := pages.Match(200) assert.NotNil(t, p, "no match") html, err := p.Render(nil) assert.NoError(t, err) assert.Equal(t, "200 page.\n", html) }) t.Run("code 403 match range", func(t *testing.T) { p := pages.Match(403) assert.NotNil(t, p, "no match") html, err := p.Render(nil) assert.NoError(t, err) assert.Equal(t, "4xx page.\n", html) }) t.Run("502 match global", func(t *testing.T) { p := pages.Match(502) assert.NotNil(t, p, "no match") data := struct { StatusText string StatusCode int }{"Bad Gateway", 502} html, err := p.Render(data) assert.NoError(t, err) assert.Equal(t, "Bad Gateway - 502.\n", html) }) } ================================================ FILE: internal/errorpage/template.go ================================================ package errorpage import "html/template" // defaultPage is the default error page. var defaultPage = template.Must(template.New("errorpage").Parse(` {{.StatusText}} – {{.StatusCode}}
{{.StatusText}} {{.StatusCode}} {{with .Variables.support_email}} Please try your request again or contact support. {{else}} Please try your request again or contact support. {{end}}
`)) ================================================ FILE: internal/errorpage/testdata/200.html ================================================ 200 page. ================================================ FILE: internal/errorpage/testdata/404.html ================================================ 404 page. ================================================ FILE: internal/errorpage/testdata/4xx.html ================================================ 4xx page. ================================================ FILE: internal/errorpage/testdata/500.html ================================================ 500 page. ================================================ FILE: internal/errorpage/testdata/error.html ================================================ {{.StatusText}} - {{.StatusCode}}. ================================================ FILE: internal/errorpage/testdata/other.html ================================================ ================================================ FILE: internal/errorpage/testdata/somedir/test.html ================================================ ================================================ FILE: internal/header/header.go ================================================ // Package header provides path-matched header injection rules. package header import ( "github.com/fanyang01/radix" ) // Fields map. type Fields map[string]string // Rules map of paths to fields. type Rules map[string]Fields // Matcher for header lookup. type Matcher struct { t *radix.PatternTrie } // Lookup returns fields for the given path. func (m *Matcher) Lookup(path string) Fields { v, ok := m.t.Lookup(path) if !ok { return nil } return v.(Fields) } // Compile the given rules to a trie. func Compile(rules Rules) (*Matcher, error) { t := radix.NewPatternTrie() m := &Matcher{t} for path, fields := range rules { t.Add(path, fields) } return m, nil } // Merge returns a new rules set giving precedence to `b`. func Merge(a, b Rules) Rules { r := make(Rules) for path, fields := range a { r[path] = fields } for path, fields := range b { if _, ok := r[path]; !ok { r[path] = make(Fields) } for name, val := range fields { r[path][name] = val } } return r } ================================================ FILE: internal/header/header_test.go ================================================ package header import ( "testing" "github.com/tj/assert" ) func TestMatcher_Lookup(t *testing.T) { rules := Rules{ "*": { "X-Type": "html", }, "*.css": { "X-Type": "css", }, "/docs/alerts": { "X-Type": "docs alerts", }, "/docs/*": { "X-Type": "docs", }, } m, err := Compile(rules) assert.NoError(t, err, "compile") assert.Equal(t, Fields{"X-Type": "html"}, m.Lookup("/something")) assert.Equal(t, Fields{"X-Type": "html"}, m.Lookup("/docs")) assert.Equal(t, Fields{"X-Type": "docs"}, m.Lookup("/docs/")) assert.Equal(t, Fields{"X-Type": "css"}, m.Lookup("/style.css")) assert.Equal(t, Fields{"X-Type": "css"}, m.Lookup("/public/css/style.css")) assert.Equal(t, Fields{"X-Type": "docs"}, m.Lookup("/docs/checks")) assert.Equal(t, Fields{"X-Type": "docs alerts"}, m.Lookup("/docs/alerts")) } func TestMerge(t *testing.T) { rules := Rules{ "*": { "X-Type": "html", "X-Foo": "bar", }, "/login": { "X-Something": "here", }, } rules = Merge(rules, Rules{ "*": { "X-Type": "pdf", }, "/admin": { "X-Something": "here", }, }) expected := Rules{ "*": { "X-Type": "pdf", "X-Foo": "bar", }, "/login": { "X-Something": "here", }, "/admin": { "X-Something": "here", }, } assert.Equal(t, expected, rules) } ================================================ FILE: internal/inject/inject.go ================================================ // Package inject provides script and style injection utilities. package inject import ( "encoding/json" "html" "io/ioutil" "strings" "github.com/apex/log" "github.com/apex/up/internal/validate" "github.com/pkg/errors" ) // TODO: template support // TODO: move config to "config" pkg // locations valid. var locations = []string{ "head", "body", } // types valid. var types = []string{ "literal", "comment", "style", "script", "inline style", "inline script", "google analytics", "segment", } // Rules is a set of rules mapped by location. type Rules map[string][]*Rule // Default rules. func (r Rules) Default() error { for pos, rules := range r { for i, rule := range rules { if err := rule.Default(); err != nil { return errors.Wrapf(err, "%s rule #%d", pos, i+1) } } } return nil } // Validate rules. func (r Rules) Validate() error { for pos, rules := range r { if err := validate.List(pos, locations); err != nil { return errors.Wrap(err, "invalid location") } for i, rule := range rules { if err := rule.Validate(); err != nil { return errors.Wrapf(err, "%s rule #%d", pos, i+1) } } } return nil } // Apply rules to html. func (r Rules) Apply(html string) string { for pos, rules := range r { log.Debugf("injecting %s rules", pos) for _, rule := range rules { log.Debugf(" inject %s %q", rule.Type, rule.Value) switch pos { case "head": html = Head(html, rule.Apply(html)) case "body": html = Body(html, rule.Apply(html)) } } } return html } // Rule is an injection rule. type Rule struct { // Type of injection, defaults to "literal" unless File is used, // or Value contains .js or .css extensions. Type string `json:"type"` // Value is the literal, inline string, or src/href of the injected tag. Value string `json:"value"` // File is used to load source from disk instead of providing Value. Note // that if Type is not explicitly provided, then it will default to // "inline script" or "inline style" for .js and .css files respectively. File string `json:"file"` } // Apply rule to html. func (r *Rule) Apply(html string) string { switch r.Type { case "literal": return r.Value case "script": return Script(r.Value) case "style": return Style(r.Value) case "inline script": return ScriptInline(r.Value) case "inline style": return StyleInline(r.Value) case "comment": return Comment(r.Value) case "segment": return Segment(r.Value) case "google analytics": return GoogleAnalytics(r.Value) default: return "" } } // Default applies defaults. func (r *Rule) Default() error { if r.Type == "" { r.Type = "literal" } if r.File != "" { if err := r.defaultFile(r.File); err != nil { return err } } return nil } // Validate returns an error if incorrect. func (r *Rule) Validate() error { if err := validate.List(r.Type, types); err != nil { return errors.Wrap(err, "invalid .type") } if strings.TrimSpace(r.Value) == "" { return errors.Errorf(`.value is required`) } return nil } // defaultFile defaults value" from the given path. func (r *Rule) defaultFile(path string) error { b, err := ioutil.ReadFile(path) if err != nil { return err } r.Value = string(b) return nil } // Head injects a string before the closing head tag. func Head(html, s string) string { return strings.Replace(html, "", " "+s+"\n ", 1) } // Body injects a string before the closing body tag. func Body(html, s string) string { return strings.Replace(html, "", " "+s+"\n ", 1) } // Script returns an script. func Script(src string) string { return `` } // ScriptInline returns an inline script. func ScriptInline(s string) string { return `` } // Style returns an style. func Style(href string) string { return `` } // StyleInline returns an inline style. func StyleInline(s string) string { return `` } // Comment returns an html comment. func Comment(s string) string { return "" } // Segment inline script with key. func Segment(key string) string { return ScriptInline(` !function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t Example

Hello World

` func ExampleStyle() { fmt.Printf("%s\n", inject.Style(`/sloth.css`)) // Output: // } func ExampleStyleInline() { fmt.Printf("%s\n", inject.StyleInline(`body { display: none }`)) // Output: // } func ExampleScript() { fmt.Printf("%s\n", inject.Script(`/sloth.js`)) // Output: // } func ExampleScriptInline() { fmt.Printf("%s\n", inject.ScriptInline(`const user = { "name": "Tobi" }`)) // Output: // } func ExampleComment() { fmt.Printf("%s\n", inject.Comment(`Hello World`)) // Output: // } func ExampleHead() { s := inject.Head(html, ``) fmt.Printf("%s\n", s) // Output: // // // // // Example // // // //

Hello World

// // } func ExampleBody() { s := inject.Body(html, inject.Comment("Version 1.0.3")) fmt.Printf("%s\n", s) // Output: // // // // // Example // // //

Hello World

// // // } func ExampleSegment() { fmt.Printf("%s\n", inject.Segment(`KEY HERE`)) // Output: // } func ExampleGoogleAnalytics() { fmt.Printf("%s\n", inject.GoogleAnalytics(`KEY HERE`)) // Output: // } func ExampleVar() { user := map[string]string{ "name": "Tobi", } fmt.Printf("%s\n", inject.Var("const", "user", user)) // Output: // } func TestRule_Default(t *testing.T) { r := inject.Rule{Value: ``} assert.NoError(t, r.Default(), "default") assert.NoError(t, r.Validate(), "validate") assert.Equal(t, "literal", r.Type) } func TestRule_Validate(t *testing.T) { r := inject.Rule{Type: "whatever"} assert.NoError(t, r.Default(), "default") assert.EqualError(t, r.Validate(), `invalid .type: "whatever" is invalid, must be one of: • literal • comment • style • script • inline style • inline script • google analytics • segment`) } func TestRules_Default(t *testing.T) { t.Run("type literal", func(t *testing.T) { rules := inject.Rules{ "head": []*inject.Rule{ { Value: ``, }, }, } assert.NoError(t, rules.Default(), "default") assert.NoError(t, rules.Validate(), "validate") }) } func TestRules_Validate(t *testing.T) { t.Run("missing value", func(t *testing.T) { rules := inject.Rules{ "head": []*inject.Rule{ { Type: "inline script", // Value: "var user = {}", }, }, } assert.NoError(t, rules.Default(), "default") assert.EqualError(t, rules.Validate(), `head rule #1: .value is required`) }) } ================================================ FILE: internal/logs/logs.go ================================================ // Package logs provides logging utilities. package logs import ( "os" "github.com/apex/log" ) // Fields returns the global log fields. func Fields() log.Fields { f := log.Fields{ "app": os.Getenv("AWS_LAMBDA_FUNCTION_NAME"), "region": os.Getenv("AWS_REGION"), "version": os.Getenv("AWS_LAMBDA_FUNCTION_VERSION"), "stage": os.Getenv("UP_STAGE"), } if s := os.Getenv("UP_COMMIT"); s != "" { f["commit"] = s } return f } // Plugin returns a log context for the given plugin name. func Plugin(name string) log.Interface { f := Fields() f["plugin"] = name return log.WithFields(f) } ================================================ FILE: internal/logs/parser/ast/ast.go ================================================ // Package ast provides the log query language abstract syntax tree. package ast import ( "fmt" "strconv" "strings" ) // Op type. type Op string // Op types. const ( LNOT Op = "not" NOT = "!" IN = "in" OR = "||" AND = "&&" NE = "!=" EQ = "=" GT = ">" LT = "<" GE = ">=" LE = "<=" ) // Node interface. type Node interface { String() string } // Root node. type Root struct { Node Node } // String implementation. func (n Root) String() string { return fmt.Sprintf(`{ %s }`, n.Node) } // Expr node. type Expr struct { Node Node } // String implementation. func (n Expr) String() string { return fmt.Sprintf(`(%s)`, n.Node) } // Literal node. type Literal string // String implementation. func (n Literal) String() string { return fmt.Sprintf(`%s`, string(n)) } // Tuple node. type Tuple []Node // String implementation. func (n Tuple) String() string { return fmt.Sprintf(`%#v`, n) } // Contains node. type Contains struct { Node Node } // String implementation. func (n Contains) String() string { switch v := n.Node.(type) { case String: return fmt.Sprintf(`"*%s*"`, string(v)) default: return fmt.Sprintf(`%s`, n.Node) } } // String node. type String string // String implementation. func (n String) String() string { return fmt.Sprintf(`$.message = %q`, string(n)) } // Property node. type Property string // String implementation. func (n Property) String() string { return fmt.Sprintf(`$.%s`, string(n)) } // Field node. type Field string // String implementation. func (n Field) String() string { return fmt.Sprintf(`$.fields.%s`, string(n)) } // Subscript node. type Subscript struct { Left Node Right Node } // String implementation. func (n Subscript) String() string { return fmt.Sprintf(`%s[%s]`, n.Left, n.Right) } // Member node. type Member struct { Left Node Right Node } // String implementation. func (n Member) String() string { return fmt.Sprintf(`%s.%s`, n.Left, n.Right) } // Number node. type Number struct { Value float64 Unit string } // String implementation. func (n Number) String() string { v := n.Value switch n.Unit { case "kb": v *= 1 << 10 case "mb": v *= 1 << 20 case "gb": v *= 1 << 30 case "s": v *= 1000 } return strconv.FormatFloat(v, 'f', -1, 64) } // Binary node. type Binary struct { Op Op Left Node Right Node } // String implementation. func (n Binary) String() string { switch n.Op { case IN: var s []string for _, v := range n.Right.(Tuple) { s = append(s, fmt.Sprintf(`%s %s %s`, n.Left, EQ, value(v))) } return fmt.Sprintf(`(%s)`, strings.Join(s, " || ")) case EQ, NE, GT, LT, GE, LE: return fmt.Sprintf(`%s %s %s`, n.Left, n.Op, value(n.Right)) default: return fmt.Sprintf(`%s %s %s`, n.Left, n.Op, n.Right) } } // Unary node. type Unary struct { Op Op Right Node } // String implementation. func (n Unary) String() string { switch n.Op { case LNOT: return fmt.Sprintf(`!(%s)`, n.Right) default: return fmt.Sprintf(`%s%s`, n.Op, n.Right) } } // value from node. func value(n Node) string { switch v := n.(type) { case String: return fmt.Sprintf("%q", string(v)) case Field: return fmt.Sprintf("%q", string(v)) default: return n.String() } } ================================================ FILE: internal/logs/parser/grammar.peg ================================================ package parser import "github.com/apex/up/internal/logs/parser/ast" type parser Peg { stack []ast.Node number string } Query <- _ Expr _ EOF PrimaryExpr <- Numbers Unit _ { p.AddNumber(text) } / Numbers _ { p.AddNumber("") } / Severity { p.AddLevel(text) } / Stage { p.AddStage(text) } / Id { p.AddField(text) } / String { p.AddString(text) } / UnquotedString { p.AddString(text) } / LPAR Expr RPAR { p.AddExpr() } TupleExpr <- LPAR Expr { p.AddTupleValue() } (COMMA Expr { p.AddTupleValue() } )* RPAR InExpr <- IN { p.AddTuple() } TupleExpr { p.AddBinary(ast.IN) } NotInExpr <- NOT IN { p.AddTuple() } TupleExpr { p.AddBinary(ast.IN); p.AddUnary(ast.LNOT) } PostfixExpr <- PrimaryExpr ( DOT Id { p.AddMember(text) } / LBRK Number _ RBRK { p.AddSubscript(text) } / InExpr / NotInExpr )* UnaryExpr <- PostfixExpr / BANG RelationalExpr { p.AddUnary(ast.NOT) } RelationalExpr <- UnaryExpr ( GE UnaryExpr { p.AddBinary(ast.GE) } / GT UnaryExpr { p.AddBinary(ast.GT) } / LE UnaryExpr { p.AddBinary(ast.LE) } / LT UnaryExpr { p.AddBinary(ast.LT) } )* EqualityExpr <- RelationalExpr ( EQEQ RelationalExpr { p.AddBinary(ast.EQ) } / NE RelationalExpr { p.AddBinary(ast.NE) } / EQ RelationalExpr { p.AddBinary(ast.EQ) } / CONTAINS RelationalExpr { p.AddBinaryContains() } )* LogicalAndExpr <- EqualityExpr ( AND EqualityExpr { p.AddBinary(ast.AND) } / ANDAND EqualityExpr { p.AddBinary(ast.AND) } / _ EqualityExpr { p.AddBinary(ast.AND) } )* LogicalOrExpr <- LogicalAndExpr ( OR LogicalAndExpr { p.AddBinary(ast.OR) } / OROR LogicalAndExpr { p.AddBinary(ast.OR) } )* LowNotExpr <- LogicalOrExpr / NOT LogicalOrExpr { p.AddUnary(ast.LNOT) } Expr <- LowNotExpr # # Strings # String <- ["] < StringChar* > ["] _ StringChar <- Escape / ![\"\n\\] . UnquotedString <- !Keyword < UnquotedStringStartChar UnquotedStringChar* > _ UnquotedStringStartChar <- [a-z] / [A-Z] / [/_] UnquotedStringChar <- [a-z] / [A-Z] / [0-9] / [/_] Escape <- SimpleEscape / OctalEscape / HexEscape / UniversalCharacter SimpleEscape <- '\\' ['\"?\\abfnrtv] OctalEscape <- '\\' [0-7][0-7]?[0-7]? HexEscape <- '\\x' HexDigit+ UniversalCharacter <- '\\u' HexQuad / '\\U' HexQuad HexQuad HexQuad <- HexDigit HexDigit HexDigit HexDigit HexDigit <- [a-f] / [A-F] / [0-9] # # Numeric # Numbers <- Number { p.SetNumber(text) } Number <- < Float > / < Integer > Integer <- [0-9]* Float <- Fraction Exponent? / [0-9]+ Exponent Fraction <- [0-9]* '.' [0-9]+ / [0-9]+ '.' Exponent <- [eE][+\-]? [0-9]+ # # Stages # Stage <- DEVELOPMENT / STAGING / PRODUCTION DEVELOPMENT <- < 'development' > !IdChar _ STAGING <- < 'staging' > !IdChar _ PRODUCTION <- < 'production' > !IdChar _ # # Units # Unit <- Bytes / Duration Duration <- S / MS S <- < 's' > !IdChar _ MS <- < 'ms' > !IdChar _ Bytes <- B / KB / MB / GB B <- < 'b' > !IdChar _ KB <- < 'kb' > !IdChar _ MB <- < 'mb' > !IdChar _ GB <- < 'gb' > !IdChar _ # # Identifiers # Id <- !Keyword < IdCharNoDigit IdChar* > _ IdChar <- [a-z] / [A-Z] / [0-9] / [_] IdCharNoDigit <- [a-z] / [A-Z] / [_] # # Severity # Severity <- DEBUG / INFO / WARN / ERROR / FATAL # # Keywords # IN <- 'in' !IdChar _ OR <- 'or' !IdChar _ AND <- 'and' !IdChar _ NOT <- 'not' !IdChar _ CONTAINS <- 'contains' !IdChar _ DEBUG <- < 'debug' > !IdChar _ INFO <- < 'info' > !IdChar _ WARN <- < 'warn' > !IdChar _ ERROR <- < 'error' > !IdChar _ FATAL <- < 'fatal' > !IdChar _ Keyword <- ( 'production' / 'staging' / 'development' / 'or' / 'and' / 'not' / 'contains' / 'debug' / 'info' / 'warn' / 'error' / 'fatal' / 'in' / 'gb' / 'mb' / 'kb' / 'b' / 'ms' / 's' ) !IdChar # # Punctuators # EQ <- '=' _ LBRK <- '[' _ RBRK <- ']' _ LPAR <- '(' _ RPAR <- ')' _ DOT <- '.' _ BANG <- '!' ![=] _ LT <- '<' ![=] _ GT <- '>' ![=] _ LE <- '<=' _ EQEQ <- '==' _ GE <- '>=' _ NE <- '!=' _ ANDAND <- '&&' _ OROR <- '||' _ COMMA <- ',' _ # # Whitespace # _ <- Whitespace* Whitespace <- ' ' / '\t' / EOL EOL <- '\r\n' / '\n' / '\r' EOF <- !. ================================================ FILE: internal/logs/parser/grammar.peg.go ================================================ package parser // Code generated by peg -inline -switch grammar.peg DO NOT EDIT import ( "fmt" "github.com/apex/up/internal/logs/parser/ast" "io" "os" "sort" "strconv" ) const endSymbol rune = 1114112 /* The rule types inferred from the grammar are below. */ type pegRule uint8 const ( ruleUnknown pegRule = iota ruleQuery rulePrimaryExpr ruleTupleExpr ruleInExpr ruleNotInExpr rulePostfixExpr ruleUnaryExpr ruleRelationalExpr ruleEqualityExpr ruleLogicalAndExpr ruleLogicalOrExpr ruleLowNotExpr ruleExpr ruleString ruleStringChar ruleUnquotedString ruleUnquotedStringStartChar ruleUnquotedStringChar ruleEscape ruleSimpleEscape ruleOctalEscape ruleHexEscape ruleUniversalCharacter ruleHexQuad ruleHexDigit ruleNumbers ruleNumber ruleInteger ruleFloat ruleFraction ruleExponent ruleStage ruleDEVELOPMENT ruleSTAGING rulePRODUCTION ruleUnit ruleDuration ruleS ruleMS ruleBytes ruleB ruleKB ruleMB ruleGB ruleId ruleIdChar ruleIdCharNoDigit ruleSeverity ruleIN ruleOR ruleAND ruleNOT ruleCONTAINS ruleDEBUG ruleINFO ruleWARN ruleERROR ruleFATAL ruleKeyword ruleEQ ruleLBRK ruleRBRK ruleLPAR ruleRPAR ruleDOT ruleBANG ruleLT ruleGT ruleLE ruleEQEQ ruleGE ruleNE ruleANDAND ruleOROR ruleCOMMA rule_ ruleWhitespace ruleEOL ruleEOF ruleAction0 ruleAction1 ruleAction2 ruleAction3 ruleAction4 ruleAction5 ruleAction6 ruleAction7 ruleAction8 ruleAction9 ruleAction10 ruleAction11 ruleAction12 ruleAction13 ruleAction14 ruleAction15 ruleAction16 ruleAction17 ruleAction18 ruleAction19 ruleAction20 ruleAction21 ruleAction22 ruleAction23 ruleAction24 ruleAction25 ruleAction26 ruleAction27 ruleAction28 ruleAction29 ruleAction30 rulePegText ruleAction31 ) var rul3s = [...]string{ "Unknown", "Query", "PrimaryExpr", "TupleExpr", "InExpr", "NotInExpr", "PostfixExpr", "UnaryExpr", "RelationalExpr", "EqualityExpr", "LogicalAndExpr", "LogicalOrExpr", "LowNotExpr", "Expr", "String", "StringChar", "UnquotedString", "UnquotedStringStartChar", "UnquotedStringChar", "Escape", "SimpleEscape", "OctalEscape", "HexEscape", "UniversalCharacter", "HexQuad", "HexDigit", "Numbers", "Number", "Integer", "Float", "Fraction", "Exponent", "Stage", "DEVELOPMENT", "STAGING", "PRODUCTION", "Unit", "Duration", "S", "MS", "Bytes", "B", "KB", "MB", "GB", "Id", "IdChar", "IdCharNoDigit", "Severity", "IN", "OR", "AND", "NOT", "CONTAINS", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "Keyword", "EQ", "LBRK", "RBRK", "LPAR", "RPAR", "DOT", "BANG", "LT", "GT", "LE", "EQEQ", "GE", "NE", "ANDAND", "OROR", "COMMA", "_", "Whitespace", "EOL", "EOF", "Action0", "Action1", "Action2", "Action3", "Action4", "Action5", "Action6", "Action7", "Action8", "Action9", "Action10", "Action11", "Action12", "Action13", "Action14", "Action15", "Action16", "Action17", "Action18", "Action19", "Action20", "Action21", "Action22", "Action23", "Action24", "Action25", "Action26", "Action27", "Action28", "Action29", "Action30", "PegText", "Action31", } type token32 struct { pegRule begin, end uint32 } func (t *token32) String() string { return fmt.Sprintf("\x1B[34m%v\x1B[m %v %v", rul3s[t.pegRule], t.begin, t.end) } type node32 struct { token32 up, next *node32 } func (node *node32) print(w io.Writer, pretty bool, buffer string) { var print func(node *node32, depth int) print = func(node *node32, depth int) { for node != nil { for c := 0; c < depth; c++ { fmt.Fprintf(w, " ") } rule := rul3s[node.pegRule] quote := strconv.Quote(string(([]rune(buffer)[node.begin:node.end]))) if !pretty { fmt.Fprintf(w, "%v %v\n", rule, quote) } else { fmt.Fprintf(w, "\x1B[34m%v\x1B[m %v\n", rule, quote) } if node.up != nil { print(node.up, depth+1) } node = node.next } } print(node, 0) } func (node *node32) Print(w io.Writer, buffer string) { node.print(w, false, buffer) } func (node *node32) PrettyPrint(w io.Writer, buffer string) { node.print(w, true, buffer) } type tokens32 struct { tree []token32 } func (t *tokens32) Trim(length uint32) { t.tree = t.tree[:length] } func (t *tokens32) Print() { for _, token := range t.tree { fmt.Println(token.String()) } } func (t *tokens32) AST() *node32 { type element struct { node *node32 down *element } tokens := t.Tokens() var stack *element for _, token := range tokens { if token.begin == token.end { continue } node := &node32{token32: token} for stack != nil && stack.node.begin >= token.begin && stack.node.end <= token.end { stack.node.next = node.up node.up = stack.node stack = stack.down } stack = &element{node: node, down: stack} } if stack != nil { return stack.node } return nil } func (t *tokens32) PrintSyntaxTree(buffer string) { t.AST().Print(os.Stdout, buffer) } func (t *tokens32) WriteSyntaxTree(w io.Writer, buffer string) { t.AST().Print(w, buffer) } func (t *tokens32) PrettyPrintSyntaxTree(buffer string) { t.AST().PrettyPrint(os.Stdout, buffer) } func (t *tokens32) Add(rule pegRule, begin, end, index uint32) { tree, i := t.tree, int(index) if i >= len(tree) { t.tree = append(tree, token32{pegRule: rule, begin: begin, end: end}) return } tree[i] = token32{pegRule: rule, begin: begin, end: end} } func (t *tokens32) Tokens() []token32 { return t.tree } type parser struct { stack []ast.Node number string Buffer string buffer []rune rules [113]func() bool parse func(rule ...int) error reset func() Pretty bool tokens32 } func (p *parser) Parse(rule ...int) error { return p.parse(rule...) } func (p *parser) Reset() { p.reset() } type textPosition struct { line, symbol int } type textPositionMap map[int]textPosition func translatePositions(buffer []rune, positions []int) textPositionMap { length, translations, j, line, symbol := len(positions), make(textPositionMap, len(positions)), 0, 1, 0 sort.Ints(positions) search: for i, c := range buffer { if c == '\n' { line, symbol = line+1, 0 } else { symbol++ } if i == positions[j] { translations[positions[j]] = textPosition{line, symbol} for j++; j < length; j++ { if i != positions[j] { continue search } } break search } } return translations } type parseError struct { p *parser max token32 } func (e *parseError) Error() string { tokens, err := []token32{e.max}, "\n" positions, p := make([]int, 2*len(tokens)), 0 for _, token := range tokens { positions[p], p = int(token.begin), p+1 positions[p], p = int(token.end), p+1 } translations := translatePositions(e.p.buffer, positions) format := "parse error near %v (line %v symbol %v - line %v symbol %v):\n%v\n" if e.p.Pretty { format = "parse error near \x1B[34m%v\x1B[m (line %v symbol %v - line %v symbol %v):\n%v\n" } for _, token := range tokens { begin, end := int(token.begin), int(token.end) err += fmt.Sprintf(format, rul3s[token.pegRule], translations[begin].line, translations[begin].symbol, translations[end].line, translations[end].symbol, strconv.Quote(string(e.p.buffer[begin:end]))) } return err } func (p *parser) PrintSyntaxTree() { if p.Pretty { p.tokens32.PrettyPrintSyntaxTree(p.Buffer) } else { p.tokens32.PrintSyntaxTree(p.Buffer) } } func (p *parser) WriteSyntaxTree(w io.Writer) { p.tokens32.WriteSyntaxTree(w, p.Buffer) } func (p *parser) Execute() { buffer, _buffer, text, begin, end := p.Buffer, p.buffer, "", 0, 0 for _, token := range p.Tokens() { switch token.pegRule { case rulePegText: begin, end = int(token.begin), int(token.end) text = string(_buffer[begin:end]) case ruleAction0: p.AddNumber(text) case ruleAction1: p.AddNumber("") case ruleAction2: p.AddLevel(text) case ruleAction3: p.AddStage(text) case ruleAction4: p.AddField(text) case ruleAction5: p.AddString(text) case ruleAction6: p.AddString(text) case ruleAction7: p.AddExpr() case ruleAction8: p.AddTupleValue() case ruleAction9: p.AddTupleValue() case ruleAction10: p.AddTuple() case ruleAction11: p.AddBinary(ast.IN) case ruleAction12: p.AddTuple() case ruleAction13: p.AddBinary(ast.IN) p.AddUnary(ast.LNOT) case ruleAction14: p.AddMember(text) case ruleAction15: p.AddSubscript(text) case ruleAction16: p.AddUnary(ast.NOT) case ruleAction17: p.AddBinary(ast.GE) case ruleAction18: p.AddBinary(ast.GT) case ruleAction19: p.AddBinary(ast.LE) case ruleAction20: p.AddBinary(ast.LT) case ruleAction21: p.AddBinary(ast.EQ) case ruleAction22: p.AddBinary(ast.NE) case ruleAction23: p.AddBinary(ast.EQ) case ruleAction24: p.AddBinaryContains() case ruleAction25: p.AddBinary(ast.AND) case ruleAction26: p.AddBinary(ast.AND) case ruleAction27: p.AddBinary(ast.AND) case ruleAction28: p.AddBinary(ast.OR) case ruleAction29: p.AddBinary(ast.OR) case ruleAction30: p.AddUnary(ast.LNOT) case ruleAction31: p.SetNumber(text) } } _, _, _, _, _ = buffer, _buffer, text, begin, end } func Pretty(pretty bool) func(*parser) error { return func(p *parser) error { p.Pretty = pretty return nil } } func Size(size int) func(*parser) error { return func(p *parser) error { p.tokens32 = tokens32{tree: make([]token32, 0, size)} return nil } } func (p *parser) Init(options ...func(*parser) error) error { var ( max token32 position, tokenIndex uint32 buffer []rune ) for _, option := range options { err := option(p) if err != nil { return err } } p.reset = func() { max = token32{} position, tokenIndex = 0, 0 p.buffer = []rune(p.Buffer) if len(p.buffer) == 0 || p.buffer[len(p.buffer)-1] != endSymbol { p.buffer = append(p.buffer, endSymbol) } buffer = p.buffer } p.reset() _rules := p.rules tree := p.tokens32 p.parse = func(rule ...int) error { r := 1 if len(rule) > 0 { r = rule[0] } matches := p.rules[r]() p.tokens32 = tree if matches { p.Trim(tokenIndex) return nil } return &parseError{p, max} } add := func(rule pegRule, begin uint32) { tree.Add(rule, begin, position, tokenIndex) tokenIndex++ if begin != position && position > max.end { max = token32{rule, begin, position} } } matchDot := func() bool { if buffer[position] != endSymbol { position++ return true } return false } /*matchChar := func(c byte) bool { if buffer[position] == c { position++ return true } return false }*/ /*matchRange := func(lower byte, upper byte) bool { if c := buffer[position]; c >= lower && c <= upper { position++ return true } return false }*/ _rules = [...]func() bool{ nil, /* 0 Query <- <(_ Expr _ EOF)> */ func() bool { position0, tokenIndex0 := position, tokenIndex { position1 := position if !_rules[rule_]() { goto l0 } if !_rules[ruleExpr]() { goto l0 } if !_rules[rule_]() { goto l0 } { position2 := position { position3, tokenIndex3 := position, tokenIndex if !matchDot() { goto l3 } goto l0 l3: position, tokenIndex = position3, tokenIndex3 } add(ruleEOF, position2) } add(ruleQuery, position1) } return true l0: position, tokenIndex = position0, tokenIndex0 return false }, /* 1 PrimaryExpr <- <((Numbers Unit _ Action0) / (Severity Action2) / (Stage Action3) / (Id Action4) / ((&('(') (LPAR Expr RPAR Action7)) | (&('"') (String Action5)) | (&('\t' | '\n' | '\r' | ' ' | '.' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9') (Numbers _ Action1)) | (&('/' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | '_' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z') (UnquotedString Action6))))> */ nil, /* 2 TupleExpr <- <(LPAR Expr Action8 (COMMA Expr Action9)* RPAR)> */ func() bool { position5, tokenIndex5 := position, tokenIndex { position6 := position if !_rules[ruleLPAR]() { goto l5 } if !_rules[ruleExpr]() { goto l5 } { add(ruleAction8, position) } l8: { position9, tokenIndex9 := position, tokenIndex { position10 := position if buffer[position] != rune(',') { goto l9 } position++ if !_rules[rule_]() { goto l9 } add(ruleCOMMA, position10) } if !_rules[ruleExpr]() { goto l9 } { add(ruleAction9, position) } goto l8 l9: position, tokenIndex = position9, tokenIndex9 } if !_rules[ruleRPAR]() { goto l5 } add(ruleTupleExpr, position6) } return true l5: position, tokenIndex = position5, tokenIndex5 return false }, /* 3 InExpr <- <(IN Action10 TupleExpr Action11)> */ nil, /* 4 NotInExpr <- <(NOT IN Action12 TupleExpr Action13)> */ nil, /* 5 PostfixExpr <- <(PrimaryExpr ((&('n') NotInExpr) | (&('i') InExpr) | (&('[') (LBRK Number _ RBRK Action15)) | (&('.') (DOT Id Action14)))*)> */ nil, /* 6 UnaryExpr <- <(PostfixExpr / (BANG RelationalExpr Action16))> */ func() bool { position15, tokenIndex15 := position, tokenIndex { position16 := position { position17, tokenIndex17 := position, tokenIndex { position19 := position { position20 := position { position21, tokenIndex21 := position, tokenIndex if !_rules[ruleNumbers]() { goto l22 } { position23 := position { position24, tokenIndex24 := position, tokenIndex { position26 := position { switch buffer[position] { case 'g': { position28 := position { position29 := position if buffer[position] != rune('g') { goto l25 } position++ if buffer[position] != rune('b') { goto l25 } position++ add(rulePegText, position29) } { position30, tokenIndex30 := position, tokenIndex if !_rules[ruleIdChar]() { goto l30 } goto l25 l30: position, tokenIndex = position30, tokenIndex30 } if !_rules[rule_]() { goto l25 } add(ruleGB, position28) } break case 'm': { position31 := position { position32 := position if buffer[position] != rune('m') { goto l25 } position++ if buffer[position] != rune('b') { goto l25 } position++ add(rulePegText, position32) } { position33, tokenIndex33 := position, tokenIndex if !_rules[ruleIdChar]() { goto l33 } goto l25 l33: position, tokenIndex = position33, tokenIndex33 } if !_rules[rule_]() { goto l25 } add(ruleMB, position31) } break case 'k': { position34 := position { position35 := position if buffer[position] != rune('k') { goto l25 } position++ if buffer[position] != rune('b') { goto l25 } position++ add(rulePegText, position35) } { position36, tokenIndex36 := position, tokenIndex if !_rules[ruleIdChar]() { goto l36 } goto l25 l36: position, tokenIndex = position36, tokenIndex36 } if !_rules[rule_]() { goto l25 } add(ruleKB, position34) } break default: { position37 := position { position38 := position if buffer[position] != rune('b') { goto l25 } position++ add(rulePegText, position38) } { position39, tokenIndex39 := position, tokenIndex if !_rules[ruleIdChar]() { goto l39 } goto l25 l39: position, tokenIndex = position39, tokenIndex39 } if !_rules[rule_]() { goto l25 } add(ruleB, position37) } break } } add(ruleBytes, position26) } goto l24 l25: position, tokenIndex = position24, tokenIndex24 { position40 := position { position41, tokenIndex41 := position, tokenIndex { position43 := position { position44 := position if buffer[position] != rune('s') { goto l42 } position++ add(rulePegText, position44) } { position45, tokenIndex45 := position, tokenIndex if !_rules[ruleIdChar]() { goto l45 } goto l42 l45: position, tokenIndex = position45, tokenIndex45 } if !_rules[rule_]() { goto l42 } add(ruleS, position43) } goto l41 l42: position, tokenIndex = position41, tokenIndex41 { position46 := position { position47 := position if buffer[position] != rune('m') { goto l22 } position++ if buffer[position] != rune('s') { goto l22 } position++ add(rulePegText, position47) } { position48, tokenIndex48 := position, tokenIndex if !_rules[ruleIdChar]() { goto l48 } goto l22 l48: position, tokenIndex = position48, tokenIndex48 } if !_rules[rule_]() { goto l22 } add(ruleMS, position46) } } l41: add(ruleDuration, position40) } } l24: add(ruleUnit, position23) } if !_rules[rule_]() { goto l22 } { add(ruleAction0, position) } goto l21 l22: position, tokenIndex = position21, tokenIndex21 { position51 := position { switch buffer[position] { case 'f': { position53 := position { position54 := position if buffer[position] != rune('f') { goto l50 } position++ if buffer[position] != rune('a') { goto l50 } position++ if buffer[position] != rune('t') { goto l50 } position++ if buffer[position] != rune('a') { goto l50 } position++ if buffer[position] != rune('l') { goto l50 } position++ add(rulePegText, position54) } { position55, tokenIndex55 := position, tokenIndex if !_rules[ruleIdChar]() { goto l55 } goto l50 l55: position, tokenIndex = position55, tokenIndex55 } if !_rules[rule_]() { goto l50 } add(ruleFATAL, position53) } break case 'e': { position56 := position { position57 := position if buffer[position] != rune('e') { goto l50 } position++ if buffer[position] != rune('r') { goto l50 } position++ if buffer[position] != rune('r') { goto l50 } position++ if buffer[position] != rune('o') { goto l50 } position++ if buffer[position] != rune('r') { goto l50 } position++ add(rulePegText, position57) } { position58, tokenIndex58 := position, tokenIndex if !_rules[ruleIdChar]() { goto l58 } goto l50 l58: position, tokenIndex = position58, tokenIndex58 } if !_rules[rule_]() { goto l50 } add(ruleERROR, position56) } break case 'w': { position59 := position { position60 := position if buffer[position] != rune('w') { goto l50 } position++ if buffer[position] != rune('a') { goto l50 } position++ if buffer[position] != rune('r') { goto l50 } position++ if buffer[position] != rune('n') { goto l50 } position++ add(rulePegText, position60) } { position61, tokenIndex61 := position, tokenIndex if !_rules[ruleIdChar]() { goto l61 } goto l50 l61: position, tokenIndex = position61, tokenIndex61 } if !_rules[rule_]() { goto l50 } add(ruleWARN, position59) } break case 'i': { position62 := position { position63 := position if buffer[position] != rune('i') { goto l50 } position++ if buffer[position] != rune('n') { goto l50 } position++ if buffer[position] != rune('f') { goto l50 } position++ if buffer[position] != rune('o') { goto l50 } position++ add(rulePegText, position63) } { position64, tokenIndex64 := position, tokenIndex if !_rules[ruleIdChar]() { goto l64 } goto l50 l64: position, tokenIndex = position64, tokenIndex64 } if !_rules[rule_]() { goto l50 } add(ruleINFO, position62) } break default: { position65 := position { position66 := position if buffer[position] != rune('d') { goto l50 } position++ if buffer[position] != rune('e') { goto l50 } position++ if buffer[position] != rune('b') { goto l50 } position++ if buffer[position] != rune('u') { goto l50 } position++ if buffer[position] != rune('g') { goto l50 } position++ add(rulePegText, position66) } { position67, tokenIndex67 := position, tokenIndex if !_rules[ruleIdChar]() { goto l67 } goto l50 l67: position, tokenIndex = position67, tokenIndex67 } if !_rules[rule_]() { goto l50 } add(ruleDEBUG, position65) } break } } add(ruleSeverity, position51) } { add(ruleAction2, position) } goto l21 l50: position, tokenIndex = position21, tokenIndex21 { position70 := position { switch buffer[position] { case 'p': { position72 := position { position73 := position if buffer[position] != rune('p') { goto l69 } position++ if buffer[position] != rune('r') { goto l69 } position++ if buffer[position] != rune('o') { goto l69 } position++ if buffer[position] != rune('d') { goto l69 } position++ if buffer[position] != rune('u') { goto l69 } position++ if buffer[position] != rune('c') { goto l69 } position++ if buffer[position] != rune('t') { goto l69 } position++ if buffer[position] != rune('i') { goto l69 } position++ if buffer[position] != rune('o') { goto l69 } position++ if buffer[position] != rune('n') { goto l69 } position++ add(rulePegText, position73) } { position74, tokenIndex74 := position, tokenIndex if !_rules[ruleIdChar]() { goto l74 } goto l69 l74: position, tokenIndex = position74, tokenIndex74 } if !_rules[rule_]() { goto l69 } add(rulePRODUCTION, position72) } break case 's': { position75 := position { position76 := position if buffer[position] != rune('s') { goto l69 } position++ if buffer[position] != rune('t') { goto l69 } position++ if buffer[position] != rune('a') { goto l69 } position++ if buffer[position] != rune('g') { goto l69 } position++ if buffer[position] != rune('i') { goto l69 } position++ if buffer[position] != rune('n') { goto l69 } position++ if buffer[position] != rune('g') { goto l69 } position++ add(rulePegText, position76) } { position77, tokenIndex77 := position, tokenIndex if !_rules[ruleIdChar]() { goto l77 } goto l69 l77: position, tokenIndex = position77, tokenIndex77 } if !_rules[rule_]() { goto l69 } add(ruleSTAGING, position75) } break default: { position78 := position { position79 := position if buffer[position] != rune('d') { goto l69 } position++ if buffer[position] != rune('e') { goto l69 } position++ if buffer[position] != rune('v') { goto l69 } position++ if buffer[position] != rune('e') { goto l69 } position++ if buffer[position] != rune('l') { goto l69 } position++ if buffer[position] != rune('o') { goto l69 } position++ if buffer[position] != rune('p') { goto l69 } position++ if buffer[position] != rune('m') { goto l69 } position++ if buffer[position] != rune('e') { goto l69 } position++ if buffer[position] != rune('n') { goto l69 } position++ if buffer[position] != rune('t') { goto l69 } position++ add(rulePegText, position79) } { position80, tokenIndex80 := position, tokenIndex if !_rules[ruleIdChar]() { goto l80 } goto l69 l80: position, tokenIndex = position80, tokenIndex80 } if !_rules[rule_]() { goto l69 } add(ruleDEVELOPMENT, position78) } break } } add(ruleStage, position70) } { add(ruleAction3, position) } goto l21 l69: position, tokenIndex = position21, tokenIndex21 if !_rules[ruleId]() { goto l82 } { add(ruleAction4, position) } goto l21 l82: position, tokenIndex = position21, tokenIndex21 { switch buffer[position] { case '(': if !_rules[ruleLPAR]() { goto l18 } if !_rules[ruleExpr]() { goto l18 } if !_rules[ruleRPAR]() { goto l18 } { add(ruleAction7, position) } break case '"': { position86 := position if buffer[position] != rune('"') { goto l18 } position++ { position87 := position l88: { position89, tokenIndex89 := position, tokenIndex { position90 := position { position91, tokenIndex91 := position, tokenIndex { position93 := position { position94, tokenIndex94 := position, tokenIndex { position96 := position if buffer[position] != rune('\\') { goto l95 } position++ { switch buffer[position] { case 'v': if buffer[position] != rune('v') { goto l95 } position++ break case 't': if buffer[position] != rune('t') { goto l95 } position++ break case 'r': if buffer[position] != rune('r') { goto l95 } position++ break case 'n': if buffer[position] != rune('n') { goto l95 } position++ break case 'f': if buffer[position] != rune('f') { goto l95 } position++ break case 'b': if buffer[position] != rune('b') { goto l95 } position++ break case 'a': if buffer[position] != rune('a') { goto l95 } position++ break case '\\': if buffer[position] != rune('\\') { goto l95 } position++ break case '?': if buffer[position] != rune('?') { goto l95 } position++ break case '"': if buffer[position] != rune('"') { goto l95 } position++ break default: if buffer[position] != rune('\'') { goto l95 } position++ break } } add(ruleSimpleEscape, position96) } goto l94 l95: position, tokenIndex = position94, tokenIndex94 { position99 := position if buffer[position] != rune('\\') { goto l98 } position++ if c := buffer[position]; c < rune('0') || c > rune('7') { goto l98 } position++ { position100, tokenIndex100 := position, tokenIndex if c := buffer[position]; c < rune('0') || c > rune('7') { goto l100 } position++ goto l101 l100: position, tokenIndex = position100, tokenIndex100 } l101: { position102, tokenIndex102 := position, tokenIndex if c := buffer[position]; c < rune('0') || c > rune('7') { goto l102 } position++ goto l103 l102: position, tokenIndex = position102, tokenIndex102 } l103: add(ruleOctalEscape, position99) } goto l94 l98: position, tokenIndex = position94, tokenIndex94 { position105 := position if buffer[position] != rune('\\') { goto l104 } position++ if buffer[position] != rune('x') { goto l104 } position++ if !_rules[ruleHexDigit]() { goto l104 } l106: { position107, tokenIndex107 := position, tokenIndex if !_rules[ruleHexDigit]() { goto l107 } goto l106 l107: position, tokenIndex = position107, tokenIndex107 } add(ruleHexEscape, position105) } goto l94 l104: position, tokenIndex = position94, tokenIndex94 { position108 := position { position109, tokenIndex109 := position, tokenIndex if buffer[position] != rune('\\') { goto l110 } position++ if buffer[position] != rune('u') { goto l110 } position++ if !_rules[ruleHexQuad]() { goto l110 } goto l109 l110: position, tokenIndex = position109, tokenIndex109 if buffer[position] != rune('\\') { goto l92 } position++ if buffer[position] != rune('U') { goto l92 } position++ if !_rules[ruleHexQuad]() { goto l92 } if !_rules[ruleHexQuad]() { goto l92 } } l109: add(ruleUniversalCharacter, position108) } } l94: add(ruleEscape, position93) } goto l91 l92: position, tokenIndex = position91, tokenIndex91 { position111, tokenIndex111 := position, tokenIndex { switch buffer[position] { case '\\': if buffer[position] != rune('\\') { goto l111 } position++ break case '\n': if buffer[position] != rune('\n') { goto l111 } position++ break default: if buffer[position] != rune('"') { goto l111 } position++ break } } goto l89 l111: position, tokenIndex = position111, tokenIndex111 } if !matchDot() { goto l89 } } l91: add(ruleStringChar, position90) } goto l88 l89: position, tokenIndex = position89, tokenIndex89 } add(rulePegText, position87) } if buffer[position] != rune('"') { goto l18 } position++ if !_rules[rule_]() { goto l18 } add(ruleString, position86) } { add(ruleAction5, position) } break case '\t', '\n', '\r', ' ', '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': if !_rules[ruleNumbers]() { goto l18 } if !_rules[rule_]() { goto l18 } { add(ruleAction1, position) } break default: { position115 := position { position116, tokenIndex116 := position, tokenIndex if !_rules[ruleKeyword]() { goto l116 } goto l18 l116: position, tokenIndex = position116, tokenIndex116 } { position117 := position { position118 := position { switch buffer[position] { case '/', '_': { position120, tokenIndex120 := position, tokenIndex if buffer[position] != rune('/') { goto l121 } position++ goto l120 l121: position, tokenIndex = position120, tokenIndex120 if buffer[position] != rune('_') { goto l18 } position++ } l120: break case 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z': if c := buffer[position]; c < rune('A') || c > rune('Z') { goto l18 } position++ break default: if c := buffer[position]; c < rune('a') || c > rune('z') { goto l18 } position++ break } } add(ruleUnquotedStringStartChar, position118) } l122: { position123, tokenIndex123 := position, tokenIndex { position124 := position { switch buffer[position] { case '/', '_': { position126, tokenIndex126 := position, tokenIndex if buffer[position] != rune('/') { goto l127 } position++ goto l126 l127: position, tokenIndex = position126, tokenIndex126 if buffer[position] != rune('_') { goto l123 } position++ } l126: break case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': if c := buffer[position]; c < rune('0') || c > rune('9') { goto l123 } position++ break case 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z': if c := buffer[position]; c < rune('A') || c > rune('Z') { goto l123 } position++ break default: if c := buffer[position]; c < rune('a') || c > rune('z') { goto l123 } position++ break } } add(ruleUnquotedStringChar, position124) } goto l122 l123: position, tokenIndex = position123, tokenIndex123 } add(rulePegText, position117) } if !_rules[rule_]() { goto l18 } add(ruleUnquotedString, position115) } { add(ruleAction6, position) } break } } } l21: add(rulePrimaryExpr, position20) } l129: { position130, tokenIndex130 := position, tokenIndex { switch buffer[position] { case 'n': { position132 := position if !_rules[ruleNOT]() { goto l130 } if !_rules[ruleIN]() { goto l130 } { add(ruleAction12, position) } if !_rules[ruleTupleExpr]() { goto l130 } { add(ruleAction13, position) } add(ruleNotInExpr, position132) } break case 'i': { position135 := position if !_rules[ruleIN]() { goto l130 } { add(ruleAction10, position) } if !_rules[ruleTupleExpr]() { goto l130 } { add(ruleAction11, position) } add(ruleInExpr, position135) } break case '[': { position138 := position if buffer[position] != rune('[') { goto l130 } position++ if !_rules[rule_]() { goto l130 } add(ruleLBRK, position138) } if !_rules[ruleNumber]() { goto l130 } if !_rules[rule_]() { goto l130 } { position139 := position if buffer[position] != rune(']') { goto l130 } position++ if !_rules[rule_]() { goto l130 } add(ruleRBRK, position139) } { add(ruleAction15, position) } break default: { position141 := position if buffer[position] != rune('.') { goto l130 } position++ if !_rules[rule_]() { goto l130 } add(ruleDOT, position141) } if !_rules[ruleId]() { goto l130 } { add(ruleAction14, position) } break } } goto l129 l130: position, tokenIndex = position130, tokenIndex130 } add(rulePostfixExpr, position19) } goto l17 l18: position, tokenIndex = position17, tokenIndex17 { position143 := position if buffer[position] != rune('!') { goto l15 } position++ { position144, tokenIndex144 := position, tokenIndex if buffer[position] != rune('=') { goto l144 } position++ goto l15 l144: position, tokenIndex = position144, tokenIndex144 } if !_rules[rule_]() { goto l15 } add(ruleBANG, position143) } if !_rules[ruleRelationalExpr]() { goto l15 } { add(ruleAction16, position) } } l17: add(ruleUnaryExpr, position16) } return true l15: position, tokenIndex = position15, tokenIndex15 return false }, /* 7 RelationalExpr <- <(UnaryExpr ((GE UnaryExpr Action17) / (GT UnaryExpr Action18) / (LE UnaryExpr Action19) / (LT UnaryExpr Action20))*)> */ func() bool { position146, tokenIndex146 := position, tokenIndex { position147 := position if !_rules[ruleUnaryExpr]() { goto l146 } l148: { position149, tokenIndex149 := position, tokenIndex { position150, tokenIndex150 := position, tokenIndex { position152 := position if buffer[position] != rune('>') { goto l151 } position++ if buffer[position] != rune('=') { goto l151 } position++ if !_rules[rule_]() { goto l151 } add(ruleGE, position152) } if !_rules[ruleUnaryExpr]() { goto l151 } { add(ruleAction17, position) } goto l150 l151: position, tokenIndex = position150, tokenIndex150 { position155 := position if buffer[position] != rune('>') { goto l154 } position++ { position156, tokenIndex156 := position, tokenIndex if buffer[position] != rune('=') { goto l156 } position++ goto l154 l156: position, tokenIndex = position156, tokenIndex156 } if !_rules[rule_]() { goto l154 } add(ruleGT, position155) } if !_rules[ruleUnaryExpr]() { goto l154 } { add(ruleAction18, position) } goto l150 l154: position, tokenIndex = position150, tokenIndex150 { position159 := position if buffer[position] != rune('<') { goto l158 } position++ if buffer[position] != rune('=') { goto l158 } position++ if !_rules[rule_]() { goto l158 } add(ruleLE, position159) } if !_rules[ruleUnaryExpr]() { goto l158 } { add(ruleAction19, position) } goto l150 l158: position, tokenIndex = position150, tokenIndex150 { position161 := position if buffer[position] != rune('<') { goto l149 } position++ { position162, tokenIndex162 := position, tokenIndex if buffer[position] != rune('=') { goto l162 } position++ goto l149 l162: position, tokenIndex = position162, tokenIndex162 } if !_rules[rule_]() { goto l149 } add(ruleLT, position161) } if !_rules[ruleUnaryExpr]() { goto l149 } { add(ruleAction20, position) } } l150: goto l148 l149: position, tokenIndex = position149, tokenIndex149 } add(ruleRelationalExpr, position147) } return true l146: position, tokenIndex = position146, tokenIndex146 return false }, /* 8 EqualityExpr <- <(RelationalExpr ((EQEQ RelationalExpr Action21) / ((&('c') (CONTAINS RelationalExpr Action24)) | (&('=') (EQ RelationalExpr Action23)) | (&('!') (NE RelationalExpr Action22))))*)> */ func() bool { position164, tokenIndex164 := position, tokenIndex { position165 := position if !_rules[ruleRelationalExpr]() { goto l164 } l166: { position167, tokenIndex167 := position, tokenIndex { position168, tokenIndex168 := position, tokenIndex { position170 := position if buffer[position] != rune('=') { goto l169 } position++ if buffer[position] != rune('=') { goto l169 } position++ if !_rules[rule_]() { goto l169 } add(ruleEQEQ, position170) } if !_rules[ruleRelationalExpr]() { goto l169 } { add(ruleAction21, position) } goto l168 l169: position, tokenIndex = position168, tokenIndex168 { switch buffer[position] { case 'c': { position173 := position if buffer[position] != rune('c') { goto l167 } position++ if buffer[position] != rune('o') { goto l167 } position++ if buffer[position] != rune('n') { goto l167 } position++ if buffer[position] != rune('t') { goto l167 } position++ if buffer[position] != rune('a') { goto l167 } position++ if buffer[position] != rune('i') { goto l167 } position++ if buffer[position] != rune('n') { goto l167 } position++ if buffer[position] != rune('s') { goto l167 } position++ { position174, tokenIndex174 := position, tokenIndex if !_rules[ruleIdChar]() { goto l174 } goto l167 l174: position, tokenIndex = position174, tokenIndex174 } if !_rules[rule_]() { goto l167 } add(ruleCONTAINS, position173) } if !_rules[ruleRelationalExpr]() { goto l167 } { add(ruleAction24, position) } break case '=': { position176 := position if buffer[position] != rune('=') { goto l167 } position++ if !_rules[rule_]() { goto l167 } add(ruleEQ, position176) } if !_rules[ruleRelationalExpr]() { goto l167 } { add(ruleAction23, position) } break default: { position178 := position if buffer[position] != rune('!') { goto l167 } position++ if buffer[position] != rune('=') { goto l167 } position++ if !_rules[rule_]() { goto l167 } add(ruleNE, position178) } if !_rules[ruleRelationalExpr]() { goto l167 } { add(ruleAction22, position) } break } } } l168: goto l166 l167: position, tokenIndex = position167, tokenIndex167 } add(ruleEqualityExpr, position165) } return true l164: position, tokenIndex = position164, tokenIndex164 return false }, /* 9 LogicalAndExpr <- <(EqualityExpr ((AND EqualityExpr Action25) / (ANDAND EqualityExpr Action26) / (_ EqualityExpr Action27))*)> */ func() bool { position180, tokenIndex180 := position, tokenIndex { position181 := position if !_rules[ruleEqualityExpr]() { goto l180 } l182: { position183, tokenIndex183 := position, tokenIndex { position184, tokenIndex184 := position, tokenIndex { position186 := position if buffer[position] != rune('a') { goto l185 } position++ if buffer[position] != rune('n') { goto l185 } position++ if buffer[position] != rune('d') { goto l185 } position++ { position187, tokenIndex187 := position, tokenIndex if !_rules[ruleIdChar]() { goto l187 } goto l185 l187: position, tokenIndex = position187, tokenIndex187 } if !_rules[rule_]() { goto l185 } add(ruleAND, position186) } if !_rules[ruleEqualityExpr]() { goto l185 } { add(ruleAction25, position) } goto l184 l185: position, tokenIndex = position184, tokenIndex184 { position190 := position if buffer[position] != rune('&') { goto l189 } position++ if buffer[position] != rune('&') { goto l189 } position++ if !_rules[rule_]() { goto l189 } add(ruleANDAND, position190) } if !_rules[ruleEqualityExpr]() { goto l189 } { add(ruleAction26, position) } goto l184 l189: position, tokenIndex = position184, tokenIndex184 if !_rules[rule_]() { goto l183 } if !_rules[ruleEqualityExpr]() { goto l183 } { add(ruleAction27, position) } } l184: goto l182 l183: position, tokenIndex = position183, tokenIndex183 } add(ruleLogicalAndExpr, position181) } return true l180: position, tokenIndex = position180, tokenIndex180 return false }, /* 10 LogicalOrExpr <- <(LogicalAndExpr ((OR LogicalAndExpr Action28) / (OROR LogicalAndExpr Action29))*)> */ func() bool { position193, tokenIndex193 := position, tokenIndex { position194 := position if !_rules[ruleLogicalAndExpr]() { goto l193 } l195: { position196, tokenIndex196 := position, tokenIndex { position197, tokenIndex197 := position, tokenIndex { position199 := position if buffer[position] != rune('o') { goto l198 } position++ if buffer[position] != rune('r') { goto l198 } position++ { position200, tokenIndex200 := position, tokenIndex if !_rules[ruleIdChar]() { goto l200 } goto l198 l200: position, tokenIndex = position200, tokenIndex200 } if !_rules[rule_]() { goto l198 } add(ruleOR, position199) } if !_rules[ruleLogicalAndExpr]() { goto l198 } { add(ruleAction28, position) } goto l197 l198: position, tokenIndex = position197, tokenIndex197 { position202 := position if buffer[position] != rune('|') { goto l196 } position++ if buffer[position] != rune('|') { goto l196 } position++ if !_rules[rule_]() { goto l196 } add(ruleOROR, position202) } if !_rules[ruleLogicalAndExpr]() { goto l196 } { add(ruleAction29, position) } } l197: goto l195 l196: position, tokenIndex = position196, tokenIndex196 } add(ruleLogicalOrExpr, position194) } return true l193: position, tokenIndex = position193, tokenIndex193 return false }, /* 11 LowNotExpr <- <(LogicalOrExpr / (NOT LogicalOrExpr Action30))> */ nil, /* 12 Expr <- */ func() bool { position205, tokenIndex205 := position, tokenIndex { position206 := position { position207 := position { position208, tokenIndex208 := position, tokenIndex if !_rules[ruleLogicalOrExpr]() { goto l209 } goto l208 l209: position, tokenIndex = position208, tokenIndex208 if !_rules[ruleNOT]() { goto l205 } if !_rules[ruleLogicalOrExpr]() { goto l205 } { add(ruleAction30, position) } } l208: add(ruleLowNotExpr, position207) } add(ruleExpr, position206) } return true l205: position, tokenIndex = position205, tokenIndex205 return false }, /* 13 String <- <('"' '"' _)> */ nil, /* 14 StringChar <- <(Escape / (!((&('\\') '\\') | (&('\n') '\n') | (&('"') '"')) .))> */ nil, /* 15 UnquotedString <- <(!Keyword <(UnquotedStringStartChar UnquotedStringChar*)> _)> */ nil, /* 16 UnquotedStringStartChar <- <((&('/' | '_') ('/' / '_')) | (&('A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z') [A-Z]) | (&('a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z') [a-z]))> */ nil, /* 17 UnquotedStringChar <- <((&('/' | '_') ('/' / '_')) | (&('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9') [0-9]) | (&('A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z') [A-Z]) | (&('a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z') [a-z]))> */ nil, /* 18 Escape <- <(SimpleEscape / OctalEscape / HexEscape / UniversalCharacter)> */ nil, /* 19 SimpleEscape <- <('\\' ((&('v') 'v') | (&('t') 't') | (&('r') 'r') | (&('n') 'n') | (&('f') 'f') | (&('b') 'b') | (&('a') 'a') | (&('\\') '\\') | (&('?') '?') | (&('"') '"') | (&('\'') '\'')))> */ nil, /* 20 OctalEscape <- <('\\' [0-7] [0-7]? [0-7]?)> */ nil, /* 21 HexEscape <- <('\\' 'x' HexDigit+)> */ nil, /* 22 UniversalCharacter <- <(('\\' 'u' HexQuad) / ('\\' 'U' HexQuad HexQuad))> */ nil, /* 23 HexQuad <- <(HexDigit HexDigit HexDigit HexDigit)> */ func() bool { position221, tokenIndex221 := position, tokenIndex { position222 := position if !_rules[ruleHexDigit]() { goto l221 } if !_rules[ruleHexDigit]() { goto l221 } if !_rules[ruleHexDigit]() { goto l221 } if !_rules[ruleHexDigit]() { goto l221 } add(ruleHexQuad, position222) } return true l221: position, tokenIndex = position221, tokenIndex221 return false }, /* 24 HexDigit <- <((&('A' | 'B' | 'C' | 'D' | 'E' | 'F') [A-F]) | (&('a' | 'b' | 'c' | 'd' | 'e' | 'f') [a-f]) | (&('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9') [0-9]))> */ func() bool { position223, tokenIndex223 := position, tokenIndex { position224 := position { switch buffer[position] { case 'A', 'B', 'C', 'D', 'E', 'F': if c := buffer[position]; c < rune('A') || c > rune('F') { goto l223 } position++ break case 'a', 'b', 'c', 'd', 'e', 'f': if c := buffer[position]; c < rune('a') || c > rune('f') { goto l223 } position++ break default: if c := buffer[position]; c < rune('0') || c > rune('9') { goto l223 } position++ break } } add(ruleHexDigit, position224) } return true l223: position, tokenIndex = position223, tokenIndex223 return false }, /* 25 Numbers <- <(Number Action31)> */ func() bool { position226, tokenIndex226 := position, tokenIndex { position227 := position if !_rules[ruleNumber]() { goto l226 } { add(ruleAction31, position) } add(ruleNumbers, position227) } return true l226: position, tokenIndex = position226, tokenIndex226 return false }, /* 26 Number <- <( / )> */ func() bool { { position230 := position { position231, tokenIndex231 := position, tokenIndex { position233 := position { position234 := position { position235, tokenIndex235 := position, tokenIndex { position237 := position { position238, tokenIndex238 := position, tokenIndex l240: { position241, tokenIndex241 := position, tokenIndex if c := buffer[position]; c < rune('0') || c > rune('9') { goto l241 } position++ goto l240 l241: position, tokenIndex = position241, tokenIndex241 } if buffer[position] != rune('.') { goto l239 } position++ if c := buffer[position]; c < rune('0') || c > rune('9') { goto l239 } position++ l242: { position243, tokenIndex243 := position, tokenIndex if c := buffer[position]; c < rune('0') || c > rune('9') { goto l243 } position++ goto l242 l243: position, tokenIndex = position243, tokenIndex243 } goto l238 l239: position, tokenIndex = position238, tokenIndex238 if c := buffer[position]; c < rune('0') || c > rune('9') { goto l236 } position++ l244: { position245, tokenIndex245 := position, tokenIndex if c := buffer[position]; c < rune('0') || c > rune('9') { goto l245 } position++ goto l244 l245: position, tokenIndex = position245, tokenIndex245 } if buffer[position] != rune('.') { goto l236 } position++ } l238: add(ruleFraction, position237) } { position246, tokenIndex246 := position, tokenIndex if !_rules[ruleExponent]() { goto l246 } goto l247 l246: position, tokenIndex = position246, tokenIndex246 } l247: goto l235 l236: position, tokenIndex = position235, tokenIndex235 if c := buffer[position]; c < rune('0') || c > rune('9') { goto l232 } position++ l248: { position249, tokenIndex249 := position, tokenIndex if c := buffer[position]; c < rune('0') || c > rune('9') { goto l249 } position++ goto l248 l249: position, tokenIndex = position249, tokenIndex249 } if !_rules[ruleExponent]() { goto l232 } } l235: add(ruleFloat, position234) } add(rulePegText, position233) } goto l231 l232: position, tokenIndex = position231, tokenIndex231 { position250 := position { position251 := position l252: { position253, tokenIndex253 := position, tokenIndex if c := buffer[position]; c < rune('0') || c > rune('9') { goto l253 } position++ goto l252 l253: position, tokenIndex = position253, tokenIndex253 } add(ruleInteger, position251) } add(rulePegText, position250) } } l231: add(ruleNumber, position230) } return true }, /* 27 Integer <- <[0-9]*> */ nil, /* 28 Float <- <((Fraction Exponent?) / ([0-9]+ Exponent))> */ nil, /* 29 Fraction <- <(([0-9]* '.' [0-9]+) / ([0-9]+ '.'))> */ nil, /* 30 Exponent <- <(('e' / 'E') ('+' / '-')? [0-9]+)> */ func() bool { position257, tokenIndex257 := position, tokenIndex { position258 := position { position259, tokenIndex259 := position, tokenIndex if buffer[position] != rune('e') { goto l260 } position++ goto l259 l260: position, tokenIndex = position259, tokenIndex259 if buffer[position] != rune('E') { goto l257 } position++ } l259: { position261, tokenIndex261 := position, tokenIndex { position263, tokenIndex263 := position, tokenIndex if buffer[position] != rune('+') { goto l264 } position++ goto l263 l264: position, tokenIndex = position263, tokenIndex263 if buffer[position] != rune('-') { goto l261 } position++ } l263: goto l262 l261: position, tokenIndex = position261, tokenIndex261 } l262: if c := buffer[position]; c < rune('0') || c > rune('9') { goto l257 } position++ l265: { position266, tokenIndex266 := position, tokenIndex if c := buffer[position]; c < rune('0') || c > rune('9') { goto l266 } position++ goto l265 l266: position, tokenIndex = position266, tokenIndex266 } add(ruleExponent, position258) } return true l257: position, tokenIndex = position257, tokenIndex257 return false }, /* 31 Stage <- <((&('p') PRODUCTION) | (&('s') STAGING) | (&('d') DEVELOPMENT))> */ nil, /* 32 DEVELOPMENT <- <(<('d' 'e' 'v' 'e' 'l' 'o' 'p' 'm' 'e' 'n' 't')> !IdChar _)> */ nil, /* 33 STAGING <- <(<('s' 't' 'a' 'g' 'i' 'n' 'g')> !IdChar _)> */ nil, /* 34 PRODUCTION <- <(<('p' 'r' 'o' 'd' 'u' 'c' 't' 'i' 'o' 'n')> !IdChar _)> */ nil, /* 35 Unit <- <(Bytes / Duration)> */ nil, /* 36 Duration <- <(S / MS)> */ nil, /* 37 S <- <(<'s'> !IdChar _)> */ nil, /* 38 MS <- <(<('m' 's')> !IdChar _)> */ nil, /* 39 Bytes <- <((&('g') GB) | (&('m') MB) | (&('k') KB) | (&('b') B))> */ nil, /* 40 B <- <(<'b'> !IdChar _)> */ nil, /* 41 KB <- <(<('k' 'b')> !IdChar _)> */ nil, /* 42 MB <- <(<('m' 'b')> !IdChar _)> */ nil, /* 43 GB <- <(<('g' 'b')> !IdChar _)> */ nil, /* 44 Id <- <(!Keyword <(IdCharNoDigit IdChar*)> _)> */ func() bool { position280, tokenIndex280 := position, tokenIndex { position281 := position { position282, tokenIndex282 := position, tokenIndex if !_rules[ruleKeyword]() { goto l282 } goto l280 l282: position, tokenIndex = position282, tokenIndex282 } { position283 := position { position284 := position { switch buffer[position] { case '_': if buffer[position] != rune('_') { goto l280 } position++ break case 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z': if c := buffer[position]; c < rune('A') || c > rune('Z') { goto l280 } position++ break default: if c := buffer[position]; c < rune('a') || c > rune('z') { goto l280 } position++ break } } add(ruleIdCharNoDigit, position284) } l286: { position287, tokenIndex287 := position, tokenIndex if !_rules[ruleIdChar]() { goto l287 } goto l286 l287: position, tokenIndex = position287, tokenIndex287 } add(rulePegText, position283) } if !_rules[rule_]() { goto l280 } add(ruleId, position281) } return true l280: position, tokenIndex = position280, tokenIndex280 return false }, /* 45 IdChar <- <((&('_') '_') | (&('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9') [0-9]) | (&('A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z') [A-Z]) | (&('a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z') [a-z]))> */ func() bool { position288, tokenIndex288 := position, tokenIndex { position289 := position { switch buffer[position] { case '_': if buffer[position] != rune('_') { goto l288 } position++ break case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': if c := buffer[position]; c < rune('0') || c > rune('9') { goto l288 } position++ break case 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z': if c := buffer[position]; c < rune('A') || c > rune('Z') { goto l288 } position++ break default: if c := buffer[position]; c < rune('a') || c > rune('z') { goto l288 } position++ break } } add(ruleIdChar, position289) } return true l288: position, tokenIndex = position288, tokenIndex288 return false }, /* 46 IdCharNoDigit <- <((&('_') '_') | (&('A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z') [A-Z]) | (&('a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z') [a-z]))> */ nil, /* 47 Severity <- <((&('f') FATAL) | (&('e') ERROR) | (&('w') WARN) | (&('i') INFO) | (&('d') DEBUG))> */ nil, /* 48 IN <- <('i' 'n' !IdChar _)> */ func() bool { position293, tokenIndex293 := position, tokenIndex { position294 := position if buffer[position] != rune('i') { goto l293 } position++ if buffer[position] != rune('n') { goto l293 } position++ { position295, tokenIndex295 := position, tokenIndex if !_rules[ruleIdChar]() { goto l295 } goto l293 l295: position, tokenIndex = position295, tokenIndex295 } if !_rules[rule_]() { goto l293 } add(ruleIN, position294) } return true l293: position, tokenIndex = position293, tokenIndex293 return false }, /* 49 OR <- <('o' 'r' !IdChar _)> */ nil, /* 50 AND <- <('a' 'n' 'd' !IdChar _)> */ nil, /* 51 NOT <- <('n' 'o' 't' !IdChar _)> */ func() bool { position298, tokenIndex298 := position, tokenIndex { position299 := position if buffer[position] != rune('n') { goto l298 } position++ if buffer[position] != rune('o') { goto l298 } position++ if buffer[position] != rune('t') { goto l298 } position++ { position300, tokenIndex300 := position, tokenIndex if !_rules[ruleIdChar]() { goto l300 } goto l298 l300: position, tokenIndex = position300, tokenIndex300 } if !_rules[rule_]() { goto l298 } add(ruleNOT, position299) } return true l298: position, tokenIndex = position298, tokenIndex298 return false }, /* 52 CONTAINS <- <('c' 'o' 'n' 't' 'a' 'i' 'n' 's' !IdChar _)> */ nil, /* 53 DEBUG <- <(<('d' 'e' 'b' 'u' 'g')> !IdChar _)> */ nil, /* 54 INFO <- <(<('i' 'n' 'f' 'o')> !IdChar _)> */ nil, /* 55 WARN <- <(<('w' 'a' 'r' 'n')> !IdChar _)> */ nil, /* 56 ERROR <- <(<('e' 'r' 'r' 'o' 'r')> !IdChar _)> */ nil, /* 57 FATAL <- <(<('f' 'a' 't' 'a' 'l')> !IdChar _)> */ nil, /* 58 Keyword <- <((('s' 't' 'a' 'g' 'i' 'n' 'g') / ('d' 'e' 'v' 'e' 'l' 'o' 'p' 'm' 'e' 'n' 't') / ('i' 'n' 'f' 'o') / ('m' 'b') / ((&('s') 's') | (&('m') ('m' 's')) | (&('b') 'b') | (&('k') ('k' 'b')) | (&('g') ('g' 'b')) | (&('i') ('i' 'n')) | (&('f') ('f' 'a' 't' 'a' 'l')) | (&('e') ('e' 'r' 'r' 'o' 'r')) | (&('w') ('w' 'a' 'r' 'n')) | (&('d') ('d' 'e' 'b' 'u' 'g')) | (&('c') ('c' 'o' 'n' 't' 'a' 'i' 'n' 's')) | (&('n') ('n' 'o' 't')) | (&('a') ('a' 'n' 'd')) | (&('o') ('o' 'r')) | (&('p') ('p' 'r' 'o' 'd' 'u' 'c' 't' 'i' 'o' 'n')))) !IdChar)> */ func() bool { position307, tokenIndex307 := position, tokenIndex { position308 := position { position309, tokenIndex309 := position, tokenIndex if buffer[position] != rune('s') { goto l310 } position++ if buffer[position] != rune('t') { goto l310 } position++ if buffer[position] != rune('a') { goto l310 } position++ if buffer[position] != rune('g') { goto l310 } position++ if buffer[position] != rune('i') { goto l310 } position++ if buffer[position] != rune('n') { goto l310 } position++ if buffer[position] != rune('g') { goto l310 } position++ goto l309 l310: position, tokenIndex = position309, tokenIndex309 if buffer[position] != rune('d') { goto l311 } position++ if buffer[position] != rune('e') { goto l311 } position++ if buffer[position] != rune('v') { goto l311 } position++ if buffer[position] != rune('e') { goto l311 } position++ if buffer[position] != rune('l') { goto l311 } position++ if buffer[position] != rune('o') { goto l311 } position++ if buffer[position] != rune('p') { goto l311 } position++ if buffer[position] != rune('m') { goto l311 } position++ if buffer[position] != rune('e') { goto l311 } position++ if buffer[position] != rune('n') { goto l311 } position++ if buffer[position] != rune('t') { goto l311 } position++ goto l309 l311: position, tokenIndex = position309, tokenIndex309 if buffer[position] != rune('i') { goto l312 } position++ if buffer[position] != rune('n') { goto l312 } position++ if buffer[position] != rune('f') { goto l312 } position++ if buffer[position] != rune('o') { goto l312 } position++ goto l309 l312: position, tokenIndex = position309, tokenIndex309 if buffer[position] != rune('m') { goto l313 } position++ if buffer[position] != rune('b') { goto l313 } position++ goto l309 l313: position, tokenIndex = position309, tokenIndex309 { switch buffer[position] { case 's': if buffer[position] != rune('s') { goto l307 } position++ break case 'm': if buffer[position] != rune('m') { goto l307 } position++ if buffer[position] != rune('s') { goto l307 } position++ break case 'b': if buffer[position] != rune('b') { goto l307 } position++ break case 'k': if buffer[position] != rune('k') { goto l307 } position++ if buffer[position] != rune('b') { goto l307 } position++ break case 'g': if buffer[position] != rune('g') { goto l307 } position++ if buffer[position] != rune('b') { goto l307 } position++ break case 'i': if buffer[position] != rune('i') { goto l307 } position++ if buffer[position] != rune('n') { goto l307 } position++ break case 'f': if buffer[position] != rune('f') { goto l307 } position++ if buffer[position] != rune('a') { goto l307 } position++ if buffer[position] != rune('t') { goto l307 } position++ if buffer[position] != rune('a') { goto l307 } position++ if buffer[position] != rune('l') { goto l307 } position++ break case 'e': if buffer[position] != rune('e') { goto l307 } position++ if buffer[position] != rune('r') { goto l307 } position++ if buffer[position] != rune('r') { goto l307 } position++ if buffer[position] != rune('o') { goto l307 } position++ if buffer[position] != rune('r') { goto l307 } position++ break case 'w': if buffer[position] != rune('w') { goto l307 } position++ if buffer[position] != rune('a') { goto l307 } position++ if buffer[position] != rune('r') { goto l307 } position++ if buffer[position] != rune('n') { goto l307 } position++ break case 'd': if buffer[position] != rune('d') { goto l307 } position++ if buffer[position] != rune('e') { goto l307 } position++ if buffer[position] != rune('b') { goto l307 } position++ if buffer[position] != rune('u') { goto l307 } position++ if buffer[position] != rune('g') { goto l307 } position++ break case 'c': if buffer[position] != rune('c') { goto l307 } position++ if buffer[position] != rune('o') { goto l307 } position++ if buffer[position] != rune('n') { goto l307 } position++ if buffer[position] != rune('t') { goto l307 } position++ if buffer[position] != rune('a') { goto l307 } position++ if buffer[position] != rune('i') { goto l307 } position++ if buffer[position] != rune('n') { goto l307 } position++ if buffer[position] != rune('s') { goto l307 } position++ break case 'n': if buffer[position] != rune('n') { goto l307 } position++ if buffer[position] != rune('o') { goto l307 } position++ if buffer[position] != rune('t') { goto l307 } position++ break case 'a': if buffer[position] != rune('a') { goto l307 } position++ if buffer[position] != rune('n') { goto l307 } position++ if buffer[position] != rune('d') { goto l307 } position++ break case 'o': if buffer[position] != rune('o') { goto l307 } position++ if buffer[position] != rune('r') { goto l307 } position++ break default: if buffer[position] != rune('p') { goto l307 } position++ if buffer[position] != rune('r') { goto l307 } position++ if buffer[position] != rune('o') { goto l307 } position++ if buffer[position] != rune('d') { goto l307 } position++ if buffer[position] != rune('u') { goto l307 } position++ if buffer[position] != rune('c') { goto l307 } position++ if buffer[position] != rune('t') { goto l307 } position++ if buffer[position] != rune('i') { goto l307 } position++ if buffer[position] != rune('o') { goto l307 } position++ if buffer[position] != rune('n') { goto l307 } position++ break } } } l309: { position315, tokenIndex315 := position, tokenIndex if !_rules[ruleIdChar]() { goto l315 } goto l307 l315: position, tokenIndex = position315, tokenIndex315 } add(ruleKeyword, position308) } return true l307: position, tokenIndex = position307, tokenIndex307 return false }, /* 59 EQ <- <('=' _)> */ nil, /* 60 LBRK <- <('[' _)> */ nil, /* 61 RBRK <- <(']' _)> */ nil, /* 62 LPAR <- <('(' _)> */ func() bool { position319, tokenIndex319 := position, tokenIndex { position320 := position if buffer[position] != rune('(') { goto l319 } position++ if !_rules[rule_]() { goto l319 } add(ruleLPAR, position320) } return true l319: position, tokenIndex = position319, tokenIndex319 return false }, /* 63 RPAR <- <(')' _)> */ func() bool { position321, tokenIndex321 := position, tokenIndex { position322 := position if buffer[position] != rune(')') { goto l321 } position++ if !_rules[rule_]() { goto l321 } add(ruleRPAR, position322) } return true l321: position, tokenIndex = position321, tokenIndex321 return false }, /* 64 DOT <- <('.' _)> */ nil, /* 65 BANG <- <('!' !'=' _)> */ nil, /* 66 LT <- <('<' !'=' _)> */ nil, /* 67 GT <- <('>' !'=' _)> */ nil, /* 68 LE <- <('<' '=' _)> */ nil, /* 69 EQEQ <- <('=' '=' _)> */ nil, /* 70 GE <- <('>' '=' _)> */ nil, /* 71 NE <- <('!' '=' _)> */ nil, /* 72 ANDAND <- <('&' '&' _)> */ nil, /* 73 OROR <- <('|' '|' _)> */ nil, /* 74 COMMA <- <(',' _)> */ nil, /* 75 _ <- */ func() bool { { position335 := position l336: { position337, tokenIndex337 := position, tokenIndex { position338 := position { switch buffer[position] { case '\t': if buffer[position] != rune('\t') { goto l337 } position++ break case ' ': if buffer[position] != rune(' ') { goto l337 } position++ break default: { position340 := position { position341, tokenIndex341 := position, tokenIndex if buffer[position] != rune('\r') { goto l342 } position++ if buffer[position] != rune('\n') { goto l342 } position++ goto l341 l342: position, tokenIndex = position341, tokenIndex341 if buffer[position] != rune('\n') { goto l343 } position++ goto l341 l343: position, tokenIndex = position341, tokenIndex341 if buffer[position] != rune('\r') { goto l337 } position++ } l341: add(ruleEOL, position340) } break } } add(ruleWhitespace, position338) } goto l336 l337: position, tokenIndex = position337, tokenIndex337 } add(rule_, position335) } return true }, /* 76 Whitespace <- <((&('\t') '\t') | (&(' ') ' ') | (&('\n' | '\r') EOL))> */ nil, /* 77 EOL <- <(('\r' '\n') / '\n' / '\r')> */ nil, /* 78 EOF <- */ nil, /* 80 Action0 <- <{ p.AddNumber(text) }> */ nil, /* 81 Action1 <- <{ p.AddNumber("") }> */ nil, /* 82 Action2 <- <{ p.AddLevel(text) }> */ nil, /* 83 Action3 <- <{ p.AddStage(text) }> */ nil, /* 84 Action4 <- <{ p.AddField(text) }> */ nil, /* 85 Action5 <- <{ p.AddString(text) }> */ nil, /* 86 Action6 <- <{ p.AddString(text) }> */ nil, /* 87 Action7 <- <{ p.AddExpr() }> */ nil, /* 88 Action8 <- <{ p.AddTupleValue() }> */ nil, /* 89 Action9 <- <{ p.AddTupleValue() }> */ nil, /* 90 Action10 <- <{ p.AddTuple() }> */ nil, /* 91 Action11 <- <{ p.AddBinary(ast.IN) }> */ nil, /* 92 Action12 <- <{ p.AddTuple() }> */ nil, /* 93 Action13 <- <{ p.AddBinary(ast.IN); p.AddUnary(ast.LNOT) }> */ nil, /* 94 Action14 <- <{ p.AddMember(text) }> */ nil, /* 95 Action15 <- <{ p.AddSubscript(text) }> */ nil, /* 96 Action16 <- <{ p.AddUnary(ast.NOT) }> */ nil, /* 97 Action17 <- <{ p.AddBinary(ast.GE) }> */ nil, /* 98 Action18 <- <{ p.AddBinary(ast.GT) }> */ nil, /* 99 Action19 <- <{ p.AddBinary(ast.LE) }> */ nil, /* 100 Action20 <- <{ p.AddBinary(ast.LT) }> */ nil, /* 101 Action21 <- <{ p.AddBinary(ast.EQ) }> */ nil, /* 102 Action22 <- <{ p.AddBinary(ast.NE) }> */ nil, /* 103 Action23 <- <{ p.AddBinary(ast.EQ) }> */ nil, /* 104 Action24 <- <{ p.AddBinaryContains() }> */ nil, /* 105 Action25 <- <{ p.AddBinary(ast.AND) }> */ nil, /* 106 Action26 <- <{ p.AddBinary(ast.AND) }> */ nil, /* 107 Action27 <- <{ p.AddBinary(ast.AND) }> */ nil, /* 108 Action28 <- <{ p.AddBinary(ast.OR) }> */ nil, /* 109 Action29 <- <{ p.AddBinary(ast.OR) }> */ nil, /* 110 Action30 <- <{ p.AddUnary(ast.LNOT) }> */ nil, nil, /* 112 Action31 <- <{ p.SetNumber(text) }> */ nil, } p.rules = _rules return nil } ================================================ FILE: internal/logs/parser/parser.go ================================================ //go:generate peg -inline -switch grammar.peg // Package parser provides a parser for Up's // log query language, abstracting away provider // specifics. package parser import ( "strconv" "github.com/apex/up/internal/logs/parser/ast" ) // Parse query string. func Parse(s string) (ast.Node, error) { p := &parser{Buffer: s} p.Init() if err := p.Parse(); err != nil { return nil, err } p.Execute() n := ast.Root{Node: p.stack[0]} return n, nil } // push node. func (p *parser) push(n ast.Node) { p.stack = append(p.stack, n) } // pop node. func (p *parser) pop() ast.Node { if len(p.stack) == 0 { panic("pop: no nodes") } n := p.stack[len(p.stack)-1] p.stack = p.stack[:len(p.stack)-1] return n } // AddLevel node. func (p *parser) AddLevel(s string) { p.AddField("level") p.AddString(s) p.AddBinary(ast.EQ) p.AddExpr() } // AddExpr node. func (p *parser) AddExpr() { p.push(ast.Expr{ Node: p.pop(), }) } // AddField node. func (p *parser) AddField(s string) { switch s { case "level", "message", "timestamp": p.push(ast.Property(s)) default: p.push(ast.Field(s)) } } // AddString node. func (p *parser) AddString(s string) { p.push(ast.String(s)) } // AddSubscript node. func (p *parser) AddSubscript(s string) { p.push(ast.Subscript{ Left: p.pop(), Right: ast.Literal(s), }) } // AddMember node. func (p *parser) AddMember(s string) { p.push(ast.Member{ Left: p.pop(), Right: ast.Literal(s), }) } // SetNumber text. func (p *parser) SetNumber(s string) { p.number = s } // AddNumber node. func (p *parser) AddNumber(unit string) { f, _ := strconv.ParseFloat(p.number, 64) p.push(ast.Number{ Value: f, Unit: unit, }) } // AddTuple node. func (p *parser) AddTuple() { p.push(ast.Tuple{}) } // AddTupleValue node. func (p *parser) AddTupleValue() { v := p.pop() t := p.pop().(ast.Tuple) t = append(t, v) p.push(t) } // AddBinary node. func (p *parser) AddBinary(op ast.Op) { p.push(ast.Binary{ Op: op, Right: p.pop(), Left: p.pop(), }) } // AddStage node. func (p *parser) AddStage(stage string) { p.push(ast.Binary{ Op: ast.EQ, Left: ast.Field("stage"), Right: ast.String(stage), }) } // AddBinaryContains node. func (p *parser) AddBinaryContains() { p.push(ast.Binary{ Op: ast.EQ, Right: ast.Contains{Node: p.pop()}, Left: p.pop(), }) } // AddUnary node. func (p *parser) AddUnary(op ast.Op) { p.push(ast.Unary{ Op: op, Right: p.pop(), }) } ================================================ FILE: internal/logs/parser/parser_test.go ================================================ package parser import ( "testing" ) // TODO: precedence... // TODO: byte size literals // TODO: support literals: `method = GET`, `ip = 70.*` etc // TODO: error tests // TODO: test error messages // TODO: add "starts with" / "ends with" to compliment "contains"? // TODO: document best practices for logging json from an app var cases = []struct { Input string Output string }{ {`production`, `{ $.fields.stage = "production" }`}, {`development`, `{ $.fields.stage = "development" }`}, {`staging`, `{ $.fields.stage = "staging" }`}, {`method = "GET"`, `{ $.fields.method = "GET" }`}, {`debug`, `{ ($.level = "debug") }`}, {`info`, `{ ($.level = "info") }`}, {`warn`, `{ ($.level = "warn") }`}, {`error`, `{ ($.level = "error") }`}, {`fatal`, `{ ($.level = "fatal") }`}, {`not info`, `{ !(($.level = "info")) }`}, {`not error or fatal`, `{ !(($.level = "error") || ($.level = "fatal")) }`}, {`!info`, `{ !($.level = "info") }`}, {`level = "info"`, `{ $.level = "info" }`}, {`message = "user signin"`, `{ $.message = "user signin" }`}, {`email = "tj@apex.sh"`, `{ $.fields.email = "tj@apex.sh" }`}, {`status = 0`, `{ $.fields.status = 0 }`}, {`status = 0.123`, `{ $.fields.status = 0.123 }`}, {`status = .123`, `{ $.fields.status = 0.123 }`}, {`status = 200`, `{ $.fields.status = 200 }`}, {`price = 1.95`, `{ $.fields.price = 1.95 }`}, {`price == 1.95`, `{ $.fields.price = 1.95 }`}, {`price > 1.95`, `{ $.fields.price > 1.95 }`}, {`price < 1.95`, `{ $.fields.price < 1.95 }`}, {`price >= 1.95`, `{ $.fields.price >= 1.95 }`}, {`price <= 1.95`, `{ $.fields.price <= 1.95 }`}, {`price != 1.95`, `{ $.fields.price != 1.95 }`}, {`!enabled`, `{ !$.fields.enabled }`}, {`! enabled`, `{ !$.fields.enabled }`}, {`foo = 1 || bar = 2`, `{ $.fields.foo = 1 || $.fields.bar = 2 }`}, {`foo = 1 && bar = 2`, `{ $.fields.foo = 1 && $.fields.bar = 2 }`}, {`foo = 1 or bar = 2`, `{ $.fields.foo = 1 || $.fields.bar = 2 }`}, {`foo = 1 and bar = 2`, `{ $.fields.foo = 1 && $.fields.bar = 2 }`}, {`foo = 1 bar = 2`, `{ $.fields.foo = 1 && $.fields.bar = 2 }`}, {`foo.bar.baz = 1`, `{ $.fields.foo.bar.baz = 1 }`}, {`level = "error" and (duration >= 500 or duration = 0)`, `{ $.level = "error" && ($.fields.duration >= 500 || $.fields.duration = 0) }`}, {`level = "error" (duration >= 500 or duration = 0)`, `{ $.level = "error" && ($.fields.duration >= 500 || $.fields.duration = 0) }`}, {`cart.total = 15.99`, `{ $.fields.cart.total = 15.99 }`}, {`user.name contains "obi"`, `{ $.fields.user.name = "*obi*" }`}, {`user in ("Tobi")`, `{ ($.fields.user = "Tobi") }`}, {`pet.age in (1, 2, 3)`, `{ ($.fields.pet.age = 1 || $.fields.pet.age = 2 || $.fields.pet.age = 3) }`}, {`user in ("Tobi", "Loki", "Jane")`, `{ ($.fields.user = "Tobi" || $.fields.user = "Loki" || $.fields.user = "Jane") }`}, {`user.name in ("Tobi", "Loki", "Jane")`, `{ ($.fields.user.name = "Tobi" || $.fields.user.name = "Loki" || $.fields.user.name = "Jane") }`}, {`not user.admin`, `{ !($.fields.user.admin) }`}, {`not user.role in ("Admin", "Moderator")`, `{ !(($.fields.user.role = "Admin" || $.fields.user.role = "Moderator")) }`}, {`user.role not in ("Admin", "Moderator")`, `{ !(($.fields.user.role = "Admin" || $.fields.user.role = "Moderator")) }`}, {`not level = "error" or level = "fatal"`, `{ !($.level = "error" || $.level = "fatal") }`}, {`cart.products[0] = "something"`, `{ $.fields.cart.products[0] = "something" }`}, {`cart.products[0].price = 15.99`, `{ $.fields.cart.products[0].price = 15.99 }`}, {`cart.products[0][1].price = 15.99`, `{ $.fields.cart.products[0][1].price = 15.99 }`}, {`cart.products[0].items[1].price = 15.99`, `{ $.fields.cart.products[0].items[1].price = 15.99 }`}, {`user.name in ("Tobi", "Loki") and status >= 500`, `{ ($.fields.user.name = "Tobi" || $.fields.user.name = "Loki") && $.fields.status >= 500 }`}, {`method in ("POST", "PUT") and ip = "207.*" and status = 200 and duration >= 50`, `{ ($.fields.method = "POST" || $.fields.method = "PUT") && $.fields.ip = "207.*" && $.fields.status = 200 && $.fields.duration >= 50 }`}, {`method in ("POST", "PUT") ip = "207.*" status = 200 duration >= 50`, `{ ($.fields.method = "POST" || $.fields.method = "PUT") && $.fields.ip = "207.*" && $.fields.status = 200 && $.fields.duration >= 50 }`}, {`size > 1kb`, `{ $.fields.size > 1024 }`}, {`size > 2kb`, `{ $.fields.size > 2048 }`}, {`size > 1.5mb`, `{ $.fields.size > 1572864 }`}, {`size > 100b`, `{ $.fields.size > 100 }`}, {`duration > 100ms`, `{ $.fields.duration > 100 }`}, {`duration > 1s`, `{ $.fields.duration > 1000 }`}, {`duration > 4.5s`, `{ $.fields.duration > 4500 }`}, {`"User Login"`, `{ $.message = "User Login" }`}, {`"User*"`, `{ $.message = "User*" }`}, {`"Signup" or "Signin"`, `{ $.message = "Signup" || $.message = "Signin" }`}, {`"User Login" method = "GET"`, `{ $.message = "User Login" && $.fields.method = "GET" }`}, {`method = GET`, `{ $.fields.method = "GET" }`}, {`method in (GET, HEAD, OPTIONS)`, `{ ($.fields.method = "GET" || $.fields.method = "HEAD" || $.fields.method = "OPTIONS") }`}, {`name = tj`, `{ $.fields.name = "tj" }`}, {`method = GET path = /account/billing`, `{ $.fields.method = "GET" && $.fields.path = "/account/billing" }`}, {`cart.products[0].name = ps4`, `{ $.fields.cart.products[0].name = "ps4" }`}, {`path = "/_health"`, `{ $.fields.path = "/_health" }`}, {`path == "/_health"`, `{ $.fields.path = "/_health" }`}, {`path > "/_health"`, `{ $.fields.path > "/_health" }`}, {`path >= "/_health"`, `{ $.fields.path >= "/_health" }`}, {`path != "/_health"`, `{ $.fields.path != "/_health" }`}, } func TestParse(t *testing.T) { for _, c := range cases { t.Logf("parsing %q", c.Input) n, err := Parse(c.Input) if err != nil { t.Errorf("error parsing %q: %s", c.Input, err) continue } if n.String() != c.Output { t.Errorf("\n\ntext: %s\nwant: %s\n got: %s\n\n", c.Input, c.Output, n.String()) } } } func BenchmarkParse(b *testing.B) { for i := 0; i < b.N; i++ { Parse(`user.name in ("Tobi", "Loki", "Jane")`) } } ================================================ FILE: internal/logs/text/text.go ================================================ // Package text implements a development-friendly textual handler. package text import ( "bytes" "fmt" "io" "sync" "time" "github.com/apex/log" "github.com/dustin/go-humanize" "github.com/apex/up/internal/colors" "github.com/apex/up/internal/util" ) var ( spacerPlaceholderBytes = []byte("{{spacer}}") spacerBytes = []byte(colors.Gray(":")) newlineBytes = []byte("\n") emptyBytes = []byte("") ) // color function. type colorFunc func(string) string // omit fields. var omit = map[string]bool{ "app": true, "stage": true, "region": true, "plugin": true, "commit": true, "version": true, } // Colors mapping. var Colors = [...]colorFunc{ log.DebugLevel: colors.Gray, log.InfoLevel: colors.Purple, log.WarnLevel: colors.Yellow, log.ErrorLevel: colors.Red, log.FatalLevel: colors.Red, } // Strings mapping. var Strings = [...]string{ log.DebugLevel: "DEBU", log.InfoLevel: "INFO", log.WarnLevel: "WARN", log.ErrorLevel: "ERRO", log.FatalLevel: "FATA", } // Handler implementation. type Handler struct { mu sync.Mutex Writer io.Writer expand bool layout string } // New handler. func New(w io.Writer) *Handler { return &Handler{ Writer: w, } } // WithExpandedFields sets the expanded field state. func (h *Handler) WithExpandedFields(v bool) *Handler { h.expand = v return h } // HandleLog implements log.Handler. func (h *Handler) HandleLog(e *log.Entry) error { switch { case h.expand: return h.handleExpanded(e) default: return h.handleInline(e) } } // handleExpanded fields. func (h *Handler) handleExpanded(e *log.Entry) error { color := Colors[e.Level] level := Strings[e.Level] names := e.Fields.Names() h.mu.Lock() defer h.mu.Unlock() ts := formatDate(e.Timestamp.Local()) fmt.Fprintf(h.Writer, " %s %s %s\n", colors.Gray(ts), bold(color(level)), colors.Purple(e.Message)) for _, name := range names { v := e.Fields.Get(name) if v == "" { continue } fmt.Fprintf(h.Writer, " %s%s%v\n", color(name), colors.Gray(": "), value(name, v)) } if len(names) > 0 { fmt.Fprintf(h.Writer, "\n") } return nil } // handleInline fields. func (h *Handler) handleInline(e *log.Entry) error { var buf bytes.Buffer var fields int color := Colors[e.Level] level := Strings[e.Level] names := e.Fields.Names() ts := formatDate(e.Timestamp.Local()) if stage, ok := e.Fields.Get("stage").(string); ok && stage != "" { fmt.Fprintf(&buf, " %s %s %s %s %s{{spacer}}", colors.Gray(ts), bold(color(level)), colors.Gray(stage), colors.Gray(version(e)), colors.Purple(e.Message)) } else { fmt.Fprintf(&buf, " %s %s %s{{spacer}}", colors.Gray(ts), bold(color(level)), colors.Purple(e.Message)) } for _, name := range names { if omit[name] { continue } v := e.Fields.Get(name) if v == "" { continue } fields++ fmt.Fprintf(&buf, " %s%s%v", color(name), colors.Gray("="), value(name, v)) } b := buf.Bytes() if fields > 0 { b = bytes.Replace(b, spacerPlaceholderBytes, spacerBytes, 1) } else { b = bytes.Replace(b, spacerPlaceholderBytes, emptyBytes, 1) } h.mu.Lock() h.Writer.Write(b) h.Writer.Write(newlineBytes) h.mu.Unlock() return nil } // value returns the formatted value. func value(name string, v interface{}) interface{} { switch name { case "size": return humanize.Bytes(uint64(util.ToFloat(v))) case "duration": return time.Millisecond * time.Duration(util.ToFloat(v)) default: return v } } // day duration. var day = time.Hour * 24 // formatDate formats t relative to now. func formatDate(t time.Time) string { return t.Format(`Jan 2` + util.DateSuffix(t) + ` 03:04:05pm`) } // version returns the entry version via GIT commit or lambda version. func version(e *log.Entry) string { if s, ok := e.Fields.Get("commit").(string); ok && s != "" { return s } if s, ok := e.Fields.Get("version").(string); ok && s != "" { return s } return "" } // bold string. func bold(s string) string { return fmt.Sprintf("\033[1m%s\033[0m", s) } ================================================ FILE: internal/logs/text/text_test.go ================================================ package text import ( "bytes" "io" "os" "testing" "time" "github.com/apex/log" ) func init() { log.Now = func() time.Time { return time.Unix(0, 0) } } func Test(t *testing.T) { var buf bytes.Buffer log.SetHandler(New(&buf)) log.WithField("user", "tj").WithField("id", "123").Info("hello") log.WithField("user", "tj").Info("something broke") log.WithField("user", "tj").Warn("something kind of broke") log.WithField("user", "tj").Error("boom") io.Copy(os.Stdout, &buf) } ================================================ FILE: internal/logs/writer/writer.go ================================================ // Package writer provides an io.Writer for capturing // process output as logs, so that stdout may become // INFO, and stderr ERROR. package writer import ( "bufio" "bytes" "encoding/json" "github.com/apex/log" "github.com/apex/up/internal/util" ) // Writer struct. type Writer struct { log log.Interface level log.Level } // New writer with the given log level. func New(l log.Level, ctx log.Interface) *Writer { return &Writer{ log: ctx, level: l, } } // Write implementation. func (w *Writer) Write(b []byte) (int, error) { s := bufio.NewScanner(bytes.NewReader(b)) for s.Scan() { if err := w.write(s.Text()); err != nil { return 0, err } } if err := s.Err(); err != nil { return 0, err } return len(b), nil } // write the line. func (w *Writer) write(s string) error { if util.IsJSONLog(s) { return w.writeJSON(s) } return w.writeText(s) } // writeJSON writes a json log, interpreting it as a log.Entry. func (w *Writer) writeJSON(s string) error { // TODO: make this less ugly in apex/log, // you should be able to write an arbitrary Entry. var e log.Entry if err := json.Unmarshal([]byte(s), &e); err != nil { return w.writeText(s) } switch e.Level { case log.DebugLevel: w.log.WithFields(e.Fields).Debug(e.Message) case log.InfoLevel: w.log.WithFields(e.Fields).Info(e.Message) case log.WarnLevel: w.log.WithFields(e.Fields).Warn(e.Message) case log.ErrorLevel: w.log.WithFields(e.Fields).Error(e.Message) case log.FatalLevel: // TODO: FATAL without exit... w.log.WithFields(e.Fields).Error(e.Message) } return nil } // writeText writes plain text. func (w *Writer) writeText(s string) error { switch w.level { case log.InfoLevel: w.log.Info(s) case log.ErrorLevel: w.log.Error(s) } return nil } ================================================ FILE: internal/logs/writer/writer_test.go ================================================ package writer import ( "bytes" "io" "strings" "testing" "time" "github.com/apex/log" "github.com/apex/log/handlers/json" "github.com/tj/assert" ) func init() { log.Now = func() time.Time { return time.Unix(0, 0).UTC() } } func TestWriter_plainTextFlat(t *testing.T) { var buf bytes.Buffer log.SetHandler(json.New(&buf)) w := New(log.InfoLevel, log.Log) input := `GET / GET /account GET /login POST / POST /logout ` _, err := io.Copy(w, strings.NewReader(input)) assert.NoError(t, err, "copy") expected := `{"fields":{},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"GET /"} {"fields":{},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"GET /account"} {"fields":{},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"GET /login"} {"fields":{},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"POST /"} {"fields":{},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"POST /logout"} ` assert.Equal(t, expected, buf.String()) } func TestWriter_json(t *testing.T) { var buf bytes.Buffer log.SetHandler(json.New(&buf)) w := New(log.InfoLevel, log.Log) input := `{ "level": "info", "message": "request", "fields": { "method": "GET", "path": "/" } } { "level": "info", "message": "request", "fields": { "method": "GET", "path": "/login" } } { "level": "info", "message": "request", "fields": { "method": "POST", "path": "/login" } } ` _, err := io.Copy(w, strings.NewReader(input)) assert.NoError(t, err, "copy") expected := `{"fields":{"method":"GET","path":"/"},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"request"} {"fields":{"method":"GET","path":"/login"},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"request"} {"fields":{"method":"POST","path":"/login"},"level":"info","timestamp":"1970-01-01T00:00:00Z","message":"request"} ` assert.Equal(t, expected, buf.String()) } ================================================ FILE: internal/metrics/metrics.go ================================================ // Package metrics provides higher level CloudWatch metrics operations. package metrics import ( "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudwatch" ) // Metrics helper. type Metrics struct { in cloudwatch.GetMetricStatisticsInput } // New metrics. func New() *Metrics { return &Metrics{} } // Namespace sets the namespace. func (m *Metrics) Namespace(name string) *Metrics { m.in.Namespace = &name return m } // Metric sets the metric name. func (m *Metrics) Metric(name string) *Metrics { m.in.MetricName = &name return m } // Stats sets the stats. func (m *Metrics) Stats(names []string) *Metrics { m.in.Statistics = aws.StringSlice(names) return m } // Stat adds the stat. func (m *Metrics) Stat(name string) *Metrics { m.in.Statistics = append(m.in.Statistics, &name) return m } // Dimension adds a dimension. func (m *Metrics) Dimension(name, value string) *Metrics { m.in.Dimensions = append(m.in.Dimensions, &cloudwatch.Dimension{ Name: &name, Value: &value, }) return m } // Period sets the period in seconds. func (m *Metrics) Period(seconds int) *Metrics { m.in.Period = aws.Int64(int64(seconds)) return m } // TimeRange sets the start and time times. func (m *Metrics) TimeRange(start, end time.Time) *Metrics { m.in.StartTime = &start m.in.EndTime = &end return m } // Params returns the API input. func (m *Metrics) Params() *cloudwatch.GetMetricStatisticsInput { return &m.in } ================================================ FILE: internal/progressreader/progressreader.go ================================================ // Package progressreader provides an io.Reader progress bar. package progressreader import ( "io" "sync" "github.com/apex/up/internal/util" "github.com/tj/go-progress" "github.com/tj/go/term" ) // reader wrapping a progress bar. type reader struct { io.ReadCloser p *progress.Bar render func(string) written int sync.Once } // Read implementation. func (r *reader) Read(b []byte) (int, error) { r.Do(term.ClearAll) n, err := r.ReadCloser.Read(b) r.written += n r.p.ValueInt(r.written) r.render(term.CenterLine(r.p.String())) return n, err } // New returns a progress bar reader. func New(size int, r io.ReadCloser) io.ReadCloser { return &reader{ ReadCloser: r, p: util.NewProgressInt(size), render: term.Renderer(), } } ================================================ FILE: internal/proxy/bin/bin.go ================================================ //go:generate sh -c "GOOS=linux GOARCH=amd64 go build -o up-proxy ../../../cmd/up-proxy/main.go" //go:generate go-bindata -modtime 0 -pkg bin -o bin_assets.go . package bin ================================================ FILE: internal/proxy/event.go ================================================ package proxy // Identity is the identity information associated with the request. type Identity struct { APIKey string `json:"apiKey"` AccountID string `json:"accountId"` UserAgent string `json:"userAgent"` SourceIP string `json:"sourceIp"` AccessKey string `json:"accessKey"` Caller string `json:"caller"` User string `json:"user"` UserARN string `json:"userARN"` CognitoIdentityID string `json:"cognitoIdentityId"` CognitoIdentityPoolID string `json:"cognitoIdentityPoolId"` CognitoAuthenticationType string `json:"cognitoAuthenticationType"` CognitoAuthenticationProvider string `json:"cognitoAuthenticationProvider"` } // RequestContext is the contextual information provided by API Gateway. type RequestContext struct { APIID string `json:"apiId"` ResourceID string `json:"resourceId"` RequestID string `json:"requestId"` HTTPMethod string `json:"-"` ResourcePath string `json:"-"` AccountID string `json:"accountId"` Stage string `json:"stage"` Identity Identity `json:"identity"` Authorizer map[string]interface{} `json:"authorizer"` } // Input is the input provided by API Gateway. type Input struct { HTTPMethod string Headers map[string]string Resource string PathParameters map[string]string Path string QueryStringParameters map[string]string Body string IsBase64Encoded bool StageVariables map[string]string RequestContext RequestContext } // Output is the output expected by API Gateway. type Output struct { StatusCode int `json:"statusCode"` Headers map[string]string `json:"headers,omitempty"` Body string `json:"body,omitempty"` IsBase64Encoded bool `json:"isBase64Encoded"` } ================================================ FILE: internal/proxy/event_test.go ================================================ package proxy import ( "encoding/json" "fmt" ) var getEvent = `{ "resource": "/{proxy+}", "path": "/pets/tobi", "httpMethod": "GET", "headers": { "Accept": "*/*", "CloudFront-Forwarded-Proto": "https", "CloudFront-Is-Desktop-Viewer": "true", "CloudFront-Is-Mobile-Viewer": "false", "CloudFront-Is-SmartTV-Viewer": "false", "CloudFront-Is-Tablet-Viewer": "false", "CloudFront-Viewer-Country": "CA", "Host": "apex-ping.com", "User-Agent": "curl/7.48.0", "Via": "2.0 a44b4468444ef3ee67472bd5c5016098.cloudfront.net (CloudFront)", "X-Amz-Cf-Id": "VRxPGF8rOXD7xpRjAjseXfRrFD3wg-QPUHY6chzB9bR7pXlct1NTpg==", "X-Amzn-Trace-Id": "Root=1-59554c99-4375fc8705ccb554008b3aad", "X-Forwarded-For": "207.102.57.26, 54.182.214.69", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https" }, "queryStringParameters": { "format": "json" }, "pathParameters": { "proxy": "pets/tobi" }, "stageVariables": { "env": "prod" }, "requestContext": { "path": "/pets/tobi", "accountId": "111111111", "resourceId": "jcl9w3", "stage": "prod", "requestId": "344b184b-5cfc-11e7-8483-27dbb2d30a77", "identity": { "cognitoIdentityPoolId": null, "accountId": null, "cognitoIdentityId": null, "caller": null, "apiKey": "", "sourceIp": "207.102.57.26", "accessKey": null, "cognitoAuthenticationType": null, "cognitoAuthenticationProvider": null, "userArn": null, "userAgent": "curl/7.48.0", "user": null }, "resourcePath": "/{proxy+}", "httpMethod": "GET", "apiId": "iwcgwgigca" }, "body": null, "isBase64Encoded": false }` var getEventBasicAuth = `{ "resource": "/{proxy+}", "path": "/pets/tobi", "httpMethod": "GET", "headers": { "Accept": "*/*", "CloudFront-Forwarded-Proto": "https", "CloudFront-Is-Desktop-Viewer": "true", "CloudFront-Is-Mobile-Viewer": "false", "CloudFront-Is-SmartTV-Viewer": "false", "CloudFront-Is-Tablet-Viewer": "false", "CloudFront-Viewer-Country": "CA", "Host": "apex-ping.com", "User-Agent": "curl/7.48.0", "Via": "2.0 a44b4468444ef3ee67472bd5c5016098.cloudfront.net (CloudFront)", "X-Amz-Cf-Id": "VRxPGF8rOXD7xpRjAjseXfRrFD3wg-QPUHY6chzB9bR7pXlct1NTpg==", "X-Amzn-Trace-Id": "Root=1-59554c99-4375fc8705ccb554008b3aad", "X-Forwarded-For": "207.102.57.26, 54.182.214.69", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https", "Authorization": "Basic dG9iaTpmZXJyZXQ=" }, "queryStringParameters": { "format": "json" }, "pathParameters": { "proxy": "pets/tobi" }, "stageVariables": { "env": "prod" }, "requestContext": { "path": "/pets/tobi", "accountId": "111111111", "resourceId": "jcl9w3", "stage": "prod", "requestId": "344b184b-5cfc-11e7-8483-27dbb2d30a77", "identity": { "cognitoIdentityPoolId": null, "accountId": null, "cognitoIdentityId": null, "caller": null, "apiKey": "", "sourceIp": "207.102.57.26", "accessKey": null, "cognitoAuthenticationType": null, "cognitoAuthenticationProvider": null, "userArn": null, "userAgent": "curl/7.48.0", "user": null }, "resourcePath": "/{proxy+}", "httpMethod": "GET", "apiId": "iwcgwgigca" }, "body": null, "isBase64Encoded": false }` var postEvent = `{ "resource": "/{proxy+}", "path": "/pets/tobi", "httpMethod": "POST", "headers": { "Accept": "*/*", "CloudFront-Forwarded-Proto": "https", "CloudFront-Is-Desktop-Viewer": "true", "CloudFront-Is-Mobile-Viewer": "false", "CloudFront-Is-SmartTV-Viewer": "false", "CloudFront-Is-Tablet-Viewer": "false", "CloudFront-Viewer-Country": "CA", "content-type": "application/json", "Host": "apex-ping.com", "User-Agent": "curl/7.48.0", "Via": "2.0 b790a9f06b09414fec5d8b87e81d4b7f.cloudfront.net (CloudFront)", "X-Amz-Cf-Id": "_h1jFD3wjq6ZIyr8be6RS7Y7665jF9SjACmVodBMRefoQCs7KwTxMw==", "X-Amzn-Trace-Id": "Root=1-59554cc9-35de2f970f0fdf017f16927f", "X-Forwarded-For": "207.102.57.26, 54.182.214.86", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https" }, "queryStringParameters": null, "pathParameters": { "proxy": "pets/tobi" }, "requestContext": { "path": "/pets/tobi", "accountId": "111111111", "resourceId": "jcl9w3", "stage": "prod", "requestId": "50f6e0ce-5cfc-11e7-ada1-4f5cfe727f01", "identity": { "cognitoIdentityPoolId": null, "accountId": null, "cognitoIdentityId": null, "caller": null, "apiKey": "", "sourceIp": "207.102.57.26", "accessKey": null, "cognitoAuthenticationType": null, "cognitoAuthenticationProvider": null, "userArn": null, "userAgent": "curl/7.48.0", "user": null }, "resourcePath": "/{proxy+}", "httpMethod": "POST", "apiId": "iwcgwgigca" }, "body": "{ \"name\": \"Tobi\" }", "isBase64Encoded": false }` var postEventBinary = `{ "resource": "/{proxy+}", "path": "/pets/tobi", "httpMethod": "POST", "headers": { "Accept": "*/*", "CloudFront-Forwarded-Proto": "https", "CloudFront-Is-Desktop-Viewer": "true", "CloudFront-Is-Mobile-Viewer": "false", "CloudFront-Is-SmartTV-Viewer": "false", "CloudFront-Is-Tablet-Viewer": "false", "CloudFront-Viewer-Country": "CA", "content-type": "text/plain", "Host": "apex-ping.com", "User-Agent": "curl/7.48.0", "Via": "2.0 b790a9f06b09414fec5d8b87e81d4b7f.cloudfront.net (CloudFront)", "X-Amz-Cf-Id": "_h1jFD3wjq6ZIyr8be6RS7Y7665jF9SjACmVodBMRefoQCs7KwTxMw==", "X-Amzn-Trace-Id": "Root=1-59554cc9-35de2f970f0fdf017f16927f", "X-Forwarded-For": "207.102.57.26, 54.182.214.86", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https" }, "queryStringParameters": null, "pathParameters": { "proxy": "pets/tobi" }, "requestContext": { "path": "/pets/tobi", "accountId": "111111111", "resourceId": "jcl9w3", "stage": "prod", "requestId": "50f6e0ce-5cfc-11e7-ada1-4f5cfe727f01", "identity": { "cognitoIdentityPoolId": null, "accountId": null, "cognitoIdentityId": null, "caller": null, "apiKey": "", "sourceIp": "207.102.57.26", "accessKey": null, "cognitoAuthenticationType": null, "cognitoAuthenticationProvider": null, "userArn": null, "userAgent": "curl/7.48.0", "user": null }, "resourcePath": "/{proxy+}", "httpMethod": "POST", "apiId": "iwcgwgigca" }, "body": "SGVsbG8gV29ybGQ=", "isBase64Encoded": true }` func output(v interface{}) { b, _ := json.MarshalIndent(v, "", " ") fmt.Printf("%s\n", string(b)) } func ExampleInput_get() { var in Input json.Unmarshal([]byte(getEvent), &in) output(in) // Output: // { // "HTTPMethod": "GET", // "Headers": { // "Accept": "*/*", // "CloudFront-Forwarded-Proto": "https", // "CloudFront-Is-Desktop-Viewer": "true", // "CloudFront-Is-Mobile-Viewer": "false", // "CloudFront-Is-SmartTV-Viewer": "false", // "CloudFront-Is-Tablet-Viewer": "false", // "CloudFront-Viewer-Country": "CA", // "Host": "apex-ping.com", // "User-Agent": "curl/7.48.0", // "Via": "2.0 a44b4468444ef3ee67472bd5c5016098.cloudfront.net (CloudFront)", // "X-Amz-Cf-Id": "VRxPGF8rOXD7xpRjAjseXfRrFD3wg-QPUHY6chzB9bR7pXlct1NTpg==", // "X-Amzn-Trace-Id": "Root=1-59554c99-4375fc8705ccb554008b3aad", // "X-Forwarded-For": "207.102.57.26, 54.182.214.69", // "X-Forwarded-Port": "443", // "X-Forwarded-Proto": "https" // }, // "Resource": "/{proxy+}", // "PathParameters": { // "proxy": "pets/tobi" // }, // "Path": "/pets/tobi", // "QueryStringParameters": { // "format": "json" // }, // "Body": "", // "IsBase64Encoded": false, // "StageVariables": { // "env": "prod" // }, // "RequestContext": { // "apiId": "iwcgwgigca", // "resourceId": "jcl9w3", // "requestId": "344b184b-5cfc-11e7-8483-27dbb2d30a77", // "accountId": "111111111", // "stage": "prod", // "identity": { // "apiKey": "", // "accountId": "", // "userAgent": "curl/7.48.0", // "sourceIp": "207.102.57.26", // "accessKey": "", // "caller": "", // "user": "", // "userARN": "", // "cognitoIdentityId": "", // "cognitoIdentityPoolId": "", // "cognitoAuthenticationType": "", // "cognitoAuthenticationProvider": "" // }, // "authorizer": null // } // } } func ExampleInput_post() { var in Input json.Unmarshal([]byte(postEvent), &in) output(in) // Output: // { // "HTTPMethod": "POST", // "Headers": { // "Accept": "*/*", // "CloudFront-Forwarded-Proto": "https", // "CloudFront-Is-Desktop-Viewer": "true", // "CloudFront-Is-Mobile-Viewer": "false", // "CloudFront-Is-SmartTV-Viewer": "false", // "CloudFront-Is-Tablet-Viewer": "false", // "CloudFront-Viewer-Country": "CA", // "Host": "apex-ping.com", // "User-Agent": "curl/7.48.0", // "Via": "2.0 b790a9f06b09414fec5d8b87e81d4b7f.cloudfront.net (CloudFront)", // "X-Amz-Cf-Id": "_h1jFD3wjq6ZIyr8be6RS7Y7665jF9SjACmVodBMRefoQCs7KwTxMw==", // "X-Amzn-Trace-Id": "Root=1-59554cc9-35de2f970f0fdf017f16927f", // "X-Forwarded-For": "207.102.57.26, 54.182.214.86", // "X-Forwarded-Port": "443", // "X-Forwarded-Proto": "https", // "content-type": "application/json" // }, // "Resource": "/{proxy+}", // "PathParameters": { // "proxy": "pets/tobi" // }, // "Path": "/pets/tobi", // "QueryStringParameters": null, // "Body": "{ \"name\": \"Tobi\" }", // "IsBase64Encoded": false, // "StageVariables": null, // "RequestContext": { // "apiId": "iwcgwgigca", // "resourceId": "jcl9w3", // "requestId": "50f6e0ce-5cfc-11e7-ada1-4f5cfe727f01", // "accountId": "111111111", // "stage": "prod", // "identity": { // "apiKey": "", // "accountId": "", // "userAgent": "curl/7.48.0", // "sourceIp": "207.102.57.26", // "accessKey": "", // "caller": "", // "user": "", // "userARN": "", // "cognitoIdentityId": "", // "cognitoIdentityPoolId": "", // "cognitoAuthenticationType": "", // "cognitoAuthenticationProvider": "" // }, // "authorizer": null // } // } } ================================================ FILE: internal/proxy/lambda.go ================================================ // Package proxy provides API Gateway and Lambda interoperability. package proxy import ( "encoding/json" "net/http" "github.com/apex/go-apex" "github.com/pkg/errors" ) // NewHandler returns an apex.Handler. func NewHandler(h http.Handler) apex.Handler { return apex.HandlerFunc(func(event json.RawMessage, ctx *apex.Context) (interface{}, error) { e := new(Input) err := json.Unmarshal(event, e) if err != nil { return nil, errors.Wrap(err, "parsing proxy event") } req, err := NewRequest(e) if err != nil { return nil, errors.Wrap(err, "creating new request from event") } res := NewResponse() h.ServeHTTP(res, req) return res.End(), nil }) } ================================================ FILE: internal/proxy/request.go ================================================ package proxy import ( "encoding/base64" "encoding/json" "net/http" "net/url" "strconv" "strings" "github.com/pkg/errors" ) // NewRequest returns a new http.Request from the given Lambda event. func NewRequest(e *Input) (*http.Request, error) { // path u, err := url.Parse(e.Path) if err != nil { return nil, errors.Wrap(err, "parsing path") } // querystring q := u.Query() for k, v := range e.QueryStringParameters { q.Set(k, v) } u.RawQuery = q.Encode() // base64 encoded body body := e.Body if e.IsBase64Encoded { b, err := base64.StdEncoding.DecodeString(body) if err != nil { return nil, errors.Wrap(err, "decoding base64 body") } body = string(b) } // new request req, err := http.NewRequest(e.HTTPMethod, u.String(), strings.NewReader(body)) if err != nil { return nil, errors.Wrap(err, "creating request") } // remote addr req.RemoteAddr = e.RequestContext.Identity.SourceIP // header fields for k, v := range e.Headers { req.Header.Set(k, v) } // content-length if req.Header.Get("Content-Length") == "" && body != "" { req.Header.Set("Content-Length", strconv.Itoa(len(body))) } // custom fields b, _ := json.Marshal(e.RequestContext) req.Header.Set("X-Context", string(b)) req.Header.Set("X-Request-Id", e.RequestContext.RequestID) req.Header.Set("X-Stage", e.RequestContext.Stage) // host req.URL.Host = req.Header.Get("Host") req.Host = req.URL.Host return req, nil } ================================================ FILE: internal/proxy/request_test.go ================================================ package proxy import ( "encoding/json" "io/ioutil" "testing" "github.com/tj/assert" ) func TestNewRequest(t *testing.T) { t.Run("GET", func(t *testing.T) { var in Input err := json.Unmarshal([]byte(getEvent), &in) assert.NoError(t, err, "unmarshal") req, err := NewRequest(&in) assert.NoError(t, err, "new request") assert.Equal(t, "GET", req.Method) assert.Equal(t, "apex-ping.com", req.Host) assert.Equal(t, "/pets/tobi", req.URL.Path) assert.Equal(t, "format=json", req.URL.Query().Encode()) assert.Equal(t, "207.102.57.26", req.RemoteAddr) }) t.Run("POST", func(t *testing.T) { var in Input err := json.Unmarshal([]byte(postEvent), &in) assert.NoError(t, err, "unmarshal") req, err := NewRequest(&in) assert.NoError(t, err, "new request") assert.Equal(t, "POST", req.Method) assert.Equal(t, "apex-ping.com", req.Host) assert.Equal(t, "/pets/tobi", req.URL.Path) assert.Equal(t, "", req.URL.Query().Encode()) assert.Equal(t, "207.102.57.26", req.RemoteAddr) b, err := ioutil.ReadAll(req.Body) assert.NoError(t, err, "read body") assert.Equal(t, `{ "name": "Tobi" }`, string(b)) }) t.Run("POST binary", func(t *testing.T) { var in Input err := json.Unmarshal([]byte(postEventBinary), &in) assert.NoError(t, err, "unmarshal") req, err := NewRequest(&in) assert.NoError(t, err, "new request") assert.Equal(t, "POST", req.Method) assert.Equal(t, "/pets/tobi", req.URL.Path) assert.Equal(t, "", req.URL.Query().Encode()) assert.Equal(t, "207.102.57.26", req.RemoteAddr) b, err := ioutil.ReadAll(req.Body) assert.NoError(t, err, "read body") assert.Equal(t, `Hello World`, string(b)) }) t.Run("Basic Auth", func(t *testing.T) { var in Input err := json.Unmarshal([]byte(getEventBasicAuth), &in) assert.NoError(t, err, "unmarshal") req, err := NewRequest(&in) assert.NoError(t, err, "new request") assert.Equal(t, "GET", req.Method) assert.Equal(t, "/pets/tobi", req.URL.Path) user, pass, ok := req.BasicAuth() assert.Equal(t, "tobi", user) assert.Equal(t, "ferret", pass) assert.True(t, ok) }) } ================================================ FILE: internal/proxy/response.go ================================================ package proxy import ( "bytes" "encoding/base64" "mime" "net/http" "strings" "github.com/apex/up/internal/util" ) // ResponseWriter implements the http.ResponseWriter interface // in order to support the API Gateway Lambda HTTP "protocol". type ResponseWriter struct { out Output buf bytes.Buffer header http.Header wroteHeader bool } // NewResponse returns a new response writer to capture http output. func NewResponse() *ResponseWriter { return &ResponseWriter{} } // Header implementation. func (w *ResponseWriter) Header() http.Header { if w.header == nil { w.header = make(http.Header) } return w.header } // Write implementation. func (w *ResponseWriter) Write(b []byte) (int, error) { if !w.wroteHeader { w.WriteHeader(http.StatusOK) } // TODO: HEAD? ignore return w.buf.Write(b) } // WriteHeader implementation. func (w *ResponseWriter) WriteHeader(status int) { if w.wroteHeader { return } if w.Header().Get("Content-Type") == "" { w.Header().Set("Content-Type", "text/plain; charset=utf8") } w.out.StatusCode = status h := make(map[string]string) // API Gateway does not support multiple set-cookie fields // so we have to stagger the casing in order to support this. util.FixMultipleSetCookie(w.Header()) for k, v := range w.Header() { if len(v) > 0 { h[k] = v[len(v)-1] } } w.out.Headers = h w.wroteHeader = true } // End the request. func (w *ResponseWriter) End() Output { w.out.IsBase64Encoded = isBinary(w.header) if w.out.IsBase64Encoded { w.out.Body = base64.StdEncoding.EncodeToString(w.buf.Bytes()) } else { w.out.Body = w.buf.String() } return w.out } // isBinary returns true if the response reprensents binary. func isBinary(h http.Header) bool { if !isTextMime(h.Get("Content-Type")) { return true } if h.Get("Content-Encoding") == "gzip" { return true } return false } // isTextMime returns true if the content type represents textual data. func isTextMime(kind string) bool { mt, _, err := mime.ParseMediaType(kind) if err != nil { return false } if strings.HasPrefix(mt, "text/") { return true } switch mt { case "image/svg+xml": return true case "application/json": return true case "application/xml": return true default: return false } } ================================================ FILE: internal/proxy/response_test.go ================================================ package proxy import ( "bytes" "testing" "github.com/tj/assert" ) func Test_JSON_isTextMime(t *testing.T) { assert.Equal(t, isTextMime("application/json"), true) assert.Equal(t, isTextMime("application/json; charset=utf-8"), true) assert.Equal(t, isTextMime("Application/JSON"), true) } func Test_XML_isTextMime(t *testing.T) { assert.Equal(t, isTextMime("application/xml"), true) assert.Equal(t, isTextMime("application/xml; charset=utf-8"), true) assert.Equal(t, isTextMime("ApPlicaTion/xMl"), true) } func TestResponseWriter_Header(t *testing.T) { w := NewResponse() w.Header().Set("Foo", "bar") w.Header().Set("Bar", "baz") var buf bytes.Buffer w.header.Write(&buf) assert.Equal(t, "Bar: baz\r\nFoo: bar\r\n", buf.String()) } func TestResponseWriter_Write_text(t *testing.T) { types := []string{ "text/x-custom", "text/plain", "text/plain; charset=utf-8", "application/json", "application/json; charset=utf-8", "application/xml", "image/svg+xml", } for _, kind := range types { t.Run(kind, func(t *testing.T) { w := NewResponse() w.Header().Set("Content-Type", kind) w.Write([]byte("hello world\n")) e := w.End() assert.Equal(t, 200, e.StatusCode) assert.Equal(t, "hello world\n", e.Body) assert.Equal(t, kind, e.Headers["Content-Type"]) assert.False(t, e.IsBase64Encoded) }) } } func TestResponseWriter_Write_binary(t *testing.T) { w := NewResponse() w.Header().Set("Content-Type", "image/png") w.Write([]byte("data")) e := w.End() assert.Equal(t, 200, e.StatusCode) assert.Equal(t, "ZGF0YQ==", e.Body) assert.Equal(t, "image/png", e.Headers["Content-Type"]) assert.True(t, e.IsBase64Encoded) } func TestResponseWriter_Write_gzip(t *testing.T) { w := NewResponse() w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Encoding", "gzip") w.Write([]byte("data")) e := w.End() assert.Equal(t, 200, e.StatusCode) assert.Equal(t, "ZGF0YQ==", e.Body) assert.Equal(t, "text/plain", e.Headers["Content-Type"]) assert.True(t, e.IsBase64Encoded) } func TestResponseWriter_WriteHeader(t *testing.T) { w := NewResponse() w.WriteHeader(404) w.Write([]byte("Not Found\n")) e := w.End() assert.Equal(t, 404, e.StatusCode) assert.Equal(t, "Not Found\n", e.Body) assert.Equal(t, "text/plain; charset=utf8", e.Headers["Content-Type"]) } ================================================ FILE: internal/redirect/redirect.go ================================================ // Package redirect provides compiling and matching // redirect and rewrite rules. package redirect import ( "fmt" "regexp" "strings" "github.com/fanyang01/radix" "github.com/pkg/errors" ) // placeholders regexp. var placeholders = regexp.MustCompile(`:(\w+)`) // Rule is a single redirect rule. type Rule struct { Path string `json:"path"` Location string `json:"location"` Status int `json:"status"` Force bool `json:"force"` names map[string]bool dynamic bool sub string path *regexp.Regexp } // URL returns the final destination after substitutions from path. func (r Rule) URL(path string) string { return r.path.ReplaceAllString(path, r.sub) } // IsDynamic returns true if a splat or placeholder is used. func (r *Rule) IsDynamic() bool { return r.dynamic } // IsRewrite returns true if the rule represents a rewrite. func (r *Rule) IsRewrite() bool { return r.Status == 200 || r.Status == 0 } // Compile the rule. func (r *Rule) Compile() { r.path, r.names = compilePath(r.Path) r.sub = compileSub(r.Path, r.Location, r.names) r.dynamic = isDynamic(r.Path) } // Rules map of paths to redirects. type Rules map[string]Rule // Matcher for header lookup. type Matcher struct { t *radix.PatternTrie } // Lookup returns fields for the given path. func (m *Matcher) Lookup(path string) *Rule { v, ok := m.t.Lookup(path) if !ok { return nil } r := v.(Rule) return &r } // Compile the given rules to a trie. func Compile(rules Rules) (*Matcher, error) { t := radix.NewPatternTrie() m := &Matcher{t} for path, rule := range rules { rule.Path = path rule.Compile() t.Add(compilePattern(path), rule) t.Add(compilePattern(path)+"/", rule) } return m, nil } // compileSub returns a substitution string. func compileSub(path, s string, names map[string]bool) string { // splat s = strings.Replace(s, `:splat`, `${splat}`, -1) // placeholders s = placeholders.ReplaceAllStringFunc(s, func(v string) string { name := v[1:] // TODO: refactor to not panic if !names[name] { panic(errors.Errorf("placeholder %q is not present in the path pattern %q", v, path)) } return fmt.Sprintf("${%s}", name) }) return s } // compilePath returns a regexp for substitutions and return // a map of placeholder names for validation. func compilePath(s string) (*regexp.Regexp, map[string]bool) { names := make(map[string]bool) // escape s = regexp.QuoteMeta(s) // splat s = strings.Replace(s, `\*`, `(?P.*?)`, -1) // placeholders s = placeholders.ReplaceAllStringFunc(s, func(v string) string { name := v[1:] names[name] = true return fmt.Sprintf(`(?P<%s>[^/]+)`, name) }) // trailing slash s += `\/?` s = fmt.Sprintf(`^%s$`, s) return regexp.MustCompile(s), names } // compilePattern to a syntax usable by the trie. func compilePattern(s string) string { return placeholders.ReplaceAllString(s, "*") } // isDynamic returns true for splats or placeholders. func isDynamic(s string) bool { return hasPlaceholder(s) || hasSplat(s) } // hasPlaceholder returns true for placeholders func hasPlaceholder(s string) bool { return strings.ContainsRune(s, ':') } // hasSplat returns true for splats. func hasSplat(s string) bool { return strings.ContainsRune(s, '*') } ================================================ FILE: internal/redirect/redirect_test.go ================================================ package redirect import ( "fmt" "testing" "github.com/tj/assert" ) func rule(from, to string) Rule { r := Rule{ Path: from, Location: to, } r.Compile() return r } func TestRule_URL(t *testing.T) { t.Run("exact", func(t *testing.T) { s := rule("/docs", "/help").URL("/docs") assert.Equal(t, "/help", s) }) t.Run("splat one segment", func(t *testing.T) { r := rule("/docs/*", "/help/:splat") assert.Equal(t, "/help/foo", r.URL("/docs/foo")) }) t.Run("splat many segments", func(t *testing.T) { r := rule("/docs/*", "/help/:splat") assert.Equal(t, "/help/foo/bar/baz", r.URL("/docs/foo/bar/baz")) }) t.Run("placeholder", func(t *testing.T) { r := rule("/shop/:brand", "/store/:brand") assert.Equal(t, "/store/apple", r.URL("/shop/apple")) }) t.Run("placeholders", func(t *testing.T) { r := rule("/shop/:brand/category/:cat", "/products/:brand/:cat") assert.Equal(t, "/products/apple/laptops", r.URL("/shop/apple/category/laptops")) }) t.Run("placeholders trailing slash", func(t *testing.T) { r := rule("/docs/:product/guides/:guide", "/help/:product/:guide") assert.Equal(t, "/help/ping/alerting", r.URL("/docs/ping/guides/alerting/")) }) t.Run("placeholders rearranged", func(t *testing.T) { r := rule("/shop/:brand/category/:cat", "/products/:cat/:brand") assert.Equal(t, "/products/laptops/apple", r.URL("/shop/apple/category/laptops")) }) t.Run("placeholders mismatch", func(t *testing.T) { // TODO: sorry :D err := func() (err error) { defer func() { err = recover().(error) }() rule("/shop/:brand/category/:category", "/products/:cat/:brand") return nil }() assert.EqualError(t, err, `placeholder ":cat" is not present in the path pattern "/shop/:brand/category/:category"`) }) } func TestMatcher_Lookup(t *testing.T) { rules := Rules{ "/docs/:product/guides/:guide": Rule{ Location: "/help/:product/:guide", Status: 301, }, "/blog": Rule{ Location: "https://blog.apex.sh", Status: 302, }, "/articles/*": Rule{ Location: "/guides/:splat", }, } m, err := Compile(rules) assert.NoError(t, err, "compile") t.Run("exact", func(t *testing.T) { assert.NotNil(t, m.Lookup("/blog")) }) t.Run("exact trailing slash", func(t *testing.T) { assert.NotNil(t, m.Lookup("/blog/")) }) t.Run("placeholders", func(t *testing.T) { assert.NotNil(t, m.Lookup("/docs/ping/guides/alerts")) }) // TODO: need to fork the trie to be less greedy // t.Run("mismatch", func(t *testing.T) { // r := m.Lookup("/docs/ping/another/guides/alerts") // assert.NotNil(t, r) // }) t.Run("splat one segment", func(t *testing.T) { assert.NotNil(t, m.Lookup("/articles/alerting")) }) t.Run("splat many segments", func(t *testing.T) { assert.NotNil(t, m.Lookup("/articles/alerting/pagerduty")) assert.NotNil(t, m.Lookup("/articles/alerting/pagerduty/")) }) } func BenchmarkMatcher_Lookup(b *testing.B) { rules := Rules{ "/docs/:product/guides/:guide": Rule{ Location: "/help/:product/:guide", Status: 301, }, } m, err := Compile(rules) assert.NoError(b, err, "compile") b.ResetTimer() b.Run("match", func(b *testing.B) { for i := 0; i < b.N; i++ { fmt.Printf("%#v\n", m.Lookup("/docs/ping/guides/alerts")) } }) b.Run("mismatch", func(b *testing.B) { for i := 0; i < b.N; i++ { m.Lookup("/some/other/page") } }) } ================================================ FILE: internal/setup/setup.go ================================================ // Package setup provides up.json initialization. package setup import ( "encoding/json" "errors" "fmt" "io/ioutil" "os" "path/filepath" "sort" "github.com/mitchellh/go-homedir" "github.com/tj/go/term" "github.com/tj/survey" "github.com/apex/up/internal/util" "github.com/apex/up/internal/validate" "github.com/apex/up/platform/aws/regions" ) // ErrNoCredentials is the error returned when no AWS credential profiles are available. var ErrNoCredentials = errors.New("no credentials") // config saved to up.json type config struct { Name string `json:"name"` Profile string `json:"profile"` Regions []string `json:"regions"` } // questions for the user. var questions = []*survey.Question{ { Name: "name", Prompt: &survey.Input{ Message: "Project name:", Default: defaultName(), }, Validate: validateName, }, { Name: "profile", Prompt: &survey.Select{ Message: "AWS profile:", Options: awsProfiles(), Default: os.Getenv("AWS_PROFILE"), PageSize: 10, }, Validate: survey.Required, }, { Name: "region", Prompt: &survey.Select{ Message: "AWS region:", Options: regions.Names, Default: defaultRegion(), PageSize: 15, }, Validate: survey.Required, }, } // Create an up.json file for the user. func Create() error { var in struct { Name string `json:"name"` Profile string `json:"profile"` Region string `json:"region"` } if len(awsProfiles()) == 0 { return ErrNoCredentials } println() // confirm var ok bool err := survey.AskOne(&survey.Confirm{ Message: fmt.Sprintf("No up.json found, create a new project?"), Default: true, }, &ok, nil) if err != nil { return err } if !ok { return errors.New("aborted") } // prompt term.MoveUp(1) term.ClearLine() if err := survey.Ask(questions, &in); err != nil { return err } c := config{ Name: in.Name, Profile: in.Profile, Regions: []string{ regions.GetIdByName(in.Region), }, } b, _ := json.MarshalIndent(c, "", " ") return ioutil.WriteFile("up.json", b, 0644) } // defaultName returns the default app name. // The name is only inferred if it is valid. func defaultName() string { dir, _ := os.Getwd() name := filepath.Base(dir) if validate.Name(name) != nil { return "" } return name } // defaultRegion returns the default aws region. func defaultRegion() string { if s := os.Getenv("AWS_DEFAULT_REGION"); s != "" { return s } if s := os.Getenv("AWS_REGION"); s != "" { return s } return "" } // validateName validates the name prompt. func validateName(v interface{}) error { if err := validate.Name(v.(string)); err != nil { return err } return survey.Required(v) } // awsProfiles returns the AWS profiles found. func awsProfiles() []string { path, err := homedir.Expand("~/.aws/credentials") if err != nil { return nil } f, err := os.Open(path) if err != nil { return nil } defer f.Close() s, err := util.ParseSections(f) if err != nil { return nil } sort.Strings(s) return s } ================================================ FILE: internal/shim/index.js ================================================ const child = require('child_process'); /** * Debug env var. */ const debug = process.env.DEBUG_SHIM; /** * A map of string(id) to callback function, used for when * many concurrent requests are outstanding. */ const callbacks = new Map(); /** * The last id attached to a request / callback pair */ let lastId = (Date.now() / 1000) | 0; /** * nextId generates ids which will only be repeated every 2^52 times being generated */ function nextId(){ // Prevent bugs where integer precision wraps around on floating point numbers // (usually around 52-53 bits) let id = (lastId + 1) | 0; if (id === lastId) { id = 1; } lastId = id; return String(id); } /** * handleLine is responsible for taking a line of output from the child process * and calling the appropiate callbacks. */ function handleLine(line) { if (debug) { console.log('[shim] parsing: `%s`', line); } let msg; try { msg = JSON.parse(line); } catch (err) { console.log('[shim] unexpected non-json line: `%s`', line); return; } if (typeof msg.id !== 'string') { console.log('[shim] unexpected line - do not use stdout: `%s`', line); return; } const c = callbacks.get(msg.id); callbacks.delete(msg.id); if (!c) { if (debug) { console.log('[shim] unexpected duplicate response: `%s`', line); } return; } c(msg.error, msg.value); } /** * Child process for binary I/O. */ const proc = child.spawn('./main', { stdio: ['pipe', 'pipe', process.stderr] }); proc.on('error', function(err){ console.error('[shim] error: %s', err); process.exit(1); }) proc.on('exit', function(code, signal){ console.error('[shim] exit: code=%s signal=%s', code, signal); process.exit(1); }) /** * Newline-delimited JSON stdout. */ // Chunks holds onto partial chunks received in the absense of a newline. // invariant: an array of Buffer objects, all of which do not have any newline characters let chunks = []; const NEWLINE = '\n'.charCodeAt(0); // Find successive newlines in this chunk, and pass them along to `handleChunk` function handleChunk(chunk) { // since this current chunk can have multple lines inside of it // keep track of how much of the current chunk we've consumed let chunkPos = 0; for (;;) { // Find the first newline in the current, in the part of the current chunk we have not // looked yet. const newlinePos = chunk.indexOf(NEWLINE, chunkPos); // We were not able to find any more newline characters in this chunk, // save the remaineder in `chunks` for later processing if (newlinePos === -1) { chunks.push(chunk.slice(chunkPos)); break; } // We have found an end of a whole line, the beginning of the line will be the combination // of all Buffers currently buffered in the `chunks` array (if any) const start = chunk.slice(chunkPos, newlinePos); chunks.push(start); const line = Buffer.concat(chunks); chunks = []; // increase the chunk position, to skip over the last line we just found chunkPos = newlinePos + 1; handleLine(line) } } const out = proc.stdout; out.on('readable', () => { for (;;) { const chunk = out.read(); if (chunk === null) { break; } // Pump all data chunks into chunk handler handleChunk(chunk); } }); /** * Handle events. */ exports.handle = function(event, ctx, cb) { ctx.callbackWaitsForEmptyEventLoop = false; const id = nextId(); callbacks.set(id, cb); proc.stdin.write(JSON.stringify({ "id": id, "event": event, "context": ctx })+'\n'); } ================================================ FILE: internal/shim/shim.go ================================================ //go:generate go-bindata -modtime 0 -pkg shim . // Package shim provides a shim for running arbitrary languages on Lambda. package shim ================================================ FILE: internal/signal/signal.go ================================================ package signal import ( "os" "os/signal" "syscall" "github.com/apex/up/internal/util" ) // close funcs. var fns []Func // Init signals channel func init() { s := make(chan os.Signal, 1) go trap(s) signal.Notify(s, syscall.SIGINT) } // Func is a close function. type Func func() error // Add registers a close handler func. func Add(fn Func) { fns = append(fns, fn) } // trap signals to invoke callbacks and exit. func trap(ch chan os.Signal) { <-ch for _, fn := range fns { if err := fn(); err != nil { util.Fatal(err) } } os.Exit(1) } ================================================ FILE: internal/stats/stats.go ================================================ // Package stats provides CLI analytics. package stats import ( "github.com/apex/log" "github.com/tj/go-cli-analytics" ) // p merged with track calls. var p = map[string]interface{}{} // Client for Segment analytics. var Client = analytics.New(&analytics.Config{ WriteKey: "qnvYCHktBBgACBkQ6V4dzh7aFCe8LF8u", Dir: ".up", }) // Track event `name` with optional `props`. func Track(name string, props map[string]interface{}) { if props == nil { props = map[string]interface{}{} } for k, v := range p { props[k] = v } log.Debugf("track %q %v", name, props) Client.Track(name, props) } // SetProperties sets global properties. func SetProperties(props map[string]interface{}) { p = props } // Flush stats. func Flush() { log.Debug("flushing analytics") if err := Client.Flush(); err != nil { log.WithError(err).Debug("flushing analytics") } log.Debug("flushing analytics") } ================================================ FILE: internal/userconfig/userconfig.go ================================================ // Package userconfig provides user machine-level configuration. package userconfig import ( "encoding/base64" "encoding/json" "io/ioutil" "os" "path/filepath" "github.com/apex/up/internal/util" "github.com/mitchellh/go-homedir" "github.com/pkg/errors" ) var ( // configDir is the dir name where up config is stored relative to HOME. configDir = ".up" // envName is the environment variable which can be used to store // Up's configuration, primarily for continuous integration. envName = "UP_CONFIG" ) // Team is the user configuration for a given team. type Team struct { // ID is the team identifier. ID string `json:"team"` // Email is the user's email. Email string `json:"email"` // Token is the access token. Token string `json:"token"` } // IsPersonal returns true if it is a personal team. func (t *Team) IsPersonal() bool { return t.Email == t.ID } // Config is the user configuration. type Config struct { // Team is the active team. Team string `json:"team"` // Teams is the user's active teams. Teams map[string]*Team `json:"teams"` } // initTeams inits the map. func (c *Config) initTeams() { if c.Teams == nil { c.Teams = make(map[string]*Team) } } // AddTeam adds or replaces the given team. func (c *Config) AddTeam(t *Team) { c.initTeams() c.Teams[t.ID] = t } // GetTeams returns a list of teams. func (c *Config) GetTeams() (teams []*Team) { for _, t := range c.Teams { teams = append(teams, t) } return } // GetTeam returns a team by id or nil func (c *Config) GetTeam(id string) *Team { return c.Teams[id] } // GetActiveTeam returns the active team. func (c *Config) GetActiveTeam() *Team { return c.GetTeam(c.Team) } // Authenticated returns true if the user has an active team. func (c *Config) Authenticated() bool { return c.GetActiveTeam() != nil } // Require requires authentication and returns the active team. func Require() (*Team, error) { var c Config if err := c.Load(); err != nil { return nil, errors.Wrap(err, "loading config") } if !c.Authenticated() { return nil, errors.New("user credentials missing, make sure to `up team login` first") } return c.GetActiveTeam(), nil } // Alter config, loading and saving after manipulation. func Alter(fn func(*Config)) error { var config Config if err := config.Load(); err != nil { return errors.Wrap(err, "loading") } fn(&config) if err := config.Save(); err != nil { return errors.Wrap(err, "saving") } return nil } // Load the configuration. func (c *Config) Load() error { path, err := c.path() if err != nil { return errors.Wrap(err, "getting path") } // env if s := os.Getenv(envName); s != "" { // if not JSON, check for base64 encoding if !util.IsJSON(s) { decoded, err := base64.StdEncoding.DecodeString(s) if err != nil { return errors.Wrap(err, "decoding base64") } s = string(decoded) } if err := json.Unmarshal([]byte(s), &c); err != nil { return errors.Wrap(err, "unmarshaling") } return nil } // file b, err := ioutil.ReadFile(path) if os.IsNotExist(err) { return nil } if err != nil { return errors.Wrap(err, "reading") } if err := json.Unmarshal(b, c); err != nil { return errors.Wrap(err, "unmarshaling") } return nil } // Save the configuration. func (c *Config) Save() error { b, err := json.MarshalIndent(c, "", " ") if err != nil { return errors.Wrap(err, "marshaling") } path, err := c.path() if err != nil { return errors.Wrap(err, "getting path") } if err := ioutil.WriteFile(path, b, 0755); err != nil { return errors.Wrap(err, "writing") } return nil } // path returns the path and sets up dir if necessary. func (c *Config) path() (string, error) { home, err := homedir.Dir() if err != nil { return "", errors.Wrap(err, "homedir") } dir := filepath.Join(home, configDir) if err := os.MkdirAll(dir, 0755); err != nil { return "", errors.Wrap(err, "mkdir") } path := filepath.Join(dir, "config.json") return path, nil } ================================================ FILE: internal/userconfig/userconfig_test.go ================================================ package userconfig import ( "os" "path/filepath" "testing" "github.com/mitchellh/go-homedir" "github.com/tj/assert" ) func init() { configDir = ".up-test" } func TestConfig_file(t *testing.T) { t.Run("load when missing", func(t *testing.T) { dir, _ := homedir.Dir() os.RemoveAll(filepath.Join(dir, configDir)) c := Config{} assert.NoError(t, c.Load(), "load") }) t.Run("save", func(t *testing.T) { c := Config{} assert.NoError(t, c.Load(), "load") assert.Equal(t, "", c.Team) c.Team = "apex" assert.NoError(t, c.Save(), "save") }) t.Run("load after save", func(t *testing.T) { c := Config{} assert.NoError(t, c.Load(), "save") assert.Equal(t, "apex", c.Team) }) } func TestConfig_env(t *testing.T) { t.Run("load", func(t *testing.T) { os.Setenv("UP_CONFIG", `{ "team": "tj@apex.sh" }`) c := Config{} assert.NoError(t, c.Load(), "load") assert.Equal(t, "tj@apex.sh", c.Team) }) } ================================================ FILE: internal/util/util.go ================================================ // Package util haters gonna hate. package util import ( "bufio" "crypto/md5" "encoding/hex" "encoding/json" "fmt" "io" "io/ioutil" "math" "net" "net/http" "net/url" "os" "os/exec" "strings" "syscall" "time" "github.com/apex/up/internal/colors" "github.com/pascaldekloe/name" "github.com/pkg/errors" "github.com/tj/backoff" "github.com/tj/go-progress" "github.com/tj/go/term" "golang.org/x/net/publicsuffix" ) // ClearHeader removes all content header fields. func ClearHeader(h http.Header) { h.Del("Content-Type") h.Del("Content-Length") h.Del("Content-Encoding") h.Del("Content-Range") h.Del("Content-MD5") h.Del("Cache-Control") h.Del("ETag") h.Del("Last-Modified") } // ManagedByUp appends "Managed by Up". func ManagedByUp(s string) string { if s == "" { return "Managed by Up." } return s + " (Managed by Up)." } // Exists returns true if the file exists. func Exists(path string) bool { _, err := os.Stat(path) return err == nil } // ReadFileJSON reads json from the given path. func ReadFileJSON(path string, v interface{}) error { b, err := ioutil.ReadFile(path) if err != nil { return errors.Wrap(err, "reading") } if err := json.Unmarshal(b, &v); err != nil { return errors.Wrap(err, "unmarshaling") } return nil } // Camelcase string with optional args. func Camelcase(s string, v ...interface{}) string { return name.CamelCase(fmt.Sprintf(s, v...), true) } // NewProgressInt with the given total. func NewProgressInt(total int) *progress.Bar { b := progress.NewInt(total) b.Template(`{{.Bar}} {{.Percent | printf "%0.0f"}}% {{.Text}}`) b.Width = 35 b.StartDelimiter = colors.Gray("|") b.EndDelimiter = colors.Gray("|") b.Filled = colors.Purple("█") b.Empty = colors.Gray("░") return b } // NewInlineProgressInt with the given total. func NewInlineProgressInt(total int) *progress.Bar { b := progress.NewInt(total) b.Template(`{{.Bar}} {{.Percent | printf "%0.0f"}}% {{.Text}}`) b.Width = 20 b.StartDelimiter = colors.Gray("|") b.EndDelimiter = colors.Gray("|") b.Filled = colors.Purple("█") b.Empty = colors.Gray(" ") return b } // Pad helper. func Pad() func() { println() return func() { println() } } // Fatal error. func Fatal(err error) { fmt.Fprintf(os.Stderr, "\n %s %s\n\n", colors.Red("Error:"), err) os.Exit(1) } // IsJSON returns true if the string looks like json. func IsJSON(s string) bool { return len(s) > 1 && s[0] == '{' && s[len(s)-1] == '}' } // IsJSONLog returns true if the string looks likes a json log. func IsJSONLog(s string) bool { return IsJSON(s) && strings.Contains(s, `"level"`) } // IsNotFound returns true if err is not nil and represents a missing resource. func IsNotFound(err error) bool { switch { case err == nil: return false case strings.Contains(err.Error(), "ResourceNotFoundException"): return true case strings.Contains(err.Error(), "NoSuchEntity"): return true case strings.Contains(err.Error(), "does not exist"): return true case strings.Contains(err.Error(), "not found"): return true default: return false } } // IsBucketExists returns true if err is not nil and represents an existing bucket. func IsBucketExists(err error) bool { switch { case err == nil: return false case strings.Contains(err.Error(), "BucketAlreadyOwnedByYou"): return true default: return false } } // IsThrottled returns true if err is not nil and represents a throttled request. func IsThrottled(err error) bool { switch { case err == nil: return false case strings.Contains(err.Error(), "Throttling: Rate exceeded"): return true default: return false } } // IsNoCredentials returns true if err is not nil and represents missing credentials. func IsNoCredentials(err error) bool { switch { case err == nil: return false case strings.Contains(err.Error(), "NoCredentialProviders"): return true default: return false } } // Env returns a slice from environment variable map. func Env(m map[string]string) (env []string) { for k, v := range m { env = append(env, fmt.Sprintf("%s=%s", k, v)) } return } // PrefixLines prefixes the lines in s with prefix. func PrefixLines(s string, prefix string) string { lines := strings.Split(s, "\n") for i, l := range lines { lines[i] = prefix + l } return strings.Join(lines, "\n") } // Indent the given string. func Indent(s string) string { return PrefixLines(s, " ") } // WaitForListen blocks until `u` is listening with timeout. func WaitForListen(u *url.URL, timeout time.Duration) error { timedout := time.After(timeout) b := backoff.Backoff{ Min: 100 * time.Millisecond, Max: time.Second, Factor: 1.5, } for { select { case <-timedout: return errors.Errorf("timed out after %s", timeout) case <-time.After(b.Duration()): if IsListening(u) { return nil } } } } // IsListening returns true if there's a server listening on `u`. func IsListening(u *url.URL) bool { conn, err := net.Dial("tcp", u.Host) if err != nil { return false } conn.Close() return true } // ExitStatus returns the exit status of cmd. func ExitStatus(cmd *exec.Cmd, err error) string { ps := cmd.ProcessState if e, ok := err.(*exec.ExitError); ok { ps = e.ProcessState } if ps != nil { s, ok := ps.Sys().(syscall.WaitStatus) if ok { return fmt.Sprintf("%d", s.ExitStatus()) } } return "?" } // StringsContains returns true if list contains s. func StringsContains(list []string, s string) bool { for _, v := range list { if v == s { return true } } return false } // BasePath returns a normalized base path, // stripping the leading '/' if present. func BasePath(s string) string { return strings.TrimLeft(s, "/") } // LogPad outputs a log message with padding. func LogPad(msg string, v ...interface{}) { defer Pad()() Log(msg, v...) } // Log outputs a log message. func Log(msg string, v ...interface{}) { fmt.Printf(" %s\n", colors.Purple(fmt.Sprintf(msg, v...))) } // LogClear clears the line and outputs a log message. func LogClear(msg string, v ...interface{}) { term.MoveUp(1) term.ClearLine() fmt.Printf("\r %s\n", colors.Purple(fmt.Sprintf(msg, v...))) } // LogTitle outputs a log title. func LogTitle(msg string, v ...interface{}) { fmt.Printf("\n \x1b[1m%s\x1b[m\n\n", fmt.Sprintf(msg, v...)) } // LogName outputs a log message with name. func LogName(name, msg string, v ...interface{}) { fmt.Printf(" %s %s\n", colors.Purple(name+":"), fmt.Sprintf(msg, v...)) } // LogListItem outputs a list item. func LogListItem(msg string, v ...interface{}) { fmt.Printf(" • %s\n", fmt.Sprintf(msg, v...)) } // ToFloat returns a float or NaN. func ToFloat(v interface{}) float64 { switch n := v.(type) { case int: return float64(n) case int8: return float64(n) case int16: return float64(n) case int32: return float64(n) case int64: return float64(n) case uint: return float64(n) case uint8: return float64(n) case uint16: return float64(n) case uint32: return float64(n) case uint64: return float64(n) case float32: return float64(n) case float64: return n default: return math.NaN() } } // Milliseconds returns the duration as milliseconds. func Milliseconds(d time.Duration) int { return int(d / time.Millisecond) } // MillisecondsSince returns the duration as milliseconds relative to time t. func MillisecondsSince(t time.Time) int { return int(time.Since(t) / time.Millisecond) } // ParseDuration string with day and month approximation support. func ParseDuration(s string) (d time.Duration, err error) { r := strings.NewReader(s) switch { case strings.HasSuffix(s, "d"): var v float64 _, err = fmt.Fscanf(r, "%fd", &v) d = time.Duration(v * float64(24*time.Hour)) case strings.HasSuffix(s, "w"): var v float64 _, err = fmt.Fscanf(r, "%fw", &v) d = time.Duration(v * float64(24*time.Hour*7)) case strings.HasSuffix(s, "mo"): var v float64 _, err = fmt.Fscanf(r, "%fmo", &v) d = time.Duration(v * float64(30*24*time.Hour)) case strings.HasSuffix(s, "M"): var v float64 _, err = fmt.Fscanf(r, "%fM", &v) d = time.Duration(v * float64(30*24*time.Hour)) default: d, err = time.ParseDuration(s) } return } // Md5 returns an md5 hash for s. func Md5(s string) string { h := md5.New() h.Write([]byte(s)) return hex.EncodeToString(h.Sum(nil)) } // Domain returns the effective domain (TLD plus one). func Domain(s string) string { d, err := publicsuffix.EffectiveTLDPlusOne(s) if err != nil { panic(errors.Wrap(err, "effective domain")) } return d } // CertDomainNames returns the certificate domain name // and alternative names for a requested domain. func CertDomainNames(s string) []string { // effective domain if Domain(s) == s { return []string{s, "*." + s} } // subdomain return []string{RemoveSubdomains(s, 1), "*." + RemoveSubdomains(s, 1)} } // IsWildcardDomain returns true if the domain is a wildcard. func IsWildcardDomain(s string) bool { return strings.HasPrefix(s, "*.") } // WildcardMatches returns true if wildcard is a wildcard domain // and it satisfies the given domain. func WildcardMatches(wildcard, domain string) bool { if !IsWildcardDomain(wildcard) { return false } w := RemoveSubdomains(wildcard, 1) d := RemoveSubdomains(domain, 1) return w == d } // RemoveSubdomains returns the domain without the n left-most subdomain(s). func RemoveSubdomains(s string, n int) string { domains := strings.Split(s, ".") return strings.Join(domains[n:], ".") } // ParseSections returns INI style sections from r. func ParseSections(r io.Reader) (sections []string, err error) { s := bufio.NewScanner(r) for s.Scan() { t := s.Text() if strings.HasPrefix(t, "[") { sections = append(sections, strings.Trim(t, "[]")) } } err = s.Err() return } // UniqueStrings returns a string slice of unique values. func UniqueStrings(s []string) (v []string) { m := make(map[string]struct{}) for _, val := range s { _, ok := m[val] if !ok { v = append(v, val) m[val] = struct{}{} } } return } // IsCI returns true if the env looks like it's a CI platform. func IsCI() bool { return os.Getenv("CI") == "true" } // EncodeAlias encodes an alias string so that it conforms to the // requirement of matching (?!^[0-9]+$)([a-zA-Z0-9-_]+). func EncodeAlias(s string) string { return "commit-" + strings.Replace(s, ".", "_", -1) } // DecodeAlias decodes an alias string which was encoded by // the EncodeAlias function. func DecodeAlias(s string) string { s = strings.Replace(s, "_", ".", -1) s = strings.Replace(s, "commit-", "", 1) return s } // DateSuffix returns the date suffix for t. func DateSuffix(t time.Time) string { switch t.Day() { case 1, 21, 31: return "st" case 2, 22: return "nd" case 3, 23: return "rd" default: return "th" } } // StripLerna strips the owner portion of a Lerna-based tag. See #670 for // details. They are in the form of "@owner/repo@0.5.0". func StripLerna(s string) string { if strings.HasPrefix(s, "@") { p := strings.Split(s, "@") return p[len(p)-1] } return s } // FixMultipleSetCookie staggers the casing of each set-cookie // value to trick API Gateway into setting multiple in the response. func FixMultipleSetCookie(h http.Header) { cookies := h["Set-Cookie"] if len(cookies) == 0 { return } h.Del("Set-Cookie") for i, v := range cookies { h[BinaryCase("set-cookie", i)] = []string{v} } } // BinaryCase ported from https://github.com/Gi60s/binary-case/blob/master/index.js#L86. func BinaryCase(s string, n int) string { var res []rune for _, c := range s { if c >= 65 && c <= 90 { if n&1 > 0 { c += 32 } res = append(res, c) n >>= 1 } else if c >= 97 && c <= 122 { if n&1 > 0 { c -= 32 } res = append(res, c) n >>= 1 } else { res = append(res, c) } } return string(res) } ================================================ FILE: internal/util/util_test.go ================================================ package util import ( "net/http" "os/exec" "strings" "testing" "time" "github.com/tj/assert" ) func TestExitStatus(t *testing.T) { t.Run("success", func(t *testing.T) { cmd := exec.Command("echo", "hello", "world") code := ExitStatus(cmd, cmd.Run()) assert.Equal(t, "0", code) }) t.Run("missing", func(t *testing.T) { cmd := exec.Command("nope") code := ExitStatus(cmd, cmd.Run()) assert.Equal(t, "?", code) }) t.Run("failure", func(t *testing.T) { cmd := exec.Command("sh", "-c", `echo hello && exit 5`) code := ExitStatus(cmd, cmd.Run()) assert.Equal(t, "5", code) }) } func TestParseDuration(t *testing.T) { t.Run("day", func(t *testing.T) { v, err := ParseDuration("1d") assert.NoError(t, err, "parsing") assert.Equal(t, time.Hour*24, v) }) t.Run("day with faction", func(t *testing.T) { v, err := ParseDuration("1.5d") assert.NoError(t, err, "parsing") assert.Equal(t, time.Duration(float64(time.Hour*24)*1.5), v) }) t.Run("week", func(t *testing.T) { v, err := ParseDuration("1w") assert.NoError(t, err, "parsing") assert.Equal(t, time.Hour*24*7, v) v, err = ParseDuration("2w") assert.NoError(t, err, "parsing") assert.Equal(t, time.Hour*24*7*2, v) }) t.Run("month", func(t *testing.T) { v, err := ParseDuration("1mo") assert.NoError(t, err, "parsing") assert.Equal(t, time.Hour*24*30, v) v, err = ParseDuration("1M") assert.NoError(t, err, "parsing") assert.Equal(t, time.Hour*24*30, v) }) t.Run("month with faction", func(t *testing.T) { v, err := ParseDuration("1.5mo") assert.NoError(t, err, "parsing") assert.Equal(t, time.Duration(float64(time.Hour*24*30)*1.5), v) }) t.Run("default", func(t *testing.T) { v, err := ParseDuration("15m") assert.NoError(t, err, "parsing") assert.Equal(t, 15*time.Minute, v) }) } func TestDomain(t *testing.T) { assert.Equal(t, "example.com", Domain("example.com")) assert.Equal(t, "example.com", Domain("api.example.com")) assert.Equal(t, "example.com", Domain("v1.api.example.com")) assert.Equal(t, "example.co.uk", Domain("example.co.uk")) assert.Equal(t, "example.co.uk", Domain("api.example.co.uk")) assert.Equal(t, "example.co.uk", Domain("v1.api.example.co.uk")) } func TestCertDomainNames(t *testing.T) { assert.Equal(t, []string{"example.com", "*.example.com"}, CertDomainNames("example.com")) assert.Equal(t, []string{"example.com", "*.example.com"}, CertDomainNames("api.example.com")) assert.Equal(t, []string{"api.example.com", "*.api.example.com"}, CertDomainNames("v1.api.example.com")) } func TestWildcardMatches(t *testing.T) { assert.True(t, WildcardMatches("*.api.example.com", "v1.api.example.com")) assert.True(t, WildcardMatches("*.example.com", "api.example.com")) assert.False(t, WildcardMatches("example.com", "api.example.com")) assert.False(t, WildcardMatches("*.api.example.com", "api.example.com")) } func TestParseSections(t *testing.T) { r := strings.NewReader(`[personal] aws_access_key_id = personal_key aws_secret_access_key = personal_secret [app] aws_access_key_id = app_key aws_secret_access_key = app_secret [foo_bar] aws_access_key_id = foo_bar_key aws_secret_access_key = foo_bar_secret `) v, err := ParseSections(r) assert.NoError(t, err) assert.Equal(t, []string{"personal", "app", "foo_bar"}, v) } func TestEncodeAlias(t *testing.T) { assert.Equal(t, `commit-v1_2_3-beta`, EncodeAlias(`v1.2.3-beta`)) } func TestDecodeAlias(t *testing.T) { assert.Equal(t, `v1.2.3-beta`, DecodeAlias(EncodeAlias(`v1.2.3-beta`))) } func TestFixMultipleSetCookie(t *testing.T) { h := http.Header{} h.Add("Set-Cookie", "first=tj") h.Add("Set-Cookie", "last=holowaychuk") h.Add("set-cookie", "pet=tobi") FixMultipleSetCookie(h) assert.Len(t, h, 3) assert.Equal(t, []string{"last=holowaychuk"}, h["Set-cookie"]) assert.Equal(t, []string{"pet=tobi"}, h["sEt-cookie"]) assert.Equal(t, []string{"first=tj"}, h["set-cookie"]) } func TestBinaryCase(t *testing.T) { var variations []string // create variations for i := 0; i < 50; i++ { variations = append(variations, BinaryCase("set-cookie", i)) } // ensure none are malformed for _, v := range variations { assert.Equal(t, "set-cookie", strings.ToLower(v)) } // ensure none are duplicates for i, a := range variations { for j, b := range variations { if i != j { assert.NotEqual(t, a, b) } } } } ================================================ FILE: internal/validate/validate.go ================================================ // Package validate provides config validation functions. package validate import ( "regexp" "strings" "github.com/pkg/errors" ) // RequiredString validation. func RequiredString(s string) error { if strings.TrimSpace(s) == "" { return errors.New("is required") } return nil } // RequiredStrings validation. func RequiredStrings(s []string) error { for i, v := range s { if err := RequiredString(v); err != nil { return errors.Wrapf(err, "at index %d", i) } } return nil } // MinStrings validation. func MinStrings(s []string, n int) error { if len(s) < n { if n == 1 { return errors.Errorf("must have at least %d value", n) } return errors.Errorf("must have at least %d values", n) } return nil } // name regexp. var name = regexp.MustCompile(`^[a-z][-a-z0-9]*$`) // Name validation. func Name(s string) error { if !name.MatchString(s) { return errors.Errorf("must contain only lowercase alphanumeric characters and '-'") } return nil } // stage regexp. var stage = regexp.MustCompile(`^[a-zA-Z0-9_]+$`) // Stage name validation. func Stage(s string) error { if !stage.MatchString(s) { return errors.Errorf("must contain only alphanumeric characters and '_'") } return nil } // List validation. func List(s string, list []string) error { for _, v := range list { if s == v { return nil } } return errors.Errorf("%q is invalid, must be one of:\n\n • %s", s, strings.Join(list, "\n • ")) } // Lists validation. func Lists(vals, list []string) error { for _, v := range vals { if err := List(v, list); err != nil { return err } } return nil } ================================================ FILE: internal/zip/testdata/.file ================================================ 👻 ================================================ FILE: internal/zip/testdata/.upignore ================================================ *.md ================================================ FILE: internal/zip/testdata/Readme.md ================================================ Hello World ================================================ FILE: internal/zip/testdata/bar.js ================================================ bar ================================================ FILE: internal/zip/testdata/foo.js ================================================ foo ================================================ FILE: internal/zip/testdata/index.js ================================================ module.exports = 'hello world' ================================================ FILE: internal/zip/zip.go ================================================ package zip import ( "bytes" "io" "io/ioutil" "os" "strings" "github.com/pkg/errors" archive "github.com/tj/go-archive" ) var transform = archive.TransformFunc(func(r io.Reader, i os.FileInfo) (io.Reader, os.FileInfo) { name := strings.Replace(i.Name(), "\\", "/", -1) i = archive.Info{ Name: name, Size: i.Size(), Mode: i.Mode() | 0555, Modified: i.ModTime(), Dir: i.IsDir(), }.FileInfo() return r, i }) // Build the given `dir`. func Build(dir string) (io.ReadCloser, *archive.Stats, error) { upignore, err := read(".upignore") if err != nil { return nil, nil, errors.Wrap(err, "reading .upignore") } defer upignore.Close() r := io.MultiReader( strings.NewReader(".*\n"), strings.NewReader("\n!vendor\n!node_modules/**\n!.pypath/**\n"), upignore, strings.NewReader("\n!main\n!server\n!_proxy.js\n!up.json\n!pom.xml\n!build.gradle\n!project.clj\ngin-bin\nup\n")) filter, err := archive.FilterPatterns(r) if err != nil { return nil, nil, errors.Wrap(err, "parsing ignore patterns") } buf := new(bytes.Buffer) zip := archive.NewZip(buf). WithFilter(filter). WithTransform(transform) if err := zip.Open(); err != nil { return nil, nil, errors.Wrap(err, "opening") } if err := zip.AddDir(dir); err != nil { return nil, nil, errors.Wrap(err, "adding dir") } if err := zip.Close(); err != nil { return nil, nil, errors.Wrap(err, "closing") } return ioutil.NopCloser(buf), zip.Stats(), nil } // read file. func read(path string) (io.ReadCloser, error) { f, err := os.Open(path) if os.IsNotExist(err) { return ioutil.NopCloser(bytes.NewReader(nil)), nil } if err != nil { return nil, err } return f, nil } ================================================ FILE: internal/zip/zip_test.go ================================================ package zip import ( "io" "io/ioutil" "os" "os/exec" "path/filepath" "sort" "testing" "github.com/tj/assert" ) // TODO: better tests func TestBuild(t *testing.T) { os.Chdir("testdata") defer os.Chdir("..") zip, _, err := Build(".") assert.NoError(t, err) out, err := ioutil.TempDir(os.TempDir(), "-up") assert.NoError(t, err, "tmpdir") dst := filepath.Join(out, "out.zip") f, err := os.Create(dst) assert.NoError(t, err, "create") _, err = io.Copy(f, zip) assert.NoError(t, err, "copy") assert.NoError(t, f.Close(), "close") cmd := exec.Command("unzip", "out.zip") cmd.Dir = out assert.NoError(t, cmd.Run(), "unzip") files, err := ioutil.ReadDir(out) assert.NoError(t, err, "readdir") var names []string for _, f := range files { names = append(names, f.Name()) } sort.Strings(names) assert.Equal(t, []string{"bar.js", "foo.js", "index.js", "out.zip"}, names) } ================================================ FILE: platform/aws/cost/cost.go ================================================ // Package cost provides utilities for calculating AWS Lambda pricing. package cost // pricePerInvoke is the cost per function invocation. var pricePerInvoke = 0.0000002 // pricePerRequestUnit is the cost per api gateway request unit. var pricePerRequestUnit = 5 // requestUnit is 5 million requests. var requestUnit = 5e6 // memoryConfigurations available. var memoryConfigurations = map[int]float64{ 128: 0.000000208, 192: 0.000000313, 256: 0.000000417, 320: 0.000000521, 384: 0.000000625, 448: 0.000000729, 512: 0.000000834, 576: 0.000000938, 640: 0.000001042, 704: 0.000001146, 768: 0.00000125, 832: 0.000001354, 896: 0.000001459, 960: 0.000001563, 1024: 0.000001667, 1088: 0.000001771, 1152: 0.000001875, 1216: 0.00000198, 1280: 0.000002084, 1344: 0.000002188, 1408: 0.000002292, 1472: 0.000002396, 1536: 0.000002501, 1600: 0.000002605, 1664: 0.000002709, 1728: 0.000002813, 1792: 0.000002917, 1856: 0.000003021, 1920: 0.000003126, 1984: 0.000003230, 2048: 0.000003334, 2112: 0.000003438, 2176: 0.000003542, 2240: 0.000003647, 2304: 0.000003751, 2368: 0.000003855, 2432: 0.000003959, 2496: 0.000004063, 2560: 0.000004168, 2624: 0.000004272, 2688: 0.000004376, 2752: 0.000004480, 2816: 0.000004584, 2880: 0.000004688, 2944: 0.000004793, 3008: 0.000004897, } // Requests returns the cost for the given number of http requests. func Requests(n int) float64 { return (float64(n) / float64(requestUnit)) * float64(pricePerRequestUnit) } // Rate returns the cost per 100ms for the given `memory` configuration in megabytes. func Rate(memory int) float64 { return memoryConfigurations[memory] } // Invocations returns the cost of `n` requests. func Invocations(n int) float64 { return pricePerInvoke * float64(n) } // Duration returns the cost of `ms` for the given `memory` configuration in megabytes. func Duration(ms, memory int) float64 { return Rate(memory) * (float64(ms) / 100) } ================================================ FILE: platform/aws/cost/cost_test.go ================================================ package cost import ( "testing" "github.com/tj/assert" ) func TestRequests(t *testing.T) { table := []struct { requests int expected float64 }{ {0, 0.0}, {1000, 0.001}, {1000000, 1.0}, } for _, row := range table { assert.Equal(t, row.expected, Requests(row.requests)) } } func TestRate(t *testing.T) { table := []struct { memory int expected float64 }{ {-1, 0.0}, {0, 0.0}, {128, 2.08e-7}, {156, 0.0}, } for _, row := range table { assert.Equal(t, row.expected, Rate(row.memory)) } } func TestInvocations(t *testing.T) { table := []struct { invocations int expected float64 }{ {0, 0.0}, {1, 2.0e-7}, {1.0e7, 2.0}, } for _, row := range table { assert.Equal(t, row.expected, Invocations(row.invocations)) } } func TestDuration(t *testing.T) { table := []struct { duration int memory int expected float64 }{ {0, 128, 0}, {100000, 256, 4.17e-4}, {1e8, 1536, 2.501}, } for _, row := range table { assert.Equal(t, row.expected, Duration(row.duration, row.memory)) } } ================================================ FILE: platform/aws/cost/domains.go ================================================ package cost import ( "encoding/csv" "strings" "github.com/apex/log" ) // tlds is a map of tlds to price. var tlds = map[string]string{} // Domain returns the price of domain's tld. func Domain(domain string) string { return TLD(strings.SplitN(domain, ".", 2)[1]) } // TLD returns the price of a tld. func TLD(tld string) string { return tlds[tld] } // priceList is a raw CSV price list. var priceList = ` ac,$48.00 academy,$32.00 accountants,$94.00 adult,$88.00 agency,$19.00 apartments,$47.00 associates,$29.00 auction,$29.00 audio,$13.00 band,$22.00 bargains,$30.00 be,$9.00 berlin,$52.00 bike,$32.00 bingo,$47.00 biz,$12.00 black,$43.00 blue,$15.00 boutique,$30.00 builders,$32.00 business,$18.00 buzz,$37.00 ca,$13.00 cab,$32.00 cafe,$31.00 camera,$46.00 camp,$46.00 capital,$47.00 cards,$29.00 care,$29.00 careers,$35.00 cash,$29.00 casino,$141.00 catering,$29.00 cc,$12.00 center,$21.00 ceo,$74.00 ch,$12.00 chat,$29.00 cheap,$30.00 church,$29.00 city,$19.00 cl,$93.00* cloud,$23.00 claims,$47.00 cleaning,$46.00 click,$7.00 clinic,$47.00 clothing,$32.00 club,$12.00 co,$25.00 co.nz,$24.00 co.uk,$9.00 co.za,$13.00 coach,$47.00 codes,$35.00 coffee,$32.00 college,$69.00 com,$12.00 com.ar,$76.00* com.au,$15.00 com.br,$58.00* com.mx,$34.00 com.sg,$47.00* community,$29.00 company,$18.00 computer,$32.00 condos,$49.00 construction,$32.00 consulting,$29.00 contractors,$32.00 cool,$30.00 coupons,$51.00 credit,$94.00 creditcard,$141.00 cruises,$49.00 dance,$22.00 dating,$49.00 de,$9.00 deals,$29.00 delivery,$47.00 democrat,$30.00 dental,$47.00 diamonds,$35.00 diet,$19.00 digital,$29.00 direct,$29.00 directory,$21.00 discount,$29.00 dog,$46.00 domains,$32.00 education,$21.00 email,$25.00 energy,$94.00 engineering,$47.00 enterprises,$32.00 equipment,$21.00 es,$10.00 estate,$32.00 eu,$13.00 events,$30.00 exchange,$29.00 expert,$49.00 exposed,$19.00 express,$31.00 fail,$29.00 farm,$32.00 fi,$24.00 finance,$47.00 financial,$47.00 fish,$29.00 fitness,$29.00 flights,$49.00 florist,$32.00 flowers,$25.00 fm,$92.00 football,$19.00 forsale,$29.00 foundation,$30.00 fr,$12.00 fund,$47.00 furniture,$47.00 futbol,$12.00 fyi,$20.00 gallery,$21.00 gg,$75.00 gift,$20.00 gifts,$19.00 glass,$46.00 global,$71.00 gold,$101.00 golf,$51.00 graphics,$21.00 gratis,$19.00 green,$71.00 gripe,$29.00 guide,$29.00 guitars,$30.00 guru,$25.00 haus,$29.00 healthcare,$47.00 help,$19.00 hiv,$254.00 hockey,$51.00 holdings,$35.00 holiday,$35.00 host,$93.00 hosting,$29.00 house,$32.00 im,$19.00 immo,$29.00 immobilien,$30.00 in,$15.00 industries,$29.00 info,$12.00 ink,$29.00 institute,$21.00 insure,$47.00 international,$21.00 investments,$94.00 io,$39.00 irish,$36.00 it,$15.00 jewelry,$51.00 jp,$90.00 juegos,$13.00 kaufen,$30.00 kim,$15.00 kitchen,$46.00 kiwi,$32.00 land,$32.00 lease,$47.00 legal,$47.00 lgbt,$43.00 life,$29.00 lighting,$21.00 limited,$29.00 limo,$35.00 link,$10.00 live,$23.00 loan,$31.00 loans,$94.00 lol,$31.00 maison,$49.00 management,$21.00 marketing,$32.00 mba,$31.00 me,$17.00 me.uk,$8.00 media,$29.00 memorial,$47.00 mobi,$12.00 moda,$22.00 money,$29.00 mortgage,$43.00 movie,$306.00 mx,$34.00 name,$9.00 net,$11.00 net.au,$15.00 net.nz,$24.00 network,$19.00 news,$23.00 ninja,$18.00 nl,$10.00 onl,$15.00 online,$39.00 org,$12.00 org.nz,$24.00 org.uk,$9.00 partners,$49.00 parts,$29.00 photo,$30.00 photography,$21.00 photos,$21.00 pics,$20.00 pictures,$10.00 pink,$15.00 pizza,$47.00 place,$29.00 plumbing,$46.00 plus,$31.00 poker,$43.00 porn,$88.00 pro,$14.00 productions,$30.00 properties,$30.00 property,$29.00 pub,$22.00 qa,$64.00* qpon,$15.00 recipes,$35.00 red,$15.00 reise,$101.00 reisen,$19.00 rentals,$30.00 repair,$32.00 report,$19.00 republican,$29.00 restaurant,$47.00 reviews,$22.00 rip,$17.00 rocks,$12.00 ru,$36.00 ruhr,$30.00 run,$20.00 sale,$29.00 sarl,$29.00 school,$29.00 schule,$19.00 se,$23.00 services,$29.00 sex,$95.00 sexy,$22.00 sg,$47.00* sh,$48.00 shiksha,$16.00 shoes,$46.00 show,$31.00 singles,$30.00 soccer,$20.00 social,$32.00 solar,$46.00 solutions,$25.00 studio,$23.00 style,$29.00 sucks,$282.00 supplies,$19.00 supply,$19.00 support,$21.00 surgery,$47.00 systems,$21.00 tattoo,$30.00 tax,$47.00 taxi,$51.00 team,$31.00 technology,$21.00 tennis,$47.00 theater,$51.00 tienda,$50.00 tips,$21.00 tires,$94.00 today,$21.00 tools,$29.00 tours,$51.00 town,$29.00 toys,$46.00 trade,$29.00 training,$27.00 tv,$32.00 uk,$9.00 university,$47.00 uno,$30.00 us,$15.00 vacations,$35.00 vc,$33.00 vegas,$57.00 ventures,$35.00 vg,$35.00 viajes,$49.00 video,$22.00 villas,$35.00 vision,$29.00 voyage,$50.00 watch,$37.00 website,$23.00 wien,$29.00 wiki,$30.00 works,$30.00 world,$29.00 wtf,$29.00 xyz,$12.00 zone,$32.00 ` func init() { r := csv.NewReader(strings.NewReader(priceList)) rows, err := r.ReadAll() if err != nil { log.WithError(err).Debug("reading price list") return } for _, row := range rows { name := row[0] price := row[1] tlds[name] = price } } ================================================ FILE: platform/aws/domains/domains.go ================================================ // Package domains provides domain management for AWS platforms. package domains import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" r "github.com/aws/aws-sdk-go/service/route53domains" "github.com/apex/up" ) // Domains implementation. type Domains struct { client *r.Route53Domains } // New returns a new domain manager. func New() *Domains { return &Domains{ client: r.New(session.New(aws.NewConfig().WithRegion("us-east-1"))), } } // List implementation. func (d *Domains) List() (v []*up.Domain, err error) { res, err := d.client.ListDomains(&r.ListDomainsInput{ MaxItems: aws.Int64(100), }) if err != nil { return } for _, d := range res.Domains { v = append(v, &up.Domain{ Name: *d.DomainName, Expiry: *d.Expiry, AutoRenew: *d.AutoRenew, }) } return } // Availability implementation. func (d *Domains) Availability(domain string) (*up.Domain, error) { res, err := d.client.CheckDomainAvailability(&r.CheckDomainAvailabilityInput{ DomainName: &domain, }) if err != nil { return nil, err } if *res.Availability == "AVAILABLE" { return &up.Domain{ Name: domain, Available: true, }, nil } return &up.Domain{ Name: domain, Available: false, }, nil } // Suggestions implementation. func (d *Domains) Suggestions(domain string) (domains []*up.Domain, err error) { res, err := d.client.GetDomainSuggestions(&r.GetDomainSuggestionsInput{ DomainName: &domain, OnlyAvailable: aws.Bool(true), SuggestionCount: aws.Int64(15), }) if err != nil { return } for _, s := range res.SuggestionsList { domains = append(domains, &up.Domain{ Name: *s.DomainName, Available: true, }) } return } // Purchase implementation. func (d *Domains) Purchase(domain string, contact up.DomainContact) error { _, err := d.client.RegisterDomain(&r.RegisterDomainInput{ DomainName: &domain, AutoRenew: aws.Bool(true), DurationInYears: aws.Int64(1), RegistrantContact: contactDetails(contact), AdminContact: contactDetails(contact), TechContact: contactDetails(contact), }) return err } // contactDetails returns route53 contact details. func contactDetails(c up.DomainContact) *r.ContactDetail { return &r.ContactDetail{ AddressLine1: aws.String(c.Address), City: aws.String(c.City), State: aws.String(c.State), ZipCode: aws.String(c.ZipCode), CountryCode: aws.String(c.CountryCode), Email: aws.String(c.Email), PhoneNumber: aws.String(c.PhoneNumber), FirstName: aws.String(c.FirstName), LastName: aws.String(c.LastName), ContactType: aws.String("PERSON"), } } ================================================ FILE: platform/aws/logs/logs.go ================================================ // Package logs provides log management for AWS platforms. package logs import ( "encoding/json" "io" "os" "strings" "time" "github.com/apex/log" jsonlog "github.com/apex/log/handlers/json" "github.com/apex/up" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/tj/aws/logs" "github.com/apex/up/internal/logs/parser" "github.com/apex/up/internal/logs/text" "github.com/apex/up/internal/util" ) // Logs implementation. type Logs struct { up.LogsConfig group string query string w io.WriteCloser io.Reader } // New log tailer. func New(group string, c up.LogsConfig) up.Logs { r, w := io.Pipe() query, err := parseQuery(c.Query) if err != nil { w.CloseWithError(err) } log.Debugf("query %q", query) l := &Logs{ LogsConfig: c, query: query, group: group, Reader: r, w: w, } go l.start() return l } // start fetching logs. func (l *Logs) start() { tailer := logs.New(logs.Config{ Service: cloudwatchlogs.New(session.New(aws.NewConfig().WithRegion(l.Region))), StartTime: l.Since, PollInterval: 2 * time.Second, Follow: l.Follow, FilterPattern: l.query, GroupNames: []string{l.group}, }) var handler log.Handler if l.OutputJSON { handler = jsonlog.New(os.Stdout) } else { handler = text.New(os.Stdout).WithExpandedFields(l.Expand) } // TODO: transform to reader of nl-delimited json, move to apex/log? // TODO: marshal/unmarshal as JSON so that numeric values are always float64... remove util.ToFloat() for l := range tailer.Start() { line := strings.TrimSpace(l.Message) // json log if util.IsJSONLog(line) { var e log.Entry err := json.Unmarshal([]byte(line), &e) if err != nil { log.Fatalf("error parsing json: %s", err) } handler.HandleLog(&e) continue } // skip START / END logs since they are redundant if skippable(l.Message) { continue } // lambda textual logs handler.HandleLog(&log.Entry{ Timestamp: l.Timestamp, Level: log.InfoLevel, Message: strings.TrimRight(l.Message, " \n"), }) } // TODO: refactor interface to delegate if err := tailer.Err(); err != nil { panic(err) } l.w.Close() } // parseQuery parses and converts the query to a CW friendly syntax. func parseQuery(s string) (string, error) { if s == "" { return s, nil } n, err := parser.Parse(s) if err != nil { return "", err } return n.String(), nil } // skippable returns true if the message is skippable. func skippable(s string) bool { return strings.Contains(s, "END RequestId") || strings.Contains(s, "START RequestId") } ================================================ FILE: platform/aws/regions/regions.go ================================================ // Package regions provides AWS region utilities. package regions import ( "fmt" "path/filepath" ) // hostedZoneIDs is a set of hosted zone ids for API Gateway. var hostedZoneIDs = map[string]string{ "us-east-2": "ZOJJZC49E0EPZ", "us-east-1": "Z1UJRXOUMOOFQ8", "us-west-1": "Z2MUQ32089INYE", "us-west-2": "Z2OJLYMUO9EFXC", "ap-east-1": "Z3FD1VL90ND7K5", "ap-south-1": "Z3VO1THU9YC4UR", "ap-northeast-3": "Z2YQB5RD63NC85", "ap-northeast-2": "Z20JF4UZKIW1U8", "ap-southeast-1": "ZL327KTPIQFUL", "ap-southeast-2": "Z2RPCDW04V8134", "ap-northeast-1": "Z1YSHQZHG15GKL", "ca-central-1": "Z19DQILCV0OWEC", "eu-central-1": "Z1U9ULNL0V5AJ3", "eu-west-1": "ZLY8HYME6SFDD", "eu-west-2": "ZJ5UAJN8Y3Z2Q", "eu-west-3": "Z3KY65QIEKYHQQ", "eu-north-1": "Z2YB950C88HT6D", "sa-east-1": "ZCMLWB8V5SYIT", } // IDs of regions. var IDs = []string{ "us-east-2", "us-east-1", "us-west-1", "us-west-2", "ap-east-1", "ap-south-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ca-central-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3", "eu-north-1", "sa-east-1", } // Names of regions. var Names = []string{ "US East (Ohio)", "US East (N. Virginia)", "US West (N. California)", "US West (Oregon)", "Asia Pacific (Hong Kong)", "Asia Pacific (Mumbai)", "Asia Pacific (Seoul)", "Asia Pacific (Singapore)", "Asia Pacific (Sydney)", "Asia Pacific (Tokyo)", "Canada (Central)", "EU (Frankfurt)", "EU (Ireland)", "EU (London)", "EU (Paris)", "EU (Stockholm)", "South America (São Paulo)", } // Match returns regions matching the pattern(s) provided. Patterns // which are not "expanded" are returned as-is. func Match(regions []string) (v []string) { for _, pattern := range regions { matched := false for _, id := range IDs { if ok, _ := filepath.Match(pattern, id); ok { v = append(v, id) matched = true } } if !matched { v = append(v, pattern) } } return } // GetIdByName returns a region id by name. func GetIdByName(name string) string { for i, n := range Names { if n == name { return IDs[i] } } return "" } // GetHostedZoneID returns a hosted zone id by region. func GetHostedZoneID(region string) string { id, ok := hostedZoneIDs[region] if !ok { panic(fmt.Sprintf("region %q is not yet supported", region)) } return id } ================================================ FILE: platform/aws/regions/regions_test.go ================================================ package regions import ( "testing" "github.com/tj/assert" ) func TestMatch(t *testing.T) { t.Run("explicit", func(t *testing.T) { v := Match([]string{"us-west-2", "us-east-1"}) assert.Equal(t, []string{"us-west-2", "us-east-1"}, v) }) t.Run("glob all", func(t *testing.T) { v := Match([]string{"*"}) assert.Equal(t, IDs, v) }) t.Run("glob some", func(t *testing.T) { v := Match([]string{"us-west-*", "ca-*"}) e := []string{"us-west-1", "us-west-2", "ca-central-1"} assert.Equal(t, e, v) }) } ================================================ FILE: platform/aws/runtime/runtime.go ================================================ package runtime import ( "os" "github.com/apex/log" "github.com/apex/up" ) // Runtime implementation. type Runtime struct { config *up.Config log log.Interface } // Option function. type Option func(*Runtime) // New with the given options. func New(c *up.Config, options ...Option) *Runtime { var v Runtime v.config = c v.log = log.Log for _, o := range options { o(&v) } return &v } // WithLogger option. func WithLogger(l log.Interface) Option { return func(v *Runtime) { v.log = l } } // Init implementation. func (r *Runtime) Init(stage string) error { os.Setenv("UP_STAGE", stage) if os.Getenv("NODE_ENV") == "" { os.Setenv("NODE_ENV", stage) } return nil } ================================================ FILE: platform/event/event.go ================================================ // Package event provides an evented mechanism for hooking into platform specifics. // // This is necessary as not all platforms have identical capabilities, // so the reporting output (among other things) may differ // slightly. package event import ( "fmt" "strings" "time" "github.com/apex/log" ) // Events channel. type Events chan *Event // Emit an event. func (e Events) Emit(name string, fields Fields) { if !strings.Contains(name, ".event") { log.Debugf("event %s %v", name, fields) } e <- &Event{ Name: name, Fields: fields, } } // Time an event. func (e Events) Time(name string, fields Fields) func() { start := time.Now() e.Emit(name, fields) return func() { if fields == nil { fields = make(Fields) } f := make(Fields) for k, v := range fields { f[k] = v } f["duration"] = time.Since(start) e.Emit(name+".complete", f) } } // Fields for an event. type Fields map[string]interface{} // Event is a representation of an operation performed // by a platform, and is used for reporting. type Event struct { Name string Fields Fields } // Strings value. func (e *Event) Strings(name string) []string { v, ok := e.Fields[name].([]string) if !ok { panic(fmt.Errorf("%#v field %s is not []string", e, name)) } return v } // String value. func (e *Event) String(name string) string { v, ok := e.Fields[name].(string) if !ok { panic(fmt.Errorf("%#v field %s is not a string", e, name)) } return v } // Duration value. func (e *Event) Duration(name string) time.Duration { v, ok := e.Fields[name].(time.Duration) if !ok { panic(fmt.Errorf("%#v field %s is not a time.Duration", e, name)) } return v } // Int64 value. func (e *Event) Int64(name string) int64 { v, ok := e.Fields[name].(int64) if !ok { panic(fmt.Errorf("%#v field %s is not a int64", e, name)) } return v } // Int value. func (e *Event) Int(name string) int { v, ok := e.Fields[name].(int) if !ok { panic(fmt.Errorf("%#v field %s is not a int", e, name)) } return v } ================================================ FILE: platform/lambda/lambda.go ================================================ // Package lambda implements the API Gateway & AWS Lambda platform. package lambda import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "os" "strings" "sync" "time" "github.com/apex/log" "github.com/apex/log/handlers/discard" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/acm" "github.com/aws/aws-sdk-go/service/apigateway" "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/lambda" "github.com/aws/aws-sdk-go/service/route53" "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3/s3manager" "github.com/dchest/uniuri" humanize "github.com/dustin/go-humanize" "github.com/golang/sync/errgroup" "github.com/pkg/errors" "github.com/apex/up" "github.com/apex/up/config" "github.com/apex/up/internal/proxy/bin" "github.com/apex/up/internal/shim" "github.com/apex/up/internal/util" "github.com/apex/up/internal/zip" "github.com/apex/up/platform/aws/domains" "github.com/apex/up/platform/aws/logs" "github.com/apex/up/platform/aws/runtime" "github.com/apex/up/platform/event" "github.com/apex/up/platform/lambda/stack" "github.com/apex/up/platform/lambda/stack/resources" ) // errFirstDeploy is returned from .deploy() when a function is created. var errFirstDeploy = errors.New("first deploy") const ( // maxCodeSize is the max code size supported by Lambda (250MiB). maxCodeSize = 250 << 20 ) // assume policy for the lambda function. var apiGatewayAssumePolicy = `{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "apigateway.amazonaws.com" }, "Action": "sts:AssumeRole" }, { "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }` // TODO: aggregate progress report for N regions or distinct progress bars // TODO: refactor with another region-scoped struct to clean this up // Platform implementation. type Platform struct { config *up.Config handler string zip *bytes.Buffer events event.Events } // New platform. func New(c *up.Config, events event.Events) *Platform { return &Platform{ config: c, handler: "_proxy.handle", events: events, } } // Build implementation. func (p *Platform) Build() error { start := time.Now() p.zip = new(bytes.Buffer) if err := p.injectProxy(); err != nil { return errors.Wrap(err, "injecting proxy") } defer p.removeProxy() r, stats, err := zip.Build(".") if err != nil { return errors.Wrap(err, "zip") } if _, err := io.Copy(p.zip, r); err != nil { return errors.Wrap(err, "copying") } if err := r.Close(); err != nil { return errors.Wrap(err, "closing") } p.events.Emit("platform.build.zip", event.Fields{ "files": stats.FilesAdded, "size_uncompressed": stats.SizeUncompressed, "size_compressed": p.zip.Len(), "duration": time.Since(start), }) if stats.SizeUncompressed > maxCodeSize { size := humanize.Bytes(uint64(stats.SizeUncompressed)) max := humanize.Bytes(uint64(maxCodeSize)) return errors.Errorf("zip contents is %s, exceeding Lambda's limit of %s", size, max) } return nil } // Zip returns the zip reader. func (p *Platform) Zip() io.Reader { return p.zip } // Init initializes the runtime. func (p *Platform) Init(stage string) error { return runtime.New( p.config, runtime.WithLogger(&log.Logger{ Handler: discard.Default, }), ).Init(stage) } // Deploy implementation. func (p *Platform) Deploy(d up.Deploy) error { regions := p.config.Regions var g errgroup.Group if err := p.createRole(); err != nil { return errors.Wrap(err, "iam") } for _, r := range regions { region := r g.Go(func() error { version, err := p.deploy(region, d) if err == nil { goto endpoint } if err != errFirstDeploy { return errors.Wrap(err, region) } if err := p.CreateStack(region, version); err != nil { return errors.Wrap(err, region) } endpoint: url, err := p.URL(region, d.Stage) if err != nil { return errors.Wrap(err, "fetching url") } p.events.Emit("platform.deploy.url", event.Fields{ "url": url, }) return nil }) } return g.Wait() } // Logs implementation. func (p *Platform) Logs(c up.LogsConfig) up.Logs { g := "/aws/lambda/" + p.config.Name return logs.New(g, c) } // Domains implementation. func (p *Platform) Domains() up.Domains { return domains.New() } // URL returns the stage url. func (p *Platform) URL(region, stage string) (string, error) { s := session.New(aws.NewConfig().WithRegion(region)) c := apigateway.New(s) api, err := p.getAPI(c) if err != nil { return "", errors.Wrap(err, "fetching api") } if api == nil { return "", errors.Errorf("cannot find the API, looks like you haven't deployed") } id := fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/%s/", *api.Id, region, stage) return id, nil } // CreateStack implementation. func (p *Platform) CreateStack(region, version string) error { versions := make(resources.Versions) for _, s := range p.config.Stages { versions[s.Name] = version } if err := p.createCerts(); err != nil { return errors.Wrap(err, "creating certs") } zones, err := p.getHostedZone() if err != nil { return errors.Wrap(err, "fetching zones") } return stack.New(p.config, p.events, zones, region).Create(versions) } // DeleteStack implementation. func (p *Platform) DeleteStack(region string, wait bool) error { versions := resources.Versions{} for _, s := range p.config.Stages { versions[s.Name] = "1" } if err := p.createRole(); err != nil { return errors.Wrap(err, "creating iam role") } log.Debug("deleting bucket objects") if err := p.deleteBucketObjects(region); err != nil && !util.IsNotFound(err) { return errors.Wrap(err, "deleting s3 objects") } log.Debug("deleting stack") if err := stack.New(p.config, p.events, nil, region).Delete(versions, wait); err != nil && !util.IsNotFound(err) { return errors.Wrap(err, "deleting stack") } log.Debug("deleting function") if err := p.deleteFunction(region); err != nil && !util.IsNotFound(err) { return errors.Wrap(err, "deleting function") } log.Debug("deleting role") if err := p.deleteRole(region); err != nil && !util.IsNotFound(err) { return errors.Wrap(err, "deleting function iam role") } return nil } // ShowStack implementation. func (p *Platform) ShowStack(region string) error { return stack.New(p.config, p.events, nil, region).Show() } // PlanStack implementation. func (p *Platform) PlanStack(region string) error { versions, err := p.getAliasVersions(region) if err != nil { return errors.Wrap(err, "fetching alias versions") } if err := p.createCerts(); err != nil { return errors.Wrap(err, "creating certs") } zones, err := p.getHostedZone() if err != nil { return errors.Wrap(err, "fetching zones") } return stack.New(p.config, p.events, zones, region).Plan(versions) } // ApplyStack implementation. func (p *Platform) ApplyStack(region string) error { if err := p.createCerts(); err != nil { return errors.Wrap(err, "creating certs") } return stack.New(p.config, p.events, nil, region).Apply() } // Exists implementation. func (p *Platform) Exists(region string) (bool, error) { log.Debug("checking if application exists") c := lambda.New(session.New(aws.NewConfig().WithRegion(region))) _, err := c.GetFunctionConfiguration(&lambda.GetFunctionConfigurationInput{ FunctionName: &p.config.Name, }) if util.IsNotFound(err) { return false, nil } if err != nil { return false, err } return true, nil } // getAliasVersions returns the function alias versions. func (p *Platform) getAliasVersions(region string) (resources.Versions, error) { var g errgroup.Group var mu sync.Mutex c := lambda.New(session.New(aws.NewConfig().WithRegion(region))) versions := make(resources.Versions) log.Debug("fetching aliases") for _, s := range p.config.Stages { s := s g.Go(func() error { log.Debugf("fetching %s alias", s.Name) version, err := p.getAliasVersion(c, s.Name) if util.IsNotFound(err) { log.Debugf("%s has no alias, defaulting to staging", s.Name) version, err = p.getAliasVersion(c, "staging") if err != nil { return errors.Wrap(err, "fetching staging alias") } } if err != nil { return errors.Wrapf(err, "fetching %q alias", s.Name) } log.Debugf("fetched %s alias (%s)", s.Name, version) mu.Lock() versions[s.Name] = version mu.Unlock() return nil }) } return versions, g.Wait() } // getAliasVersion retruns the alias version for a stage. func (p *Platform) getAliasVersion(c *lambda.Lambda, stage string) (string, error) { res, err := c.GetAlias(&lambda.GetAliasInput{ FunctionName: &p.config.Name, Name: &stage, }) if err != nil { return "", err } return *res.FunctionVersion, nil } // getHostedZone returns existing hosted zones. func (p *Platform) getHostedZone() (zones []*route53.HostedZone, err error) { r := route53.New(session.New(aws.NewConfig())) log.Debug("fetching hosted zones") res, err := r.ListHostedZonesByName(&route53.ListHostedZonesByNameInput{ MaxItems: aws.String("100"), }) if err != nil { return } zones = res.HostedZones return } // createCerts creates the certificates if necessary. // // We perform this task outside of CloudFormation because // the certificates currently must be created in the us-east-1 // region. This also gives us a chance to let the user know // that they have to confirm an email. func (p *Platform) createCerts() error { s := session.New(aws.NewConfig().WithRegion("us-east-1")) a := acm.New(s) var domains []string // existing certs log.Debug("fetching existing certs") certs, err := getCerts(a) if err != nil { return errors.Wrap(err, "fetching certs") } // request certs for _, s := range p.config.Stages.List() { if s.Domain == "" { continue } certDomains := util.CertDomainNames(s.Domain) // see if the cert exists log.Debugf("looking up cert for %s", s.Domain) arn := getCert(certs, s.Domain) if arn != "" { log.Debugf("found cert for %s: %s", s.Domain, arn) s.Cert = arn continue } option := acm.DomainValidationOption{ DomainName: aws.String(certDomains[0]), ValidationDomain: aws.String(util.Domain(s.Domain)), } options := []*acm.DomainValidationOption{ &option, } // request the cert res, err := a.RequestCertificate(&acm.RequestCertificateInput{ DomainName: aws.String(certDomains[0]), DomainValidationOptions: options, SubjectAlternativeNames: aws.StringSlice(certDomains[1:]), }) if err != nil { return errors.Wrapf(err, "requesting cert for %v", certDomains) } domains = append(domains, certDomains[0]) s.Cert = *res.CertificateArn } // no certs needed if len(domains) == 0 { return nil } defer p.events.Time("platform.certs.create", event.Fields{ "domains": domains, })() // wait for approval for range time.Tick(4 * time.Second) { res, err := a.ListCertificates(&acm.ListCertificatesInput{ MaxItems: aws.Int64(1000), CertificateStatuses: aws.StringSlice([]string{acm.CertificateStatusPendingValidation}), }) if err != nil { return errors.Wrap(err, "listing") } if len(res.CertificateSummaryList) == 0 { break } } return nil } // deploy to the given region. func (p *Platform) deploy(region string, d up.Deploy) (version string, err error) { start := time.Now() fields := event.Fields{ "commit": d.Commit, "stage": d.Stage, "region": region, } p.events.Emit("platform.deploy", fields) defer func() { fields["duration"] = time.Since(start) fields["commit"] = d.Commit fields["version"] = version p.events.Emit("platform.deploy.complete", fields) }() ctx := log.WithField("region", region) s := session.New(aws.NewConfig().WithRegion(region)) u := s3manager.NewUploaderWithClient(s3.New(s)) a := apigateway.New(s) c := lambda.New(s) ctx.Debug("fetching function config") _, err = c.GetFunctionConfiguration(&lambda.GetFunctionConfigurationInput{ FunctionName: &p.config.Name, }) if util.IsNotFound(err) { defer p.events.Time("platform.function.create", fields) return p.createFunction(c, a, u, region, d) } if err != nil { return "", errors.Wrap(err, "fetching function config") } defer p.events.Time("platform.function.update", fields) return p.updateFunction(c, a, u, region, d) } // createFunction creates the function. func (p *Platform) createFunction(c *lambda.Lambda, a *apigateway.APIGateway, up *s3manager.Uploader, region string, d up.Deploy) (version string, err error) { // ensure bucket exists if err := p.createBucket(region); err != nil && !util.IsBucketExists(err) { return "", errors.Wrap(err, "creating s3 bucket") } // upload to s3 b := aws.String(p.getS3BucketName(region)) k := aws.String(p.getS3Key(d.Stage)) log.Debugf("uploading function to bucket %s key %s", *b, *k) _, err = up.Upload(&s3manager.UploadInput{ Bucket: b, Key: k, Body: bytes.NewReader(p.zip.Bytes()), ServerSideEncryption: aws.String("aws:kms"), }) if err != nil { return "", errors.Wrap(err, "uploading function") } // load environment env, err := p.loadEnvironment(d) if err != nil { return "", errors.Wrap(err, "loading environment variables") } // create function retry: log.Debug("creating function") res, err := c.CreateFunction(&lambda.CreateFunctionInput{ FunctionName: &p.config.Name, Handler: &p.handler, Runtime: &p.config.Lambda.Runtime, Role: &p.config.Lambda.Role, MemorySize: aws.Int64(int64(p.config.Lambda.Memory)), Timeout: aws.Int64(int64(p.config.Lambda.Timeout)), Publish: aws.Bool(true), Environment: env, Code: &lambda.FunctionCode{ S3Bucket: b, S3Key: k, }, VpcConfig: p.vpc(), }) // IAM is eventually consistent apparently, so we have to keep retrying if isCreatingRole(err) { log.Debug("waiting for role to be created") time.Sleep(500 * time.Millisecond) goto retry } if err != nil { return "", errors.Wrap(err, "creating function") } return *res.Version, errFirstDeploy } // updateFunction updates the function. func (p *Platform) updateFunction(c *lambda.Lambda, a *apigateway.APIGateway, up *s3manager.Uploader, region string, d up.Deploy) (version string, err error) { b := aws.String(p.getS3BucketName(region)) k := aws.String(p.getS3Key(d.Stage)) // upload log.Debugf("uploading function to bucket %s key %s", *b, *k) _, err = up.Upload(&s3manager.UploadInput{ Bucket: b, Key: k, Body: bytes.NewReader(p.zip.Bytes()), ServerSideEncryption: aws.String("aws:kms"), }) // ensure bucket exists if util.IsNotFound(err) { if err := p.createBucket(region); err != nil { return "", errors.Wrap(err, "creating s3 bucket") } err = nil } if err != nil { return "", errors.Wrap(err, "uploading function") } // load environment env, err := p.loadEnvironment(d) if err != nil { return "", errors.Wrap(err, "loading environment variables") } // update function config log.Debug("updating function") if err := p.isPending(c); err != nil { return "", err } _, err = c.UpdateFunctionConfiguration(&lambda.UpdateFunctionConfigurationInput{ FunctionName: &p.config.Name, Handler: &p.handler, Runtime: &p.config.Lambda.Runtime, Role: &p.config.Lambda.Role, MemorySize: aws.Int64(int64(p.config.Lambda.Memory)), Timeout: aws.Int64(int64(p.config.Lambda.Timeout)), Environment: env, VpcConfig: p.vpc(), }) if err != nil { return "", errors.Wrap(err, "updating function config") } // update function code log.Debug("updating function code") if err := p.isPending(c); err != nil { return "", err } res, err := c.UpdateFunctionCode(&lambda.UpdateFunctionCodeInput{ FunctionName: &p.config.Name, Publish: aws.Bool(true), S3Bucket: b, S3Key: k, }) if err != nil { return "", errors.Wrap(err, "updating function code") } // create stage alias if err := p.alias(c, d.Stage, *res.Version); err != nil { return "", errors.Wrapf(err, "creating function stage %q alias", d.Stage) } // create git alias if d.Commit != "" { if err := p.alias(c, util.EncodeAlias(d.Commit), *res.Version); err != nil { return "", errors.Wrapf(err, "creating function git %q alias", d.Commit) } } return *res.Version, nil } // isPending implementation. func (p *Platform) isPending(c *lambda.Lambda) error { var attempt int maxAttempts := 30 // TODO: ideally max attempts is configurable wait := time.Second * 5 // TODO: ideally some backoff retry: attempt++ log.Debugf("checking if function is pending (attempt %d of %d)", attempt, maxAttempts) conf, err := c.GetFunctionConfiguration(&lambda.GetFunctionConfigurationInput{ FunctionName: &p.config.Name, }) if err != nil { return errors.Wrapf(err, "getting function config") } if *conf.State == "Active" && *conf.LastUpdateStatus != "InProgress" { log.Debugf("function is in state %q / %q", *conf.State, *conf.LastUpdateStatus) return nil } if attempt >= maxAttempts { log.Debugf("max attempts exceeded") return errors.Errorf("function is stuck in the state %q / %q", *conf.State, *conf.LastUpdateStatus) } log.Debugf("function is in state %q / %q, trying again in %s", *conf.State, *conf.LastUpdateStatus, wait) time.Sleep(wait) goto retry } // vpc returns the vpc configuration or nil. func (p *Platform) vpc() *lambda.VpcConfig { v := p.config.Lambda.VPC if v == nil { return nil } return &lambda.VpcConfig{ SubnetIds: aws.StringSlice(v.Subnets), SecurityGroupIds: aws.StringSlice(v.SecurityGroups), } } // alias creates or updates an alias. func (p *Platform) alias(c *lambda.Lambda, alias, version string) error { log.Debugf("alias %s to %s", alias, version) _, err := c.UpdateAlias(&lambda.UpdateAliasInput{ FunctionName: &p.config.Name, FunctionVersion: &version, Name: &alias, Description: aws.String(util.ManagedByUp("")), }) if util.IsNotFound(err) { _, err = c.CreateAlias(&lambda.CreateAliasInput{ FunctionName: &p.config.Name, FunctionVersion: &version, Name: &alias, Description: aws.String(util.ManagedByUp("")), }) } return err } // deleteFunction deletes the lambda function. func (p *Platform) deleteFunction(region string) error { // TODO: sessions all over... refactor c := lambda.New(session.New(aws.NewConfig().WithRegion(region))) _, err := c.DeleteFunction(&lambda.DeleteFunctionInput{ FunctionName: &p.config.Name, }) return err } // loadEnvironment loads environment variables. func (p *Platform) loadEnvironment(d up.Deploy) (*lambda.Environment, error) { m := aws.StringMap(p.config.Environment) m["UP_STAGE"] = &d.Stage m["UP_COMMIT"] = &d.Commit m["UP_AUTHOR"] = &d.Author return &lambda.Environment{ Variables: m, }, nil } // createRole creates the IAM role unless it is present. func (p *Platform) createRole() error { s := session.New(aws.NewConfig()) c := iam.New(s) name := p.roleName() desc := util.ManagedByUp("") // role is provided if s := p.config.Lambda.Role; s != "" { log.Debugf("using role from config %s", s) return nil } log.Debug("checking for role") existing, err := c.GetRole(&iam.GetRoleInput{ RoleName: &name, }) // network or permission error if err != nil && !util.IsNotFound(err) { return errors.Wrap(err, "fetching role") } // use the existing role if err == nil { log.Debug("found existing role") if err := p.updateRole(c); err != nil { return errors.Wrap(err, "updating role policy") } p.setRoleARN(*existing.Role.Arn) return nil } log.Debug("creating role") role, err := c.CreateRole(&iam.CreateRoleInput{ RoleName: &name, Description: &desc, AssumeRolePolicyDocument: &apiGatewayAssumePolicy, }) if err != nil { return errors.Wrap(err, "creating role") } if err := p.updateRole(c); err != nil { return errors.Wrap(err, "updating role policy") } p.setRoleARN(*role.Role.Arn) return nil } // updateRole updates the IAM role. func (p *Platform) updateRole(c *iam.IAM) error { name := p.roleName() policy, err := p.functionPolicy() if err != nil { return errors.Wrap(err, "creating function policy") } log.Debug("updating role policy") _, err = c.PutRolePolicy(&iam.PutRolePolicyInput{ PolicyName: &name, RoleName: &name, PolicyDocument: &policy, }) return err } // setRoleARN sets the role ARN. func (p *Platform) setRoleARN(arn string) { log.Debugf("set role to %s", arn) p.config.Lambda.Role = arn } // roleName returns the IAM role name. func (p *Platform) roleName() string { return fmt.Sprintf("%s-function", p.config.Name) } // deleteRole deletes the role and policy. func (p *Platform) deleteRole(region string) error { name := fmt.Sprintf("%s-function", p.config.Name) c := iam.New(session.New(aws.NewConfig().WithRegion(region))) // role is provided if s := p.config.Lambda.Role; s != "" { log.Debugf("using role from config %s; not deleting", s) return nil } _, err := c.DeleteRolePolicy(&iam.DeleteRolePolicyInput{ RoleName: &name, PolicyName: &name, }) if err != nil { return errors.Wrap(err, "deleting policy") } _, err = c.DeleteRole(&iam.DeleteRoleInput{ RoleName: &name, }) if err != nil { return errors.Wrap(err, "deleting iam role") } return nil } // createBucket creates the bucket. func (p *Platform) createBucket(region string) error { s := s3.New(session.New(aws.NewConfig().WithRegion(region))) n := p.getS3BucketName(region) log.WithField("name", n).Debug("creating s3 bucket") _, err := s.CreateBucket(&s3.CreateBucketInput{ Bucket: &n, }) return err } // deleteBucketObjects deletes the objects for the app. func (p *Platform) deleteBucketObjects(region string) error { s := s3.New(session.New(aws.NewConfig().WithRegion(region))) b := aws.String(p.getS3BucketName(region)) prefix := p.config.Name + "/" params := &s3.ListObjectsInput{ Bucket: b, Prefix: &prefix, } return s.ListObjectsPages(params, func(page *s3.ListObjectsOutput, lastPage bool) bool { for _, c := range page.Contents { ctx := log.WithField("key", *c.Key) ctx.Debug("deleting object") _, err := s.DeleteObject(&s3.DeleteObjectInput{ Bucket: b, Key: c.Key, }) if err != nil { ctx.WithError(err).Warn("deleting object") } } return *page.IsTruncated }) } // getAPI returns the API if present or nil. func (p *Platform) getAPI(c *apigateway.APIGateway) (api *apigateway.RestApi, err error) { name := p.config.Name res, err := c.GetRestApis(&apigateway.GetRestApisInput{ Limit: aws.Int64(500), }) if err != nil { return nil, errors.Wrap(err, "fetching apis") } for _, a := range res.Items { if *a.Name == name { api = a } } return } // injectProxy injects the Go proxy. func (p *Platform) injectProxy() error { log.Debugf("injecting proxy") if err := ioutil.WriteFile("main", bin.MustAsset("up-proxy"), 0777); err != nil { return errors.Wrap(err, "writing up-proxy") } if err := ioutil.WriteFile("_proxy.js", shim.MustAsset("index.js"), 0755); err != nil { return errors.Wrap(err, "writing _proxy.js") } return nil } // removeProxy removes the Go proxy. func (p *Platform) removeProxy() error { log.Debugf("removing proxy") os.Remove("main") os.Remove("_proxy.js") return nil } // getS3Key returns a randomized s3 key. func (p *Platform) getS3Key(stage string) string { ts := time.Now().Unix() uid := uniuri.New() return fmt.Sprintf("%s/%s/%d-%s.zip", p.config.Name, stage, ts, uid) } // getS3BucketName returns the s3 bucket name. func (p *Platform) getS3BucketName(region string) string { return fmt.Sprintf("up-%s-%s", p.getAccountID(), region) } // getAccountID returns the AWS account id derived from Lambda role, // which is currently always present, implicitly or explicitly. func (p *Platform) getAccountID() string { return strings.Split(p.config.Lambda.Role, ":")[4] } // functionPolicy returns the IAM function role policy. func (p *Platform) functionPolicy() (string, error) { policy := struct { Version string Statement []config.IAMPolicyStatement }{ Version: "2012-10-17", Statement: p.config.Lambda.Policy, } b, err := json.MarshalIndent(policy, "", " ") if err != nil { return "", err } return string(b), nil } // isCreatingRole returns true if the role has not been created. func isCreatingRole(err error) bool { return err != nil && strings.Contains(err.Error(), "role defined for the function cannot be assumed by Lambda") } // getCerts returns the certificates available. func getCerts(a *acm.ACM) (certs []*acm.CertificateDetail, err error) { var g errgroup.Group var mu sync.Mutex res, err := a.ListCertificates(&acm.ListCertificatesInput{ MaxItems: aws.Int64(1000), }) if err != nil { return nil, errors.Wrap(err, "listing") } for _, c := range res.CertificateSummaryList { c := c g.Go(func() error { res, err := a.DescribeCertificate(&acm.DescribeCertificateInput{ CertificateArn: c.CertificateArn, }) if err != nil { return errors.Wrap(err, "describing") } mu.Lock() certs = append(certs, res.Certificate) mu.Unlock() return nil }) } err = g.Wait() return } // getCert returns the ARN of a certificate with can satisfy domain, // favoring more specific certificates, then falling back on wildcards. func getCert(certs []*acm.CertificateDetail, domain string) string { // exact domain for _, c := range certs { if *c.DomainName == domain { return *c.CertificateArn } } // exact alt for _, c := range certs { for _, a := range c.SubjectAlternativeNames { if *a == domain { return *c.CertificateArn } } } // wildcards for _, c := range certs { if util.WildcardMatches(*c.DomainName, domain) { return *c.CertificateArn } for _, a := range c.SubjectAlternativeNames { if util.WildcardMatches(*a, domain) { return *c.CertificateArn } } } return "" } ================================================ FILE: platform/lambda/lambda_test.go ================================================ package lambda import ( "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/acm" "github.com/tj/assert" "github.com/apex/up/config" "github.com/apex/up/platform/event" ) func TestGetCert(t *testing.T) { certs := []*acm.CertificateDetail{ { DomainName: aws.String("example.com"), CertificateArn: aws.String("arn:example.com"), SubjectAlternativeNames: aws.StringSlice([]string{ "*.example.com", }), }, { DomainName: aws.String("*.apex.sh"), CertificateArn: aws.String("arn:*.apex.sh"), }, { DomainName: aws.String("api.example.com"), CertificateArn: aws.String("arn:api.example.com"), SubjectAlternativeNames: aws.StringSlice([]string{ "*.api.example.com", "something.example.com", }), }, } arn := getCert(certs, "example.com") assert.Equal(t, "arn:example.com", arn) arn = getCert(certs, "www.example.com") assert.Equal(t, "arn:example.com", arn) arn = getCert(certs, "api.example.com") assert.Equal(t, "arn:api.example.com", arn) arn = getCert(certs, "apex.sh") assert.Empty(t, arn) arn = getCert(certs, "api.apex.sh") assert.Equal(t, "arn:*.apex.sh", arn) arn = getCert(certs, "v1.api.example.com") assert.Equal(t, "arn:api.example.com", arn) arn = getCert(certs, "something.example.com") assert.Equal(t, "arn:api.example.com", arn) arn = getCert(certs, "staging.v1.api.example.com") assert.Empty(t, arn) } func TestCreateRole(t *testing.T) { t.Run("doesn't attempt to create configured role", func(t *testing.T) { c := &config.Config{ Lambda: config.Lambda{ Role: "custom-role-name", }, } events := make(event.Events) p := New(c, events) assert.NoError(t, p.createRole(), "createRole") }) } func TestDeleteRole(t *testing.T) { t.Run("doesn't attempt to delete configured role", func(t *testing.T) { c := &config.Config{ Lambda: config.Lambda{ Role: "custom-role-name", }, } events := make(event.Events) p := New(c, events) assert.NoError(t, p.deleteRole("us-west-2"), "deleteRole") }) } ================================================ FILE: platform/lambda/metrics.go ================================================ package lambda import ( "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/golang/sync/errgroup" "github.com/apex/up/internal/metrics" "github.com/apex/up/platform/event" ) // TODO: write a higher level pkg in tj/aws // TODO: move the metrics pkg to tj/aws type stat struct { Namespace string Name string Metric string Stat string point *cloudwatch.Datapoint } // Value returns the metric value. func (s *stat) Value() int { if s.point == nil { return 0 } switch s.Stat { case "Sum": return int(*s.point.Sum) case "Average": return int(*s.point.Average) case "Minimum": return int(*s.point.Minimum) case "Maximum": return int(*s.point.Maximum) default: return 0 } } // stats to fetch. var stats = []*stat{ {"AWS/ApiGateway", "Requests", "Count", "Sum", nil}, {"AWS/ApiGateway", "Duration min", "Latency", "Minimum", nil}, {"AWS/ApiGateway", "Duration avg", "Latency", "Average", nil}, {"AWS/ApiGateway", "Duration max", "Latency", "Maximum", nil}, {"AWS/Lambda", "Duration sum", "Duration", "Sum", nil}, {"AWS/ApiGateway", "Errors 4xx", "4XXError", "Sum", nil}, {"AWS/ApiGateway", "Errors 5xx", "5XXError", "Sum", nil}, {"AWS/Lambda", "Invocations", "Invocations", "Sum", nil}, {"AWS/Lambda", "Errors", "Errors", "Sum", nil}, {"AWS/Lambda", "Throttles", "Throttles", "Sum", nil}, } // ShowMetrics implementation. func (p *Platform) ShowMetrics(region, stage string, start time.Time) error { s := session.New(aws.NewConfig().WithRegion(region)) c := cloudwatch.New(s) var g errgroup.Group name := p.config.Name d := time.Now().UTC().Sub(start) for _, s := range stats { s := s g.Go(func() error { m := metrics.New(). Namespace(s.Namespace). TimeRange(time.Now().Add(-d), time.Now()). Period(int(d.Seconds() * 2)). Stat(s.Stat). Metric(s.Metric) switch s.Namespace { case "AWS/ApiGateway": m = m.Dimension("ApiName", name).Dimension("Stage", stage) case "AWS/Lambda": m = m.Dimension("FunctionName", name).Dimension("Resource", name+":"+stage) } res, err := c.GetMetricStatistics(m.Params()) if err != nil { return err } if len(res.Datapoints) > 0 { s.point = res.Datapoints[0] } return nil }) } if err := g.Wait(); err != nil { return err } for _, s := range stats { p.events.Emit("metrics.value", event.Fields{ "name": s.Name, "value": s.Value(), "memory": p.config.Lambda.Memory, }) } return nil } ================================================ FILE: platform/lambda/prune.go ================================================ package lambda import ( "sort" "time" "github.com/apex/log" "github.com/apex/up/platform/event" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" "github.com/pkg/errors" ) // Prune implementation. func (p *Platform) Prune(region, stage string, versions int) error { p.events.Emit("prune", nil) if err := p.createRole(); err != nil { return errors.Wrap(err, "creating iam role") } s := s3.New(session.New(aws.NewConfig().WithRegion(region))) b := aws.String(p.getS3BucketName(region)) prefix := p.config.Name + "/" + stage + "/" params := &s3.ListObjectsInput{ Bucket: b, Prefix: &prefix, } start := time.Now() var objects []*s3.Object var count int var size int64 // fetch objects err := s.ListObjectsPages(params, func(page *s3.ListObjectsOutput, lastPage bool) bool { for _, o := range page.Contents { objects = append(objects, o) } return *page.IsTruncated }) if err != nil { return errors.Wrap(err, "listing s3 objects") } // sort by time descending sort.Slice(objects, func(i int, j int) bool { a := objects[i] b := objects[j] return (*b).LastModified.Before(*a.LastModified) }) // remove old versions for i, o := range objects { ctx := log.WithFields(log.Fields{ "index": i, "key": *o.Key, "size": *o.Size, "last_modified": *o.LastModified, }) if i < versions { ctx.Debug("retain") continue } ctx.Debug("remove") size += *o.Size count++ _, err := s.DeleteObject(&s3.DeleteObjectInput{ Bucket: b, Key: o.Key, }) if err != nil { return errors.Wrap(err, "removing object") } } p.events.Emit("prune.complete", event.Fields{ "duration": time.Since(start), "size": size, "count": count, }) return nil } ================================================ FILE: platform/lambda/reporter/reporter.go ================================================ package reporter import "strings" // TODO: move most of reporting here // types map. var types = map[string]string{ "AWS::CloudFormation::Stack": "Stack", "AWS::Lambda::Alias": "Lambda alias", "AWS::Lambda::Permission": "Lambda permission", "AWS::ApiGateway::RestApi": "API", "AWS::ApiGateway::Method": "API method", "AWS::ApiGateway::Deployment": "API deployment", "AWS::ApiGateway::Resource": "API resource", "AWS::ApiGateway::DomainName": "API domain", "AWS::ApiGateway::BasePathMapping": "API mapping", "AWS::Route53::HostedZone": "DNS zone", "AWS::Route53::RecordSet": "DNS record", } // ResourceType returns a human-friendly resource type name. func ResourceType(s string) string { if types[s] != "" { return strings.ToLower(types[s]) } return s } ================================================ FILE: platform/lambda/stack/resources/resources.go ================================================ package resources import ( "fmt" "strconv" "github.com/apex/up" "github.com/apex/up/config" "github.com/apex/up/internal/util" "github.com/aws/aws-sdk-go/service/route53" ) // Map type. type Map map[string]interface{} // Versions is a map of stage to lambda function version. type Versions map[string]string // Config for the resource template. type Config struct { // Zones already present in route53. This is used to // ensure that existing zones previously set up, or // automatically configured when purchasing a domain // are not duplicated. Zones []*route53.HostedZone // Versions map used to maintain the correct lambda // function aliases when updating a stack. Versions Versions *up.Config } // New template. func New(c *Config) map[string]interface{} { return Map{ "AWSTemplateFormatVersion": "2010-09-09", "Parameters": parameters(c), "Outputs": outputs(c), "Resources": resources(c), } } // ref of id. func ref(id string) Map { return Map{ "Ref": id, } } // get value from named ref. func get(name, value string) Map { return Map{ "Fn::GetAtt": []string{ name, value, }, } } // join strings with delim. func join(delim string, s ...interface{}) Map { return Map{ "Fn::Join": []interface{}{ delim, s, }, } } // stageVariable by name. func stageVariable(name string) string { return fmt.Sprintf("${stageVariables.%s}", name) } // lambda ARN for function name. func lambdaArn(name string) Map { return join(":", "arn", "aws", "lambda", ref("AWS::Region"), ref("AWS::AccountId"), "function", ref(name)) } // lambda ARN for function name with qualifier. func lambdaArnQualifier(name, qualifier string) Map { return join(":", "arn", "aws", "lambda", ref("AWS::Region"), ref("AWS::AccountId"), "function", join(":", ref(name), qualifier)) } // getZone returns a zone by domain or nil. func getZone(c *Config, domain string) *route53.HostedZone { for _, z := range c.Zones { if *z.Name == domain+"." { return z } } return nil } // dnsZone returns the ref to a new zone, or id to an existing zone. func dnsZone(c *Config, m Map, domain string) interface{} { // already exists if z := getZone(c, domain); z != nil { return *z.Id } id := util.Camelcase("dns_zone_%s", domain) // already registered for creation if m[id] != nil { return ref(id) } // new zone m[id] = Map{ "Type": "AWS::Route53::HostedZone", "DeletionPolicy": "Retain", "UpdateReplacePolicy": "Retain", "Properties": Map{ "Name": domain, }, } return ref(id) } // api sets up the app resources. func api(c *Config, m Map) { m["Api"] = Map{ "Type": "AWS::ApiGateway::RestApi", "Properties": Map{ "Name": ref("Name"), "Description": util.ManagedByUp(c.Description), "BinaryMediaTypes": []string{ "*/*", }, }, } integration := Map{ "Type": "AWS_PROXY", "IntegrationHttpMethod": "POST", "Uri": join("", "arn:aws:apigateway:", ref("AWS::Region"), ":lambda:path/2015-03-31/functions/", lambdaArnQualifier("FunctionName", stageVariable("qualifier")), "/invocations"), } m["ApiRootMethod"] = Map{ "Type": "AWS::ApiGateway::Method", "Properties": Map{ "RestApiId": ref("Api"), "ResourceId": get("Api", "RootResourceId"), "HttpMethod": "ANY", "AuthorizationType": "NONE", "Integration": integration, }, } m["ApiProxyResource"] = Map{ "Type": "AWS::ApiGateway::Resource", "Properties": Map{ "RestApiId": ref("Api"), "ParentId": get("Api", "RootResourceId"), "PathPart": "{proxy+}", }, } m["ApiProxyMethod"] = Map{ "Type": "AWS::ApiGateway::Method", "Properties": Map{ "RestApiId": ref("Api"), "ResourceId": ref("ApiProxyResource"), "HttpMethod": "ANY", "AuthorizationType": "NONE", "Integration": integration, }, } stages(c, m) } // stages sets up the stage specific resources. func stages(c *Config, m Map) { for _, s := range c.Stages.List() { if s.IsRemote() { stage(c, s, m) } } } // stage sets up the stage specific resources. func stage(c *Config, s *config.Stage, m Map) { aliasID := stageAlias(c, s, m) deploymentID := stageDeployment(c, s, m, aliasID) stagePermissions(c, s, m, aliasID) stageDomain(c, s, m, deploymentID) } // stageAlias sets up the lambda alias and deployment and returns the alias id. func stageAlias(c *Config, s *config.Stage, m Map) string { id := util.Camelcase("api_function_alias_%s", s.Name) version, ok := c.Versions[s.Name] if !ok { panic(fmt.Sprintf("stage %q is missing a function version mapping", s.Name)) } m[id] = Map{ "Type": "AWS::Lambda::Alias", "Properties": Map{ "Name": s.Name, "Description": util.ManagedByUp(""), "FunctionName": ref("FunctionName"), "FunctionVersion": version, }, } return id } // stagePermissions sets up the lambda:invokeFunction permissions for API Gateway. func stagePermissions(c *Config, s *config.Stage, m Map, aliasID string) { id := util.Camelcase("api_lambda_permission_%s", s.Name) m[id] = Map{ "Type": "AWS::Lambda::Permission", "DependsOn": aliasID, "Properties": Map{ "Action": "lambda:invokeFunction", "FunctionName": lambdaArnQualifier("FunctionName", s.Name), "Principal": "apigateway.amazonaws.com", "SourceArn": join("", "arn:aws:execute-api", ":", ref("AWS::Region"), ":", ref("AWS::AccountId"), ":", ref("Api"), "/*"), }, } } // stageDeployment sets up the API Gateway deployment. func stageDeployment(c *Config, s *config.Stage, m Map, aliasID string) string { id := util.Camelcase("api_deployment_%s", s.Name) m[id] = Map{ "Type": "AWS::ApiGateway::Deployment", "DependsOn": []string{"ApiRootMethod", "ApiProxyMethod", aliasID}, "Properties": Map{ "RestApiId": ref("Api"), "StageName": s.Name, "StageDescription": Map{ "Variables": Map{ "qualifier": s.Name, }, }, }, } return id } // stageDomain sets up a custom domain, dns record and path mapping. func stageDomain(c *Config, s *config.Stage, m Map, deploymentID string) { if s.Domain == "" { return } id := util.Camelcase("api_domain_%s", s.Name) m[id] = Map{ "Type": "AWS::ApiGateway::DomainName", "Properties": Map{ "CertificateArn": s.Cert, "DomainName": s.Domain, }, } stagePathMapping(c, s, m, deploymentID, id) if s.Zone != false { stageDNSRecord(c, s, m, id) } } // stagePathMapping sets up the stage deployment mapping. func stagePathMapping(c *Config, s *config.Stage, m Map, deploymentID, domainID string) { id := util.Camelcase("api_domain_%s_path_mapping", s.Name) m[id] = Map{ "Type": "AWS::ApiGateway::BasePathMapping", "DependsOn": []string{deploymentID, domainID}, "Properties": Map{ "DomainName": s.Domain, "BasePath": util.BasePath(s.Path), "RestApiId": ref("Api"), "Stage": s.Name, }, } } // stageDNSRecord sets up an ALIAS record and zone if necessary for a custom domain. func stageDNSRecord(c *Config, s *config.Stage, m Map, domainID string) { id := util.Camelcase("dns_zone_%s_record_%s", util.Domain(s.Domain), s.Domain) zoneName := util.Domain(s.Domain) // explicit .zone was specified if s, ok := s.Zone.(string); ok { zoneName = s } zone := dnsZone(c, m, zoneName) m[id] = Map{ "Type": "AWS::Route53::RecordSet", "Properties": Map{ "Name": s.Domain, "Type": "A", "Comment": util.ManagedByUp(""), "HostedZoneId": zone, "AliasTarget": Map{ "DNSName": get(domainID, "DistributionDomainName"), "HostedZoneId": "Z2FDTNDATAQYW2", }, }, } } // dns setups the the user-defined DNS zones and records. func dns(c *Config, m Map) { for _, z := range c.DNS.Zones { zone := dnsZone(c, m, z.Name) for _, r := range z.Records { id := util.Camelcase("dns_zone_%s_record_%s_%s", z.Name, r.Name, r.Type) m[id] = Map{ "Type": "AWS::Route53::RecordSet", "Properties": Map{ "Name": r.Name, "Type": r.Type, "TTL": strconv.Itoa(r.TTL), "ResourceRecords": r.Value, "HostedZoneId": zone, "Comment": util.ManagedByUp(""), }, } } } } // resources of the stack. func resources(c *Config) Map { m := Map{} api(c, m) dns(c, m) return m } // parameters of the stack. func parameters(c *Config) Map { return Map{ "Name": Map{ "Description": "Name of application", "Type": "String", }, "FunctionName": Map{ "Description": "Name of application function", "Type": "String", }, } } // outputs of the stack. func outputs(c *Config) Map { return Map{ "ApiName": Map{ "Description": "API name", "Value": ref("Name"), }, "ApiFunctionName": Map{ "Description": "API Lambda function name", "Value": ref("FunctionName"), }, "ApiFunctionArn": Map{ "Description": "API Lambda function ARN", "Value": lambdaArn("FunctionName"), }, } } ================================================ FILE: platform/lambda/stack/resources/resources_test.go ================================================ package resources import ( "encoding/json" "fmt" "os" "strings" "github.com/apex/up" "github.com/apex/up/config" ) // keys returns keys from a map. func keys(m Map) (v []string) { for k := range m { v = append(v, k) } return } // getResource returns resource by name. func getResource(c *Config, name string) Map { tmpl := New(c) r := tmpl["Resources"].(Map) v, ok := r[name].(Map) if !ok { k := strings.Join(keys(r), "\n - ") panic(fmt.Sprintf("resource %q does not exist in:\n\n - %s", name, k)) } return v } // dump a resource to stdout. func dump(c *Config, name string) { r := getResource(c, name) { enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") enc.Encode(r) } } func Example_api() { c := &Config{ Config: &up.Config{ Name: "polls", }, } dump(c, "Api") // Output: // { // "Properties": { // "BinaryMediaTypes": [ // "*/*" // ], // "Description": "Managed by Up.", // "Name": { // "Ref": "Name" // } // }, // "Type": "AWS::ApiGateway::RestApi" // } } func Example_apiRootMethod() { c := &Config{ Config: &up.Config{ Name: "polls", }, } dump(c, "ApiRootMethod") // Output: // { // "Properties": { // "AuthorizationType": "NONE", // "HttpMethod": "ANY", // "Integration": { // "IntegrationHttpMethod": "POST", // "Type": "AWS_PROXY", // "Uri": { // "Fn::Join": [ // "", // [ // "arn:aws:apigateway:", // { // "Ref": "AWS::Region" // }, // ":lambda:path/2015-03-31/functions/", // { // "Fn::Join": [ // ":", // [ // "arn", // "aws", // "lambda", // { // "Ref": "AWS::Region" // }, // { // "Ref": "AWS::AccountId" // }, // "function", // { // "Fn::Join": [ // ":", // [ // { // "Ref": "FunctionName" // }, // "${stageVariables.qualifier}" // ] // ] // } // ] // ] // }, // "/invocations" // ] // ] // } // }, // "ResourceId": { // "Fn::GetAtt": [ // "Api", // "RootResourceId" // ] // }, // "RestApiId": { // "Ref": "Api" // } // }, // "Type": "AWS::ApiGateway::Method" // } } func Example_apiProxyResource() { c := &Config{ Config: &up.Config{ Name: "polls", }, } dump(c, "ApiProxyResource") // Output: // { // "Properties": { // "ParentId": { // "Fn::GetAtt": [ // "Api", // "RootResourceId" // ] // }, // "PathPart": "{proxy+}", // "RestApiId": { // "Ref": "Api" // } // }, // "Type": "AWS::ApiGateway::Resource" // } } func Example_apiProxyMethod() { c := &Config{ Config: &up.Config{ Name: "polls", }, } dump(c, "ApiProxyMethod") // Output: // { // "Properties": { // "AuthorizationType": "NONE", // "HttpMethod": "ANY", // "Integration": { // "IntegrationHttpMethod": "POST", // "Type": "AWS_PROXY", // "Uri": { // "Fn::Join": [ // "", // [ // "arn:aws:apigateway:", // { // "Ref": "AWS::Region" // }, // ":lambda:path/2015-03-31/functions/", // { // "Fn::Join": [ // ":", // [ // "arn", // "aws", // "lambda", // { // "Ref": "AWS::Region" // }, // { // "Ref": "AWS::AccountId" // }, // "function", // { // "Fn::Join": [ // ":", // [ // { // "Ref": "FunctionName" // }, // "${stageVariables.qualifier}" // ] // ] // } // ] // ] // }, // "/invocations" // ] // ] // } // }, // "ResourceId": { // "Ref": "ApiProxyResource" // }, // "RestApiId": { // "Ref": "Api" // } // }, // "Type": "AWS::ApiGateway::Method" // } } func Example_stageAlias() { c := &Config{ Config: &up.Config{ Name: "polls", Stages: config.Stages{ "production": &config.Stage{ Name: "production", }, }, }, Versions: Versions{ "production": "15", }, } dump(c, "ApiFunctionAliasProduction") // Output: // { // "Properties": { // "Description": "Managed by Up.", // "FunctionName": { // "Ref": "FunctionName" // }, // "FunctionVersion": "15", // "Name": "production" // }, // "Type": "AWS::Lambda::Alias" // } } func Example_stagePermission() { c := &Config{ Config: &up.Config{ Name: "polls", Stages: config.Stages{ "production": &config.Stage{ Name: "production", }, }, }, Versions: Versions{ "production": "15", }, } dump(c, "ApiLambdaPermissionProduction") // Output: // { // "DependsOn": "ApiFunctionAliasProduction", // "Properties": { // "Action": "lambda:invokeFunction", // "FunctionName": { // "Fn::Join": [ // ":", // [ // "arn", // "aws", // "lambda", // { // "Ref": "AWS::Region" // }, // { // "Ref": "AWS::AccountId" // }, // "function", // { // "Fn::Join": [ // ":", // [ // { // "Ref": "FunctionName" // }, // "production" // ] // ] // } // ] // ] // }, // "Principal": "apigateway.amazonaws.com", // "SourceArn": { // "Fn::Join": [ // "", // [ // "arn:aws:execute-api", // ":", // { // "Ref": "AWS::Region" // }, // ":", // { // "Ref": "AWS::AccountId" // }, // ":", // { // "Ref": "Api" // }, // "/*" // ] // ] // } // }, // "Type": "AWS::Lambda::Permission" // } } func Example_stageDeployment() { c := &Config{ Config: &up.Config{ Name: "polls", Stages: config.Stages{ "production": &config.Stage{ Name: "production", }, }, }, Versions: Versions{ "production": "15", }, } dump(c, "ApiDeploymentProduction") // Output: // { // "DependsOn": [ // "ApiRootMethod", // "ApiProxyMethod", // "ApiFunctionAliasProduction" // ], // "Properties": { // "RestApiId": { // "Ref": "Api" // }, // "StageDescription": { // "Variables": { // "qualifier": "production" // } // }, // "StageName": "production" // }, // "Type": "AWS::ApiGateway::Deployment" // } } func Example_stageDomain() { c := &Config{ Config: &up.Config{ Name: "polls", Stages: config.Stages{ "production": &config.Stage{ Name: "production", Domain: "up-example.com", Cert: "arn::something", }, }, }, Versions: Versions{ "production": "15", }, } dump(c, "ApiDomainProduction") // Output: // { // "Properties": { // "CertificateArn": "arn::something", // "DomainName": "up-example.com" // }, // "Type": "AWS::ApiGateway::DomainName" // } } func Example_stagePathMapping() { c := &Config{ Config: &up.Config{ Name: "polls", Stages: config.Stages{ "production": &config.Stage{ Name: "production", Domain: "up-example.com", }, }, }, Versions: Versions{ "production": "15", }, } dump(c, "ApiDomainProductionPathMapping") // Output: // { // "DependsOn": [ // "ApiDeploymentProduction", // "ApiDomainProduction" // ], // "Properties": { // "BasePath": "", // "DomainName": "up-example.com", // "RestApiId": { // "Ref": "Api" // }, // "Stage": "production" // }, // "Type": "AWS::ApiGateway::BasePathMapping" // } } func Example_stageDNSZone() { c := &Config{ Config: &up.Config{ Name: "polls", Stages: config.Stages{ "production": &config.Stage{ Name: "production", Domain: "up-example.com", }, }, }, Versions: Versions{ "production": "15", }, } dump(c, "DnsZoneUpExampleCom") // Output: // { // "DeletionPolicy": "Retain", // "Properties": { // "Name": "up-example.com" // }, // "Type": "AWS::Route53::HostedZone", // "UpdateReplacePolicy": "Retain" // } } func Example_stageDNSZoneRecord() { c := &Config{ Config: &up.Config{ Name: "polls", Stages: config.Stages{ "production": &config.Stage{ Name: "production", Domain: "up-example.com", }, }, }, Versions: Versions{ "production": "15", }, } dump(c, "DnsZoneUpExampleComRecordUpExampleCom") // Output: // { // "Properties": { // "AliasTarget": { // "DNSName": { // "Fn::GetAtt": [ // "ApiDomainProduction", // "DistributionDomainName" // ] // }, // "HostedZoneId": "Z2FDTNDATAQYW2" // }, // "Comment": "Managed by Up.", // "HostedZoneId": { // "Ref": "DnsZoneUpExampleCom" // }, // "Name": "up-example.com", // "Type": "A" // }, // "Type": "AWS::Route53::RecordSet" // } } func Example_dnsZone() { c := &Config{ Config: &up.Config{ Name: "polls", DNS: config.DNS{ Zones: []*config.Zone{ { Name: "up-example.com", }, }, }, }, } dump(c, "DnsZoneUpExampleCom") // Output: // { // "DeletionPolicy": "Retain", // "Properties": { // "Name": "up-example.com" // }, // "Type": "AWS::Route53::HostedZone", // "UpdateReplacePolicy": "Retain" // } } func Example_dnsZoneRecord() { c := &Config{ Config: &up.Config{ Name: "polls", DNS: config.DNS{ Zones: []*config.Zone{ { Name: "up-example.com", Records: []*config.Record{ { Name: "blog.up-example.com", Type: "CNAME", TTL: 600, Value: []string{"example.medium.com"}, }, }, }, }, }, }, } dump(c, "DnsZoneUpExampleComRecordBlogUpExampleComCNAME") // Output: // { // "Properties": { // "Comment": "Managed by Up.", // "HostedZoneId": { // "Ref": "DnsZoneUpExampleCom" // }, // "Name": "blog.up-example.com", // "ResourceRecords": [ // "example.medium.com" // ], // "TTL": "600", // "Type": "CNAME" // }, // "Type": "AWS::Route53::RecordSet" // } } ================================================ FILE: platform/lambda/stack/stack.go ================================================ // Package stack provides CloudFormation stack support. package stack import ( "encoding/json" "strings" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/apigateway" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/lambda" "github.com/aws/aws-sdk-go/service/route53" "github.com/pkg/errors" "github.com/apex/log" "github.com/apex/up" "github.com/apex/up/config" "github.com/apex/up/internal/util" "github.com/apex/up/platform/event" "github.com/apex/up/platform/lambda/stack/resources" ) // TODO: refactor a lot // TODO: backoff // TODO: profile changeset name and description flags // TODO: flags for changeset name / description // defaultChangeset name. var defaultChangeset = "changes" // Map type. type Map = resources.Map // Stack represents a single CloudFormation stack. type Stack struct { client *cloudformation.CloudFormation lambda *lambda.Lambda route53 *route53.Route53 apigateway *apigateway.APIGateway events event.Events zones []*route53.HostedZone config *up.Config } // New stack. func New(c *up.Config, events event.Events, zones []*route53.HostedZone, region string) *Stack { sess := session.New(aws.NewConfig().WithRegion(region)) return &Stack{ client: cloudformation.New(sess), lambda: lambda.New(sess), route53: route53.New(sess), apigateway: apigateway.New(sess), events: events, zones: zones, config: c, } } // template returns a configured resource template. func (s *Stack) template(versions resources.Versions) Map { return resources.New(&resources.Config{ Config: s.config, Zones: s.zones, Versions: versions, }) } // Create the stack. func (s *Stack) Create(versions resources.Versions) error { c := s.config tmpl := s.template(versions) name := c.Name b, err := json.MarshalIndent(tmpl, "", " ") if err != nil { return errors.Wrap(err, "marshaling") } _, err = s.client.CreateStack(&cloudformation.CreateStackInput{ StackName: &name, TemplateBody: aws.String(string(b)), TimeoutInMinutes: aws.Int64(60), DisableRollback: aws.Bool(true), Capabilities: aws.StringSlice([]string{"CAPABILITY_NAMED_IAM"}), Parameters: []*cloudformation.Parameter{ { ParameterKey: aws.String("Name"), ParameterValue: &name, }, { ParameterKey: aws.String("FunctionName"), ParameterValue: &name, }, }, }) if err != nil { return errors.Wrap(err, "creating stack") } if err := s.report(resourceStateFromTemplate(tmpl, CreateComplete)); err != nil { return errors.Wrap(err, "reporting") } stack, err := s.getStack() if err != nil { return errors.Wrap(err, "fetching stack") } status := Status(*stack.StackStatus) if status.State() == Failure { return errors.New(*stack.StackStatusReason) } return nil } // Delete the stack, optionally waiting for completion. func (s *Stack) Delete(versions resources.Versions, wait bool) error { _, err := s.client.DeleteStack(&cloudformation.DeleteStackInput{ StackName: &s.config.Name, }) if err != nil { return errors.Wrap(err, "deleting") } if wait { tmpl := s.template(versions) if err := s.report(resourceStateFromTemplate(tmpl, DeleteComplete)); err != nil { return errors.Wrap(err, "reporting") } } return nil } // Show resources. func (s *Stack) Show() error { defer s.events.Time("platform.stack.show", nil)() // show stack status stack, err := s.getStack() if err != nil { return errors.Wrap(err, "fetching stack") } s.events.Emit("platform.stack.show.stack", event.Fields{ "stack": stack, }) // stages for _, stage := range s.config.Stages.List() { if stage.Domain == "" { continue } s.events.Emit("platform.stack.show.stage", event.Fields{ "name": stage.Name, "domain": stage.Domain, }) // show cloudfront endpoint if err := s.showCloudfront(stage); err != nil { log.WithError(err).Debug("showing cloudfront") } // show function version if err := s.showVersion(stage); err != nil { log.WithError(err).Debug("showing version") } // show nameservers if err := s.showNameservers(stage); err != nil { return errors.Wrap(err, "showing nameservers") } } // skip events if everything is ok if Status(*stack.StackStatus).State() == Success { return nil } // show events s.events.Emit("platform.stack.show.stack.events", nil) events, err := s.getFailedEvents() if err != nil { return errors.Wrap(err, "fetching latest events") } for _, e := range events { if *e.LogicalResourceId == s.config.Name { continue } s.events.Emit("platform.stack.show.stack.event", event.Fields{ "event": e, }) } return nil } // Plan changes. func (s *Stack) Plan(versions resources.Versions) error { c := s.config tmpl := s.template(versions) name := c.Name b, err := json.MarshalIndent(tmpl, "", " ") if err != nil { return errors.Wrap(err, "marshaling") } defer s.events.Time("platform.stack.plan", nil) log.Debug("deleting changeset") _, err = s.client.DeleteChangeSet(&cloudformation.DeleteChangeSetInput{ StackName: &name, ChangeSetName: &defaultChangeset, }) if err != nil { return errors.Wrap(err, "deleting changeset") } log.Debug("creating changeset") _, err = s.client.CreateChangeSet(&cloudformation.CreateChangeSetInput{ StackName: &name, ChangeSetName: &defaultChangeset, TemplateBody: aws.String(string(b)), Capabilities: aws.StringSlice([]string{"CAPABILITY_NAMED_IAM"}), ChangeSetType: aws.String("UPDATE"), Description: aws.String("Managed by Up."), Parameters: []*cloudformation.Parameter{ { ParameterKey: aws.String("Name"), ParameterValue: &name, }, { ParameterKey: aws.String("FunctionName"), ParameterValue: &name, }, }, }) if err != nil { return errors.Wrap(err, "creating changeset") } var next *string for { log.Debug("describing changeset") res, err := s.client.DescribeChangeSet(&cloudformation.DescribeChangeSetInput{ StackName: &name, ChangeSetName: &defaultChangeset, NextToken: next, }) if err != nil { return errors.Wrap(err, "describing changeset") } status := Status(*res.Status) if status.State() == Failure { if _, err := s.client.DeleteChangeSet(&cloudformation.DeleteChangeSetInput{ StackName: &name, ChangeSetName: &defaultChangeset, }); err != nil { return errors.Wrap(err, "deleting changeset") } return errors.New(*res.StatusReason) } if !status.IsDone() { log.Debug("waiting for completion") time.Sleep(750 * time.Millisecond) continue } for _, c := range res.Changes { s.events.Emit("platform.stack.plan.change", event.Fields{ "change": c, }) } next = res.NextToken if next == nil { break } } return nil } // Apply changes. func (s *Stack) Apply() error { c := s.config name := c.Name res, err := s.client.DescribeChangeSet(&cloudformation.DescribeChangeSetInput{ StackName: &name, ChangeSetName: &defaultChangeset, }) if isNotFound(err) { return errors.Errorf("changeset does not exist, run `up stack plan` first") } if err != nil { return errors.Wrap(err, "describing changeset") } defer s.events.Time("platform.stack.apply", event.Fields{ "changes": len(res.Changes), })() _, err = s.client.ExecuteChangeSet(&cloudformation.ExecuteChangeSetInput{ StackName: &name, ChangeSetName: &defaultChangeset, }) if err != nil { return errors.Wrap(err, "executing changeset") } if err := s.report(resourceStateFromChanges(res.Changes)); err != nil { return errors.Wrap(err, "reporting") } return nil } // report events with a map of desired stats from logical or physical id, // any resources not mapped are ignored as they do not contribute to changes. func (s *Stack) report(states map[string]Status) error { defer s.events.Time("platform.stack.report", event.Fields{ "total": len(states), "complete": 0, })() ticker := time.NewTicker(time.Second) defer ticker.Stop() for range ticker.C { stack, err := s.getStack() if util.IsNotFound(err) { return nil } if util.IsThrottled(err) { time.Sleep(3 * time.Second) continue } if err != nil { return errors.Wrap(err, "fetching stack") } status := Status(*stack.StackStatus) if status.IsDone() { return nil } res, err := s.client.DescribeStackResources(&cloudformation.DescribeStackResourcesInput{ StackName: &s.config.Name, }) if util.IsThrottled(err) { time.Sleep(time.Second * 3) continue } if err != nil { return errors.Wrap(err, "describing stack resources") } complete := len(resourcesCompleted(res.StackResources, states)) s.events.Emit("platform.stack.report.event", event.Fields{ "total": len(states), "complete": complete, }) } return nil } // showVersion emits events for showing the Lambda version. func (s *Stack) showVersion(stage *config.Stage) error { res, err := s.lambda.GetAlias(&lambda.GetAliasInput{ FunctionName: &s.config.Name, Name: &stage.Name, }) if err != nil { return errors.Wrap(err, "fetching alias") } s.events.Emit("platform.stack.show.version", event.Fields{ "domain": stage.Domain, "version": *res.FunctionVersion, }) return nil } // showCloudfront emits events for listing cloudfront end-points. func (s *Stack) showCloudfront(stage *config.Stage) error { if stage.Domain == "" { return nil } res, err := s.apigateway.GetDomainName(&apigateway.GetDomainNameInput{ DomainName: &stage.Domain, }) if err != nil { return errors.Wrap(err, "getting domain mapping") } if res.DistributionDomainName == nil { return nil } s.events.Emit("platform.stack.show.domain", event.Fields{ "domain": stage.Domain, "endpoint": *res.DistributionDomainName, }) return nil } // showNameservers emits events for listing name servers. func (s *Stack) showNameservers(stage *config.Stage) error { if stage.Domain == "" { return nil } res, err := s.route53.ListHostedZonesByName(&route53.ListHostedZonesByNameInput{ DNSName: &stage.Domain, MaxItems: aws.String("1"), }) if err != nil { return errors.Wrap(err, "listing hosted zone") } if len(res.HostedZones) == 0 { return nil } z := res.HostedZones[0] if stage.Domain+"." != *z.Name { return nil } zone, err := s.route53.GetHostedZone(&route53.GetHostedZoneInput{ Id: z.Id, }) if err != nil { return errors.Wrap(err, "fetching hosted zone") } var ns []string for _, s := range zone.DelegationSet.NameServers { ns = append(ns, *s) } s.events.Emit("platform.stack.show.nameservers", event.Fields{ "nameservers": ns, }) return nil } // getStack returns the stack. func (s *Stack) getStack() (*cloudformation.Stack, error) { res, err := s.client.DescribeStacks(&cloudformation.DescribeStacksInput{ StackName: &s.config.Name, }) if err != nil { return nil, err } stack := res.Stacks[0] return stack, nil } // getLatestEvents returns the latest events for each resource. func (s *Stack) getLatestEvents() (v []*cloudformation.StackEvent, err error) { events, err := s.getEvents() if err != nil { return } hit := make(map[string]bool) for _, e := range events { id := *e.LogicalResourceId if hit[id] { continue } hit[id] = true v = append(v, e) } return } // getFailedEvents returns failed events. func (s *Stack) getFailedEvents() (v []*cloudformation.StackEvent, err error) { events, err := s.getEvents() if err != nil { return } for _, e := range events { if Status(*e.ResourceStatus).State() == Failure { v = append(v, e) } } return } // getEvents returns events. func (s *Stack) getEvents() (events []*cloudformation.StackEvent, err error) { var next *string for { res, err := s.client.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ StackName: &s.config.Name, NextToken: next, }) if err != nil { return nil, err } events = append(events, res.StackEvents...) next = res.NextToken if next == nil { break } } return } // resourceStateFromTemplate returns a map of the logical ids from template t, to status s. func resourceStateFromTemplate(t Map, s Status) map[string]Status { r := t["Resources"].(Map) m := make(map[string]Status) for id := range r { m[id] = s } return m } // TODO: ignore deletes since they're in cleanup phase? // resourceStateFromChanges returns a map of statuses from a changeset. func resourceStateFromChanges(changes []*cloudformation.Change) map[string]Status { m := make(map[string]Status) for _, c := range changes { var state Status var id string if s := c.ResourceChange.PhysicalResourceId; s != nil { id = *s } if id == "" { id = *c.ResourceChange.LogicalResourceId } switch a := *c.ResourceChange.Action; a { case "Add": state = CreateComplete case "Modify": state = UpdateComplete case "Remove": state = DeleteComplete default: panic(errors.Errorf("unhandled Action %q", a)) } m[id] = state } return m } // resourcesCompleted returns a map of the completed resources. When the resource is not // present in states, it is ignored as no changes are expected. func resourcesCompleted(resources []*cloudformation.StackResource, states map[string]Status) map[string]*cloudformation.StackResource { m := make(map[string]*cloudformation.StackResource) for _, r := range resources { var expected Status var id string // try physical id first, this is necessary as // replacement of a logical id will cause the id // to appear twice (once for Add once for Remove). if s := r.PhysicalResourceId; s != nil { if _, ok := states[*s]; ok { id = *s } } // try logical id if s := *r.LogicalResourceId; id == "" { if _, ok := states[s]; ok { id = s } } // expected state if id != "" { expected = states[id] } // matched expected state if expected == Status(*r.ResourceStatus) { m[id] = r } } return m } // isNotFound returns true if the error indicates a missing changeset. func isNotFound(err error) bool { return err != nil && strings.Contains(err.Error(), "ChangeSetNotFound") } ================================================ FILE: platform/lambda/stack/stack_test.go ================================================ package stack import ( "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/tj/assert" ) func TestResourcesCompleted(t *testing.T) { resources := []*cloudformation.StackResource{ { LogicalResourceId: aws.String("DnsZoneSomethingComRecordApiSomethingCom"), PhysicalResourceId: aws.String("api.something.com"), ResourceStatus: aws.String("CREATE_IN_PROGRESS"), ResourceStatusReason: aws.String("Resource creation Initiated"), ResourceType: aws.String("AWS::Route53::RecordSet"), StackId: aws.String("arn:aws:cloudformation:us-west-2:foobarbaz:stack/app/ad3af570-8511-11e7-8832-50d5ca789e4a"), StackName: aws.String("app"), }, { LogicalResourceId: aws.String("ApiProxyMethod"), PhysicalResourceId: aws.String("app-ApiProx-33K7PKBL7HNI"), ResourceStatus: aws.String("CREATE_COMPLETE"), ResourceType: aws.String("AWS::ApiGateway::Method"), StackId: aws.String("arn:aws:cloudformation:us-west-2:foobarbaz:stack/app/ad3af570-8511-11e7-8832-50d5ca789e4a"), StackName: aws.String("app"), }, { LogicalResourceId: aws.String("Another"), ResourceStatus: aws.String("CREATE_COMPLETE"), ResourceType: aws.String("AWS::ApiGateway::Method"), StackId: aws.String("arn:aws:cloudformation:us-west-2:foobarbaz:stack/app/ad3af570-8511-11e7-8832-50d5ca789e4a"), StackName: aws.String("app"), }, } states := map[string]Status{ "DnsZoneSomethingComRecordApiSomethingCom": CreateComplete, "app-ApiProx-33K7PKBL7HNI": CreateComplete, } c := resourcesCompleted(resources, states) assert.Len(t, c, 1) } ================================================ FILE: platform/lambda/stack/status.go ================================================ package stack import ( "fmt" "github.com/apex/up/internal/colors" ) // status map for humanization. var statusMap = map[Status]string{ Unknown: "Unknown", CreateInProgress: "Creating", CreateFailed: "Failed to create", CreateComplete: "Created", DeleteInProgress: "Deleting", DeleteFailed: "Failed to delete", DeleteComplete: "Deleted", DeleteSkipped: "Skipped", UpdateInProgress: "Updating", UpdateFailed: "Failed to update", UpdateComplete: "Updated", UpdateCompleteCleanup: "Update complete cleanup in progress", UpdateRollbackCompleteCleanup: "Update rollback complete cleanup in progress", UpdateRollbackInProgress: "Update rollback in progress", UpdateRollbackComplete: "Update rollback complete", RollbackInProgress: "Rolling back", RollbackFailed: "Failed to rollback", RollbackComplete: "Rollback complete", CreatePending: "Create pending", Failed: "Failed", } // State represents a generalized stack event state. type State int // States available. const ( Success State = iota Pending Failure ) // Status represents a stack event status. type Status string // Statuses available. const ( Unknown Status = "" CreateInProgress = "CREATE_IN_PROGRESS" CreateFailed = "CREATE_FAILED" CreateComplete = "CREATE_COMPLETE" CreatePending = "CREATE_PENDING" DeleteInProgress = "DELETE_IN_PROGRESS" DeleteFailed = "DELETE_FAILED" DeleteComplete = "DELETE_COMPLETE" DeleteSkipped = "DELETE_SKIPPED" UpdateInProgress = "UPDATE_IN_PROGRESS" UpdateFailed = "UPDATE_FAILED" UpdateComplete = "UPDATE_COMPLETE" UpdateRollbackInProgress = "UPDATE_ROLLBACK_IN_PROGRESS" UpdateRollbackComplete = "UPDATE_ROLLBACK_COMPLETE" UpdateRollbackCompleteCleanup = "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS" UpdateCompleteCleanup = "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS" RollbackInProgress = "ROLLBACK_IN_PROGRESS" RollbackFailed = "ROLLBACK_FAILED" RollbackComplete = "ROLLBACK_COMPLETE" Failed = "FAILED" ) // String returns the human representation. func (s Status) String() string { return statusMap[s] } // IsDone returns true when failed or complete. func (s Status) IsDone() bool { return s.State() != Pending } // Color the given string based on the status. func (s Status) Color(v string) string { switch s.State() { case Success: return colors.Blue(v) case Pending: return colors.Yellow(v) case Failure: return colors.Red(v) default: return v } } // State returns a generalized state. func (s Status) State() State { switch s { case CreateFailed, UpdateFailed, DeleteFailed, RollbackFailed, Failed, UpdateRollbackCompleteCleanup, UpdateRollbackComplete: return Failure case CreateInProgress, UpdateInProgress, DeleteInProgress, RollbackInProgress, CreatePending, UpdateRollbackInProgress: return Pending case CreateComplete, UpdateComplete, DeleteComplete, DeleteSkipped, RollbackComplete, UpdateCompleteCleanup: return Success default: panic(fmt.Sprintf("unhandled state %q", string(s))) } } ================================================ FILE: platform/lambda/stack/status_test.go ================================================ package stack import ( "testing" "github.com/tj/assert" ) func TestStatus_String(t *testing.T) { assert.Equal(t, "Unknown", Status("").String()) assert.Equal(t, "Creating", Status("CREATE_IN_PROGRESS").String()) assert.Equal(t, "Deleting", Status("DELETE_IN_PROGRESS").String()) assert.Equal(t, "Failed to update", Status("UPDATE_FAILED").String()) } func TestStatus_State(t *testing.T) { assert.Equal(t, Pending, Status("CREATE_IN_PROGRESS").State()) assert.Equal(t, Pending, Status("UPDATE_IN_PROGRESS").State()) assert.Equal(t, Success, Status("CREATE_COMPLETE").State()) assert.Equal(t, Failure, Status("CREATE_FAILED").State()) } func TestStatus_IsDone(t *testing.T) { assert.False(t, Status("CREATE_IN_PROGRESS").IsDone()) assert.False(t, Status("UPDATE_IN_PROGRESS").IsDone()) assert.True(t, Status("CREATE_COMPLETE").IsDone()) assert.True(t, Status("UPDATE_COMPLETE").IsDone()) assert.True(t, Status("DELETE_COMPLETE").IsDone()) assert.True(t, Status("DELETE_FAILED").IsDone()) } ================================================ FILE: platform.go ================================================ package up import ( "io" "time" ) // TODO: finalize and finish documentation // LogsConfig is configuration for viewing logs. type LogsConfig struct { // Region is the target region. Region string // Query is the filter pattern. Query string // Since is used as the starting point when filtering // historical logs, no logs before this point are returned. Since time.Time // Follow is used to stream new logs. Follow bool // Expand is used to expand logs to a verbose format. Expand bool // OutputJSON is used to output raw json. OutputJSON bool } // Logs is the interface for viewing platform logs. type Logs interface { io.Reader } // Domains is the interface for purchasing and // managing domains names. type Domains interface { Availability(domain string) (*Domain, error) Suggestions(domain string) ([]*Domain, error) Purchase(domain string, contact DomainContact) error List() ([]*Domain, error) } // Deploy config. type Deploy struct { Stage string Commit string Author string Build bool } // Platform is the interface for platform integration, // defining the basic set of functionality required for // Up applications. type Platform interface { // Build the project. Build() error // Deploy to the given stage, to the // region(s) configured by the user. Deploy(Deploy) error // Logs returns an interface for working // with logging data. Logs(LogsConfig) Logs // Domains returns an interface for // managing domain names. Domains() Domains // URL returns the endpoint for the given // region and stage combination, or an // empty string. URL(region, stage string) (string, error) // Exists returns true if the application has been created. Exists(region string) (bool, error) CreateStack(region, version string) error DeleteStack(region string, wait bool) error ShowStack(region string) error PlanStack(region string) error ApplyStack(region string) error ShowMetrics(region, stage string, start time.Time) error } // Pruner is the interface used to prune old versions and // the artifacts associated such as S3 zip files for Lambda. type Pruner interface { Prune(region, stage string, versions int) error } // Runtime is the interface used by a platform to support // runtime operations such as initializing environment // variables from remote storage. type Runtime interface { Init(stage string) error } // Zipper is the interface used by platforms which // utilize zips for delivery of deployments. type Zipper interface { Zip() io.Reader } // Domain is a domain name and its availability. type Domain struct { Name string Available bool Expiry time.Time AutoRenew bool } // DomainContact is the domain name contact // information required for registration. type DomainContact struct { Email string FirstName string LastName string CountryCode string City string Address string OrganizationName string PhoneNumber string State string ZipCode string } ================================================ FILE: reporter/discard/discard.go ================================================ // Package discard provides a reporter for discarding events. package discard import "github.com/apex/up/platform/event" // Report events. func Report(events <-chan *event.Event) { for range events { // :) } } ================================================ FILE: reporter/plain/plain.go ================================================ // Package plain provides plain-text reporting for CI. package plain import ( "fmt" "time" "github.com/dustin/go-humanize" "github.com/apex/up/platform/event" ) // Report on events. func Report(events <-chan *event.Event) { r := reporter{ events: events, } r.Start() } // reporter struct. type reporter struct { events <-chan *event.Event } // complete log with duration. func (r *reporter) complete(name, value string, d time.Duration) { duration := fmt.Sprintf("(%s)", d.Round(time.Millisecond)) fmt.Printf(" %s %s %s\n", name+":", value, duration) } // log line. func (r *reporter) log(name, value string) { fmt.Printf(" %s %s\n", name+":", value) } // error line. func (r *reporter) error(name, value string) { fmt.Printf(" %s %s\n", name+":", value) } // Start handling events. func (r *reporter) Start() { for e := range r.events { switch e.Name { case "account.login.verify": r.log("verify", "Check your email for a confirmation link") case "account.login.verified": r.log("verify", "complete") case "hook": r.log("hook", e.String("name")) case "hook.complete": r.complete("hook", e.String("name"), e.Duration("duration")) case "platform.build.zip": s := fmt.Sprintf("%s files, %s", humanize.Comma(e.Int64("files")), humanize.Bytes(uint64(e.Int("size_compressed")))) r.complete("build", s, e.Duration("duration")) case "platform.deploy.complete": s := "complete" if v := e.String("version"); v != "" { s = "version " + v } r.complete("deploy", s, e.Duration("duration")) } } } ================================================ FILE: reporter/reporter.go ================================================ package reporter import ( "github.com/apex/up/reporter/discard" "github.com/apex/up/reporter/plain" "github.com/apex/up/reporter/text" ) var ( // Discard reporter. Discard = discard.Report // Plain reporter. Plain = plain.Report // Text reporter. Text = text.Report ) ================================================ FILE: reporter/text/text.go ================================================ // Package text provides a reporter for humanized interactive events. package text import ( "fmt" "strings" "time" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/dustin/go-humanize" "github.com/tj/go-progress" "github.com/tj/go-spin" "github.com/tj/go/term" "github.com/apex/up/internal/colors" "github.com/apex/up/internal/util" "github.com/apex/up/platform/aws/cost" "github.com/apex/up/platform/event" lambdautil "github.com/apex/up/platform/lambda/reporter" "github.com/apex/up/platform/lambda/stack" ) // TODO: platform-specific reporting should live in the platform // TODO: typed events would be nicer.. refactor event names // TODO: refactor, this is a hot mess :D // Report events. func Report(events <-chan *event.Event) { r := reporter{ events: events, spinner: spin.New(), } r.Start() } // reporter struct. type reporter struct { events <-chan *event.Event spinner *spin.Spinner prevTime time.Time bar *progress.Bar inlineProgress bool pendingName string pendingValue string } // spin the spinner by moving to the start of the line and re-printing. func (r *reporter) spin() { if r.pendingName != "" { r.pending(r.pendingName, r.pendingValue) } } // clear the liner. func (r *reporter) clear() { r.pendingName = "" r.pendingValue = "" term.ClearLine() } // pending log with spinner. func (r *reporter) pending(name, value string) { r.pendingName = name r.pendingValue = value term.ClearLine() fmt.Printf("\r %s %s", colors.Purple(r.spinner.Next()+" "+name+":"), value) } // complete log with duration. func (r *reporter) complete(name, value string, d time.Duration) { r.pendingName = "" r.pendingValue = "" term.ClearLine() duration := fmt.Sprintf("(%s)", d.Round(time.Millisecond)) fmt.Printf("\r %s %s %s\n", colors.Purple(name+":"), value, colors.Gray(duration)) } // completeWithoutDuration log without duration. func (r *reporter) completeWithoutDuration(name, value string) { r.pendingName = "" r.pendingValue = "" term.ClearLine() fmt.Printf("\r %s %s\n", colors.Purple(name+":"), value) } // log line. func (r *reporter) log(name, value string) { fmt.Printf("\r %s %s\n", colors.Purple(name+":"), value) } // error line. func (r *reporter) error(name, value string) { fmt.Printf("\r %s %s\n", colors.Red(name+":"), value) } // Start handling events. func (r *reporter) Start() { tick := time.NewTicker(150 * time.Millisecond) defer tick.Stop() render := term.Renderer() for { select { case <-tick.C: r.spin() case e := <-r.events: switch e.Name { case "account.login.verify": term.HideCursor() r.pending("verify", "Check your email for a confirmation link") case "account.login.verified": term.ShowCursor() r.completeWithoutDuration("verify", "complete") case "hook": r.pending(e.String("name"), "") case "hook.complete": name := e.String("name") if name != "build" { r.clear() } case "deploy", "stack.delete", "platform.stack.apply": term.HideCursor() case "deploy.complete", "stack.delete.complete", "platform.stack.apply.complete": term.ShowCursor() case "platform.build.zip": s := fmt.Sprintf("%s files, %s", humanize.Comma(e.Int64("files")), humanize.Bytes(uint64(e.Int("size_compressed")))) r.complete("build", s, e.Duration("duration")) case "platform.deploy": r.pending("deploy", e.String("stage")) case "platform.deploy.complete": s := e.String("stage") if v := e.String("commit"); v != "" { s += " (commit " + v + ")" } else if v := e.String("version"); v != "" { s += " (version " + v + ")" } r.complete("deploy", s, e.Duration("duration")) case "platform.deploy.url": r.log("endpoint", e.String("url")) case "platform.function.create": r.inlineProgress = true case "stack.create": r.inlineProgress = true case "platform.stack.report": if r.inlineProgress { r.bar = util.NewInlineProgressInt(e.Int("total")) r.pending("stack", r.bar.String()) } else { term.ClearAll() r.bar = util.NewProgressInt(e.Int("total")) render(term.CenterLine(r.bar.String())) } case "platform.stack.report.event": if r.inlineProgress { r.bar.ValueInt(e.Int("complete")) r.pending("stack", r.bar.String()) } else { r.bar.ValueInt(e.Int("complete")) render(term.CenterLine(r.bar.String())) } case "platform.stack.report.complete": if r.inlineProgress { r.complete("stack", "complete", e.Duration("duration")) } else { term.ClearAll() term.ShowCursor() } case "platform.stack.show", "platform.stack.show.complete": fmt.Printf("\n") case "platform.stack.show.stack": s := e.Fields["stack"].(*cloudformation.Stack) util.LogName("status", "%s", stack.Status(*s.StackStatus)) if reason := s.StackStatusReason; reason != nil { util.LogName("reason", *reason) } case "platform.stack.show.stack.events": util.LogTitle("Events") case "platform.stack.show.nameservers": util.Log("nameservers:") for _, ns := range e.Strings("nameservers") { util.LogListItem(ns) } case "platform.stack.show.stack.event": event := e.Fields["event"].(*cloudformation.StackEvent) status := stack.Status(*event.ResourceStatus) if status.State() == stack.Failure { r.error(*event.LogicalResourceId, *event.ResourceStatusReason) } else { r.log(*event.LogicalResourceId, status.String()) } case "platform.stack.show.stage": util.LogTitle(strings.Title(e.String("name"))) if s := e.String("domain"); s != "" { util.LogName("domain", e.String("domain")) } case "platform.stack.show.domain": util.LogName("endpoint", e.String("endpoint")) case "platform.stack.show.version": util.LogName("version", e.String("version")) case "stack.plan": fmt.Printf("\n") case "platform.stack.plan.change": c := e.Fields["change"].(*cloudformation.Change).ResourceChange if *c.ResourceType == "AWS::Lambda::Alias" { continue } color := actionColor(*c.Action) fmt.Printf(" %s %s\n", color(*c.Action), lambdautil.ResourceType(*c.ResourceType)) fmt.Printf(" %s: %s\n", color("id"), *c.LogicalResourceId) if c.Replacement != nil { fmt.Printf(" %s: %s\n", color("replace"), *c.Replacement) } fmt.Printf("\n") case "platform.certs.create": domains := util.UniqueStrings(e.Fields["domains"].([]string)) r.log("domains", "Check your email to approve the certificate") r.pending("confirm", strings.Join(domains, ", ")) case "platform.certs.create.complete": r.complete("confirm", "complete", e.Duration("duration")) fmt.Printf("\n") case "metrics", "metrics.complete": fmt.Printf("\n") case "metrics.value": switch n := e.String("name"); n { case "Duration min", "Duration avg", "Duration max": r.log(n, fmt.Sprintf("%dms", e.Int("value"))) case "Requests": v := humanize.Comma(int64(e.Int("value"))) c := cost.Requests(e.Int("value")) r.log(n, fmt.Sprintf("%s %s", v, currency(c))) case "Duration sum": d := time.Millisecond * time.Duration(e.Int("value")) c := cost.Duration(e.Int("value"), e.Int("memory")) r.log(n, fmt.Sprintf("%s %s", d, currency(c))) case "Invocations": d := humanize.Comma(int64(e.Int("value"))) c := cost.Invocations(e.Int("value")) r.log(n, fmt.Sprintf("%s %s", d, currency(c))) default: r.log(n, humanize.Comma(int64(e.Int("value")))) } case "prune": fmt.Printf("\n") r.pending("prune", "removing old releases") case "prune.complete": n := e.Int("count") b := e.Int64("size") s := fmt.Sprintf("%d old files removed from S3 (%s)", n, humanize.Bytes(uint64(b))) r.complete("prune", s, e.Duration("duration")) fmt.Printf("\n") } r.prevTime = time.Now() } } } // currency format. func currency(n float64) string { return colors.Gray(fmt.Sprintf("($%0.2f)", n)) } // countEventsByStatus returns the number of events with the given state. func countEventsByStatus(events []*cloudformation.StackEvent, desired stack.Status) (n int) { for _, e := range events { status := stack.Status(*e.ResourceStatus) if *e.ResourceType == "AWS::CloudFormation::Stack" { continue } if status == desired { n++ } } return } // countEventsComplete returns the number of completed or failed events. func countEventsComplete(events []*cloudformation.StackEvent) (n int) { for _, e := range events { status := stack.Status(*e.ResourceStatus) if *e.ResourceType == "AWS::CloudFormation::Stack" { continue } if status.IsDone() { n++ } } return } // actionColor returns a color func by action. func actionColor(s string) colors.Func { switch s { case "Add": return colors.Purple case "Remove": return colors.Red case "Modify": return colors.Blue default: return colors.Gray } } ================================================ FILE: up.go ================================================ package up import ( "io" "os" "os/exec" "time" "github.com/apex/log" "github.com/pkg/errors" "github.com/apex/up/config" "github.com/apex/up/internal/util" "github.com/apex/up/platform/event" ) // Config for a project. type Config = config.Config // ReadConfig reads the configuration from `path`. var ReadConfig = config.ReadConfig // ParseConfigString returns config from JSON string. var ParseConfigString = config.ParseConfigString // MustParseConfigString returns config from JSON string. var MustParseConfigString = config.MustParseConfigString // Project manager. type Project struct { Platform config *Config events event.Events } // New project. func New(c *Config, events event.Events) *Project { return &Project{ config: c, events: events, } } // WithPlatform to `platform`. func (p *Project) WithPlatform(platform Platform) *Project { p.Platform = platform return p } // RunHook runs a hook by name. func (p *Project) RunHook(name string) error { hook := p.config.Hooks.Get(name) if hook.IsEmpty() { log.Debugf("hook %s is not defined", name) return nil } defer p.events.Time("hook", event.Fields{ "name": name, "hook": hook, })() for _, command := range hook { log.Debugf("hook %q command %q", name, command) cmd := exec.Command("sh", "-c", command) cmd.Env = os.Environ() cmd.Env = append(cmd.Env, util.Env(p.config.Environment)...) cmd.Env = append(cmd.Env, "PATH=node_modules/.bin:"+os.Getenv("PATH")) b, err := cmd.CombinedOutput() if err != nil { return errors.Errorf("%q: %s", command, b) } } return nil } // RunHooks runs hooks by name. func (p *Project) RunHooks(names ...string) error { for _, n := range names { if err := p.RunHook(n); err != nil { return errors.Wrapf(err, "%q hook", n) } } return nil } // Build the project. func (p *Project) Build(hooks bool) error { defer p.events.Time("platform.build", nil)() if hooks { if err := p.RunHooks("prebuild", "build"); err != nil { return err } } if err := p.Platform.Build(); err != nil { return errors.Wrap(err, "building") } if hooks { return p.RunHooks("postbuild") } return nil } // Deploy the project. func (p *Project) Deploy(d Deploy) error { defer p.events.Time("deploy", event.Fields{ "commit": d.Commit, "stage": d.Stage, })() if err := p.Build(d.Build); err != nil { return errors.Wrap(err, "building") } if err := p.deploy(d); err != nil { return errors.Wrap(err, "deploying") } if d.Build { if err := p.RunHook("clean"); err != nil { return errors.Wrap(err, "clean hook") } } return nil } // deploy stage. func (p *Project) deploy(d Deploy) error { if err := p.RunHooks("predeploy", "deploy"); err != nil { return err } if err := p.Platform.Deploy(d); err != nil { return err } return p.RunHooks("postdeploy") } // Zip returns the zip if supported by the platform. func (p *Project) Zip() (io.Reader, error) { z, ok := p.Platform.(Zipper) if !ok { return nil, errors.Errorf("platform does not support zips") } return z.Zip(), nil } // Init initializes the runtime such as remote environment variables. func (p *Project) Init(stage string) error { r, ok := p.Platform.(Runtime) if !ok { return nil } return r.Init(stage) } // CreateStack implementation. func (p *Project) CreateStack(region, version string) error { defer p.events.Time("stack.create", event.Fields{ "region": region, "version": version, })() return p.Platform.CreateStack(region, version) } // DeleteStack implementation. func (p *Project) DeleteStack(region string, wait bool) error { defer p.events.Time("stack.delete", event.Fields{ "region": region, })() return p.Platform.DeleteStack(region, wait) } // ShowStack implementation. func (p *Project) ShowStack(region string) error { defer p.events.Time("stack.show", event.Fields{ "region": region, })() return p.Platform.ShowStack(region) } // ShowMetrics implementation. func (p *Project) ShowMetrics(region, stage string, start time.Time) error { defer p.events.Time("metrics", event.Fields{ "region": region, "stage": stage, "start": start, })() return p.Platform.ShowMetrics(region, stage, start) } // PlanStack implementation. func (p *Project) PlanStack(region string) error { defer p.events.Time("stack.plan", event.Fields{ "region": region, })() return p.Platform.PlanStack(region) } // ApplyStack implementation. func (p *Project) ApplyStack(region string) error { defer p.events.Time("stack.apply", event.Fields{ "region": region, })() return p.Platform.ApplyStack(region) } // Prune implementation. func (p *Project) Prune(region, stage string, versions int) error { pruner, ok := p.Platform.(Pruner) if !ok { return errors.Errorf("platform does not support pruning") } return pruner.Prune(region, stage, versions) }