use std::collections::BTreeMap;
use std::path::Path;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use stackless_core::def::{Namespace, StackDef};
use stackless_core::substrate::StepResource;
use stackless_core::types::DnsName;
use stackless_stripe_projects::catalog::verify::CatalogService;
use stackless_stripe_projects::project;
use stackless_stripe_projects::provision::{ProvisionContext, provision_outputs};
use stackless_stripe_projects::stripe::{CommandRunner, StripeProjects};
use crate::ProviderOps;
use crate::config;
use crate::error::IntegrationError;
use crate::hostable::Hostable;
use crate::observation::IntegrationObservation;
pub trait CatalogResource: Hostable {
type Config: CatalogService + Send + Sync;
const PROVIDER_PREFIX: &'static str;
const OUTPUT_FIELDS: &'static [(&'static str, &'static str, bool)];
fn build_config(ctx: &ProvisionContext<'_>) -> Result<Self::Config, IntegrationError>;
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResourcePayload {
pub stripe_resource: String,
pub outputs: BTreeMap<String, String>,
}
#[async_trait]
impl<T: CatalogResource + Send + Sync> ProviderOps for T {
#[allow(clippy::too_many_arguments)]
async fn provision(
&self,
stripe: &StripeProjects<&dyn CommandRunner>,
def: &StackDef,
definition_dir: &Path,
instance: &str,
name: &str,
substrate: &str,
skip_stripe_instance_context: bool,
) -> Result<StepResource, IntegrationError> {
provision_resource::<T>(
stripe,
def,
definition_dir,
instance,
name,
substrate,
skip_stripe_instance_context,
)
.await
}
async fn observe(
&self,
stripe: &StripeProjects<&dyn CommandRunner>,
checkpoint_payload: &str,
fallback_resource: &str,
) -> Result<IntegrationObservation, IntegrationError> {
observe_resource(stripe, checkpoint_payload, fallback_resource).await
}
async fn destroy(
&self,
stripe: &StripeProjects<&dyn CommandRunner>,
checkpoint_payload: &str,
fallback_resource: &str,
) -> Result<(), IntegrationError> {
destroy_resource(stripe, checkpoint_payload, fallback_resource).await
}
}
#[allow(clippy::too_many_arguments)]
pub async fn provision_resource<T: CatalogResource>(
stripe: &StripeProjects<&dyn CommandRunner>,
def: &StackDef,
definition_dir: &Path,
instance: &str,
name: &str,
substrate: &str,
skip_instance_context: bool,
) -> Result<StepResource, IntegrationError> {
if !def.integrations.contains_key(name) {
return Err(IntegrationError::ConfigInvalid {
location: format!("integrations.{name}"),
detail: "integration not in definition".into(),
});
}
let ctx = ProvisionContext {
def,
instance,
logical_name: name,
definition_dir,
substrate,
skip_instance_context,
};
let config = T::build_config(&ctx)?;
let catalog = stripe.catalog_for_reference(T::Config::REFERENCE).await?;
let (resource_name, outputs) = provision_outputs(
stripe,
&catalog,
&ctx,
&config,
T::PROVIDER_PREFIX,
T::OUTPUT_FIELDS,
)
.await?;
let payload = ResourcePayload {
stripe_resource: resource_name.clone(),
outputs,
};
Ok(StepResource {
resource_kind: T::RESOURCE_KIND.into(),
resource_id: resource_name,
payload: serde_json::to_string(&payload).unwrap_or_default(),
})
}
pub async fn observe_resource(
stripe: &StripeProjects<&dyn CommandRunner>,
checkpoint_payload: &str,
fallback_resource: &str,
) -> Result<IntegrationObservation, IntegrationError> {
let resource = stripe_resource(checkpoint_payload).unwrap_or_else(|| fallback_resource.into());
let present = project::resource_registered(stripe, &resource).await?;
Ok(if present {
IntegrationObservation::Present { drift: vec![] }
} else {
IntegrationObservation::Gone
})
}
pub async fn destroy_resource(
stripe: &StripeProjects<&dyn CommandRunner>,
checkpoint_payload: &str,
fallback_resource: &str,
) -> Result<(), IntegrationError> {
let resource = stripe_resource(checkpoint_payload).unwrap_or_else(|| fallback_resource.into());
project::remove_resource(stripe, &resource).await?;
Ok(())
}
fn stripe_resource(payload: &str) -> Option<String> {
serde_json::from_str::<ResourcePayload>(payload)
.ok()
.map(|payload| payload.stripe_resource)
}
pub fn integration_config(
ctx: &ProvisionContext<'_>,
) -> Result<BTreeMap<String, toml::Value>, IntegrationError> {
let spec = ctx.def.integrations.get(ctx.logical_name).ok_or_else(|| {
IntegrationError::ConfigInvalid {
location: format!("integrations.{}", ctx.logical_name),
detail: "integration not in definition".into(),
}
})?;
Ok(spec.effective_config(ctx.substrate, &[]))
}
pub fn interp_required(
ctx: &ProvisionContext<'_>,
config: &BTreeMap<String, toml::Value>,
key: &str,
) -> Result<String, IntegrationError> {
let raw =
config::config_string(config, key).map_err(|err| cfg_invalid(ctx, key, err.to_string()))?;
interp_value(ctx, key, &raw)
}
pub fn interp_optional(
ctx: &ProvisionContext<'_>,
config: &BTreeMap<String, toml::Value>,
key: &str,
) -> Result<Option<String>, IntegrationError> {
match config::config_optional_string(config, key) {
None => Ok(None),
Some(raw) => Ok(Some(interp_value(ctx, key, &raw)?)),
}
}
fn interp_value(
ctx: &ProvisionContext<'_>,
key: &str,
raw: &str,
) -> Result<String, IntegrationError> {
let namespace = Namespace {
stack_name: ctx.def.stack.name.clone(),
instance_name: DnsName::from_stored(ctx.instance),
..Namespace::default()
};
let location = format!("integrations.{}.{key}", ctx.logical_name);
stackless_core::def::interp::resolve(raw, &namespace, &location)
.map_err(|err| cfg_invalid(ctx, key, err.to_string()))
}
pub fn int_required(
ctx: &ProvisionContext<'_>,
config: &BTreeMap<String, toml::Value>,
key: &str,
) -> Result<i64, IntegrationError> {
config
.get(key)
.and_then(toml::Value::as_integer)
.ok_or_else(|| {
cfg_invalid(
ctx,
key,
format!("{key} is required and must be an integer"),
)
})
}
pub fn int_optional(
ctx: &ProvisionContext<'_>,
config: &BTreeMap<String, toml::Value>,
key: &str,
) -> Result<Option<i64>, IntegrationError> {
match config.get(key) {
None => Ok(None),
Some(value) => value
.as_integer()
.map(Some)
.ok_or_else(|| cfg_invalid(ctx, key, format!("{key} must be an integer when set"))),
}
}
pub fn bool_required(
ctx: &ProvisionContext<'_>,
config: &BTreeMap<String, toml::Value>,
key: &str,
) -> Result<bool, IntegrationError> {
config
.get(key)
.and_then(toml::Value::as_bool)
.ok_or_else(|| cfg_invalid(ctx, key, format!("{key} is required and must be a boolean")))
}
pub fn bool_optional(
ctx: &ProvisionContext<'_>,
config: &BTreeMap<String, toml::Value>,
key: &str,
) -> Result<Option<bool>, IntegrationError> {
match config.get(key) {
None => Ok(None),
Some(value) => value
.as_bool()
.map(Some)
.ok_or_else(|| cfg_invalid(ctx, key, format!("{key} must be a boolean when set"))),
}
}
fn cfg_invalid(ctx: &ProvisionContext<'_>, key: &str, detail: String) -> IntegrationError {
IntegrationError::ConfigInvalid {
location: format!("integrations.{}.{key}", ctx.logical_name),
detail,
}
}