From 5fe2fff57cd8ac786c1668fd7a1429520efef008 Mon Sep 17 00:00:00 2001 From: sdairs Date: Tue, 15 Sep 2026 17:15:07 +0100 Subject: [PATCH] feat(postgres): show provider in service list --- README.md | 2 + crates/clickhousectl/src/cloud/postgres.rs | 3 + .../tests/cli_request_shape_test.rs | 63 +++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/README.md b/README.md index 50524ab9..7b503bf5 100644 --- a/README.md +++ b/README.md @@ -1334,6 +1334,8 @@ Use `clickhousectl cloud postgres create --help` for the complete option list. S `postgres list --filter KEY=VALUE` is applied client-side to the listing and is repeatable; every filter must match. Supported keys are `state`, `region`, `name`, `provider` and `isPrimary` (the `Primary` column; `true`/`false`, or the `yes`/`no` the column shows). Keys are case-insensitive, `state` and `provider` match the wire value case-insensitively, and `region`/`name` match exactly. An unknown key, a missing `=` or an empty value is a usage error (exit 2) listing the valid keys — it never returns an unfiltered list. A field the API omitted matches no filter value, so filtering on it excludes that service. This is unrelated to `cloud service list --filter`, which sends server-side resource-tag filters (`tag:env=production`) to the API. +The human-readable list includes each service's cloud provider; an omitted provider is shown as `-`. JSON output keeps the API response shape. + `postgres promote` and `postgres switchover` change which service is primary. Both are issued as-is, and the API acknowledges them before (or without) applying them, so exit 0 on its own means accepted, not applied: - `--wait` (optionally `--wait-timeout SECONDS`, default 300) is how you confirm the roles actually changed. It polls the target every 5s until it reports the expected `isPrimary` — `true` for `promote`, the opposite of the value read just before the command for `switchover` — and exits 1 with the last observed role if it never does. stdout then carries the polled state rather than the state-change response, which for `promote` omits `isPrimary` entirely. Without `--wait` neither command reads the service. A `switchover --wait` whose pre-command read omits `isPrimary` is refused before the command is issued, because there is no prior role to compare a swap against. diff --git a/crates/clickhousectl/src/cloud/postgres.rs b/crates/clickhousectl/src/cloud/postgres.rs index 46605294..4700d458 100644 --- a/crates/clickhousectl/src/cloud/postgres.rs +++ b/crates/clickhousectl/src/cloud/postgres.rs @@ -1383,6 +1383,8 @@ pub async fn postgres_list( id: String, #[tabled(rename = "State")] state: String, + #[tabled(rename = "Provider")] + provider: String, #[tabled(rename = "Region")] region: String, #[tabled(rename = "Size")] @@ -1401,6 +1403,7 @@ pub async fn postgres_list( name: or_absent(i.name.as_deref()), id: or_absent(i.id.as_ref()), state: state_label(i.state.as_ref()), + provider: enum_label(i.provider.as_ref()), region: or_absent(i.region.as_deref()), size: enum_label(i.size.as_ref()), pg: enum_label(i.postgres_version.as_ref()), diff --git a/crates/clickhousectl/tests/cli_request_shape_test.rs b/crates/clickhousectl/tests/cli_request_shape_test.rs index eac1857f..f2446827 100644 --- a/crates/clickhousectl/tests/cli_request_shape_test.rs +++ b/crates/clickhousectl/tests/cli_request_shape_test.rs @@ -4335,6 +4335,69 @@ async fn postgres_list_applies_supported_filters_client_side() { assert_eq!(names("name=nope"), Vec::::new()); } +#[tokio::test] +async fn postgres_list_shows_provider_in_human_output_and_preserves_json() { + let mock = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/v1/organizations/org-1/postgres")) + .respond_with(postgres_list_response()) + .expect(2) + .mount(&mock) + .await; + + let human = + invoke_cli_with_cloud_credentials_human(&mock, &["postgres", "list", "--org-id", "org-1"]); + assert_success(&human); + let human = String::from_utf8(human.stdout).expect("human output should be UTF-8"); + let row_cells = |name: &str| -> Vec<&str> { + human + .lines() + .find(|line| line.contains(name)) + .unwrap_or_else(|| panic!("missing row for {name}:\n{human}")) + .split('|') + .map(str::trim) + .filter(|cell| !cell.is_empty()) + .collect() + }; + assert_eq!( + row_cells("Name"), + [ + "Name", "ID", "State", "Provider", "Region", "Size", "PG", "HA", "Primary" + ] + ); + assert_eq!(row_cells("primary-pg")[3], "aws"); + assert_eq!(row_cells("unknown-pg")[3], "-"); + + let json = invoke_cli_with_cloud_credentials(&mock, &["postgres", "list", "--org-id", "org-1"]); + assert_success(&json); + assert_eq!( + serde_json::from_slice::(&json.stdout).expect("JSON output should be valid"), + serde_json::json!([ + { + "id": "11111111-2222-3333-4444-555555555555", + "name": "primary-pg", + "state": "running", + "region": "us-east-1", + "provider": "aws", + "isPrimary": true, + }, + { + "id": "66666666-7777-8888-9999-000000000000", + "name": "replica-pg", + "state": "restoring_backup", + "region": "us-east-1", + "provider": "aws", + "isPrimary": false, + }, + { + "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "name": "unknown-pg", + "region": "us-east-1", + }, + ]) + ); +} + // ── Postgres promote / switchover role changes (issue #604) ─────────────── const ROLE_TEST_POSTGRES_ID: &str = "11111111-2222-3333-4444-555555555555";