#[cfg(test)]
use std::sync::{LazyLock, Mutex};
use std::{
fmt,
io::Error as IoError,
net::{AddrParseError, SocketAddr},
num::{NonZeroU64, NonZeroUsize, ParseIntError},
path::{Path, PathBuf},
thread::available_parallelism,
};
mod env;
mod secrets;
use shardline_protocol::{SecretBytes, SecretString};
use shardline_storage::S3ObjectStoreConfig;
use thiserror::Error;
use crate::{
reconstruction_cache::{
DEFAULT_RECONSTRUCTION_CACHE_MEMORY_MAX_ENTRIES, DEFAULT_RECONSTRUCTION_CACHE_TTL_SECONDS,
ReconstructionCacheAdapter,
},
server_frontend::ServerFrontend,
server_role::ServerRole,
};
use secrets::ensure_secret_size_within_limit;
#[cfg(test)]
use secrets::{
PendingS3ObjectStoreConfig, configure_s3_object_store_config, optional_s3_secret_from_sources,
};
#[cfg(test)]
use secrets::{configure_provider_runtime_from_paths, read_secret_file_bytes};
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct AuthConfig {
pub(crate) token_signing_key: Option<SecretBytes>,
pub(crate) auth_provider: AuthProviderKind,
pub(crate) auth_oidc_issuer: Option<String>,
pub(crate) auth_jwks_url: Option<String>,
pub(crate) auth_jwks_issuer: Option<String>,
}
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct OciConfig {
pub(crate) upload_session_ttl_seconds: NonZeroU64,
pub(crate) upload_max_active_sessions: NonZeroUsize,
pub(crate) registry_token_ttl_seconds: NonZeroU64,
pub(crate) registry_token_max_in_flight_requests: NonZeroUsize,
}
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct CacheConfig {
pub(crate) adapter: ReconstructionCacheAdapter,
pub(crate) ttl_seconds: NonZeroU64,
pub(crate) memory_max_entries: NonZeroUsize,
pub(crate) redis_url: Option<SecretString>,
}
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct ProviderConfig {
pub(crate) config_path: Option<PathBuf>,
pub(crate) api_key: Option<SecretBytes>,
pub(crate) token_issuer: Option<String>,
pub(crate) token_ttl_seconds: Option<NonZeroU64>,
}
#[derive(Clone, PartialEq, Eq)]
pub struct ServerConfig {
bind_addr: SocketAddr,
server_role: ServerRole,
server_frontends: Vec<ServerFrontend>,
public_base_url: String,
root_dir: PathBuf,
object_storage_adapter: ObjectStorageAdapter,
s3_object_store_config: Option<S3ObjectStoreConfig>,
max_request_body_bytes: NonZeroUsize,
shard_metadata_limits: ShardMetadataLimits,
chunk_size: NonZeroUsize,
upload_max_in_flight_chunks: NonZeroUsize,
transfer_max_in_flight_chunks: NonZeroUsize,
index_postgres_url: Option<SecretString>,
metrics_token: Option<SecretBytes>,
auth: AuthConfig,
oci: OciConfig,
cache: CacheConfig,
provider: ProviderConfig,
}
impl fmt::Debug for ServerConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ServerConfig")
.field("bind_addr", &self.bind_addr)
.field("server_role", &self.server_role)
.field("server_frontends", &self.server_frontends)
.field("public_base_url", &self.public_base_url)
.field("root_dir", &self.root_dir)
.field("object_storage_adapter", &self.object_storage_adapter)
.field("s3_object_store_config", &self.s3_object_store_config)
.field("max_request_body_bytes", &self.max_request_body_bytes)
.field("shard_metadata_limits", &self.shard_metadata_limits)
.field("chunk_size", &self.chunk_size)
.field(
"upload_max_in_flight_chunks",
&self.upload_max_in_flight_chunks,
)
.field(
"transfer_max_in_flight_chunks",
&self.transfer_max_in_flight_chunks,
)
.field(
"index_postgres_url",
&self.index_postgres_url.as_ref().map(|_url| "***"),
)
.field(
"metrics_token",
&self.metrics_token.as_ref().map(|_token| "***"),
)
.field("auth", &self.auth)
.field("oci", &self.oci)
.field("cache", &self.cache)
.field("provider", &self.provider)
.finish()
}
}
impl fmt::Debug for AuthConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("AuthConfig")
.field(
"token_signing_key",
&self.token_signing_key.as_ref().map(|_key| "***"),
)
.field("auth_provider", &self.auth_provider)
.field("auth_oidc_issuer", &self.auth_oidc_issuer)
.field("auth_jwks_url", &self.auth_jwks_url)
.field("auth_jwks_issuer", &self.auth_jwks_issuer)
.finish()
}
}
impl fmt::Debug for OciConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OciConfig")
.field(
"upload_session_ttl_seconds",
&self.upload_session_ttl_seconds,
)
.field(
"upload_max_active_sessions",
&self.upload_max_active_sessions,
)
.field(
"registry_token_ttl_seconds",
&self.registry_token_ttl_seconds,
)
.field(
"registry_token_max_in_flight_requests",
&self.registry_token_max_in_flight_requests,
)
.finish()
}
}
impl fmt::Debug for CacheConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CacheConfig")
.field("adapter", &self.adapter)
.field("ttl_seconds", &self.ttl_seconds)
.field("memory_max_entries", &self.memory_max_entries)
.field("redis_url", &self.redis_url.as_ref().map(|_url| "***"))
.finish()
}
}
impl fmt::Debug for ProviderConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ProviderConfig")
.field("config_path", &self.config_path)
.field("api_key", &self.api_key.as_ref().map(|_key| "***"))
.field("token_issuer", &self.token_issuer)
.field("token_ttl_seconds", &self.token_ttl_seconds)
.finish()
}
}
const MIN_DEFAULT_TRANSFER_MAX_IN_FLIGHT_CHUNKS: NonZeroUsize = match NonZeroUsize::new(64) {
Some(value) => value,
None => NonZeroUsize::MIN,
};
const MAX_DEFAULT_TRANSFER_MAX_IN_FLIGHT_CHUNKS: NonZeroUsize = match NonZeroUsize::new(1024) {
Some(value) => value,
None => NonZeroUsize::MIN,
};
const MIN_DEFAULT_UPLOAD_MAX_IN_FLIGHT_CHUNKS: NonZeroUsize = match NonZeroUsize::new(64) {
Some(value) => value,
None => NonZeroUsize::MIN,
};
const MAX_DEFAULT_UPLOAD_MAX_IN_FLIGHT_CHUNKS: NonZeroUsize = match NonZeroUsize::new(256) {
Some(value) => value,
None => NonZeroUsize::MIN,
};
const DEFAULT_MAX_REQUEST_BODY_BYTES: NonZeroUsize = match NonZeroUsize::new(67_108_864) {
Some(value) => value,
None => NonZeroUsize::MIN,
};
const DEFAULT_MAX_SHARD_FILES: NonZeroUsize = match NonZeroUsize::new(16_384) {
Some(value) => value,
None => NonZeroUsize::MIN,
};
const DEFAULT_MAX_SHARD_XORBS: NonZeroUsize = match NonZeroUsize::new(16_384) {
Some(value) => value,
None => NonZeroUsize::MIN,
};
const DEFAULT_MAX_SHARD_RECONSTRUCTION_TERMS: NonZeroUsize = match NonZeroUsize::new(65_536) {
Some(value) => value,
None => NonZeroUsize::MIN,
};
const DEFAULT_MAX_SHARD_XORB_CHUNKS: NonZeroUsize = match NonZeroUsize::new(65_536) {
Some(value) => value,
None => NonZeroUsize::MIN,
};
const MAX_TOKEN_SIGNING_KEY_BYTES: u64 = 1_048_576;
const MAX_PROVIDER_API_KEY_BYTES: u64 = 4096;
const MAX_METRICS_TOKEN_BYTES: u64 = 4096;
const MAX_S3_CREDENTIAL_BYTES: u64 = 4096;
const DEFAULT_PARALLELISM_FALLBACK: NonZeroUsize = match NonZeroUsize::new(8) {
Some(value) => value,
None => NonZeroUsize::MIN,
};
const DEFAULT_OCI_UPLOAD_SESSION_TTL_SECONDS: NonZeroU64 = match NonZeroU64::new(3_600) {
Some(value) => value,
None => NonZeroU64::MIN,
};
const DEFAULT_OCI_UPLOAD_MAX_ACTIVE_SESSIONS: NonZeroUsize = match NonZeroUsize::new(1_024) {
Some(value) => value,
None => NonZeroUsize::MIN,
};
const DEFAULT_OCI_REGISTRY_TOKEN_TTL_SECONDS: NonZeroU64 = match NonZeroU64::new(300) {
Some(value) => value,
None => NonZeroU64::MIN,
};
const DEFAULT_OCI_REGISTRY_TOKEN_MAX_IN_FLIGHT_REQUESTS: NonZeroUsize = match NonZeroUsize::new(64)
{
Some(value) => value,
None => NonZeroUsize::MIN,
};
#[cfg(test)]
type SecretFileReadHook = Box<dyn FnOnce() + Send>;
#[cfg(test)]
struct SecretFileReadHookRegistration {
path: PathBuf,
hook: SecretFileReadHook,
}
#[cfg(test)]
type SecretFileReadHookSlot = Vec<SecretFileReadHookRegistration>;
#[cfg(test)]
static BEFORE_SECRET_FILE_READ_HOOK: LazyLock<Mutex<SecretFileReadHookSlot>> =
LazyLock::new(|| Mutex::new(Vec::new()));
pub use shardline_server_core::DEFAULT_SHARD_METADATA_LIMITS;
impl ServerConfig {
#[must_use]
pub fn new(
bind_addr: SocketAddr,
public_base_url: String,
root_dir: PathBuf,
chunk_size: NonZeroUsize,
) -> Self {
Self {
bind_addr,
server_role: ServerRole::All,
server_frontends: vec![ServerFrontend::Xet],
public_base_url,
root_dir,
object_storage_adapter: ObjectStorageAdapter::Local,
s3_object_store_config: None,
max_request_body_bytes: DEFAULT_MAX_REQUEST_BODY_BYTES,
shard_metadata_limits: DEFAULT_SHARD_METADATA_LIMITS,
chunk_size,
upload_max_in_flight_chunks: default_upload_max_in_flight_chunks(),
transfer_max_in_flight_chunks: default_transfer_max_in_flight_chunks(),
index_postgres_url: None,
metrics_token: None,
auth: AuthConfig {
token_signing_key: None,
auth_provider: AuthProviderKind::Local,
auth_oidc_issuer: None,
auth_jwks_url: None,
auth_jwks_issuer: None,
},
oci: OciConfig {
upload_session_ttl_seconds: DEFAULT_OCI_UPLOAD_SESSION_TTL_SECONDS,
upload_max_active_sessions: DEFAULT_OCI_UPLOAD_MAX_ACTIVE_SESSIONS,
registry_token_ttl_seconds: DEFAULT_OCI_REGISTRY_TOKEN_TTL_SECONDS,
registry_token_max_in_flight_requests:
DEFAULT_OCI_REGISTRY_TOKEN_MAX_IN_FLIGHT_REQUESTS,
},
cache: CacheConfig {
adapter: ReconstructionCacheAdapter::Memory,
ttl_seconds: DEFAULT_RECONSTRUCTION_CACHE_TTL_SECONDS,
memory_max_entries: DEFAULT_RECONSTRUCTION_CACHE_MEMORY_MAX_ENTRIES,
redis_url: None,
},
provider: ProviderConfig {
config_path: None,
api_key: None,
token_issuer: None,
token_ttl_seconds: None,
},
}
}
pub fn from_env() -> Result<Self, ServerConfigError> {
env::load_server_config_from_env()
}
#[must_use]
pub const fn bind_addr(&self) -> SocketAddr {
self.bind_addr
}
#[must_use]
pub const fn server_role(&self) -> ServerRole {
self.server_role
}
#[must_use]
pub fn server_frontends(&self) -> &[ServerFrontend] {
&self.server_frontends
}
#[must_use]
pub fn public_base_url(&self) -> &str {
&self.public_base_url
}
#[must_use]
pub fn root_dir(&self) -> &Path {
&self.root_dir
}
#[must_use]
pub(crate) const fn object_storage_adapter(&self) -> ObjectStorageAdapter {
self.object_storage_adapter
}
#[must_use]
pub(crate) const fn s3_object_store_config(&self) -> Option<&S3ObjectStoreConfig> {
self.s3_object_store_config.as_ref()
}
#[must_use]
pub const fn max_request_body_bytes(&self) -> NonZeroUsize {
self.max_request_body_bytes
}
#[must_use]
pub const fn shard_metadata_limits(&self) -> ShardMetadataLimits {
self.shard_metadata_limits
}
#[must_use]
pub const fn chunk_size(&self) -> NonZeroUsize {
self.chunk_size
}
#[must_use]
pub const fn with_chunk_size(mut self, chunk_size: NonZeroUsize) -> Self {
self.chunk_size = chunk_size;
self
}
#[must_use]
pub const fn upload_max_in_flight_chunks(&self) -> NonZeroUsize {
self.upload_max_in_flight_chunks
}
#[must_use]
pub const fn transfer_max_in_flight_chunks(&self) -> NonZeroUsize {
self.transfer_max_in_flight_chunks
}
#[must_use]
pub(crate) const fn reconstruction_cache_adapter(&self) -> ReconstructionCacheAdapter {
self.cache.adapter
}
#[must_use]
pub(crate) const fn reconstruction_cache_ttl_seconds(&self) -> NonZeroU64 {
self.cache.ttl_seconds
}
#[must_use]
pub(crate) const fn reconstruction_cache_memory_max_entries(&self) -> NonZeroUsize {
self.cache.memory_max_entries
}
#[must_use]
pub(crate) const fn oci_upload_session_ttl_seconds(&self) -> NonZeroU64 {
self.oci.upload_session_ttl_seconds
}
#[must_use]
pub(crate) const fn oci_upload_max_active_sessions(&self) -> NonZeroUsize {
self.oci.upload_max_active_sessions
}
#[must_use]
pub(crate) const fn oci_registry_token_ttl_seconds(&self) -> NonZeroU64 {
self.oci.registry_token_ttl_seconds
}
#[must_use]
pub(crate) const fn oci_registry_token_max_in_flight_requests(&self) -> NonZeroUsize {
self.oci.registry_token_max_in_flight_requests
}
#[must_use]
pub(crate) fn reconstruction_cache_redis_url(&self) -> Option<&str> {
self.cache
.redis_url
.as_ref()
.map(SecretString::expose_secret)
}
#[must_use]
pub fn with_root_dir(mut self, root_dir: PathBuf) -> Self {
self.root_dir = root_dir;
self
}
#[must_use]
pub fn with_object_storage(
mut self,
adapter: ObjectStorageAdapter,
s3_config: Option<S3ObjectStoreConfig>,
) -> Self {
self.object_storage_adapter = adapter;
self.s3_object_store_config = s3_config;
self
}
#[must_use]
pub const fn with_max_request_body_bytes(
mut self,
max_request_body_bytes: NonZeroUsize,
) -> Self {
self.max_request_body_bytes = max_request_body_bytes;
self
}
#[must_use]
pub const fn with_shard_metadata_limits(
mut self,
shard_metadata_limits: ShardMetadataLimits,
) -> Self {
self.shard_metadata_limits = shard_metadata_limits;
self
}
#[must_use]
pub const fn with_server_role(mut self, server_role: ServerRole) -> Self {
self.server_role = server_role;
self
}
pub fn with_server_frontends(
mut self,
server_frontends: impl IntoIterator<Item = ServerFrontend>,
) -> Result<Self, ServerConfigError> {
let server_frontends = deduplicated_server_frontends(server_frontends);
if server_frontends.is_empty() {
return Err(ServerConfigError::MissingServerFrontends);
}
self.server_frontends = server_frontends;
Ok(self)
}
#[must_use]
pub const fn with_upload_max_in_flight_chunks(
mut self,
upload_max_in_flight_chunks: NonZeroUsize,
) -> Self {
self.upload_max_in_flight_chunks = upload_max_in_flight_chunks;
self
}
#[must_use]
pub const fn with_transfer_max_in_flight_chunks(
mut self,
transfer_max_in_flight_chunks: NonZeroUsize,
) -> Self {
self.transfer_max_in_flight_chunks = transfer_max_in_flight_chunks;
self
}
#[must_use]
pub fn with_reconstruction_cache_disabled(mut self) -> Self {
self.cache.adapter = ReconstructionCacheAdapter::Disabled;
self.cache.redis_url = None;
self
}
#[must_use]
pub fn with_reconstruction_cache_memory(
mut self,
reconstruction_cache_ttl_seconds: NonZeroU64,
reconstruction_cache_memory_max_entries: NonZeroUsize,
) -> Self {
self.cache.adapter = ReconstructionCacheAdapter::Memory;
self.cache.ttl_seconds = reconstruction_cache_ttl_seconds;
self.cache.memory_max_entries = reconstruction_cache_memory_max_entries;
self.cache.redis_url = None;
self
}
#[must_use]
pub const fn with_oci_upload_session_ttl_seconds(
mut self,
oci_upload_session_ttl_seconds: NonZeroU64,
) -> Self {
self.oci.upload_session_ttl_seconds = oci_upload_session_ttl_seconds;
self
}
#[must_use]
pub const fn with_oci_upload_max_active_sessions(
mut self,
oci_upload_max_active_sessions: NonZeroUsize,
) -> Self {
self.oci.upload_max_active_sessions = oci_upload_max_active_sessions;
self
}
#[must_use]
pub const fn with_oci_registry_token_ttl_seconds(
mut self,
oci_registry_token_ttl_seconds: NonZeroU64,
) -> Self {
self.oci.registry_token_ttl_seconds = oci_registry_token_ttl_seconds;
self
}
#[must_use]
pub const fn with_oci_registry_token_max_in_flight_requests(
mut self,
oci_registry_token_max_in_flight_requests: NonZeroUsize,
) -> Self {
self.oci.registry_token_max_in_flight_requests = oci_registry_token_max_in_flight_requests;
self
}
pub fn with_reconstruction_cache_redis(
mut self,
reconstruction_cache_redis_url: String,
reconstruction_cache_ttl_seconds: NonZeroU64,
) -> Result<Self, ServerConfigError> {
if reconstruction_cache_redis_url.trim().is_empty() {
return Err(ServerConfigError::EmptyReconstructionCacheRedisUrl);
}
self.cache.adapter = ReconstructionCacheAdapter::Redis;
self.cache.ttl_seconds = reconstruction_cache_ttl_seconds;
self.cache.redis_url = Some(SecretString::new(reconstruction_cache_redis_url));
Ok(self)
}
#[must_use]
pub fn index_postgres_url(&self) -> Option<&str> {
self.index_postgres_url
.as_ref()
.map(SecretString::expose_secret)
}
#[must_use]
pub const fn auth_provider(&self) -> AuthProviderKind {
self.auth.auth_provider
}
#[must_use]
pub fn auth_oidc_issuer(&self) -> Option<&str> {
self.auth.auth_oidc_issuer.as_deref()
}
#[must_use]
pub fn auth_jwks_url(&self) -> Option<&str> {
self.auth.auth_jwks_url.as_deref()
}
#[must_use]
pub fn auth_jwks_issuer(&self) -> Option<&str> {
self.auth.auth_jwks_issuer.as_deref()
}
#[must_use]
pub fn token_signing_key(&self) -> Option<&[u8]> {
self.auth
.token_signing_key
.as_ref()
.map(SecretBytes::expose_secret)
}
#[must_use]
pub fn metrics_token(&self) -> Option<&[u8]> {
self.metrics_token.as_ref().map(SecretBytes::expose_secret)
}
#[must_use]
pub fn provider_config_path(&self) -> Option<&Path> {
self.provider.config_path.as_deref()
}
#[must_use]
pub fn provider_api_key(&self) -> Option<&[u8]> {
self.provider
.api_key
.as_ref()
.map(SecretBytes::expose_secret)
}
#[must_use]
pub fn provider_token_issuer(&self) -> Option<&str> {
self.provider.token_issuer.as_deref()
}
#[must_use]
pub const fn provider_token_ttl_seconds(&self) -> Option<NonZeroU64> {
self.provider.token_ttl_seconds
}
pub fn with_index_postgres_url(
mut self,
index_postgres_url: String,
) -> Result<Self, ServerConfigError> {
if index_postgres_url.trim().is_empty() {
return Err(ServerConfigError::EmptyIndexPostgresUrl);
}
self.index_postgres_url = Some(SecretString::new(index_postgres_url));
Ok(self)
}
pub fn with_token_signing_key(
mut self,
token_signing_key: Vec<u8>,
) -> Result<Self, ServerConfigError> {
if token_signing_key.is_empty() {
return Err(ServerConfigError::EmptyTokenSigningKey);
}
ensure_secret_size_within_limit(
u64::try_from(token_signing_key.len()).unwrap_or(u64::MAX),
MAX_TOKEN_SIGNING_KEY_BYTES,
|observed_bytes, maximum_bytes| ServerConfigError::TokenSigningKeyTooLarge {
observed_bytes,
maximum_bytes,
},
)?;
self.auth.token_signing_key = Some(SecretBytes::new(token_signing_key));
Ok(self)
}
#[must_use]
pub const fn with_auth_provider(mut self, auth_provider: AuthProviderKind) -> Self {
self.auth.auth_provider = auth_provider;
self
}
#[must_use]
pub fn with_auth_oidc_issuer(mut self, issuer: String) -> Self {
self.auth.auth_oidc_issuer = Some(issuer);
self
}
#[must_use]
pub fn with_auth_jwks_url(mut self, url: String) -> Self {
self.auth.auth_jwks_url = Some(url);
self
}
#[must_use]
pub fn with_auth_jwks_issuer(mut self, issuer: String) -> Self {
self.auth.auth_jwks_issuer = Some(issuer);
self
}
pub fn with_metrics_token(mut self, metrics_token: Vec<u8>) -> Result<Self, ServerConfigError> {
if metrics_token.is_empty() {
return Err(ServerConfigError::EmptyMetricsToken);
}
ensure_secret_size_within_limit(
u64::try_from(metrics_token.len()).unwrap_or(u64::MAX),
MAX_METRICS_TOKEN_BYTES,
|observed_bytes, maximum_bytes| ServerConfigError::MetricsTokenTooLarge {
observed_bytes,
maximum_bytes,
},
)?;
self.metrics_token = Some(SecretBytes::new(metrics_token));
Ok(self)
}
pub fn with_provider_runtime(
mut self,
provider_config_path: PathBuf,
provider_api_key: Vec<u8>,
issuer_identity: String,
ttl_seconds: NonZeroU64,
) -> Result<Self, ServerConfigError> {
if provider_api_key.is_empty() {
return Err(ServerConfigError::EmptyProviderApiKey);
}
ensure_secret_size_within_limit(
u64::try_from(provider_api_key.len()).unwrap_or(u64::MAX),
MAX_PROVIDER_API_KEY_BYTES,
|observed_bytes, maximum_bytes| ServerConfigError::ProviderApiKeyTooLarge {
observed_bytes,
maximum_bytes,
},
)?;
if issuer_identity.trim().is_empty() {
return Err(ServerConfigError::EmptyProviderTokenIssuer);
}
if self.auth.token_signing_key.is_none() {
return Err(ServerConfigError::ProviderTokensRequireSigningKey);
}
self.provider.config_path = Some(provider_config_path);
self.provider.api_key = Some(SecretBytes::new(provider_api_key));
self.provider.token_issuer = Some(issuer_identity);
self.provider.token_ttl_seconds = Some(ttl_seconds);
Ok(self)
}
pub const fn validate_runtime_requirements(&self) -> Result<(), ServerConfigError> {
if self.auth.token_signing_key.is_none()
&& (self.server_role.serves_api() || self.server_role.serves_transfer())
{
return Err(ServerConfigError::MissingTokenSigningKeyForServedRoutes);
}
Ok(())
}
}
#[must_use]
pub(crate) fn default_upload_max_in_flight_chunks() -> NonZeroUsize {
adaptive_default_in_flight_chunks(
2,
MIN_DEFAULT_UPLOAD_MAX_IN_FLIGHT_CHUNKS,
MAX_DEFAULT_UPLOAD_MAX_IN_FLIGHT_CHUNKS,
)
}
#[must_use]
pub(crate) fn default_transfer_max_in_flight_chunks() -> NonZeroUsize {
adaptive_default_in_flight_chunks(
8,
MIN_DEFAULT_TRANSFER_MAX_IN_FLIGHT_CHUNKS,
MAX_DEFAULT_TRANSFER_MAX_IN_FLIGHT_CHUNKS,
)
}
fn deduplicated_server_frontends(
server_frontends: impl IntoIterator<Item = ServerFrontend>,
) -> Vec<ServerFrontend> {
let mut deduplicated = Vec::new();
for frontend in server_frontends {
if !deduplicated.contains(&frontend) {
deduplicated.push(frontend);
}
}
deduplicated
}
fn adaptive_default_in_flight_chunks(
multiplier: usize,
minimum: NonZeroUsize,
maximum: NonZeroUsize,
) -> NonZeroUsize {
let parallelism = available_parallelism().unwrap_or(DEFAULT_PARALLELISM_FALLBACK);
adaptive_default_in_flight_chunks_for_parallelism(
parallelism.get(),
multiplier,
minimum,
maximum,
)
}
fn adaptive_default_in_flight_chunks_for_parallelism(
parallelism: usize,
multiplier: usize,
minimum: NonZeroUsize,
maximum: NonZeroUsize,
) -> NonZeroUsize {
let scaled = parallelism.saturating_mul(multiplier);
let bounded = scaled.clamp(minimum.get(), maximum.get());
NonZeroUsize::new(bounded).unwrap_or(minimum)
}
pub use shardline_server_core::ShardMetadataLimits;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthProviderKind {
Local,
Oidc,
Jwks,
Passthrough,
}
impl AuthProviderKind {
pub fn parse(value: &str) -> Result<Self, ServerConfigError> {
match value {
"local" => Ok(Self::Local),
"oidc" => Ok(Self::Oidc),
"jwks" => Ok(Self::Jwks),
"passthrough" => Ok(Self::Passthrough),
_other => Err(ServerConfigError::InvalidAuthProvider),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjectStorageAdapter {
Local,
S3,
}
impl ObjectStorageAdapter {
pub fn parse(value: &str) -> Result<Self, ServerConfigError> {
match value {
"local" => Ok(Self::Local),
"s3" => Ok(Self::S3),
_other => Err(ServerConfigError::InvalidObjectStorageAdapter),
}
}
}
#[derive(Debug, Error)]
pub enum ServerConfigError {
#[error("invalid bind address")]
BindAddress(#[from] AddrParseError),
#[error("invalid local deployment root")]
RootDir(#[source] IoError),
#[error("invalid server role")]
InvalidServerRole,
#[error("invalid server frontend")]
InvalidServerFrontend,
#[error("at least one server frontend must be enabled")]
MissingServerFrontends,
#[error("invalid object storage adapter")]
InvalidObjectStorageAdapter,
#[error("invalid auth provider")]
InvalidAuthProvider,
#[error("s3 object storage requires SHARDLINE_S3_BUCKET")]
MissingS3Bucket,
#[error("invalid s3 allow-http flag")]
InvalidS3AllowHttp,
#[error("invalid s3 virtual-hosted-style request flag")]
InvalidS3VirtualHostedStyleRequest,
#[error("s3 credential source conflict: both {env} and {file_env} are set")]
S3CredentialSourceConflict {
env: &'static str,
file_env: &'static str,
},
#[error("s3 credential file {name} could not be read")]
S3CredentialFile {
name: &'static str,
#[source]
source: IoError,
},
#[error("s3 credential file {name} exceeded the bounded parser ceiling")]
S3CredentialTooLarge {
name: &'static str,
observed_bytes: u64,
maximum_bytes: u64,
},
#[error("s3 credential file {name} changed during bounded read")]
S3CredentialLengthMismatch {
name: &'static str,
expected_bytes: u64,
observed_bytes: u64,
},
#[error("s3 credential file {name} was not valid utf-8")]
S3CredentialUtf8 {
name: &'static str,
},
#[error("invalid chunk size")]
ChunkSize(#[from] ParseIntError),
#[error("invalid max request body size")]
MaxRequestBodyBytes(ParseIntError),
#[error("max request body size must be greater than zero")]
ZeroMaxRequestBodyBytes,
#[error("invalid max shard file section count")]
MaxShardFiles(ParseIntError),
#[error("max shard file section count must be greater than zero")]
ZeroMaxShardFiles,
#[error("invalid max shard xorb section count")]
MaxShardXorbs(ParseIntError),
#[error("max shard xorb section count must be greater than zero")]
ZeroMaxShardXorbs,
#[error("invalid max shard reconstruction term count")]
MaxShardReconstructionTerms(ParseIntError),
#[error("max shard reconstruction term count must be greater than zero")]
ZeroMaxShardReconstructionTerms,
#[error("invalid max shard xorb chunk record count")]
MaxShardXorbChunks(ParseIntError),
#[error("max shard xorb chunk record count must be greater than zero")]
ZeroMaxShardXorbChunks,
#[error("chunk size must be greater than zero")]
ZeroChunkSize,
#[error("upload max in-flight chunks must be greater than zero")]
ZeroUploadMaxInFlightChunks,
#[error("transfer max in-flight chunks must be greater than zero")]
ZeroTransferMaxInFlightChunks,
#[error("invalid reconstruction cache adapter")]
InvalidReconstructionCacheAdapter,
#[error("reconstruction cache ttl must be greater than zero")]
ZeroReconstructionCacheTtlSeconds,
#[error("reconstruction cache memory max entries must be greater than zero")]
ZeroReconstructionCacheMemoryMaxEntries,
#[error("invalid oci upload session ttl")]
OciUploadSessionTtl(ParseIntError),
#[error("oci upload session ttl must be greater than zero")]
ZeroOciUploadSessionTtlSeconds,
#[error("invalid oci upload max active sessions")]
OciUploadMaxActiveSessions(ParseIntError),
#[error("oci upload max active sessions must be greater than zero")]
ZeroOciUploadMaxActiveSessions,
#[error("invalid oci registry token ttl")]
OciRegistryTokenTtl(ParseIntError),
#[error("oci registry token ttl must be greater than zero")]
ZeroOciRegistryTokenTtlSeconds,
#[error("invalid oci registry token max in-flight requests")]
OciRegistryTokenMaxInFlightRequests(ParseIntError),
#[error("oci registry token max in-flight requests must be greater than zero")]
ZeroOciRegistryTokenMaxInFlightRequests,
#[error("reconstruction cache redis url must not be empty")]
EmptyReconstructionCacheRedisUrl,
#[error("redis reconstruction cache requires SHARDLINE_RECONSTRUCTION_CACHE_REDIS_URL")]
MissingReconstructionCacheRedisUrl,
#[error("postgres metadata url must not be empty")]
EmptyIndexPostgresUrl,
#[error("token signing key could not be read")]
TokenSigningKey(#[source] IoError),
#[error("token signing key source conflict: both {env} and {file_env} are set")]
TokenSigningKeySourceConflict {
env: &'static str,
file_env: &'static str,
},
#[error("token signing key exceeded the bounded parser ceiling")]
TokenSigningKeyTooLarge {
observed_bytes: u64,
maximum_bytes: u64,
},
#[error("token signing key changed during bounded read")]
TokenSigningKeyLengthMismatch {
expected_bytes: u64,
observed_bytes: u64,
},
#[error("invalid provider token ttl")]
ProviderTokenTtl,
#[error("token signing key must not be empty")]
EmptyTokenSigningKey,
#[error("metrics token could not be read")]
MetricsToken(#[source] IoError),
#[error("metrics token must not be empty")]
EmptyMetricsToken,
#[error("metrics token exceeded the bounded parser ceiling")]
MetricsTokenTooLarge {
observed_bytes: u64,
maximum_bytes: u64,
},
#[error("metrics token changed during bounded read")]
MetricsTokenLengthMismatch {
expected_bytes: u64,
observed_bytes: u64,
},
#[error("served shardline routes require shardline token signing key configuration")]
MissingTokenSigningKeyForServedRoutes,
#[error("provider bootstrap key must not be empty")]
EmptyProviderApiKey,
#[error("provider bootstrap key could not be read")]
ProviderApiKey(#[source] IoError),
#[error("provider bootstrap key exceeded the bounded parser ceiling")]
ProviderApiKeyTooLarge {
observed_bytes: u64,
maximum_bytes: u64,
},
#[error("provider bootstrap key changed during bounded read")]
ProviderApiKeyLengthMismatch {
expected_bytes: u64,
observed_bytes: u64,
},
#[error("provider token issuer must not be empty")]
EmptyProviderTokenIssuer,
#[error("provider token ttl must be greater than zero")]
ZeroProviderTokenTtl,
#[error("provider token issuance requires both provider config and provider api key files")]
IncompleteProviderTokenConfig,
#[error("provider token issuance requires shardline token signing key configuration")]
ProviderTokensRequireSigningKey,
#[error("chunk size must not exceed 1 GB")]
ChunkSizeTooLarge,
#[error("SHARDLINE_PUBLIC_BASE_URL is not a valid URL: {0}")]
InvalidPublicBaseUrl(String),
#[error("oidc auth provider requires SHARDLINE_AUTH_OIDC_ISSUER")]
MissingOidcIssuer,
#[error("jwks auth provider requires SHARDLINE_AUTH_JWKS_URL")]
MissingJwksUrl,
#[error(
"hub frontend requires auth configuration (SHARDLINE_AUTH_PROVIDER with token signing key or oidc/jwks)"
)]
HubRequiresAuth,
}
#[cfg(test)]
fn run_before_secret_file_read_hook_for_tests(path: &Path) {
let hook = match BEFORE_SECRET_FILE_READ_HOOK.lock() {
Ok(mut guard) => take_secret_file_read_hook_for_path(&mut guard, path),
Err(poisoned) => take_secret_file_read_hook_for_path(&mut poisoned.into_inner(), path),
};
if let Some(hook) = hook {
hook();
}
}
#[cfg(test)]
fn take_secret_file_read_hook_for_path(
slot: &mut SecretFileReadHookSlot,
path: &Path,
) -> Option<SecretFileReadHook> {
let index = slot
.iter()
.position(|registration| registration.path == path)?;
Some(slot.remove(index).hook)
}
#[cfg(not(test))]
const fn run_before_secret_file_read_hook_for_tests(_path: &Path) {}
#[cfg(test)]
mod tests;