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..bc1e933 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,27 @@ 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(); + 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()); + 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 +227,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 +247,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 +276,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 +484,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 +511,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(()) }