use std::sync::Arc;
use serde::Deserialize;
use serde::de::DeserializeOwned;
use super::error::AdminError;
use super::service::DesiredStateEdit;
use crate::desired_state::{
AliasTarget, BlobKind, BlobRef, BudgetBound, BudgetPolicy, CatalogOffering, Checksum,
ConcurrencyPolicy, DesiredState, DisplayName, ModelAliasBody, ModelEnablementBody,
ModelLifecycle, ModelOwner, ObservedPrice, OfferingId, PolicyBody, PolicyEpoch, PolicyScope,
ProjectBody, ProjectId, ProviderBody, ProviderCredentialBody, ResourceBody, ResourceId,
ResourceKind, ResourceRef, ResourceScope, ResourceVersion, ResourceVersionNumber,
RevocationPolicy, SecretId, SecretLifecycle, SecretOwner, SecretRef, SecretVersion, Slug,
Surface, TenantBody, TenantId, TenantLifecycle, ValidationError, WireFamily,
};
pub struct ResourcePlan {
pub scope: ResourceScope,
pub edit: Arc<dyn DesiredStateEdit>,
pub retires: bool,
}
impl ResourcePlan {
fn new<E>(scope: ResourceScope, edit: E) -> Self
where
E: Fn(&mut DesiredState) -> Result<(), ValidationError> + Send + Sync + 'static,
{
Self {
scope,
edit: Arc::new(edit),
retires: false,
}
}
#[must_use]
fn retiring(mut self, retires: bool) -> Self {
self.retires = retires;
self
}
}
pub trait AdminResourceRequest: DeserializeOwned + Send + Sync + 'static {
const SCHEMA: &'static str;
const SURFACE: Surface;
fn plan(self) -> Result<ResourcePlan, AdminError>;
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MutationEnvelope<R> {
pub summary: String,
#[serde(default)]
pub mutation: MutationKindInput,
pub resource: R,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MutationKindInput {
Create,
#[default]
Update,
Delete,
Rotate,
}
impl MutationKindInput {
pub const fn kind(self) -> crate::desired_state::MutationKind {
use crate::desired_state::MutationKind;
match self {
Self::Create => MutationKind::Create,
Self::Update => MutationKind::Update,
Self::Delete => MutationKind::Delete,
Self::Rotate => MutationKind::Rotate,
}
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RollbackRequest {
pub summary: String,
pub revision: String,
#[serde(default)]
pub tenant: Option<String>,
#[serde(default)]
pub project: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TenantRequest {
pub tenant: String,
pub slug: String,
pub display_name: String,
#[serde(default)]
pub lifecycle: Option<String>,
}
impl AdminResourceRequest for TenantRequest {
const SCHEMA: &'static str = "tenant";
const SURFACE: Surface = Surface::Tenant;
fn plan(self) -> Result<ResourcePlan, AdminError> {
let tenant = tenant_id::<Self>(&self.tenant)?;
let slug = slug::<Self>(&self.slug)?;
let display_name = display_name::<Self>(&self.display_name)?;
let lifecycle = match self.lifecycle.as_deref() {
None => TenantLifecycle::Active,
Some(text) => {
TenantLifecycle::parse(text).ok_or_else(|| unknown::<Self>("lifecycle", text))?
}
};
Ok(ResourcePlan::new(
ResourceScope::Deployment,
move |state: &mut DesiredState| {
let body = TenantBody::new(tenant, display_name.clone()).in_lifecycle(lifecycle);
let version = next_version(state, ResourceKind::Tenant, body.resource_id());
publish(state, body.version_at(slug.clone(), version))?;
Ok(())
},
)
.retiring(lifecycle == TenantLifecycle::Deleted))
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProjectRequest {
pub project: String,
pub tenant: String,
pub slug: String,
pub display_name: String,
}
impl AdminResourceRequest for ProjectRequest {
const SCHEMA: &'static str = "project";
const SURFACE: Surface = Surface::Project;
fn plan(self) -> Result<ResourcePlan, AdminError> {
let project = project_id::<Self>(&self.project)?;
let tenant = tenant_id::<Self>(&self.tenant)?;
let slug = slug::<Self>(&self.slug)?;
let display_name = display_name::<Self>(&self.display_name)?;
let body = ProjectBody::new(project, tenant, display_name);
Ok(ResourcePlan::new(
body.scope(),
move |state: &mut DesiredState| {
let version = next_version(state, ResourceKind::Project, body.resource_id());
publish(state, body.version_at(slug.clone(), version))?;
Ok(())
},
))
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProviderRequest {
pub provider: String,
pub tenant: String,
#[serde(default)]
pub project: Option<String>,
pub slug: String,
pub display_name: String,
pub wire_family: String,
pub endpoint: String,
}
impl AdminResourceRequest for ProviderRequest {
const SCHEMA: &'static str = "provider";
const SURFACE: Surface = Surface::Provider;
fn plan(self) -> Result<ResourcePlan, AdminError> {
let provider = resource_id::<Self>("provider", &self.provider)?;
let tenant = tenant_id::<Self>(&self.tenant)?;
let project = self
.project
.as_deref()
.map(project_id::<Self>)
.transpose()?;
let slug = slug::<Self>(&self.slug)?;
let display_name = display_name::<Self>(&self.display_name)?;
let wire_family = wire_family::<Self>(&self.wire_family)?;
let body = ProviderBody::for_tenant(
provider,
tenant,
display_name,
wire_family,
self.endpoint.clone(),
);
let body = match project {
Some(project) => body.owned_by_project(project),
None => body,
};
Ok(ResourcePlan::new(
body.scope(),
move |state: &mut DesiredState| {
let version = next_version(state, ResourceKind::Provider, body.resource_id());
publish(state, body.version_at(slug.clone(), version))?;
Ok(())
},
))
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CredentialRequest {
pub credential: String,
pub tenant: String,
#[serde(default)]
pub project: Option<String>,
pub provider: String,
pub slug: String,
pub display_name: String,
pub secret: String,
#[serde(default)]
pub secret_version: Option<u64>,
#[serde(default)]
pub lifecycle: Option<String>,
#[serde(default)]
pub rotate: bool,
}
impl AdminResourceRequest for CredentialRequest {
const SCHEMA: &'static str = "provider-credential";
const SURFACE: Surface = Surface::Credential;
fn plan(self) -> Result<ResourcePlan, AdminError> {
let credential = resource_id::<Self>("credential", &self.credential)?;
let tenant = tenant_id::<Self>(&self.tenant)?;
let project = self
.project
.as_deref()
.map(project_id::<Self>)
.transpose()?;
let provider = resource_id::<Self>("provider", &self.provider)?;
let slug = slug::<Self>(&self.slug)?;
let display_name = display_name::<Self>(&self.display_name)?;
let secret = SecretId::parse(&self.secret)
.map_err(|error| malformed::<Self>("secret", &error.to_string()))?;
let version = match self.secret_version {
None => None,
Some(version) => Some(
SecretVersion::new(version)
.ok_or_else(|| malformed::<Self>("secret_version", "versions start at 1"))?,
),
};
let lifecycle = match self.lifecycle.as_deref() {
None => None,
Some(text) => Some(
SecretLifecycle::parse(text).ok_or_else(|| unknown::<Self>("lifecycle", text))?,
),
};
let owner = match project {
Some(project) => SecretOwner::project(tenant, project),
None => SecretOwner::tenant(tenant),
};
let rotate = self.rotate;
Ok(
ResourcePlan::new(owner.scope(), move |state: &mut DesiredState| {
let reference = state.version_of(ResourceKind::ProviderCredential, credential);
let previous = match reference {
Some(resource) => Some(ProviderCredentialBody::read(resource)?),
None => None,
};
let staged = |material: SecretRef| {
ProviderCredentialBody::staged(
credential,
owner,
provider,
display_name.clone(),
material,
)
};
let in_force = previous.as_ref().map(ProviderCredentialBody::secret);
let authored = match (version, in_force) {
(Some(version), _) => SecretRef::new(secret, version),
(None, Some(held)) if held.secret == secret => held,
(None, _) => SecretRef::first(secret),
};
let body = match (previous, rotate) {
(Some(previous), true) => previous.reauthored(staged(authored)).rotated(),
(Some(previous), false) => previous.reauthored(staged(authored)),
(None, _) => staged(authored),
};
let body = match lifecycle {
Some(lifecycle) => body.transitioned(lifecycle)?,
None => body,
};
let next = next_version(state, ResourceKind::ProviderCredential, credential);
publish(state, body.version_at(slug.clone(), next))?;
Ok(())
})
.retiring(matches!(
lifecycle,
Some(SecretLifecycle::Revoked | SecretLifecycle::Tombstoned)
)),
)
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CatalogRequest {
pub catalog: String,
pub slug: String,
pub digest: String,
pub size_bytes: u64,
}
impl AdminResourceRequest for CatalogRequest {
const SCHEMA: &'static str = "catalog-snapshot";
const SURFACE: Surface = Surface::Model;
fn plan(self) -> Result<ResourcePlan, AdminError> {
let catalog = resource_id::<Self>("catalog", &self.catalog)?;
let slug = slug::<Self>(&self.slug)?;
let digest = checksum::<Self>("digest", &self.digest)?;
let blob = BlobRef {
kind: BlobKind::CatalogSnapshot,
digest,
size_bytes: self.size_bytes,
};
Ok(ResourcePlan::new(
ResourceScope::Deployment,
move |state: &mut DesiredState| {
refuse_withdrawing_a_pinned_snapshot(state, catalog, digest)?;
let version = next_version(state, ResourceKind::CatalogModel, catalog);
state.declare_blob(blob);
publish(
state,
ResourceVersion::new(
ResourceRef::new(ResourceKind::CatalogModel, catalog, version),
ResourceScope::Deployment,
slug.clone(),
ResourceBody::Blob(blob),
),
)?;
state.retain_referenced_blobs();
Ok(())
},
))
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ModelRequest {
pub enablement: String,
pub tenant: String,
#[serde(default)]
pub project: Option<String>,
pub slug: String,
pub offering: String,
pub catalog: String,
pub snapshot: String,
pub wire_family: String,
#[serde(default)]
pub state: Option<String>,
#[serde(default)]
pub observed_input_micros_per_million: Option<u64>,
#[serde(default)]
pub observed_output_micros_per_million: Option<u64>,
}
impl AdminResourceRequest for ModelRequest {
const SCHEMA: &'static str = "model-enablement";
const SURFACE: Surface = Surface::Model;
fn plan(self) -> Result<ResourcePlan, AdminError> {
let enablement = resource_id::<Self>("enablement", &self.enablement)?;
let tenant = tenant_id::<Self>(&self.tenant)?;
let project = self
.project
.as_deref()
.map(project_id::<Self>)
.transpose()?;
let slug = slug::<Self>(&self.slug)?;
let offering = OfferingId::parse(&self.offering)
.map_err(|error| malformed::<Self>("offering", &error.to_string()))?;
let catalog = resource_id::<Self>("catalog", &self.catalog)?;
let snapshot = checksum::<Self>("snapshot", &self.snapshot)?;
let wire_family = wire_family::<Self>(&self.wire_family)?;
let state = match self.state.as_deref() {
None => ModelLifecycle::Enabled,
Some(text) => {
ModelLifecycle::parse(text).ok_or_else(|| unknown::<Self>("state", text))?
}
};
let observed = match (
self.observed_input_micros_per_million,
self.observed_output_micros_per_million,
) {
(Some(input), Some(output)) => Some(ObservedPrice::new(input, output)),
(None, None) => None,
_ => {
return Err(malformed::<Self>(
"observed_input_micros_per_million",
"an observed price needs both an input and an output rate",
));
}
};
let owner = match project {
Some(project) => ModelOwner::project(tenant, project),
None => ModelOwner::tenant(tenant),
};
let body = ModelEnablementBody::new(
enablement,
owner,
CatalogOffering::new(offering, snapshot),
wire_family,
)
.transitioned(state);
let body = match observed {
Some(observed) => body.observing(observed),
None => body,
};
Ok(
ResourcePlan::new(owner.scope(), move |state: &mut DesiredState| {
let pinned = state
.version_of(ResourceKind::CatalogModel, catalog)
.map_or(
ResourceRef::new(
ResourceKind::CatalogModel,
catalog,
ResourceVersionNumber::FIRST,
),
|resource| resource.reference,
);
let version = next_version(state, ResourceKind::ModelEnablement, enablement);
publish(state, body.version_at(slug.clone(), version, pinned))?;
Ok(())
})
.retiring(state == ModelLifecycle::Disabled),
)
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AliasTargetRequest {
pub enablement: String,
#[serde(default)]
pub version: Option<u64>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AliasRequest {
pub alias: String,
pub tenant: String,
pub project: String,
pub slug: String,
pub wire_family: String,
#[serde(default)]
pub state: Option<String>,
pub targets: Vec<AliasTargetRequest>,
}
impl AdminResourceRequest for AliasRequest {
const SCHEMA: &'static str = "model-alias";
const SURFACE: Surface = Surface::Alias;
fn plan(self) -> Result<ResourcePlan, AdminError> {
let alias = resource_id::<Self>("alias", &self.alias)?;
let tenant = tenant_id::<Self>(&self.tenant)?;
let project = project_id::<Self>(&self.project)?;
let slug = slug::<Self>(&self.slug)?;
let wire_family = wire_family::<Self>(&self.wire_family)?;
let lifecycle = match self.state.as_deref() {
None => ModelLifecycle::Enabled,
Some(text) => {
ModelLifecycle::parse(text).ok_or_else(|| unknown::<Self>("state", text))?
}
};
let mut targets = Vec::with_capacity(self.targets.len());
for target in &self.targets {
let enablement = resource_id::<Self>("targets.enablement", &target.enablement)?;
let version = match target.version {
None => None,
Some(version) => {
Some(ResourceVersionNumber::new(version).ok_or_else(|| {
malformed::<Self>("targets.version", "versions start at 1")
})?)
}
};
targets.push((enablement, version));
}
Ok(ResourcePlan::new(
ResourceScope::Project { tenant, project },
move |state: &mut DesiredState| {
let resolved = targets
.iter()
.map(|(enablement, version)| {
let version = version.unwrap_or_else(|| {
state
.version_of(ResourceKind::ModelEnablement, *enablement)
.map_or(ResourceVersionNumber::FIRST, |held| held.reference.version)
});
AliasTarget::new(*enablement, version)
})
.collect::<Vec<_>>();
let body = ModelAliasBody::new(alias, tenant, project, wire_family, resolved)
.transitioned(lifecycle);
let version = next_version(state, ResourceKind::Alias, alias);
publish(state, body.version_at(slug.clone(), version))?;
Ok(())
},
)
.retiring(lifecycle == ModelLifecycle::Disabled))
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PolicyRequest {
pub tenant: String,
#[serde(default)]
pub project: Option<String>,
pub slug: String,
pub epoch: u64,
pub subject_limit_microdollars: u64,
#[serde(default)]
pub namespace_limit_microdollars: Option<u64>,
pub reservation_ttl_seconds: u64,
pub max_in_flight_per_subject: u64,
pub lease_ttl_seconds: u64,
#[serde(default)]
pub minimum_token_epoch: u64,
}
impl AdminResourceRequest for PolicyRequest {
const SCHEMA: &'static str = "policy";
const SURFACE: Surface = Surface::Policy;
fn plan(self) -> Result<ResourcePlan, AdminError> {
let tenant = tenant_id::<Self>(&self.tenant)?;
let project = self
.project
.as_deref()
.map(project_id::<Self>)
.transpose()?;
let slug = slug::<Self>(&self.slug)?;
let epoch = PolicyEpoch::new(self.epoch)
.map_err(|error| malformed::<Self>("epoch", &error.to_string()))?;
let budget = BudgetPolicy::new(
self.subject_limit_microdollars,
self.namespace_limit_microdollars,
self.reservation_ttl_seconds,
)
.map_err(|error| {
let field = match BudgetPolicy::unmet_bound(
self.subject_limit_microdollars,
self.namespace_limit_microdollars,
) {
BudgetBound::SubjectLimit => "subject_limit_microdollars",
BudgetBound::NamespaceLimit => "namespace_limit_microdollars",
BudgetBound::ReservationTtl => "reservation_ttl_seconds",
};
malformed::<Self>(field, &error.to_string())
})?;
let concurrency =
ConcurrencyPolicy::new(self.max_in_flight_per_subject, self.lease_ttl_seconds)
.map_err(|error| {
let field = ConcurrencyPolicy::unmet_bound(self.max_in_flight_per_subject)
.document_field();
malformed::<Self>(field, &error.to_string())
})?;
let revocation = RevocationPolicy::new(self.minimum_token_epoch);
let scope = match project {
Some(project) => PolicyScope::Project { tenant, project },
None => PolicyScope::Tenant(tenant),
};
let body = PolicyBody::new(scope, epoch, budget, concurrency, revocation);
Ok(ResourcePlan::new(
scope.resource_scope(),
move |state: &mut DesiredState| {
let version = next_version(state, ResourceKind::Policy, body.resource_id());
publish(state, body.version_at(slug.clone(), version))?;
Ok(())
},
))
}
}
fn refuse_withdrawing_a_pinned_snapshot(
state: &DesiredState,
catalog: ResourceId,
digest: Checksum,
) -> Result<(), ValidationError> {
let Some(held) = state.version_of(ResourceKind::CatalogModel, catalog) else {
return Ok(());
};
let withdrawn = match held.body.blob() {
Some(blob) if blob.digest != digest => blob.digest,
_ => return Ok(()),
};
let catalog = held.reference;
for resource in state.resources() {
if resource.reference.kind != ResourceKind::ModelEnablement {
continue;
}
let pinned = ModelEnablementBody::read(resource)
.map(|body| body.offering().is_pinned_to(withdrawn))
.unwrap_or(false);
if pinned {
return Err(ValidationError::PinnedSnapshotWithdrawn {
catalog,
enablement: resource.reference,
digest: withdrawn,
});
}
}
Ok(())
}
fn publish(state: &mut DesiredState, resource: ResourceVersion) -> Result<(), ValidationError> {
let current = resource.reference;
let superseded = state
.version_of(current.kind, current.id)
.map(|held| held.reference);
state.supersede(resource)?;
match superseded {
Some(superseded) => restack(state, superseded, current),
None => Ok(()),
}
}
fn restack(
state: &mut DesiredState,
superseded: ResourceRef,
current: ResourceRef,
) -> Result<(), ValidationError> {
let dependents: Vec<ResourceVersion> = state
.resources()
.filter(|resource| resource.depends_on.contains(&superseded))
.cloned()
.collect();
for dependent in dependents {
let previous = dependent.reference;
let version = previous.version.next();
let advanced = if previous.kind == ResourceKind::Alias {
let body = ModelAliasBody::read(&dependent)?;
let targets = body.targets().iter().map(|target| {
if target.enablement == superseded.id && target.version == superseded.version {
AliasTarget::new(target.enablement, current.version)
} else {
*target
}
});
body.clone()
.retargeted(targets.collect::<Vec<_>>())
.version_at(dependent.slug.clone(), version)
} else {
let mut depends_on = dependent.depends_on.clone();
depends_on.remove(&superseded);
depends_on.insert(current);
ResourceVersion::new(
ResourceRef::new(previous.kind, previous.id, version),
dependent.scope.clone(),
dependent.slug.clone(),
dependent.body.clone(),
)
.depending_on(depends_on)
};
let advanced_reference = advanced.reference;
state.supersede(advanced)?;
restack(state, previous, advanced_reference)?;
}
Ok(())
}
fn next_version(state: &DesiredState, kind: ResourceKind, id: ResourceId) -> ResourceVersionNumber {
state
.version_of(kind, id)
.map_or(ResourceVersionNumber::FIRST, |resource| {
resource.reference.version.next()
})
}
fn resource_id<R: AdminResourceRequest>(
field: &'static str,
text: &str,
) -> Result<ResourceId, AdminError> {
ResourceId::parse(text).map_err(|error| malformed::<R>(field, &error.to_string()))
}
fn tenant_id<R: AdminResourceRequest>(text: &str) -> Result<TenantId, AdminError> {
TenantId::parse(text).map_err(|error| malformed::<R>("tenant", &error.to_string()))
}
fn project_id<R: AdminResourceRequest>(text: &str) -> Result<ProjectId, AdminError> {
ProjectId::parse(text).map_err(|error| malformed::<R>("project", &error.to_string()))
}
fn slug<R: AdminResourceRequest>(text: &str) -> Result<Slug, AdminError> {
Slug::parse(text).map_err(|error| malformed::<R>("slug", &error.to_string()))
}
fn display_name<R: AdminResourceRequest>(text: &str) -> Result<DisplayName, AdminError> {
DisplayName::parse(text).map_err(|error| malformed::<R>("display_name", &error.to_string()))
}
fn checksum<R: AdminResourceRequest>(
field: &'static str,
text: &str,
) -> Result<Checksum, AdminError> {
Checksum::parse(text).map_err(|error| malformed::<R>(field, &error.to_string()))
}
fn wire_family<R: AdminResourceRequest>(text: &str) -> Result<WireFamily, AdminError> {
WireFamily::parse(text).ok_or_else(|| unknown::<R>("wire_family", text))
}
fn malformed<R: AdminResourceRequest>(field: &'static str, detail: &str) -> AdminError {
AdminError::RequestInvalid {
schema: R::SCHEMA,
detail: format!("`{field}`: {detail}"),
}
}
fn unknown<R: AdminResourceRequest>(field: &'static str, value: &str) -> AdminError {
AdminError::RequestInvalid {
schema: R::SCHEMA,
detail: format!("`{field}`: `{value}` is not a value this build knows"),
}
}