alien_core/resources/
azure_storage_account.rs1use crate::error::{ErrorData, Result};
2use crate::resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef, ResourceType};
3use crate::{LifecycleRule, ResourceLifecycle, Stack, Storage};
4use alien_error::AlienError;
5use bon::Builder;
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8use std::any::Any;
9use std::collections::{BTreeMap, BTreeSet};
10use std::fmt::Debug;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
14#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
15#[serde(rename_all = "camelCase", deny_unknown_fields)]
16#[builder(start_fn = new)]
17pub struct AzureStorageAccount {
18 #[builder(start_fn)]
21 pub id: String,
22}
23
24impl AzureStorageAccount {
25 pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("azure_storage_account");
27
28 pub fn setup_settings_digest(stack: &Stack) -> Option<String> {
32 let settings = azure_storage_settings(stack);
33 (settings != AzureStorageSettings::default()).then(|| {
34 let encoded = serde_json::to_vec(&settings)
35 .expect("Azure storage settings contain only serializable values");
36 format!("{:x}", Sha256::digest(encoded))
37 })
38 }
39
40 pub fn id(&self) -> &str {
42 &self.id
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
49#[serde(rename_all = "camelCase")]
50pub struct AzureStorageAccountOutputs {
51 pub account_name: String,
53 pub resource_id: String,
55 pub primary_blob_endpoint: String,
57 pub primary_file_endpoint: String,
59 pub primary_queue_endpoint: String,
61 pub primary_table_endpoint: String,
63}
64
65impl ResourceOutputsDefinition for AzureStorageAccountOutputs {
66 fn get_resource_type(&self) -> ResourceType {
67 AzureStorageAccount::RESOURCE_TYPE.clone()
68 }
69
70 fn as_any(&self) -> &dyn Any {
71 self
72 }
73
74 fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
75 Box::new(self.clone())
76 }
77
78 fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
79 other.as_any().downcast_ref::<AzureStorageAccountOutputs>() == Some(self)
80 }
81
82 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
83 serde_json::to_value(self)
84 }
85}
86
87impl ResourceDefinition for AzureStorageAccount {
89 fn get_resource_type(&self) -> ResourceType {
90 Self::RESOURCE_TYPE
91 }
92
93 fn id(&self) -> &str {
94 &self.id
95 }
96
97 fn replace_after_failed_create_is_safe(&self) -> bool {
98 false
100 }
101
102 fn get_dependencies(&self) -> Vec<ResourceRef> {
103 Vec::new()
104 }
105
106 fn validate_update(&self, _new_config: &dyn ResourceDefinition) -> Result<()> {
107 Err(AlienError::new(ErrorData::InvalidResourceUpdate {
108 resource_id: self.id.clone(),
109 reason: "Azure storage accounts cannot be updated once created".to_string(),
110 }))
111 }
112
113 fn as_any(&self) -> &dyn Any {
114 self
115 }
116
117 fn as_any_mut(&mut self) -> &mut dyn Any {
118 self
119 }
120
121 fn box_clone(&self) -> Box<dyn ResourceDefinition> {
122 Box::new(self.clone())
123 }
124
125 fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
126 other.as_any().downcast_ref::<AzureStorageAccount>() == Some(self)
127 }
128
129 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
130 serde_json::to_value(self)
131 }
132}
133
134#[derive(Default, PartialEq, Serialize)]
137#[serde(rename_all = "camelCase")]
138struct AzureStorageSettings {
139 public: BTreeSet<Option<String>>,
140 versioning: BTreeSet<Option<String>>,
141 cors: BTreeMap<String, BTreeSet<Option<String>>>,
142 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
144 upload_cors: BTreeMap<String, BTreeSet<Option<String>>>,
145 expiry: BTreeMap<String, (ResourceLifecycle, Option<String>, Vec<LifecycleRule>)>,
146}
147
148fn azure_storage_settings(stack: &Stack) -> AzureStorageSettings {
149 let mut settings = AzureStorageSettings::default();
150 for (id, entry) in stack.resources() {
151 let Some(storage) = entry.config.downcast_ref::<Storage>() else {
152 continue;
153 };
154 if storage.public_read {
155 settings.public.insert(entry.enabled_when.clone());
156 }
157 if storage.versioning {
158 settings.versioning.insert(entry.enabled_when.clone());
159 }
160 for origin in &storage.cors_allowed_origins {
161 settings
162 .cors
163 .entry(origin.clone())
164 .or_default()
165 .insert(entry.enabled_when.clone());
166 }
167 for origin in &storage.cors_upload_origins {
168 settings
169 .upload_cors
170 .entry(origin.clone())
171 .or_default()
172 .insert(entry.enabled_when.clone());
173 }
174 if !storage.lifecycle_rules.is_empty() {
175 settings.expiry.insert(
176 id.clone(),
177 (
178 entry.lifecycle,
179 entry.enabled_when.clone(),
180 storage.lifecycle_rules.clone(),
181 ),
182 );
183 }
184 }
185 for gates in [&mut settings.public, &mut settings.versioning]
187 .into_iter()
188 .chain(settings.cors.values_mut())
189 .chain(settings.upload_cors.values_mut())
190 {
191 if gates.contains(&None) {
192 gates.clear();
193 gates.insert(None);
194 }
195 }
196 settings
197}