├── .editorconfig
├── .github
└── workflows
│ └── ci.yaml
├── .gitignore
├── .tool-versions
├── .vscode
└── settings.json
├── CODEOWNERS
├── LICENSE
├── Makefile
├── README.md
├── _build
├── build-local.bash
├── nupkg
│ ├── ReadMe.md
│ ├── _TODO.txt
│ ├── rendercli.nuspec.template
│ └── tools
│ │ ├── LICENSE.txt
│ │ └── VERIFICATION.txt
└── write-version.bash
├── _data
└── render.schema.json
├── _share
├── bash
│ └── bash_completion.d
│ │ └── render.bash
├── fish
│ └── vendor_completions.d
│ │ └── render.fish
└── zsh
│ └── site-functions
│ └── render.zsh
├── _test
├── invalid-render.yaml
└── sveltekit-render.yaml
├── api
├── config.ts
├── error-handling.ts
├── index.ts
└── requests.ts
├── blueprints
└── validator.ts
├── buildpack
├── constants.ts
├── crud.ts
├── templates
│ └── dockerfile_template.ts
├── types.ts
└── validate.ts
├── commands
├── _helpers.ts
├── blueprint
│ ├── edit.ts
│ ├── from-template.ts
│ ├── index.ts
│ └── launch.ts
├── buildpack
│ ├── add.ts
│ ├── index.ts
│ ├── init.ts
│ └── remove.ts
├── commands.ts
├── config
│ ├── _shared.ts
│ ├── add-profile.ts
│ ├── index.ts
│ ├── init.ts
│ ├── profiles.ts
│ └── schema.ts
├── custom-domains
│ ├── index.ts
│ └── list.ts
├── dashboard.ts
├── deploys
│ ├── index.ts
│ └── list.ts
├── docs.ts
├── index.ts
├── jobs
│ ├── index.ts
│ └── list.ts
├── regions.ts
├── repo
│ ├── from-template.ts
│ └── index.ts
├── services
│ ├── delete.ts
│ ├── index.ts
│ ├── list.ts
│ ├── show.ts
│ ├── ssh.ts
│ └── tail.ts
└── version.ts
├── config
├── index.ts
└── types
│ ├── enums.ts
│ ├── index.ts
│ └── v1.ts
├── deno.json
├── deno.lock
├── deps-lock.json
├── deps.ts
├── entry-point.ts
├── errors.ts
├── import_map.json
├── new
├── blueprint
│ └── index.ts
└── repo
│ ├── index.test.ts
│ └── index.ts
├── render.yaml
├── services
├── constants.ts
├── ssh
│ ├── index.ts
│ └── known-hosts.ts
└── types.ts
├── util
├── ajv.ts
├── errors.ts
├── find-up.test.ts
├── find-up.ts
├── fn.ts
├── git.ts
├── iter.ts
├── logging.ts
├── objects.ts
├── paths.ts
└── shell.ts
└── version.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.{js,ts,jsx,tsx,json,json5,yaml,yml,toml,md,sh,bash}]
4 | indent_size = 2
5 | tab_width = 2
6 | indent_style = space
7 | end_of_line = lf
8 | insert_final_newline = true
9 | charset = utf-8
10 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | pull_request: {}
4 | push:
5 | branches: [main]
6 | tags: ['v*']
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: denoland/setup-deno@v1
13 | with:
14 | deno-version: v1.x
15 | - name: Check out code (full depth for tags)
16 | uses: actions/checkout@v2
17 | with:
18 | fetch-depth: 0
19 | - name: caching deps
20 | run: make deps
21 | # this version will be a git describe-tags output, so if the commit is the most recent tag
22 | # it will BE the tag, or will be of the form `v0.1.0-N-aabbccdd`. We then append to it
23 | # `-$1-$kernel-$arch`.
24 | - name: outputting version
25 | run: ./_build/write-version.bash gha > version.ts
26 | - name: running tests
27 | run: make test
28 | - name: compiling executable for x86_64-unknown-linux-gnu
29 | run: make build-linux-x86_64
30 | - name: compiling executable for x86_64-apple-darwin
31 | run: make build-macos-x86_64
32 | - name: compiling executable for aarch64-apple-darwin
33 | run: make build-macos-aarch64
34 | - name: compiling executable for x86_64-pc-windows-msvc
35 | run: make build-windows-x86_64
36 | - name: setting chmod
37 | run: chmod +x /tmp/render-*
38 | - name: test-running linux artifact just to be sure...
39 | run: /tmp/render-linux-x86_64 version
40 | - name: uploading artifacts (all)
41 | uses: actions/upload-artifact@v2
42 | # if: startsWith(github.ref, 'refs/tags/')
43 | with:
44 | name: render-binaries
45 | path: /tmp/render-*
46 |
47 | make-nupkg:
48 | runs-on: ubuntu-latest
49 | needs: build
50 | if: startsWith(github.ref, 'refs/tags/')
51 | steps:
52 | # we need to remove the 'v' from this, choco doesn't like it
53 | - name: Check out code
54 | uses: actions/checkout@v2
55 | - name: Create temp directories
56 | run:
57 | mkdir -p ./tmp/artifacts ./tmp/nupkg
58 | - uses: actions/download-artifact@v3
59 | with:
60 | name: render-binaries
61 | path: ./tmp/artifacts
62 | - name: Prep executable
63 | run: cp ./tmp/artifacts/render-windows-x86_64.exe ./tmp/nupkg/render.exe
64 | - name: copy tools to nupkg
65 | run:
66 | cp -r ./_build/nupkg/tools ./tmp/nupkg/tools
67 | - name: output templated nuspec
68 | run:
69 | cat ./_build/nupkg/rendercli.nuspec.template | sed "s/NUSPEC_PACKAGE_VERSION/${GITHUB_REF/refs\/tags\/v/}/g" > ./tmp/nupkg/rendercli.nuspec
70 | - name: show templated nuspec
71 | run: cat ./tmp/nupkg/rendercli.nuspec
72 | - name: enumerating nupkg files
73 | run: find ./tmp/nupkg
74 | - name: pack nupkg
75 | uses: crazy-max/ghaction-chocolatey@v2
76 | with:
77 | args: pack ./tmp/nupkg/rendercli.nuspec
78 | - name: check for nupkg
79 | run: ls . && find ./tmp
80 | - name: uploading nupkg
81 | uses: actions/upload-artifact@v2
82 | # if: startsWith(github.ref, 'refs/tags/')
83 | with:
84 | name: nupkg
85 | path: ./rendercli.*.nupkg
86 |
87 | make-release:
88 | runs-on: ubuntu-latest
89 | needs:
90 | - build
91 | - make-nupkg
92 | if: startsWith(github.ref, 'refs/tags/')
93 | steps:
94 | - name: Check out code
95 | uses: actions/checkout@v2
96 | - run: mkdir -p /tmp/artifacts /tmp/nupkg
97 | - uses: actions/download-artifact@v3
98 | with:
99 | name: render-binaries
100 | path: /tmp/artifacts
101 | - uses: actions/download-artifact@v3
102 | with:
103 | name: nupkg
104 | path: /tmp/nupkg
105 | - name: Do GitHub release
106 | uses: softprops/action-gh-release@v1
107 | with:
108 | draft: true
109 | files: |
110 | /tmp/artifacts/*
111 | /tmp/nupkg/*
112 | LICENSE
113 | README.md
114 |
115 | push-nupkg:
116 | runs-on: ubuntu-latest
117 | needs:
118 | - make-release
119 | if: startsWith(github.ref, 'refs/tags/')
120 | steps:
121 | - uses: actions/download-artifact@v3
122 | with:
123 | name: nupkg
124 | path: ./tmp/nupkg
125 | - name: rename nupkg for action
126 | run: cp ./tmp/nupkg/rendercli.${GITHUB_REF/refs\/tags\/v/}.nupkg ./tmp/nupkg/rendercli.nupkg
127 | - name: "push nupkg to chocolatey"
128 | uses: crazy-max/ghaction-chocolatey@v2
129 | with:
130 | args: push --key ${{ secrets.CHOCOLATEY_APIKEY }} --source https://push.chocolatey.org ./tmp/nupkg/rendercli.nupkg
131 |
132 | permissions:
133 | contents: write
134 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ~*
2 | *~
3 | .DS_Store
4 |
5 | build/
6 | dist/
7 | tmp/
8 | vendor/
9 | node_modules/
10 |
11 | *.log
12 |
13 | # executables and build artifacts
14 | /share
15 | /bin
16 | render
17 | render-*
18 | !render-cli.nuspec.template
19 |
20 | # this is now automatically updated
21 | version.ts
22 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | deno 1.28.2
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "deno.unstable": true,
4 | }
5 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @growth
2 |
3 | .editorconfig ed@render.com
4 | .gitignore ed@render.com
5 | .tool-versions ed@render.com
6 | CODEOWNERS ed@render.com
7 | deno.json ed@render.com
8 | deps-lock.json ed@render.com
9 | import_map.json ed@render.com
10 | deps.ts ed@render.com
11 | .github/**/* ed@render.com
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2022 Render, Inc.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | OUTDIR ?= "/tmp"
2 |
3 | build-local:
4 | ./_build/build-local.bash
5 |
6 | cache-deps:
7 | deno cache --lock=deps-lock.json --lock-write --import-map=import_map.json deps.ts
8 |
9 | deps:
10 | deno cache --lock=deps-lock.json deps.ts
11 |
12 | test:
13 | deno test --allow-write --allow-read --allow-net --allow-env --allow-run
14 |
15 | build-linux-x86_64: deps
16 | $(eval OUTFILE ?= render-linux-x86_64)
17 | deno compile \
18 | --unstable \
19 | --allow-net \
20 | --allow-read \
21 | --allow-run \
22 | --allow-write \
23 | --allow-env \
24 | --target=x86_64-unknown-linux-gnu \
25 | --output=${OUTDIR}/${OUTFILE} \
26 | ./entry-point.ts
27 |
28 | build-macos-x86_64: deps
29 | $(eval OUTFILE ?= render-macos-x86_64)
30 | deno compile \
31 | --unstable \
32 | --allow-net \
33 | --allow-read \
34 | --allow-run \
35 | --allow-write \
36 | --allow-env \
37 | --target=x86_64-apple-darwin \
38 | --output=${OUTDIR}/${OUTFILE} \
39 | ./entry-point.ts
40 |
41 | build-macos-aarch64: deps
42 | $(eval OUTFILE ?= render-macos-aarch64)
43 | deno compile \
44 | --unstable \
45 | --allow-net \
46 | --allow-read \
47 | --allow-run \
48 | --allow-write \
49 | --allow-env \
50 | --target=aarch64-apple-darwin \
51 | --output=${OUTDIR}/${OUTFILE} \
52 | ./entry-point.ts
53 |
54 | build-windows-x86_64: deps
55 | $(eval OUTFILE ?= render-windows-x86_64)
56 | deno compile \
57 | --unstable \
58 | --allow-net \
59 | --allow-read \
60 | --allow-run \
61 | --allow-write \
62 | --allow-env \
63 | --target=x86_64-pc-windows-msvc \
64 | --output=${OUTDIR}/${OUTFILE} \
65 | ./entry-point.ts
66 |
67 | build-completions: build-local
68 | mkdir -p ./share/fish/vendor_completions.d ./share/bash/bash_completion.d ./share/zsh/site-functions
69 | ./bin/render completions fish > ./share/fish/vendor_completions.d/render.fish
70 | ./bin/render completions bash > ./share/bash/bash_completion.d/render.bash
71 | ./bin/render completions zsh > ./share/zsh/site-functions/render.zsh
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # render-cli (deprecated) #
2 |
3 | > [!WARNING]
4 | > **This CLI is deprecated and no longer under active development. We recommend using our newer [actively-maintained CLI](https://github.com/render-oss/cli) instead.**
5 |
6 | ## Getting render-cli ##
7 | ### The easy way: getting releases ###
8 | You can download a platform-specific build of `render-cli` from the [releases page](https://github.com/render-oss/render-cli/releases).
9 |
10 | ### The easy way for OSX users: homebrew ###
11 | ```bash
12 | brew tap render-oss/render
13 | brew install render
14 | ```
15 | See [render-oss/homebrew-render](https://github.com/render-oss/homebrew-render) for more details.
16 |
17 | ### The less-easy-but-still-easy way: getting builds from `main` ###
18 | If you head to [GitHub Actions](https://github.com/render-oss/render-cli/actions) and click a passing build, you can download the latest artifacts at the bottom of the page.
19 |
20 | ### The moderately difficult way: pull the repo ###
21 | _This is necessary to run `render-cli` on platforms not supported by `deno compile`, such as Linux `arm64`._
22 |
23 | You can also clone this repository (fork it first, if you want!) and run the application from the repo itself. Something like this will get you sorted:
24 |
25 | ```bash
26 | git clone git@github.com:render-oss/render-cli.git
27 | cd render-cli
28 | make deps
29 |
30 | # 'deno task run' replaces 'render' in executable invocations
31 | deno task run --help
32 | ```
33 |
34 | To build a local binary, run `make build-local`. It will emit a platform-correct binary on supported platforms and write it
35 | to `./bin/render`.
36 |
37 | ## Using render-cli ##
38 | `render-cli` attempts to be a friendly and explorable command-line tool. For any command or subcommand under `render`, you can pass `--help` for detailed information on how it works.
39 |
40 | **You'll want to get started by building a config file.** The magic words for this are `render config init`, and it'll walk you through getting set up.
41 |
42 | Is something hard to use, unpleasant to use, or unclear? **Please let us know!** As the CLI is an open-source project, we try to work in the open as much as possible; please [check the issues](https://github.com/render-oss/render-cli/issues) and file a new one if appropriate!
43 |
44 | ### Shell completions ###
45 | `render-cli` supports completions for `bash`, `zsh`, and `fish`. Once installed, type `render completions --help` for details.
46 |
47 | ## Environment variables ##
48 | - `RENDERCLI_CONFIG_FILE`: the path to a render-cli configuration file. If this file does not exist, render-cli will _not_ halt (but the application may fail due to missing configuration).
49 | - `RENDERCLI_PROFILE`: selects a profile from the Render configuration. Is overridden by the `--profile` option. Defaults to `default` if neither is set.
50 | - `RENDERCLI_REGION`: selects a region. Is overridden by the `--region` option. Defaults to your selected profile's `defaultRegion` if neither is set, and `oregon` if your profile lacks a `defaultRegion`.
51 | - `RENDERCLI_APIKEY`: provides an API key for use with the Render CLI. Overrides the value in your selected profile. Has no default; operations that require API access will fail if this is unset and the current profile does not have an API key.
52 |
--------------------------------------------------------------------------------
/_build/build-local.bash:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | # This script is a little easier than trying to do it
3 | # all in a Makefile. It handles Deno-specific build arguments
4 | # and dumps the compiled binary in `./out/render`.
5 |
6 | SCRIPT_DIR="$(dirname "$0")"
7 | ROOT_DIR="$(dirname "$SCRIPT_DIR")"
8 |
9 | OS_IMPL=$(uname -s)
10 | MACHINE=$(uname -m)
11 |
12 | dispatch_build() {
13 | cd "$(dirname "$(dirname "$0")")" || exit 1
14 |
15 | MAKE_TARGET="build-$1-$2"
16 |
17 | export OUTDIR="$ROOT_DIR/bin"
18 | mkdir -p "$OUTDIR"
19 | export OUTFILE="render"
20 |
21 | [ -f "${OUTDIR}"/"${OUTFILE}" ] && rm "${OUTDIR}"/"${OUTFILE}"
22 | make "${MAKE_TARGET}"
23 | }
24 |
25 | case "$OS_IMPL" in
26 | "Linux")
27 | case "$MACHINE" in
28 | "x86_64")
29 | dispatch_build "linux" "x86_64"
30 | ;;
31 |
32 | *)
33 | echo "Unsupported ${OS_IMPL} implementation: ${MACHINE}"
34 | exit 1
35 | ;;
36 | esac
37 | ;;
38 | "Darwin")
39 | case "$MACHINE" in
40 | "x86_64")
41 | dispatch_build "macos" "x86_64"
42 | ;;
43 |
44 | "arm64")
45 | dispatch_build "macos" "aarch64"
46 | ;;
47 |
48 | *)
49 | echo "Unsupported ${OS_IMPL} implementation: ${MACHINE}"
50 | exit 1
51 | ;;
52 | esac
53 | ;;
54 |
55 | *)
56 | echo "Unsupported OS implementation: ${OS_IMPL}"
57 | exit 1
58 | ;;
59 | esac
60 |
--------------------------------------------------------------------------------
/_build/nupkg/ReadMe.md:
--------------------------------------------------------------------------------
1 | ## Summary
2 | How do I create packages? See https://chocolatey.org/docs/create-packages
3 |
4 | If you are submitting packages to the community feed (https://chocolatey.org)
5 | always try to ensure you have read, understood and adhere to the create
6 | packages wiki link above.
7 |
8 | ## Automatic Packaging Updates?
9 | Consider making this package an automatic package, for the best
10 | maintainability over time. Read up at https://chocolatey.org/docs/automatic-packages
11 |
12 | ## Shim Generation
13 | Any executables you include in the package or download (but don't call
14 | install against using the built-in functions) will be automatically shimmed.
15 |
16 | This means those executables will automatically be included on the path.
17 | Shim generation runs whether the package is self-contained or uses automation
18 | scripts.
19 |
20 | By default, these are considered console applications.
21 |
22 | If the application is a GUI, you should create an empty file next to the exe
23 | named 'name.exe.gui' e.g. 'bob.exe' would need a file named 'bob.exe.gui'.
24 | See https://chocolatey.org/docs/create-packages#how-do-i-set-up-shims-for-applications-that-have-a-gui
25 |
26 | If you want to ignore the executable, create an empty file next to the exe
27 | named 'name.exe.ignore' e.g. 'bob.exe' would need a file named
28 | 'bob.exe.ignore'.
29 | See https://chocolatey.org/docs/create-packages#how-do-i-exclude-executables-from-getting-shims
30 |
31 | ## Self-Contained?
32 | If you have a self-contained package, you can remove the automation scripts
33 | entirely and just include the executables, they will automatically get shimmed,
34 | which puts them on the path. Ensure you have the legal right to distribute
35 | the application though. See https://chocolatey.org/docs/legal.
36 |
37 | You should read up on the Shim Generation section to familiarize yourself
38 | on what to do with GUI applications and/or ignoring shims.
39 |
40 | ## Automation Scripts
41 | You have a powerful use of Chocolatey, as you are using PowerShell. So you
42 | can do just about anything you need. Choco has some very handy built-in
43 | functions that you can use, these are sometimes called the helpers.
44 |
45 | ### Built-In Functions
46 | https://chocolatey.org/docs/helpers-reference
47 |
48 | A note about a couple:
49 | * Get-BinRoot - this is a horribly named function that doesn't do what new folks think it does. It gets you the 'tools' root, which by default is set to 'c:\tools', not the chocolateyInstall bin folder - see https://chocolatey.org/docs/helpers-get-tools-location
50 | * Install-BinFile - used for non-exe files - executables are automatically shimmed... - see https://chocolatey.org/docs/helpers-install-bin-file
51 | * Uninstall-BinFile - used for non-exe files - executables are automatically shimmed - see https://chocolatey.org/docs/helpers-uninstall-bin-file
52 |
53 | ### Getting package specific information
54 | Use the package parameters pattern - see https://chocolatey.org/docs/how-to-parse-package-parameters-argument
55 |
56 | ### Need to mount an ISO?
57 | https://chocolatey.org/docs/how-to-mount-an-iso-in-chocolatey-package
58 |
59 | ### Environment Variables
60 | Chocolatey makes a number of environment variables available (You can access any of these with $env:TheVariableNameBelow):
61 |
62 | * TEMP/TMP - Overridden to the CacheLocation, but may be the same as the original TEMP folder
63 | * ChocolateyInstall - Top level folder where Chocolatey is installed
64 | * ChocolateyPackageName - The name of the package, equivalent to the `` field in the nuspec (0.9.9+)
65 | * ChocolateyPackageTitle - The title of the package, equivalent to the `
` field in the nuspec (0.10.1+)
66 | * ChocolateyPackageVersion - The version of the package, equivalent to the `` field in the nuspec (0.9.9+)
67 | * ChocolateyPackageFolder - The top level location of the package folder - the folder where Chocolatey has downloaded and extracted the NuGet package, typically `C:\ProgramData\chocolatey\lib\packageName`.
68 |
69 | #### Advanced Environment Variables
70 | The following are more advanced settings:
71 |
72 | * ChocolateyPackageParameters - Parameters to use with packaging, not the same as install arguments (which are passed directly to the native installer). Based on `--package-parameters`. (0.9.8.22+)
73 | * CHOCOLATEY_VERSION - The version of Choco you normally see. Use if you are 'lighting' things up based on choco version. (0.9.9+) - Otherwise take a dependency on the specific version you need.
74 | * ChocolateyForceX86 = If available and set to 'true', then user has requested 32bit version. (0.9.9+) - Automatically handled in built in Choco functions.
75 | * OS_PLATFORM - Like Windows, OSX, Linux. (0.9.9+)
76 | * OS_VERSION - The version of OS, like 6.1 something something for Windows. (0.9.9+)
77 | * OS_NAME - The reported name of the OS. (0.9.9+)
78 | * USER_NAME = The user name (0.10.6+)
79 | * USER_DOMAIN = The user domain name (could also be local computer name) (0.10.6+)
80 | * IS_PROCESSELEVATED = Is the process elevated? (0.9.9+)
81 | * IS_SYSTEM = Is the user the system account? (0.10.6+)
82 | * IS_REMOTEDESKTOP = Is the user in a terminal services session? (0.10.6+)
83 | * ChocolateyToolsLocation - formerly 'ChocolateyBinRoot' ('ChocolateyBinRoot' will be removed with Chocolatey v2.0.0), this is where tools being installed outside of Chocolatey packaging will go. (0.9.10+)
84 |
85 | #### Set By Options and Configuration
86 | Some environment variables are set based on options that are passed, configuration and/or features that are turned on:
87 |
88 | * ChocolateyEnvironmentDebug - Was `--debug` passed? If using the built-in PowerShell host, this is always true (but only logs debug messages to console if `--debug` was passed) (0.9.10+)
89 | * ChocolateyEnvironmentVerbose - Was `--verbose` passed? If using the built-in PowerShell host, this is always true (but only logs verbose messages to console if `--verbose` was passed). (0.9.10+)
90 | * ChocolateyForce - Was `--force` passed? (0.9.10+)
91 | * ChocolateyForceX86 - Was `-x86` passed? (CHECK)
92 | * ChocolateyRequestTimeout - How long before a web request will time out. Set by config `webRequestTimeoutSeconds` (CHECK)
93 | * ChocolateyResponseTimeout - How long to wait for a download to complete? Set by config `commandExecutionTimeoutSeconds` (CHECK)
94 | * ChocolateyPowerShellHost - Are we using the built-in PowerShell host? Set by `--use-system-powershell` or the feature `powershellHost` (0.9.10+)
95 |
96 | #### Business Edition Variables
97 |
98 | * ChocolateyInstallArgumentsSensitive - Encrypted arguments passed from command line `--install-arguments-sensitive` that are not logged anywhere. (0.10.1+ and licensed editions 1.6.0+)
99 | * ChocolateyPackageParametersSensitive - Package parameters passed from command line `--package-parameters-senstivite` that are not logged anywhere. (0.10.1+ and licensed editions 1.6.0+)
100 | * ChocolateyLicensedVersion - What version is the licensed edition on?
101 | * ChocolateyLicenseType - What edition / type of the licensed edition is installed?
102 | * USER_CONTEXT - The original user context - different when self-service is used (Licensed v1.10.0+)
103 |
104 | #### Experimental Environment Variables
105 | The following are experimental or use not recommended:
106 |
107 | * OS_IS64BIT = This may not return correctly - it may depend on the process the app is running under (0.9.9+)
108 | * CHOCOLATEY_VERSION_PRODUCT = the version of Choco that may match CHOCOLATEY_VERSION but may be different (0.9.9+) - based on git describe
109 | * IS_ADMIN = Is the user an administrator? But doesn't tell you if the process is elevated. (0.9.9+)
110 | * IS_REMOTE = Is the user in a remote session? (0.10.6+)
111 |
112 | #### Not Useful Or Anti-Pattern If Used
113 |
114 | * ChocolateyInstallOverride = Not for use in package automation scripts. Based on `--override-arguments` being passed. (0.9.9+)
115 | * ChocolateyInstallArguments = The installer arguments meant for the native installer. You should use chocolateyPackageParameters instead. Based on `--install-arguments` being passed. (0.9.9+)
116 | * ChocolateyIgnoreChecksums - Was `--ignore-checksums` passed or the feature `checksumFiles` turned off? (0.9.9.9+)
117 | * ChocolateyAllowEmptyChecksums - Was `--allow-empty-checksums` passed or the feature `allowEmptyChecksums` turned on? (0.10.0+)
118 | * ChocolateyAllowEmptyChecksumsSecure - Was `--allow-empty-checksums-secure` passed or the feature `allowEmptyChecksumsSecure` turned on? (0.10.0+)
119 | * ChocolateyCheckLastExitCode - Should Chocolatey check LASTEXITCODE? Is the feature `scriptsCheckLastExitCode` turned on? (0.10.3+)
120 | * ChocolateyChecksum32 - Was `--download-checksum` passed? (0.10.0+)
121 | * ChocolateyChecksumType32 - Was `--download-checksum-type` passed? (0.10.0+)
122 | * ChocolateyChecksum64 - Was `--download-checksum-x64` passed? (0.10.0)+
123 | * ChocolateyChecksumType64 - Was `--download-checksum-type-x64` passed? (0.10.0)+
124 | * ChocolateyPackageExitCode - The exit code of the script that just ran - usually set by `Set-PowerShellExitCode` (CHECK)
125 | * ChocolateyLastPathUpdate - Set by Chocolatey as part of install, but not used for anything in particular in packaging.
126 | * ChocolateyProxyLocation - The explicit proxy location as set in the configuration `proxy` (0.9.9.9+)
127 | * ChocolateyDownloadCache - Use available download cache? Set by `--skip-download-cache`, `--use-download-cache`, or feature `downloadCache` (0.9.10+ and licensed editions 1.1.0+)
128 | * ChocolateyProxyBypassList - Explicitly set locations to ignore in configuration `proxyBypassList` (0.10.4+)
129 | * ChocolateyProxyBypassOnLocal - Should the proxy bypass on local connections? Set based on configuration `proxyBypassOnLocal` (0.10.4+)
130 | * http_proxy - Set by original `http_proxy` passthrough, or same as `ChocolateyProxyLocation` if explicitly set. (0.10.4+)
131 | * https_proxy - Set by original `https_proxy` passthrough, or same as `ChocolateyProxyLocation` if explicitly set. (0.10.4+)
132 | * no_proxy- Set by original `no_proxy` passthrough, or same as `ChocolateyProxyBypassList` if explicitly set. (0.10.4+)
133 |
134 |
--------------------------------------------------------------------------------
/_build/nupkg/_TODO.txt:
--------------------------------------------------------------------------------
1 | TODO
2 |
3 | 1. Determine Package Use:
4 |
5 | Organization? Internal Use? - You are not subject to distribution
6 | rights when you keep everything internal. Put the binaries directly
7 | into the tools directory (as long as total nupkg size is under 1GB).
8 | When bigger, look to use from a share or download binaries from an
9 | internal location. Embedded binaries makes for the most reliable use
10 | of Chocolatey. Use `$fileLocation` (`$file`/`$file64`) and
11 | `Install-ChocolateyInstallPackage`/`Get-ChocolateyUnzip` in
12 | tools\chocolateyInstall.ps1.
13 |
14 | You can also choose to download from internal urls, see the next
15 | section, but ignore whether you have distribution rights or not, it
16 | doesn't apply. Under no circumstances should download from the
17 | internet, it is completely unreliable. See
18 | https://chocolatey.org/docs/community-packages-disclaimer#organizations
19 | to understand the limitations of a publicly available repository.
20 |
21 | Community Repository?
22 | Have Distribution Rights?
23 | If you are the software vendor OR the software EXPLICITLY allows
24 | redistribution and the total nupkg size will be under 200MB, you
25 | have the option to embed the binaries directly into the package to
26 | provide the most reliable install experience. Put the binaries
27 | directly into the tools folder, use `$fileLocation` (`$file`/
28 | `$file64`) and `Install-ChocolateyInstallPackage`/
29 | `Get-ChocolateyUnzip` in tools\chocolateyInstall.ps1. Additionally,
30 | fill out the LICENSE and VERIFICATION file (see 3 below and those
31 | files for specifics).
32 |
33 | NOTE: You can choose to download binaries at runtime, but be sure
34 | the download location will remain stable. See the next section.
35 |
36 | Do Not Have Distribution Rights?
37 | - Note: Packages built this way cannot be 100% reliable, but it's a
38 | constraint of publicly available packages and there is little
39 | that can be done to change that. See
40 | https://chocolatey.org/docs/community-packages-disclaimer#organizations
41 | to better understand the limitations of a publicly available
42 | repository.
43 | Download Location is Publicly Available?
44 | You will need to download the runtime files from their official
45 | location at runtime. Use `$url`/`$url64` and
46 | `Install-ChocolateyPackage`/`Install-ChocolateyZipPackage` in
47 | tools\chocolateyInstall.ps1.
48 | Download Location is Not Publicly Available?
49 | Stop here, you can't push this to the community repository. You
50 | can ask the vendor for permission to embed, then include a PDF of
51 | that signed permission directly in the package. Otherwise you
52 | will need to seek alternate locations to non-publicly host the
53 | package.
54 | Download Location Is Same For All Versions?
55 | You still need to point to those urls, but you may wish to set up
56 | something like Automatic Updater (AU) so that when a new version
57 | of the software becomes available, the new package version
58 | automatically gets pushed up to the community repository. See
59 | https://chocolatey.org/docs/automatic-packages#automatic-updater-au
60 |
61 | 2. Determine Package Type:
62 |
63 | - Installer Package - contains an installer (everything in template is
64 | geared towards this type of package)
65 | - Zip Package - downloads or embeds and unpacks archives, may unpack
66 | and run an installer using `Install-ChocolateyInstallPackage` as a
67 | secondary step.
68 | - Portable Package - Contains runtime binaries (or unpacks them as a
69 | zip package) - cannot require administrative permissions to install
70 | or use
71 | - Config Package - sets config like files, registry keys, etc
72 | - Extension Package - Packages that add PowerShell functions to
73 | Chocolatey - https://chocolatey.org/docs/how-to-create-extensions
74 | - Template Package - Packages that add templates like this for `choco
75 | new -t=name` - https://chocolatey.org/docs/how-to-create-custom-package-templates
76 | - Other - there are other types of packages as well, these are the main
77 | package types seen in the wild
78 |
79 | 3. Fill out the package contents:
80 |
81 | - tools\chocolateyBeforeModify.ps1 - remove if you have no processes
82 | or services to shut down before upgrade/uninstall
83 | - tools\LICENSE.txt / tools\VERIFICATION.txt - Remove if you are not
84 | embedding binaries. Keep and fill out if you are embedding binaries
85 | in the package AND pushing to the community repository, even if you
86 | are the author of software. The file becomes easier to fill out
87 | (does not require changes each version) if you are the software
88 | vendor. If you are building packages for internal use (organization,
89 | etc), you don't need these files as you are not subject to
90 | distribution rights internally.
91 | - tools\chocolateyUninstall.ps1 - remove if autouninstaller can
92 | automatically uninstall and you have nothing additional to do during
93 | uninstall
94 | - Readme.txt - delete this file once you have read over and used
95 | anything you've needed from here
96 | - nuspec - fill this out, then clean out all the comments (you may wish
97 | to leave the headers for the package vs software metadata)
98 | - tools\chocolateyInstall.ps1 - instructions in next section.
99 |
100 | 4. ChocolateyInstall.ps1:
101 |
102 | - For embedded binaries - use `$fileLocation` (`$file`/`$file64`) and
103 | `Install-ChocolateyInstallPackage`/ `Get-ChocolateyUnzip`.
104 | - Downloading binaries at runtime - use `$url`/`$url64` and
105 | `Install-ChocolateyPackage` / `Install-ChocolateyZipPackage`.
106 | - Other needs (creating files, setting registry keys), use regular
107 | PowerShell to do so or see if there is a function already defined:
108 | https://chocolatey.org/docs/helpers-reference
109 | - There may also be functions available in extension packages, see
110 | https://chocolatey.org/packages?q=id%3A.extension for examples and
111 | availability.
112 | - Clean out the comments and sections you are not using.
113 |
114 | 5. Test the package to ensure install/uninstall work appropriately.
115 | There is a test environment you can use for this -
116 | https://github.com/chocolatey/chocolatey-test-environment
117 |
118 | 6. Learn more about Chocolatey packaging - go through the workshop at
119 | https://github.com/ferventcoder/chocolatey-workshop
120 | You will learn about
121 | - General packaging
122 | - Customizing package behavior at runtime (package parameters)
123 | - Extension packages
124 | - Custom packaging templates
125 | - Setting up an internal Chocolatey.Server repository
126 | - Adding and using internal repositories
127 | - Reporting
128 | - Advanced packaging techniques when installers are not friendly to
129 | automation
130 |
131 | 7. Delete this file.
132 |
--------------------------------------------------------------------------------
/_build/nupkg/rendercli.nuspec.template:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | rendercli
6 | NUSPEC_PACKAGE_VERSION
7 | https://github.com/render-oss/render-cli
8 | Render Inc.
9 |
10 | Render CLI
11 | Render Inc.
12 | https://github.com/render-oss/render-cli
13 | https://github.com/render-oss/render-cli/blob/main/LICENSE
14 | false
15 | https://github.com/render-oss/render-cli
16 | https://render.com/docs
17 | https://github.com/render-oss/render-cli
18 | render cloud paas
19 | Launch your next project into the cloud with Render
20 |
21 | Render is a unified cloud to build and run all your apps and websites with free TLS certificates, a global CDN, DDoS protection, private networks, and auto deploys from Git.
22 |
23 | By using our command-line tools you'll be able to quickly kick off new projects from scratch or from our library of examples. Once you're live, you can also use the Render CLI to get information about the state of your infrastructure and to make changes as you see fit.
24 |
25 |
26 | https://github.com/render-oss/render-cli/releases
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/_build/nupkg/tools/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Note: Include this file if including binaries you have the right to distribute.
3 | Otherwise delete. this file.
4 |
5 | ===DELETE ABOVE THIS LINE AND THIS LINE===
6 |
7 | From:
8 |
9 | LICENSE
10 |
11 |
12 |
--------------------------------------------------------------------------------
/_build/nupkg/tools/VERIFICATION.txt:
--------------------------------------------------------------------------------
1 | VERIFICATION
2 | Verification is intended to assist the Chocolatey moderators and community
3 | in verifying that this package's contents are trustworthy.
4 |
5 | Render, Inc. manages the render-cli package as well as its source code, and
6 | ships binaries herein.
--------------------------------------------------------------------------------
/_build/write-version.bash:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | ROOT=$(dirname "$0")
4 | cd "$ROOT" || exit 6
5 | cd .. || exit 7
6 |
7 | SUFFIX="${1:-default}"
8 |
9 | if [[ -d './.git' ]]; then
10 | VERSION="$(git describe --tags)"
11 | else
12 | VERSION="nogit"
13 | fi
14 |
15 | echo "export const VERSION = '$VERSION-$SUFFIX' as const;"
16 |
--------------------------------------------------------------------------------
/_data/render.schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$comment": "DO NOT EDIT THIS FILE MANUALLY. IT IS GENERATED BY RUNNING `npm run generate`.",
3 | "$schema": "http://json-schema.org/draft-07/schema",
4 | "$id": "http://render.com/schemas/renderSchema.json",
5 | "type": "object",
6 | "title": "render.yaml schema",
7 | "description": "The root schema comprises the entire JSON document.",
8 | "default": {},
9 | "additionalProperties": false,
10 | "properties": {
11 | "previewsEnabled": {
12 | "$comment": "Is this deprecated? It is not mentioned in docs.",
13 | "type": "boolean"
14 | },
15 | "previewsExpireAfterDays": {
16 | "$comment": "Is this deprecated? It is not mentioned in docs.",
17 | "type": "number"
18 | },
19 | "services": {
20 | "$schema": "http://json-schema.org/draft-07/schema",
21 | "$id": "subschemas/services.json",
22 | "type": "array",
23 | "title": "The services schema",
24 | "description": "An explanation about the purpose of this instance.",
25 | "default": [],
26 | "items": {
27 | "$id": "#/properties/services/items",
28 | "type": "object",
29 | "required": [
30 | "type",
31 | "name"
32 | ],
33 | "additionalProperties": false,
34 | "properties": {
35 | "type": {
36 | "type": "string",
37 | "enum": [
38 | "web",
39 | "worker",
40 | "pserv",
41 | "cron",
42 | "redis"
43 | ]
44 | },
45 | "name": {
46 | "type": "string"
47 | },
48 | "env": {
49 | "type": "string",
50 | "enum": [
51 | "docker",
52 | "elixir",
53 | "go",
54 | "node",
55 | "python",
56 | "ruby",
57 | "rust",
58 | "static"
59 | ]
60 | },
61 | "repo": {
62 | "type": "string"
63 | },
64 | "autoDeploy": {
65 | "type": "boolean"
66 | },
67 | "branch": {
68 | "type": "string"
69 | },
70 | "buildCommand": {
71 | "type": "string"
72 | },
73 | "initialDeployHook": {
74 | "type": "string"
75 | },
76 | "region": {
77 | "type": "string",
78 | "default": "oregon",
79 | "enum": [
80 | "frankfurt",
81 | "oregon",
82 | "ohio",
83 | "singapore"
84 | ]
85 | },
86 | "envVars": {
87 | "$schema": "http://json-schema.org/draft-07/schema",
88 | "$id": "subschemas/envVars.json",
89 | "type": "array",
90 | "title": "The envVars schema",
91 | "description": "An explanation about the purpose of this instance.",
92 | "default": [],
93 | "items": {
94 | "$id": "#/properties/envVars/items",
95 | "anyOf": [
96 | {
97 | "$id": "#/properties/envVars/items/anyOf/0",
98 | "type": "object",
99 | "examples": [
100 | {
101 | "key": "A",
102 | "value": "B"
103 | }
104 | ],
105 | "required": [
106 | "key",
107 | "value"
108 | ],
109 | "additionalProperties": false,
110 | "properties": {
111 | "key": {
112 | "$id": "#/properties/envVars/items/anyOf/0/properties/key",
113 | "type": "string"
114 | },
115 | "value": {
116 | "$id": "#/properties/envVars/items/anyOf/0/properties/value"
117 | },
118 | "previewValue": {
119 | "$id": "#/properties/envVars/items/anyOf/0/properties/previewValue"
120 | }
121 | }
122 | },
123 | {
124 | "$id": "#/properties/envVars/items/anyOf/1",
125 | "type": "object",
126 | "examples": [
127 | {
128 | "key": "DATABASE_URL",
129 | "fromDatabase": {
130 | "name": "prod",
131 | "property": "connectionString"
132 | }
133 | }
134 | ],
135 | "required": [
136 | "key",
137 | "fromDatabase"
138 | ],
139 | "additionalProperties": false,
140 | "properties": {
141 | "key": {
142 | "$id": "#/properties/envVars/items/anyOf/1/properties/key",
143 | "type": "string"
144 | },
145 | "fromDatabase": {
146 | "$id": "#/properties/envVars/items/anyOf/1/properties/fromDatabase",
147 | "type": "object",
148 | "required": [
149 | "name",
150 | "property"
151 | ],
152 | "additionalProperties": false,
153 | "properties": {
154 | "name": {
155 | "$id": "#/properties/envVars/items/anyOf/1/properties/fromDatabase/properties/name",
156 | "type": "string"
157 | },
158 | "property": {
159 | "$id": "#/properties/envVars/items/anyOf/1/properties/fromDatabase/properties/property",
160 | "type": "string",
161 | "enum": [
162 | "host",
163 | "port",
164 | "database",
165 | "user",
166 | "password",
167 | "connectionString"
168 | ]
169 | }
170 | }
171 | },
172 | "previewValue": {
173 | "$id": "#/properties/envVars/items/anyOf/1/properties/previewValue"
174 | }
175 | }
176 | },
177 | {
178 | "$id": "#/properties/envVars/items/anyOf/2",
179 | "type": "object",
180 | "examples": [
181 | {
182 | "key": "REDIS_HOST",
183 | "fromService": {
184 | "name": "redis",
185 | "type": "pserv",
186 | "property": "host"
187 | }
188 | }
189 | ],
190 | "required": [
191 | "key",
192 | "fromService"
193 | ],
194 | "additionalProperties": false,
195 | "properties": {
196 | "key": {
197 | "$id": "#/properties/envVars/items/anyOf/2/properties/key",
198 | "type": "string"
199 | },
200 | "fromService": {
201 | "$id": "#/properties/envVars/items/anyOf/2/properties/fromService",
202 | "oneOf": [
203 | {
204 | "type": "object",
205 | "required": [
206 | "name",
207 | "type",
208 | "property"
209 | ],
210 | "additionalProperties": false,
211 | "properties": {
212 | "name": {
213 | "type": "string"
214 | },
215 | "type": {
216 | "type": "string",
217 | "enum": [
218 | "web",
219 | "worker",
220 | "pserv",
221 | "cron"
222 | ]
223 | },
224 | "property": {
225 | "type": "string",
226 | "enum": [
227 | "host",
228 | "port",
229 | "hostport"
230 | ]
231 | }
232 | }
233 | },
234 | {
235 | "type": "object",
236 | "required": [
237 | "name",
238 | "type",
239 | "envVarKey"
240 | ],
241 | "additionalProperties": false,
242 | "properties": {
243 | "name": {
244 | "type": "string"
245 | },
246 | "type": {
247 | "type": "string",
248 | "enum": [
249 | "web",
250 | "worker",
251 | "pserv",
252 | "cron"
253 | ]
254 | },
255 | "envVarKey": {
256 | "type": "string"
257 | }
258 | }
259 | }
260 | ]
261 | },
262 | "previewValue": {
263 | "$id": "#/properties/envVars/items/anyOf/2/properties/previewValue"
264 | }
265 | }
266 | },
267 | {
268 | "$id": "#/properties/envVars/items/anyOf/3",
269 | "type": "object",
270 | "examples": [
271 | {
272 | "key": "APP_SECRET",
273 | "generateValue": true
274 | }
275 | ],
276 | "required": [
277 | "key",
278 | "generateValue"
279 | ],
280 | "additionalProperties": false,
281 | "properties": {
282 | "key": {
283 | "$id": "#/properties/envVars/items/anyOf/3/properties/key",
284 | "type": "string"
285 | },
286 | "generateValue": {
287 | "$id": "#/properties/envVars/items/anyOf/3/properties/generateValue",
288 | "type": "boolean"
289 | },
290 | "previewValue": {
291 | "$id": "#/properties/envVars/items/anyOf/3/properties/previewValue"
292 | }
293 | }
294 | },
295 | {
296 | "$id": "#/properties/envVars/items/anyOf/4",
297 | "type": "object",
298 | "examples": [
299 | {
300 | "key": "SOME_SECRET",
301 | "sync": false
302 | }
303 | ],
304 | "required": [
305 | "key",
306 | "sync"
307 | ],
308 | "additionalProperties": false,
309 | "properties": {
310 | "key": {
311 | "$id": "#/properties/envVars/items/anyOf/4/properties/key",
312 | "type": "string"
313 | },
314 | "sync": {
315 | "$id": "#/properties/envVars/items/anyOf/4/properties/sync",
316 | "type": "boolean",
317 | "default": false
318 | },
319 | "previewValue": {
320 | "$id": "#/properties/envVars/items/anyOf/4/properties/previewValue"
321 | }
322 | }
323 | },
324 | {
325 | "$id": "#/properties/envVars/items/anyOf/5",
326 | "type": "object",
327 | "title": "The sixth anyOf schema",
328 | "description": "An explanation about the purpose of this instance.",
329 | "default": {},
330 | "examples": [
331 | {
332 | "fromGroup": "my-env-group"
333 | }
334 | ],
335 | "required": [
336 | "fromGroup"
337 | ],
338 | "properties": {
339 | "fromGroup": {
340 | "$id": "#/properties/envVars/items/anyOf/5/properties/fromGroup",
341 | "type": "string",
342 | "title": "The fromGroup schema",
343 | "description": "An explanation about the purpose of this instance.",
344 | "default": "",
345 | "examples": [
346 | "my-env-group"
347 | ]
348 | }
349 | },
350 | "additionalProperties": true
351 | }
352 | ]
353 | }
354 | },
355 | "disk": {
356 | "$comment": "not valid for static env",
357 | "type": "object",
358 | "required": [
359 | "name",
360 | "mountPath"
361 | ],
362 | "properties": {
363 | "name": {
364 | "type": "string"
365 | },
366 | "mountPath": {
367 | "type": "string"
368 | },
369 | "sizeGB": {
370 | "type": "integer"
371 | }
372 | }
373 | },
374 | "dockerCommand": {
375 | "$comment": "for docker env only",
376 | "type": "string"
377 | },
378 | "dockerContext": {
379 | "$comment": "for docker env only",
380 | "type": "string"
381 | },
382 | "dockerfilePath": {
383 | "$comment": "for docker env only",
384 | "type": "string"
385 | },
386 | "domains": {
387 | "$comment": "for web type only",
388 | "type": "array",
389 | "items": {
390 | "type": "string"
391 | }
392 | },
393 | "headers": {
394 | "$comment": "only for static env",
395 | "type": "array",
396 | "items": {
397 | "type": "object",
398 | "required": [
399 | "path",
400 | "name",
401 | "value"
402 | ],
403 | "properties": {
404 | "path": {
405 | "type": "string"
406 | },
407 | "name": {
408 | "type": "string"
409 | },
410 | "value": {
411 | "type": "string"
412 | }
413 | }
414 | }
415 | },
416 | "maxmemoryPolicy": {
417 | "$comment": "for redis type only",
418 | "type": "string",
419 | "default": "allkeys-lru",
420 | "enum": [
421 | "noeviction",
422 | "volatile-lru",
423 | "volatile-lfu",
424 | "allkeys-lfu",
425 | "volatile-random",
426 | "allkeys-random",
427 | "volatile-ttl"
428 | ]
429 | },
430 | "healthCheckPath": {
431 | "type": "string"
432 | },
433 | "numInstances": {
434 | "type": "integer",
435 | "default": 1
436 | },
437 | "plan": {
438 | "type": "string",
439 | "enum": [
440 | "free",
441 | "starter",
442 | "starter plus",
443 | "standard",
444 | "standard plus",
445 | "pro",
446 | "pro plus"
447 | ]
448 | },
449 | "previewPlan": {
450 | "$comment": "Is this deprecated? It is not mentioned in docs.",
451 | "type": "string",
452 | "enum": [
453 | "starter",
454 | "starter plus",
455 | "standard",
456 | "standard plus",
457 | "pro",
458 | "pro plus"
459 | ]
460 | },
461 | "pullRequestPreviewsEnabled": {
462 | "$comment": "only for web type",
463 | "type": "boolean"
464 | },
465 | "routes": {
466 | "$comment": "only for static env",
467 | "type": "array",
468 | "items": {
469 | "type": "object",
470 | "required": [
471 | "type",
472 | "source",
473 | "destination"
474 | ],
475 | "properties": {
476 | "type": {
477 | "type": "string",
478 | "enum": [
479 | "redirect",
480 | "rewrite"
481 | ]
482 | },
483 | "source": {
484 | "type": "string"
485 | },
486 | "destination": {
487 | "type": "string"
488 | }
489 | }
490 | }
491 | },
492 | "scaling": {
493 | "type": "object",
494 | "properties": {
495 | "minInstances": {
496 | "type": "integer"
497 | },
498 | "maxInstances": {
499 | "type": "integer"
500 | },
501 | "targetMemoryPercent": {
502 | "type": "integer"
503 | },
504 | "targetCPUPercent": {
505 | "type": "integer"
506 | }
507 | }
508 | },
509 | "schedule": {
510 | "$comment": "only for cron type",
511 | "type": "string"
512 | },
513 | "startCommand": {
514 | "type": "string"
515 | },
516 | "staticPublishPath": {
517 | "type": "string"
518 | },
519 | "ipAllowList": {
520 | "$comment": "only for Redis",
521 | "type": "array",
522 | "items": {
523 | "type": "object",
524 | "required": [
525 | "source",
526 | "description"
527 | ],
528 | "additionalProperties": false,
529 | "properties": {
530 | "source": {
531 | "type": "string"
532 | },
533 | "description": {
534 | "type": "string"
535 | }
536 | }
537 | }
538 | }
539 | },
540 | "allOf": [
541 | {
542 | "$comment": "Conditionally disallow certain properties"
543 | },
544 | {
545 | "$comment": "Disallow docker-related commands unless `env` is `docker`.",
546 | "if": {
547 | "not": {
548 | "properties": {
549 | "env": {
550 | "const": "docker"
551 | }
552 | }
553 | }
554 | },
555 | "then": {
556 | "not": {
557 | "anyOf": [
558 | {
559 | "required": [
560 | "dockerCommand"
561 | ]
562 | },
563 | {
564 | "required": [
565 | "dockerContext"
566 | ]
567 | },
568 | {
569 | "required": [
570 | "dockerfilePath"
571 | ]
572 | }
573 | ]
574 | }
575 | }
576 | },
577 | {
578 | "$comment": "Disallow `domains` unless `type` is `web`.",
579 | "if": {
580 | "not": {
581 | "properties": {
582 | "type": {
583 | "const": "web"
584 | }
585 | }
586 | }
587 | },
588 | "then": {
589 | "not": {
590 | "required": [
591 | "domains"
592 | ]
593 | }
594 | }
595 | },
596 | {
597 | "if": {
598 | "not": {
599 | "properties": {
600 | "env": {
601 | "const": "static"
602 | }
603 | }
604 | }
605 | },
606 | "then": {
607 | "$comment": "Disallow `routes` and `headers` unless `env` is `static`.",
608 | "not": {
609 | "anyOf": [
610 | {
611 | "required": [
612 | "routes"
613 | ]
614 | },
615 | {
616 | "required": [
617 | "headers"
618 | ]
619 | },
620 | {
621 | "required": [
622 | "staticPublishPath"
623 | ]
624 | }
625 | ]
626 | }
627 | }
628 | },
629 | {
630 | "$comment": "Disallow `disk` if `env` is `static`.",
631 | "if": {
632 | "properties": {
633 | "env": {
634 | "const": "static"
635 | }
636 | }
637 | },
638 | "then": {
639 | "not": {
640 | "anyOf": [
641 | {
642 | "required": [
643 | "disk"
644 | ]
645 | },
646 | {
647 | "required": [
648 | "region"
649 | ]
650 | }
651 | ]
652 | }
653 | }
654 | },
655 | {
656 | "$comment": "Disallow `maxmemoryPolicy` unless `type` is `redis`.",
657 | "if": {
658 | "not": {
659 | "properties": {
660 | "type": {
661 | "const": "redis"
662 | }
663 | }
664 | }
665 | },
666 | "then": {
667 | "not": {
668 | "required": [
669 | "maxmemoryPolicy"
670 | ]
671 | }
672 | }
673 | },
674 | {
675 | "$comment": "Require `env` property if `type` is not `redis`.",
676 | "if": {
677 | "not": {
678 | "properties": {
679 | "type": {
680 | "const": "redis"
681 | }
682 | }
683 | }
684 | },
685 | "then": {
686 | "required": [
687 | "env"
688 | ]
689 | }
690 | },
691 | {
692 | "$comment": "Disallow `schedule` unless `type` is `cron`.",
693 | "if": {
694 | "not": {
695 | "properties": {
696 | "type": {
697 | "const": "cron"
698 | }
699 | }
700 | }
701 | },
702 | "then": {
703 | "not": {
704 | "required": [
705 | "schedule"
706 | ]
707 | }
708 | }
709 | },
710 | {
711 | "$comment": "Disallow `ipAllowList` unless `type` is `redis`.",
712 | "if": {
713 | "not": {
714 | "properties": {
715 | "type": {
716 | "const": "redis"
717 | }
718 | }
719 | }
720 | },
721 | "then": {
722 | "not": {
723 | "required": [
724 | "ipAllowList"
725 | ]
726 | }
727 | }
728 | }
729 | ]
730 | },
731 | "definitions": {
732 | "docker": {
733 | "$comment": "Disallow docker-related commands unless `env` is `docker`.",
734 | "if": {
735 | "not": {
736 | "properties": {
737 | "env": {
738 | "const": "docker"
739 | }
740 | }
741 | }
742 | },
743 | "then": {
744 | "not": {
745 | "anyOf": [
746 | {
747 | "required": [
748 | "dockerCommand"
749 | ]
750 | },
751 | {
752 | "required": [
753 | "dockerContext"
754 | ]
755 | },
756 | {
757 | "required": [
758 | "dockerfilePath"
759 | ]
760 | }
761 | ]
762 | }
763 | }
764 | },
765 | "domains": {
766 | "$comment": "Disallow `domains` unless `type` is `web`.",
767 | "if": {
768 | "not": {
769 | "properties": {
770 | "type": {
771 | "const": "web"
772 | }
773 | }
774 | }
775 | },
776 | "then": {
777 | "not": {
778 | "required": [
779 | "domains"
780 | ]
781 | }
782 | }
783 | },
784 | "not-static": {
785 | "if": {
786 | "not": {
787 | "properties": {
788 | "env": {
789 | "const": "static"
790 | }
791 | }
792 | }
793 | },
794 | "then": {
795 | "$comment": "Disallow `routes` and `headers` unless `env` is `static`.",
796 | "not": {
797 | "anyOf": [
798 | {
799 | "required": [
800 | "routes"
801 | ]
802 | },
803 | {
804 | "required": [
805 | "headers"
806 | ]
807 | },
808 | {
809 | "required": [
810 | "staticPublishPath"
811 | ]
812 | }
813 | ]
814 | }
815 | }
816 | },
817 | "static": {
818 | "$comment": "Disallow `disk` if `env` is `static`.",
819 | "if": {
820 | "properties": {
821 | "env": {
822 | "const": "static"
823 | }
824 | }
825 | },
826 | "then": {
827 | "not": {
828 | "anyOf": [
829 | {
830 | "required": [
831 | "disk"
832 | ]
833 | },
834 | {
835 | "required": [
836 | "region"
837 | ]
838 | }
839 | ]
840 | }
841 | }
842 | },
843 | "redis-maxmem": {
844 | "$comment": "Disallow `maxmemoryPolicy` unless `type` is `redis`.",
845 | "if": {
846 | "not": {
847 | "properties": {
848 | "type": {
849 | "const": "redis"
850 | }
851 | }
852 | }
853 | },
854 | "then": {
855 | "not": {
856 | "required": [
857 | "maxmemoryPolicy"
858 | ]
859 | }
860 | }
861 | },
862 | "redis-no-env": {
863 | "$comment": "Require `env` property if `type` is not `redis`.",
864 | "if": {
865 | "not": {
866 | "properties": {
867 | "type": {
868 | "const": "redis"
869 | }
870 | }
871 | }
872 | },
873 | "then": {
874 | "required": [
875 | "env"
876 | ]
877 | }
878 | },
879 | "cron": {
880 | "$comment": "Disallow `schedule` unless `type` is `cron`.",
881 | "if": {
882 | "not": {
883 | "properties": {
884 | "type": {
885 | "const": "cron"
886 | }
887 | }
888 | }
889 | },
890 | "then": {
891 | "not": {
892 | "required": [
893 | "schedule"
894 | ]
895 | }
896 | }
897 | },
898 | "ipallowlist": {
899 | "$comment": "Disallow `ipAllowList` unless `type` is `redis`.",
900 | "if": {
901 | "not": {
902 | "properties": {
903 | "type": {
904 | "const": "redis"
905 | }
906 | }
907 | }
908 | },
909 | "then": {
910 | "not": {
911 | "required": [
912 | "ipAllowList"
913 | ]
914 | }
915 | }
916 | }
917 | }
918 | },
919 | "databases": {
920 | "$schema": "http://json-schema.org/draft-07/schema",
921 | "$id": "subschemas/databases.json",
922 | "type": "array",
923 | "title": "The databases schema",
924 | "description": "An explanation about the purpose of this instance.",
925 | "default": [],
926 | "items": {
927 | "$id": "#/properties/databases/items",
928 | "type": "object",
929 | "required": [
930 | "name"
931 | ],
932 | "additionalProperties": false,
933 | "properties": {
934 | "name": {
935 | "type": "string"
936 | },
937 | "region": {
938 | "type": "string",
939 | "default": "oregon",
940 | "enum": [
941 | "frankfurt",
942 | "oregon",
943 | "ohio",
944 | "singapore"
945 | ]
946 | },
947 | "plan": {
948 | "type": "string",
949 | "default": "starter",
950 | "enum": [
951 | "starter",
952 | "standard",
953 | "standard plus",
954 | "pro",
955 | "pro plus"
956 | ]
957 | },
958 | "previewPlan": {
959 | "$comment": "Is this deprecated? It is not mentioned in docs.",
960 | "type": "string",
961 | "enum": [
962 | "starter",
963 | "standard",
964 | "standard plus",
965 | "pro",
966 | "pro plus"
967 | ]
968 | },
969 | "databaseName": {
970 | "type": "string"
971 | },
972 | "user": {
973 | "type": "string"
974 | },
975 | "ipAllowList": {
976 | "$comment": "What should the `default` value be? `null`? `[]` disallows all external IPs according to docs.",
977 | "type": "array",
978 | "items": {
979 | "type": "object",
980 | "required": [
981 | "source",
982 | "description"
983 | ],
984 | "additionalProperties": false,
985 | "properties": {
986 | "source": {
987 | "type": "string"
988 | },
989 | "description": {
990 | "type": "string"
991 | }
992 | }
993 | }
994 | },
995 | "postgresMajorVersion": {
996 | "type": "integer",
997 | "enum": [
998 | 11,
999 | 12,
1000 | 13
1001 | ]
1002 | }
1003 | }
1004 | }
1005 | },
1006 | "envVarGroups": {
1007 | "$schema": "http://json-schema.org/draft-07/schema",
1008 | "$id": "subschemas/envVarGroups.json",
1009 | "type": "array",
1010 | "title": "The envVarGroups schema",
1011 | "description": "An explanation about the purpose of this instance.",
1012 | "default": [],
1013 | "items": {
1014 | "$id": "#/properties/envVarGroups/items",
1015 | "type": "object",
1016 | "required": [
1017 | "name"
1018 | ],
1019 | "properties": {
1020 | "name": {
1021 | "type": "string"
1022 | },
1023 | "envVars": {
1024 | "type": "array",
1025 | "items": {
1026 | "anyOf": [
1027 | {
1028 | "type": "object",
1029 | "examples": [
1030 | {
1031 | "key": "A",
1032 | "value": "B"
1033 | }
1034 | ],
1035 | "required": [
1036 | "key",
1037 | "value"
1038 | ],
1039 | "additionalProperties": false,
1040 | "properties": {
1041 | "key": {
1042 | "type": "string"
1043 | },
1044 | "value": {},
1045 | "previewValue": {}
1046 | }
1047 | },
1048 | {
1049 | "type": "object",
1050 | "examples": [
1051 | {
1052 | "key": "APP_SECRET",
1053 | "generateValue": true
1054 | }
1055 | ],
1056 | "required": [
1057 | "key",
1058 | "generateValue"
1059 | ],
1060 | "additionalProperties": false,
1061 | "properties": {
1062 | "key": {
1063 | "type": "string"
1064 | },
1065 | "generateValue": {
1066 | "type": "boolean"
1067 | },
1068 | "previewValue": {}
1069 | }
1070 | },
1071 | {
1072 | "type": "object",
1073 | "examples": [
1074 | {
1075 | "key": "SOME_SECRET",
1076 | "sync": false
1077 | }
1078 | ],
1079 | "required": [
1080 | "key",
1081 | "sync"
1082 | ],
1083 | "additionalProperties": false,
1084 | "properties": {
1085 | "key": {
1086 | "type": "string"
1087 | },
1088 | "sync": {
1089 | "type": "boolean",
1090 | "default": false
1091 | },
1092 | "previewValue": {}
1093 | }
1094 | }
1095 | ]
1096 | }
1097 | }
1098 | }
1099 | }
1100 | }
1101 | }
1102 | }
--------------------------------------------------------------------------------
/_share/bash/bash_completion.d/render.bash:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # bash completion support for render
3 |
4 | _render() {
5 | local word cur prev listFiles
6 | local -a opts
7 | COMPREPLY=()
8 | cur="${COMP_WORDS[COMP_CWORD]}"
9 | prev="${COMP_WORDS[COMP_CWORD-1]}"
10 | cmd="_"
11 | opts=()
12 | listFiles=0
13 |
14 | _render_complete() {
15 | local action="$1"; shift
16 | mapfile -t values < <( render completions complete "${action}" "${@}" )
17 | for i in "${values[@]}"; do
18 | opts+=("$i")
19 | done
20 | }
21 |
22 | _render_expand() {
23 | [ "$cur" != "${cur%\\}" ] && cur="$cur\\"
24 |
25 | # expand ~username type directory specifications
26 | if [[ "$cur" == \~*/* ]]; then
27 | # shellcheck disable=SC2086
28 | eval cur=$cur
29 |
30 | elif [[ "$cur" == \~* ]]; then
31 | cur=${cur#\~}
32 | # shellcheck disable=SC2086,SC2207
33 | COMPREPLY=( $( compgen -P '~' -u $cur ) )
34 | return ${#COMPREPLY[@]}
35 | fi
36 | }
37 |
38 | # shellcheck disable=SC2120
39 | _render_file_dir() {
40 | listFiles=1
41 | local IFS=$'\t\n' xspec #glob
42 | _render_expand || return 0
43 |
44 | if [ "${1:-}" = -d ]; then
45 | # shellcheck disable=SC2206,SC2207,SC2086
46 | COMPREPLY=( ${COMPREPLY[@]:-} $( compgen -d -- $cur ) )
47 | #eval "$glob" # restore glob setting.
48 | return 0
49 | fi
50 |
51 | xspec=${1:+"!*.$1"} # set only if glob passed in as $1
52 | # shellcheck disable=SC2206,SC2207
53 | COMPREPLY=( ${COMPREPLY[@]:-} $( compgen -f -X "$xspec" -- "$cur" ) $( compgen -d -- "$cur" ) )
54 | }
55 |
56 | __render() {
57 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region version commands config regions repo blueprint buildpack services deploys jobs custom-domains completions)
58 |
59 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
60 | return 0
61 | fi
62 | case "${prev}" in
63 | -h|--help) opts=() ;;
64 | -v|--verbose) ;;
65 | --non-interactive) ;;
66 | --pretty-json) ;;
67 | --json-record-per-line) ;;
68 | -p|--profile) opts=(); _render_complete string ;;
69 | -r|--region) opts=(); _render_complete string ;;
70 | esac
71 | }
72 |
73 | __render_version() {
74 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region)
75 |
76 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
77 | return 0
78 | fi
79 | case "${prev}" in
80 | -h|--help) opts=() ;;
81 | -v|--verbose) ;;
82 | --non-interactive) ;;
83 | --pretty-json) ;;
84 | --json-record-per-line) ;;
85 | -p|--profile) opts=(); _render_complete string version ;;
86 | -r|--region) opts=(); _render_complete string version ;;
87 | esac
88 | }
89 |
90 | __render_commands() {
91 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region)
92 |
93 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
94 | return 0
95 | fi
96 | case "${prev}" in
97 | -h|--help) opts=() ;;
98 | -v|--verbose) ;;
99 | --non-interactive) ;;
100 | --pretty-json) ;;
101 | --json-record-per-line) ;;
102 | -p|--profile) opts=(); _render_complete string commands ;;
103 | -r|--region) opts=(); _render_complete string commands ;;
104 | esac
105 | }
106 |
107 | __render_config() {
108 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region schema init)
109 |
110 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
111 | return 0
112 | fi
113 | case "${prev}" in
114 | -h|--help) opts=() ;;
115 | -v|--verbose) ;;
116 | --non-interactive) ;;
117 | --pretty-json) ;;
118 | --json-record-per-line) ;;
119 | -p|--profile) opts=(); _render_complete string config ;;
120 | -r|--region) opts=(); _render_complete string config ;;
121 | esac
122 | }
123 |
124 | __render_config_schema() {
125 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region)
126 |
127 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
128 | return 0
129 | fi
130 | case "${prev}" in
131 | -h|--help) opts=() ;;
132 | -v|--verbose) ;;
133 | --non-interactive) ;;
134 | --pretty-json) ;;
135 | --json-record-per-line) ;;
136 | -p|--profile) opts=(); _render_complete string config schema ;;
137 | -r|--region) opts=(); _render_complete string config schema ;;
138 | esac
139 | }
140 |
141 | __render_config_init() {
142 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region -f --force)
143 |
144 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
145 | return 0
146 | fi
147 | case "${prev}" in
148 | -h|--help) opts=() ;;
149 | -v|--verbose) ;;
150 | --non-interactive) ;;
151 | --pretty-json) ;;
152 | --json-record-per-line) ;;
153 | -p|--profile) opts=(); _render_complete string config init ;;
154 | -r|--region) opts=(); _render_complete string config init ;;
155 | -f|--force) ;;
156 | esac
157 | }
158 |
159 | __render_regions() {
160 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region)
161 |
162 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
163 | return 0
164 | fi
165 | case "${prev}" in
166 | -h|--help) opts=() ;;
167 | -v|--verbose) ;;
168 | --non-interactive) ;;
169 | --pretty-json) ;;
170 | --json-record-per-line) ;;
171 | -p|--profile) opts=(); _render_complete string regions ;;
172 | -r|--region) opts=(); _render_complete string regions ;;
173 | esac
174 | }
175 |
176 | __render_repo() {
177 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region from-template)
178 |
179 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
180 | return 0
181 | fi
182 | case "${prev}" in
183 | -h|--help) opts=() ;;
184 | -v|--verbose) ;;
185 | --non-interactive) ;;
186 | --pretty-json) ;;
187 | --json-record-per-line) ;;
188 | -p|--profile) opts=(); _render_complete string repo ;;
189 | -r|--region) opts=(); _render_complete string repo ;;
190 | esac
191 | }
192 |
193 | __render_repo_from_template() {
194 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region -o --output-directory -f --force --skip-cleanup)
195 | _render_complete string repo from-template
196 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
197 | return 0
198 | fi
199 | case "${prev}" in
200 | -h|--help) opts=() ;;
201 | -v|--verbose) ;;
202 | --non-interactive) ;;
203 | --pretty-json) ;;
204 | --json-record-per-line) ;;
205 | -p|--profile) opts=(); _render_complete string repo from-template ;;
206 | -r|--region) opts=(); _render_complete string repo from-template ;;
207 | -o|--output-directory) opts=(); _render_complete string repo from-template ;;
208 | -f|--force) ;;
209 | --skip-cleanup) ;;
210 | esac
211 | }
212 |
213 | __render_blueprint() {
214 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region launch)
215 |
216 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
217 | return 0
218 | fi
219 | case "${prev}" in
220 | -h|--help) opts=() ;;
221 | -v|--verbose) ;;
222 | --non-interactive) ;;
223 | --pretty-json) ;;
224 | --json-record-per-line) ;;
225 | -p|--profile) opts=(); _render_complete string blueprint ;;
226 | -r|--region) opts=(); _render_complete string blueprint ;;
227 | esac
228 | }
229 |
230 | __render_blueprint_launch() {
231 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region -l --link -r --remote)
232 | _render_complete string blueprint launch
233 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
234 | return 0
235 | fi
236 | case "${prev}" in
237 | -h|--help) opts=() ;;
238 | -v|--verbose) ;;
239 | --non-interactive) ;;
240 | --pretty-json) ;;
241 | --json-record-per-line) ;;
242 | -p|--profile) opts=(); _render_complete string blueprint launch ;;
243 | -r|--region) opts=(); _render_complete string blueprint launch ;;
244 | -l|--link) ;;
245 | -r|--remote) opts=(); _render_complete string blueprint launch ;;
246 | esac
247 | }
248 |
249 | __render_buildpack() {
250 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region init remove add)
251 |
252 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
253 | return 0
254 | fi
255 | case "${prev}" in
256 | -h|--help) opts=() ;;
257 | -v|--verbose) ;;
258 | --non-interactive) ;;
259 | --pretty-json) ;;
260 | --json-record-per-line) ;;
261 | -p|--profile) opts=(); _render_complete string buildpack ;;
262 | -r|--region) opts=(); _render_complete string buildpack ;;
263 | esac
264 | }
265 |
266 | __render_buildpack_init() {
267 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region -f --force --dir --skip-dockerfile)
268 | _render_complete string buildpack init
269 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
270 | return 0
271 | fi
272 | case "${prev}" in
273 | -h|--help) opts=() ;;
274 | -v|--verbose) ;;
275 | --non-interactive) ;;
276 | --pretty-json) ;;
277 | --json-record-per-line) ;;
278 | -p|--profile) opts=(); _render_complete string buildpack init ;;
279 | -r|--region) opts=(); _render_complete string buildpack init ;;
280 | -f|--force) ;;
281 | --dir) opts=(); _render_complete string buildpack init ;;
282 | --skip-dockerfile) ;;
283 | esac
284 | }
285 |
286 | __render_buildpack_remove() {
287 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region --dir)
288 | _render_complete string buildpack remove
289 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
290 | return 0
291 | fi
292 | case "${prev}" in
293 | -h|--help) opts=() ;;
294 | -v|--verbose) ;;
295 | --non-interactive) ;;
296 | --pretty-json) ;;
297 | --json-record-per-line) ;;
298 | -p|--profile) opts=(); _render_complete string buildpack remove ;;
299 | -r|--region) opts=(); _render_complete string buildpack remove ;;
300 | --dir) opts=(); _render_complete string buildpack remove ;;
301 | esac
302 | }
303 |
304 | __render_buildpack_add() {
305 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region --dir)
306 | _render_complete string buildpack add
307 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
308 | return 0
309 | fi
310 | case "${prev}" in
311 | -h|--help) opts=() ;;
312 | -v|--verbose) ;;
313 | --non-interactive) ;;
314 | --pretty-json) ;;
315 | --json-record-per-line) ;;
316 | -p|--profile) opts=(); _render_complete string buildpack add ;;
317 | -r|--region) opts=(); _render_complete string buildpack add ;;
318 | --dir) opts=(); _render_complete string buildpack add ;;
319 | esac
320 | }
321 |
322 | __render_services() {
323 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region show list tail ssh)
324 |
325 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
326 | return 0
327 | fi
328 | case "${prev}" in
329 | -h|--help) opts=() ;;
330 | -v|--verbose) ;;
331 | --non-interactive) ;;
332 | --pretty-json) ;;
333 | --json-record-per-line) ;;
334 | -p|--profile) opts=(); _render_complete string services ;;
335 | -r|--region) opts=(); _render_complete string services ;;
336 | esac
337 | }
338 |
339 | __render_services_show() {
340 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region --id)
341 |
342 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
343 | return 0
344 | fi
345 | case "${prev}" in
346 | -h|--help) opts=() ;;
347 | -v|--verbose) ;;
348 | --non-interactive) ;;
349 | --pretty-json) ;;
350 | --json-record-per-line) ;;
351 | -p|--profile) opts=(); _render_complete string services show ;;
352 | -r|--region) opts=(); _render_complete string services show ;;
353 | --id) opts=(); _render_complete string services show ;;
354 | esac
355 | }
356 |
357 | __render_services_list() {
358 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region --format --columns --name --type --env --service-region --ownerid --created-before --created-after --updated-before --updated-after)
359 |
360 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
361 | return 0
362 | fi
363 | case "${prev}" in
364 | -h|--help) opts=() ;;
365 | -v|--verbose) ;;
366 | --non-interactive) ;;
367 | --pretty-json) ;;
368 | --json-record-per-line) ;;
369 | -p|--profile) opts=(); _render_complete string services list ;;
370 | -r|--region) opts=(); _render_complete string services list ;;
371 | --format) opts=(); _render_complete string services list ;;
372 | --columns) opts=(); _render_complete string services list ;;
373 | --name) opts=(); _render_complete string services list ;;
374 | --type) opts=(); _render_complete string services list ;;
375 | --env) opts=(); _render_complete string services list ;;
376 | --service-region) opts=(); _render_complete string services list ;;
377 | --ownerid) opts=(); _render_complete string services list ;;
378 | --created-before) opts=(); _render_complete string services list ;;
379 | --created-after) opts=(); _render_complete string services list ;;
380 | --updated-before) opts=(); _render_complete string services list ;;
381 | --updated-after) opts=(); _render_complete string services list ;;
382 | esac
383 | }
384 |
385 | __render_services_tail() {
386 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region --raw --json --deploy-id --id)
387 |
388 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
389 | return 0
390 | fi
391 | case "${prev}" in
392 | -h|--help) opts=() ;;
393 | -v|--verbose) ;;
394 | --non-interactive) ;;
395 | --pretty-json) ;;
396 | --json-record-per-line) ;;
397 | -p|--profile) opts=(); _render_complete string services tail ;;
398 | -r|--region) opts=(); _render_complete string services tail ;;
399 | --raw) ;;
400 | --json) ;;
401 | --deploy-id) opts=(); _render_complete string services tail ;;
402 | --id) opts=(); _render_complete string services tail ;;
403 | esac
404 | }
405 |
406 | __render_services_ssh() {
407 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region --preserve-hosts --id)
408 | _render_complete string services ssh
409 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
410 | return 0
411 | fi
412 | case "${prev}" in
413 | -h|--help) opts=() ;;
414 | -v|--verbose) ;;
415 | --non-interactive) ;;
416 | --pretty-json) ;;
417 | --json-record-per-line) ;;
418 | -p|--profile) opts=(); _render_complete string services ssh ;;
419 | -r|--region) opts=(); _render_complete string services ssh ;;
420 | --preserve-hosts) ;;
421 | --id) opts=(); _render_complete string services ssh ;;
422 | esac
423 | }
424 |
425 | __render_deploys() {
426 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region list)
427 |
428 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
429 | return 0
430 | fi
431 | case "${prev}" in
432 | -h|--help) opts=() ;;
433 | -v|--verbose) ;;
434 | --non-interactive) ;;
435 | --pretty-json) ;;
436 | --json-record-per-line) ;;
437 | -p|--profile) opts=(); _render_complete string deploys ;;
438 | -r|--region) opts=(); _render_complete string deploys ;;
439 | esac
440 | }
441 |
442 | __render_deploys_list() {
443 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region --format --columns --service-id --start-time --end-time)
444 |
445 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
446 | return 0
447 | fi
448 | case "${prev}" in
449 | -h|--help) opts=() ;;
450 | -v|--verbose) ;;
451 | --non-interactive) ;;
452 | --pretty-json) ;;
453 | --json-record-per-line) ;;
454 | -p|--profile) opts=(); _render_complete string deploys list ;;
455 | -r|--region) opts=(); _render_complete string deploys list ;;
456 | --format) opts=(); _render_complete string deploys list ;;
457 | --columns) opts=(); _render_complete string deploys list ;;
458 | --service-id) opts=(); _render_complete string deploys list ;;
459 | --start-time) opts=(); _render_complete number deploys list ;;
460 | --end-time) opts=(); _render_complete number deploys list ;;
461 | esac
462 | }
463 |
464 | __render_jobs() {
465 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region list)
466 |
467 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
468 | return 0
469 | fi
470 | case "${prev}" in
471 | -h|--help) opts=() ;;
472 | -v|--verbose) ;;
473 | --non-interactive) ;;
474 | --pretty-json) ;;
475 | --json-record-per-line) ;;
476 | -p|--profile) opts=(); _render_complete string jobs ;;
477 | -r|--region) opts=(); _render_complete string jobs ;;
478 | esac
479 | }
480 |
481 | __render_jobs_list() {
482 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region --format --columns --service-id --status --created-before --created-after --started-before --started-after --finished-before --finished-after)
483 |
484 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
485 | return 0
486 | fi
487 | case "${prev}" in
488 | -h|--help) opts=() ;;
489 | -v|--verbose) ;;
490 | --non-interactive) ;;
491 | --pretty-json) ;;
492 | --json-record-per-line) ;;
493 | -p|--profile) opts=(); _render_complete string jobs list ;;
494 | -r|--region) opts=(); _render_complete string jobs list ;;
495 | --format) opts=(); _render_complete string jobs list ;;
496 | --columns) opts=(); _render_complete string jobs list ;;
497 | --service-id) opts=(); _render_complete string jobs list ;;
498 | --status) opts=(); _render_complete string jobs list ;;
499 | --created-before) opts=(); _render_complete string jobs list ;;
500 | --created-after) opts=(); _render_complete string jobs list ;;
501 | --started-before) opts=(); _render_complete string jobs list ;;
502 | --started-after) opts=(); _render_complete string jobs list ;;
503 | --finished-before) opts=(); _render_complete string jobs list ;;
504 | --finished-after) opts=(); _render_complete string jobs list ;;
505 | esac
506 | }
507 |
508 | __render_custom_domains() {
509 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region list)
510 |
511 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
512 | return 0
513 | fi
514 | case "${prev}" in
515 | -h|--help) opts=() ;;
516 | -v|--verbose) ;;
517 | --non-interactive) ;;
518 | --pretty-json) ;;
519 | --json-record-per-line) ;;
520 | -p|--profile) opts=(); _render_complete string custom-domains ;;
521 | -r|--region) opts=(); _render_complete string custom-domains ;;
522 | esac
523 | }
524 |
525 | __render_custom_domains_list() {
526 | opts=(-h --help -v --verbose --non-interactive --pretty-json --json-record-per-line -p --profile -r --region --format --columns --service-id --name --domain-type --verification-status --created-before --created-after)
527 |
528 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
529 | return 0
530 | fi
531 | case "${prev}" in
532 | -h|--help) opts=() ;;
533 | -v|--verbose) ;;
534 | --non-interactive) ;;
535 | --pretty-json) ;;
536 | --json-record-per-line) ;;
537 | -p|--profile) opts=(); _render_complete string custom-domains list ;;
538 | -r|--region) opts=(); _render_complete string custom-domains list ;;
539 | --format) opts=(); _render_complete string custom-domains list ;;
540 | --columns) opts=(); _render_complete string custom-domains list ;;
541 | --service-id) opts=(); _render_complete string custom-domains list ;;
542 | --name) opts=(); _render_complete string custom-domains list ;;
543 | --domain-type) opts=(); _render_complete string custom-domains list ;;
544 | --verification-status) opts=(); _render_complete string custom-domains list ;;
545 | --created-before) opts=(); _render_complete string custom-domains list ;;
546 | --created-after) opts=(); _render_complete string custom-domains list ;;
547 | esac
548 | }
549 |
550 | __render_completions() {
551 | opts=(-h --help bash fish zsh)
552 |
553 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
554 | return 0
555 | fi
556 | case "${prev}" in
557 | -h|--help) opts=() ;;
558 | esac
559 | }
560 |
561 | __render_completions_bash() {
562 | opts=(-h --help)
563 |
564 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
565 | return 0
566 | fi
567 | case "${prev}" in
568 | -h|--help) opts=() ;;
569 | esac
570 | }
571 |
572 | __render_completions_fish() {
573 | opts=(-h --help)
574 |
575 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
576 | return 0
577 | fi
578 | case "${prev}" in
579 | -h|--help) opts=() ;;
580 | esac
581 | }
582 |
583 | __render_completions_zsh() {
584 | opts=(-h --help)
585 |
586 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
587 | return 0
588 | fi
589 | case "${prev}" in
590 | -h|--help) opts=() ;;
591 | esac
592 | }
593 |
594 | for word in "${COMP_WORDS[@]}"; do
595 | case "${word}" in
596 | -*) ;;
597 | *)
598 | cmd_tmp="${cmd}_${word//[^[:alnum:]]/_}"
599 | if type "${cmd_tmp}" &>/dev/null; then
600 | cmd="${cmd_tmp}"
601 | fi
602 | esac
603 | done
604 |
605 | ${cmd}
606 |
607 | if [[ listFiles -eq 1 ]]; then
608 | return 0
609 | fi
610 |
611 | if [[ ${#opts[@]} -eq 0 ]]; then
612 | # shellcheck disable=SC2207
613 | COMPREPLY=($(compgen -f "${cur}"))
614 | return 0
615 | fi
616 |
617 | local values
618 | values="$( printf "\n%s" "${opts[@]}" )"
619 | local IFS=$'\n'
620 | # shellcheck disable=SC2207
621 | local result=($(compgen -W "${values[@]}" -- "${cur}"))
622 | if [[ ${#result[@]} -eq 0 ]]; then
623 | # shellcheck disable=SC2207
624 | COMPREPLY=($(compgen -f "${cur}"))
625 | else
626 | # shellcheck disable=SC2207
627 | COMPREPLY=($(printf '%q\n' "${result[@]}"))
628 | fi
629 |
630 | return 0
631 | }
632 |
633 | complete -F _render -o bashdefault -o default render
634 |
--------------------------------------------------------------------------------
/_test/invalid-render.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | - type: web
3 | name: sveltekit
4 | env: nerd
5 | buildCommand: 42
6 | startCommand: 106
7 |
--------------------------------------------------------------------------------
/_test/sveltekit-render.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | - type: web
3 | name: sveltekit
4 | env: node
5 | buildCommand: npm install && npm run build
6 | startCommand: node build/index.js
7 |
--------------------------------------------------------------------------------
/api/config.ts:
--------------------------------------------------------------------------------
1 | import { RuntimeConfiguration } from "../config/types/index.ts";
2 | import { APIKeyRequired } from "../errors.ts";
3 |
4 | export function apiKeyOrThrow(cfg: RuntimeConfiguration): string {
5 | if (!cfg.profile.apiKey) {
6 | throw new APIKeyRequired();
7 | }
8 |
9 | return cfg.profile.apiKey;
10 | }
11 |
12 | export function apiHost(cfg: RuntimeConfiguration) {
13 | return cfg.profile.apiHost ?? "api.render.com";
14 | }
15 |
--------------------------------------------------------------------------------
/api/error-handling.ts:
--------------------------------------------------------------------------------
1 | import { Log } from "../deps.ts";
2 | import { getPaths } from "../util/paths.ts";
3 |
4 | export async function handleApiErrors(logger: Log.Logger, fn: () => Promise): Promise {
5 | try {
6 | // for clarity: you almost never do `return await` but if we don't, the try-catch won't fire
7 | return await fn();
8 | } catch (err) {
9 | const configFile = (await getPaths()).configFile;
10 |
11 | if (err instanceof DOMException) {
12 | // TODO: can this be better?
13 | // DOMException.code is deprecated and not very useful. I assume these are relatively safe.
14 | if (err.message.includes("404")) {
15 | logger.error('Command received a 404 Not Found. This usually means that')
16 | logger.error('no resource could be found with a given name or ID. If you')
17 | logger.error('used a resource ID directly, e.g. `srv-12345678`, please check')
18 | logger.error('it for correctness. If you used a resource name or slug, it\'s')
19 | logger.error('likely that we couldn\'t find a resource so named.');
20 | } else if (err.message.includes("401")) {
21 | logger.error('Command received a 401 Unauthorized. This usually means that')
22 | logger.error('you haven\'t provided credentials to the Render API or the credentials')
23 | logger.error('are incorrect. Please check your API key in your config file,')
24 | logger.error(`stored at '${configFile}', and refresh it or add it if need be.`)
25 | } else if (err.message.includes("403")) {
26 | logger.error('Command received a 403 Forbidden. This usually means that while')
27 | logger.error('you have made a request with valid Render credentials, the API')
28 | logger.error('doesn\'t think you have access to that resource. Check to make sure')
29 | logger.error('you\'re using the right profile and that your user account is a member')
30 | logger.error('of the correct team.');
31 | }
32 | } else {
33 | logger.error("Unrecognized error: " + JSON.stringify(err, null, 2));
34 | }
35 |
36 | throw err;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/api/index.ts:
--------------------------------------------------------------------------------
1 | export { handleApiErrors } from './error-handling.ts';
2 | export * from './config.ts';
3 | export * from './requests.ts';
4 |
--------------------------------------------------------------------------------
/api/requests.ts:
--------------------------------------------------------------------------------
1 | // deno-lint-ignore-file no-explicit-any
2 | import { Log, QueryString } from "../deps.ts";
3 |
4 | import { RuntimeConfiguration } from "../config/types/index.ts";
5 | import { VERSION } from "../version.ts";
6 | import { apiHost, apiKeyOrThrow } from "./config.ts";
7 | import { handleApiErrors } from "./error-handling.ts";
8 |
9 | function queryStringify(query: Record) {
10 | return QueryString.stringify(query, {
11 | arrayFormat: 'comma',
12 | });
13 | }
14 |
15 | type httpMethod =
16 | | "GET"
17 | | "HEAD"
18 | | "POST"
19 | | "PUT"
20 | | "DELETE"
21 | | "CONNECT"
22 | | "OPTIONS"
23 | | "TRACE"
24 | | "PATCH";
25 |
26 | let apiReqCount = 0;
27 |
28 | export function getRequestRaw(
29 | logger: Log.Logger,
30 | cfg: RuntimeConfiguration,
31 | path: string,
32 | query: Record = {}
33 | ): Promise {
34 | return requestRaw(logger, cfg, path, query, "GET");
35 | }
36 |
37 | export function deleteRequestRaw(
38 | logger: Log.Logger,
39 | cfg: RuntimeConfiguration,
40 | path: string,
41 | query: Record = {}
42 | ): Promise {
43 | return requestRaw(logger, cfg, path, query, "DELETE");
44 | }
45 |
46 | function requestRaw(
47 | logger: Log.Logger,
48 | cfg: RuntimeConfiguration,
49 | path: string,
50 | query: Record = {},
51 | method: httpMethod = "GET"
52 | ): Promise {
53 | return handleApiErrors(logger, async () => {
54 | const reqNumber = apiReqCount++;
55 | const authorization = `Bearer ${apiKeyOrThrow(cfg)}`;
56 |
57 | if (!path.startsWith("/")) {
58 | path = "/" + path;
59 | }
60 |
61 | const url = `https://${apiHost(cfg)}/v1${path}?${queryStringify(query)}`;
62 |
63 | logger.debug(`api dispatch: ${reqNumber}: method: ${method} url ${url} (query: ${JSON.stringify(query)})`);
64 |
65 | const response = await fetch(url, {
66 | headers: {
67 | authorization,
68 | 'user-agent': `Render CLI/${VERSION}`,
69 | accept: 'application/json',
70 | },
71 | method: method,
72 | });
73 | if (!response.ok) {
74 | // this kind of has to be a DOMException because it encapsulates the notion of NetworkError
75 | // and we don't want to special-case WebSocketStream errors separately. Deno "is a browser",
76 | // in its mind, so this makes some sense.
77 | logger.error(`api error ${reqNumber}: ${JSON.stringify(response.json())}`);
78 | throw new DOMException(`Error calling ${url}: ${response.status} ${response.statusText}`);
79 | }
80 |
81 | return response;
82 | });
83 | }
84 |
85 | export async function getRequestJSON(
86 | logger: Log.Logger,
87 | cfg: RuntimeConfiguration,
88 | path: string,
89 | query: Record = {},
90 | ): Promise {
91 | const resp = await getRequestRaw(logger, cfg, path, query);
92 |
93 | return resp.json();
94 | }
95 |
96 | export async function getRequestJSONList(
97 | logger: Log.Logger,
98 | cfg: RuntimeConfiguration,
99 | dataKey: string,
100 | path: string,
101 | query: Record = {},
102 | ): Promise> {
103 | let cursor: string | null = null;
104 | const limit = Deno.env.get("RENDERCLI_LIST_LIMIT") ?? 100;
105 |
106 | let acc: Array = [];
107 |
108 | let iter = 0;
109 | while (true) {
110 | iter += 1;
111 | const q: typeof query = {
112 | limit,
113 | ...query,
114 | };
115 |
116 | logger.debug(`Query #${iter} with cursor: ${cursor}`);
117 |
118 | if (cursor) {
119 | q.cursor = cursor;
120 | }
121 |
122 | // without using `dataKey` as a unique symbol (not reasonable here) we're kind of
123 | // stuck with an 'any', since we're pretty far out of type-safety land right now.
124 | const list = await getRequestJSON>(logger, cfg, path, q);
125 | const items = list.map(i => i[dataKey]);
126 |
127 | acc = [
128 | ...acc,
129 | ...items,
130 | ];
131 | logger.debug(`Query #${iter} returned ${list.length} items. Acc now ${acc.length} items.`);
132 |
133 | if (list.length === 0) {
134 | break;
135 | }
136 |
137 | cursor = list[list.length - 1].cursor;
138 | }
139 |
140 | return acc;
141 | }
142 |
--------------------------------------------------------------------------------
/blueprints/validator.ts:
--------------------------------------------------------------------------------
1 | import { ajv } from "../util/ajv.ts";
2 |
3 | import RenderYAMLSchema from '../_data/render.schema.json' assert { type: 'json' };
4 | import { AjvErrorObject, YAML } from "../deps.ts";
5 |
6 |
7 | export type ValidateSchemaArgs =
8 | | { path: string }
9 | | { content: string }
10 | | { object: unknown }
11 | ;
12 |
13 | export type ValidateSchemaRetDetails = {
14 | jsonSchema?: Array,
15 | }
16 |
17 | export type ValidateSchemaRet =
18 | | [false, ValidateSchemaRetDetails]
19 | | [true, null]
20 | ;
21 |
22 | export async function validateSchema(args: ValidateSchemaArgs): Promise {
23 | if ('path' in args) {
24 | const decoder = new TextDecoder();
25 | const content = decoder.decode(await Deno.readFile(args.path));
26 | return validateSchema({ content });
27 | }
28 |
29 | if ('content' in args) {
30 | const object = YAML.load(args.content);
31 | return validateSchema({ object });
32 | }
33 |
34 | const isValid = ajv.validate(RenderYAMLSchema, args.object);
35 | if (isValid) {
36 | return [true, null];
37 | }
38 |
39 | return [false, { jsonSchema: ajv.errors ?? [] }];
40 | }
41 |
--------------------------------------------------------------------------------
/buildpack/constants.ts:
--------------------------------------------------------------------------------
1 | export const HEROKU_BUILDPACKS = [
2 | "heroku/ruby",
3 | "heroku/nodejs",
4 | "heroku/clojure",
5 | "heroku/python",
6 | "heroku/java",
7 | "heroku/scala",
8 | "heroku/php",
9 | "heroku/go",
10 | ];
11 |
--------------------------------------------------------------------------------
/buildpack/crud.ts:
--------------------------------------------------------------------------------
1 | import { ForceRequiredError } from "../errors.ts";
2 | import { ajv, assertType } from "../util/ajv.ts";
3 | import { pathExists } from "../util/paths.ts";
4 | import { DOCKERFILE_TEMPLATE } from "./templates/dockerfile_template.ts";
5 | import { RenderBuildpackFile } from "./types.ts";
6 |
7 | const BUILDPACKS_VALIDATOR = ajv.compile(RenderBuildpackFile);
8 |
9 | export async function initDockerfile(dir: string, force: boolean) {
10 | const f = `${dir}/Dockerfile.render`;
11 | if (await pathExists(f) && !force) {
12 | throw new ForceRequiredError('Dockerfile.render already exists and no --skip-dockerfile');
13 | }
14 |
15 | await Deno.writeTextFile(f, DOCKERFILE_TEMPLATE);
16 | }
17 |
18 | export async function initBuildpacksFile(dir: string, buildpacks: Array, force: boolean) {
19 | const f = `${dir}/.render-buildpacks.json`;
20 | if (await pathExists(f) && !force) {
21 | throw new ForceRequiredError('.render-buildpacks.json already exists');
22 | }
23 |
24 | return writeBuildpacksFile(dir, {
25 | version: 'v4',
26 | buildpacks,
27 | });
28 | }
29 |
30 | export async function readBuildpacksFile(dir: string): Promise {
31 | const f = `${dir}/.render-buildpacks.json`;
32 |
33 | const content = JSON.parse(await Deno.readTextFile(f));
34 | assertType(RenderBuildpackFile, content);
35 |
36 | return content;
37 | }
38 |
39 | export async function writeBuildpacksFile(dir: string, content: RenderBuildpackFile) {
40 | assertType(RenderBuildpackFile, content);
41 |
42 | const f = `${dir}/.render-buildpacks.json`;
43 |
44 | const json = JSON.stringify(content, null, 2);
45 | await Deno.writeTextFile(f, json);
46 | }
47 |
--------------------------------------------------------------------------------
/buildpack/templates/dockerfile_template.ts:
--------------------------------------------------------------------------------
1 | export const DOCKERFILE_TEMPLATE = `
2 | # "v4-" stacks use our new, more rigorous buildpacks management system. They
3 | # allow you to use multiple buildpacks in a single application, as well as to
4 | # use custom buildpacks.
5 | #
6 | # - \`v2-\` images work with heroku-import v3.x.
7 | # - \`v4-\` images work with heroku-import v4.x. (We synced the tags.)
8 |
9 | ARG IMPORT_VERSION=v4
10 | ARG HEROKU_STACK=\${IMPORT_VERSION}-heroku-22
11 | FROM ghcr.io/renderinc/heroku-app-builder:\${HEROKU_STACK} AS builder
12 |
13 |
14 | # Below, please specify any build-time environment variables that you need to
15 | # reference in your build (as called by your buildpacks). If you don't specify
16 | # the arg below, you won't be able to access it in your build. You can also
17 | # specify a default value, as with any Docker \`ARG\`, if appropriate for your
18 | # use case.
19 |
20 | # ARG MY_BUILD_TIME_ENV_VAR
21 | # ARG DATABASE_URL
22 |
23 | # The FROM statement above refers to an image with the base buildpacks already
24 | # in place. We then run the apply-buildpacks.py script here because, unlike our
25 | # \`v2\` image, this allows us to expose build-time env vars to your app.
26 | RUN /render/build-scripts/apply-buildpacks.py \${HEROKU_STACK}
27 |
28 | # We strongly recommend that you package a Procfile with your application, but
29 | # if you don't, we'll try to guess one for you. If this is incorrect, please
30 | # add a Procfile that tells us what you need us to run.
31 | RUN if [[ -f /app/Procfile ]]; then \
32 | /render/build-scripts/create-process-types "/app/Procfile"; \
33 | fi;
34 |
35 | # For running the app, we use a clean base image and also one without Ubuntu development packages
36 | # https://devcenter.heroku.com/articles/heroku-20-stack#heroku-20-docker-image
37 | FROM ghcr.io/renderinc/heroku-app-runner:\${HEROKU_STACK} AS runner
38 |
39 | # Here we copy your build artifacts from the build image to the runner so that
40 | # the image that we deploy to Render is smaller and, therefore, can start up
41 | # faster.
42 | COPY --from=builder --chown=1000:1000 /render /render/
43 | COPY --from=builder --chown=1000:1000 /app /app/
44 |
45 | # Here we're switching to a non-root user in the container to remove some categories
46 | # of container-escape attack.
47 | USER 1000:1000
48 | WORKDIR /app
49 |
50 | # This sources all /app/.profile.d/*.sh files before process start.
51 | # These are created by buildpacks, and you probably don't have to worry about this.
52 | # https://devcenter.heroku.com/articles/buildpack-api#profile-d-scripts
53 | ENTRYPOINT [ "/render/setup-env" ]
54 |
55 | # 3. By default, we run the 'web' process type defined in the app's Procfile
56 | # You may override the process type that is run by replacing 'web' with another
57 | # process type name in the CMD line below. That process type must have been
58 | # defined in the app's Procfile during build.
59 | CMD [ "/render/process/web" ]
60 | `;
61 |
--------------------------------------------------------------------------------
/buildpack/types.ts:
--------------------------------------------------------------------------------
1 | import { type Static, Type } from "../deps.ts";
2 |
3 | // `v4` is the assumed value of any buildpack until we change it.
4 | export const RenderBuildpackFile = Type.Object({
5 | version: Type.Optional(Type.Literal('v4')),
6 | buildpacks: Type.Array(Type.String(), { minItems: 1 }),
7 | });
8 | export type RenderBuildpackFile = Static;
9 |
--------------------------------------------------------------------------------
/buildpack/validate.ts:
--------------------------------------------------------------------------------
1 | import { HEROKU_BUILDPACKS } from "./constants.ts";
2 |
3 | export const isHerokuBuildpack = (bp: string) => HEROKU_BUILDPACKS.includes(bp);
4 | export const isGitUrl = (bp: string) => bp.startsWith("https://") && bp.endsWith(".git");
5 | export const isTarball = (bp: string) => bp.startsWith("https://") && (bp.endsWith(".tgz") || bp.endsWith(".tar.gz"));
6 |
7 | export function isValidBuildpackEntry(bp: string) {
8 | return (
9 | isHerokuBuildpack(bp) ||
10 | isGitUrl(bp) ||
11 | isTarball(bp)
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/commands/_helpers.ts:
--------------------------------------------------------------------------------
1 | import { getConfig } from "../config/index.ts";
2 | import { RuntimeConfiguration } from "../config/types/index.ts";
3 | import { Cliffy, Log } from '../deps.ts';
4 | import { getLogger, NON_INTERACTIVE, renderInteractiveOutput, renderJsonOutput } from "../util/logging.ts";
5 | import { RenderCLIError } from "../errors.ts";
6 |
7 | const { Command } = Cliffy;
8 |
9 | export type GlobalOptions = {
10 | verbose?: true; // this is a load-bearing 'true'
11 | nonInteractive?: true; // ditto
12 | prettyJson?: true;
13 | profile?: string;
14 | region?: string;
15 | }
16 |
17 | // @ts-ignore Deno is confused, but this is fine; TODO: resolve
18 | export const Subcommand = Command;
19 |
20 | export async function withConfig(fn: (cfg: RuntimeConfiguration) => Promise) {
21 | const config = await getConfig();
22 |
23 | return fn(config);
24 | }
25 |
26 | export type ErrorContext = {
27 | path?: string,
28 | }
29 |
30 | export function printErrors(logger: Log.Logger, err: unknown) {
31 | if (err instanceof Deno.errors.NotFound) {
32 | logger.error(`Path not found or unreadable, but we should be giving you a better error for '${err?.constructor?.name ?? 'CLASS_NAME_NOT_FOUND'}'. Please file an issue so we can help.`, err);
33 | } else if (err instanceof RenderCLIError) {
34 | logger.error(err.message);
35 | } else {
36 | logger.error("Unrecognized error; dumping to console.error for full trace.");
37 | console.error(err);
38 | }
39 | }
40 |
41 |
42 | export interface InterrogativeAction {
43 | /**
44 | * The _interrogative form_ of an interactive action. A standard action that uses
45 | * this argument form MUST be async (because it is expected that you use Cliffy's
46 | * `prompt` module to ask questions of the user). No output handling will be
47 | * performed on your behalf; everything should be handled in your method.
48 | *
49 | * Should return the exit code for the command.
50 | *
51 | * https://cliffy.io/docs/prompt
52 | */
53 | interactive: (logger: Log.Logger) => Promise | number,
54 | }
55 |
56 | export interface ProcessingAction {
57 | /**
58 | * Shared code between interactive and non-interactive modes. Whatever is returned from
59 | * this function will be passed to `interactive` or `nonInteractive`, respectively.
60 | */
61 | processing: (logger: Log.Logger, isNonInteractive: boolean) => T | Promise;
62 |
63 | /**
64 | * The interactive formatter for this action. It's expected that all output that is
65 | * not tabular be printed via `logger`, and tabular content to be printed via Cliffy's
66 | * `table` facility. https://cliffy.io/docs/table
67 | */
68 | interactive: (result: T, logger: Log.Logger) => void | Promise;
69 |
70 | /**
71 | * The non-interactive formatter for this action. If this is not set, the CLI will
72 | * bail before processing when `--non-interactive` is passed or when stdout isn't
73 | * a TTY.
74 | *
75 | * You are expected to print only human-readable debug data to `logger`. Any data
76 | * intended to be consumed (e.g., piped to another application) MUST be passed ONLY
77 | * to `console.log`.
78 | */
79 | nonInteractive?: (result: T, logger: Log.Logger) => void | Promise;
80 |
81 | /**
82 | * Unifies return code logic. After either interactive() or nonInteractive() are called,
83 | * this will return the exit code for the command.
84 | *
85 | * If you return `true` or an array that is not empty, the return code will be 0. If you
86 | * return `false`, `null`, or `undefined`, the return code will be 1. If you return a
87 | * number, the exit code will be that number.
88 | */
89 | exitCode: (result: T) => unknown;
90 | }
91 |
92 |
93 | export type StandardActionArgs =
94 | | InterrogativeAction
95 | | ProcessingAction
96 | ;
97 |
98 | function computeExitCode(exitResult: unknown): number {
99 | if (Array.isArray(exitResult)) {
100 | return exitResult.length > 0 ? 0 : 1;
101 | }
102 | if (typeof(exitResult) === 'number') {
103 | return exitResult;
104 | }
105 |
106 | return exitResult ? 0 : 1;
107 | }
108 |
109 | export function standardAction(
110 | args: StandardActionArgs,
111 | ) {
112 |
113 | return (async () => {
114 | const logger = await getLogger();
115 |
116 | if (NON_INTERACTIVE && !('nonInteractive' in args)) {
117 | logger.error("This command can only be run in interactive mode.");
118 | Deno.exit(3);
119 | }
120 |
121 | try {
122 | if (!('processing' in args)) {
123 | logger.debug("Entering interrogative action.");
124 | // interrogative, interactive action
125 | await args.interactive(logger);
126 | } else {
127 | logger.debug("Performing processing.");
128 | const result = await args.processing(logger, NON_INTERACTIVE);
129 |
130 | if (NON_INTERACTIVE) {
131 | logger.debug("Performing non-interactive rendering.");
132 |
133 | if (!args.nonInteractive) {
134 | throw new Error("firewall: got to non-interactive rendering of an action with no non-interactive renderer; should have bailed earlier?");
135 | }
136 |
137 | await args.nonInteractive(result, logger);
138 | } else {
139 | logger.debug("Performing interactive rendering.");
140 | await args.interactive(result, logger);
141 | }
142 |
143 | const exitResult = args.exitCode(result);
144 | Deno.exit(computeExitCode(exitResult));
145 | }
146 | } catch (err) {
147 | printErrors(logger, err);
148 | Deno.exit(2);
149 | }
150 | })();
151 | }
152 |
153 | export type APIGetActionArgs =
154 | | {
155 | processing: (logger: Log.Logger, isNonInteractive: boolean) => T | Promise;
156 |
157 | format?: string,
158 | tableColumns?: string[],
159 | };
160 |
161 | export function apiGetAction(args: APIGetActionArgs) {
162 | return standardAction({
163 | processing: args.processing,
164 | interactive: (items: unknown | Array, logger: Log.Logger) => {
165 | if (items && (!Array.isArray(items) || items.length > 0)) {
166 | renderInteractiveOutput(items, args.tableColumns, args.format);
167 | } else {
168 | logger.warning("No results found.");
169 | }
170 | },
171 | nonInteractive: (items: unknown | Array) => {
172 | renderJsonOutput(items);
173 | },
174 | exitCode: (items: unknown | Array) =>
175 | Array.isArray(items)
176 | ? items.length > 0
177 | ? 0
178 | : 1
179 | : !!items,
180 | });
181 | }
182 |
--------------------------------------------------------------------------------
/commands/blueprint/edit.ts:
--------------------------------------------------------------------------------
1 | import { interactivelyEditBlueprint } from "../../new/blueprint/index.ts";
2 | import { Subcommand } from "../_helpers.ts";
3 |
4 | const desc =
5 | `Interactively edits a Render Blueprint (render.yaml) file.`;
6 |
7 | export const blueprintNewCommand =
8 | new Subcommand()
9 | .name('new')
10 | .description(desc)
11 | .arguments<[string]>("")
12 | .action((_opts, path = './render.yaml') => interactivelyEditBlueprint(path));
13 |
--------------------------------------------------------------------------------
/commands/blueprint/from-template.ts:
--------------------------------------------------------------------------------
1 | import { Log } from "../../deps.ts";
2 | import { templateNewProject, writeExampleBlueprint } from "../../new/repo/index.ts";
3 | import { standardAction, Subcommand } from "../_helpers.ts";
4 |
5 | const desc =
6 | `Adds a Render Blueprint (render.yaml) to this repo, based on a template.
7 |
8 | You can fetch a Render Blueprint from a repo via any of the following identifiers:
9 |
10 | repo-name
11 | repo-name@gitref
12 | user/repo-name
13 | user/repo-name@gitref
14 | github:user/repo-name
15 | github:user/repo-name@gitref
16 |
17 | If \`user\` is not provided, \`render-examples\` is assumed. If no source prefix is provided, \`github\` is assumed.
18 |
19 | At present, \`render blueprint from-template\` does not support private repositories.
20 |
21 | Once you've added a Render Blueprint to your repo, you can run \`render blueprint launch\` to deploy your Blueprint.
22 |
23 | (Future TODO: enable \`gitlab:\` prefix, enable arbitrary Git repositories, enable private repositories.)`;
24 |
25 | export const blueprintFromTemplateCommand =
26 | new Subcommand()
27 | .name('from-template')
28 | .description(desc)
29 | .arguments<[string]>("")
30 | .option("--directory ", "directory to write the blueprint to (default: current directory)")
31 | .option("--skip-cleanup", "skips cleaning up tmpdir (on success or failure)")
32 | .action((opts, identifier) =>
33 | standardAction({
34 | interactive: async (_logger: Log.Logger): Promise => {
35 | await writeExampleBlueprint({
36 | identifier,
37 | repoDir: opts.directory,
38 | skipCleanup: opts.skipCleanup,
39 | });
40 | return 0;
41 | }
42 | }));
43 |
--------------------------------------------------------------------------------
/commands/blueprint/index.ts:
--------------------------------------------------------------------------------
1 | import { Subcommand } from "../_helpers.ts";
2 | import { blueprintFromTemplateCommand } from "./from-template.ts";
3 | import { blueprintLaunchCommand } from "./launch.ts";
4 |
5 | const desc =
6 | `Commands for interacting with Render Blueprints (render.yaml files).`;
7 |
8 | export const blueprintCommand =
9 | new Subcommand()
10 | .name("blueprint")
11 | .description(desc)
12 | .action(function() {
13 | this.showHelp();
14 | Deno.exit(1);
15 | })
16 | // .command("new", blueprintNewCommand)
17 | .command("launch", blueprintLaunchCommand)
18 | .command("from-template", blueprintFromTemplateCommand)
19 | // .command("validate", blueprintValidateCommand)
20 | ;
21 |
--------------------------------------------------------------------------------
/commands/blueprint/launch.ts:
--------------------------------------------------------------------------------
1 | import { sleep } from "https://deno.land/x/sleep@v1.2.1/sleep.ts";
2 | import { Log, openAWebsite } from "../../deps.ts";
3 | import { gitUrlToHttpsUrl, listRemotes } from "../../util/git.ts";
4 | import { RenderCLIError } from "../../errors.ts";
5 | import { standardAction, Subcommand } from "../_helpers.ts";
6 |
7 | const desc =
8 | `Opens Render Deploy for this repo.
9 |
10 | This will open Render Deploy for the \`origin\` upstream for the repo (for the current working directory) or a remote repo that you've specified. You must have created a GitHub or GitLab repo and pushed to it for this to work!
11 |
12 | If it can't open your browser, it'll provide you a link instead.
13 |
14 | NOTE: This is only tested for GitHub and GitLab HTTPS repos. Git SSH URLs will be converted to HTTPS as best we can.`;
15 |
16 | export const blueprintLaunchCommand =
17 | new Subcommand()
18 | .name('new')
19 | .description(desc)
20 | .option("-l, --link", "Prints out the Render Deploy URL rather than attempting to open it.")
21 | .option("-r, --remote ", "The remote to use")
22 | .arguments<[string]>("[path:string]")
23 | .action((opts, path) => standardAction({
24 | processing: async () => {
25 | let httpsGitUrl: string;
26 | if (path) {
27 | httpsGitUrl = gitUrlToHttpsUrl(path);
28 | } else {
29 | const originName = opts.remote ?? 'origin';
30 | const remotes = await listRemotes({ https: true });
31 |
32 | const originUrl = remotes[originName];
33 | if (!originUrl) {
34 | throw new RenderCLIError(`Could not find an '${originName}' upstream for this repo. Upstreams found: ${Object.keys(remotes).join(', ')}`);
35 | }
36 | httpsGitUrl = originUrl;
37 | }
38 |
39 | return `https://render.com/deploy?repo=${httpsGitUrl}`;
40 | },
41 | interactive: async (httpsGitUrl: string, logger: Log.Logger) => {
42 | if (opts.link) {
43 | console.log(httpsGitUrl);
44 | } else {
45 | logger.info(`Taking you to the deploy page: ${httpsGitUrl}`);
46 | await sleep(1);
47 |
48 | const p = await openAWebsite(httpsGitUrl);
49 |
50 | if (!(await p.status()).success) {
51 | logger.error(`Could not automatically open browser. Please navigate to this URL directly: ${httpsGitUrl}`);
52 | }
53 | }
54 | },
55 | nonInteractive: (httpsGitUrl: string) => {
56 | console.log(httpsGitUrl);
57 | },
58 | exitCode: () => 0,
59 | }));
60 |
--------------------------------------------------------------------------------
/commands/buildpack/add.ts:
--------------------------------------------------------------------------------
1 | import { readBuildpacksFile, writeBuildpacksFile } from "../../buildpack/crud.ts";
2 | import { isValidBuildpackEntry } from "../../buildpack/validate.ts";
3 | import { RenderCLIError } from "../../errors.ts";
4 | import { getLogger } from "../../util/logging.ts";
5 | import { Subcommand } from "../_helpers.ts";
6 |
7 | const desc =
8 | `Adds buildpacks to your .render-buildpacks.json file.`;
9 |
10 | export const buildpackAddCommand =
11 | new Subcommand()
12 | .name('new')
13 | .description(desc)
14 | .option("--dir ", "the directory in which to run this command")
15 | .arguments("")
16 | .action(async (opts, ...buildpacks) => {
17 | const dir = opts.dir ?? Deno.cwd();
18 | const logger = await getLogger();
19 |
20 | if (buildpacks.length === 0) {
21 | throw new RenderCLIError("At least one buildpack must be specified.");
22 | }
23 |
24 | const buildpacksFile = await readBuildpacksFile(dir);
25 |
26 | for (const bp of buildpacks) {
27 | if (!isValidBuildpackEntry(bp)) {
28 | throw new RenderCLIError(`Invalid buildpack entry: ${bp}`);
29 | }
30 |
31 | const idx = buildpacksFile.buildpacks.indexOf(bp);
32 | if (idx >= 0) {
33 | throw new RenderCLIError(`Buildpack entry '${bp}' already exists.`);
34 | }
35 |
36 | logger.info(`Adding '${bp}' to ${dir}/.render-buildpacks.json.`);
37 | buildpacksFile.buildpacks.push(bp);
38 | }
39 |
40 | await writeBuildpacksFile(dir, buildpacksFile);
41 | });
42 |
--------------------------------------------------------------------------------
/commands/buildpack/index.ts:
--------------------------------------------------------------------------------
1 | import { Subcommand } from "../_helpers.ts";
2 | import { buildpackAddCommand } from "./add.ts";
3 | import { buildpackInitCommand } from "./init.ts";
4 | import { buildpackRemoveCommand } from "./remove.ts";
5 |
6 | const desc =
7 | `Commands for using buildpacks in Render`;
8 |
9 | export const buildpackCommand =
10 | new Subcommand()
11 | .name("buildpack")
12 | .description(desc)
13 | .action(function() {
14 | this.showHelp();
15 | Deno.exit(1);
16 | })
17 | .command("init", buildpackInitCommand)
18 | .command("remove", buildpackRemoveCommand)
19 | .command("add", buildpackAddCommand)
20 | ;
21 |
--------------------------------------------------------------------------------
/commands/buildpack/init.ts:
--------------------------------------------------------------------------------
1 | import { Cliffy } from "../../deps.ts";
2 | import { HEROKU_BUILDPACKS } from "../../buildpack/constants.ts";
3 | import { initBuildpacksFile, initDockerfile } from "../../buildpack/crud.ts";
4 | import { isValidBuildpackEntry } from "../../buildpack/validate.ts";
5 | import { standardAction, Subcommand } from "../_helpers.ts";
6 | import { pathExists } from "../../util/paths.ts";
7 | import { ForceRequiredError } from "../../errors.ts";
8 |
9 | const desc =
10 | `Initializes a repository for use with Heroku-style buildpacks within Render.
11 |
12 | You can learn more about buildpacks and Heroku migration at:
13 | https://render.com/docs/migrate-from-heroku
14 |
15 | Similar to our \`heroku-import\` tool, this command will create a
16 | \`Dockerfile.render\` file and a \`.render-buildpacks.json\` file in the
17 | specified directory. If a path is specified, that path will be used
18 | as the first buildpack in \`.render-buildpacks.json\`; otherwise, you
19 | will be asked for one.`;
20 |
21 | export const buildpackInitCommand =
22 | new Subcommand()
23 | .name('new')
24 | .description(desc)
25 | .option("-f, --force", "overwrites existing files if found.")
26 | .option("--dir ", "the directory in which to run this command")
27 | .option("--skip-dockerfile", "don't emit a Dockerfile")
28 | .arguments("[buildpacks...:string]")
29 | .action((opts, ...buildpacks) => standardAction({
30 | interactive: async () => {
31 | const dir = opts.dir ?? '.';
32 |
33 | // this is duplicated in the buildpack crud but we want to fail before we ask
34 | // the user questions that require mental effort
35 | const files = [
36 | `${dir}/.render-buildpacks.json`,
37 | ];
38 | (!opts.skipDockerfile) && files.push(`${dir}/Dockerfile`);
39 | for (const f of files) {
40 | if (await pathExists(f) && !opts.force) {
41 | throw new ForceRequiredError(`${f} exists`);
42 | }
43 | }
44 |
45 | const initialBuildpacks: Array = buildpacks ?? [];
46 |
47 | if (initialBuildpacks.length === 0) {
48 | const resp1 = await Cliffy.prompt([
49 | {
50 | name: "buildpacks",
51 | message: "Select appropriate buildpacks (space to toggle)",
52 | type: Cliffy.Checkbox,
53 | options: [
54 | ...HEROKU_BUILDPACKS,
55 | 'I need a custom buildpack',
56 | ],
57 | }
58 | ]);
59 |
60 | initialBuildpacks.push(...(resp1.buildpacks ?? []).filter(bp => HEROKU_BUILDPACKS.includes(bp)));
61 |
62 | if (resp1.buildpacks?.includes("I need a custom buildpack")) {
63 | console.log(
64 | `
65 | Render's buildpack system accepts either of the following:
66 |
67 | - a git url, e.g. \`https://github.com/heroku/heroku-geo-buildpack.git\`
68 | - a full path to a tarball, e.g.
69 | \`https://github.com/username/repo/archive/refs/heads/main.tar.gz\`
70 |
71 | Enter a blank line to finish entering custom buildpacks. Also remember that
72 | you can add more buildpacks later with \`render buildpack add\`!
73 | `);
74 |
75 | while (true) {
76 | const { bpUrl } = await Cliffy.prompt([
77 | {
78 | name: 'bpUrl',
79 | message: "enter a URL for a buildpack (blank line to finish)",
80 | type: Cliffy.Input,
81 | },
82 | ]);
83 |
84 | if (!bpUrl || bpUrl === '') {
85 | break;
86 | }
87 |
88 | if (!isValidBuildpackEntry(bpUrl)) {
89 | console.error(`!!! '${bpUrl}' isn't a valid buildpack URL.`);
90 | } else {
91 | console.log(`${bpUrl} added.`);
92 | initialBuildpacks.push(bpUrl);
93 | }
94 | }
95 | }
96 | }
97 |
98 | (!opts.skipDockerfile) && await initDockerfile(dir, !!opts.force);
99 | await initBuildpacksFile(dir, initialBuildpacks, !!opts.force);
100 |
101 | console.log(
102 | `
103 |
104 | ${initialBuildpacks.length} buildpacks configured. You're good to go!
105 |
106 | We've created the following files for you:
107 |
108 | ${files.map(f => `- ${f}`).join('\n')}
109 |
110 | You can now use this Dockerfile on Render either by creating a Blueprint, also
111 | known as a render.yaml file, or by adding a service manually to the Render
112 | Dashboard.
113 |
114 | - For more information on using Dockerfiles on Render, and a step-by-step guide
115 | to adding a Docker service via the Render Dashboard, please visit
116 | https://rndr.in/dockerfiles.
117 | - For more information on Blueprints, please visit https://rndr.in/blueprints.
118 |
119 | As always if you run into any trouble, Render's support team is standing by to
120 | help. You can reach them by emailing support@render.com.
121 |
122 | Thanks for using Render!
123 |
124 | `
125 | );
126 |
127 | return 0;
128 | }
129 | }));
130 |
--------------------------------------------------------------------------------
/commands/buildpack/remove.ts:
--------------------------------------------------------------------------------
1 | import { readBuildpacksFile, writeBuildpacksFile } from "../../buildpack/crud.ts";
2 | import { Cliffy } from "../../deps.ts";
3 | import { RenderCLIError } from "../../errors.ts";
4 | import { getLogger, NON_INTERACTIVE } from "../../util/logging.ts";
5 | import { Subcommand } from "../_helpers.ts";
6 |
7 | const desc =
8 | `Removes buildpacks from your .render-buildpacks.json file.`;
9 |
10 | export const buildpackRemoveCommand =
11 | new Subcommand()
12 | .name('new')
13 | .description(desc)
14 | .option("--dir ", "the directory in which to run this command")
15 | .arguments("[buildpacks...:string]")
16 | .action(async (opts, ...buildpacks) => {
17 | const dir = opts.dir ?? Deno.cwd();
18 | const logger = await getLogger();
19 | const buildpacksToRemove = [...buildpacks];
20 |
21 | const buildpacksFile = await readBuildpacksFile(dir);
22 |
23 | if (buildpacksToRemove.length === 0) {
24 | if (NON_INTERACTIVE) {
25 | throw new RenderCLIError('Buildpacks must be specified as arguments in non-interactive mode.');
26 | }
27 |
28 | const responses = await Cliffy.prompt([{
29 | name: 'buildpack',
30 | message: 'Select buildpacks to remove (toggle with space), or hit Ctrl-C to cancel.',
31 | type: Cliffy.Checkbox,
32 | options: buildpacksFile.buildpacks,
33 | }]);
34 |
35 | if (!responses.buildpack) {
36 | Deno.exit(1);
37 | }
38 |
39 | buildpacksToRemove.push(...responses.buildpack);
40 | }
41 |
42 | for (const bp of buildpacksToRemove) {
43 | const idx = buildpacksFile.buildpacks.indexOf(bp);
44 | if (idx < 0) {
45 | logger.warning(`No buildpack '${bp}' found in buildpack file in ${dir}/.render-buildpacks.json.`);
46 | continue;
47 | }
48 |
49 | logger.info(`Removing '${bp}' from ${dir}/.render-buildpacks.json.`);
50 | buildpacksFile.buildpacks.splice(idx, 1);
51 | }
52 |
53 | await writeBuildpacksFile(dir, buildpacksFile);
54 | });
55 |
--------------------------------------------------------------------------------
/commands/commands.ts:
--------------------------------------------------------------------------------
1 | import { Cliffy, sortBy } from "../deps.ts";
2 | import { renderJsonOutput } from "../util/logging.ts";
3 | import { standardAction, Subcommand } from "./_helpers.ts";
4 |
5 | const desc =
6 | `Lists all commands and subcommands.`;
7 |
8 | type CmdInfo = {
9 | fullName: string;
10 | shortDesc: string;
11 | }
12 |
13 | type CmdTreeNode = {
14 | name: string;
15 | description: string;
16 | subcommands: Array;
17 | }
18 |
19 | export const commandsCommand =
20 | new Subcommand()
21 | .name('commands')
22 | .description(desc)
23 | .action(function () {
24 | return standardAction({
25 | processing: (logger) => {
26 | // TODO: some odd typing here. are the Deno imports correct?
27 | const rootCommand = this.getGlobalParent() as Cliffy.Command;
28 |
29 | function listCommands(cmd: Cliffy.Command): CmdTreeNode {
30 | if (!cmd) {
31 | logger.debug("can't happen: command was falsy in 'render commands'?");
32 | throw new Error();
33 | }
34 |
35 | const subcommands = sortBy(
36 | cmd.getCommands(false).map(subcmd => listCommands(subcmd)),
37 | (i: CmdTreeNode) => i.name,
38 | );
39 |
40 | const ret: CmdTreeNode = {
41 | name: cmd.getName(),
42 | description: cmd.getDescription().split("\n")[0].trim(),
43 | subcommands,
44 | }
45 |
46 | return ret;
47 | }
48 |
49 | return listCommands(rootCommand);
50 | },
51 | interactive: (result, _logger) => {
52 | function printCommand(cmd: CmdTreeNode, cmdPath: Array = [], indent = "") {
53 | if (cmdPath.length !== 0) {
54 | const precedingPath = Cliffy.colors.cyan(cmdPath.join(" "));
55 | const currentNode = Cliffy.colors.brightWhite(cmd.name);
56 | console.log(`${indent}${precedingPath} ${currentNode}: ${cmd.description}`);
57 | }
58 |
59 | cmd.subcommands.forEach(subcmd => {
60 | printCommand(
61 | subcmd,
62 | [...cmdPath, cmd.name],
63 | indent + (cmdPath.length === 0 ? '' : ' \\- '),
64 | );
65 | });
66 | }
67 |
68 | printCommand(result);
69 | console.log();
70 | console.log(`Remember that you can always pass ${Cliffy.colors.brightWhite('--help')} to any command (for example,`);
71 | console.log(`${Cliffy.colors.cyan("render services tail --help")}) to see more information about it.`);
72 | },
73 | nonInteractive: (result) => {
74 | renderJsonOutput(result);
75 | },
76 | exitCode: () => 0,
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/commands/config/_shared.ts:
--------------------------------------------------------------------------------
1 | import { ALL_REGIONS, Region } from "../../config/types/enums.ts";
2 | import { ConfigLatest, ProfileLatest } from "../../config/types/index.ts";
3 | import { Cliffy, Log, YAML } from "../../deps.ts";
4 | import { RenderCLIError } from "../../errors.ts";
5 |
6 | export async function requestProfileInfo(): Promise {
7 | const resp = await Cliffy.prompt([
8 | {
9 | name: 'region',
10 | message: "What is this profile's default region?",
11 | type: Cliffy.Select,
12 | options: ALL_REGIONS,
13 | default: 'oregon',
14 | hint: "You can access any region with the CLI; this'll just save you some typing."
15 | },
16 | {
17 | name: 'apiKey',
18 | message: 'Provide your API key from the Dashboard:',
19 | label: "API key",
20 | type: Cliffy.Secret,
21 | minLength: 32,
22 | hint: "This will begin with `rnd_` and be 32 characters long."
23 | }
24 | ]);
25 |
26 | // should never be hit, but Cliffy doesn't have a way to specify that apiKey
27 | // won't be falsy or zero-length.
28 | if (!resp.apiKey) {
29 | throw new RenderCLIError("No api key provided; exiting.");
30 | }
31 |
32 | return {
33 | defaultRegion: (resp.region ?? 'oregon') as Region,
34 | apiKey: resp.apiKey,
35 | };
36 | }
37 |
38 | export async function writeProfile(logger: Log.Logger, configFile: string, cfg: ConfigLatest) {
39 | logger.debug(`writing config to '${configFile}'`);
40 | await Deno.writeTextFile(configFile, YAML.dump(cfg));
41 | await chmodConfigIfPossible(logger, configFile);
42 | }
43 |
44 | export async function chmodConfigIfPossible(logger: Log.Logger, configFile: string) {
45 | if (Deno.build.os === 'windows') {
46 | logger.warning(`Deno does not currently support file permissions on Windows. As such,`);
47 | logger.warning(`'${configFile}' has user-level default permissions. On single-user`);
48 | logger.warning(`systems, this is fine. On multi-user systems, you may wish to further`);
49 | logger.warning(`secure your Render credentials.`)
50 | logger.warning('');
51 | logger.warning('See https://rndr.in/windows-file-acl for potential solutions.');
52 | } else {
53 | logger.debug(`chmod '${configFile}' to 600`);
54 | await Deno.chmod(configFile, 0o600);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/commands/config/add-profile.ts:
--------------------------------------------------------------------------------
1 | import { ConfigLatest } from "../../config/types/index.ts";
2 | import { Log, YAML } from "../../deps.ts";
3 | import { ForceRequiredError, InitRequiredError } from "../../errors.ts";
4 | import { ajv } from "../../util/ajv.ts";
5 | import { pathExists } from "../../util/paths.ts";
6 | import { getPaths } from "../../util/paths.ts";
7 | import { standardAction, Subcommand } from "../_helpers.ts";
8 | import { requestProfileInfo, writeProfile } from "./_shared.ts";
9 |
10 | const desc =
11 | `Adds a new profile to a Render CLI config file.`;
12 |
13 | export const configAddProfileCommand =
14 | new Subcommand()
15 | .name('init')
16 | .description(desc)
17 | .option("-f, --force", "overwrites existing profile if found.")
18 | .arguments("")
19 | .action((opts, profileName) => standardAction({
20 | interactive: async (logger: Log.Logger) => {
21 | const { configFile } = await getPaths();
22 |
23 | if (!pathExists(configFile)) {
24 | throw new InitRequiredError(`Render config file does not exist at '${configFile}'`);
25 | }
26 |
27 | logger.debug({ configFile }, "Loading config.");
28 | const cfg = YAML.load(await Deno.readTextFile(configFile)) as ConfigLatest;
29 |
30 | logger.debug("Validating config...");
31 | ajv.validate(ConfigLatest, cfg);
32 |
33 | if (cfg.profiles[profileName] && !opts.force) {
34 | throw new ForceRequiredError(`Profile '${profileName}' already exists in '${configFile}'`);
35 | }
36 |
37 | logger.info(`Let's create a profile named '${profileName}'.`);
38 | const profile = await requestProfileInfo();
39 |
40 | cfg.profiles[profileName] = profile;
41 |
42 | await writeProfile(logger, configFile, cfg);
43 |
44 | return 0;
45 | }
46 | })
47 | );
48 |
--------------------------------------------------------------------------------
/commands/config/index.ts:
--------------------------------------------------------------------------------
1 | import { Subcommand } from "../_helpers.ts";
2 | import { configAddProfileCommand } from "./add-profile.ts";
3 | import { configInitCommand } from "./init.ts";
4 | import { configProfilesCommand } from "./profiles.ts";
5 | import { configSchemaCommand } from "./schema.ts";
6 |
7 | const desc =
8 | `Commands for interacting with the render-cli configuration.`;
9 |
10 | export const configCommand =
11 | new Subcommand()
12 | .name("config")
13 | .description(desc)
14 | .action(function() {
15 | this.showHelp();
16 | Deno.exit(1);
17 | })
18 | .command("init", configInitCommand)
19 | .command("add-profile", configAddProfileCommand)
20 | .command("profiles", configProfilesCommand)
21 | .command("schema", configSchemaCommand)
22 | ;
23 |
--------------------------------------------------------------------------------
/commands/config/init.ts:
--------------------------------------------------------------------------------
1 | import { ConfigLatest } from "../../config/types/index.ts";
2 | import { Cliffy, Log, YAML } from "../../deps.ts";
3 | import { ForceRequiredError } from "../../errors.ts";
4 | import { pathExists } from "../../util/paths.ts";
5 | import { getPaths } from "../../util/paths.ts";
6 | import { standardAction, Subcommand } from "../_helpers.ts";
7 | import { chmodConfigIfPossible, requestProfileInfo, writeProfile } from "./_shared.ts";
8 |
9 | const desc =
10 | `Interactively creates a Render CLI config file.`;
11 |
12 | export const configInitCommand =
13 | new Subcommand()
14 | .name('init')
15 | .description(desc)
16 | .option("-f, --force", "overwrites existing files if found.")
17 | .action((opts) => standardAction({
18 | interactive: async (logger: Log.Logger) => {
19 | const { renderDir, configFile } = await getPaths();
20 |
21 | if (await pathExists(configFile) && !opts.force) {
22 | throw new ForceRequiredError(`Render config file already exists at '${configFile}'`);
23 | }
24 |
25 | logger.debug("ensuring Render directory exists...");
26 | await Deno.mkdir(renderDir, { recursive: true });
27 |
28 | logger.info("Let's create your default profile.");
29 | const defaultProfile = await requestProfileInfo();
30 |
31 | const { sshPreserveHosts } = await Cliffy.prompt([
32 | {
33 | name: 'sshPreserveHosts',
34 | type: Cliffy.Confirm,
35 | message: "Enable SSH pinning in `known_hosts`?",
36 | hint: "Render can protect you against Trust On First Use (TOFU) attacks by keeping your SSH `known_hosts` file updated with our SSH fingerprints. We strongly recommend enabling this.",
37 | default: true,
38 | },
39 | ]);
40 |
41 | const cfg: ConfigLatest = {
42 | version: 1,
43 | sshPreserveHosts,
44 | profiles: {
45 | default: defaultProfile,
46 | },
47 | };
48 |
49 | await writeProfile(logger, configFile, cfg);
50 |
51 | logger.info("Done! You're ready to use the Render CLI!");
52 | return 0;
53 | },
54 | }))
55 | ;
56 |
--------------------------------------------------------------------------------
/commands/config/profiles.ts:
--------------------------------------------------------------------------------
1 |
2 | import { Subcommand } from "../_helpers.ts";
3 | import { getConfig } from "../../config/index.ts";
4 |
5 | const desc =
6 | `Lists your configured profiles.`;
7 |
8 | export const configProfilesCommand =
9 | new Subcommand()
10 | .name('profiles')
11 | .description(desc)
12 | .action(async () => {
13 | const config = await getConfig();
14 | Object.keys(config.fullConfig.profiles).forEach((profile) => console.log(profile));
15 | return 0;
16 | });
17 |
--------------------------------------------------------------------------------
/commands/config/schema.ts:
--------------------------------------------------------------------------------
1 | import { YAML } from "../../deps.ts";
2 |
3 | import { ConfigLatest } from "../../config/types/index.ts";
4 | import { standardAction, Subcommand } from "../_helpers.ts";
5 |
6 | const desc =
7 | `Displays the render-cli config schema (YAML; JSON Schema format).`;
8 |
9 | export const configSchemaCommand =
10 | new Subcommand()
11 | .name('regions')
12 | .description(desc)
13 | .action(() => standardAction({
14 | interactive: () => {
15 | console.log(YAML.dump(ConfigLatest));
16 | return 0;
17 | },
18 | }));
19 |
--------------------------------------------------------------------------------
/commands/custom-domains/index.ts:
--------------------------------------------------------------------------------
1 | import { Subcommand } from "../_helpers.ts";
2 | import { customDomainsListCommand } from "./list.ts";
3 |
4 | const desc =
5 | `Commands for observing and managing custom domains.`;
6 |
7 | export const customDomainsCommand =
8 | new Subcommand()
9 | .name("custom-domains")
10 | .description(desc)
11 | .action(function() {
12 | this.showHelp();
13 | Deno.exit(1);
14 | })
15 | .command("list", customDomainsListCommand)
16 | ;
17 |
--------------------------------------------------------------------------------
/commands/custom-domains/list.ts:
--------------------------------------------------------------------------------
1 | import { apiGetAction, Subcommand } from "../_helpers.ts";
2 | import { getConfig } from "../../config/index.ts";
3 | import { getRequestJSONList } from "../../api/index.ts";
4 | import { getLogger } from "../../util/logging.ts";
5 |
6 | const desc =
7 | `Lists the custom domains for a given service.`;
8 |
9 | export const customDomainsListCommand =
10 | new Subcommand()
11 | .name('list')
12 | .description(desc)
13 | .group("Presentational controls")
14 | .option("--format ", "interactive output format", {
15 | default: 'table',
16 | })
17 | .option("--columns ", "if --format table, the columns to show.", {
18 | default: ['id', 'name', 'createdAt', 'verificationStatus'],
19 | })
20 | .group("API parameters")
21 | .option(
22 | "--service-id ",
23 | "the service whose deploys to retrieve",
24 | { required: true },
25 | )
26 | .option("--name ", "domain names to filter by", { collect: true })
27 | .option("--domain-type ", "'apex' or 'subdomain'", { collect: true })
28 | .option("--verification-status ", "'verified' or 'unverified'", { collect: true })
29 | .option("--created-before ", "services created before (ISO8601)")
30 | .option("--created-after ", "services created after (ISO8601)")
31 | .action((opts) => apiGetAction({
32 | format: opts.format,
33 | tableColumns: opts.columns,
34 | processing: async () => {
35 | const cfg = await getConfig();
36 | const logger = await getLogger();
37 |
38 | logger.debug("dispatching getRequestJSONList");
39 | const ret = await getRequestJSONList(
40 | logger,
41 | cfg,
42 | 'customDomain',
43 | `/services/${opts.serviceId}/custom-domains`,
44 | {
45 | name: opts.name?.flat(Infinity),
46 | domainType: opts.domainType?.flat(Infinity),
47 | verificationStatus: opts.verificationStatus?.flat(Infinity),
48 | createdBefore: opts.createdBefore,
49 | createdAfter: opts.createdAfter,
50 | },
51 | );
52 | logger.info(`list call returned ${ret.length} entries.`);
53 |
54 | return ret;
55 | },
56 | }));
57 |
--------------------------------------------------------------------------------
/commands/dashboard.ts:
--------------------------------------------------------------------------------
1 | import { sleep } from "https://deno.land/x/sleep@v1.2.1/sleep.ts";
2 | import { Log, openAWebsite } from "../deps.ts";
3 | import { standardAction, Subcommand } from "./_helpers.ts";
4 |
5 | const desc =
6 | `Opens the Render dashboard in your browser.`;
7 |
8 | export const dashboardCommand =
9 | new Subcommand()
10 | .name('dashboard')
11 | .description(desc)
12 | .option("-l, --link", "Prints out the Render Dashboard URL rather than attempting to open it.")
13 | .arguments<[string]>("[path:string]")
14 | .action((opts, path) => standardAction({
15 | processing: () => {
16 | return `https://rndr.in/c/dashboard`;
17 | },
18 | interactive: async (url: string, logger: Log.Logger) => {
19 | if (opts.link) {
20 | console.log(url);
21 | } else {
22 | logger.info(`Taking you to the dashboard: ${url}`);
23 | await sleep(1);
24 |
25 | const p = await openAWebsite(url);
26 |
27 | if (!(await p.status()).success) {
28 | logger.error(`Could not automatically open browser. Please navigate to this URL directly: ${url}`);
29 | }
30 | }
31 | },
32 | nonInteractive: (url: string) => {
33 | console.log(url);
34 | },
35 | exitCode: () => 0,
36 | }));
37 |
--------------------------------------------------------------------------------
/commands/deploys/index.ts:
--------------------------------------------------------------------------------
1 | import { Subcommand } from "../_helpers.ts";
2 | import { deploysListCommand } from "./list.ts";
3 |
4 | const desc =
5 | `Commands for observing and managing deploys of Render services.`;
6 |
7 | export const deploysCommand =
8 | new Subcommand()
9 | .name("deploys")
10 | .description(desc)
11 | .action(function() {
12 | this.showHelp();
13 | Deno.exit(1);
14 | })
15 | .command("list", deploysListCommand)
16 | ;
17 |
--------------------------------------------------------------------------------
/commands/deploys/list.ts:
--------------------------------------------------------------------------------
1 | import { apiGetAction, Subcommand } from "../_helpers.ts";
2 | import { getConfig } from "../../config/index.ts";
3 | import { getRequestJSONList } from "../../api/index.ts";
4 | import { getLogger } from "../../util/logging.ts";
5 |
6 | const desc =
7 | `Lists the deploys for a given service.`;
8 |
9 | export const deploysListCommand =
10 | new Subcommand()
11 | .name('list')
12 | .description(desc)
13 | .group("Presentational controls")
14 | .option("--format ", "interactive output format", {
15 | default: 'table',
16 | })
17 | .option("--columns ", "if --format table, the columns to show.", {
18 | default: ['id', 'status', 'createdAt', 'commit.id', 'commit.message'],
19 | })
20 | .group("API parameters")
21 | .option(
22 | "--service-id ",
23 | "the service whose deploys to retrieve",
24 | { required: true },
25 | )
26 | .option(
27 | "--start-time ", "start of the time range to return"
28 | )
29 | .option(
30 | "--end-time ", "end of the time range to return"
31 | )
32 | .action((opts) => apiGetAction({
33 | format: opts.format,
34 | tableColumns: opts.columns,
35 | processing: async () => {
36 | const cfg = await getConfig();
37 | const logger = await getLogger();
38 |
39 | logger.debug("dispatching getRequestJSONList");
40 | const ret = await getRequestJSONList(
41 | logger,
42 | cfg,
43 | 'deploy',
44 | `/services/${opts.serviceId}/deploys`,
45 | {
46 | startTime: opts.startTime,
47 | endTime: opts.endTime,
48 | },
49 | );
50 | logger.debug(`list call returned ${ret.length} entries.`);
51 |
52 | return ret;
53 | },
54 | }
55 | )
56 | );
57 |
--------------------------------------------------------------------------------
/commands/docs.ts:
--------------------------------------------------------------------------------
1 | import { sleep } from "https://deno.land/x/sleep@v1.2.1/sleep.ts";
2 | import { Log, openAWebsite } from "../deps.ts";
3 | import { standardAction, Subcommand } from "./_helpers.ts";
4 |
5 | const desc =
6 | `Opens the Render docs in your browser.`;
7 |
8 | export const docsCommand =
9 | new Subcommand()
10 | .name('docs')
11 | .description(desc)
12 | .option("-l, --link", "Prints out the Render Docs URL rather than attempting to open it.")
13 | .arguments<[string]>("[path:string]")
14 | .action((opts, path) => standardAction({
15 | processing: () => {
16 | return `https://rndr.in/c/docs`;
17 | },
18 | interactive: async (url: string, logger: Log.Logger) => {
19 | if (opts.link) {
20 | console.log(url);
21 | } else {
22 | logger.info(`Taking you to the Render docs: ${url}`);
23 | await sleep(1);
24 |
25 | const p = await openAWebsite(url);
26 |
27 | if (!(await p.status()).success) {
28 | logger.error(`Could not automatically open browser. Please navigate to this URL directly: ${url}`);
29 | }
30 | }
31 | },
32 | nonInteractive: (url: string) => {
33 | console.log(url);
34 | },
35 | exitCode: () => 0,
36 | }));
37 |
--------------------------------------------------------------------------------
/commands/index.ts:
--------------------------------------------------------------------------------
1 | import { Cliffy } from '../deps.ts';
2 |
3 | import { getLogger, jsonRecordPerLine, nonInteractive, prettyJson, verboseLogging } from "../util/logging.ts";
4 | import { blueprintCommand } from "./blueprint/index.ts";
5 | import { buildpackCommand } from './buildpack/index.ts';
6 | import { commandsCommand } from "./commands.ts";
7 | import { configCommand } from "./config/index.ts";
8 | import { repoCommand } from './repo/index.ts';
9 | import { regionsCommand } from "./regions.ts";
10 | import { servicesCommand } from "./services/index.ts";
11 | import { deploysCommand } from "./deploys/index.ts";
12 | import { customDomainsCommand } from "./custom-domains/index.ts";
13 | import { jobsCommand } from "./jobs/index.ts";
14 | import { versionCommand } from './version.ts';
15 | import { dashboardCommand } from './dashboard.ts';
16 | import { getPaths, pathExists } from '../util/paths.ts';
17 | import { funcError } from "../util/errors.ts";
18 | import { docsCommand } from './docs.ts';
19 |
20 | async function emitHello() {
21 | const { configFile } = await getPaths();
22 |
23 | console.log("render-cli is the command line interface for Render. The goal for");
24 | console.log("this application is to make it easier to interact with Render in");
25 | console.log("a programmatic way. It's also a good way to learn about the Render");
26 | console.log("API and what's available in the programming language of your choice.");
27 |
28 | if (!await pathExists(configFile)) {
29 | console.log();
30 | console.log(`To get started, run ${Cliffy.colors.cyan('render config init')} to create a configuration file.`);
31 | }
32 |
33 | console.log();
34 | console.log("We're always adding more functionality to the Render CLI. Here are some");
35 | console.log("common commands you might want to try:");
36 | console.log();
37 | console.log(`- ${Cliffy.colors.cyan('render repo from-template')} instantiates a new project from an example`);
38 | console.log(` in the Render example library at [ https://github.com/render-examples ].`);
39 | console.log(`- ${Cliffy.colors.cyan('render blueprint launch')} helps deploy the git repository you're in to`);
40 | console.log(` Render, using the blueprint in the repository.`);
41 | console.log(`- ${Cliffy.colors.cyan('render services ssh')} helps you connect to a service's shell.`);
42 | console.log(`- ${Cliffy.colors.cyan('render services tail')} lets you stream a service's logs as they happen.`);
43 | console.log(`- ${Cliffy.colors.cyan('render buildpack')} helps you manage the buildpacks used in services`);
44 | console.log(` created by the Render Heroku importer or explicitly instantiated via`);
45 | console.log(` ${Cliffy.colors.cyan('render buildpack init')}.`);
46 | console.log(`- ${Cliffy.colors.cyan('render commands')} will give you a full list of all commands in the Render`);
47 | console.log(` CLI, and ${Cliffy.colors.cyan('render --help')} will give you more details on a given`);
48 | console.log(` command.`);
49 |
50 | console.log();
51 | console.log(`You can also run ${Cliffy.colors.cyan('render --help')} for a more structured help file.`);
52 | console.log();
53 | console.log("Thanks for using Render!");
54 | }
55 |
56 | export const ROOT_COMMAND =
57 | (new Cliffy.Command())
58 | .name("render")
59 | .description("The CLI for the easiest cloud platform you'll ever use.\n\nType `render config init` to get started.")
60 | .globalOption(
61 | "-v, --verbose",
62 | "Makes render-cli a lot more chatty.",
63 | {
64 | action: () => verboseLogging(),
65 | })
66 | .globalOption(
67 | "--non-interactive",
68 | "Forces Render to act as though it's not in a TTY.",
69 | {
70 | action: async (opts) => {
71 | (await getLogger()).debug("--non-interactive", opts);
72 | nonInteractive();
73 | },
74 | })
75 | .globalOption(
76 | "--pretty-json",
77 | "If in non-interactive mode, prints prettified JSON.",
78 | {
79 | action: async (opts) => {
80 | (await getLogger()).debug("--pretty-json", opts);
81 | prettyJson();
82 | },
83 | })
84 | .globalOption(
85 | "--json-record-per-line",
86 | "if emitting JSON, prints each JSON record as a separate line of stdout.",
87 | {
88 | action: async (opts) => {
89 | (await getLogger()).debug("--json-record-per-line", opts);
90 | jsonRecordPerLine();
91 | },
92 | conflicts: ["pretty-json"],
93 | })
94 | .globalOption(
95 | "-p, --profile ",
96 | "The Render profile to use for this invocation. Overrides RENDERCLI_PROFILE.",
97 | {
98 | action: async (opts) => {
99 | (await getLogger()).debug("--profile", opts);
100 | Deno.env.set(
101 | "RENDERCLI_PROFILE",
102 | opts.profile || funcError(new Error("--profile passed but no argument received")),
103 | );
104 | },
105 | }
106 | )
107 | .globalOption(
108 | "-r, --region ",
109 | "The Render region to use for this invocation; always accepted but not always relevant. Overrides RENDERCLI_REGION.",
110 | {
111 | action: async (opts) => {
112 | (await getLogger()).debug("--region", opts);
113 | Deno.env.set(
114 | "RENDERCLI_REGION",
115 | opts.region || funcError(new Error("--region passed but no argument received")),
116 | );
117 | },
118 | })
119 | .helpOption(false)
120 | .option(
121 | "-h, --help",
122 | "Shows help for this command.",
123 | {
124 | action: async function() {
125 | const { configFile } = await getPaths();
126 |
127 | if (!await pathExists(configFile)) {
128 | await emitHello();
129 | } else {
130 | this.showHelp();
131 | }
132 |
133 | Deno.exit(1);
134 | },
135 | },
136 | )
137 | .action(async function() {
138 | await emitHello();
139 | Deno.exit(1);
140 | })
141 | .command("version", versionCommand)
142 | .command("commands", commandsCommand)
143 | .command("config", configCommand)
144 | .command("regions", regionsCommand)
145 | .command("dashboard", dashboardCommand)
146 | .command("docs", docsCommand)
147 | .command("repo", repoCommand)
148 | .command("blueprint", blueprintCommand)
149 | .command("buildpack", buildpackCommand)
150 | .command("services", servicesCommand)
151 | .command("completions", new Cliffy.CompletionsCommand())
152 | .command("deploys", deploysCommand)
153 | .command("jobs", jobsCommand)
154 | .command("custom-domains", customDomainsCommand)
155 | ;
156 |
--------------------------------------------------------------------------------
/commands/jobs/index.ts:
--------------------------------------------------------------------------------
1 | import { Subcommand } from "../_helpers.ts";
2 | import { jobsListCommand } from "./list.ts";
3 |
4 | const desc =
5 | `Commands for observing and managing Render jobs.`;
6 |
7 | export const jobsCommand =
8 | new Subcommand()
9 | .name("jobs")
10 | .description(desc)
11 | .action(function() {
12 | this.showHelp();
13 | Deno.exit(1);
14 | })
15 | .command("list", jobsListCommand)
16 | ;
17 |
--------------------------------------------------------------------------------
/commands/jobs/list.ts:
--------------------------------------------------------------------------------
1 | import { apiGetAction, Subcommand } from "../_helpers.ts";
2 | import { getConfig } from "../../config/index.ts";
3 | import { getRequestJSONList } from "../../api/index.ts";
4 | import { getLogger } from "../../util/logging.ts";
5 |
6 | const desc =
7 | `Lists the jobs this user can see.`;
8 |
9 | export const jobsListCommand =
10 | new Subcommand()
11 | .name('list')
12 | .description(desc)
13 | .group("Presentational controls")
14 | .option("--format ", "interactive output format", {
15 | default: 'table',
16 | })
17 | .option("--columns ", "if --format table, the columns to show.", {
18 | default: ['id', 'startCommand', 'planId', 'status', 'finishedAt'],
19 | })
20 | .group("API parameters")
21 | .option(
22 | "--service-id ",
23 | "the service whose deploys to retrieve",
24 | { required: true },
25 | )
26 | .option("--status ", "'pending', 'running', 'succeeded', or 'failed'", { collect: true })
27 | .option("--created-before ", "jobs created before (ISO8601)")
28 | .option("--created-after ", "jobs created after (ISO8601)")
29 | .option("--started-before ", "jobs started before (ISO8601)")
30 | .option("--started-after ", "jobs started after (ISO8601)")
31 | .option("--finished-before ", "jobs finished before (ISO8601)")
32 | .option("--finished-after ", "jobs finished after (ISO8601)")
33 | .action((opts) => apiGetAction({
34 | format: opts.format,
35 | tableColumns: opts.columns,
36 | processing: async () => {
37 | const cfg = await getConfig();
38 | const logger = await getLogger();
39 |
40 | logger.debug("dispatching getRequestJSONList");
41 | const ret = await getRequestJSONList(
42 | logger,
43 | cfg,
44 | 'job',
45 | `/services/${opts.serviceId}/jobs`,
46 | {
47 | status: opts.status?.flat(Infinity),
48 | createdBefore: opts.createdBefore,
49 | createdAfter: opts.createdAfter,
50 | startedBefore: opts.startedBefore,
51 | startedAfter: opts.startedAfter,
52 | finishedBefore: opts.finishedBefore,
53 | finishedAfter: opts.finishedAfter,
54 | },
55 | );
56 | logger.debug(`list call returned ${ret.length} entries.`);
57 |
58 | return ret;
59 | },
60 | }));
61 |
--------------------------------------------------------------------------------
/commands/regions.ts:
--------------------------------------------------------------------------------
1 | import { ALL_REGIONS } from "../config/types/enums.ts";
2 | import { Subcommand } from "./_helpers.ts";
3 |
4 | const desc =
5 | `Lists the Render regions that this version of the CLI knows about.
6 |
7 | NOTE: This list is shipped with a list baked in; in the future we might replace this with an API call.`;
8 |
9 | export const regionsCommand =
10 | new Subcommand()
11 | .name('regions')
12 | .description(desc)
13 | .action(() => {
14 | for (const region of ALL_REGIONS) {
15 | console.log(region);
16 | }
17 | });
18 |
--------------------------------------------------------------------------------
/commands/repo/from-template.ts:
--------------------------------------------------------------------------------
1 | import { Log } from "../../deps.ts";
2 | import { templateNewProject } from "../../new/repo/index.ts";
3 | import { standardAction, Subcommand } from "../_helpers.ts";
4 |
5 | const desc =
6 | `Initializes a new project repository from a template.
7 |
8 | If you want to create a Render Blueprint (render.yaml) for an existing project, see \`render new blueprint\`.
9 |
10 | You can initialize a project with any of the following identifiers:
11 |
12 | repo-name
13 | repo-name@gitref
14 | user/repo-name
15 | user/repo-name@gitref
16 | github:user/repo-name
17 | github:user/repo-name@gitref
18 |
19 | If \`user\` is not provided, \`render-examples\` is assumed. If no source prefix is provided, \`github\` is assumed.
20 |
21 | At present, \`render repo from-template\` does not support private repositories.
22 |
23 | (Future TODO: enable \`gitlab:\` prefix, enable arbitrary Git repositories, enable private repositories.)`;
24 |
25 | export const repoFromTemplateCommand =
26 | new Subcommand()
27 | .name('from-template')
28 | .description(desc)
29 | .arguments<[string]>("")
30 | .option("-o, --output-directory ", "target directory for new repo", { required: false })
31 | .option("-f, --force", "overwrites existing directory if found.")
32 | .option("--skip-cleanup", "skips cleaning up tmpdir (on success or failure)")
33 | .action((opts, identifier) =>
34 | standardAction({
35 | interactive: async (_logger: Log.Logger): Promise => {
36 | await templateNewProject({
37 | identifier,
38 | outputDir: opts.outputDirectory,
39 | force: opts.force,
40 | skipCleanup: opts.skipCleanup,
41 | });
42 | return 0;
43 | }
44 | }));
45 |
--------------------------------------------------------------------------------
/commands/repo/index.ts:
--------------------------------------------------------------------------------
1 | import { Subcommand } from "../_helpers.ts";
2 | import { repoFromTemplateCommand } from "./from-template.ts";
3 |
4 | const desc =
5 | `Commands for managing Render projects/repos.`;
6 |
7 | export const repoCommand =
8 | new Subcommand()
9 | .name("repo")
10 | .description(desc)
11 | .action(function() {
12 | this.showHelp();
13 | Deno.exit(1);
14 | })
15 | .command("from-template", repoFromTemplateCommand)
16 | ;
17 |
--------------------------------------------------------------------------------
/commands/services/delete.ts:
--------------------------------------------------------------------------------
1 | import { standardAction, Subcommand } from "../_helpers.ts";
2 | import { getConfig } from "../../config/index.ts";
3 | import { deleteRequestRaw } from "../../api/index.ts";
4 | import { getLogger } from "../../util/logging.ts";
5 |
6 | const desc = `Deletes a service`;
7 |
8 | export const servicesDeleteCommand = new Subcommand()
9 | .name("list")
10 | .description(desc)
11 | .group("API parameters")
12 | .option("--id ", "the service ID (e.g. `srv-12345`)")
13 | .action((opts) =>
14 | standardAction({
15 | exitCode: (res) => (res?.status == 204 ? 0 : 1),
16 | interactive: () => undefined,
17 | processing: async () => {
18 | const cfg = await getConfig();
19 | const logger = await getLogger();
20 |
21 | const ret = await deleteRequestRaw(logger, cfg, `/services/${opts.id}`);
22 | logger.debug(`deleted service ${opts.id}: ${ret.status}`);
23 |
24 | return ret;
25 | },
26 | })
27 | );
28 |
--------------------------------------------------------------------------------
/commands/services/index.ts:
--------------------------------------------------------------------------------
1 | import { Subcommand } from "../_helpers.ts";
2 | import { servicesListCommand } from "./list.ts";
3 | import { servicesShowCommand } from "./show.ts";
4 | import { servicesSshCommand } from "./ssh.ts";
5 | import { servicesTailCommand } from "./tail.ts";
6 | import { servicesDeleteCommand } from "./delete.ts";
7 |
8 | const desc =
9 | `Commands for observing and managing Render services.`;
10 |
11 | export const servicesCommand =
12 | new Subcommand()
13 | .name("new")
14 | .description(desc)
15 | .action(function() {
16 | this.showHelp();
17 | Deno.exit(1);
18 | })
19 | .command("show", servicesShowCommand)
20 | .command("delete", servicesDeleteCommand)
21 | .command("list", servicesListCommand)
22 | .command("tail", servicesTailCommand)
23 | .command("ssh", servicesSshCommand)
24 | ;
25 |
--------------------------------------------------------------------------------
/commands/services/list.ts:
--------------------------------------------------------------------------------
1 | import { apiGetAction, Subcommand } from "../_helpers.ts";
2 | import { getConfig } from "../../config/index.ts";
3 | import { getRequestJSONList } from "../../api/index.ts";
4 | import { getLogger } from "../../util/logging.ts";
5 | import { funcError } from "../../util/errors.ts";
6 | import { RenderCLIError } from "../../errors.ts";
7 |
8 | const desc =
9 | `Lists the services this user can see.`;
10 |
11 | export const servicesListCommand =
12 | new Subcommand()
13 | .name('list')
14 | .description(desc)
15 | .group("Presentational controls")
16 | .option("--format ", "interactive output format", {
17 | default: 'table',
18 | })
19 | .option("--columns ", "if --format table, the columns to show.", {
20 | default: ['id', 'name', 'type', 'serviceDetails.env', 'slug', 'serviceDetails.numInstances'],
21 | })
22 | .group("API parameters")
23 | .option("--name ", "the name of a service to filter by", { collect: true })
24 | .option("--type ", "the service type to filter by", { collect: true })
25 | .option("--env ", "the runtime environment (docker, ruby, python, etc.)", { collect: true })
26 | .option("--service-region ", "the region in which a service is located", { collect: true })
27 | .option("--ownerid ", "the owner ID for the service", { collect: true })
28 | .option("--created-before ", "services created before (ISO8601)")
29 | .option("--created-after ", "services created after (ISO8601)")
30 | .option("--updated-before ", "services updated before (ISO8601)")
31 | .option("--updated-after ", "services updated after (ISO8601)")
32 | .group("API-nonstandard parameters")
33 | .option("--suspended ", "'true'/'yes', 'false'/'no', or 'all'", {
34 | default: 'false',
35 | })
36 | .action((opts) => apiGetAction({
37 | format: opts.format,
38 | tableColumns: opts.columns,
39 | processing: async () => {
40 | const cfg = await getConfig();
41 | const logger = await getLogger();
42 |
43 | const ret = await getRequestJSONList(
44 | logger,
45 | cfg,
46 | 'service',
47 | '/services',
48 | {
49 | name: opts.name?.flat(Infinity),
50 | type: opts.type?.flat(Infinity),
51 | env: opts.env?.flat(Infinity),
52 | region: opts.serviceRegion?.flat(Infinity),
53 | suspended:
54 | opts.suspended === 'all'
55 | ? ['suspended', 'not_suspended' ]
56 | : ['true', 'yes'].includes(opts.suspended.toLowerCase())
57 | ? ['suspended']
58 | : ['false', 'no'].includes(opts.suspended.toLowerCase())
59 | ? ['not_suspended']
60 | : funcError(new RenderCLIError(`invalid --suspended: ${opts.suspended}`)),
61 | createdBefore: opts.createdBefore,
62 | createdAfter: opts.createdAfter,
63 | updatedBefore: opts.updatedBefore,
64 | updatedAfter: opts.createdAfter,
65 | },
66 | );
67 | logger.debug(`list call returned ${ret.length} entries.`);
68 |
69 | return ret;
70 | },
71 | }
72 | )
73 | );
74 |
--------------------------------------------------------------------------------
/commands/services/show.ts:
--------------------------------------------------------------------------------
1 | import { apiGetAction, Subcommand } from "../_helpers.ts";
2 | import { getConfig } from "../../config/index.ts";
3 | import { getRequestJSON } from "../../api/index.ts";
4 | import { getLogger } from "../../util/logging.ts";
5 |
6 | const desc =
7 | `Fetches full details about a single service.`;
8 |
9 | export const servicesShowCommand =
10 | new Subcommand()
11 | .name('list')
12 | .description(desc)
13 | .group("Presentational controls")
14 | .group("API parameters")
15 | .option("--id ", "the service ID (e.g. `srv-12345`)")
16 | .action((opts) => apiGetAction({
17 | processing: async () => {
18 | const cfg = await getConfig();
19 | const logger = await getLogger();
20 |
21 | logger.debug("dispatching getRequestJSON");
22 | const ret = await getRequestJSON(
23 | logger,
24 | cfg,
25 | `/services/${opts.id}`,
26 | );
27 |
28 | return ret;
29 | },
30 | }
31 | )
32 | );
33 |
--------------------------------------------------------------------------------
/commands/services/ssh.ts:
--------------------------------------------------------------------------------
1 | import { getConfig, validateRegion } from "../../config/index.ts";
2 | import { runSSH } from "../../services/ssh/index.ts";
3 | import { getLogger } from "../../util/logging.ts";
4 | import { Subcommand } from "./../_helpers.ts";
5 |
6 | const desc =
7 | `Opens a SSH session to a Render service.
8 |
9 | This command wraps your local \`ssh\` binary and passes any additional command line arguments to that binary. Before invoking \`ssh\`, \`render ssh\` ensures that your local known hosts are updated with current fingerprints for Render services, reducing the likelihood of a TOFU (trust-on-first-use) attack.`;
10 |
11 | export const servicesSshCommand =
12 | new Subcommand()
13 | .name('ssh')
14 | .description(desc)
15 | .arguments("[sshArgs...:string]")
16 | .stopEarly() // args after the service name will not be parsed, including flags
17 | .option("--preserve-hosts", "Do not update ~/.ssh/known_hosts with Render public keys.")
18 | .option("--id ", "The service ID to access via SSH.", { required: true })
19 | .action(async (opts, ...sshArgs) => {
20 | const logger = await getLogger();
21 | const config = await getConfig();
22 |
23 | const { id } = opts;
24 |
25 | const status = await runSSH({
26 | config,
27 | serviceId: id,
28 | region: validateRegion(opts.region ?? config.profile.defaultRegion),
29 | sshArgs: sshArgs ?? [],
30 | noHosts: opts.preserveHosts ?? config.fullConfig.sshPreserveHosts ?? false,
31 | });
32 |
33 | if (!status.success) {
34 | logger.error(`Underlying SSH failure: exit code ${status.code}, signal ${status.signal ?? 'none'}`);
35 | }
36 | Deno.exit(status.code);
37 | });
38 |
--------------------------------------------------------------------------------
/commands/services/tail.ts:
--------------------------------------------------------------------------------
1 | import { apiHost, apiKeyOrThrow, getRequestJSON, handleApiErrors } from "../../api/index.ts";
2 | import { RenderCLIError } from "../../errors.ts";
3 | import { UNLOGGABLE_SERVICE_TYPES } from "../../services/constants.ts";
4 | import { LogTailEntry } from "../../services/types.ts";
5 | import { ajv, logAjvErrors } from "../../util/ajv.ts";
6 | import { getLogger } from "../../util/logging.ts";
7 | import { Subcommand, withConfig } from "../_helpers.ts";
8 |
9 | const desc =
10 | `Tails logs for a given service.
11 |
12 | TODO: support specifying a particular deploy ID from which to source logs.
13 |
14 | TODO: support pulling only the last X lines of a log.`;
15 |
16 | export const servicesTailCommand =
17 | new Subcommand()
18 | .name('new')
19 | .description(desc)
20 | .option("--raw", "only prints the bare text of the log to stdout")
21 | .option("--json", "prints Render's log tail as JSON, one per message", { conflicts: ['raw'] })
22 | .option("--deploy-id ", "filter logs to the requested deploy ID", { collect: true })
23 | .option("--id ", "the service ID whose logs to request", { required: true })
24 | .action((opts) => withConfig(async (cfg) => {
25 | const logger = await getLogger();
26 | const apiKey = apiKeyOrThrow(cfg);
27 |
28 | opts.deployId = opts.deployId ?? [];
29 | const deployIds = opts.deployId.length > 0 ? new Set(opts.deployId ?? []) : null;
30 |
31 | const url = `wss://${apiHost(cfg)}/v1/services/${opts.id}/logs/tail`;
32 | logger.debug(`tail url: ${url}, profile name: ${cfg.profileName}`);
33 |
34 | await handleApiErrors(logger, async () => {
35 | logger.debug("dispatching to check for static_site");
36 | const service = (await getRequestJSON(
37 | logger,
38 | cfg,
39 | `/services/${opts.id}`,
40 | // TODO: resolve later when API clients are functional
41 | // deno-lint-ignore no-explicit-any
42 | )) as any;
43 |
44 | if (UNLOGGABLE_SERVICE_TYPES.has(service.type)) {
45 | throw new RenderCLIError(`Service '${opts.id}' is of type '${service.type}', which has no logs.`);
46 | }
47 |
48 | const stream = new WebSocketStream(
49 | url,
50 | {
51 | headers: {
52 | Authorization: `Bearer ${apiKey}`,
53 | },
54 | }
55 | );
56 |
57 | let conn: WebSocketConnection;
58 | let reader: ReadableStreamDefaultReader;
59 | let writer: WritableStreamDefaultWriter;
60 |
61 | try {
62 | conn = await stream.opened;
63 | reader = await conn.readable.getReader();
64 | writer = await conn.writable.getWriter();
65 | } catch (err) {
66 | throw err;
67 | }
68 |
69 | const logEntryValidator = ajv.compile(LogTailEntry);
70 |
71 |
72 | setInterval(() => {
73 | writer.write('{ "ping": true }');
74 | }, 15000);
75 |
76 | while (true) {
77 | const input = (await reader.read()).value;
78 | if (!input) {
79 | logger.debug("empty packet received from web socket?");
80 | continue;
81 | }
82 |
83 | let json: string;
84 | if (typeof(input) === 'string') {
85 | json = input;
86 | } else {
87 | json = (new TextDecoder()).decode(input);
88 | }
89 |
90 | const rawMsg = JSON.parse(json);
91 |
92 | if (logEntryValidator(rawMsg)) {
93 | const msg = rawMsg as {deployID: string; text: string}
94 | if (deployIds && !deployIds.has(msg.deployID)) {
95 | continue;
96 | }
97 |
98 | let output;
99 | if (opts.json) {
100 | output = json.trim();
101 | } else {
102 | output = "";
103 |
104 | if (!opts.raw) {
105 | output += `${msg.deployID}: `;
106 | }
107 |
108 | output += msg.text;
109 | }
110 |
111 | console.log(output);
112 | } else {
113 | logger.error("Unparseable entry from tail socket.")
114 | logAjvErrors(logger, logEntryValidator.errors);
115 | continue;
116 | }
117 | }
118 | });
119 | }));
120 |
--------------------------------------------------------------------------------
/commands/version.ts:
--------------------------------------------------------------------------------
1 | import { VERSION } from "../version.ts";
2 | import { Subcommand } from "./_helpers.ts";
3 |
4 | const desc = `Shows the application version.`;
5 |
6 | export const versionCommand =
7 | new Subcommand()
8 | .name('version')
9 | .description(desc)
10 | .action(() => {
11 | console.log(`${VERSION} (${Deno.build.target})`);
12 | });
13 |
--------------------------------------------------------------------------------
/config/index.ts:
--------------------------------------------------------------------------------
1 | import { Log, YAML, } from "../deps.ts";
2 | import { ajv } from "../util/ajv.ts";
3 | import { identity } from "../util/fn.ts";
4 | import { getPaths } from "../util/paths.ts";
5 | import { APIKeyRequired } from '../errors.ts';
6 |
7 | import { ALL_REGIONS, Region } from "./types/enums.ts";
8 | import { assertValidRegion } from "./types/enums.ts";
9 | import { ConfigAny, ConfigLatest, ProfileLatest, RuntimeConfiguration } from "./types/index.ts";
10 | import { getLogger } from "../util/logging.ts";
11 |
12 | let config: RuntimeConfiguration | null = null;
13 |
14 | // TODO: smarten this up with type checks
15 | const CONFIG_UPGRADE_MAPS = {
16 | 1: identity,
17 | }
18 | const FALLBACK_PROFILE: Partial = {
19 | defaultRegion: 'oregon', // mimics dashboard behavior
20 | };
21 |
22 | const FALLBACK_CONFIG: ConfigLatest = {
23 | version: 1,
24 | profiles: {},
25 | }
26 |
27 | export async function getConfig(): Promise {
28 | if (config === null) {
29 | const cfg = await fetchAndParseConfig();
30 |
31 | const runtimeProfile = await buildRuntimeProfile(cfg);
32 | const ret: RuntimeConfiguration = {
33 | fullConfig: cfg,
34 | ...runtimeProfile,
35 | }
36 |
37 | config = ret;
38 | }
39 |
40 | return config;
41 | }
42 |
43 | export async function withConfig(fn: (cfg: RuntimeConfiguration) => T | Promise): Promise {
44 | const cfg = await getConfig();
45 |
46 | return fn(cfg);
47 | }
48 |
49 | function upgradeConfigFile(config: ConfigAny): ConfigLatest {
50 | const upgradePath = CONFIG_UPGRADE_MAPS[config.version];
51 |
52 | if (!upgradePath) {
53 | throw new Error(`Unrecognized version, cannot upgrade (is render-cli too old?): ${config.version}`);
54 | }
55 |
56 | return upgradePath(config);
57 | }
58 |
59 | async function parseConfig(content: string): Promise {
60 | const data = await YAML.load(content);
61 | const ret = {
62 | ...FALLBACK_CONFIG,
63 | // deno-lint-ignore no-explicit-any
64 | ...(data as any),
65 | };
66 |
67 | await ajv.validate(ConfigAny, ret);
68 | if (!ajv.errors) {
69 | return upgradeConfigFile(ret as ConfigAny);
70 | }
71 |
72 | throw new Error(`Config validation error: ${Deno.inspect(ajv.errors)}`);
73 | }
74 |
75 | async function fetchAndParseConfig(): Promise {
76 | const path = getPaths().configFile;
77 |
78 | try {
79 | const decoder = new TextDecoder();
80 | const content = decoder.decode(await Deno.readFile(path));
81 | return parseConfig(content);
82 | } catch (err) {
83 | if (err instanceof Deno.errors.NotFound) {
84 | return FALLBACK_CONFIG;
85 | }
86 |
87 | throw err;
88 | }
89 | }
90 |
91 | async function buildRuntimeProfile(
92 | cfg: ConfigLatest,
93 | ): Promise<{ profile: ProfileLatest, profileName: string }> {
94 | const logger = await getLogger();
95 | const profileFromEnv = Deno.env.get("RENDERCLI_PROFILE");
96 | const profileName = profileFromEnv ?? 'default';
97 | logger.debug(`Using profile '${profileName}' (env: ${profileFromEnv})`);
98 | const profile = cfg.profiles[profileName] ?? {};
99 |
100 | const ret: ProfileLatest = {
101 | ...FALLBACK_PROFILE,
102 | ...profile,
103 | }
104 |
105 | const actualRegion = Deno.env.get("RENDERCLI_REGION") ?? ret.defaultRegion;
106 | assertValidRegion(actualRegion);
107 | // TODO: clean this up - the assertion should be making the cast unnecessary, but TS disagrees
108 | ret.defaultRegion = actualRegion as Region;
109 |
110 | ret.apiKey = Deno.env.get("RENDERCLI_APIKEY") ?? ret.apiKey;
111 | ret.apiHost = Deno.env.get("RENDERCLI_APIHOST") ?? ret.apiHost;
112 |
113 | if (!ret.apiKey) {
114 | throw new APIKeyRequired();
115 | }
116 |
117 | return { profile: ret, profileName };
118 | }
119 |
120 | export function validateRegion(s: string): Region {
121 | if (!ajv.validate(Region, s)) {
122 | throw new Error(`Invalid region '${s}'. Valid regions: ${ALL_REGIONS.join(' ')}`);
123 | }
124 |
125 | return s as Region;
126 | }
127 |
--------------------------------------------------------------------------------
/config/types/enums.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Static,
3 | Type
4 | } from '../../deps.ts';
5 | import { ajv } from "../../util/ajv.ts";
6 |
7 | export const Region = Type.Union([
8 | Type.Literal('singapore'),
9 | Type.Literal('oregon'),
10 | Type.Literal('ohio'),
11 | Type.Literal('frankfurt'),
12 | ]);
13 | export type Region = Static;
14 | export const ALL_REGIONS = Region.anyOf.map(i => i.const);
15 |
16 | export function assertValidRegion(s: string): asserts s is Region {
17 | if (!ajv.validate(Region, s)) {
18 | throw new Error(`Region '${s}' is not one of: ${ALL_REGIONS.join(' ')}`);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/config/types/index.ts:
--------------------------------------------------------------------------------
1 | import { Type } from "../../deps.ts";
2 | import { ConfigV1, ProfileV1 } from './v1.ts';
3 |
4 | export type ConfigAny =
5 | | ConfigV1;
6 | export const ConfigAny = Type.Union([
7 | ConfigV1,
8 | ]);
9 |
10 | export type ConfigLatest = ConfigV1;
11 | export const ConfigLatest = ConfigV1;
12 |
13 | export type ProfileLatest = ProfileV1;
14 | export const ProfileLatest = ProfileV1;
15 |
16 | export type UpgradeFn = (cfg: T) => ConfigLatest;
17 |
18 | export type RuntimeConfiguration = {
19 | fullConfig: ConfigLatest;
20 | profileName: string;
21 | profile: ProfileLatest;
22 | }
23 |
--------------------------------------------------------------------------------
/config/types/v1.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Static,
3 | Type
4 | } from '../../deps.ts';
5 | import { Region } from "./enums.ts";
6 |
7 | export const APIKeyV1 = Type.String({
8 | pattern: 'rnd_[0-9a-zA-Z\_]+',
9 | description: "Your Render API key. Will begin with 'rnd_'.",
10 | });
11 | export type APIKeyV1 = Static;
12 |
13 | export const ProfileV1 = Type.Object({
14 | apiKey: APIKeyV1,
15 | apiHost: Type.Optional(Type.String()),
16 | defaultRegion: Region,
17 | });
18 | export type ProfileV1 = Static;
19 |
20 | export const ConfigV1 = Type.Object({
21 | version: Type.Literal(1),
22 | sshPreserveHosts: Type.Optional(Type.Boolean({
23 | description: "If true, render-cli will not keep ~/.ssh/known_hosts up to date with current public keys.",
24 | })),
25 | profiles: Type.Record(Type.String(), ProfileV1),
26 | });
27 | export type ConfigV1 = Static;
28 |
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "importMap": "import_map.json",
3 | "lock": "deps-lock.json",
4 | "tasks": {
5 | "run": "deno run --unstable --allow-read --allow-net --allow-run --allow-write --allow-env -- entry-point.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/deps.ts:
--------------------------------------------------------------------------------
1 | export {
2 | assertEquals,
3 | assertThrows,
4 | } from 'https://deno.land/std@0.151.0/testing/asserts.ts';;
5 | export * as Path from "https://deno.land/std@0.151.0/path/mod.ts";
6 | export * as FS from "https://deno.land/std@0.151.0/fs/mod.ts";
7 | export * as Log from "https://deno.land/std@0.151.0/log/mod.ts";
8 | export * as QueryString from "https://deno.land/x/querystring@v1.0.2/mod.js";
9 | export {
10 | sortBy
11 | } from "https://raw.githubusercontent.com/lodash/lodash/4.17.21-es/lodash.js";
12 |
13 | export { Type, type Static } from "https://deno.land/x/typebox@0.28.10/src/typebox.ts";
14 | export * as Typebox from "https://deno.land/x/typebox@0.28.10/src/typebox.ts";
15 |
16 | export { default as Ajv } from "https://esm.sh/v86/ajv@8.11.0";
17 | export { default as AjvFormats } from "https://esm.sh/v86/ajv-formats@2.1.1";
18 |
19 | export * as Cliffy from "https://deno.land/x/cliffy@v0.25.6/mod.ts";
20 |
21 | export { default as stripIndent } from "https://esm.sh/v86/strip-indent@4.0.0";
22 | export { default as YAML } from "https://esm.sh/v86/js-yaml@4.1.0";
23 | export { default as pipe } from "https://deno.land/x/froebel@v0.20.0/pipe.ts";
24 | export { openAWebsite } from "https://deno.land/x/open_a_website@0.1.1/mod.ts";
25 |
26 |
27 | export { isURL } from "https://deno.land/x/is_url/mod.ts";
28 | export { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
29 | export { tgz } from "https://deno.land/x/compress@v0.4.4/mod.ts";
30 | export { download } from "https://deno.land/x/download@v1.0.1/mod.ts";
31 |
--------------------------------------------------------------------------------
/entry-point.ts:
--------------------------------------------------------------------------------
1 | import { ROOT_COMMAND } from "./commands/index.ts";
2 |
3 | await (ROOT_COMMAND.parse(Deno.args));
4 |
--------------------------------------------------------------------------------
/errors.ts:
--------------------------------------------------------------------------------
1 | import { Typebox } from "./deps.ts";
2 | import { ajv } from './util/ajv.ts';
3 |
4 | export class RenderCLIError extends Error {
5 |
6 | }
7 |
8 | export class InitRequiredError extends RenderCLIError {
9 | constructor(msg: string) {
10 | super(`${msg}; run 'render config init' to create a config file.`);
11 | }
12 | }
13 |
14 | export class ForceRequiredError extends RenderCLIError {
15 | constructor(msg: string) {
16 | super(`${msg}; pass --force to do this anyway.`);
17 | }
18 | }
19 |
20 | export class PathNotFound extends RenderCLIError {
21 | constructor (
22 | path: string,
23 | cause: Deno.errors.NotFound,
24 | ) {
25 | super(`path '${path}' not found or not readable.`, { cause });
26 | }
27 | }
28 |
29 | export class RepoNotFound extends RenderCLIError {
30 | constructor (
31 | name: string,
32 | ) {
33 | super(`Repo '${name}' not found.`);
34 | }
35 | }
36 |
37 | export class APIKeyRequired extends RenderCLIError {
38 | constructor() {
39 | super(
40 | 'No API key found. Please set the RENDERCLI_APIKEY environment variable or run `render config init`.',
41 | );
42 | }
43 | }
44 |
45 | export class ValidationFailed extends RenderCLIError {
46 | constructor(schema: Typebox.TSchema, errors: typeof ajv.errors) {
47 | super(
48 | `Error validating object of type ${schema.title ?? schema.$id ?? 'unknown'}: ` +
49 | "\n\n" +
50 | (errors ?? []).map((error:any) => ajv.errorsText([error])).join("\n")
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/import_map.json:
--------------------------------------------------------------------------------
1 | {
2 | "imports": {}
3 | }
4 |
--------------------------------------------------------------------------------
/new/blueprint/index.ts:
--------------------------------------------------------------------------------
1 | import { NON_INTERACTIVE } from "../../util/logging.ts";
2 |
3 | export async function interactivelyEditBlueprint(path: string): Promise {
4 | if (NON_INTERACTIVE) {
5 | throw new Error("Not usable in non-interactive mode.");
6 | }
7 |
8 | throw new Error("TODO: implement");
9 | }
--------------------------------------------------------------------------------
/new/repo/index.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals } from "https://deno.land/std@0.151.0/testing/asserts.ts";
2 | import { verboseLogging } from "../../util/logging.ts";
3 | import { pathExists } from "../../util/paths.ts";
4 | import { templateNewProject, writeExampleBlueprint } from "./index.ts";
5 |
6 | // TODO: tests DO pass, but currently leak resources from the `tgz` dep and we should remove it
7 | // uncomment once fixed (n.b.: this is not a problem in the CLI itself, just the tests)
8 | // Also be aware of GitHub API rate limiting when running tests.
9 |
10 | // TODO: add tests for 'resolveTemplateIdentifier' when not just github
11 | // TODO: add tests for `force` option
12 | // Deno.test("repo from-template", async (t) => {
13 | // const root = await Deno.makeTempDir();
14 | // const outputDir = `${root}/output`;
15 |
16 | // await templateNewProject({
17 | // identifier: "sveltekit",
18 | // outputDir,
19 | // });
20 |
21 | // assertEquals(true, !!(await pathExists(`${outputDir}/render.yaml`)));
22 |
23 | // await Deno.remove(root, { recursive: true });
24 |
25 | // });
26 |
27 | // Deno.test("blueprint from-template", async (t) => {
28 | // const root = Deno.makeTempDirSync();
29 | // const repoDir = `${root}/output`;
30 |
31 | // await Deno.mkdir(repoDir);
32 | // const proc = Deno.run({
33 | // cmd: [ 'git', 'init' ],
34 | // cwd: repoDir,
35 | // });
36 | // const success = (await proc.status()).success;
37 | // assertEquals(true, success);
38 |
39 | // await writeExampleBlueprint({
40 | // identifier: "sveltekit",
41 | // repoDir,
42 | // });
43 |
44 | // assertEquals(true, !!(await pathExists(`${repoDir}/render.yaml`)));
45 |
46 | // await Deno.remove(root, { recursive: true });
47 | // });
48 |
--------------------------------------------------------------------------------
/new/repo/index.ts:
--------------------------------------------------------------------------------
1 | import { ForceRequiredError, RepoNotFound } from "../../errors.ts";
2 | import { Cliffy, Path, FS, download, tgz } from "../../deps.ts";
3 | import { identity } from "../../util/fn.ts";
4 | import { unwrapAsyncIterator } from "../../util/iter.ts";
5 | import { getLogger } from "../../util/logging.ts";
6 | import { pathExists } from "../../util/paths.ts";
7 | import { findUp } from "../../util/find-up.ts";
8 |
9 | export type TemplateNewProjectArgs = {
10 | identifier: string;
11 | outputDir?: string;
12 | force?: boolean;
13 | skipCleanup?: boolean;
14 | }
15 |
16 | const GITHUB_REPO_API_BASE = "https://api.github.com/repos";
17 | const IDENTIFIER_REGEX = /^(?:(?:(?(github)):)?(?:(?[a-zA-Z0-9\-\_]+)\/)?(?:(?[a-zA-Z0-9\-\_]+))(?:@(?.+)))|(?[a-zA-Z0-9\-\_]+)$/;
18 | const DEFAULT_PROVIDER = 'github';
19 | const DEFAULT_USER = 'render-examples';
20 |
21 | type Locator = {
22 | provider: string,
23 | user: string,
24 | repo: string,
25 | gitref?: string,
26 | }
27 |
28 | function resolveTemplateIdentifier(s: string): Locator {
29 | if (s.startsWith('http:') || s.startsWith('https:')) {
30 | // the reason we don't support arbitrary git repos is that GitHub lets us download
31 | // a given gitref as a tarball and it's WAY WAY faster, plus doesn't run the risk of
32 | // hitting weird git-credential-manager jank.
33 | throw new Error("TODO: support arbitrary git repos as Render templates.");
34 | }
35 |
36 | const match = IDENTIFIER_REGEX.exec(s);
37 | if (!match) {
38 | throw new Error(`Invalid template identifier: ${s}`);
39 | }
40 |
41 | const ret: Locator = {
42 | provider: match.groups?.provider ?? DEFAULT_PROVIDER,
43 | user: match.groups?.user ?? DEFAULT_USER,
44 | repo: match.groups?.repoSolo ?? match.groups?.repo!,
45 | gitref: match.groups?.gitref
46 | };
47 |
48 | if (!ret.repo) {
49 | throw new Error(`Couldn't resolve template identifier '${s}'; best guess: '${Deno.inspect(ret)}'.`);
50 | }
51 |
52 | return ret;
53 | }
54 |
55 |
56 |
57 | export async function templateNewProject(args: TemplateNewProjectArgs): Promise {
58 | const logger = await getLogger();
59 |
60 | const tempDir = await Deno.makeTempDir({ prefix: 'rendercli_' });
61 |
62 | try {
63 | logger.debug("CWD:", Deno.cwd());
64 | logger.debug(`Attempting to resolve template '${args.identifier}'.`);
65 | const locator = resolveTemplateIdentifier(args.identifier);
66 |
67 | logger.debug(`Ensuring repo is a valid Render template: ${Deno.inspect(locator)}`);
68 | await ensureRepoIsValid(locator);
69 |
70 | if (!args.outputDir) {
71 | const response = await Cliffy.prompt([
72 | {
73 | name: 'outputDir',
74 | message: 'What do you want this project to be called?',
75 | default: locator.repo,
76 | type: Cliffy.Input,
77 | },
78 | ]);
79 |
80 | args.outputDir = response.outputDir ?? `./${locator.repo}`;
81 | }
82 |
83 | args.outputDir = Path.resolve(args.outputDir);
84 |
85 | if (await pathExists(args.outputDir) && !args.force) {
86 | throw new ForceRequiredError(`'${args.outputDir}' already exists`);
87 | }
88 |
89 | logger.debug(`Initializing your project at '${args.outputDir}'...`);
90 | await downloadRepo(locator, tempDir, args.outputDir, args.force);
91 |
92 | // TODO: much like degit, do we want to offer customization steps per-template?
93 | // this could be a separate yaml file in the repo, deleted once the repo
94 | // is instantiated
95 |
96 | console.log(`🎉 Done! 🎉 Your project's now ready to use! Just`);
97 | console.log("");
98 | console.log(Cliffy.colors.brightYellow(`cd ${args.outputDir}`));
99 | console.log("");
100 | console.log("and you're good to go!");
101 | console.log("");
102 | console.log(Cliffy.colors.brightWhite("Now that you have a new blueprint set up, you'll need to push it to your repo."))
103 | console.log(`Once done, ${Cliffy.colors.cyan("render buildpack launch")} to easily deploy your project to Render.`);
104 | console.log("");
105 | console.log("Thanks for using Render, and good luck with your new project!");
106 | } finally {
107 | if (!args.skipCleanup) {
108 | await Deno.remove(tempDir, { recursive: true });
109 | }
110 | }
111 | }
112 |
113 | async function ensureRepoIsValid(loc: Locator) {
114 | const logger = await getLogger();
115 |
116 | switch (loc.provider) {
117 | case 'github': {
118 | const repoInfoResp = await fetch([ GITHUB_REPO_API_BASE, loc.user, loc.repo ].join('/'));
119 | if (repoInfoResp.status !== 200) {
120 | throw new RepoNotFound(`github:${loc.user}/${loc.repo}`);
121 | }
122 |
123 | const repoInfo = await repoInfoResp.json();
124 | logger.debug(`Found a repo: ${repoInfo.full_name}`);
125 | return true;
126 | }
127 | default: {
128 | throw new Error(`unrecognized project provider: ${loc.provider}`);
129 | }
130 | }
131 | }
132 |
133 | // this isn't "clone" repo because we're using the github tarball service to save
134 | // user time.
135 | async function downloadRepo(loc: Locator, tempDir: string, outDir: string, force?: boolean): Promise {
136 | const logger = await getLogger();
137 |
138 | switch (loc.provider) {
139 | case 'github': {
140 | const tarballUrl = [GITHUB_REPO_API_BASE, loc.user, loc.repo, loc.gitref, 'tarball'].filter(identity).join('/');
141 | logger.debug(`tarball URL: ${tarballUrl}`);
142 |
143 | const destinationFile = `${tempDir}/repo.tgz`;
144 | const tempUnzip = `${tempDir}/unzipped`
145 |
146 | logger.debug(`Downloading to '${destinationFile}'...`);
147 | await download(tarballUrl, { dir: tempDir, file: 'repo.tgz' });
148 |
149 | logger.debug(`Decompressing to '${tempUnzip}'...`);
150 | await tgz.uncompress(destinationFile, tempUnzip);
151 |
152 | // github sticks the actual repo one level deep, so we gotta go get it
153 | const entries = await unwrapAsyncIterator(FS.expandGlob(`${loc.user}-${loc.repo}-*`, { root: tempUnzip }));
154 | if (entries.length !== 1) {
155 | throw new Error(`Wrong number of dir entries in Github tarball download: ${Deno.inspect(entries.map(e => e.path))}`);
156 | }
157 |
158 | const entry = entries[0];
159 | logger.debug(`Checking '${entry.path}' for render.yaml...`);
160 | if (!(await pathExists(`${entry.path}/render.yaml`))) {
161 | throw new Error("This repo doesn't seem to be a valid Render template; there's no render.yaml file. Contact us for assistance!");
162 | }
163 |
164 | logger.debug(`Moving '${entry.path}' to '${outDir}'...`);
165 | await FS.move(entry.path, outDir, { overwrite: force });
166 |
167 | return;
168 | }
169 | default: {
170 | throw new Error(`unrecognized project provider: ${loc.provider}`);
171 | }
172 | }
173 | }
174 |
175 | export type WriteExampleBlueprintArgs = {
176 | identifier: string;
177 | repoDir?: string;
178 | skipCleanup?: boolean;
179 | };
180 |
181 | export async function writeExampleBlueprint(
182 | args: WriteExampleBlueprintArgs,
183 | ) {
184 | const logger = await getLogger();
185 |
186 | const tempDir = await Deno.makeTempDir({ prefix: 'rendercli_' });
187 | const userRepoLocation = Path.resolve(args.repoDir ?? Deno.cwd());
188 |
189 | try {
190 | logger.debug("Repo diretory:", userRepoLocation);
191 | logger.debug(`Attempting to resolve template '${args.identifier}'.`);
192 | const locator = resolveTemplateIdentifier(args.identifier);
193 |
194 | logger.debug(`Ensuring repo is a valid Render template: ${Deno.inspect(locator)}`);
195 | await ensureRepoIsValid(locator);
196 |
197 |
198 | logger.debug(`Making sure we're in a git repo from '${userRepoLocation}'...`);
199 | const gitDir = await findUp(userRepoLocation, '.git', { searchFor: 'directories' });;
200 |
201 | if (!gitDir) {
202 | throw new Error("You must be in a git repository to use this command.");
203 | }
204 |
205 | const repoDir = Path.dirname(Path.resolve(gitDir));
206 | logger.debug(`Repo dir: ${repoDir}`);
207 |
208 | const renderYamlPath = `${repoDir}/render.yaml`;
209 | if (await pathExists(renderYamlPath)) {
210 | logger.warning("This repo already has a render.yaml file; we're going to copy it to 'render.yaml.old'.");
211 | await Deno.copyFile(renderYamlPath, `${renderYamlPath}.old`);
212 | }
213 |
214 | const unzipDir = `${tempDir}/blueprint-source`;
215 | await downloadRepo(locator, tempDir, unzipDir, true);
216 |
217 | logger.debug(`Copying '${unzipDir}/render.yaml' to '${renderYamlPath}'...`);
218 | await Deno.copyFile(`${unzipDir}/render.yaml`, renderYamlPath);
219 |
220 | console.log(`🎉 Done! 🎉 Your project's now ready to use! Your new ${Cliffy.colors.cyan(locator.repo)} blueprint has been saved to`);
221 | console.log("");
222 | console.log(Cliffy.colors.brightYellow(renderYamlPath));
223 | console.log("");
224 | console.log(Cliffy.colors.brightWhite("Please make sure to review the blueprint's contents to make sure they're appropriate for your app!"));
225 | console.log("");
226 | console.log(Cliffy.colors.brightWhite("Now that you have a new blueprint set up, you'll need to push it to your repo."))
227 | console.log(`Once done, ${Cliffy.colors.cyan("render buildpack launch")} to easily deploy your project to Render.`);
228 | console.log("");
229 | console.log("Thanks for using Render, and good luck with your new project!");
230 | } finally {
231 | if (!args.skipCleanup) {
232 | await Deno.remove(tempDir, { recursive: true });
233 | }
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/render.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | - type: web
3 | name: sveltekit
4 | env: node
5 | buildCommand: npm install && npm run build
6 | startCommand: node build/index.js
7 | autoDeploy: false
--------------------------------------------------------------------------------
/services/constants.ts:
--------------------------------------------------------------------------------
1 | export const UNLOGGABLE_SERVICE_TYPES = Object.freeze(new Set([
2 | 'static_site',
3 | ]));
4 |
5 | export const UNSHELLABLE_SERVICE_TYPES = Object.freeze(new Set([
6 | 'static_site',
7 | ]));
8 |
--------------------------------------------------------------------------------
/services/ssh/index.ts:
--------------------------------------------------------------------------------
1 | import { getRequestJSON } from "../../api/requests.ts";
2 | import { Region } from "../../config/types/enums.ts";
3 | import { RuntimeConfiguration } from "../../config/types/index.ts";
4 | import { RenderCLIError } from "../../errors.ts";
5 | import { getLogger, NON_INTERACTIVE } from "../../util/logging.ts";
6 | import { UNSHELLABLE_SERVICE_TYPES } from "../constants.ts";
7 | import { updateKnownHosts } from "./known-hosts.ts";
8 |
9 | const RENDER_CLI_SUBCOMMAND_ENV = {
10 | IS_RENDERCLI_SUBCOMMAND: "1",
11 | };
12 |
13 | export type RunSSHArgs = {
14 | config: RuntimeConfiguration,
15 | serviceId: string;
16 | region: Region;
17 | sshArgs: Array;
18 | noHosts?: boolean;
19 | }
20 |
21 | export async function runSSH(args: RunSSHArgs): Promise {
22 | const config = args.config;
23 | const logger = await getLogger();
24 |
25 | logger.debug("dispatching to check for static_site");
26 | const service = (await getRequestJSON(
27 | logger,
28 | config,
29 | `/services/${args.serviceId}`,
30 | // TODO: resolve later when API clients are functional
31 | // deno-lint-ignore no-explicit-any
32 | )) as any;
33 |
34 | if (UNSHELLABLE_SERVICE_TYPES.has(service.type)) {
35 | throw new RenderCLIError(`Service '${args.serviceId}' is of type '${service.type}', which cannot be SSH'd into.`);
36 | }
37 |
38 | logger.debug("Updating known hosts before invoking.");
39 | if (!args.noHosts) {
40 | await updateKnownHosts();
41 | }
42 |
43 | const sshTarget = `${args.serviceId}@ssh.${args.region}.render.com`;
44 |
45 | logger.debug(`SSH target: ${sshTarget}`);
46 |
47 | const cmd = ['ssh', sshTarget, ...args.sshArgs];
48 | logger.debug(`Deno.run args: ${JSON.stringify(cmd)}`);
49 |
50 | const process = Deno.run({
51 | cmd,
52 | stdout: 'inherit',
53 | stderr: 'inherit',
54 | stdin: NON_INTERACTIVE ? 'null' : 'inherit',
55 | env: RENDER_CLI_SUBCOMMAND_ENV,
56 | });
57 |
58 | logger.debug(`SSH process started as PID ${process.pid}`);
59 | const status = await process.status();
60 |
61 | return status;
62 | }
63 |
--------------------------------------------------------------------------------
/services/ssh/known-hosts.ts:
--------------------------------------------------------------------------------
1 | import { FS } from "../../deps.ts";
2 |
3 | import { Region } from "../../config/types/enums.ts";
4 | import { getLogger } from "../../util/logging.ts";
5 | import { getPaths } from "../../util/paths.ts";
6 |
7 |
8 | export type SSHEndpointInfo = Readonly<{
9 | pubKey: string,
10 | }>;
11 |
12 | export const SSH_ENDPOINT_KEYS: Readonly> = {
13 | ohio: {
14 | pubKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMjC1BfZQ3CYotN1/EqI48hvBpZ80zfgRdK8NpP58v1",
15 | },
16 | frankfurt: {
17 | pubKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILg6kMvQOQjMREehk1wvBKsfe1I3+acRuS8cVSdLjinK",
18 | },
19 | oregon: {
20 | pubKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFON8eay2FgHDBIVOLxWn/AWnsDJhCVvlY1igWEFoLD2",
21 | },
22 | singapore: {
23 | pubKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGVcVcsy7RXA60ZyHs/OMS5aQj4YQy7Qn2nJCXHz4zLA",
24 | },
25 | };
26 |
27 | const SSH_SERVER_REGEX = RegExp(
28 | "^ssh.(?(" +
29 | Object.keys(SSH_ENDPOINT_KEYS).join("|") +
30 | ")).render.com",
31 | );
32 |
33 | export async function updateKnownHosts(path?: string) {
34 | // this fn is written as it is because if we don't actually change known_hosts,
35 | // we don't want to update the one that the user already has (because a change in
36 | // mtime can trigger security stuff in some endpoint protection).
37 | const p = path ?? `${(await getPaths()).sshDir}/known_hosts`;
38 |
39 | const logger = await getLogger();
40 | const file = await Deno.readTextFile(p);
41 | const lines = file.split("\n");
42 | const outLines: Array = [];
43 |
44 | let regions = Object.keys(SSH_ENDPOINT_KEYS);
45 | let changesMade = false;
46 |
47 | lines.forEach(line => {
48 | const match = SSH_SERVER_REGEX.exec(line);
49 |
50 | if (line.length === 0 || !match) {
51 | outLines.push(line);
52 | return;
53 | }
54 |
55 | const region = match.groups?.region;
56 | if (!region) {
57 | // should never actually hit this, given the regex
58 | throw new Error("firewall: bad ssh known_hosts match");
59 | }
60 |
61 | const [host, ...rest] = line.split(" ");
62 | const key = rest.join(' ');
63 |
64 | const knownKey = SSH_ENDPOINT_KEYS[region as Region].pubKey;
65 | if (key?.trim() !== knownKey) {
66 | logger.warning(`Incorrect SSH pubkey found for '${host}'; was '${key}', updated to '${knownKey}'.`);
67 | outLines.push(`${host} ${knownKey}`);
68 | changesMade = true;
69 | }
70 |
71 | regions = regions.filter(r => r === region);
72 | });
73 |
74 | logger.debug(`Regions not in known hosts: ${regions.join(' ')}`);
75 |
76 | for (const region of regions) {
77 | const { pubKey } = SSH_ENDPOINT_KEYS[region as Region];
78 | changesMade = true;
79 |
80 | const newLine = `ssh.${region}.render.com ${pubKey}`;
81 | logger.info("Appending to known_hosts:", newLine);
82 | outLines.push(newLine);
83 | }
84 |
85 | if (changesMade) {
86 | logger.info(`Updating '${p}'. Copying old to '${p}.old'.`);
87 | await FS.copy(p, `${p}.old`, { overwrite: true });
88 | await Deno.writeTextFile(p, outLines.join("\n"));
89 | } else {
90 | logger.debug("No updates to known_hosts.");
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/services/types.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Static,
3 | Type
4 | } from '../deps.ts';
5 |
6 | export const LogTailEntry = Type.Object({
7 | id: Type.String(),
8 | serviceID: Type.String(),
9 | deployID: Type.String(),
10 | timestamp: Type.Number(),
11 | text: Type.String(),
12 | });
13 | export type LogTailEntry = Static;
14 |
15 |
--------------------------------------------------------------------------------
/util/ajv.ts:
--------------------------------------------------------------------------------
1 | import { Ajv, AjvFormats, Log, Typebox } from "../deps.ts";
2 | import { ValidationFailed } from "../errors.ts";
3 |
4 | // @ts-ignore esm.sh compile weirdness; typecheck is correct.
5 | export const ajv = AjvFormats(new Ajv({
6 | allErrors: true,
7 | }), [
8 | 'date-time',
9 | 'time',
10 | 'date',
11 | 'email',
12 | 'hostname',
13 | 'ipv4',
14 | 'ipv6',
15 | 'uri',
16 | 'uri-reference',
17 | 'uuid',
18 | 'uri-template',
19 | 'json-pointer',
20 | 'relative-json-pointer',
21 | 'regex',
22 | ])
23 | .addKeyword("kind")
24 | .addKeyword("modifier");
25 |
26 | export function logAjvErrors(logger: Log.Logger, errors: typeof ajv.errors) {
27 | if (!errors) {
28 | return;
29 | }
30 |
31 | for (const error of errors) {
32 | // TODO: ajv default errors aren't great
33 | logger.error(ajv.errorsText([error]));
34 | }
35 | }
36 |
37 | export function assertType(schema: T, content: unknown): asserts content is Typebox.Static {
38 | const isValid = ajv.validate(schema, content);
39 | if (!isValid) {
40 | throw new ValidationFailed(schema, ajv.errors);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/util/errors.ts:
--------------------------------------------------------------------------------
1 | // any here lets us use `funcError` in a ternary tree
2 | // deno-lint-ignore no-explicit-any
3 | export function funcError(err: Error): T {
4 | throw err;
5 | }
6 |
--------------------------------------------------------------------------------
/util/find-up.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals } from "../deps.ts";
2 | import {
3 | findUp,
4 | } from './find-up.ts';
5 |
6 | function f(path: string) {
7 | Deno.writeFileSync(path, new Uint8Array());
8 | }
9 |
10 | Deno.test('findUp', async (t) => {
11 | const root = Deno.makeTempDirSync({
12 | prefix: 'find-up-test-',
13 | });
14 |
15 | Deno.mkdirSync(`${root}/a/b/c/d/e/f`, { recursive: true });
16 | Deno.mkdirSync(`${root}/a/b/c/X/Y/Z`, { recursive: true });
17 | Deno.mkdirSync(`${root}/1/2/3/4/5/6/abc`, { recursive: true });
18 | Deno.mkdirSync(`${root}/1/abc`, { recursive: true });
19 |
20 | f(`${root}/a/b/c/d/e/f/package.json`);
21 | f(`${root}/a/b/c/package.json`);
22 | f(`${root}/1/2/3/4/abc`);
23 |
24 | await t.step('should find a file in the current directory', async () => {
25 | const ret = await findUp(`${root}/a/b/c/d/e/f`, 'package.json');
26 | assertEquals(ret, `${root}/a/b/c/d/e/f/package.json`);
27 | });
28 |
29 | await t.step('should traverse upwards to find a file', async () => {
30 | const ret = await findUp(`${root}/a/b/c/d/e`, 'package.json');
31 | assertEquals(ret, `${root}/a/b/c/package.json`);
32 | });
33 |
34 | await t.step('should iterate multiple start paths', async () => {
35 | const ret = await findUp([`${root}/1/2/3/4/5/6`, `${root}/a/b/c/d/e`], 'package.json');
36 | assertEquals(ret, `${root}/a/b/c/package.json`);
37 | });
38 |
39 | await t.step('should skip directories when opts.searchFor === "files"', async () => {
40 | const ret = await findUp(`${root}/1/2/3/4/5/6`, 'abc', { searchFor: 'files' });
41 | assertEquals(ret, `${root}/1/2/3/4/abc`);
42 | });
43 |
44 | await t.step('should skip files when opts.searchFor === "directories"', async () => {
45 | const ret = await findUp(`${root}/1/2/3/4/5`, 'abc', { searchFor: 'directories' });
46 | assertEquals(ret, `${root}/1/abc`);
47 | });
48 |
49 | await t.step('should return null when no file is found', async () => {
50 | const ret = await findUp(`${root}/a/b/c/d/e`, 'does-not-exist');
51 | assertEquals(ret, null);
52 | });
53 |
54 | await Deno.remove(root, { recursive: true });
55 | });
56 |
--------------------------------------------------------------------------------
/util/find-up.ts:
--------------------------------------------------------------------------------
1 |
2 | import { Path } from "../deps.ts";
3 | import { pathExists } from "./paths.ts";
4 |
5 | // this originates from @eropple/find-up:
6 | // https://github.com/eropple/find-up/blob/master/src/index.ts
7 |
8 | export interface FindUpOpts {
9 | /**
10 | * Determines whether to search for directories or files. Leave undefined for either.
11 | */
12 | searchFor?: 'directories' | 'files';
13 | }
14 |
15 | /**
16 | * Searches upward from the given `startPath`(s) and return the full path of a file that
17 | * matches `searchFile`.
18 | *
19 | * While multiple `startPath` entries can be given, the first `searchFile` found in is the
20 | * only one that will be returned. Subsequent `startPath`s will not be evaluated.
21 | *
22 | * @param startPaths The set of paths to search from. Will search in order.
23 | * @param searchFile The filename to search for. Does not support globs.
24 | */
25 | export async function findUp(startPaths: string | Array, searchFile: string, opts: FindUpOpts = {}): Promise {
26 | startPaths = (typeof startPaths === 'string') ? [startPaths] : startPaths;
27 |
28 | for (const startPath of startPaths) {
29 | let p: string = startPath;
30 | let lastPath: string | null = null;
31 | do {
32 | const candidate = `${p}/${searchFile}`;
33 |
34 | const fstat = await pathExists(candidate);
35 | if (fstat) {
36 | if (opts.searchFor) {
37 | if (opts.searchFor === 'files' && fstat.isFile) {
38 | return candidate;
39 | }
40 |
41 | if (opts.searchFor === 'directories' && fstat.isDirectory) {
42 | return candidate;
43 | }
44 | } else {
45 | return candidate;
46 | }
47 | }
48 |
49 | // `path.resolve('/..') === '/'`; I assume the same is true on Windows.
50 | lastPath = p;
51 | p = Path.resolve(`${p}/..`);
52 | } while (p !== lastPath);
53 | }
54 |
55 | return null;
56 | }
57 |
--------------------------------------------------------------------------------
/util/fn.ts:
--------------------------------------------------------------------------------
1 | export const identity = (a: T) => a;
2 |
--------------------------------------------------------------------------------
/util/git.ts:
--------------------------------------------------------------------------------
1 | // no good deno git library yet. :(
2 |
3 | export async function listRemotes(args: { cwd?: string, https?: boolean }): Promise> {
4 | const process = await Deno.run({
5 | cmd: ['git', 'remote', '-v'],
6 | cwd: args.cwd ?? Deno.cwd(),
7 | stdin: 'null',
8 | stdout: 'piped',
9 | });
10 |
11 | const decoder = new TextDecoder();
12 | const lines =
13 | decoder.decode(await process.output())
14 | .split("\n").map(i => i.trim());
15 |
16 | const ret: Record = {};
17 |
18 | lines.forEach(line => {
19 | const [name, url] = line.split(/[\ \t]/).filter(i => i);
20 |
21 | if (!name || !url) {
22 | return;
23 | }
24 |
25 | ret[name] = gitUrlToHttpsUrl(url);
26 | });
27 |
28 | return ret;
29 | }
30 |
31 | export function gitUrlToHttpsUrl(gitUrl: string) {
32 | if (gitUrl.startsWith('https://')) {
33 | return gitUrl;
34 | }
35 |
36 | const [sshPart, urlPart] = gitUrl.replace("git+ssh://", "").split(':');
37 |
38 | switch (sshPart) {
39 | case 'git@github.com':
40 | return `https://github.com/${urlPart}`;
41 | case 'git@gitlab.com':
42 | return `https://gitlab.com/${urlPart}`;
43 | default:
44 | throw new Error('Unrecognized SSH URL: ' + gitUrl);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/util/iter.ts:
--------------------------------------------------------------------------------
1 | export async function unwrapAsyncIterator(iter: AsyncIterableIterator): Promise> {
2 | const ret: Array = [];
3 |
4 | // deno-lint-ignore no-explicit-any
5 | let i: IteratorResult;
6 | do {
7 | i = await iter.next();
8 | i.value && ret.push(i.value);
9 | } while (!i.done);
10 |
11 | return ret;
12 | }
13 |
--------------------------------------------------------------------------------
/util/logging.ts:
--------------------------------------------------------------------------------
1 | import { Cliffy, Log } from '../deps.ts';
2 | import { VERSION } from '../version.ts';
3 | import { getAtPath } from "./objects.ts";
4 |
5 | let isLoggingSetup = false;
6 | let LOG_VERBOSITY: 'INFO' | 'DEBUG' = 'INFO';
7 | let PRETTY_JSON = false;
8 | let JSON_RECORD_PER_LINE = false;
9 | export let NON_INTERACTIVE = !Deno.isatty(Deno.stdout.rid);
10 |
11 | export async function verboseLogging() {
12 | if (isLoggingSetup) {
13 | Log.getLogger().warning("`verboseLogging` called after logging setup.");
14 | }
15 |
16 | // Deno's log function may be already called when these methods are called
17 | // because the CLI framework doesn't seem consistent in what order it calls
18 | // the `action` parameter for global options, so as a compromise we blow away
19 | // the existing logging config and re-instantiate.
20 | LOG_VERBOSITY = 'DEBUG'
21 | isLoggingSetup = false;
22 | await setupLogging();
23 | }
24 |
25 | export function nonInteractive() {
26 | if (isLoggingSetup) {
27 | Log.getLogger().warning("`nonInteractive` called after logging setup.");
28 | }
29 |
30 | NON_INTERACTIVE = true;
31 | }
32 |
33 | export function prettyJson() {
34 | if (isLoggingSetup) {
35 | Log.getLogger().warning("`prettyJson` called after logging setup.");
36 | }
37 |
38 | PRETTY_JSON = true;
39 | }
40 |
41 | export function jsonRecordPerLine() {
42 | if (isLoggingSetup) {
43 | Log.getLogger().warning("`prettyJson` called after logging setup.");
44 | }
45 |
46 | JSON_RECORD_PER_LINE = true;
47 | }
48 |
49 |
50 | export type SetupLoggingArgs = {
51 | nonInteractive?: boolean,
52 | verbosity?: 'INFO' | 'DEBUG',
53 | }
54 |
55 | // @ts-ignore: Deno types for Log.handlers seem odd, but it's legit
56 | class StderrHandler extends Log.handlers.ConsoleHandler {
57 | private readonly encoder = new TextEncoder();
58 |
59 | override log(msg: string): void {
60 | Deno.stderr.writeSync(this.encoder.encode(msg + "\n"));
61 | }
62 | }
63 |
64 | async function setupLogging() {
65 | if (!isLoggingSetup) {
66 | const verbosity = LOG_VERBOSITY;
67 | const handler = NON_INTERACTIVE
68 | ? new StderrHandler(verbosity)
69 | : new Log.handlers.ConsoleHandler(verbosity);
70 |
71 | await Log.setup({
72 | handlers: {
73 | console: handler,
74 | },
75 | loggers: {
76 | default: {
77 | level: verbosity,
78 | handlers: ['console'],
79 | },
80 | }
81 | });
82 |
83 | isLoggingSetup = true;
84 | }
85 |
86 | if (VERSION.startsWith("0")) {
87 | (await getLogger()).warning(`render-cli is still pre-1.0.0 and as such all functionality should be considered subject to change.`);
88 | }
89 | }
90 |
91 | export async function getLogger(name?: string) {
92 | if (!isLoggingSetup) {
93 | await setupLogging();
94 | }
95 |
96 | return Log.getLogger(name);
97 | }
98 |
99 | export function renderInteractiveOutput(
100 | obj: unknown,
101 | tableColumns?: string[],
102 | format = 'table',
103 | ): void {
104 | switch (format) {
105 | case 'json':
106 | renderJsonOutput(obj);
107 | return;
108 | case 'table':
109 | if (Array.isArray(obj)) {
110 | if (!tableColumns) {
111 | throw new Error(`Interactive output in table mode, but no table columns provided.`);
112 | }
113 |
114 | const table = Cliffy.Table.from(
115 | obj.map(
116 | // deno-lint-ignore no-explicit-any
117 | (o: any) => tableColumns.map(c => getAtPath(c, o)),
118 | ),
119 | );
120 | table.unshift(tableColumns);
121 |
122 | table.render();
123 | } else {
124 | console.log(Deno.inspect(obj, {
125 | colors: true,
126 | depth: Infinity,
127 | sorted: true,
128 | }));
129 | }
130 | return;
131 | default:
132 | throw new Error(`Unrecognized interactive output format: '${format}'`);
133 | }
134 | }
135 |
136 | export function renderJsonOutput(obj: unknown): void {
137 | const output = (
138 | JSON_RECORD_PER_LINE
139 | ? Array.isArray(obj)
140 | ? obj.map(item => JSON.stringify(item)).join("\n")
141 | : JSON.stringify(obj)
142 | : PRETTY_JSON
143 | ? JSON.stringify(obj, null, 2)
144 | : JSON.stringify(obj)
145 | );
146 |
147 | console.log(output);
148 | }
149 |
--------------------------------------------------------------------------------
/util/objects.ts:
--------------------------------------------------------------------------------
1 | // we are deep in "dealing with untyped data" territory here. hopefully
2 | // we can get riud of stuff like this once we have a typed API client.
3 | //
4 | // deno-lint-ignore-file no-explicit-any
5 | export function getAtPath(path: string | Array, obj: Record): any {
6 | const p =
7 | typeof(path) === 'string'
8 | ? path.split('.')
9 | : path;
10 |
11 | let curr = obj;
12 | while (p.length > 0) {
13 | curr = curr[p.shift()!];
14 | }
15 |
16 | return curr;
17 | }
18 |
--------------------------------------------------------------------------------
/util/paths.ts:
--------------------------------------------------------------------------------
1 | export type AppPaths = {
2 | renderDir: string,
3 | configFile: string,
4 | cacheDir: string,
5 | sshDir: string,
6 | };
7 |
8 | export function getPaths(): AppPaths {
9 | const defaultHome = Deno.env.get("HOME");
10 | if (!defaultHome) {
11 | throw new Error("No $HOME env var set.");
12 | }
13 |
14 | const renderDir = `${defaultHome}/.render`;
15 |
16 | return {
17 | renderDir,
18 | configFile: Deno.env.get("RENDERCLI_CONFIG_FILE") ?? `${renderDir}/config.yaml`,
19 | cacheDir: Deno.env.get("RENDERCLI_CACHE_DIR") ?? `${renderDir}/cache`,
20 | sshDir: Deno.env.get("RENDERCLI_SSH_DIR") ?? `${defaultHome}/.ssh`,
21 | };
22 | }
23 |
24 |
25 | export async function pathExists(
26 | path: string,
27 | ): Promise {
28 | try {
29 | const ret = await Deno.stat(path);
30 | return ret;
31 | } catch (err) {
32 | if (err instanceof Deno.errors.NotFound) {
33 | return false;
34 | }
35 |
36 | throw err;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/util/shell.ts:
--------------------------------------------------------------------------------
1 | import { RenderCLIError } from "../errors.ts";
2 |
3 | export async function simpleRun(cmd: Array, msgOnFail?: string): Promise {
4 | const process = Deno.run({
5 | cmd,
6 | stderr: 'null',
7 | stdout: 'piped',
8 | });
9 | const code = (await process.status()).code;
10 | if (code != 0) {
11 | throw new RenderCLIError(
12 | `${msgOnFail ?? 'Command failed.'} \`${cmd.join(' ')}\` exit code: ${code}`
13 | )
14 | }
15 |
16 | const arr = await process.output();
17 | return (new TextDecoder()).decode(arr);
18 | }
19 |
--------------------------------------------------------------------------------
/version.ts:
--------------------------------------------------------------------------------
1 | export const VERSION = "git-tree" as const;
2 |
--------------------------------------------------------------------------------