Skip to content

Commit 23db80d

Browse files
committed
ci: integration tests
1 parent 66a26fd commit 23db80d

File tree

5 files changed

+237
-10
lines changed

5 files changed

+237
-10
lines changed

Dockerfile.integration

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Dockerfile for Management API Integration Tests
2+
# Multi-stage build for running management API in integration environment
3+
4+
FROM rustlang/rust:nightly-bookworm-slim AS builder
5+
6+
# Install build dependencies
7+
RUN apt-get update && apt-get install -y \
8+
pkg-config \
9+
libssl-dev \
10+
curl \
11+
wget \
12+
clang \
13+
libclang-dev \
14+
protobuf-compiler \
15+
&& rm -rf /var/lib/apt/lists/*
16+
17+
# Install FoundationDB client libraries (required for compilation)
18+
# Note: FDB uses _aarch64 naming for ARM64, not _arm64
19+
# Version 7.3.69 matches the FDB server in docker-compose
20+
ARG TARGETARCH
21+
RUN echo "Installing FDB clients for architecture: ${TARGETARCH:-amd64}" && \
22+
if [ "$TARGETARCH" = "arm64" ]; then \
23+
wget -q https://github.com/apple/foundationdb/releases/download/7.3.69/foundationdb-clients_7.3.69-1_aarch64.deb -O /tmp/fdb-clients.deb; \
24+
else \
25+
wget -q https://github.com/apple/foundationdb/releases/download/7.3.69/foundationdb-clients_7.3.69-1_amd64.deb -O /tmp/fdb-clients.deb; \
26+
fi && \
27+
dpkg -i /tmp/fdb-clients.deb || (apt-get update && apt-get install -f -y) && \
28+
rm /tmp/fdb-clients.deb && \
29+
ldconfig && \
30+
# Verify installation
31+
ls -la /usr/include/foundationdb/ && \
32+
test -f /usr/include/foundationdb/fdb.options
33+
34+
WORKDIR /workspace
35+
36+
# Copy dependency manifests
37+
COPY Cargo.toml Cargo.lock ./
38+
COPY crates ./crates
39+
40+
# Build dependencies first (cached layer)
41+
RUN cargo build --release --bin inferadb-management
42+
43+
# Final stage - minimal runtime image
44+
FROM debian:bookworm-slim
45+
46+
# Install runtime dependencies
47+
RUN apt-get update && apt-get install -y \
48+
ca-certificates \
49+
libssl3 \
50+
curl \
51+
&& rm -rf /var/lib/apt/lists/*
52+
53+
WORKDIR /app
54+
55+
# Copy built binary
56+
COPY --from=builder /workspace/target/release/inferadb-management /app/inferadb-management
57+
58+
# Copy integration test config file
59+
COPY config.integration.yaml /app/config.yaml
60+
61+
# Expose management API port
62+
EXPOSE 8081
63+
64+
# Health check
65+
HEALTHCHECK --interval=5s --timeout=3s --retries=10 --start-period=15s \
66+
CMD curl -f http://localhost:8081/health || exit 1
67+
68+
# Run management API
69+
CMD ["/app/inferadb-management", "--config", "/app/config.yaml"]

config.integration.yaml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# InferaDB Management API Configuration - Integration Tests
2+
# This config is optimized for Docker container E2E testing
3+
4+
frontend_base_url: "http://localhost:3000"
5+
6+
server:
7+
http_host: "0.0.0.0" # Bind to all interfaces for Docker
8+
http_port: 8081 # Match docker-compose exposed port
9+
grpc_host: "0.0.0.0"
10+
grpc_port: 8082
11+
worker_threads: 2 # Reduced for test environment
12+
13+
storage:
14+
backend: "memory" # In-memory for fast tests
15+
16+
auth:
17+
session_ttl_web: 3600 # 1 hour for tests
18+
session_ttl_cli: 7200 # 2 hours for tests
19+
session_ttl_sdk: 7200 # 2 hours for tests
20+
password_min_length: 8 # Relaxed for tests
21+
max_sessions_per_user: 5
22+
webauthn:
23+
rp_id: "localhost"
24+
rp_name: "InferaDB Test"
25+
origin: "http://localhost:3000"
26+
27+
email:
28+
smtp_host: "localhost"
29+
smtp_port: 1025
30+
from_email: "test@inferadb.local"
31+
from_name: "InferaDB Test"
32+
33+
rate_limiting:
34+
login_attempts_per_ip_per_hour: 1000 # Relaxed for tests
35+
registrations_per_ip_per_day: 100
36+
email_verification_tokens_per_hour: 100
37+
password_reset_tokens_per_hour: 100
38+
39+
observability:
40+
log_level: "debug" # Verbose logging for tests
41+
metrics_enabled: true
42+
tracing_enabled: false
43+
44+
id_generation:
45+
worker_id: 0
46+
max_clock_skew_ms: 5000
47+
48+
server_api:
49+
grpc_endpoint: "http://server:8080" # Docker service name
50+
tls_enabled: false

crates/infera-management-api/src/lib.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
// REST API handlers and routes
22

3+
use infera_management_core::ManagementConfig;
4+
use infera_management_grpc::ServerApiClient;
5+
use infera_management_storage::Backend;
6+
use std::sync::Arc;
7+
use tracing::info;
8+
39
pub mod audit;
410
pub mod handlers;
511
pub mod middleware;
@@ -15,3 +21,79 @@ pub use middleware::{
1521
};
1622
pub use pagination::{Paginated, PaginationMeta, PaginationParams, PaginationQuery};
1723
pub use routes::create_router_with_state;
24+
25+
/// Graceful shutdown signal handler
26+
async fn shutdown_signal() {
27+
use tokio::signal;
28+
29+
let ctrl_c = async {
30+
signal::ctrl_c()
31+
.await
32+
.expect("failed to install Ctrl+C handler");
33+
};
34+
35+
#[cfg(unix)]
36+
let terminate = async {
37+
signal::unix::signal(signal::unix::SignalKind::terminate())
38+
.expect("failed to install SIGTERM handler")
39+
.recv()
40+
.await;
41+
};
42+
43+
#[cfg(not(unix))]
44+
let terminate = std::future::pending::<()>();
45+
46+
tokio::select! {
47+
_ = ctrl_c => {
48+
info!("Received Ctrl+C signal, initiating shutdown");
49+
}
50+
_ = terminate => {
51+
info!("Received SIGTERM signal, initiating shutdown");
52+
}
53+
}
54+
}
55+
56+
/// Start the Management API HTTP server
57+
pub async fn serve(
58+
storage: Arc<Backend>,
59+
config: Arc<ManagementConfig>,
60+
server_client: Arc<ServerApiClient>,
61+
worker_id: u16,
62+
leader: Option<Arc<infera_management_core::LeaderElection<Backend>>>,
63+
email_service: Option<Arc<infera_management_core::EmailService>>,
64+
) -> anyhow::Result<()> {
65+
// Create AppState with services
66+
let state = AppState::new(
67+
storage,
68+
config.clone(),
69+
server_client,
70+
worker_id,
71+
leader,
72+
email_service,
73+
);
74+
75+
let app = create_router_with_state(state);
76+
77+
let addr = format!("{}:{}", config.server.http_host, config.server.http_port);
78+
info!("Starting Management API HTTP server on {}", addr);
79+
80+
let listener = tokio::net::TcpListener::bind(&addr).await?;
81+
82+
// Setup graceful shutdown
83+
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
84+
85+
// Spawn task to handle shutdown signals
86+
tokio::spawn(async move {
87+
shutdown_signal().await;
88+
let _ = shutdown_tx.send(());
89+
});
90+
91+
// Serve with graceful shutdown
92+
axum::serve(listener, app)
93+
.with_graceful_shutdown(async {
94+
shutdown_rx.await.ok();
95+
})
96+
.await?;
97+
98+
Ok(())
99+
}

crates/infera-management-core/src/repository_context.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ use infera_management_storage::StorageBackend;
44
/// Consolidated repository context to reduce boilerplate in handlers.
55
///
66
/// Instead of manually instantiating repositories in every handler:
7-
/// ```rust
7+
/// ```rust,ignore
88
/// let org_repo = OrganizationRepository::new((*state.storage).clone());
99
/// let member_repo = OrganizationMemberRepository::new((*state.storage).clone());
1010
/// let team_repo = OrganizationTeamRepository::new((*state.storage).clone());
1111
/// // ... repeated 10-20 times
1212
/// ```
1313
///
1414
/// You can now use:
15-
/// ```rust
15+
/// ```rust,ignore
1616
/// let repos = RepositoryContext::new(state.storage.clone());
1717
/// repos.org.create(...).await?;
1818
/// repos.member.list(...).await?;
@@ -59,7 +59,7 @@ impl<S: StorageBackend + Clone> RepositoryContext<S> {
5959
/// * `storage` - The storage backend to use for all repositories
6060
///
6161
/// # Example
62-
/// ```rust
62+
/// ```rust,ignore
6363
/// let repos = RepositoryContext::new((*state.storage).clone());
6464
/// let user = repos.user.get(user_id).await?;
6565
/// ```

crates/infera-management/src/main.rs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use anyhow::Result;
22
use clap::Parser;
33
use infera_management_core::{logging, ManagementConfig};
4+
use infera_management_grpc::ServerApiClient;
5+
use infera_management_storage::factory::{create_storage_backend, StorageConfig};
6+
use std::sync::Arc;
47

58
#[derive(Parser, Debug)]
69
#[command(name = "inferadb-management")]
@@ -42,15 +45,38 @@ async fn main() -> Result<()> {
4245
"Starting InferaDB Management API"
4346
);
4447

45-
// TODO: Initialize storage
46-
// TODO: Start HTTP server
47-
// TODO: Start gRPC server
48+
// Initialize storage backend
49+
tracing::info!(backend = %config.storage.backend, "Initializing storage backend");
50+
let storage_config = match config.storage.backend.as_str() {
51+
"memory" => StorageConfig::memory(),
52+
"foundationdb" => StorageConfig::foundationdb(config.storage.fdb_cluster_file.clone()),
53+
_ => anyhow::bail!("Invalid storage backend: {}", config.storage.backend),
54+
};
55+
let storage = Arc::new(create_storage_backend(&storage_config).await?);
56+
tracing::info!("Storage backend initialized successfully");
4857

49-
tracing::info!("Management API started successfully");
58+
// Initialize server API client (for gRPC communication with @server)
59+
tracing::info!(endpoint = %config.server_api.grpc_endpoint, "Initializing server API client");
60+
let server_client = Arc::new(ServerApiClient::new(config.server_api.grpc_endpoint.clone())?);
61+
tracing::info!("Server API client initialized successfully");
5062

51-
// Keep running until interrupted
52-
tokio::signal::ctrl_c().await?;
53-
tracing::info!("Shutting down gracefully");
63+
// Wrap config in Arc for sharing across services
64+
let config = Arc::new(config);
65+
66+
// Start HTTP server
67+
// Note: Leader election and email service are optional for now
68+
// They can be initialized and passed when needed for multi-node deployments
69+
tracing::info!("Starting HTTP server");
70+
infera_management_api::serve(
71+
storage.clone(),
72+
config.clone(),
73+
server_client.clone(),
74+
config.id_generation.worker_id,
75+
None, // leader election (optional, for multi-node)
76+
None, // email service (optional, can be initialized later)
77+
)
78+
.await?;
5479

80+
tracing::info!("Shutting down gracefully");
5581
Ok(())
5682
}

0 commit comments

Comments
 (0)