From 24c1c6832b241178ebee023d6439f5d08921845f Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Wed, 23 Apr 2025 18:11:32 +0200 Subject: [PATCH 1/6] feat: JWT auth --- src/headers.rs | 24 +++++- src/insert.rs | 11 +-- src/lib.rs | 202 +++++++++++++++++++++++++++++++++++++++++++++++-- src/query.rs | 10 +-- src/watch.rs | 2 +- 5 files changed, 223 insertions(+), 26 deletions(-) diff --git a/src/headers.rs b/src/headers.rs index d7588546..0a818e56 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -1,5 +1,5 @@ -use crate::ProductInfo; -use hyper::header::USER_AGENT; +use crate::{Authentication, ProductInfo}; +use hyper::header::{AUTHORIZATION, USER_AGENT}; use hyper::http::request::Builder; use std::collections::HashMap; use std::env::consts::OS; @@ -22,6 +22,7 @@ fn get_user_agent(products_info: &[ProductInfo]) -> String { } } +#[inline] pub(crate) fn with_request_headers( mut builder: Builder, headers: &HashMap, @@ -33,3 +34,22 @@ pub(crate) fn with_request_headers( builder = builder.header(USER_AGENT.to_string(), get_user_agent(products_info)); builder } + +#[inline] +pub(crate) fn with_authentication(mut builder: Builder, auth: &Authentication) -> Builder { + match auth { + Authentication::Jwt { access_token } => { + let bearer = format!("Bearer {access_token}"); + builder = builder.header(AUTHORIZATION, bearer); + } + Authentication::Credentials { user, password } => { + if let Some(user) = &user { + builder = builder.header("X-ClickHouse-User", user); + } + if let Some(password) = &password { + builder = builder.header("X-ClickHouse-Key", password); + } + } + } + builder +} diff --git a/src/insert.rs b/src/insert.rs index 42208a3e..2af4a56b 100644 --- a/src/insert.rs +++ b/src/insert.rs @@ -10,7 +10,7 @@ use tokio::{ }; use url::Url; -use crate::headers::with_request_headers; +use crate::headers::{with_authentication, with_request_headers}; use crate::{ error::{Error, Result}, request_body::{ChunkSender, RequestBody}, @@ -353,14 +353,7 @@ impl Insert { let mut builder = Request::post(url.as_str()); builder = with_request_headers(builder, &client.headers, &client.products_info); - - if let Some(user) = &client.user { - builder = builder.header("X-ClickHouse-User", user); - } - - if let Some(password) = &client.password { - builder = builder.header("X-ClickHouse-Key", password); - } + builder = with_authentication(builder, &client.authentication); let (sender, body) = RequestBody::chunked(); diff --git a/src/lib.rs b/src/lib.rs index 7d02cdca..74f3eea8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,8 +42,7 @@ pub struct Client { url: String, database: Option, - user: Option, - password: Option, + authentication: Authentication, compression: Compression, options: HashMap, headers: HashMap, @@ -62,6 +61,26 @@ impl Display for ProductInfo { } } +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum Authentication { + Credentials { + user: Option, + password: Option, + }, + Jwt { + access_token: String, + }, +} + +impl Default for Authentication { + fn default() -> Self { + Self::Credentials { + user: None, + password: None, + } + } +} + impl Default for Client { fn default() -> Self { Self::with_http_client(http_client::default()) @@ -77,8 +96,7 @@ impl Client { http: Arc::new(client), url: String::new(), database: None, - user: None, - password: None, + authentication: Authentication::default(), compression: Compression::default(), options: HashMap::new(), headers: HashMap::new(), @@ -118,19 +136,70 @@ impl Client { /// let client = Client::default().with_user("test"); /// ``` pub fn with_user(mut self, user: impl Into) -> Self { - self.user = Some(user.into()); + match self.authentication { + Authentication::Jwt { .. } => { + panic!("`user` cannot be set together with `access_token`"); + } + Authentication::Credentials { password, .. } => { + self.authentication = Authentication::Credentials { + user: Some(user.into()), + password, + }; + } + } self } /// Specifies a password. /// + /// # Panics + /// If called after [`Client::with_access_token`]. + /// /// # Examples /// ``` /// # use clickhouse::Client; /// let client = Client::default().with_password("secret"); /// ``` pub fn with_password(mut self, password: impl Into) -> Self { - self.password = Some(password.into()); + match self.authentication { + Authentication::Jwt { .. } => { + panic!("`password` cannot be set together with `access_token`"); + } + Authentication::Credentials { user, .. } => { + self.authentication = Authentication::Credentials { + user, + password: Some(password.into()), + }; + } + } + self + } + + /// A JWT access token to authenticate with ClickHouse. + /// JWT token authentication is supported in ClickHouse Cloud only. + /// Should not be called after [`Client::with_user`] or [`Client::with_password`]. + /// + /// # Panics + /// If called after [`Client::with_user`] or [`Client::with_password`]. + /// + /// # Examples + /// ``` + /// # use clickhouse::Client; + /// let client = Client::default().with_access_token("jwt"); + /// ``` + pub fn with_access_token(mut self, access_token: impl Into) -> Self { + match self.authentication { + Authentication::Credentials { user, password } + if user.is_some() || password.is_some() => + { + panic!("`access_token` cannot be set together with `user` or `password`"); + } + _ => { + self.authentication = Authentication::Jwt { + access_token: access_token.into(), + } + } + } self } @@ -266,3 +335,124 @@ pub mod _priv { crate::compression::lz4::compress(uncompressed) } } + +#[cfg(test)] +mod client_tests { + use crate::{Authentication, Client}; + + #[test] + fn it_can_use_credentials_auth() { + assert_eq!( + Client::default() + .with_user("bob") + .with_password("secret") + .authentication, + Authentication::Credentials { + user: Some("bob".into()), + password: Some("secret".into()), + } + ); + } + + #[test] + fn it_can_use_credentials_auth_user_only() { + assert_eq!( + Client::default().with_user("alice").authentication, + Authentication::Credentials { + user: Some("alice".into()), + password: None, + } + ); + } + + #[test] + fn it_can_use_credentials_auth_password_only() { + assert_eq!( + Client::default().with_password("secret").authentication, + Authentication::Credentials { + user: None, + password: Some("secret".into()), + } + ); + } + + #[test] + fn it_can_override_credentials_auth() { + assert_eq!( + Client::default() + .with_user("bob") + .with_password("secret") + .with_user("alice") + .with_password("something_else") + .authentication, + Authentication::Credentials { + user: Some("alice".into()), + password: Some("something_else".into()), + } + ); + } + + #[test] + fn it_can_use_jwt_auth() { + assert_eq!( + Client::default().with_access_token("my_jwt").authentication, + Authentication::Jwt { + access_token: "my_jwt".into(), + } + ); + } + + #[test] + fn it_can_override_jwt_auth() { + assert_eq!( + Client::default() + .with_access_token("my_jwt") + .with_access_token("my_jwt_2") + .authentication, + Authentication::Jwt { + access_token: "my_jwt_2".into(), + } + ); + } + + #[test] + #[should_panic(expected = "`access_token` cannot be set together with `user` or `password`")] + fn it_cannot_use_jwt_after_with_user() { + let _ = Client::default() + .with_user("bob") + .with_access_token("my_jwt"); + } + + #[test] + #[should_panic(expected = "`access_token` cannot be set together with `user` or `password`")] + fn it_cannot_use_jwt_after_with_password() { + let _ = Client::default() + .with_password("secret") + .with_access_token("my_jwt"); + } + + #[test] + #[should_panic(expected = "`access_token` cannot be set together with `user` or `password`")] + fn it_cannot_use_jwt_after_both_with_user_and_with_password() { + let _ = Client::default() + .with_user("alice") + .with_password("secret") + .with_access_token("my_jwt"); + } + + #[test] + #[should_panic(expected = "`user` cannot be set together with `access_token`")] + fn it_cannot_use_with_user_after_jwt() { + let _ = Client::default() + .with_access_token("my_jwt") + .with_user("alice"); + } + + #[test] + #[should_panic(expected = "`password` cannot be set together with `access_token`")] + fn it_cannot_use_with_password_after_jwt() { + let _ = Client::default() + .with_access_token("my_jwt") + .with_password("secret"); + } +} diff --git a/src/query.rs b/src/query.rs index 7a76dbd5..374eebb9 100644 --- a/src/query.rs +++ b/src/query.rs @@ -16,6 +16,7 @@ use crate::{ const MAX_QUERY_LEN_TO_USE_GET: usize = 8192; pub use crate::cursors::{BytesCursor, RowCursor}; +use crate::headers::with_authentication; #[must_use] #[derive(Clone)] @@ -178,6 +179,7 @@ impl Query { let mut builder = Request::builder().method(method).uri(url.as_str()); builder = with_request_headers(builder, &self.client.headers, &self.client.products_info); + builder = with_authentication(builder, &self.client.authentication); if content_length == 0 { builder = builder.header(CONTENT_LENGTH, "0"); @@ -185,14 +187,6 @@ impl Query { builder = builder.header(CONTENT_LENGTH, content_length.to_string()); } - if let Some(user) = &self.client.user { - builder = builder.header("X-ClickHouse-User", user); - } - - if let Some(password) = &self.client.password { - builder = builder.header("X-ClickHouse-Key", password); - } - let request = builder .body(body) .map_err(|err| Error::InvalidParams(Box::new(err)))?; diff --git a/src/watch.rs b/src/watch.rs index deaa4465..109b642e 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -102,7 +102,7 @@ impl Watch { } /// # Panics - /// Panics if `T` are rows without specified names. + /// If `T` are rows without specified names. /// Only structs are supported in this API. #[track_caller] pub fn fetch(self) -> Result> { From f807c590a99a3bee81097b2a5fac8bd84dcffb60 Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Wed, 23 Apr 2025 18:22:05 +0200 Subject: [PATCH 2/6] docs: update changelog, document panic condition in `Client::with_user` --- CHANGELOG.md | 2 ++ src/lib.rs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98a70755..929c9b83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - ReleaseDate +### Added +- client: added `Client::with_access_token` to support JWT authentication (ClickHouse Cloud feature). ## [0.13.2] - 2025-03-12 ### Added diff --git a/src/lib.rs b/src/lib.rs index 74f3eea8..fee88ba4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -130,6 +130,9 @@ impl Client { /// Specifies a user. /// + /// # Panics + /// If called after [`Client::with_access_token`]. + /// /// # Examples /// ``` /// # use clickhouse::Client; From b2e78ccb42f6078119322a64909c86889d5babf5 Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Wed, 23 Apr 2025 20:13:48 +0200 Subject: [PATCH 3/6] test: add minimal ClickHouse Cloud tests support --- .github/workflows/ci.yml | 107 ++++++++++++++++++++++----------------- CHANGELOG.md | 4 +- src/lib.rs | 2 +- tests/it/cloud_jwt.rs | 28 ++++++++++ tests/it/main.rs | 86 ++++++++++++++++++++++++++----- 5 files changed, 167 insertions(+), 60 deletions(-) create mode 100644 tests/it/cloud_jwt.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d67a237..502ff3cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,66 +17,66 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - run: rustup show active-toolchain -v - - run: cargo build --all-targets - - run: cargo build --all-targets --no-default-features - - run: cargo build --all-targets --all-features + - uses: actions/checkout@v4 + - run: rustup show active-toolchain -v + - run: cargo build --all-targets + - run: cargo build --all-targets --no-default-features + - run: cargo build --all-targets --all-features msrv: runs-on: ubuntu-latest env: RUSTFLAGS: "" # remove -Dwarnings steps: - - uses: actions/checkout@v4 - - run: rustup toolchain install ${{ env.MSRV }} --profile minimal - - run: rustup override set ${{ env.MSRV }} - - run: rustup show active-toolchain -v - - run: cargo update -p native-tls --precise 0.2.13 # 0.2.14 requires rustc 1.80 - - run: cargo update -p litemap --precise 0.7.4 # 0.7.5 requires rustc 1.81 - - run: cargo update -p zerofrom --precise 0.1.5 # 0.1.6 requires rustc 1.81 - - run: cargo build - - run: cargo build --no-default-features - - run: cargo build --features uuid,time,chrono - - run: cargo build --all-features + - uses: actions/checkout@v4 + - run: rustup toolchain install ${{ env.MSRV }} --profile minimal + - run: rustup override set ${{ env.MSRV }} + - run: rustup show active-toolchain -v + - run: cargo update -p native-tls --precise 0.2.13 # 0.2.14 requires rustc 1.80 + - run: cargo update -p litemap --precise 0.7.4 # 0.7.5 requires rustc 1.81 + - run: cargo update -p zerofrom --precise 0.1.5 # 0.1.6 requires rustc 1.81 + - run: cargo build + - run: cargo build --no-default-features + - run: cargo build --features uuid,time,chrono + - run: cargo build --all-features rustfmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - run: rustup show active-toolchain -v - - run: rustup component add rustfmt - - run: cargo fmt --version - - run: cargo fmt -- --check + - uses: actions/checkout@v4 + - run: rustup show active-toolchain -v + - run: rustup component add rustfmt + - run: cargo fmt --version + - run: cargo fmt -- --check clippy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - run: rustup show active-toolchain -v - - run: rustup component add clippy - - run: cargo clippy --version - - run: cargo clippy - - run: cargo clippy --all-targets --no-default-features - - run: cargo clippy --all-targets --all-features + - uses: actions/checkout@v4 + - run: rustup show active-toolchain -v + - run: rustup component add clippy + - run: cargo clippy --version + - run: cargo clippy + - run: cargo clippy --all-targets --no-default-features + - run: cargo clippy --all-targets --all-features - # TLS - - run: cargo clippy --features native-tls - - run: cargo clippy --features rustls-tls - - run: cargo clippy --features rustls-tls-ring,rustls-tls-webpki-roots - - run: cargo clippy --features rustls-tls-ring,rustls-tls-native-roots - - run: cargo clippy --features rustls-tls-aws-lc,rustls-tls-webpki-roots - - run: cargo clippy --features rustls-tls-aws-lc,rustls-tls-native-roots + # TLS + - run: cargo clippy --features native-tls + - run: cargo clippy --features rustls-tls + - run: cargo clippy --features rustls-tls-ring,rustls-tls-webpki-roots + - run: cargo clippy --features rustls-tls-ring,rustls-tls-native-roots + - run: cargo clippy --features rustls-tls-aws-lc,rustls-tls-webpki-roots + - run: cargo clippy --features rustls-tls-aws-lc,rustls-tls-native-roots test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - run: rustup show active-toolchain -v - - run: cargo test - - run: cargo test --no-default-features - - run: cargo test --features uuid,time - - run: cargo test --all-features + - uses: actions/checkout@v4 + - run: rustup show active-toolchain -v + - run: cargo test + - run: cargo test --no-default-features + - run: cargo test --features uuid,time + - run: cargo test --all-features services: clickhouse: @@ -84,14 +84,29 @@ jobs: ports: - 8123:8123 + cloud-test: + runs-on: ubuntu-latest + needs: [ test ] + steps: + - uses: actions/checkout@v4 + - run: rustup show active-toolchain -v + - name: Run tests + env: + CLICKHOUSE_TEST_ENVIRONMENT: cloud + CLICKHOUSE_CLOUD_HOST: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_HOST_SMT }} + CLICKHOUSE_CLOUD_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD_SMT }} + CLICKHOUSE_CLOUD_JWT_ACCESS_TOKEN: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_JWT_DESERT_VM_43 }} + run: | + cargo test cloud_ --features rustls-tls + docs: needs: build runs-on: ubuntu-latest env: RUSTDOCFLAGS: -Dwarnings --cfg docsrs steps: - - uses: actions/checkout@v4 - - run: rustup toolchain install nightly - - run: rustup override set nightly - - run: rustup show active-toolchain -v - - run: cargo doc --all-features + - uses: actions/checkout@v4 + - run: rustup toolchain install nightly + - run: rustup override set nightly + - run: rustup show active-toolchain -v + - run: cargo doc --all-features diff --git a/CHANGELOG.md b/CHANGELOG.md index 929c9b83..ad14c0c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - ReleaseDate ### Added -- client: added `Client::with_access_token` to support JWT authentication (ClickHouse Cloud feature). +- client: added `Client::with_access_token` to support JWT authentication ClickHouse Cloud feature ([#215]). + +[#215]: https://github.com/ClickHouse/clickhouse-rs/pull/215 ## [0.13.2] - 2025-03-12 ### Added diff --git a/src/lib.rs b/src/lib.rs index fee88ba4..72ba0000 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -213,7 +213,7 @@ impl Client { /// ``` /// # use clickhouse::{Client, Compression}; /// # #[cfg(feature = "lz4")] - /// let client = Client::default().with_compression(Compression::Lz4Hc(4)); + /// let client = Client::default().with_compression(Compression::Lz4); /// ``` pub fn with_compression(mut self, compression: Compression) -> Self { self.compression = compression; diff --git a/tests/it/cloud_jwt.rs b/tests/it/cloud_jwt.rs new file mode 100644 index 00000000..8c6297cf --- /dev/null +++ b/tests/it/cloud_jwt.rs @@ -0,0 +1,28 @@ +use crate::{get_cloud_url, require_env_var}; +use clickhouse::Client; + +#[tokio::test] +async fn test_jwt_auth() { + check_cloud_test_env!(); + let valid_token = require_env_var("CLICKHOUSE_CLOUD_JWT_TOKEN"); + let client = Client::default() + .with_url(get_cloud_url()) + .with_access_token(valid_token); + let result = client.query("SELECT 42").fetch_one::().await.unwrap(); + assert_eq!(result, 42); +} + +#[tokio::test] +async fn test_invalid_jwt_auth() { + check_cloud_test_env!(); + let client = Client::default() + .with_url(get_cloud_url()) + .with_access_token("invalid_token"); + let result = client.query("SELECT 42").fetch_one::().await; + let err_msg = format!("{}", result.expect_err("result should be an error")); + assert!( + err_msg.contains("JWT decoding error: invalid token supplied"), + "err_msg = {}", + err_msg + ); +} diff --git a/tests/it/main.rs b/tests/it/main.rs index 5e0385db..310eb2da 100644 --- a/tests/it/main.rs +++ b/tests/it/main.rs @@ -1,4 +1,26 @@ -use clickhouse::{sql, sql::Identifier, Client, Row}; +//! ## Integration tests +//! +//! - The `wait_end_of_query` setting that is used for all DDLs forces HTTP response buffering. +//! We will get the response only when the DDL is executed on every cluster node. +//! See also: https://clickhouse.com/docs/en/interfaces/http/#response-buffering +//! +//! - When tests are executed against ClickHouse Cloud, `ENGINE = MergeTree` +//! is automatically replaced with `ENGINE = SharedMergeTree` by the server. +//! In this case, no modifications to the DDLs are required, unlike on-premise clusters. +//! +//! - `CLICKHOUSE_TEST_ENVIRONMENT` env variable determines whether we are going to use +//! a local ClickHouse instance in Docker (`local`, default value) or ClickHouse Cloud (`cloud`). +//! NB: `cloud` will require one of the TLS features to be enabled. +//! +//! - ClickHouse server credentials are set via `CLICKHOUSE_CLOUD_HOST`, `CLICKHOUSE_CLOUD_USER`, +//! and `CLICKHOUSE_CLOUD_PASSWORD`. Specific Cloud-only tests might also require JWT access token, +//! which should be provided via `CLICKHOUSE_CLOUD_JWT_ACCESS_TOKEN`. +//! +//! - Created database names should match the following template: `chrs__{...}__{unix_millis}`. +//! This allows to simply clean up the databases from the Cloud instance based on its creation time. +//! See [`_priv::make_db_name`]. + +use clickhouse::{sql::Identifier, Client, Row}; use serde::{Deserialize, Serialize}; macro_rules! prepare_database { @@ -14,6 +36,38 @@ macro_rules! prepare_database { }; } +macro_rules! check_cloud_test_env { + () => { + match std::env::var("CLICKHOUSE_TEST_ENVIRONMENT") { + Ok(test_env) if test_env == "cloud" => (), + _ => { + eprintln!("Skipping test as it is Cloud only"); + return; + } + } + }; +} + +pub(crate) fn get_client() -> Client { + let client = Client::default(); + match std::env::var("CLICKHOUSE_TEST_ENVIRONMENT") { + Ok(test_env) if test_env == "cloud" => client + .with_url(get_cloud_url()) + .with_user("default") + .with_password(require_env_var("CLICKHOUSE_CLOUD_PASSWORD")), + _ => client.with_url("http://localhost:8123"), + } +} + +pub(crate) fn require_env_var(name: &str) -> String { + std::env::var(name).unwrap_or_else(|_| panic!("{name} environment variable is not set")) +} + +pub(crate) fn get_cloud_url() -> String { + let hostname = require_env_var("CLICKHOUSE_CLOUD_HOST"); + format!("https://{hostname}:8443") +} + #[derive(Debug, Row, Serialize, Deserialize, PartialEq)] struct SimpleRow { id: u64, @@ -32,6 +86,7 @@ impl SimpleRow { async fn create_simple_table(client: &Client, table_name: &str) { client .query("CREATE TABLE ?(id UInt64, data String) ENGINE = MergeTree ORDER BY id") + .with_option("wait_end_of_query", "1") .bind(Identifier(table_name)) .execute() .await @@ -55,6 +110,7 @@ async fn flush_query_log(client: &Client) { } mod chrono; +mod cloud_jwt; mod compression; mod cursor_error; mod cursor_stats; @@ -71,38 +127,44 @@ mod uuid; mod variant; mod watch; -const HOST: &str = "localhost:8123"; - mod _priv { use super::*; + use std::time::SystemTime; pub(crate) async fn prepare_database(fn_path: &str) -> Client { - let name = make_db_name(fn_path); - let client = Client::default().with_url(format!("http://{HOST}")); + let db_name = make_db_name(fn_path); + let client = get_client(); client .query("DROP DATABASE IF EXISTS ?") - .bind(sql::Identifier(&name)) + .with_option("wait_end_of_query", "1") + .bind(Identifier(&db_name)) .execute() .await - .expect("cannot drop db"); + .unwrap_or_else(|err| panic!("cannot drop db {db_name}, cause: {err}")); client .query("CREATE DATABASE ?") - .bind(sql::Identifier(&name)) + .with_option("wait_end_of_query", "1") + .bind(Identifier(&db_name)) .execute() .await - .expect("cannot create db"); + .unwrap_or_else(|err| panic!("cannot create db {db_name}, cause: {err}")); - client.with_database(name) + println!("Created database {db_name}"); + client.with_database(db_name) } - // `it::compression::lz4::{{closure}}::f` -> `chrs__compression__lz4` + // `it::compression::lz4::{{closure}}::f` -> `chrs__compression__lz4__{unix_millis}` fn make_db_name(fn_path: &str) -> String { assert!(fn_path.starts_with("it::")); let mut iter = fn_path.split("::").skip(1); let module = iter.next().unwrap(); let test = iter.next().unwrap(); - format!("chrs__{module}__{test}") + let now_unix_millis = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_millis(); + format!("chrs__{module}__{test}__{now_unix_millis}") } } From da8b84224c69e1165acc9a58510e12a4eae3d2ee Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Wed, 23 Apr 2025 20:23:31 +0200 Subject: [PATCH 4/6] test: fix env variable name --- tests/it/cloud_jwt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/it/cloud_jwt.rs b/tests/it/cloud_jwt.rs index 8c6297cf..149d9242 100644 --- a/tests/it/cloud_jwt.rs +++ b/tests/it/cloud_jwt.rs @@ -4,7 +4,7 @@ use clickhouse::Client; #[tokio::test] async fn test_jwt_auth() { check_cloud_test_env!(); - let valid_token = require_env_var("CLICKHOUSE_CLOUD_JWT_TOKEN"); + let valid_token = require_env_var("CLICKHOUSE_CLOUD_JWT_ACCESS_TOKEN"); let client = Client::default() .with_url(get_cloud_url()) .with_access_token(valid_token); From 68d0d7138d9f38c93ad400d8662c80eee534de7c Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Wed, 23 Apr 2025 20:23:39 +0200 Subject: [PATCH 5/6] ci: rearrange GHA --- .github/workflows/ci.yml | 45 ++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 502ff3cf..2b3fff88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,29 +68,36 @@ jobs: - run: cargo clippy --features rustls-tls-aws-lc,rustls-tls-webpki-roots - run: cargo clippy --features rustls-tls-aws-lc,rustls-tls-native-roots + docs: + needs: build + runs-on: ubuntu-latest + env: + RUSTDOCFLAGS: -Dwarnings --cfg docsrs + steps: + - uses: actions/checkout@v4 + - run: rustup toolchain install nightly + - run: rustup override set nightly + - run: rustup show active-toolchain -v + - run: cargo doc --all-features + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Start ClickHouse in Docker + uses: hoverkraft-tech/compose-action@v2.2.0 + with: + compose-file: 'docker-compose.yml' + down-flags: '--volumes' + - run: rustup show active-toolchain -v - run: cargo test - run: cargo test --no-default-features - run: cargo test --features uuid,time - run: cargo test --all-features - services: - clickhouse: - image: clickhouse/clickhouse-server:24.10-alpine - ports: - - 8123:8123 - - cloud-test: - runs-on: ubuntu-latest - needs: [ test ] - steps: - - uses: actions/checkout@v4 - - run: rustup show active-toolchain -v - - name: Run tests + # Temporary runs tests with `cloud_` prefix only until we validate that the rest of the tests are working + - name: Run tests with ClickHouse Cloud env: CLICKHOUSE_TEST_ENVIRONMENT: cloud CLICKHOUSE_CLOUD_HOST: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_HOST_SMT }} @@ -98,15 +105,3 @@ jobs: CLICKHOUSE_CLOUD_JWT_ACCESS_TOKEN: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_JWT_DESERT_VM_43 }} run: | cargo test cloud_ --features rustls-tls - - docs: - needs: build - runs-on: ubuntu-latest - env: - RUSTDOCFLAGS: -Dwarnings --cfg docsrs - steps: - - uses: actions/checkout@v4 - - run: rustup toolchain install nightly - - run: rustup override set nightly - - run: rustup show active-toolchain -v - - run: cargo doc --all-features From 156d1c5adfc5dd70bcb476f4418267fef8767355 Mon Sep 17 00:00:00 2001 From: slvrtrn Date: Wed, 23 Apr 2025 20:31:05 +0200 Subject: [PATCH 6/6] ci: add `--nocapture` for debugging purposes --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b3fff88..da9ee2eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,4 +104,4 @@ jobs: CLICKHOUSE_CLOUD_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD_SMT }} CLICKHOUSE_CLOUD_JWT_ACCESS_TOKEN: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_JWT_DESERT_VM_43 }} run: | - cargo test cloud_ --features rustls-tls + cargo test cloud_ --features rustls-tls -- --nocapture