From 92dabb6b80961a4655e1e1af073bbd5c72ea979f Mon Sep 17 00:00:00 2001 From: Gabrielle Poncey Date: Thu, 30 Jul 2026 12:11:25 -0700 Subject: [PATCH 1/7] docs: Restish cli guide Adds a guide for using restish as a cli tool. Document a quickstart process (rapid installation and experimentation), profile management, project configuration and mtls profile setup, database config files and secrets. PLAT-671 --- docs/api/restish.md | 262 ++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 263 insertions(+) create mode 100644 docs/api/restish.md diff --git a/docs/api/restish.md b/docs/api/restish.md new file mode 100644 index 00000000..b77367da --- /dev/null +++ b/docs/api/restish.md @@ -0,0 +1,262 @@ +# Using Restish as a CLI + +The pgEdge Control Plane makes use of a tool called [Restish](https://rest.sh) to get a CLI-like experience against the Control Plane's HTTP API. Restish is a generic, open-source REST client that turns any [OpenAPI](openapi.md)-described API into a set of generated commands, complete with shell completion and readable output. + +Jump to what you're trying to do: + +- **[Quickstart](#quickstart)** — install Restish and run your first command against a cluster. +- **[Managing Multiple Environments](#managing-multiple-environments)** — one cluster isn't enough, or you want a persistent setup you (and your team, if you have one) can reuse instead of reconnecting by hand every time. +- **[Managing Database Configuration as Files](#managing-database-configuration-as-files)** — commit database specs to source control instead of typing JSON inline. + +## Quickstart + +If you already have a Control Plane cluster running (see the +[installation quickstart](../installation/quickstart.md) if not), you can +be running commands against it in three steps: + +First, install Restish: + +```sh +brew install restish +``` + +Then connect Restish to your cluster: + +!!! warning + + Only connect this way to clusters and databases you're okay with experimenting on. See [Managing Multiple Environments](#managing-multiple-environments) + before connecting Restish to anything production. + + +```sh +restish api connect pgedge http://localhost:3000 +``` + +Then run your first command: + +```sh +restish pgedge list-databases +``` + +That's enough to start experimenting — `restish pgedge --help` lists every +generated command, and once you have a database config file (see +[Managing Database Configuration as Files](#managing-database-configuration-as-files) +below), `restish pgedge create-database < your-file.json` creates one. + +See the [Restish install guide](https://rest.sh/docs/getting-started/install/) +for other install options, including release archives, containers, and +building from source. + +## Managing Multiple Environments + +Restish doesn't enforce any naming convention for the APIs you connect to. +We recommend using Restish's **profiles** feature: one API registration, `pgedge`, holds a profile per environment, and each profile can override the base URL (and, if you need it later, auth or other per-environment request details). + +**Use descriptive cluster names.** Every cluster has its own durable `id`, +set at initialization and returned by `get-cluster`. `init-cluster` takes +an optional `cluster_id` query parameter. Setting it to something descriptive will allow you to keep track of multiple different clusters. + +```sh +curl "http://host1.internal:3000/v1/cluster/init?cluster_id=production" +``` + +Then select that cluster with a matching profile: + +```sh +restish -p production pgedge list-databases +restish -p staging pgedge list-databases +``` + +**Leave the `default` profile pointed at something safe.** Restish falls +back to the `default` profile whenever you don't pass `-p`/`--rsh-profile` +or set `RSH_PROFILE`, so be sure to point `default` at your local or informal cluster. + +```sh +restish pgedge list-databases # default profile: local/informal cluster +``` + +!!! note + + A cluster is made up of multiple hosts, each with its own host ID + (e.g. `host-1`). Any of them can serve a request for the whole + cluster, which is why one `base_url` per environment is enough for + routine use — you don't need a profile per host. To target a specific + host directly (retrying against a different one, or comparing behavior + across hosts while debugging), give it its own profile the same way: + `-p production-host-1`. + +### Persisting Connections in a Project Config + +Register `pgedge` once, with a profile per environment, in a `.restish.json` +file at the root of your infrastructure repo, instead of re-running +`restish api connect` by hand every time you check it out. Restish +discovers this file by walking up from your current directory: + +```jsonc +{ + "apis": { + "pgedge": { + "base_url": "http://host1.internal:3000", + "profiles": { + "default": {}, + "staging": { + "base_url": "http://host1.staging.internal:3000" + }, + "production": { + "base_url": "https://host1.prod.internal:3000" + } + } + } + } +} +``` + +The top-level `base_url` is what the `default` profile falls back to — the +same safe-fallback rule described above, expressed as config instead of +prose. `staging` and `production` each override it, and only take effect +when you pass `-p`. + +Commit this file, then run the following once per checkout: + +```sh +restish config trust +``` + +Restish records that trust decision outside the repo, keyed to the file's +contents, so your connections and profiles are ready every time you check +out the repo. If working with a team, everyone ends up with the same +setup without manually running `restish api connect` or keeping a personal +copy in sync. + +!!! note + + A project config only honors the `apis` and `theme` keys — nothing + else. Keep it secret-free: the Control Plane's dev and local endpoints + don't need auth, so these profiles can stay limited to `base_url` as + shown above. If a cluster you connect to does require auth, reference + the value as `env:NAME` rather than committing it literally. + +If a cluster has [mTLS enabled](../installation/mtls.md), add the CA and +client certificate paths to that profile. These are file paths, not the +credentials themselves, so — unlike a token or password — they're fine to +commit as long as everyone using the file also has the actual `ca.crt`, +`client.crt`, and `client.key` in place locally: + +```jsonc +"production": { + "base_url": "https://host1.prod.internal:3000", + "ca_cert": "/opt/pgedge/control-plane/ca.crt", + "client_cert": "/opt/pgedge/control-plane/client.crt", + "client_key": "/opt/pgedge/control-plane/client.key" +} +``` + +Not every connection belongs in that shared file, though. Anything that's + per-machine (eg. a Lima VM IP that's different for every developer, a personal sandbox) should stay out of source control. Restish also +won't let you add a profile to an API that came from trusted project +config, so register a personal one under its own name instead, which +writes to your own local Restish config: + +```sh +restish api connect pgedge-sandbox http://192.168.64.3:3000 +``` + +If a cluster uses TLS with a private CA, or discovery fails for a +connection set up this way, pass `--spec` with an explicit URL or local +file: + +```sh +restish api connect pgedge-sandbox https://192.168.64.3:3000 --spec https://192.168.64.3:3000/v1/openapi.json +``` + +Either way you connect something, the same commands work afterward: + +```sh +restish api list # every connection you've configured +restish api inspect pgedge # the URL, profiles, and spec Restish resolved +restish api remove pgedge-sandbox # disconnect (personal connections only) +restish pgedge --help # every generated command +restish pgedge list-databases --help # options for one command +``` + +## Managing Database Configuration as Files + +Keep one file per database, and use it as the source of truth for that +database's configuration: + +```sh +mkdir -p databases +cat > databases/example.json <<'EOF' +{ + "id": "example", + "spec": { + "database_name": "example", + "database_users": [ + { + "username": "admin", + "password": "password", + "db_owner": true, + "attributes": ["SUPERUSER", "LOGIN"] + } + ], + "port": 5432, + "nodes": [ + { "name": "n1", "host_ids": ["host-1"] }, + { "name": "n2", "host_ids": ["host-2"] }, + { "name": "n3", "host_ids": ["host-3"] } + ] + } +} +EOF +``` + +Apply it by piping the file into the generated command, rather than typing +the JSON body inline: + +```sh +restish pgedge create-database < databases/example.json +``` + +Update the same database by editing the file and re-applying it against the +`update-database` command: + +```sh +restish pgedge update-database example < databases/example.json +``` + +To apply the same file to a specific environment instead of your default +cluster, add the profile you set up in +[Managing Multiple Environments](#managing-multiple-environments): + +```sh +restish -p staging pgedge create-database < databases/example.json +restish -p production pgedge update-database example < databases/example.json +``` + +This gives you a directory of database configuration files you can commit +to source control, diff, review in a pull request, and re-apply — the same +workflow you'd use for any other infrastructure-as-code. + +### Handling Secrets + +The one field in these files that doesn't belong in source control is +`database_users[].password` (and, if you're configuring backups, +credentials like `s3_key_secret`). The Control Plane's update endpoint is +built to make this easy: **secret fields are only required the first time +you create a database. On every `update-database` call after that, you can +leave them out entirely** — the Control Plane keeps whatever value is +already stored unless you explicitly send a new one. See +[Updating a Database](../using/update-db.md) for the full behavior. + +In practice, that means the default workflow is: + +1. Run `create-database` once, with real secret values, from a copy of the + file that never gets committed. +2. Immediately delete the secret fields from `databases/example.json` before + committing it. +3. From then on, `update-database` runs against the secret-free file. If you + need to rotate a password, set it in the file for that one apply, then + remove it again before committing. + +The committed file is always safe to read, diff, and share — it never holds +a credential past the moment it was first used. diff --git a/mkdocs.yml b/mkdocs.yml index 3b09811d..3ddbb59c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -92,6 +92,7 @@ nav: - API: - OpenAPI: api/openapi.md - API Reference: api/reference.md + - Using Restish as a CLI: api/restish.md - Development: - Development Process: development/development.md - Automated End-to-End tests: development/e2e-tests.md From edb357eb39af6ba48111b080b311c202b5d71831 Mon Sep 17 00:00:00 2001 From: Gabrielle Poncey Date: Fri, 31 Jul 2026 12:39:49 -0700 Subject: [PATCH 2/7] docs: fix secrets and TLS gaps in Restish guide Addresses review findings against the guide added in 92dabb6: - Stop putting a real password in the committed databases/example.json example. The initial create-database call now sources secrets from a separate, deleted-after-use file, and the "Handling Secrets" section describes that actual workflow instead of claiming a file that once held a credential is safe after the fact. - Add --rsh-ca-cert to the private-CA connection example; --spec alone only fixes discovery failures; it doesn't establish trust for a private CA. - Make the personal-connection follow-up commands (api inspect, --help) consistently target pgedge-sandbox instead of mixing in the shared pgedge name. --- docs/api/restish.md | 85 +++++++++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/docs/api/restish.md b/docs/api/restish.md index b77367da..cfd6b393 100644 --- a/docs/api/restish.md +++ b/docs/api/restish.md @@ -5,7 +5,7 @@ The pgEdge Control Plane makes use of a tool called [Restish](https://rest.sh) t Jump to what you're trying to do: - **[Quickstart](#quickstart)** — install Restish and run your first command against a cluster. -- **[Managing Multiple Environments](#managing-multiple-environments)** — one cluster isn't enough, or you want a persistent setup you (and your team, if you have one) can reuse instead of reconnecting by hand every time. +- **[Managing Multiple Environments](#managing-multiple-environments)** — one cluster isn't enough, or you want a persistent setup. - **[Managing Database Configuration as Files](#managing-database-configuration-as-files)** — commit database specs to source control instead of typing JSON inline. ## Quickstart @@ -161,22 +161,24 @@ writes to your own local Restish config: restish api connect pgedge-sandbox http://192.168.64.3:3000 ``` -If a cluster uses TLS with a private CA, or discovery fails for a -connection set up this way, pass `--spec` with an explicit URL or local -file: +If a cluster uses TLS with a private CA, pass `--rsh-ca-cert` with the CA +file so Restish trusts it; if discovery also fails for a connection set up +this way, add `--spec` with an explicit URL or local file: ```sh -restish api connect pgedge-sandbox https://192.168.64.3:3000 --spec https://192.168.64.3:3000/v1/openapi.json +restish api connect pgedge-sandbox https://192.168.64.3:3000 \ + --rsh-ca-cert ./ca.crt \ + --spec https://192.168.64.3:3000/v1/openapi.json ``` Either way you connect something, the same commands work afterward: ```sh -restish api list # every connection you've configured -restish api inspect pgedge # the URL, profiles, and spec Restish resolved -restish api remove pgedge-sandbox # disconnect (personal connections only) -restish pgedge --help # every generated command -restish pgedge list-databases --help # options for one command +restish api list # every connection you've configured +restish api inspect pgedge-sandbox # the URL, profiles, and spec Restish resolved +restish api remove pgedge-sandbox # disconnect (personal connections only) +restish pgedge-sandbox --help # every generated command +restish pgedge-sandbox list-databases --help # options for one command ``` ## Managing Database Configuration as Files @@ -194,7 +196,6 @@ cat > databases/example.json <<'EOF' "database_users": [ { "username": "admin", - "password": "password", "db_owner": true, "attributes": ["SUPERUSER", "LOGIN"] } @@ -210,15 +211,41 @@ cat > databases/example.json <<'EOF' EOF ``` -Apply it by piping the file into the generated command, rather than typing -the JSON body inline: +`databases/example.json` never contains a password, so it's safe to commit +right away. Creating a database still needs a real password the first +time, though, so pass that from a separate file you don't commit instead +of adding it to `databases/example.json`: ```sh -restish pgedge create-database < databases/example.json +cat > /tmp/example.create.json <<'EOF' +{ + "id": "example", + "spec": { + "database_name": "example", + "database_users": [ + { + "username": "admin", + "password": "changeme", + "db_owner": true, + "attributes": ["SUPERUSER", "LOGIN"] + } + ], + "port": 5432, + "nodes": [ + { "name": "n1", "host_ids": ["host-1"] }, + { "name": "n2", "host_ids": ["host-2"] }, + { "name": "n3", "host_ids": ["host-3"] } + ] + } +} +EOF +restish pgedge create-database < /tmp/example.create.json +rm /tmp/example.create.json ``` -Update the same database by editing the file and re-applying it against the -`update-database` command: +Update the same database by editing `databases/example.json` and +re-applying it against the `update-database` command. No password is +needed, since it's omitted from the request entirely: ```sh restish pgedge update-database example < databases/example.json @@ -229,7 +256,7 @@ cluster, add the profile you set up in [Managing Multiple Environments](#managing-multiple-environments): ```sh -restish -p staging pgedge create-database < databases/example.json +restish -p staging pgedge update-database example < databases/example.json restish -p production pgedge update-database example < databases/example.json ``` @@ -248,15 +275,15 @@ leave them out entirely** — the Control Plane keeps whatever value is already stored unless you explicitly send a new one. See [Updating a Database](../using/update-db.md) for the full behavior. -In practice, that means the default workflow is: - -1. Run `create-database` once, with real secret values, from a copy of the - file that never gets committed. -2. Immediately delete the secret fields from `databases/example.json` before - committing it. -3. From then on, `update-database` runs against the secret-free file. If you - need to rotate a password, set it in the file for that one apply, then - remove it again before committing. - -The committed file is always safe to read, diff, and share — it never holds -a credential past the moment it was first used. +In practice, that means the default workflow is the one shown above: keep +`databases/example.json` secret-free from the start, and pass real secret +values only from a separate, uncommitted file for the one `create-database` +call that needs them — deleting that file immediately afterward rather than +editing secrets out of the committed file after the fact. From then on, +`update-database` runs against the secret-free file as-is. If you need to +rotate a password, apply it the same way: a temporary file with the new +value, passed once, then removed. + +This keeps `databases/example.json` safe to read, diff, and share at any +point — it's never the file that held the credential, so there's no window +where committing it (or `git add -A`, or a stray backup) could leak one. From 44e159b043b775b04185d1b2b352f288e823f9a9 Mon Sep 17 00:00:00 2001 From: Gabrielle Poncey Date: Mon, 24 Aug 2026 23:09:31 -0700 Subject: [PATCH 3/7] docs: harden temp secrets file example in Restish guide The create-database example wrote a real password to a predictable /tmp path with default permissions and relied on a trailing `rm` for cleanup, which a failed command or an interrupted copy-paste could skip. Switch to umask 077 + mktemp + an EXIT trap so the file is unreadable by other users and always removed on shell exit. Apply the same pattern to the password-rotation description in "Handling Secrets". --- docs/api/restish.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/api/restish.md b/docs/api/restish.md index cfd6b393..4ea12a25 100644 --- a/docs/api/restish.md +++ b/docs/api/restish.md @@ -217,7 +217,10 @@ time, though, so pass that from a separate file you don't commit instead of adding it to `databases/example.json`: ```sh -cat > /tmp/example.create.json <<'EOF' +umask 077 +tmpfile=$(mktemp) +trap 'rm -f "$tmpfile"' EXIT +cat > "$tmpfile" <<'EOF' { "id": "example", "spec": { @@ -239,10 +242,15 @@ cat > /tmp/example.create.json <<'EOF' } } EOF -restish pgedge create-database < /tmp/example.create.json -rm /tmp/example.create.json +restish pgedge create-database < "$tmpfile" ``` +`umask 077` keeps the file unreadable by anyone else on the machine while +it exists, `mktemp` avoids a predictable filename, and the `trap` removes +it as soon as this shell exits — including if `create-database` itself +fails — rather than relying on a final `rm` that a failure or an +interrupted copy-paste could skip. + Update the same database by editing `databases/example.json` and re-applying it against the `update-database` command. No password is needed, since it's omitted from the request entirely: @@ -278,11 +286,13 @@ already stored unless you explicitly send a new one. See In practice, that means the default workflow is the one shown above: keep `databases/example.json` secret-free from the start, and pass real secret values only from a separate, uncommitted file for the one `create-database` -call that needs them — deleting that file immediately afterward rather than -editing secrets out of the committed file after the fact. From then on, -`update-database` runs against the secret-free file as-is. If you need to -rotate a password, apply it the same way: a temporary file with the new -value, passed once, then removed. +call that needs them — a file created with a restrictive `umask`, an +unpredictable `mktemp` path, and an `EXIT` trap so it's removed even if +the command fails, rather than editing secrets out of the committed file +after the fact. From then on, `update-database` runs against the +secret-free file as-is. If you need to rotate a password, apply it the +same way: the same `umask`/`mktemp`/`trap` pattern, with the new value, +passed once. This keeps `databases/example.json` safe to read, diff, and share at any point — it's never the file that held the credential, so there's no window From ab9a30e25da2573b8b61d687293f47a08a4331be Mon Sep 17 00:00:00 2001 From: Gabrielle Poncey Date: Thu, 27 Aug 2026 12:35:38 -0700 Subject: [PATCH 4/7] docs: phrasing, scope secret cleanup to subshell --- docs/api/restish.md | 62 +++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/docs/api/restish.md b/docs/api/restish.md index 4ea12a25..f5ce071c 100644 --- a/docs/api/restish.md +++ b/docs/api/restish.md @@ -1,6 +1,6 @@ # Using Restish as a CLI -The pgEdge Control Plane makes use of a tool called [Restish](https://rest.sh) to get a CLI-like experience against the Control Plane's HTTP API. Restish is a generic, open-source REST client that turns any [OpenAPI](openapi.md)-described API into a set of generated commands, complete with shell completion and readable output. +The pgEdge Control Plane can be used with a tool called [Restish](https://rest.sh) to get a CLI-like experience against the Control Plane's HTTP API. Restish is a generic, open-source REST client that turns any [OpenAPI](openapi.md)-described API into a set of generated commands, complete with shell completion and readable output. Jump to what you're trying to do: @@ -14,23 +14,24 @@ If you already have a Control Plane cluster running (see the [installation quickstart](../installation/quickstart.md) if not), you can be running commands against it in three steps: -First, install Restish: -```sh -brew install restish -``` +### 1. Installation -Then connect Restish to your cluster: +First, install Restish [via Restish's official website](https://rest.sh/docs/getting-started/install/). Restish supports many installation methods, including Homebrew (macOS), github releases, and OCI images; select the option that most aligns with your organizations preferences and practices. +### 2. Connection !!! warning Only connect this way to clusters and databases you're okay with experimenting on. See [Managing Multiple Environments](#managing-multiple-environments) before connecting Restish to anything production. +Connect Restish to your cluster: + ```sh restish api connect pgedge http://localhost:3000 ``` +### 3. Verification Then run your first command: @@ -43,10 +44,6 @@ generated command, and once you have a database config file (see [Managing Database Configuration as Files](#managing-database-configuration-as-files) below), `restish pgedge create-database < your-file.json` creates one. -See the [Restish install guide](https://rest.sh/docs/getting-started/install/) -for other install options, including release archives, containers, and -building from source. - ## Managing Multiple Environments Restish doesn't enforce any naming convention for the APIs you connect to. @@ -217,10 +214,12 @@ time, though, so pass that from a separate file you don't commit instead of adding it to `databases/example.json`: ```sh -umask 077 -tmpfile=$(mktemp) -trap 'rm -f "$tmpfile"' EXIT -cat > "$tmpfile" <<'EOF' +( + set -e + umask 077 + tmpfile=$(mktemp) + trap 'rm -f "$tmpfile"' EXIT + cat > "$tmpfile" <<'EOF' { "id": "example", "spec": { @@ -242,14 +241,16 @@ cat > "$tmpfile" <<'EOF' } } EOF -restish pgedge create-database < "$tmpfile" + restish pgedge create-database < "$tmpfile" +) ``` `umask 077` keeps the file unreadable by anyone else on the machine while -it exists, `mktemp` avoids a predictable filename, and the `trap` removes -it as soon as this shell exits — including if `create-database` itself -fails — rather than relying on a final `rm` that a failure or an -interrupted copy-paste could skip. +it exists, `mktemp` avoids a predictable filename, and the subshell `(...)` +limits the `trap`'s scope: when `create-database` returns (or fails), the +subshell exits and the trap fires immediately, removing the file before +control returns to your interactive shell — rather than waiting until you +close the terminal. Update the same database by editing `databases/example.json` and re-applying it against the `update-database` command. No password is @@ -259,6 +260,16 @@ needed, since it's omitted from the request entirely: restish pgedge update-database example < databases/example.json ``` +!!! note + + Restish retries network errors and transient server errors (`429`, + `500`, `502`, `503`, `504`) automatically, but not `create-database`, + `update-database`, or `delete-database` themselves — POST/PUT/PATCH/DELETE + requests are only retried if you explicitly pass `--rsh-retry-unsafe`, + which prints a warning when used. Leave that flag off for database + operations: retrying a request that already partially succeeded on the + server can double-process it. + To apply the same file to a specific environment instead of your default cluster, add the profile you set up in [Managing Multiple Environments](#managing-multiple-environments): @@ -287,12 +298,13 @@ In practice, that means the default workflow is the one shown above: keep `databases/example.json` secret-free from the start, and pass real secret values only from a separate, uncommitted file for the one `create-database` call that needs them — a file created with a restrictive `umask`, an -unpredictable `mktemp` path, and an `EXIT` trap so it's removed even if -the command fails, rather than editing secrets out of the committed file -after the fact. From then on, `update-database` runs against the -secret-free file as-is. If you need to rotate a password, apply it the -same way: the same `umask`/`mktemp`/`trap` pattern, with the new value, -passed once. +unpredictable `mktemp` path, and an `EXIT` trap inside a subshell so it's +removed as soon as `create-database` returns (even if it fails), rather +than waiting until you close the terminal or editing secrets out of the +committed file after the fact. From then on, `update-database` runs against +the secret-free file as-is. If you need to rotate a password, apply it the +same way: the same `umask`/`mktemp`/`trap`-in-subshell pattern, with the +new value, passed once. This keeps `databases/example.json` safe to read, diff, and share at any point — it's never the file that held the credential, so there's no window From 170db7d9aa96c1024a653df8bf16bba664baafc5 Mon Sep 17 00:00:00 2001 From: Gabrielle Poncey Date: Tue, 1 Sep 2026 06:42:47 -0700 Subject: [PATCH 5/7] docs: prompt for password instead of hardcoding it Replace the hardcoded "changeme" password in the create-database heredoc with an interactive `read -rsp` prompt piped through `jq`, so no secret appears in the example text. Minor phrasing change as well. --- docs/api/restish.md | 62 ++++++++++++++++----------------------------- 1 file changed, 22 insertions(+), 40 deletions(-) diff --git a/docs/api/restish.md b/docs/api/restish.md index f5ce071c..64be3a2b 100644 --- a/docs/api/restish.md +++ b/docs/api/restish.md @@ -17,7 +17,7 @@ be running commands against it in three steps: ### 1. Installation -First, install Restish [via Restish's official website](https://rest.sh/docs/getting-started/install/). Restish supports many installation methods, including Homebrew (macOS), github releases, and OCI images; select the option that most aligns with your organizations preferences and practices. +First, install Restish [via Restish's official website](https://rest.sh/docs/getting-started/install/). Restish supports many installation methods, including Homebrew (macOS), GitHub Releases, and OCI images; select the option that most aligns with your organization's preferences and practices. ### 2. Connection !!! warning @@ -219,38 +219,21 @@ of adding it to `databases/example.json`: umask 077 tmpfile=$(mktemp) trap 'rm -f "$tmpfile"' EXIT - cat > "$tmpfile" <<'EOF' -{ - "id": "example", - "spec": { - "database_name": "example", - "database_users": [ - { - "username": "admin", - "password": "changeme", - "db_owner": true, - "attributes": ["SUPERUSER", "LOGIN"] - } - ], - "port": 5432, - "nodes": [ - { "name": "n1", "host_ids": ["host-1"] }, - { "name": "n2", "host_ids": ["host-2"] }, - { "name": "n3", "host_ids": ["host-3"] } - ] - } -} -EOF + read -rsp "Password: " DB_PASS; echo + jq --arg pw "$DB_PASS" '.spec.database_users[0].password = $pw' \ + databases/example.json > "$tmpfile" restish pgedge create-database < "$tmpfile" ) ``` -`umask 077` keeps the file unreadable by anyone else on the machine while -it exists, `mktemp` avoids a predictable filename, and the subshell `(...)` -limits the `trap`'s scope: when `create-database` returns (or fails), the -subshell exits and the trap fires immediately, removing the file before -control returns to your interactive shell — rather than waiting until you -close the terminal. +`read -rsp` prompts for the password without echo, so it never appears in +your terminal output or shell history. `jq` receives the value via +`--arg` and injects it into `databases/example.json` at runtime, so the +password never appears in the command text itself. `umask 077` keeps the +tmpfile unreadable by anyone else on the machine while it exists, and the +subshell `(...)` limits the `trap`'s scope: when `create-database` returns +(or fails), the subshell exits and the trap fires immediately, removing the +file before control returns to your interactive shell. Update the same database by editing `databases/example.json` and re-applying it against the `update-database` command. No password is @@ -262,8 +245,8 @@ restish pgedge update-database example < databases/example.json !!! note - Restish retries network errors and transient server errors (`429`, - `500`, `502`, `503`, `504`) automatically, but not `create-database`, + Restish retries network errors and transient server errors (`408`, + `429`, `500`, `502`, `503`, `504`) automatically, but not `create-database`, `update-database`, or `delete-database` themselves — POST/PUT/PATCH/DELETE requests are only retried if you explicitly pass `--rsh-retry-unsafe`, which prints a warning when used. Leave that flag off for database @@ -296,15 +279,14 @@ already stored unless you explicitly send a new one. See In practice, that means the default workflow is the one shown above: keep `databases/example.json` secret-free from the start, and pass real secret -values only from a separate, uncommitted file for the one `create-database` -call that needs them — a file created with a restrictive `umask`, an -unpredictable `mktemp` path, and an `EXIT` trap inside a subshell so it's -removed as soon as `create-database` returns (even if it fails), rather -than waiting until you close the terminal or editing secrets out of the -committed file after the fact. From then on, `update-database` runs against -the secret-free file as-is. If you need to rotate a password, apply it the -same way: the same `umask`/`mktemp`/`trap`-in-subshell pattern, with the -new value, passed once. +values only from a separate, uncommitted tmpfile for the one +`create-database` call that needs them — prompted interactively with echo +disabled (so the value never enters shell history), injected via `jq` at +runtime, and written to a file created with a restrictive `umask` inside a +subshell so it's removed as soon as `create-database` returns (even if it +fails). From then on, `update-database` runs against the secret-free file +as-is. If you need to rotate a password, apply it the same way: `read -rsp` +for the new value, `jq` to inject it, passed once. This keeps `databases/example.json` safe to read, diff, and share at any point — it's never the file that held the credential, so there's no window From 8ea5d5d7d4c470b0866b9af47f556cf63d508e66 Mon Sep 17 00:00:00 2001 From: Gabrielle Poncey Date: Mon, 7 Sep 2026 20:22:31 -0700 Subject: [PATCH 6/7] docs: refactor secrets and setup Replace curl commands with restish, separates a dedicated mtls section, links to restish docs for more info on project config. Adds a tip for pulling specs. --- docs/api/restish.md | 127 ++++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 74 deletions(-) diff --git a/docs/api/restish.md b/docs/api/restish.md index 64be3a2b..4e64c0ce 100644 --- a/docs/api/restish.md +++ b/docs/api/restish.md @@ -20,8 +20,8 @@ be running commands against it in three steps: First, install Restish [via Restish's official website](https://rest.sh/docs/getting-started/install/). Restish supports many installation methods, including Homebrew (macOS), GitHub Releases, and OCI images; select the option that most aligns with your organization's preferences and practices. ### 2. Connection -!!! warning +Note: Only connect this way to clusters and databases you're okay with experimenting on. See [Managing Multiple Environments](#managing-multiple-environments) before connecting Restish to anything production. @@ -49,12 +49,12 @@ below), `restish pgedge create-database < your-file.json` creates one. Restish doesn't enforce any naming convention for the APIs you connect to. We recommend using Restish's **profiles** feature: one API registration, `pgedge`, holds a profile per environment, and each profile can override the base URL (and, if you need it later, auth or other per-environment request details). -**Use descriptive cluster names.** Every cluster has its own durable `id`, +**Use descriptive cluster names.** Every cluster has an **immutable** `id`, set at initialization and returned by `get-cluster`. `init-cluster` takes an optional `cluster_id` query parameter. Setting it to something descriptive will allow you to keep track of multiple different clusters. ```sh -curl "http://host1.internal:3000/v1/cluster/init?cluster_id=production" +restish pgedge init-cluster --cluster-id production ``` Then select that cluster with a matching profile: @@ -82,77 +82,18 @@ restish pgedge list-databases # default profile: local/informal cluster across hosts while debugging), give it its own profile the same way: `-p production-host-1`. -### Persisting Connections in a Project Config - -Register `pgedge` once, with a profile per environment, in a `.restish.json` -file at the root of your infrastructure repo, instead of re-running -`restish api connect` by hand every time you check it out. Restish -discovers this file by walking up from your current directory: - -```jsonc -{ - "apis": { - "pgedge": { - "base_url": "http://host1.internal:3000", - "profiles": { - "default": {}, - "staging": { - "base_url": "http://host1.staging.internal:3000" - }, - "production": { - "base_url": "https://host1.prod.internal:3000" - } - } - } - } -} -``` - -The top-level `base_url` is what the `default` profile falls back to — the -same safe-fallback rule described above, expressed as config instead of -prose. `staging` and `production` each override it, and only take effect -when you pass `-p`. - -Commit this file, then run the following once per checkout: +Add a profile per environment to the same `pgedge` registration with +`restish api set`: ```sh -restish config trust +restish api set pgedge 'profiles.staging.base_url: http://host1.staging.internal:3000' +restish api set pgedge 'profiles.production.base_url: https://host1.prod.internal:3000' ``` -Restish records that trust decision outside the repo, keyed to the file's -contents, so your connections and profiles are ready every time you check -out the repo. If working with a team, everyone ends up with the same -setup without manually running `restish api connect` or keeping a personal -copy in sync. - -!!! note - - A project config only honors the `apis` and `theme` keys — nothing - else. Keep it secret-free: the Control Plane's dev and local endpoints - don't need auth, so these profiles can stay limited to `base_url` as - shown above. If a cluster you connect to does require auth, reference - the value as `env:NAME` rather than committing it literally. - -If a cluster has [mTLS enabled](../installation/mtls.md), add the CA and -client certificate paths to that profile. These are file paths, not the -credentials themselves, so — unlike a token or password — they're fine to -commit as long as everyone using the file also has the actual `ca.crt`, -`client.crt`, and `client.key` in place locally: - -```jsonc -"production": { - "base_url": "https://host1.prod.internal:3000", - "ca_cert": "/opt/pgedge/control-plane/ca.crt", - "client_cert": "/opt/pgedge/control-plane/client.crt", - "client_key": "/opt/pgedge/control-plane/client.key" -} -``` - -Not every connection belongs in that shared file, though. Anything that's - per-machine (eg. a Lima VM IP that's different for every developer, a personal sandbox) should stay out of source control. Restish also -won't let you add a profile to an API that came from trusted project -config, so register a personal one under its own name instead, which -writes to your own local Restish config: +Not every connection belongs on your everyday `pgedge` registration, +though. Anything that's per-machine (eg. a Lima VM IP that's different for +every developer, a personal sandbox) is better off registered under its +own name so it doesn't collide with your regular setup: ```sh restish api connect pgedge-sandbox http://192.168.64.3:3000 @@ -168,6 +109,9 @@ restish api connect pgedge-sandbox https://192.168.64.3:3000 \ --spec https://192.168.64.3:3000/v1/openapi.json ``` +If the cluster also requires a client certificate for mTLS, see +[Connecting Over mTLS](#connecting-over-mtls). + Either way you connect something, the same commands work afterward: ```sh @@ -178,6 +122,33 @@ restish pgedge-sandbox --help # every generated command restish pgedge-sandbox list-databases --help # options for one command ``` +Restish also supports registering connections in a `.restish.json` project +config file so a whole team shares the same setup automatically — most +See Restish's own docs on +[project configuration](https://rest.sh/docs/reference/config/). + +### Connecting Over mTLS + +If a cluster has [mTLS enabled](../installation/mtls.md), pass the CA +certificate plus a client certificate and key when you connect: + +```sh +restish api connect pgedge-sandbox https://192.168.64.3:3000 \ + --rsh-ca-cert ./ca.crt \ + --rsh-client-cert ./client.crt \ + --rsh-client-key ./client.key +``` + +For a shared registration like `pgedge`, set the same paths per profile +with `restish api set`: + +```sh +restish api set pgedge \ + 'profiles.production.ca_cert: /opt/pgedge/control-plane/ca.crt' \ + 'profiles.production.client_cert: /opt/pgedge/control-plane/client.crt' \ + 'profiles.production.client_key: /opt/pgedge/control-plane/client.key' +``` + ## Managing Database Configuration as Files Keep one file per database, and use it as the source of truth for that @@ -268,10 +239,10 @@ workflow you'd use for any other infrastructure-as-code. ### Handling Secrets -The one field in these files that doesn't belong in source control is -`database_users[].password` (and, if you're configuring backups, -credentials like `s3_key_secret`). The Control Plane's update endpoint is -built to make this easy: **secret fields are only required the first time +Secret fields, such as `database_users[].password` or `s3_key_secret`, +should be excluded from any files committed to source control. The +Control Plane's update endpoint is built to make this easy: **secret +fields are only required the first time you create a database. On every `update-database` call after that, you can leave them out entirely** — the Control Plane keeps whatever value is already stored unless you explicitly send a new one. See @@ -291,3 +262,11 @@ for the new value, `jq` to inject it, passed once. This keeps `databases/example.json` safe to read, diff, and share at any point — it's never the file that held the credential, so there's no window where committing it (or `git add -A`, or a stray backup) could leak one. + +!!! tip + + The Control Plane excludes every secret field from its responses, you can skip manual redaction entirely: create the database from a one-off request that includes all of its secrets, then pull the sanitized spec back into your file: + + ```sh + restish pgedge get-database example | jq '{ id, spec }' > databases/example.json + ``` From 7ec2e261a4ef36e983d71c6808d96b348881f614 Mon Sep 17 00:00:00 2001 From: Gabrielle Poncey Date: Wed, 9 Sep 2026 12:51:44 -0700 Subject: [PATCH 7/7] docs: revisit Restish CLI examples Use a rawfile in example create-database, update certain example snippets to be safe to copy by validating ouput. --- docs/api/restish.md | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/docs/api/restish.md b/docs/api/restish.md index 4e64c0ce..1f4f2e7d 100644 --- a/docs/api/restish.md +++ b/docs/api/restish.md @@ -86,7 +86,7 @@ Add a profile per environment to the same `pgedge` registration with `restish api set`: ```sh -restish api set pgedge 'profiles.staging.base_url: http://host1.staging.internal:3000' +restish api set pgedge 'profiles.staging.base_url: https://host1.staging.internal:3000' restish api set pgedge 'profiles.production.base_url: https://host1.prod.internal:3000' ``` @@ -123,7 +123,9 @@ restish pgedge-sandbox list-databases --help # options for one command ``` Restish also supports registering connections in a `.restish.json` project -config file so a whole team shares the same setup automatically — most +config file so a whole team shares the same setup automatically. Restish +won't use a discovered project config until you review it and run +`restish config trust`; re-run that command after any change to the file. See Restish's own docs on [project configuration](https://rest.sh/docs/reference/config/). @@ -184,23 +186,25 @@ right away. Creating a database still needs a real password the first time, though, so pass that from a separate file you don't commit instead of adding it to `databases/example.json`: -```sh +```bash ( set -e umask 077 tmpfile=$(mktemp) trap 'rm -f "$tmpfile"' EXIT read -rsp "Password: " DB_PASS; echo - jq --arg pw "$DB_PASS" '.spec.database_users[0].password = $pw' \ + jq --rawfile pw <(printf '%s' "$DB_PASS") \ + '.spec.database_users[0].password = $pw' \ databases/example.json > "$tmpfile" restish pgedge create-database < "$tmpfile" ) ``` `read -rsp` prompts for the password without echo, so it never appears in -your terminal output or shell history. `jq` receives the value via -`--arg` and injects it into `databases/example.json` at runtime, so the -password never appears in the command text itself. `umask 077` keeps the +your terminal output or shell history. `jq` reads the value from a file +descriptor (`--rawfile pw <(printf '%s' "$DB_PASS")`) rather than a +command-line argument, so the password never appears in the command text +or in the process list. `umask 077` keeps the tmpfile unreadable by anyone else on the machine while it exists, and the subshell `(...)` limits the `trap`'s scope: when `create-database` returns (or fails), the subshell exits and the trap fires immediately, removing the @@ -267,6 +271,19 @@ where committing it (or `git add -A`, or a stray backup) could leak one. The Control Plane excludes every secret field from its responses, you can skip manual redaction entirely: create the database from a one-off request that includes all of its secrets, then pull the sanitized spec back into your file: - ```sh - restish pgedge get-database example | jq '{ id, spec }' > databases/example.json + ```bash + ( + set -e + set -o pipefail + tmpfile=$(mktemp) + trap 'rm -f "$tmpfile"' EXIT + restish pgedge get-database example | jq '{ id, spec }' > "$tmpfile" + jq empty "$tmpfile" + mv "$tmpfile" databases/example.json + ) ``` + + The redirect writes to a tmpfile first; `databases/example.json` is + replaced only after `get-database` succeeds (`set -e` plus + `pipefail`) and `jq empty` confirms the output parses as JSON, and the + `mv` swaps it in atomically.