use crate::permissions::{ManagementPermissions, PermissionProfile, PermissionsConfig};
use crate::{Platform, Resource, ResourceLifecycle, ResourceRef};
use bon::Builder;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct ResourceEntry {
pub config: Resource,
pub lifecycle: ResourceLifecycle,
pub dependencies: Vec<ResourceRef>,
#[serde(default)]
pub remote_access: bool,
}
#[derive(Builder, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
#[builder(start_fn = new)]
pub struct Stack {
#[builder(start_fn)]
pub id: String,
#[builder(field)]
pub resources: IndexMap<String, ResourceEntry>,
#[builder(field)]
#[serde(default)]
pub permissions: PermissionsConfig,
#[builder(field)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supported_platforms: Option<Vec<Platform>>,
}
impl Stack {
pub fn resources(&self) -> impl Iterator<Item = (&String, &ResourceEntry)> {
self.resources.iter()
}
pub fn resources_mut(&mut self) -> impl Iterator<Item = (&String, &mut ResourceEntry)> {
self.resources.iter_mut()
}
pub fn id(&self) -> &str {
&self.id
}
pub fn current() -> StackRef {
StackRef::Current
}
pub fn permissions(&self) -> &PermissionsConfig {
&self.permissions
}
pub fn permission_profiles(&self) -> &IndexMap<String, PermissionProfile> {
&self.permissions.profiles
}
pub fn management(&self) -> &ManagementPermissions {
&self.permissions.management
}
pub fn supported_platforms(&self) -> Option<&[Platform]> {
self.supported_platforms.as_deref()
}
pub fn supports_platform(&self, platform: &Platform) -> bool {
match &self.supported_platforms {
Some(platforms) => platforms.contains(platform),
None => true,
}
}
}
impl StackBuilder {
pub fn add<T: crate::ResourceDefinition>(
self,
resource: T,
lifecycle: ResourceLifecycle,
) -> Self {
self.add_with_dependencies(resource, lifecycle, vec![])
}
pub fn add_with_dependencies<T: crate::ResourceDefinition>(
mut self,
resource: T,
lifecycle: ResourceLifecycle,
additional_dependencies: Vec<ResourceRef>,
) -> Self {
let resource = Resource::new(resource);
self.resources.insert(
resource.id().to_string(),
ResourceEntry {
config: resource,
lifecycle,
dependencies: additional_dependencies,
remote_access: false,
},
);
self
}
pub fn add_with_remote_access<T: crate::ResourceDefinition>(
mut self,
resource: T,
lifecycle: ResourceLifecycle,
) -> Self {
let resource = Resource::new(resource);
self.resources.insert(
resource.id().to_string(),
ResourceEntry {
config: resource,
lifecycle,
dependencies: vec![],
remote_access: true,
},
);
self
}
pub fn permissions(mut self, permissions: PermissionsConfig) -> Self {
self.permissions = permissions;
self
}
pub fn permission(mut self, name: impl Into<String>, profile: PermissionProfile) -> Self {
self.permissions.profiles.insert(name.into(), profile);
self
}
pub fn platforms(mut self, platforms: Vec<Platform>) -> Self {
self.supported_platforms = Some(platforms);
self
}
pub fn management(mut self, management: ManagementPermissions) -> Self {
self.permissions.management = management;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub enum StackRef {
Current,
External(String),
}
impl StackRef {
pub fn from_stack(stack: &Stack) -> Self {
StackRef::External(stack.id().to_string())
}
}
impl From<&Stack> for StackRef {
fn from(stack: &Stack) -> Self {
StackRef::External(stack.id().to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resource::ResourceLifecycle;
use crate::{PermissionSetReference, Storage, Worker};
use insta::assert_json_snapshot;
#[test]
fn test_stack_serialization() {
use crate::WorkerCode;
let storage = Storage::new("my-bucket".to_string())
.public_read(true)
.build();
let worker = Worker::new("my-worker".to_string())
.code(WorkerCode::Image {
image: "rust:latest".to_string(),
})
.permissions("execution".to_string())
.link(&storage)
.build();
let mut permissions = IndexMap::new();
let mut execution_profile = PermissionProfile::new();
execution_profile.0.insert(
"*".to_string(),
vec![
PermissionSetReference::from_name("storage/data-read"),
PermissionSetReference::from_name("storage/data-write"),
],
);
permissions.insert("execution".to_string(), execution_profile);
let stack_builder = Stack::new("test-stack".to_string())
.add(storage, ResourceLifecycle::Frozen)
.add(worker.clone(), ResourceLifecycle::Live);
let stack = stack_builder
.permissions(PermissionsConfig {
profiles: permissions,
management: ManagementPermissions::Auto,
})
.build();
let serialized_stack =
serde_json::to_string_pretty(&stack).expect("Failed to serialize stack");
let deserialized_stack: Stack =
serde_json::from_str(&serialized_stack).expect("Failed to deserialize stack");
assert_eq!(
stack, deserialized_stack,
"Original and deserialized stacks do not match."
);
let mut settings = insta::Settings::clone_current();
settings.set_sort_maps(true);
settings.bind(|| {
assert_json_snapshot!("stack_serialization_account_managed", stack);
});
}
#[test]
fn test_empty_stack_serialization() {
let stack_builder = Stack::new("empty-test-stack".to_string());
let stack = stack_builder
.permissions(PermissionsConfig::new()) .build();
let serialized_stack =
serde_json::to_string_pretty(&stack).expect("Failed to serialize empty stack");
let deserialized_stack: Stack =
serde_json::from_str(&serialized_stack).expect("Failed to deserialize empty stack");
assert_eq!(
stack, deserialized_stack,
"Original and deserialized empty stacks do not match."
);
let mut settings = insta::Settings::clone_current();
settings.set_sort_maps(true);
settings.bind(|| {
assert_json_snapshot!("empty_stack_serialization_account", stack);
});
}
#[test]
fn test_stack_with_permissions() {
use crate::permissions::PermissionProfile;
use indexmap::IndexMap;
let storage = Storage::new("test-storage".to_string()).build();
let mut permission_profile = PermissionProfile::new();
permission_profile.0.insert(
"*".to_string(),
vec![PermissionSetReference::from_name("storage/data-read")],
);
let mut permissions = IndexMap::new();
permissions.insert("reader".to_string(), permission_profile);
let stack = Stack::new("test-permissions-stack".to_string())
.add(storage, ResourceLifecycle::Frozen)
.permissions(PermissionsConfig {
profiles: permissions,
management: ManagementPermissions::Auto,
})
.build();
assert_eq!(stack.permission_profiles().len(), 1);
assert!(stack.permission_profiles().contains_key("reader"));
let reader_profile = stack.permission_profiles().get("reader").unwrap();
assert_eq!(reader_profile.0.len(), 1);
assert!(reader_profile.0.contains_key("*"));
let global_permissions = reader_profile.0.get("*").unwrap();
assert_eq!(
global_permissions,
&vec![PermissionSetReference::from_name("storage/data-read")]
);
let serialized = serde_json::to_string_pretty(&stack).expect("Failed to serialize");
let deserialized: Stack = serde_json::from_str(&serialized).expect("Failed to deserialize");
assert_eq!(stack, deserialized);
}
#[test]
fn test_stack_with_management_permissions() {
use crate::permissions::{ManagementPermissions, PermissionProfile};
let storage = Storage::new("test-storage".to_string()).build();
let mut management_profile = PermissionProfile::new();
management_profile.0.insert(
"*".to_string(),
vec![PermissionSetReference::from_name("vault/data-write")],
);
let stack_auto = Stack::new("test-auto-management-stack".to_string())
.add(storage.clone(), ResourceLifecycle::Frozen)
.management(ManagementPermissions::auto())
.build();
assert!(stack_auto.management().is_auto());
assert!(stack_auto.management().profile().is_none());
let stack_extend = Stack::new("test-extend-management-stack".to_string())
.add(storage.clone(), ResourceLifecycle::Frozen)
.management(ManagementPermissions::extend(management_profile.clone()))
.build();
assert!(stack_extend.management().is_extend());
assert_eq!(
stack_extend.management().profile().unwrap(),
&management_profile
);
let stack_override = Stack::new("test-override-management-stack".to_string())
.add(storage.clone(), ResourceLifecycle::Frozen)
.management(ManagementPermissions::override_(management_profile.clone()))
.build();
assert!(stack_override.management().is_override());
assert_eq!(
stack_override.management().profile().unwrap(),
&management_profile
);
let stack_default = Stack::new("test-default-management-stack".to_string())
.add(storage, ResourceLifecycle::Frozen)
.build();
assert!(stack_default.management().is_auto());
let serialized = serde_json::to_string_pretty(&stack_extend).expect("Failed to serialize");
let deserialized: Stack = serde_json::from_str(&serialized).expect("Failed to deserialize");
assert_eq!(stack_extend, deserialized);
}
}