1use crate::ResourceLifecycle;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SetupEmission {
10 Never,
12 WhenFrozen,
14 Always,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct ResourceOwnershipPolicy {
22 default_lifecycle: ResourceLifecycle,
23 allow_frozen: bool,
24 allow_live: bool,
25 emit_in_setup: SetupEmission,
26 requires_management_permissions: bool,
27 runtime_cleanup_before_teardown: bool,
28}
29
30impl ResourceOwnershipPolicy {
31 pub const fn new(
32 default_lifecycle: ResourceLifecycle,
33 allow_frozen: bool,
34 allow_live: bool,
35 emit_in_setup: SetupEmission,
36 requires_management_permissions: bool,
37 runtime_cleanup_before_teardown: bool,
38 ) -> Self {
39 Self {
40 default_lifecycle,
41 allow_frozen,
42 allow_live,
43 emit_in_setup,
44 requires_management_permissions,
45 runtime_cleanup_before_teardown,
46 }
47 }
48
49 pub const fn default_lifecycle(self) -> ResourceLifecycle {
50 self.default_lifecycle
51 }
52
53 pub const fn allows_frozen(self) -> bool {
54 self.allow_frozen
55 }
56
57 pub const fn allows_live(self) -> bool {
58 self.allow_live
59 }
60
61 pub const fn allows_lifecycle(self, lifecycle: ResourceLifecycle) -> bool {
62 match lifecycle {
63 ResourceLifecycle::Frozen => self.allow_frozen,
64 ResourceLifecycle::Live => self.allow_live,
65 }
66 }
67
68 pub const fn should_emit_in_setup(self, lifecycle: ResourceLifecycle) -> bool {
75 !matches!(self.emit_in_setup, SetupEmission::Never)
76 && matches!(lifecycle, ResourceLifecycle::Frozen)
77 }
78
79 pub const fn emits_setup_scaffolding(self, lifecycle: ResourceLifecycle) -> bool {
86 match self.emit_in_setup {
87 SetupEmission::Never => false,
88 SetupEmission::WhenFrozen => matches!(lifecycle, ResourceLifecycle::Frozen),
89 SetupEmission::Always => true,
90 }
91 }
92
93 pub const fn always_emits_setup_scaffolding(self) -> bool {
96 matches!(self.emit_in_setup, SetupEmission::Always)
97 }
98
99 pub const fn requires_management_permissions(self) -> bool {
100 self.requires_management_permissions
101 }
102
103 pub const fn has_runtime_cleanup_before_teardown(self) -> bool {
104 self.runtime_cleanup_before_teardown
105 }
106
107 pub fn allowed_lifecycles(self) -> &'static str {
108 match (self.allow_frozen, self.allow_live) {
109 (true, true) => "Frozen or Live",
110 (true, false) => "Frozen",
111 (false, true) => "Live",
112 (false, false) => "no lifecycle",
113 }
114 }
115}
116
117pub fn ownership_policy_for_resource_type(resource_type: &str) -> ResourceOwnershipPolicy {
118 match resource_type {
119 "function" | "container-cluster" => removed_resource_type(),
120 "worker" | "daemon" | "container" => live_only(),
121 "compute-cluster" => frozen_with_runtime_cleanup(),
122 "sandbox" => sandbox_lifecycle(),
123 "artifact-registry" | "key" => frozen_with_management(),
124 "build"
125 | "network"
126 | "remote-stack-management"
127 | "resource-access"
128 | "service-account"
129 | "service_activation"
130 | "service-activation"
131 | "azure_resource_group"
132 | "azure-resource-group"
133 | "azure_storage_account"
134 | "azure-storage-account"
135 | "azure_container_apps_environment"
136 | "azure-container-apps-environment"
137 | "azure_service_bus_namespace"
138 | "azure-service-bus-namespace"
139 | "email" => frozen_only(),
142 "experimental/aws-opensearch" => frozen_only(),
145 "redis" => user_choice_with_management(),
149 "storage" | "queue" | "kv" | "vault" | "postgres" | "ai" => user_choice(),
150 _ => user_choice(),
151 }
152}
153
154const fn frozen_only() -> ResourceOwnershipPolicy {
155 ResourceOwnershipPolicy::new(
156 ResourceLifecycle::Frozen,
157 true,
158 false,
159 SetupEmission::WhenFrozen,
160 false,
161 false,
162 )
163}
164
165const fn frozen_with_management() -> ResourceOwnershipPolicy {
166 ResourceOwnershipPolicy::new(
167 ResourceLifecycle::Frozen,
168 true,
169 false,
170 SetupEmission::WhenFrozen,
171 true,
172 false,
173 )
174}
175
176const fn frozen_with_runtime_cleanup() -> ResourceOwnershipPolicy {
177 ResourceOwnershipPolicy::new(
178 ResourceLifecycle::Frozen,
179 true,
180 false,
181 SetupEmission::WhenFrozen,
182 true,
183 true,
184 )
185}
186
187const fn sandbox_lifecycle() -> ResourceOwnershipPolicy {
193 ResourceOwnershipPolicy::new(
194 ResourceLifecycle::Frozen,
195 true,
196 true,
197 SetupEmission::Always,
198 true,
199 true,
200 )
201}
202
203const fn live_only() -> ResourceOwnershipPolicy {
204 ResourceOwnershipPolicy::new(
205 ResourceLifecycle::Live,
206 false,
207 true,
208 SetupEmission::Never,
209 false,
210 false,
211 )
212}
213
214const fn removed_resource_type() -> ResourceOwnershipPolicy {
215 ResourceOwnershipPolicy::new(
216 ResourceLifecycle::Live,
217 false,
218 false,
219 SetupEmission::Never,
220 false,
221 false,
222 )
223}
224
225const fn user_choice_with_management() -> ResourceOwnershipPolicy {
226 ResourceOwnershipPolicy::new(
227 ResourceLifecycle::Frozen,
228 true,
229 true,
230 SetupEmission::WhenFrozen,
231 true,
232 false,
233 )
234}
235
236const fn user_choice() -> ResourceOwnershipPolicy {
237 ResourceOwnershipPolicy::new(
238 ResourceLifecycle::Frozen,
239 true,
240 true,
241 SetupEmission::WhenFrozen,
242 false,
243 false,
244 )
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn workload_resources_are_live_only() {
253 for resource_type in ["worker", "daemon", "container"] {
254 let policy = ownership_policy_for_resource_type(resource_type);
255 assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Live);
256 assert!(!policy.allows_lifecycle(ResourceLifecycle::Frozen));
257 assert!(policy.allows_lifecycle(ResourceLifecycle::Live));
258 assert!(!policy.should_emit_in_setup(ResourceLifecycle::Live));
259 }
260 }
261
262 #[test]
263 fn compute_cluster_is_frozen_with_runtime_cleanup() {
264 let policy = ownership_policy_for_resource_type("compute-cluster");
265 assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Frozen);
266 assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
267 assert!(!policy.allows_lifecycle(ResourceLifecycle::Live));
268 assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
269 assert!(policy.requires_management_permissions());
270 assert!(policy.has_runtime_cleanup_before_teardown());
271 }
272
273 #[test]
274 fn sandbox_defaults_to_frozen_but_may_be_live() {
275 let policy = ownership_policy_for_resource_type("sandbox");
276 assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Frozen);
277 assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
278 assert!(policy.allows_lifecycle(ResourceLifecycle::Live));
279 assert!(policy.requires_management_permissions());
280 assert!(policy.has_runtime_cleanup_before_teardown());
281 }
282
283 #[test]
287 fn a_live_sandbox_is_scaffolded_by_setup_but_not_owned_by_it() {
288 let policy = ownership_policy_for_resource_type("sandbox");
289
290 assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
291 assert!(policy.emits_setup_scaffolding(ResourceLifecycle::Frozen));
292
293 assert!(!policy.should_emit_in_setup(ResourceLifecycle::Live));
294 assert!(policy.emits_setup_scaffolding(ResourceLifecycle::Live));
295 }
296
297 #[test]
301 fn only_the_sandbox_separates_ownership_from_scaffolding() {
302 let types = crate::gateability::MANIFEST_TYPES.iter().copied().chain([
303 "function",
304 "container-cluster",
305 "compute-cluster",
306 "artifact-registry",
307 "key",
308 "build",
309 "network",
310 "remote-stack-management",
311 "resource-access",
312 "service-account",
313 "service_activation",
314 "service-activation",
315 "azure_resource_group",
316 "azure-resource-group",
317 "azure_storage_account",
318 "azure-storage-account",
319 "azure_container_apps_environment",
320 "azure-container-apps-environment",
321 "azure_service_bus_namespace",
322 "azure-service-bus-namespace",
323 "an-unregistered-extension-type",
324 ]);
325
326 for resource_type in types {
327 if resource_type == "sandbox" {
328 continue;
329 }
330 let policy = ownership_policy_for_resource_type(resource_type);
331 for lifecycle in [ResourceLifecycle::Frozen, ResourceLifecycle::Live] {
332 assert_eq!(
333 policy.should_emit_in_setup(lifecycle),
334 policy.emits_setup_scaffolding(lifecycle),
335 "'{resource_type}' answers the two questions differently under {lifecycle:?}"
336 );
337 }
338 }
339 }
340
341 #[test]
342 fn artifact_registry_is_frozen_with_management() {
343 let policy = ownership_policy_for_resource_type("artifact-registry");
344 assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Frozen);
345 assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
346 assert!(!policy.allows_lifecycle(ResourceLifecycle::Live));
347 assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
348 assert!(policy.requires_management_permissions());
349 assert!(!policy.has_runtime_cleanup_before_teardown());
350 }
351
352 #[test]
353 fn removed_resource_type_tags_are_not_normal_policy_entries() {
354 for resource_type in ["function", "container-cluster"] {
355 let policy = ownership_policy_for_resource_type(resource_type);
356 assert!(!policy.allows_lifecycle(ResourceLifecycle::Frozen));
357 assert!(!policy.allows_lifecycle(ResourceLifecycle::Live));
358 assert!(!policy.requires_management_permissions());
359 assert!(!policy.has_runtime_cleanup_before_teardown());
360 }
361 }
362
363 #[test]
364 fn data_resources_can_be_frozen_or_live() {
365 for resource_type in ["storage", "queue", "kv", "vault", "postgres", "redis", "ai"] {
366 let policy = ownership_policy_for_resource_type(resource_type);
367 assert_eq!(
368 policy.requires_management_permissions(),
369 resource_type == "redis"
370 );
371 assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Frozen);
372 assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
373 assert!(policy.allows_lifecycle(ResourceLifecycle::Live));
374 assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
375 assert!(!policy.should_emit_in_setup(ResourceLifecycle::Live));
376 }
377 }
378
379 #[test]
380 fn experimental_aws_opensearch_is_frozen_only() {
381 let policy = ownership_policy_for_resource_type("experimental/aws-opensearch");
382 assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Frozen);
383 assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
384 assert!(!policy.allows_lifecycle(ResourceLifecycle::Live));
385 assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
386 assert!(!policy.requires_management_permissions());
387 }
388
389 #[test]
390 fn setup_resources_are_frozen_only() {
391 for resource_type in [
392 "build",
393 "network",
394 "remote-stack-management",
395 "resource-access",
396 "service-account",
397 "service_activation",
398 "azure_resource_group",
399 "azure_storage_account",
400 "azure_container_apps_environment",
401 "azure_service_bus_namespace",
402 "email",
403 ] {
404 let policy = ownership_policy_for_resource_type(resource_type);
405 assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
406 assert!(!policy.allows_lifecycle(ResourceLifecycle::Live));
407 assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
408 }
409 }
410}