From 8fdf24e2e4378480f117e12011c8ff836d298cc4 Mon Sep 17 00:00:00 2001 From: sdairs Date: Wed, 22 Jul 2026 17:38:35 +0100 Subject: [PATCH 1/2] Stop depending on Postgres get echoing credentials The Managed Postgres API stops returning the superuser password and connectionString outside the create and password-reset responses on July 31, 2026. Source credentials from the create response instead: - postgres_cdc_test: capture username/password and parse port/database from the create response's connection string, connect to Postgres from discrete parts (get's hostname + saved credentials), and drop the get-response connection-string assertion. - integration_postgres_test: assert credentials on the create response and drop the connection-string assertion on the polled get. - CLI `postgres get`: stop printing the echoed connection string. Both test changes are safe against the old and new API behavior. Model changes to PostgresService land later with the standard drift remediation once the upstream spec is published. Part of #306 Co-Authored-By: Claude Fable 5 --- .../tests/clickpipes/postgres_cdc_test.rs | 58 ++++++++++++++----- .../tests/integration_postgres_test.rs | 21 +++++-- crates/clickhousectl/src/cloud/postgres.rs | 6 +- 3 files changed, 65 insertions(+), 20 deletions(-) diff --git a/crates/clickhouse-cloud-api/tests/clickpipes/postgres_cdc_test.rs b/crates/clickhouse-cloud-api/tests/clickpipes/postgres_cdc_test.rs index 614c195..3be6e2b 100644 --- a/crates/clickhouse-cloud-api/tests/clickpipes/postgres_cdc_test.rs +++ b/crates/clickhouse-cloud-api/tests/clickpipes/postgres_cdc_test.rs @@ -152,6 +152,23 @@ async fn cloud_clickpipe_postgres_cdc() -> TestResult<()> { .result .ok_or("postgres create returned no result")?; let postgres_id = pg_created.id.to_string(); + // The create response is the only API surface guaranteed to return + // credentials: from July 31, 2026 the get endpoint stops echoing + // `password` and `connectionString`, so capture everything + // credential-derived here rather than from the polled get below. + let pg_username = pg_created.username.clone(); + let pg_password = pg_created.password.clone(); + let pg_port = parse_pg_port(&pg_created.connection_string).unwrap_or(5432); + let pg_database = parse_pg_database(&pg_created.connection_string) + .unwrap_or_else(|| "postgres".to_string()); + assert!( + !pg_username.is_empty(), + "postgres create returned empty username" + ); + assert!( + !pg_password.is_empty(), + "postgres create returned empty password" + ); cleanup.register_postgres(postgres_id.clone()); eprintln!(" provisioned postgres id "); @@ -206,10 +223,6 @@ async fn cloud_clickpipe_postgres_cdc() -> TestResult<()> { ); let (pg_ready, ch_ready) = tokio::try_join!(pg_ready_fut, ch_ready_fut)?; - assert!( - !pg_ready.connection_string.is_empty(), - "empty pg connection string" - ); assert!(!pg_ready.hostname.is_empty(), "empty pg hostname"); let ch_endpoint = ch_ready .endpoints @@ -230,14 +243,21 @@ async fn cloud_clickpipe_postgres_cdc() -> TestResult<()> { .postgres_service_certs_get(&ctx.org_id, &postgres_id) .await .ok(); - let pg_client = connect_postgres(&pg_ready.connection_string, pg_ca_pem.as_deref()).await?; + let pg_client = connect_postgres( + &pg_ready.hostname, + pg_port, + &pg_database, + &pg_username, + &pg_password, + pg_ca_pem.as_deref(), + ) + .await?; configure_pg_for_cdc(&pg_client).await?; // ── Create ClickPipe ──────────────────────────────────────── log_phase("Create ClickPipe"); - let pg_port = parse_pg_port(&pg_ready.connection_string).unwrap_or(5432); let pipe_request = ClickPipePostRequest { name: format!("cdc-{}", ctx.run_id), destination: ClickPipeMutateDestination { @@ -252,11 +272,10 @@ async fn cloud_clickpipe_postgres_cdc() -> TestResult<()> { authentication: ClickPipeMutatePostgresSourceAuthentication::Basic, ca_certificate: pg_ca_pem.clone(), credentials: PLAIN { - username: pg_ready.username.clone(), - password: pg_ready.password.clone(), + username: pg_username.clone(), + password: pg_password.clone(), }, - database: parse_pg_database(&pg_ready.connection_string) - .unwrap_or_else(|| "postgres".to_string()), + database: pg_database.clone(), host: pg_ready.hostname.clone(), port: pg_port as i64, settings: ClickPipePostgresPipeSettings { @@ -461,8 +480,15 @@ fn filters_match_tags(filters: &[String], tags: &[ResourceTagsV1]) -> bool { }) } +// Takes discrete connection parts rather than a connection string: the get +// endpoint stops echoing `connectionString` on July 31, 2026, so callers +// assemble host/port/database/credentials from the create response + get. async fn connect_postgres( - connection_string: &str, + host: &str, + port: u16, + database: &str, + username: &str, + password: &str, extra_ca_pem: Option<&str>, ) -> TestResult { let mut roots = RootCertStore::empty(); @@ -481,8 +507,14 @@ async fn connect_postgres( .with_no_client_auth(); let tls = MakeRustlsConnect::new(client_config); - let mut config = tokio_postgres::Config::from_str(connection_string)?; - // Connection strings returned by Cloud may not specify SSL mode; force require. + let mut config = tokio_postgres::Config::new(); + config + .host(host) + .port(port) + .dbname(database) + .user(username) + .password(password); + // Cloud Postgres requires TLS; force require. config.ssl_mode(tokio_postgres::config::SslMode::Require); // tokio-postgres-rustls doesn't expose the TLS exporter material that // SCRAM-SHA-256-PLUS needs, so disable channel binding even if the Cloud diff --git a/crates/clickhouse-cloud-api/tests/integration_postgres_test.rs b/crates/clickhouse-cloud-api/tests/integration_postgres_test.rs index 699f991..d7ef499 100644 --- a/crates/clickhouse-cloud-api/tests/integration_postgres_test.rs +++ b/crates/clickhouse-cloud-api/tests/integration_postgres_test.rs @@ -81,6 +81,23 @@ async fn cloud_postgres_crud_lifecycle() -> TestResult<()> { eprintln!("postgres_id: "); cleanup.register_postgres(postgres_id.clone()); + // The create response is the only API surface guaranteed to return + // credentials: from July 31, 2026 the get endpoint stops echoing + // `password` and `connectionString`, so assert them here rather + // than on the polled get below. + assert!( + !created.username.is_empty(), + "postgres create returned empty username" + ); + assert!( + !created.password.is_empty(), + "postgres create returned empty password" + ); + assert!( + !created.connection_string.is_empty(), + "postgres create returned empty connection string" + ); + let ready = failures .run( &ctx, @@ -129,10 +146,6 @@ async fn cloud_postgres_crud_lifecycle() -> TestResult<()> { !ready.hostname.is_empty(), "running postgres service returned empty hostname" ); - assert!( - !ready.connection_string.is_empty(), - "running postgres service returned empty connection string" - ); let listed = failures .run( diff --git a/crates/clickhousectl/src/cloud/postgres.rs b/crates/clickhousectl/src/cloud/postgres.rs index c87d0b0..6422a72 100644 --- a/crates/clickhousectl/src/cloud/postgres.rs +++ b/crates/clickhousectl/src/cloud/postgres.rs @@ -537,10 +537,10 @@ pub async fn postgres_get( if json { println!("{}", serde_json::to_string_pretty(&svc)?); } else { + // No connection string here: the get endpoint stops returning + // credentials on July 31, 2026 — they are only available from + // `postgres create` and `postgres reset-password`. render_postgres_service(&svc); - if !svc.connection_string.is_empty() { - println!(" Connection string: {}", svc.connection_string); - } } Ok(()) } From 414367f3e62e0583323b3a11f8efa3759ab9a8d1 Mon Sep 17 00:00:00 2001 From: sdairs Date: Wed, 22 Jul 2026 19:10:33 +0100 Subject: [PATCH 2/2] Assert create response connection string is non-empty before parsing Addresses Bugbot review: pg_port/pg_database silently fell back to defaults if the create response's connection string was empty. Co-Authored-By: Claude Fable 5 --- .../tests/clickpipes/postgres_cdc_test.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/clickhouse-cloud-api/tests/clickpipes/postgres_cdc_test.rs b/crates/clickhouse-cloud-api/tests/clickpipes/postgres_cdc_test.rs index 3be6e2b..bc1e933 100644 --- a/crates/clickhouse-cloud-api/tests/clickpipes/postgres_cdc_test.rs +++ b/crates/clickhouse-cloud-api/tests/clickpipes/postgres_cdc_test.rs @@ -158,6 +158,10 @@ async fn cloud_clickpipe_postgres_cdc() -> TestResult<()> { // credential-derived here rather than from the polled get below. let pg_username = pg_created.username.clone(); let pg_password = pg_created.password.clone(); + assert!( + !pg_created.connection_string.is_empty(), + "postgres create returned empty connection string" + ); let pg_port = parse_pg_port(&pg_created.connection_string).unwrap_or(5432); let pg_database = parse_pg_database(&pg_created.connection_string) .unwrap_or_else(|| "postgres".to_string());