diff --git a/Cargo.lock b/Cargo.lock index 6a8ba75..f56bf7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1464,13 +1464,10 @@ name = "dsdk-facet-core" version = "0.0.1" dependencies = [ "async-trait", - "aws-config", - "aws-credential-types", - "aws-sdk-s3", - "aws-sigv4", - "aws-smithy-runtime-api", "bon", "chrono", + "dsdk-facet-postgres", + "dsdk-facet-testcontainers", "ed25519-dalek", "hex", "http 1.4.0", @@ -1479,9 +1476,6 @@ dependencies = [ "mockall", "once_cell", "pingora", - "pingora-core", - "pingora-http", - "pingora-proxy", "pkcs8 0.10.2", "rand 0.9.2", "regex", @@ -1491,9 +1485,6 @@ dependencies = [ "serde", "serde_json", "sodiumoxide", - "sqlx", - "testcontainers", - "testcontainers-modules", "thiserror 2.0.18", "tokio", "url", @@ -1501,6 +1492,85 @@ dependencies = [ "wiremock", ] +[[package]] +name = "dsdk-facet-hashicorp-vault" +version = "0.0.1" +dependencies = [ + "async-trait", + "bon", + "chrono", + "dsdk-facet-core", + "dsdk-facet-testcontainers", + "log", + "rand 0.9.2", + "reqwest", + "serde", + "serde_json", + "tokio", + "wiremock", +] + +[[package]] +name = "dsdk-facet-postgres" +version = "0.0.1" +dependencies = [ + "async-trait", + "bon", + "chrono", + "dsdk-facet-core", + "dsdk-facet-testcontainers", + "once_cell", + "rand 0.9.2", + "serde", + "sodiumoxide", + "sqlx", + "tokio", + "uuid", +] + +[[package]] +name = "dsdk-facet-proxy" +version = "0.0.1" +dependencies = [ + "async-trait", + "aws-config", + "aws-credential-types", + "aws-sdk-s3", + "aws-sigv4", + "aws-smithy-runtime-api", + "bon", + "dsdk-facet-core", + "dsdk-facet-postgres", + "dsdk-facet-testcontainers", + "http 1.4.0", + "once_cell", + "pingora", + "pingora-core", + "pingora-http", + "pingora-proxy", + "serde_json", + "tokio", + "url", + "uuid", +] + +[[package]] +name = "dsdk-facet-testcontainers" +version = "0.0.1" +dependencies = [ + "aws-config", + "aws-sdk-s3", + "chrono", + "reqwest", + "serde", + "serde_json", + "sqlx", + "testcontainers", + "testcontainers-modules", + "tokio", + "uuid", +] + [[package]] name = "dunce" version = "1.0.5" diff --git a/crates/facet-core/Cargo.toml b/crates/facet-core/Cargo.toml index 3e3092b..3b3f8d1 100644 --- a/crates/facet-core/Cargo.toml +++ b/crates/facet-core/Cargo.toml @@ -15,32 +15,23 @@ serde_json = { workspace = true } rand = { workspace = true } ed25519-dalek = { workspace = true } pkcs8 = { workspace = true } -rstest = { workspace = true } rsa = { workspace = true } async-trait = {workspace = true} http = {workspace = true} pingora = {workspace = true} -pingora-core = {workspace = true} -pingora-proxy = {workspace = true} -pingora-http = "0.6" -aws-sigv4 = "1.2" -aws-credential-types = {workspace = true} -aws-smithy-runtime-api = {workspace = true} regex = {workspace = true} url = "2.5" -sqlx = {workspace = true} tokio = {workspace = true} reqwest = {workspace = true} log = {workspace = true} [dev-dependencies] -aws-sdk-s3 = { workspace = true } -aws-config = { workspace = true } -testcontainers = { workspace = true } -testcontainers-modules = { workspace = true, features = ["hashicorp_vault"] } +dsdk-facet-testcontainers = { path = "../facet-testcontainers" } +dsdk-facet-postgres = { path = "../facet-postgres" } tokio = { workspace = true } reqwest = { workspace = true } wiremock = { workspace = true } uuid = { workspace = true } once_cell = { workspace = true } mockall = { workspace = true } +rstest = { workspace = true } diff --git a/crates/facet-core/src/auth/mod.rs b/crates/facet-core/src/auth/mod.rs index e104fb9..4108a10 100644 --- a/crates/facet-core/src/auth/mod.rs +++ b/crates/facet-core/src/auth/mod.rs @@ -29,7 +29,6 @@ mod tests; mod mem; -mod postgres; use crate::context::ParticipantContext; use bon::Builder; @@ -37,7 +36,6 @@ use regex::Regex; use thiserror::Error; pub use mem::MemoryAuthorizationEvaluator; -pub use postgres::PostgresAuthorizationEvaluator; /// Represents an operation with specific attributes that describe its scope, action, and resource. /// diff --git a/crates/facet-core/src/lib.rs b/crates/facet-core/src/lib.rs index 8ec3e64..6caa3f3 100644 --- a/crates/facet-core/src/lib.rs +++ b/crates/facet-core/src/lib.rs @@ -14,7 +14,6 @@ pub mod auth; pub mod context; pub mod jwt; pub mod lock; -pub mod proxy; pub mod token; pub mod util; pub mod vault; diff --git a/crates/facet-core/src/lock/mod.rs b/crates/facet-core/src/lock/mod.rs index 87fe166..5acf91f 100644 --- a/crates/facet-core/src/lock/mod.rs +++ b/crates/facet-core/src/lock/mod.rs @@ -16,11 +16,9 @@ use std::sync::Arc; use thiserror::Error; pub mod mem; -pub mod postgres; mod tests; pub use mem::MemoryLockManager; -pub use postgres::PostgresLockManager; /// Provide distributed locking for coordinating access to shared resources. /// @@ -118,9 +116,9 @@ pub struct LockGuard { } impl LockGuard { - pub(crate) fn new(lock_manager: Arc, identifier: impl Into, owner: impl Into) -> Self + pub fn new(lock_manager: Arc, identifier: impl Into, owner: impl Into) -> Self where - T: LockManagerInternal + 'static, + T: LockManager + UnlockOps + 'static, { Self { lock_manager, diff --git a/crates/facet-core/src/proxy/mod.rs b/crates/facet-core/src/proxy/mod.rs deleted file mode 100644 index 3be2358..0000000 --- a/crates/facet-core/src/proxy/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2026 Metaform Systems, Inc -// -// This program and the accompanying materials are made available under the -// terms of the Apache License, Version 2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: Apache-2.0 -// -// Contributors: -// Metaform Systems, Inc. - initial API and implementation -// - -pub mod s3; diff --git a/crates/facet-core/src/token/mod.rs b/crates/facet-core/src/token/mod.rs index fa62a64..62262aa 100644 --- a/crates/facet-core/src/token/mod.rs +++ b/crates/facet-core/src/token/mod.rs @@ -12,13 +12,11 @@ pub mod mem; pub mod oauth; -pub mod postgres; #[cfg(test)] mod tests; pub use mem::MemoryTokenStore; -pub use postgres::PostgresTokenStore; const FIVE_SECONDS_MILLIS: i64 = 5_000; diff --git a/crates/facet-core/src/vault/mod.rs b/crates/facet-core/src/vault/mod.rs index c07d157..5f21bad 100644 --- a/crates/facet-core/src/vault/mod.rs +++ b/crates/facet-core/src/vault/mod.rs @@ -10,8 +10,6 @@ // Metaform Systems, Inc. - initial API and implementation // -pub mod hashicorp; - #[cfg(test)] mod tests; diff --git a/crates/facet-core/src/vault/tests/mod.rs b/crates/facet-core/src/vault/tests/mod.rs index f0767d1..9a89c7b 100644 --- a/crates/facet-core/src/vault/tests/mod.rs +++ b/crates/facet-core/src/vault/tests/mod.rs @@ -10,7 +10,5 @@ // Metaform Systems, Inc. - initial API and implementation // -#[cfg(test)] -mod hashicorp; #[cfg(test)] mod mem; diff --git a/crates/facet-core/tests/token_api.rs b/crates/facet-core/tests/token_api.rs index 3152d0a..051e237 100644 --- a/crates/facet-core/tests/token_api.rs +++ b/crates/facet-core/tests/token_api.rs @@ -10,20 +10,19 @@ // Metaform Systems, Inc. - initial API and implementation // -mod common; - -use crate::common::setup_postgres_container; use chrono::{TimeDelta, Utc}; use dsdk_facet_core::context::ParticipantContext; use dsdk_facet_core::jwt::jwtutils::{ StaticSigningKeyResolver, StaticVerificationKeyResolver, generate_ed25519_keypair_pem, }; use dsdk_facet_core::jwt::{JwtVerifier, LocalJwtGenerator, LocalJwtVerifier}; -use dsdk_facet_core::lock::PostgresLockManager; use dsdk_facet_core::token::oauth::OAuth2TokenClient; -use dsdk_facet_core::token::{PostgresTokenStore, TokenClientApi, TokenData, TokenStore}; +use dsdk_facet_core::token::{TokenClientApi, TokenData, TokenStore}; use dsdk_facet_core::util::clock::default_clock; use dsdk_facet_core::util::encryption::encryption_key; +use dsdk_facet_postgres::lock::PostgresLockManager; +use dsdk_facet_postgres::token::PostgresTokenStore; +use dsdk_facet_testcontainers::postgres::setup_postgres_container; use once_cell::sync::Lazy; use sodiumoxide::crypto::secretbox; use std::sync::Arc; diff --git a/crates/facet-hashicorp-vault/Cargo.toml b/crates/facet-hashicorp-vault/Cargo.toml new file mode 100644 index 0000000..eadda94 --- /dev/null +++ b/crates/facet-hashicorp-vault/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "dsdk-facet-hashicorp-vault" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +dsdk-facet-core = { path = "../facet-core" } +bon = { workspace = true } +rand = { workspace = true } +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +log = { workspace = true } +reqwest = { workspace = true } +async-trait = { workspace = true } +chrono = { workspace = true } + + +[dev-dependencies] +wiremock = { workspace = true } +dsdk-facet-testcontainers = { path = "../facet-testcontainers" } diff --git a/crates/facet-core/src/vault/hashicorp/auth.rs b/crates/facet-hashicorp-vault/src/auth.rs similarity index 99% rename from crates/facet-core/src/vault/hashicorp/auth.rs rename to crates/facet-hashicorp-vault/src/auth.rs index 08b9079..7bdbab2 100644 --- a/crates/facet-core/src/vault/hashicorp/auth.rs +++ b/crates/facet-hashicorp-vault/src/auth.rs @@ -10,9 +10,9 @@ // Metaform Systems, Inc. - initial API and implementation // -use crate::vault::VaultError; use async_trait::async_trait; use bon::Builder; +use dsdk_facet_core::vault::VaultError; use reqwest::Client; use serde::{Deserialize, Serialize}; diff --git a/crates/facet-core/src/vault/hashicorp/client.rs b/crates/facet-hashicorp-vault/src/client.rs similarity index 98% rename from crates/facet-core/src/vault/hashicorp/client.rs rename to crates/facet-hashicorp-vault/src/client.rs index 40314a9..1e2a94f 100644 --- a/crates/facet-core/src/vault/hashicorp/client.rs +++ b/crates/facet-hashicorp-vault/src/client.rs @@ -14,10 +14,10 @@ use super::auth::{JwtVaultAuthClient, VaultAuthClient, handle_error_response}; use super::config::{CONTENT_KEY, DEFAULT_ROLE, HashicorpVaultConfig}; use super::renewal::{RenewalHandle, TokenRenewer}; use super::state::VaultClientState; -use crate::context::ParticipantContext; -use crate::util::clock::Clock; -use crate::vault::{VaultClient, VaultError}; use async_trait::async_trait; +use dsdk_facet_core::context::ParticipantContext; +use dsdk_facet_core::util::clock::Clock; +use dsdk_facet_core::vault::{VaultClient, VaultError}; use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; use std::sync::Arc; diff --git a/crates/facet-core/src/vault/hashicorp/config.rs b/crates/facet-hashicorp-vault/src/config.rs similarity index 97% rename from crates/facet-core/src/vault/hashicorp/config.rs rename to crates/facet-hashicorp-vault/src/config.rs index a072c54..e7b17f7 100644 --- a/crates/facet-core/src/vault/hashicorp/config.rs +++ b/crates/facet-hashicorp-vault/src/config.rs @@ -10,9 +10,9 @@ // Metaform Systems, Inc. - initial API and implementation // -use crate::util::clock::{Clock, default_clock}; -use crate::vault::VaultError; use bon::Builder; +use dsdk_facet_core::util::clock::{Clock, default_clock}; +use dsdk_facet_core::vault::VaultError; use std::sync::Arc; use std::time::Duration; diff --git a/crates/facet-core/src/vault/hashicorp/mod.rs b/crates/facet-hashicorp-vault/src/lib.rs similarity index 96% rename from crates/facet-core/src/vault/hashicorp/mod.rs rename to crates/facet-hashicorp-vault/src/lib.rs index 15af1d9..e92ffd0 100644 --- a/crates/facet-core/src/vault/hashicorp/mod.rs +++ b/crates/facet-hashicorp-vault/src/lib.rs @@ -20,5 +20,8 @@ pub mod renewal; #[doc(hidden)] pub mod state; +#[cfg(test)] +mod tests; + pub use client::HashicorpVaultClient; pub use config::{ErrorCallback, HashicorpVaultConfig, HashicorpVaultConfigBuilder}; diff --git a/crates/facet-core/src/vault/hashicorp/renewal.rs b/crates/facet-hashicorp-vault/src/renewal.rs similarity index 98% rename from crates/facet-core/src/vault/hashicorp/renewal.rs rename to crates/facet-hashicorp-vault/src/renewal.rs index 16756b9..a31c295 100644 --- a/crates/facet-core/src/vault/hashicorp/renewal.rs +++ b/crates/facet-hashicorp-vault/src/renewal.rs @@ -15,10 +15,10 @@ use super::config::{ DEFAULT_MAX_CONSECUTIVE_FAILURES, DEFAULT_RENEWAL_JITTER, DEFAULT_TOKEN_RENEWAL_PERCENTAGE, ErrorCallback, }; use super::state::VaultClientState; -use crate::util::backoff::{BackoffConfig, calculate_backoff_interval}; -use crate::util::clock::Clock; -use crate::vault::VaultError; use bon::Builder; +use dsdk_facet_core::util::backoff::{BackoffConfig, calculate_backoff_interval}; +use dsdk_facet_core::util::clock::Clock; +use dsdk_facet_core::vault::VaultError; use log::{debug, error}; use rand::Rng; use reqwest::Client; diff --git a/crates/facet-core/src/vault/hashicorp/state.rs b/crates/facet-hashicorp-vault/src/state.rs similarity index 100% rename from crates/facet-core/src/vault/hashicorp/state.rs rename to crates/facet-hashicorp-vault/src/state.rs diff --git a/crates/facet-core/src/vault/tests/hashicorp.rs b/crates/facet-hashicorp-vault/src/tests/client.rs similarity index 96% rename from crates/facet-core/src/vault/tests/hashicorp.rs rename to crates/facet-hashicorp-vault/src/tests/client.rs index b27ef33..8fa3a97 100644 --- a/crates/facet-core/src/vault/tests/hashicorp.rs +++ b/crates/facet-hashicorp-vault/src/tests/client.rs @@ -10,14 +10,14 @@ // Metaform Systems, Inc. - initial API and implementation // -use crate::util::clock::{Clock, MockClock}; -use crate::vault::VaultError; -use crate::vault::hashicorp::auth::VaultAuthClient; -use crate::vault::hashicorp::config::ErrorCallback; -use crate::vault::hashicorp::renewal::TokenRenewer; -use crate::vault::hashicorp::state::VaultClientState; +use crate::auth::VaultAuthClient; +use crate::config::ErrorCallback; +use crate::renewal::TokenRenewer; +use crate::state::VaultClientState; use async_trait::async_trait; use chrono::{TimeDelta, Utc}; +use dsdk_facet_core::util::clock::{Clock, MockClock}; +use dsdk_facet_core::vault::VaultError; use reqwest::Client; use std::sync::Arc; use std::time::Duration; diff --git a/crates/facet-hashicorp-vault/src/tests/mod.rs b/crates/facet-hashicorp-vault/src/tests/mod.rs new file mode 100644 index 0000000..b79c47f --- /dev/null +++ b/crates/facet-hashicorp-vault/src/tests/mod.rs @@ -0,0 +1 @@ +mod client; diff --git a/crates/facet-core/tests/common/mod.rs b/crates/facet-hashicorp-vault/tests/common/mod.rs similarity index 51% rename from crates/facet-core/tests/common/mod.rs rename to crates/facet-hashicorp-vault/tests/common/mod.rs index e696a95..53f679d 100644 --- a/crates/facet-core/tests/common/mod.rs +++ b/crates/facet-hashicorp-vault/tests/common/mod.rs @@ -1,46 +1,8 @@ -// Copyright (c) 2026 Metaform Systems, Inc -// -// This program and the accompanying materials are made available under the -// terms of the Apache License, Version 2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: Apache-2.0 -// -// Contributors: -// Metaform Systems, Inc. - initial API and implementation -// - -// Allow dead code in this module since test utilities are shared across multiple test files -// and each test binary is compiled separately -#![allow(dead_code)] - -mod keycloak; -mod minio; -mod mocks; -mod postgres; -mod proxy_s3; -mod vault; - use chrono::{TimeDelta, Utc}; -use dsdk_facet_core::vault::hashicorp::state::VaultClientState; -#[allow(unused_imports)] // Used in some test files but not others -pub use keycloak::*; -#[allow(unused_imports)] // Used in some test files but not others -pub use minio::*; -#[allow(unused_imports)] // Mocks are used in some test files but not others -pub use mocks::*; -#[allow(unused_imports)] // Used in some test files but not others -pub use postgres::*; -#[allow(unused_imports)] // Used in some test files but not others -pub use proxy_s3::*; -use std::net::TcpListener; +use dsdk_facet_hashicorp_vault::state::VaultClientState; use std::sync::Arc; use std::time::Duration; -use testcontainers::bollard::Docker; -use testcontainers::bollard::models::NetworkCreateRequest; use tokio::sync::RwLock; -#[allow(unused_imports)] // Used in some test files but not others -pub use vault::*; // ============================================================================ // VaultClientState Test Fixtures @@ -156,81 +118,6 @@ pub fn wrapped_test_state( consecutive_failures, ))) } - -/// Creates a Docker network and returns its name. -/// -/// Automatically cleans up old test networks matching the pattern "test-network-*" -/// to prevent Docker address pool exhaustion. Networks are not automatically cleaned -/// up by testcontainers when containers drop, so we must manually remove them. -/// -/// Note: Rust Testcontainers lacks network creation functionality (which is available in Go and Java). -pub async fn create_network() -> String { - // Try to connect to Docker using the socket path from DOCKER_HOST env var, - // or fall back to platform-specific defaults - let docker = if let Ok(docker_host) = std::env::var("DOCKER_HOST") { - Docker::connect_with_unix(&docker_host, 120, testcontainers::bollard::API_DEFAULT_VERSION) - .expect("Failed to connect to Docker via DOCKER_HOST") - } else if cfg!(target_os = "macos") { - // On macOS with Docker Desktop, socket is typically at ~/.docker/run/docker.sock - let home = std::env::var("HOME").expect("HOME env var not set"); - let socket_path = format!("{}/.docker/run/docker.sock", home); - Docker::connect_with_unix(&socket_path, 120, testcontainers::bollard::API_DEFAULT_VERSION).unwrap_or_else( - |_| { - // Fall back to default if custom path doesn't work - Docker::connect_with_local_defaults().expect("Failed to connect to Docker") - }, - ) - } else { - Docker::connect_with_local_defaults().expect("Failed to connect to Docker") - }; - - // Clean up old test networks before creating a new one - cleanup_old_test_networks(&docker).await; - - let network_name = format!("test-network-{}", uuid::Uuid::new_v4()); - - let config = NetworkCreateRequest { - name: network_name.clone(), - ..Default::default() - }; - - docker - .create_network(config) - .await - .expect("Failed to create Docker network"); - - network_name -} - -/// Cleans up old test networks to prevent address pool exhaustion -async fn cleanup_old_test_networks(docker: &Docker) { - use testcontainers::bollard::query_parameters::ListNetworksOptions; - - // List all networks (best effort - ignore errors) - let networks = match docker.list_networks(Option::::None).await { - Ok(networks) => networks, - Err(_) => return, - }; - - for network in networks { - if let Some(name) = &network.name { - // Clean up networks matching our test pattern - if name.starts_with("test-network-") { - // Best effort cleanup - ignore errors - let _ = docker.remove_network(name).await; - } - } - } -} - -/// Get an available port by binding to port 0 and retrieving the assigned port -pub fn get_available_port() -> u16 { - let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to port 0"); - let port = listener.local_addr().expect("Failed to get local address").port(); - drop(listener); - port -} - /// Poll state until a condition is met or timeout expires pub async fn wait_for_condition(state: &Arc>, condition: F, timeout: Duration) -> bool where diff --git a/crates/facet-core/tests/vault_hashicorp.rs b/crates/facet-hashicorp-vault/tests/vault_hashicorp.rs similarity index 98% rename from crates/facet-core/tests/vault_hashicorp.rs rename to crates/facet-hashicorp-vault/tests/vault_hashicorp.rs index 6ec2ced..0b67c48 100644 --- a/crates/facet-core/tests/vault_hashicorp.rs +++ b/crates/facet-hashicorp-vault/tests/vault_hashicorp.rs @@ -10,12 +10,12 @@ // Metaform Systems, Inc. - initial API and implementation // -mod common; - -use crate::common::{create_network, setup_keycloak_container, setup_vault_container}; use dsdk_facet_core::context::ParticipantContext; use dsdk_facet_core::vault::VaultClient; -use dsdk_facet_core::vault::hashicorp::{HashicorpVaultClient, HashicorpVaultConfig}; +use dsdk_facet_hashicorp_vault::{HashicorpVaultClient, HashicorpVaultConfig}; +use dsdk_facet_testcontainers::{ + keycloak::setup_keycloak_container, utils::create_network, vault::setup_vault_container, +}; fn create_test_context() -> ParticipantContext { ParticipantContext { diff --git a/crates/facet-core/tests/vault_hashicorp_renewal.rs b/crates/facet-hashicorp-vault/tests/vault_hashicorp_renewal.rs similarity index 97% rename from crates/facet-core/tests/vault_hashicorp_renewal.rs rename to crates/facet-hashicorp-vault/tests/vault_hashicorp_renewal.rs index caa5d8c..6564536 100644 --- a/crates/facet-core/tests/vault_hashicorp_renewal.rs +++ b/crates/facet-hashicorp-vault/tests/vault_hashicorp_renewal.rs @@ -13,15 +13,16 @@ //! These tests verify the renewal loop behavior without requiring full container setup, //! using WireMock to simulate Vault and OAuth2 endpoints. +#[allow(unused)] mod common; use crate::common::{wait_for_condition, wrapped_test_state}; use dsdk_facet_core::util::clock::default_clock; -use dsdk_facet_core::vault::hashicorp::auth::JwtVaultAuthClient; -use dsdk_facet_core::vault::hashicorp::config::DEFAULT_ROLE; -use dsdk_facet_core::vault::hashicorp::renewal::TokenRenewer; -use dsdk_facet_core::vault::hashicorp::state::VaultClientState; -use dsdk_facet_core::vault::hashicorp::{ErrorCallback, HashicorpVaultConfig}; +use dsdk_facet_hashicorp_vault::auth::JwtVaultAuthClient; +use dsdk_facet_hashicorp_vault::config::DEFAULT_ROLE; +use dsdk_facet_hashicorp_vault::renewal::TokenRenewer; +use dsdk_facet_hashicorp_vault::state::VaultClientState; +use dsdk_facet_hashicorp_vault::{ErrorCallback, HashicorpVaultConfig}; use reqwest::Client; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; diff --git a/crates/facet-postgres/Cargo.toml b/crates/facet-postgres/Cargo.toml new file mode 100644 index 0000000..0a4c65b --- /dev/null +++ b/crates/facet-postgres/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "dsdk-facet-postgres" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +dsdk-facet-core = { path = "../facet-core" } +sqlx = {workspace = true} +async-trait = {workspace = true} +bon = { workspace = true } +rand = { workspace = true } +chrono = { workspace = true } +serde = { workspace = true } +sodiumoxide = { workspace = true } + +[dev-dependencies] +dsdk-facet-testcontainers = { path = "../facet-testcontainers" } +tokio = { workspace = true } +uuid = { workspace = true } +once_cell = { workspace = true } diff --git a/crates/facet-core/src/auth/postgres.rs b/crates/facet-postgres/src/auth.rs similarity index 98% rename from crates/facet-core/src/auth/postgres.rs rename to crates/facet-postgres/src/auth.rs index ba5756f..00922f4 100644 --- a/crates/facet-core/src/auth/postgres.rs +++ b/crates/facet-postgres/src/auth.rs @@ -10,8 +10,10 @@ // Metaform Systems, Inc. - initial API and implementation // -use crate::auth::{AuthorizationError, AuthorizationEvaluator, Operation, Rule, RuleStore}; -use crate::context::ParticipantContext; +use dsdk_facet_core::{ + auth::{AuthorizationError, AuthorizationEvaluator, Operation, Rule, RuleStore}, + context::ParticipantContext, +}; use sqlx::PgPool; /// Postgres-backed authorization evaluator using SQLx connection pooling. diff --git a/crates/facet-postgres/src/lib.rs b/crates/facet-postgres/src/lib.rs new file mode 100644 index 0000000..e6172c7 --- /dev/null +++ b/crates/facet-postgres/src/lib.rs @@ -0,0 +1,3 @@ +pub mod auth; +pub mod lock; +pub mod token; diff --git a/crates/facet-core/src/lock/postgres.rs b/crates/facet-postgres/src/lock.rs similarity index 99% rename from crates/facet-core/src/lock/postgres.rs rename to crates/facet-postgres/src/lock.rs index bca2824..49fd80c 100644 --- a/crates/facet-core/src/lock/postgres.rs +++ b/crates/facet-postgres/src/lock.rs @@ -10,11 +10,11 @@ // Metaform Systems, Inc. - initial API and implementation // -use crate::lock::{LockError, LockGuard, LockManager, UnlockOps}; -use crate::util::clock::{Clock, default_clock}; use async_trait::async_trait; use bon::Builder; use chrono::TimeDelta; +use dsdk_facet_core::lock::{LockError, LockGuard, LockManager, UnlockOps}; +use dsdk_facet_core::util::clock::{Clock, default_clock}; use rand::Rng; use sqlx::PgPool; use std::sync::Arc; diff --git a/crates/facet-core/src/token/postgres.rs b/crates/facet-postgres/src/token.rs similarity index 97% rename from crates/facet-core/src/token/postgres.rs rename to crates/facet-postgres/src/token.rs index da9a22d..1b31a1f 100644 --- a/crates/facet-core/src/token/postgres.rs +++ b/crates/facet-postgres/src/token.rs @@ -10,13 +10,13 @@ // Metaform Systems, Inc. - initial API and implementation // -use crate::context::ParticipantContext; -use crate::token::{TokenData, TokenError, TokenStore}; -use crate::util::clock::{Clock, default_clock}; -use crate::util::encryption::{decrypt, encrypt}; use async_trait::async_trait; use bon::Builder; use chrono::DateTime; +use dsdk_facet_core::context::ParticipantContext; +use dsdk_facet_core::token::{TokenData, TokenError, TokenStore}; +use dsdk_facet_core::util::clock::{Clock, default_clock}; +use dsdk_facet_core::util::encryption::{decrypt, encrypt}; use sodiumoxide::crypto::secretbox; use sqlx::PgPool; use std::sync::Arc; @@ -43,7 +43,7 @@ use std::sync::Arc; /// must be consistent across instances and restarts and should be stored securely: /// /// ```no_run -/// # use dsdk_facet_core::token::PostgresTokenStore; +/// # use dsdk_facet_postgres::token::PostgresTokenStore; /// # use sqlx::PgPool; /// /// # async fn launch() -> Result<(), Box> { diff --git a/crates/facet-core/tests/auth_postgres.rs b/crates/facet-postgres/tests/auth_postgres.rs similarity index 99% rename from crates/facet-core/tests/auth_postgres.rs rename to crates/facet-postgres/tests/auth_postgres.rs index eeeb872..26dbd05 100644 --- a/crates/facet-core/tests/auth_postgres.rs +++ b/crates/facet-postgres/tests/auth_postgres.rs @@ -10,13 +10,10 @@ // Metaform Systems, Inc. - initial API and implementation // -mod common; - -use crate::common::setup_postgres_container; -use dsdk_facet_core::auth::{ - AuthorizationError, AuthorizationEvaluator, Operation, PostgresAuthorizationEvaluator, Rule, RuleStore, -}; +use dsdk_facet_core::auth::{AuthorizationError, AuthorizationEvaluator, Operation, Rule, RuleStore}; use dsdk_facet_core::context::ParticipantContext; +use dsdk_facet_postgres::auth::PostgresAuthorizationEvaluator; +use dsdk_facet_testcontainers::postgres::setup_postgres_container; use std::sync::Arc; #[tokio::test] diff --git a/crates/facet-core/tests/lock_manager_postgres.rs b/crates/facet-postgres/tests/lock_manager_postgres.rs similarity index 99% rename from crates/facet-core/tests/lock_manager_postgres.rs rename to crates/facet-postgres/tests/lock_manager_postgres.rs index 283fc9e..6ae2324 100644 --- a/crates/facet-core/tests/lock_manager_postgres.rs +++ b/crates/facet-postgres/tests/lock_manager_postgres.rs @@ -10,14 +10,12 @@ // Metaform Systems, Inc. - initial API and implementation // -mod common; - -use crate::common::setup_postgres_container; use chrono::{TimeDelta, Utc}; use dsdk_facet_core::lock::LockError::{LockAlreadyHeld, LockNotFound}; -use dsdk_facet_core::lock::postgres::PostgresLockManager; use dsdk_facet_core::lock::{LockManager, UnlockOps}; use dsdk_facet_core::util::clock::{Clock, MockClock}; +use dsdk_facet_postgres::lock::PostgresLockManager; +use dsdk_facet_testcontainers::postgres::setup_postgres_container; use std::sync::Arc; use uuid::Uuid; diff --git a/crates/facet-core/tests/token_store_postgres.rs b/crates/facet-postgres/tests/token_store_postgres.rs similarity index 99% rename from crates/facet-core/tests/token_store_postgres.rs rename to crates/facet-postgres/tests/token_store_postgres.rs index fb6badc..7db1340 100644 --- a/crates/facet-core/tests/token_store_postgres.rs +++ b/crates/facet-postgres/tests/token_store_postgres.rs @@ -10,14 +10,14 @@ // Metaform Systems, Inc. - initial API and implementation // -mod common; - use chrono::{TimeDelta, Utc}; -use common::{setup_postgres_container, truncate_to_micros}; + use dsdk_facet_core::context::ParticipantContext; -use dsdk_facet_core::token::{PostgresTokenStore, TokenData, TokenError, TokenStore}; +use dsdk_facet_core::token::{TokenData, TokenError, TokenStore}; use dsdk_facet_core::util::clock::{Clock, MockClock}; use dsdk_facet_core::util::encryption::encryption_key; +use dsdk_facet_postgres::token::PostgresTokenStore; +use dsdk_facet_testcontainers::postgres::{setup_postgres_container, truncate_to_micros}; use once_cell::sync::Lazy; use sodiumoxide::crypto::secretbox; use std::sync::Arc; diff --git a/crates/facet-proxy/Cargo.toml b/crates/facet-proxy/Cargo.toml new file mode 100644 index 0000000..a7c254e --- /dev/null +++ b/crates/facet-proxy/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "dsdk-facet-proxy" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +dsdk-facet-core = { path = "../facet-core" } +bon = { workspace = true } +http = {workspace = true} +async-trait = {workspace = true} +serde_json = { workspace = true } +aws-sigv4 = "1.2" +aws-credential-types = {workspace = true} +aws-smithy-runtime-api = {workspace = true} +pingora = {workspace = true} +pingora-core = {workspace = true} +pingora-proxy = {workspace = true} +pingora-http = "0.6" +url = "2.5" + +[dev-dependencies] +dsdk-facet-postgres = { path = "../facet-postgres" } +dsdk-facet-testcontainers = { path = "../facet-testcontainers" } +tokio = { workspace = true } +uuid = { workspace = true } +once_cell = { workspace = true } +aws-sdk-s3 = { workspace = true } +aws-config = { workspace = true } diff --git a/crates/facet-proxy/src/lib.rs b/crates/facet-proxy/src/lib.rs new file mode 100644 index 0000000..7dce405 --- /dev/null +++ b/crates/facet-proxy/src/lib.rs @@ -0,0 +1 @@ +pub mod s3; diff --git a/crates/facet-core/src/proxy/s3/mod.rs b/crates/facet-proxy/src/s3/mod.rs similarity index 98% rename from crates/facet-core/src/proxy/s3/mod.rs rename to crates/facet-proxy/src/s3/mod.rs index 44de688..a0a7bd5 100644 --- a/crates/facet-core/src/proxy/s3/mod.rs +++ b/crates/facet-proxy/src/s3/mod.rs @@ -59,15 +59,15 @@ pub mod opparser; #[cfg(test)] mod tests; -use crate::auth::{AuthorizationEvaluator, Operation}; -use crate::context::{ParticipantContext, ParticipantContextResolver}; -use crate::jwt::{JwtVerificationError, JwtVerifier, TokenClaims}; use async_trait::async_trait; use aws_credential_types::Credentials; use aws_sigv4::http_request::{SignableBody, SignableRequest, SigningSettings, sign}; use aws_sigv4::sign::v4; use aws_smithy_runtime_api::client::identity::Identity; use bon::Builder; +use dsdk_facet_core::auth::{AuthorizationEvaluator, Operation}; +use dsdk_facet_core::context::{ParticipantContext, ParticipantContextResolver}; +use dsdk_facet_core::jwt::{JwtVerificationError, JwtVerifier, TokenClaims}; use pingora_core::upstreams::peer::HttpPeer; use pingora_core::{Error, ErrorType, Result}; use pingora_http::RequestHeader; diff --git a/crates/facet-core/src/proxy/s3/opparser.rs b/crates/facet-proxy/src/s3/opparser.rs similarity index 99% rename from crates/facet-core/src/proxy/s3/opparser.rs rename to crates/facet-proxy/src/s3/opparser.rs index 58203b3..a12957d 100644 --- a/crates/facet-core/src/proxy/s3/opparser.rs +++ b/crates/facet-proxy/src/s3/opparser.rs @@ -16,7 +16,7 @@ //! following AWS S3's operation model. use super::S3OperationParser; -use crate::auth::Operation; +use dsdk_facet_core::auth::Operation; use pingora_core::Result; use pingora_http::RequestHeader; use url::Url; diff --git a/crates/facet-core/src/proxy/s3/tests/auth.rs b/crates/facet-proxy/src/s3/tests/auth.rs similarity index 99% rename from crates/facet-core/src/proxy/s3/tests/auth.rs rename to crates/facet-proxy/src/s3/tests/auth.rs index 0dda2ef..721e8f4 100644 --- a/crates/facet-core/src/proxy/s3/tests/auth.rs +++ b/crates/facet-proxy/src/s3/tests/auth.rs @@ -10,10 +10,10 @@ // Metaform Systems, Inc. - initial API and implementation // -use crate::auth::{AuthorizationEvaluator, MemoryAuthorizationEvaluator, Rule, RuleStore}; -use crate::context::ParticipantContext; -use crate::proxy::s3::opparser::DefaultS3OperationParser; -use crate::proxy::s3::{S3OperationParser, S3Resources}; +use crate::s3::opparser::DefaultS3OperationParser; +use crate::s3::{S3OperationParser, S3Resources}; +use dsdk_facet_core::auth::{AuthorizationEvaluator, MemoryAuthorizationEvaluator, Rule, RuleStore}; +use dsdk_facet_core::context::ParticipantContext; use pingora_http::RequestHeader; /// Helper function to create a RequestHeader for testing diff --git a/crates/facet-core/src/proxy/s3/tests/mod.rs b/crates/facet-proxy/src/s3/tests/mod.rs similarity index 100% rename from crates/facet-core/src/proxy/s3/tests/mod.rs rename to crates/facet-proxy/src/s3/tests/mod.rs diff --git a/crates/facet-core/src/proxy/s3/tests/opparser.rs b/crates/facet-proxy/src/s3/tests/opparser.rs similarity index 99% rename from crates/facet-core/src/proxy/s3/tests/opparser.rs rename to crates/facet-proxy/src/s3/tests/opparser.rs index 2afe940..fe34999 100644 --- a/crates/facet-core/src/proxy/s3/tests/opparser.rs +++ b/crates/facet-proxy/src/s3/tests/opparser.rs @@ -10,8 +10,8 @@ // Metaform Systems, Inc. - initial API and implementation // -use crate::proxy::s3::S3OperationParser; -use crate::proxy::s3::opparser::DefaultS3OperationParser; +use crate::s3::S3OperationParser; +use crate::s3::opparser::DefaultS3OperationParser; use pingora_http::RequestHeader; // ==================== Object GET Operations ==================== diff --git a/crates/facet-core/src/proxy/s3/tests/s3.rs b/crates/facet-proxy/src/s3/tests/s3.rs similarity index 98% rename from crates/facet-core/src/proxy/s3/tests/s3.rs rename to crates/facet-proxy/src/s3/tests/s3.rs index 3481019..9d9de75 100644 --- a/crates/facet-core/src/proxy/s3/tests/s3.rs +++ b/crates/facet-proxy/src/s3/tests/s3.rs @@ -11,8 +11,8 @@ // use super::super::*; -use crate::auth::TrueAuthorizationEvaluator; -use crate::context::{ParticipantContext, StaticParticipantContextResolver}; +use dsdk_facet_core::auth::TrueAuthorizationEvaluator; +use dsdk_facet_core::context::{ParticipantContext, StaticParticipantContextResolver}; use std::sync::Arc; #[test] diff --git a/crates/facet-core/tests/common/mocks.rs b/crates/facet-proxy/tests/common/mocks.rs similarity index 96% rename from crates/facet-core/tests/common/mocks.rs rename to crates/facet-proxy/tests/common/mocks.rs index 789c17a..7d9fc13 100644 --- a/crates/facet-core/tests/common/mocks.rs +++ b/crates/facet-proxy/tests/common/mocks.rs @@ -13,7 +13,7 @@ use dsdk_facet_core::auth::{AuthorizationError, AuthorizationEvaluator, Operation}; use dsdk_facet_core::context::ParticipantContext; use dsdk_facet_core::jwt::{JwtVerificationError, JwtVerifier, TokenClaims}; -use dsdk_facet_core::proxy::s3::{S3CredentialResolver, S3Credentials, S3OperationParser}; +use dsdk_facet_proxy::s3::{S3CredentialResolver, S3Credentials, S3OperationParser}; use pingora_core::Result; use pingora_http::RequestHeader; use serde_json::{Map, Value}; @@ -127,7 +127,7 @@ pub struct FailingCredentialResolver { impl S3CredentialResolver for FailingCredentialResolver { fn resolve_credentials(&self, _context: &ParticipantContext) -> Result { - Err(dsdk_facet_core::proxy::s3::internal_error(format!( + Err(dsdk_facet_proxy::s3::internal_error(format!( "Database connection to {} failed: timeout after 30s, last error: connection refused", self.internal_detail ))) @@ -160,7 +160,7 @@ pub struct FailingOperationParser { impl S3OperationParser for FailingOperationParser { fn parse_operation(&self, _scope: &str, _request: &RequestHeader) -> Result { - Err(dsdk_facet_core::proxy::s3::internal_error(format!( + Err(dsdk_facet_proxy::s3::internal_error(format!( "Parser cache corrupted at {}, memory address 0x7fff5fbff000, stack trace: [parser.rs:123]", self.internal_detail ))) diff --git a/crates/facet-proxy/tests/common/mod.rs b/crates/facet-proxy/tests/common/mod.rs new file mode 100644 index 0000000..d08bfe5 --- /dev/null +++ b/crates/facet-proxy/tests/common/mod.rs @@ -0,0 +1,32 @@ +// Copyright (c) 2026 Metaform Systems, Inc +// +// This program and the accompanying materials are made available under the +// terms of the Apache License, Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +// +// Contributors: +// Metaform Systems, Inc. - initial API and implementation +// + +// Allow dead code in this module since test utilities are shared across multiple test files +// and each test binary is compiled separately +#![allow(dead_code)] + +mod mocks; +mod proxy_s3; + +#[allow(unused_imports)] // Mocks are used in some test files but not others +pub use mocks::*; +#[allow(unused_imports)] // Used in some test files but not others +pub use proxy_s3::*; +use std::net::TcpListener; + +/// Get an available port by binding to port 0 and retrieving the assigned port +pub fn get_available_port() -> u16 { + let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to port 0"); + let port = listener.local_addr().expect("Failed to get local address").port(); + drop(listener); + port +} diff --git a/crates/facet-core/tests/common/proxy_s3.rs b/crates/facet-proxy/tests/common/proxy_s3.rs similarity index 98% rename from crates/facet-core/tests/common/proxy_s3.rs rename to crates/facet-proxy/tests/common/proxy_s3.rs index ee218c7..5a38f2e 100644 --- a/crates/facet-core/tests/common/proxy_s3.rs +++ b/crates/facet-proxy/tests/common/proxy_s3.rs @@ -11,7 +11,6 @@ // use super::mocks::{PassthroughCredentialsResolver, TestJwtVerifier, TokenMatchingJwtVerifier}; -use super::{MINIO_ACCESS_KEY, MINIO_SECRET_KEY}; use aws_config::Region; use aws_credential_types::Credentials; use aws_sdk_s3::Client; @@ -19,9 +18,10 @@ use aws_smithy_runtime_api::client::behavior_version::BehaviorVersion; use dsdk_facet_core::auth::{AuthorizationEvaluator, MemoryAuthorizationEvaluator, RuleStore}; use dsdk_facet_core::context::{ParticipantContext, ParticipantContextResolver, StaticParticipantContextResolver}; use dsdk_facet_core::jwt::JwtVerifier; -use dsdk_facet_core::proxy::s3::{ +use dsdk_facet_proxy::s3::{ DefaultS3OperationParser, S3CredentialResolver, S3Credentials, S3OperationParser, S3Proxy, UpstreamStyle, }; +use dsdk_facet_testcontainers::minio::{MINIO_ACCESS_KEY, MINIO_SECRET_KEY}; use pingora::server::Server; use pingora::server::configuration::Opt; use pingora_proxy::http_proxy_service; diff --git a/crates/facet-core/tests/proxy_s3.rs b/crates/facet-proxy/tests/proxy_s3.rs similarity index 92% rename from crates/facet-core/tests/proxy_s3.rs rename to crates/facet-proxy/tests/proxy_s3.rs index 0b33ed4..7354cd7 100644 --- a/crates/facet-core/tests/proxy_s3.rs +++ b/crates/facet-proxy/tests/proxy_s3.rs @@ -13,12 +13,18 @@ mod common; use common::{ - MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MinioInstance, PassthroughCredentialsResolver, ProxyConfig, TEST_BUCKET, - TestJwtVerifier, create_test_client, get_available_port, launch_s3proxy, setup_postgres_container, + PassthroughCredentialsResolver, ProxyConfig, TestJwtVerifier, create_test_client, get_available_port, + launch_s3proxy, }; -use dsdk_facet_core::auth::{AuthorizationEvaluator, Operation, PostgresAuthorizationEvaluator, Rule, RuleStore}; + +use dsdk_facet_core::auth::{AuthorizationEvaluator, Operation, Rule, RuleStore}; use dsdk_facet_core::context::{ParticipantContext, StaticParticipantContextResolver}; -use dsdk_facet_core::proxy::s3::{DefaultS3OperationParser, S3Credentials, UpstreamStyle}; +use dsdk_facet_postgres::auth::PostgresAuthorizationEvaluator; +use dsdk_facet_proxy::s3::{DefaultS3OperationParser, S3Credentials, UpstreamStyle}; +use dsdk_facet_testcontainers::{ + minio::{MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MinioInstance, TEST_BUCKET}, + postgres::setup_postgres_container, +}; use std::sync::Arc; #[tokio::test] diff --git a/crates/facet-core/tests/proxy_s3_auth.rs b/crates/facet-proxy/tests/proxy_s3_auth.rs similarity index 98% rename from crates/facet-core/tests/proxy_s3_auth.rs rename to crates/facet-proxy/tests/proxy_s3_auth.rs index aba0269..f57ec34 100644 --- a/crates/facet-core/tests/proxy_s3_auth.rs +++ b/crates/facet-proxy/tests/proxy_s3_auth.rs @@ -12,15 +12,14 @@ mod common; -use crate::common::{ - MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MinioInstance, ProxyConfig, TEST_BUCKET, add_auth_rule, create_test_client, - get_available_port, launch_s3proxy, -}; +use crate::common::{ProxyConfig, add_auth_rule, create_test_client, get_available_port, launch_s3proxy}; + use aws_config::BehaviorVersion; use aws_sdk_s3::Client; use aws_sdk_s3::config::{Credentials, Region}; use aws_sdk_s3::primitives::ByteStream; use dsdk_facet_core::auth::MemoryAuthorizationEvaluator; +use dsdk_facet_testcontainers::minio::{MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MinioInstance, TEST_BUCKET}; use std::sync::Arc; // ==================== Object GET Operations - Allow ==================== diff --git a/crates/facet-core/tests/proxy_s3_error_prop.rs b/crates/facet-proxy/tests/proxy_s3_error_prop.rs similarity index 97% rename from crates/facet-core/tests/proxy_s3_error_prop.rs rename to crates/facet-proxy/tests/proxy_s3_error_prop.rs index 605d563..68fef20 100644 --- a/crates/facet-core/tests/proxy_s3_error_prop.rs +++ b/crates/facet-proxy/tests/proxy_s3_error_prop.rs @@ -23,11 +23,14 @@ mod common; use crate::common::{ DefaultOperationParser, DetailedFailureJwtVerifier, FailingAuthEvaluator, FailingCredentialResolver, FailingOperationParser, PassthroughCredentialsResolver, PermissiveAuthEvaluator, ProxyConfig, - SuspiciousCredentialResolver, TEST_BUCKET, TEST_KEY, TestJwtVerifier, get_available_port, launch_s3proxy, + SuspiciousCredentialResolver, TestJwtVerifier, get_available_port, launch_s3proxy, }; + use aws_config::BehaviorVersion; use aws_sdk_s3::Client; use aws_sdk_s3::config::{Credentials, Region}; +use dsdk_facet_proxy::s3::S3Credentials; +use dsdk_facet_testcontainers::minio::{TEST_BUCKET, TEST_KEY}; use std::sync::Arc; // ============================================================================ @@ -90,7 +93,7 @@ async fn test_authorization_evaluator_failure_does_not_leak_details() { launch_s3proxy(ProxyConfig::for_error_testing( proxy_port, Arc::new(PassthroughCredentialsResolver { - credentials: dsdk_facet_core::proxy::s3::S3Credentials { + credentials: S3Credentials { access_key_id: "test-key".to_string(), secret_key: "test-secret".to_string(), region: "us-east-1".to_string(), @@ -146,7 +149,7 @@ async fn test_operation_parser_failure_does_not_leak_details() { launch_s3proxy(ProxyConfig::for_error_testing( proxy_port, Arc::new(PassthroughCredentialsResolver { - credentials: dsdk_facet_core::proxy::s3::S3Credentials { + credentials: S3Credentials { access_key_id: "test-key".to_string(), secret_key: "test-secret".to_string(), region: "us-east-1".to_string(), @@ -249,7 +252,7 @@ async fn test_jwt_verification_failure_message_is_generic() { launch_s3proxy(ProxyConfig::for_error_testing( proxy_port, Arc::new(PassthroughCredentialsResolver { - credentials: dsdk_facet_core::proxy::s3::S3Credentials { + credentials: S3Credentials { access_key_id: "test-key".to_string(), secret_key: "test-secret".to_string(), region: "us-east-1".to_string(), diff --git a/crates/facet-core/tests/proxy_s3_token.rs b/crates/facet-proxy/tests/proxy_s3_token.rs similarity index 94% rename from crates/facet-core/tests/proxy_s3_token.rs rename to crates/facet-proxy/tests/proxy_s3_token.rs index 8d3f5d8..e08555b 100644 --- a/crates/facet-core/tests/proxy_s3_token.rs +++ b/crates/facet-proxy/tests/proxy_s3_token.rs @@ -12,14 +12,12 @@ mod common; -use crate::common::{ - MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MinioInstance, ProxyConfig, TEST_BUCKET, TEST_KEY, get_available_port, - launch_s3proxy, -}; +use crate::common::{ProxyConfig, get_available_port, launch_s3proxy}; use aws_config::BehaviorVersion; use aws_sdk_s3::Client; use aws_sdk_s3::config::{Credentials, Region}; -use dsdk_facet_core::proxy::s3::UpstreamStyle; +use dsdk_facet_proxy::s3::UpstreamStyle; +use dsdk_facet_testcontainers::minio::{MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MinioInstance, TEST_BUCKET, TEST_KEY}; const TEST_CONTENT: &str = "Hello from Pingora proxy test!"; const VALID_SESSION_TOKEN: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; diff --git a/crates/facet-testcontainers/Cargo.toml b/crates/facet-testcontainers/Cargo.toml new file mode 100644 index 0000000..a6baf23 --- /dev/null +++ b/crates/facet-testcontainers/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "dsdk-facet-testcontainers" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +tokio = {workspace = true} +reqwest = {workspace = true} +serde = { workspace = true } +serde_json = { workspace = true } +testcontainers = { workspace = true } +testcontainers-modules = { workspace = true, features = ["hashicorp_vault"] } +aws-sdk-s3 = { workspace = true } +aws-config = { workspace = true } +chrono = { workspace = true } +sqlx = {workspace = true} +uuid = { workspace = true } diff --git a/crates/facet-core/tests/common/keycloak.rs b/crates/facet-testcontainers/src/keycloak.rs similarity index 99% rename from crates/facet-core/tests/common/keycloak.rs rename to crates/facet-testcontainers/src/keycloak.rs index 24e3f18..a8098d1 100644 --- a/crates/facet-core/tests/common/keycloak.rs +++ b/crates/facet-testcontainers/src/keycloak.rs @@ -147,7 +147,7 @@ pub async fn setup_keycloak_container(network: &str) -> (KeycloakSetup, testcont let ready = tokio::time::timeout(tokio::time::Duration::from_secs(30), async { loop { if client - .get(&format!("{}/realms/master", keycloak_url)) + .get(format!("{}/realms/master", keycloak_url)) .send() .await .is_ok() diff --git a/crates/facet-testcontainers/src/lib.rs b/crates/facet-testcontainers/src/lib.rs new file mode 100644 index 0000000..21bc613 --- /dev/null +++ b/crates/facet-testcontainers/src/lib.rs @@ -0,0 +1,5 @@ +pub mod keycloak; +pub mod minio; +pub mod postgres; +pub mod utils; +pub mod vault; diff --git a/crates/facet-core/tests/common/minio.rs b/crates/facet-testcontainers/src/minio.rs similarity index 97% rename from crates/facet-core/tests/common/minio.rs rename to crates/facet-testcontainers/src/minio.rs index e62052f..94d64bf 100644 --- a/crates/facet-core/tests/common/minio.rs +++ b/crates/facet-testcontainers/src/minio.rs @@ -131,10 +131,10 @@ impl MinioInstance { /// Verify object content matches the expected value bypassing the proxy. pub async fn verify_object_content(&self, bucket: &str, key: &str, expected: &[u8]) -> bool { let client = create_direct_minio_client(&self.endpoint).await; - if let Ok(response) = client.get_object().bucket(bucket).key(key).send().await { - if let Ok(body) = response.body.collect().await { - return body.to_vec() == expected; - } + if let Ok(response) = client.get_object().bucket(bucket).key(key).send().await + && let Ok(body) = response.body.collect().await + { + return body.to_vec() == expected; } false } diff --git a/crates/facet-core/tests/common/postgres.rs b/crates/facet-testcontainers/src/postgres.rs similarity index 100% rename from crates/facet-core/tests/common/postgres.rs rename to crates/facet-testcontainers/src/postgres.rs diff --git a/crates/facet-testcontainers/src/utils.rs b/crates/facet-testcontainers/src/utils.rs new file mode 100644 index 0000000..536a90f --- /dev/null +++ b/crates/facet-testcontainers/src/utils.rs @@ -0,0 +1,67 @@ +use testcontainers::bollard::{Docker, secret::NetworkCreateRequest}; + +/// Creates a Docker network and returns its name. +/// +/// Automatically cleans up old test networks matching the pattern "test-network-*" +/// to prevent Docker address pool exhaustion. Networks are not automatically cleaned +/// up by testcontainers when containers drop, so we must manually remove them. +/// +/// Note: Rust Testcontainers lacks network creation functionality (which is available in Go and Java). +pub async fn create_network() -> String { + // Try to connect to Docker using the socket path from DOCKER_HOST env var, + // or fall back to platform-specific defaults + let docker = if let Ok(docker_host) = std::env::var("DOCKER_HOST") { + Docker::connect_with_unix(&docker_host, 120, testcontainers::bollard::API_DEFAULT_VERSION) + .expect("Failed to connect to Docker via DOCKER_HOST") + } else if cfg!(target_os = "macos") { + // On macOS with Docker Desktop, socket is typically at ~/.docker/run/docker.sock + let home = std::env::var("HOME").expect("HOME env var not set"); + let socket_path = format!("{}/.docker/run/docker.sock", home); + Docker::connect_with_unix(&socket_path, 120, testcontainers::bollard::API_DEFAULT_VERSION).unwrap_or_else( + |_| { + // Fall back to default if custom path doesn't work + Docker::connect_with_local_defaults().expect("Failed to connect to Docker") + }, + ) + } else { + Docker::connect_with_local_defaults().expect("Failed to connect to Docker") + }; + + // Clean up old test networks before creating a new one + cleanup_old_test_networks(&docker).await; + + let network_name = format!("test-network-{}", uuid::Uuid::new_v4()); + + let config = NetworkCreateRequest { + name: network_name.clone(), + ..Default::default() + }; + + docker + .create_network(config) + .await + .expect("Failed to create Docker network"); + + network_name +} + +/// Cleans up old test networks to prevent address pool exhaustion +async fn cleanup_old_test_networks(docker: &Docker) { + use testcontainers::bollard::query_parameters::ListNetworksOptions; + + // List all networks (best effort - ignore errors) + let networks = match docker.list_networks(Option::::None).await { + Ok(networks) => networks, + Err(_) => return, + }; + + for network in networks { + if let Some(name) = &network.name { + // Clean up networks matching our test pattern + if name.starts_with("test-network-") { + // Best effort cleanup - ignore errors + let _ = docker.remove_network(name).await; + } + } + } +} diff --git a/crates/facet-core/tests/common/vault.rs b/crates/facet-testcontainers/src/vault.rs similarity index 92% rename from crates/facet-core/tests/common/vault.rs rename to crates/facet-testcontainers/src/vault.rs index 710438e..e32725e 100644 --- a/crates/facet-core/tests/common/vault.rs +++ b/crates/facet-testcontainers/src/vault.rs @@ -44,7 +44,7 @@ pub async fn setup_vault_container_with_ttl( // Wait for Vault to be ready for _ in 0..30 { - if client.get(&format!("{}/v1/sys/health", vault_url)).send().await.is_ok() { + if client.get(format!("{}/v1/sys/health", vault_url)).send().await.is_ok() { break; } tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; @@ -52,7 +52,7 @@ pub async fn setup_vault_container_with_ttl( // Enable JWT auth method let enable_jwt = client - .post(&format!("{}/v1/sys/auth/jwt", vault_url)) + .post(format!("{}/v1/sys/auth/jwt", vault_url)) .header("X-Vault-Token", root_token) .json(&json!({ "type": "jwt" @@ -69,7 +69,7 @@ pub async fn setup_vault_container_with_ttl( // Configure JWT auth with Keycloak JWKS URL let config_jwt = client - .post(&format!("{}/v1/auth/jwt/config", vault_url)) + .post(format!("{}/v1/auth/jwt/config", vault_url)) .header("X-Vault-Token", root_token) .json(&json!({ "jwks_url": keycloak_jwks_url, @@ -87,7 +87,7 @@ pub async fn setup_vault_container_with_ttl( // Create a policy for secret access let create_policy = client - .put(&format!("{}/v1/sys/policy/test-policy", vault_url)) + .put(format!("{}/v1/sys/policy/test-policy", vault_url)) .header("X-Vault-Token", root_token) .json(&json!({ "policy": "path \"secret/*\" {\n capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\"]\n}" @@ -105,7 +105,7 @@ pub async fn setup_vault_container_with_ttl( // Create a role for JWT authentication matching Keycloak token structure let keycloak_issuer = format!("http://{}:8080/realms/master", keycloak_container_id); let create_role = client - .post(&format!("{}/v1/auth/jwt/role/provisioner", vault_url)) + .post(format!("{}/v1/auth/jwt/role/provisioner", vault_url)) .header("X-Vault-Token", root_token) .json(&json!({ "role_type": "jwt",