Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 49 additions & 13 deletions crates/clickhouse-cloud-api/tests/clickpipes/postgres_cdc_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
Comment thread
cursor[bot] marked this conversation as resolved.
cleanup.register_postgres(postgres_id.clone());
eprintln!(" provisioned postgres id <redacted>");

Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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<tokio_postgres::Client> {
let mut roots = RootCertStore::empty();
Expand All @@ -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
Expand Down
21 changes: 17 additions & 4 deletions crates/clickhouse-cloud-api/tests/integration_postgres_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,23 @@ async fn cloud_postgres_crud_lifecycle() -> TestResult<()> {
eprintln!("postgres_id: <redacted>");
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,
Expand Down Expand Up @@ -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(
Expand Down
6 changes: 3 additions & 3 deletions crates/clickhousectl/src/cloud/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand Down