Skip to main content

alien_core/
ownership.rs

1use crate::ResourceLifecycle;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub struct ResourceOwnershipPolicy {
5    default_lifecycle: ResourceLifecycle,
6    allow_frozen: bool,
7    allow_live: bool,
8    emit_in_setup: bool,
9    requires_management_permissions: bool,
10    runtime_cleanup_before_teardown: bool,
11}
12
13impl ResourceOwnershipPolicy {
14    pub const fn new(
15        default_lifecycle: ResourceLifecycle,
16        allow_frozen: bool,
17        allow_live: bool,
18        emit_in_setup: bool,
19        requires_management_permissions: bool,
20        runtime_cleanup_before_teardown: bool,
21    ) -> Self {
22        Self {
23            default_lifecycle,
24            allow_frozen,
25            allow_live,
26            emit_in_setup,
27            requires_management_permissions,
28            runtime_cleanup_before_teardown,
29        }
30    }
31
32    pub const fn default_lifecycle(self) -> ResourceLifecycle {
33        self.default_lifecycle
34    }
35
36    pub const fn allows_frozen(self) -> bool {
37        self.allow_frozen
38    }
39
40    pub const fn allows_live(self) -> bool {
41        self.allow_live
42    }
43
44    pub const fn allows_lifecycle(self, lifecycle: ResourceLifecycle) -> bool {
45        match lifecycle {
46            ResourceLifecycle::Frozen => self.allow_frozen,
47            ResourceLifecycle::Live => self.allow_live,
48        }
49    }
50
51    pub const fn should_emit_in_setup(self, lifecycle: ResourceLifecycle) -> bool {
52        self.emit_in_setup && matches!(lifecycle, ResourceLifecycle::Frozen)
53    }
54
55    pub const fn requires_management_permissions(self) -> bool {
56        self.requires_management_permissions
57    }
58
59    pub const fn has_runtime_cleanup_before_teardown(self) -> bool {
60        self.runtime_cleanup_before_teardown
61    }
62
63    pub fn allowed_lifecycles(self) -> &'static str {
64        match (self.allow_frozen, self.allow_live) {
65            (true, true) => "Frozen or Live",
66            (true, false) => "Frozen",
67            (false, true) => "Live",
68            (false, false) => "no lifecycle",
69        }
70    }
71}
72
73pub fn ownership_policy_for_resource_type(resource_type: &str) -> ResourceOwnershipPolicy {
74    match resource_type {
75        "function" | "container-cluster" => removed_resource_type(),
76        "worker" | "daemon" | "container" => live_only(),
77        "compute-cluster" => frozen_with_runtime_cleanup(),
78        "artifact-registry" => frozen_with_management(),
79        "build"
80        | "network"
81        | "remote-stack-management"
82        | "service-account"
83        | "service_activation"
84        | "service-activation"
85        | "azure_resource_group"
86        | "azure-resource-group"
87        | "azure_storage_account"
88        | "azure-storage-account"
89        | "azure_container_apps_environment"
90        | "azure-container-apps-environment"
91        | "azure_service_bus_namespace"
92        | "azure-service-bus-namespace"
93        // Email holds durable routing state (domain identities, DKIM
94        // verification, receipt rules) that setup owns end to end.
95        | "email" => frozen_only(),
96        // Durable search state, setup-owned only: there is no runtime
97        // controller that could provision or replace the collection.
98        "experimental/aws-opensearch" => frozen_only(),
99        "storage" | "queue" | "kv" | "vault" | "postgres" | "ai" => user_choice(),
100        _ => user_choice(),
101    }
102}
103
104const fn frozen_only() -> ResourceOwnershipPolicy {
105    ResourceOwnershipPolicy::new(ResourceLifecycle::Frozen, true, false, true, false, false)
106}
107
108const fn frozen_with_management() -> ResourceOwnershipPolicy {
109    ResourceOwnershipPolicy::new(ResourceLifecycle::Frozen, true, false, true, true, false)
110}
111
112const fn frozen_with_runtime_cleanup() -> ResourceOwnershipPolicy {
113    ResourceOwnershipPolicy::new(ResourceLifecycle::Frozen, true, false, true, true, true)
114}
115
116const fn live_only() -> ResourceOwnershipPolicy {
117    ResourceOwnershipPolicy::new(ResourceLifecycle::Live, false, true, false, false, false)
118}
119
120const fn removed_resource_type() -> ResourceOwnershipPolicy {
121    ResourceOwnershipPolicy::new(ResourceLifecycle::Live, false, false, false, false, false)
122}
123
124const fn user_choice() -> ResourceOwnershipPolicy {
125    ResourceOwnershipPolicy::new(ResourceLifecycle::Frozen, true, true, true, false, false)
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn workload_resources_are_live_only() {
134        for resource_type in ["worker", "daemon", "container"] {
135            let policy = ownership_policy_for_resource_type(resource_type);
136            assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Live);
137            assert!(!policy.allows_lifecycle(ResourceLifecycle::Frozen));
138            assert!(policy.allows_lifecycle(ResourceLifecycle::Live));
139            assert!(!policy.should_emit_in_setup(ResourceLifecycle::Live));
140        }
141    }
142
143    #[test]
144    fn compute_cluster_is_frozen_with_runtime_cleanup() {
145        let policy = ownership_policy_for_resource_type("compute-cluster");
146        assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Frozen);
147        assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
148        assert!(!policy.allows_lifecycle(ResourceLifecycle::Live));
149        assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
150        assert!(policy.requires_management_permissions());
151        assert!(policy.has_runtime_cleanup_before_teardown());
152    }
153
154    #[test]
155    fn artifact_registry_is_frozen_with_management() {
156        let policy = ownership_policy_for_resource_type("artifact-registry");
157        assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Frozen);
158        assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
159        assert!(!policy.allows_lifecycle(ResourceLifecycle::Live));
160        assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
161        assert!(policy.requires_management_permissions());
162        assert!(!policy.has_runtime_cleanup_before_teardown());
163    }
164
165    #[test]
166    fn removed_resource_type_tags_are_not_normal_policy_entries() {
167        for resource_type in ["function", "container-cluster"] {
168            let policy = ownership_policy_for_resource_type(resource_type);
169            assert!(!policy.allows_lifecycle(ResourceLifecycle::Frozen));
170            assert!(!policy.allows_lifecycle(ResourceLifecycle::Live));
171            assert!(!policy.requires_management_permissions());
172            assert!(!policy.has_runtime_cleanup_before_teardown());
173        }
174    }
175
176    #[test]
177    fn data_resources_can_be_frozen_or_live() {
178        for resource_type in ["storage", "queue", "kv", "vault", "postgres", "ai"] {
179            let policy = ownership_policy_for_resource_type(resource_type);
180            assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Frozen);
181            assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
182            assert!(policy.allows_lifecycle(ResourceLifecycle::Live));
183            assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
184            assert!(!policy.should_emit_in_setup(ResourceLifecycle::Live));
185        }
186    }
187
188    #[test]
189    fn experimental_aws_opensearch_is_frozen_only() {
190        let policy = ownership_policy_for_resource_type("experimental/aws-opensearch");
191        assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Frozen);
192        assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
193        assert!(!policy.allows_lifecycle(ResourceLifecycle::Live));
194        assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
195        assert!(!policy.requires_management_permissions());
196    }
197
198    #[test]
199    fn setup_resources_are_frozen_only() {
200        for resource_type in [
201            "build",
202            "network",
203            "remote-stack-management",
204            "service-account",
205            "service_activation",
206            "azure_resource_group",
207            "azure_storage_account",
208            "azure_container_apps_environment",
209            "azure_service_bus_namespace",
210            "email",
211        ] {
212            let policy = ownership_policy_for_resource_type(resource_type);
213            assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
214            assert!(!policy.allows_lifecycle(ResourceLifecycle::Live));
215            assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
216        }
217    }
218}