use std::collections::BTreeMap;
use magma_cty::{CtyType, DynamicValue};
#[derive(Debug, thiserror::Error)]
pub enum SchemaError {
#[error("attribute {0:?} has neither a type nor a nested_type")]
AttributeNoType(String),
#[error("cty type decode for {0:?}: {1}")]
Cty(String, magma_cty::CtyError),
#[error("invalid nesting mode {0} for {1:?}")]
BadNesting(i32, String),
#[error("nested block {0:?} has no inner block")]
EmptyNestedBlock(String),
}
#[derive(Debug, Clone)]
pub struct ProviderSchema {
pub provider_config: CtyType,
pub resources: BTreeMap<String, CtyType>,
pub data_sources: BTreeMap<String, CtyType>,
pub resource_versions: BTreeMap<String, i64>,
}
impl ProviderSchema {
pub fn resource(&self, type_name: &str) -> Option<&CtyType> {
self.resources.get(type_name)
}
pub fn data_source(&self, type_name: &str) -> Option<&CtyType> {
self.data_sources.get(type_name)
}
#[must_use]
pub fn resource_version(&self, type_name: &str) -> i64 {
self.resource_versions.get(type_name).copied().unwrap_or(0)
}
#[must_use]
pub fn resource_version_u64(&self, type_name: &str) -> u64 {
u64::try_from(self.resource_version(type_name)).unwrap_or(0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diag {
pub severity: Severity,
pub summary: String,
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlannedChange {
pub state: DynamicValue,
pub requires_replace: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
Unknown,
}
#[derive(Debug, thiserror::Error)]
pub enum ProviderError {
#[error("provider RPC transport: {0}")]
Transport(String),
#[error("provider returned {} error diagnostic(s): {}", .0.len(), fmt_diags(.0))]
Diagnostics(Vec<Diag>),
#[error("provider returned no new_state from apply")]
NoNewState,
#[error("provider returned {} error diagnostic(s) WITH a new_state (partial apply — resource is committed): {}", .diags.len(), fmt_diags(.diags))]
PartiallyApplied {
diags: Vec<Diag>,
state: Box<DynamicValue>,
},
#[error("schema: {0}")]
Schema(#[from] SchemaError),
}
#[must_use]
pub fn is_retryable(e: &ProviderError) -> bool {
const TRANSIENT: &[&str] = &[
"rate limit",
"secondary rate",
"too many request",
"abuse",
"quota",
"try again",
"retry",
"429",
"503",
"resource_exhausted",
"unavailable",
"timeout",
"timed out",
"connection reset",
"broken pipe",
"tls",
"transport",
"h2 protocol",
"eof",
];
let hit = |s: &str| {
let l = s.to_ascii_lowercase();
TRANSIENT.iter().any(|p| l.contains(p))
};
match e {
ProviderError::Transport(s) => hit(s),
ProviderError::Diagnostics(diags) => {
diags.iter().any(|d| hit(&d.summary) || hit(&d.detail))
}
ProviderError::PartiallyApplied { .. } => false,
ProviderError::NoNewState | ProviderError::Schema(_) => false,
}
}
fn fmt_diags(diags: &[Diag]) -> String {
diags
.iter()
.map(|d| format!("{}: {}", d.summary, d.detail))
.collect::<Vec<_>>()
.join("; ")
}
#[async_trait::async_trait]
pub trait Provider: Send + Sync {
async fn get_schema(&mut self) -> Result<ProviderSchema, ProviderError>;
async fn configure(
&mut self,
config: &DynamicValue,
terraform_version: &str,
) -> Result<(), ProviderError>;
async fn plan_resource_change(
&mut self,
type_name: &str,
prior_state: &DynamicValue,
proposed_new_state: &DynamicValue,
config: &DynamicValue,
) -> Result<PlannedChange, ProviderError>;
async fn apply_resource_change(
&mut self,
type_name: &str,
prior_state: &DynamicValue,
planned_state: &DynamicValue,
config: &DynamicValue,
) -> Result<DynamicValue, ProviderError>;
async fn read_resource(
&mut self,
type_name: &str,
current_state: &DynamicValue,
) -> Result<Option<DynamicValue>, ProviderError>;
async fn read_data_source(
&mut self,
type_name: &str,
config: &DynamicValue,
) -> Result<Option<DynamicValue>, ProviderError>;
async fn import_resource_state(
&mut self,
type_name: &str,
id: &str,
) -> Result<Option<DynamicValue>, ProviderError>;
async fn upgrade_resource_state(
&mut self,
type_name: &str,
stored_version: i64,
raw_json: &[u8],
) -> Result<DynamicValue, ProviderError>;
}
#[cfg(test)]
mod tests {
use super::*;
struct TransportlessProvider;
#[async_trait::async_trait]
impl Provider for TransportlessProvider {
async fn get_schema(&mut self) -> Result<ProviderSchema, ProviderError> {
Ok(ProviderSchema {
provider_config: CtyType::Object(BTreeMap::new()),
resources: BTreeMap::new(),
data_sources: BTreeMap::new(),
resource_versions: BTreeMap::new(),
})
}
async fn configure(&mut self, _: &DynamicValue, _: &str) -> Result<(), ProviderError> {
Ok(())
}
async fn plan_resource_change(
&mut self,
_: &str,
_: &DynamicValue,
_: &DynamicValue,
_: &DynamicValue,
) -> Result<PlannedChange, ProviderError> {
Err(ProviderError::NoNewState)
}
async fn apply_resource_change(
&mut self,
_: &str,
_: &DynamicValue,
_: &DynamicValue,
_: &DynamicValue,
) -> Result<DynamicValue, ProviderError> {
Err(ProviderError::NoNewState)
}
async fn read_resource(
&mut self,
_: &str,
_: &DynamicValue,
) -> Result<Option<DynamicValue>, ProviderError> {
Ok(None)
}
async fn read_data_source(
&mut self,
_: &str,
_: &DynamicValue,
) -> Result<Option<DynamicValue>, ProviderError> {
Ok(None)
}
async fn import_resource_state(
&mut self,
_: &str,
_: &str,
) -> Result<Option<DynamicValue>, ProviderError> {
Ok(None)
}
async fn upgrade_resource_state(
&mut self,
_: &str,
_: i64,
_: &[u8],
) -> Result<DynamicValue, ProviderError> {
Err(ProviderError::NoNewState)
}
}
#[test]
fn the_contract_is_send_and_sync() {
const fn require<T: Send + Sync + ?Sized>() {}
require::<dyn Provider>();
}
#[tokio::test]
async fn the_contract_is_object_safe_and_transport_free() {
let mut p: Box<dyn Provider> = Box::new(TransportlessProvider);
let schema = p.get_schema().await.expect("stub schema");
assert!(schema.resources.is_empty());
let empty = CtyType::Object(BTreeMap::new());
let dv = DynamicValue::from_json(&serde_json::json!({}), &empty).expect("empty object");
assert!(p.read_resource("x", &dv).await.expect("read").is_none());
}
}