use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::path::PathBuf;
use greentic_deploy_spec::{
BundleDeployment, BundleDeploymentStatus, BundleId, CapabilitySlot, CustomerId, DeploymentId,
EnvId, EnvPackBinding, Environment, EnvironmentHostConfig, ExtensionBinding, HealthStatus,
IdempotencyKey, MessagingEndpoint, MessagingEndpointId, PackId, RetentionPolicy,
RevenueShareEntry, Revision, RevisionId, RevisionLifecycle, RevocationConfig, RouteBinding,
TrafficSplit, TrafficSplitEntry,
};
use serde_json::Value;
use super::StoreError;
use super::lifecycle::HealthGateFailure;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ExtensionKey {
pub kind_path: String,
pub instance_id: Option<String>,
}
impl ExtensionKey {
pub fn new(kind_path: impl Into<String>, instance_id: Option<String>) -> Self {
Self {
kind_path: kind_path.into(),
instance_id,
}
}
pub fn from_binding(b: &ExtensionBinding) -> Self {
Self {
kind_path: b.kind.path().to_string(),
instance_id: b.instance_id.clone(),
}
}
pub fn matches(&self, b: &ExtensionBinding) -> bool {
b.kind.path() == self.kind_path && b.instance_id.as_deref() == self.instance_id.as_deref()
}
}
impl std::fmt::Display for ExtensionKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.instance_id {
Some(inst) => write!(f, "{}/{}", self.kind_path, inst),
None => f.write_str(&self.kind_path),
}
}
}
#[derive(Debug, Clone)]
pub struct RevisionTransitionOutcome {
pub revision: Revision,
pub environment: Environment,
pub starting_lifecycle: RevisionLifecycle,
}
#[derive(Debug, Clone)]
pub struct TrustRootSeed {
pub key_id: String,
pub public_key_pem: String,
pub trusted_key_count: usize,
}
#[derive(Debug, Clone)]
pub struct TrustRootAddOutcome {
pub added_key_id: String,
pub trusted_key_count: usize,
}
#[derive(Debug, Clone)]
pub struct TrustRootRemoveOutcome {
pub removed_key_id: String,
pub removed_public_key_pem: Option<String>,
pub trusted_key_count: usize,
}
#[derive(Debug, Clone, Default)]
pub struct UpdateEnvironmentPayload {
pub name: Option<String>,
pub region: Option<String>,
pub tenant_org_id: Option<String>,
pub listen_addr: Option<SocketAddr>,
pub public_base_url: Option<String>,
}
#[derive(Debug, Clone)]
pub struct MigrateSeedPayload {
pub host_config: EnvironmentHostConfig,
pub revocation: RevocationConfig,
pub retention: RetentionPolicy,
pub health: HealthStatus,
}
#[derive(Debug, Clone)]
pub struct MigrateMergePayload {
pub packs: Vec<EnvPackBinding>,
pub extensions: Vec<ExtensionBinding>,
pub seed_if_missing: Option<MigrateSeedPayload>,
}
#[derive(Debug, Clone)]
pub struct StageRevisionPayload {
pub deployment_id: DeploymentId,
pub bundle_digest: String,
pub pack_list_lock_ref: String,
pub pack_config_refs: BTreeMap<String, String>,
pub config_digest: Option<String>,
pub signature_sidecar_ref: Option<PathBuf>,
pub drain_seconds: Option<u32>,
pub idempotency_key: IdempotencyKey,
}
#[derive(Debug, Clone)]
pub struct WarmRevisionPayload {
pub revision_id: RevisionId,
pub health_gate: Result<(), HealthGateFailure>,
pub idempotency_key: IdempotencyKey,
}
#[derive(Debug, Clone)]
pub struct AddBundlePayload {
pub bundle_id: BundleId,
pub customer_id: CustomerId,
pub revenue_share: Vec<RevenueShareEntry>,
pub route_binding: Option<RouteBinding>,
pub authorization_ref: Option<String>,
pub config_overrides: BTreeMap<String, BTreeMap<String, Value>>,
}
#[derive(Debug, Clone)]
pub struct AddMessagingEndpointPayload {
pub provider_id: String,
pub provider_type: String,
pub display_name: String,
pub secret_refs: Vec<String>,
pub updated_by: String,
pub idempotency_key: IdempotencyKey,
}
#[derive(Debug, Clone)]
pub struct SetMessagingWelcomeFlowPayload {
pub endpoint_id: MessagingEndpointId,
pub bundle_id: BundleId,
pub pack_id: PackId,
pub flow_id: String,
pub updated_by: String,
pub idempotency_key: IdempotencyKey,
}
pub trait EnvironmentMutations: Send + Sync {
fn create_environment(
&self,
env_id: &EnvId,
name: String,
host_config: EnvironmentHostConfig,
) -> Result<Environment, StoreError>;
fn update_environment(
&self,
env_id: &EnvId,
patch: UpdateEnvironmentPayload,
) -> Result<Environment, StoreError>;
fn migrate_merge_bindings(
&self,
target_env_id: &EnvId,
payload: MigrateMergePayload,
) -> Result<(Vec<String>, Vec<String>), StoreError>;
fn stage_revision(
&self,
env_id: &EnvId,
payload: StageRevisionPayload,
) -> Result<Revision, StoreError>;
fn warm_revision(
&self,
env_id: &EnvId,
payload: WarmRevisionPayload,
) -> Result<RevisionTransitionOutcome, StoreError>;
fn drain_revision(
&self,
env_id: &EnvId,
revision_id: RevisionId,
idempotency_key: IdempotencyKey,
) -> Result<RevisionTransitionOutcome, StoreError>;
fn archive_revision(
&self,
env_id: &EnvId,
revision_id: RevisionId,
idempotency_key: IdempotencyKey,
) -> Result<RevisionTransitionOutcome, StoreError>;
fn add_bundle(
&self,
env_id: &EnvId,
payload: AddBundlePayload,
) -> Result<BundleDeployment, StoreError>;
fn update_bundle(
&self,
env_id: &EnvId,
deployment_id: DeploymentId,
status: Option<BundleDeploymentStatus>,
route_binding: Option<RouteBinding>,
revenue_share: Option<Vec<RevenueShareEntry>>,
config_overrides: Option<BTreeMap<String, BTreeMap<String, Value>>>,
) -> Result<BundleDeployment, StoreError>;
fn remove_bundle(
&self,
env_id: &EnvId,
deployment_id: DeploymentId,
) -> Result<BundleDeployment, StoreError>;
fn add_pack_binding(
&self,
env_id: &EnvId,
binding: EnvPackBinding,
idempotency_key: IdempotencyKey,
) -> Result<EnvPackBinding, StoreError>;
fn update_pack_binding(
&self,
env_id: &EnvId,
slot: CapabilitySlot,
binding: EnvPackBinding,
idempotency_key: IdempotencyKey,
) -> Result<(EnvPackBinding, u64), StoreError>;
fn remove_pack_binding(
&self,
env_id: &EnvId,
slot: CapabilitySlot,
idempotency_key: IdempotencyKey,
) -> Result<(EnvPackBinding, u64), StoreError>;
fn rollback_pack_binding(
&self,
env_id: &EnvId,
slot: CapabilitySlot,
idempotency_key: IdempotencyKey,
) -> Result<(EnvPackBinding, u64), StoreError>;
fn add_extension_binding(
&self,
env_id: &EnvId,
binding: ExtensionBinding,
idempotency_key: IdempotencyKey,
) -> Result<ExtensionBinding, StoreError>;
fn update_extension_binding(
&self,
env_id: &EnvId,
key: ExtensionKey,
binding: ExtensionBinding,
idempotency_key: IdempotencyKey,
) -> Result<(ExtensionBinding, u64), StoreError>;
fn remove_extension_binding(
&self,
env_id: &EnvId,
key: ExtensionKey,
idempotency_key: IdempotencyKey,
) -> Result<(ExtensionBinding, u64), StoreError>;
fn rollback_extension_binding(
&self,
env_id: &EnvId,
key: ExtensionKey,
idempotency_key: IdempotencyKey,
) -> Result<(ExtensionBinding, u64), StoreError>;
fn set_traffic_split(
&self,
env_id: &EnvId,
deployment_id: DeploymentId,
entries: Vec<TrafficSplitEntry>,
idempotency_key: IdempotencyKey,
updated_by: String,
authorization_ref: Option<String>,
) -> Result<TrafficSplit, StoreError>;
fn rollback_traffic_split(
&self,
env_id: &EnvId,
deployment_id: DeploymentId,
) -> Result<TrafficSplit, StoreError>;
fn add_messaging_endpoint(
&self,
env_id: &EnvId,
payload: AddMessagingEndpointPayload,
) -> Result<MessagingEndpoint, StoreError>;
fn link_messaging_bundle(
&self,
env_id: &EnvId,
endpoint_id: MessagingEndpointId,
bundle_id: BundleId,
updated_by: String,
idempotency_key: IdempotencyKey,
) -> Result<MessagingEndpoint, StoreError>;
fn unlink_messaging_bundle(
&self,
env_id: &EnvId,
endpoint_id: MessagingEndpointId,
bundle_id: BundleId,
updated_by: String,
idempotency_key: IdempotencyKey,
) -> Result<MessagingEndpoint, StoreError>;
fn set_messaging_welcome_flow(
&self,
env_id: &EnvId,
payload: SetMessagingWelcomeFlowPayload,
) -> Result<MessagingEndpoint, StoreError>;
fn remove_messaging_endpoint(
&self,
env_id: &EnvId,
endpoint_id: MessagingEndpointId,
) -> Result<MessagingEndpointId, StoreError>;
fn rotate_messaging_webhook_secret(
&self,
env_id: &EnvId,
endpoint_id: MessagingEndpointId,
updated_by: String,
idempotency_key: IdempotencyKey,
) -> Result<MessagingEndpoint, StoreError>;
fn bootstrap_trust_root(&self, env_id: &EnvId) -> Result<TrustRootSeed, StoreError>;
fn seed_trust_root_if_absent(
&self,
env_id: &EnvId,
) -> Result<Option<TrustRootSeed>, StoreError>;
fn add_trusted_key(
&self,
env_id: &EnvId,
key_id: String,
public_key_pem: String,
idempotency_key: IdempotencyKey,
) -> Result<TrustRootAddOutcome, StoreError>;
fn remove_trusted_key(
&self,
env_id: &EnvId,
key_id: String,
idempotency_key: IdempotencyKey,
) -> Result<TrustRootRemoveOutcome, StoreError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(dead_code)]
fn _is_object_safe(_: &dyn EnvironmentMutations) {}
#[test]
fn extension_key_roundtrips() {
let key = ExtensionKey::new("capability/memory/long-term", Some("default".to_string()));
assert_eq!(key.kind_path, "capability/memory/long-term");
assert_eq!(key.instance_id, Some("default".to_string()));
let mut set = std::collections::HashSet::new();
set.insert(key.clone());
assert!(set.contains(&key));
let unnamed = ExtensionKey::new("capability/memory/long-term", None);
assert_ne!(unnamed, key, "None and Some(_) must differ");
assert!(
!set.contains(&unnamed),
"None key must not hash-collide with Some(_) key"
);
set.insert(unnamed.clone());
assert_eq!(set.len(), 2);
}
}