use self::log::{LoggingConfig, LoggingStyle};
use crate::config::config_holder::{CACHE_DIR_PREFIX, DATA_DIR_PREFIX, PathPrefixes};
use crate::config::deployment::{
AllowedHostToml, ConfigName, DurationConfig, DurationConfigOptional, InflightSemaphore,
ValueOrUnlimited,
};
use crate::config::env_var::{interpolate_env_vars_plaintext, interpolate_env_vars_secret};
use crate::config::secret_registry::{SecretRegistry, SecretsToml};
use concepts::ContentDigest;
use concepts::component_id::Digest;
use db_postgres::postgres_dao::{self, PostgresConfig};
use db_sqlite::sqlite_dao::SqliteConfig;
use schemars::JsonSchema;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Default, Deserialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct ServerConfigToml {
#[serde(skip)]
#[schemars(skip)]
pub(crate) source_path: Option<PathBuf>,
#[serde(default, rename = "obelisk-version")]
pub(crate) obelisk_version: Option<String>,
#[serde(default)]
pub(crate) secrets: SecretsToml,
#[serde(default)]
pub(crate) allow_exec_activities: AllowExecActivities,
#[serde(default)]
pub(crate) outbound_http: OutboundHttpToml,
#[serde(default)]
pub(crate) max_deployment_file_bytes: MaxDeploymentFileBytes,
#[serde(default)]
pub(crate) api: ApiConfig,
#[serde(default)]
pub(crate) database: DatabaseConfigToml,
#[serde(default)]
pub(crate) webui: WebUIConfig,
#[serde(default)]
pub(crate) external: ExternalServerConfig,
#[serde(default, rename = "wasm")]
pub(crate) wasm_global_config: WasmGlobalConfigToml,
#[serde(default, rename = "workflows")]
pub(crate) workflows_global_config: WorkflowsGlobalConfigToml,
#[serde(default)]
pub(crate) timers_watcher: TimersWatcherTomlConfig,
#[serde(default)]
pub(crate) cancel_watcher: CancelWatcherTomlConfig,
#[cfg(feature = "otlp")]
#[serde(default)]
pub(crate) otlp: Option<otlp::OtlpConfig>,
#[serde(default)]
pub(crate) log: LoggingConfig,
#[serde(default, rename = "http_server")]
pub(crate) http_servers: Vec<HttpServer>,
}
#[derive(Debug, Default, Deserialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct OutboundHttpToml {
#[serde(default, rename = "allowed_host")]
pub(crate) allowed_hosts: Vec<AllowedHostToml>,
}
#[derive(Debug, Deserialize, JsonSchema, Clone, Copy)]
#[serde(transparent)]
pub(crate) struct MaxDeploymentFileBytes(pub(crate) u32);
impl Default for MaxDeploymentFileBytes {
fn default() -> Self {
Self(MAX_DEPLOYMENT_FILE_BYTES)
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub(crate) enum AllowExecActivities {
#[default]
Deny,
AllowAny,
Allowlist(BTreeMap<String, ContentDigest>),
LegacyAllowlist(Vec<ContentDigest>),
}
impl<'de> Deserialize<'de> for AllowExecActivities {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct AllowExecActivitiesVisitor;
impl<'de> serde::de::Visitor<'de> for AllowExecActivitiesVisitor {
type Value = AllowExecActivities;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str(
"a boolean or a map from exec activity names to `sha256:...` content digests",
)
}
fn visit_bool<E: serde::de::Error>(self, v: bool) -> Result<Self::Value, E> {
Ok(if v {
AllowExecActivities::AllowAny
} else {
AllowExecActivities::Deny
})
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
match v.parse::<bool>() {
Ok(v) => self.visit_bool(v),
Err(_) => Err(E::invalid_value(serde::de::Unexpected::Str(v), &self)),
}
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(
self,
mut seq: A,
) -> Result<Self::Value, A::Error> {
let mut digests = Vec::new();
while let Some(digest) = seq.next_element::<ContentDigest>()? {
digests.push(digest);
}
Ok(AllowExecActivities::LegacyAllowlist(digests))
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<Self::Value, A::Error> {
let mut digests = BTreeMap::new();
while let Some((name, digest)) = map.next_entry::<String, ContentDigest>()? {
if digests.insert(name.clone(), digest).is_some() {
return Err(serde::de::Error::custom(format!(
"duplicate exec activity name `{name}`"
)));
}
}
Ok(AllowExecActivities::Allowlist(digests))
}
}
deserializer.deserialize_any(AllowExecActivitiesVisitor)
}
}
impl JsonSchema for AllowExecActivities {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("AllowExecActivities")
}
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"anyOf": [
{"type": "boolean"},
{"type": "object", "additionalProperties": {"type": "string"}},
{"type": "array", "items": {"type": "string"}}
]
})
}
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct ApiConfig {
#[serde(default = "default_true")]
pub(crate) enabled: bool,
#[serde(default = "default_api_listening_addr")]
pub(crate) listening_addr: SocketAddr,
#[serde(default)]
pub(crate) token_hashes: Vec<Digest>,
}
impl Default for ApiConfig {
fn default() -> Self {
Self {
enabled: true,
listening_addr: default_api_listening_addr(),
token_hashes: Vec::new(),
}
}
}
fn default_api_listening_addr() -> SocketAddr {
"127.0.0.1:5005".parse().expect("valid default address")
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[serde(rename_all = "snake_case")]
pub(crate) enum DatabaseConfigToml {
Sqlite(SqliteConfigToml),
Postgres(PostgresConfigToml),
}
impl DatabaseConfigToml {
pub fn get_subscription_interruption(&self) -> Option<Duration> {
match self {
DatabaseConfigToml::Sqlite(_) => None,
DatabaseConfigToml::Postgres(postgres_config_toml) => {
postgres_config_toml.subscription_interruption.into()
}
}
}
}
impl Default for DatabaseConfigToml {
fn default() -> DatabaseConfigToml {
DatabaseConfigToml::Sqlite(SqliteConfigToml::default())
}
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct PostgresConfigToml {
host: String,
user: String,
password: String,
db_name: String,
#[serde(default = "default_subscription_interruption")]
pub subscription_interruption: DurationConfigOptional,
#[serde(default)]
provision_policy: PostgresProvisionPolicy,
}
impl PostgresConfigToml {
pub fn as_config(
&self,
secret_registry: &SecretRegistry,
) -> Result<PostgresConfig, anyhow::Error> {
Ok(PostgresConfig {
host: interpolate_env_vars_plaintext(&self.host, secret_registry)?,
user: interpolate_env_vars_plaintext(&self.user, secret_registry)?,
password: interpolate_env_vars_secret(&self.password, secret_registry)?,
db_name: interpolate_env_vars_plaintext(&self.db_name, secret_registry)?,
})
}
pub fn as_provision_policy(&self) -> postgres_dao::ProvisionPolicy {
match self.provision_policy {
PostgresProvisionPolicy::Never => postgres_dao::ProvisionPolicy::NeverCreate,
PostgresProvisionPolicy::Auto => postgres_dao::ProvisionPolicy::Auto,
}
}
}
#[derive(Debug, Deserialize, JsonSchema, Clone, Default)]
#[serde(rename_all = "snake_case")]
pub enum PostgresProvisionPolicy {
#[default]
Never,
Auto,
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct SqliteConfigToml {
#[serde(default)]
directory: Option<String>,
#[serde(default = "default_sqlite_queue_capacity")]
queue_capacity: usize,
#[serde(default)]
pragma: std::collections::HashMap<String, String>, #[serde(default)]
metrics_threshold: Option<DurationConfig>,
}
impl Default for SqliteConfigToml {
fn default() -> Self {
Self {
directory: None,
queue_capacity: default_sqlite_queue_capacity(),
pragma: std::collections::HashMap::default(),
metrics_threshold: Option::default(),
}
}
}
impl SqliteConfigToml {
pub(crate) async fn get_sqlite_dir(
&self,
path_prefixes: &PathPrefixes,
secret_registry: &SecretRegistry,
) -> Result<PathBuf, anyhow::Error> {
let sqlite_file = self.directory.as_deref().unwrap_or_else(|| {
if path_prefixes.project_dirs.is_some() {
DEFAULT_SQLITE_DIR_IF_PROJECT_DIRS
} else {
DEFAULT_SQLITE_DIR
}
});
path_prefixes
.server_config_replace_path_prefix_mkdir(sqlite_file, secret_registry)
.await
}
pub(crate) fn as_sqlite_config(&self) -> SqliteConfig {
SqliteConfig {
queue_capacity: self.queue_capacity,
pragma_override: Some(self.pragma.clone().into_iter().collect()),
metrics_threshold: self.metrics_threshold.map(Duration::from),
}
}
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct WebUIConfig {
#[serde(default = "default_true")]
pub(crate) enabled: bool,
#[serde(default = "default_webui_listening_addr")]
pub(crate) listening_addr: String,
}
impl Default for WebUIConfig {
fn default() -> Self {
Self {
enabled: true,
listening_addr: default_webui_listening_addr(),
}
}
}
fn default_webui_listening_addr() -> String {
"127.0.0.1:8080".to_string()
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct ExternalServerConfig {
#[serde(default = "default_true")]
pub(crate) enabled: bool,
#[serde(default = "default_external_listening_addr")]
pub(crate) listening_addr: SocketAddr,
}
impl Default for ExternalServerConfig {
fn default() -> Self {
Self {
enabled: true,
listening_addr: default_external_listening_addr(),
}
}
}
fn default_external_listening_addr() -> SocketAddr {
"127.0.0.1:9090".parse().expect("valid default address")
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct WasmGlobalConfigToml {
#[serde(default)]
pub(crate) codegen_cache: CodegenCache,
#[serde(default)]
cache_directory: Option<String>,
#[serde(default)]
pub(crate) allocator_config: WasmtimeAllocatorConfig,
#[serde(default)]
pub(crate) global_executor_instance_limiter: InflightSemaphore,
#[serde(default)]
pub(crate) global_webhook_instance_limiter: InflightSemaphore,
#[serde(default)]
pub(crate) fuel: ValueOrUnlimited<u64>,
#[serde(default)]
pub(crate) build_semaphore: ValueOrUnlimited<u64>,
#[serde(default = "default_parallel_compilation")]
pub(crate) parallel_compilation: bool,
#[serde(default)]
pub(crate) wasmtime_pooling_config: WasmtimePoolingAllocatorConfig,
#[serde(default = "default_debug")]
pub(crate) debug: bool,
}
impl Default for WasmGlobalConfigToml {
fn default() -> Self {
WasmGlobalConfigToml {
codegen_cache: CodegenCache::default(),
cache_directory: Option::default(),
allocator_config: WasmtimeAllocatorConfig::default(),
global_executor_instance_limiter: InflightSemaphore::default(),
global_webhook_instance_limiter: InflightSemaphore::default(),
fuel: ValueOrUnlimited::default(),
build_semaphore: ValueOrUnlimited::default(),
parallel_compilation: default_parallel_compilation(),
wasmtime_pooling_config: WasmtimePoolingAllocatorConfig::default(),
debug: default_debug(),
}
}
}
impl WasmGlobalConfigToml {
pub(crate) async fn get_wasm_cache_directory(
&self,
path_prefixes: &PathPrefixes,
secret_registry: &SecretRegistry,
) -> Result<PathBuf, anyhow::Error> {
let wasm_directory = self.cache_directory.as_deref().unwrap_or_else(|| {
if path_prefixes.project_dirs.is_some() {
DEFAULT_WASM_DIRECTORY_IF_PROJECT_DIRS
} else {
DEFAULT_WASM_DIRECTORY
}
});
path_prefixes
.server_config_replace_path_prefix_mkdir(wasm_directory, secret_registry)
.await
}
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct WorkflowsGlobalConfigToml {
#[serde(default)]
pub(crate) lock_extension_leeway: Option<DurationConfig>,
#[serde(default = "default_max_replay_captured_writes")]
pub(crate) max_replay_captured_writes: usize,
#[serde(default = "default_max_events_per_run")]
#[schemars(range(min = 1))]
pub(crate) max_events_per_run: usize,
#[serde(default = "default_response_refresh_interval")]
#[schemars(range(min = 1))]
pub(crate) response_refresh_interval: usize,
}
impl Default for WorkflowsGlobalConfigToml {
fn default() -> Self {
Self {
lock_extension_leeway: None,
max_replay_captured_writes: default_max_replay_captured_writes(),
max_events_per_run: default_max_events_per_run(),
response_refresh_interval: default_response_refresh_interval(),
}
}
}
const fn default_max_replay_captured_writes() -> usize {
100
}
const fn default_max_events_per_run() -> usize {
100
}
const fn default_response_refresh_interval() -> usize {
32
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct CodegenCache {
#[serde(default = "default_codegen_enabled")]
enabled: bool,
#[serde(default)]
directory: Option<String>,
}
impl Default for CodegenCache {
fn default() -> Self {
Self {
enabled: default_codegen_enabled(),
directory: None,
}
}
}
impl CodegenCache {
pub(crate) async fn get_directory(
&self,
path_prefixes: &PathPrefixes,
secret_registry: &SecretRegistry,
) -> Result<Option<PathBuf>, anyhow::Error> {
if self.enabled {
let directory = self.directory.as_deref().unwrap_or_else(|| {
if path_prefixes.project_dirs.is_some() {
DEFAULT_CODEGEN_CACHE_DIRECTORY_IF_PROJECT_DIRS
} else {
DEFAULT_CODEGEN_CACHE_DIRECTORY
}
});
path_prefixes
.server_config_replace_path_prefix_mkdir(directory, secret_registry)
.await
.map(Some)
} else {
Ok(None)
}
}
}
#[derive(Debug, Deserialize, JsonSchema, Clone, Copy)]
#[serde(deny_unknown_fields)]
pub(crate) struct TimersWatcherTomlConfig {
#[serde(default = "default_timers_watcher_enabled")]
pub(crate) enabled: bool,
#[serde(default = "default_timers_watcher_leeway")]
pub(crate) leeway: DurationConfig,
#[serde(default = "default_timers_watcher_tick_sleep")]
pub(crate) tick_sleep: DurationConfig,
}
impl Default for TimersWatcherTomlConfig {
fn default() -> Self {
Self {
enabled: default_timers_watcher_enabled(),
leeway: default_timers_watcher_leeway(),
tick_sleep: default_timers_watcher_tick_sleep(),
}
}
}
#[derive(Debug, Deserialize, JsonSchema, Clone, Copy)]
#[serde(deny_unknown_fields)]
pub(crate) struct CancelWatcherTomlConfig {
#[serde(default = "default_cancel_watcher_tick_sleep")]
pub(crate) tick_sleep: DurationConfig,
}
impl Default for CancelWatcherTomlConfig {
fn default() -> Self {
Self {
tick_sleep: default_cancel_watcher_tick_sleep(),
}
}
}
#[derive(Debug, Deserialize, JsonSchema, Clone, Copy, Default)]
#[serde(rename_all = "snake_case")]
pub(crate) enum WasmtimeAllocatorConfig {
#[default]
Auto,
OnDemand,
Pooling,
}
#[derive(Debug, Deserialize, JsonSchema, Clone, Copy, Default)]
#[serde(deny_unknown_fields)]
pub(crate) struct WasmtimePoolingAllocatorConfig {
#[serde(default)]
pooling_memory_keep_resident: Option<usize>,
#[serde(default)]
pooling_table_keep_resident: Option<usize>,
#[serde(default)]
memory_protection_keys: Option<bool>,
#[serde(default)]
pooling_total_core_instances: Option<u32>,
#[serde(default)]
pooling_total_component_instances: Option<u32>,
#[serde(default)]
pooling_total_memories: Option<u32>,
#[serde(default)]
pooling_total_tables: Option<u32>,
#[serde(default)]
pooling_total_stacks: Option<u32>,
#[serde(default)]
pooling_max_memory_size: Option<usize>,
}
impl From<WasmtimePoolingAllocatorConfig> for wasm_workers::engines::PoolingOptions {
fn from(value: WasmtimePoolingAllocatorConfig) -> wasm_workers::engines::PoolingOptions {
wasm_workers::engines::PoolingOptions {
pooling_memory_keep_resident: value.pooling_memory_keep_resident,
pooling_table_keep_resident: value.pooling_table_keep_resident,
memory_protection_keys: value.memory_protection_keys,
pooling_total_core_instances: value.pooling_total_core_instances,
pooling_total_component_instances: value.pooling_total_component_instances,
pooling_total_memories: value.pooling_total_memories,
pooling_total_tables: value.pooling_total_tables,
pooling_total_stacks: value.pooling_total_stacks,
pooling_max_memory_size: value.pooling_max_memory_size,
}
}
}
#[cfg(feature = "otlp")]
pub(crate) mod otlp {
use super::{Deserialize, log};
use log::EnvFilter;
use schemars::JsonSchema;
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct OtlpConfig {
pub(crate) enabled: bool,
#[serde(default)]
pub(crate) level: EnvFilter,
#[serde(default = "default_service_name")]
pub(crate) service_name: String,
#[serde(default = "default_otlp_endpoint")]
pub(crate) otlp_endpoint: String,
}
fn default_service_name() -> String {
"obelisk-server".to_string()
}
fn default_otlp_endpoint() -> String {
"http://localhost:4317".to_string()
}
}
pub(crate) mod log {
use super::{Deserialize, JsonSchema, default_console_enabled, default_console_style};
use serde_with::serde_as;
use std::str::FromStr;
#[derive(Debug, Deserialize, JsonSchema, Default, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct LoggingConfig {
#[serde(default)]
pub(crate) file: Option<AppenderRollingFile>,
#[serde(default)]
pub(crate) console: AppenderConsole,
}
#[derive(Debug, Deserialize, JsonSchema, Default, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub(crate) enum SpanConfig {
#[default]
None,
New,
Enter,
Exit,
Close,
Active,
Full,
}
impl From<SpanConfig> for tracing_subscriber::fmt::format::FmtSpan {
fn from(value: SpanConfig) -> Self {
match value {
SpanConfig::None => Self::NONE,
SpanConfig::New => Self::NEW,
SpanConfig::Enter => Self::ENTER,
SpanConfig::Exit => Self::EXIT,
SpanConfig::Close => Self::CLOSE,
SpanConfig::Active => Self::ACTIVE,
SpanConfig::Full => Self::FULL,
}
}
}
#[derive(Debug, Deserialize, JsonSchema, Default, Clone)]
#[serde(rename_all = "snake_case")]
pub(crate) enum LoggingStyle {
#[default]
Plain,
PlainCompact,
Json,
}
#[serde_as]
#[derive(Debug, Deserialize, JsonSchema, Default, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct AppenderCommon {
#[serde(default)]
pub(crate) level: EnvFilter,
#[serde(default)]
pub(crate) span: SpanConfig,
#[serde(default)]
pub(crate) target: bool,
}
#[derive(Debug, serde_with::DeserializeFromStr, JsonSchema, Clone)]
pub(crate) struct EnvFilter(
#[schemars(with = "String")] pub(crate) tracing_subscriber::EnvFilter,
);
impl FromStr for EnvFilter {
type Err = tracing_subscriber::filter::ParseError;
fn from_str(directives: &str) -> Result<Self, Self::Err> {
tracing_subscriber::EnvFilter::builder()
.parse(directives)
.map(Self)
}
}
impl Default for EnvFilter {
fn default() -> Self {
Self::from_str("info,app=debug").expect("empty directive must not fail to parse")
}
}
#[derive(Copy, Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Eq)]
pub(crate) enum AppenderConsoleWriter {
#[default]
Stderr,
Stdout,
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct AppenderConsole {
#[serde(default = "default_console_enabled")]
pub(crate) enabled: bool,
#[serde(flatten, default)]
pub(crate) common: AppenderCommon,
#[serde(default = "default_console_style")]
pub(crate) style: LoggingStyle,
#[serde(default)]
pub(crate) writer: AppenderConsoleWriter,
}
impl Default for AppenderConsole {
fn default() -> Self {
Self {
enabled: default_console_enabled(),
common: AppenderCommon::default(),
style: default_console_style(),
writer: AppenderConsoleWriter::default(),
}
}
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct AppenderRollingFile {
pub(crate) enabled: bool,
#[serde(flatten, default)]
pub(crate) common: AppenderCommon,
pub(crate) directory: String,
pub(crate) prefix: String,
#[serde(default)]
pub(crate) rotation: Rotation,
#[serde(default)]
pub(crate) style: LoggingStyle,
}
#[derive(Debug, Deserialize, JsonSchema, Clone, Copy, Default)]
#[serde(rename_all = "snake_case")]
pub(crate) enum Rotation {
Minutely,
Hourly,
Daily,
#[default]
Never,
}
impl From<Rotation> for tracing_appender::rolling::Rotation {
fn from(value: Rotation) -> Self {
match value {
Rotation::Minutely => Self::MINUTELY,
Rotation::Hourly => Self::HOURLY,
Rotation::Daily => Self::DAILY,
Rotation::Never => Self::NEVER,
}
}
}
}
const fn default_true() -> bool {
true
}
const fn default_parallel_compilation() -> bool {
true
}
const fn default_debug() -> bool {
false
}
const fn default_codegen_enabled() -> bool {
true
}
const fn default_subscription_interruption() -> DurationConfigOptional {
DurationConfigOptional::Seconds(1)
}
fn default_console_enabled() -> bool {
true
}
fn default_console_style() -> LoggingStyle {
LoggingStyle::PlainCompact
}
fn default_sqlite_queue_capacity() -> usize {
SqliteConfig::default().queue_capacity
}
fn default_timers_watcher_enabled() -> bool {
true
}
fn default_timers_watcher_leeway() -> DurationConfig {
DurationConfig::Milliseconds(500)
}
fn default_timers_watcher_tick_sleep() -> DurationConfig {
DurationConfig::Milliseconds(100)
}
fn default_cancel_watcher_tick_sleep() -> DurationConfig {
DurationConfig::Seconds(1)
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[serde(deny_unknown_fields)]
pub(crate) struct HttpServer {
pub(crate) name: ConfigName,
pub(crate) listening_addr: SocketAddr,
}
const DEFAULT_SQLITE_DIR_IF_PROJECT_DIRS: &str =
const_format::formatcp!("{}obelisk-sqlite", DATA_DIR_PREFIX);
const DEFAULT_SQLITE_DIR: &str = "obelisk-sqlite";
pub(crate) const SQLITE_FILE_NAME: &str = "obelisk.sqlite";
const DEFAULT_WASM_DIRECTORY_IF_PROJECT_DIRS: &str =
const_format::formatcp!("{}wasm", CACHE_DIR_PREFIX);
const DEFAULT_WASM_DIRECTORY: &str = "cache/wasm";
const DEFAULT_CODEGEN_CACHE_DIRECTORY_IF_PROJECT_DIRS: &str =
const_format::formatcp!("{}codegen", CACHE_DIR_PREFIX);
const DEFAULT_CODEGEN_CACHE_DIRECTORY: &str = "cache/codegen";
pub(crate) const MAX_DEPLOYMENT_FILE_BYTES: u32 = 20 * 1024 * 1024;
#[cfg(test)]
mod tests {
use super::*;
use crate::config::deployment::{MethodsInput, ReplaceIn};
mod outbound_http {
use super::*;
#[test]
fn server_allowlist_uses_deployment_allowed_host_shape() {
let config: ServerConfigToml = toml::from_str(
r#"
[secrets]
API_KEY = { env = "API_KEY_SOURCE" }
[[outbound_http.allowed_host]]
pattern = "api.example.com"
methods = ["POST"]
request_url_regex = "^POST https://api\\.example\\.com/v1/"
secrets = ["API_KEY"]
replace_in = ["headers"]
"#,
)
.unwrap();
let entry = &config.outbound_http.allowed_hosts[0];
assert_eq!(entry.pattern, "api.example.com");
assert_eq!(entry.secrets, ["API_KEY"]);
assert!(matches!(
entry.methods,
Some(MethodsInput::List(ref methods)) if methods.as_slice() == ["POST"]
));
assert!(matches!(entry.replace_in.as_slice(), [ReplaceIn::Headers]));
}
#[test]
fn omitted_server_allowlist_is_empty() {
let config: ServerConfigToml = toml::from_str("").unwrap();
assert!(config.outbound_http.allowed_hosts.is_empty());
}
}
mod allow_exec_activities {
use super::*;
#[derive(serde::Deserialize, Debug)]
struct TestConfig {
#[serde(default)]
allow: AllowExecActivities,
}
const DIGEST: &str =
"sha256:abababababababababababababababababababababababababababababababab";
#[test]
fn deserialize_bool_map_and_legacy_digest_list() {
let actual: TestConfig = toml::from_str("allow = true").unwrap();
assert_eq!(AllowExecActivities::AllowAny, actual.allow);
let actual: TestConfig = toml::from_str("allow = false").unwrap();
assert_eq!(AllowExecActivities::Deny, actual.allow);
let actual: TestConfig = toml::from_str("").unwrap();
assert_eq!(AllowExecActivities::Deny, actual.allow);
let actual: TestConfig =
toml::from_str(&format!("[allow]\ngreet = \"{DIGEST}\"")).unwrap();
assert_eq!(
AllowExecActivities::Allowlist(BTreeMap::from([(
"greet".to_string(),
DIGEST.parse().unwrap()
)])),
actual.allow
);
let actual: TestConfig = toml::from_str(&format!("allow = [\"{DIGEST}\"]")).unwrap();
assert_eq!(
AllowExecActivities::LegacyAllowlist(vec![DIGEST.parse().unwrap()]),
actual.allow
);
}
#[test]
fn deserialize_bool_string_as_sent_by_env_override() {
let actual: TestConfig = toml::from_str(r#"allow = "true""#).unwrap();
assert_eq!(AllowExecActivities::AllowAny, actual.allow);
toml::from_str::<TestConfig>(r#"allow = "yes""#).unwrap_err();
}
}
}