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        | "resource-access"
83        | "service-account"
84        | "service_activation"
85        | "service-activation"
86        | "azure_resource_group"
87        | "azure-resource-group"
88        | "azure_storage_account"
89        | "azure-storage-account"
90        | "azure_container_apps_environment"
91        | "azure-container-apps-environment"
92        | "azure_service_bus_namespace"
93        | "azure-service-bus-namespace"
94        // Email holds durable routing state (domain identities, DKIM
95        // verification, receipt rules) that setup owns end to end.
96        | "email" => frozen_only(),
97        // Durable search state, setup-owned only: there is no runtime
98        // controller that could provision or replace the collection.
99        "experimental/aws-opensearch" => frozen_only(),
100        "storage" | "queue" | "kv" | "vault" | "postgres" | "ai" => user_choice(),
101        _ => user_choice(),
102    }
103}
104
105const fn frozen_only() -> ResourceOwnershipPolicy {
106    ResourceOwnershipPolicy::new(ResourceLifecycle::Frozen, true, false, true, false, false)
107}
108
109const fn frozen_with_management() -> ResourceOwnershipPolicy {
110    ResourceOwnershipPolicy::new(ResourceLifecycle::Frozen, true, false, true, true, false)
111}
112
113const fn frozen_with_runtime_cleanup() -> ResourceOwnershipPolicy {
114    ResourceOwnershipPolicy::new(ResourceLifecycle::Frozen, true, false, true, true, true)
115}
116
117const fn live_only() -> ResourceOwnershipPolicy {
118    ResourceOwnershipPolicy::new(ResourceLifecycle::Live, false, true, false, false, false)
119}
120
121const fn removed_resource_type() -> ResourceOwnershipPolicy {
122    ResourceOwnershipPolicy::new(ResourceLifecycle::Live, false, false, false, false, false)
123}
124
125const fn user_choice() -> ResourceOwnershipPolicy {
126    ResourceOwnershipPolicy::new(ResourceLifecycle::Frozen, true, true, true, false, false)
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn workload_resources_are_live_only() {
135        for resource_type in ["worker", "daemon", "container"] {
136            let policy = ownership_policy_for_resource_type(resource_type);
137            assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Live);
138            assert!(!policy.allows_lifecycle(ResourceLifecycle::Frozen));
139            assert!(policy.allows_lifecycle(ResourceLifecycle::Live));
140            assert!(!policy.should_emit_in_setup(ResourceLifecycle::Live));
141        }
142    }
143
144    #[test]
145    fn compute_cluster_is_frozen_with_runtime_cleanup() {
146        let policy = ownership_policy_for_resource_type("compute-cluster");
147        assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Frozen);
148        assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
149        assert!(!policy.allows_lifecycle(ResourceLifecycle::Live));
150        assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
151        assert!(policy.requires_management_permissions());
152        assert!(policy.has_runtime_cleanup_before_teardown());
153    }
154
155    #[test]
156    fn artifact_registry_is_frozen_with_management() {
157        let policy = ownership_policy_for_resource_type("artifact-registry");
158        assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Frozen);
159        assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
160        assert!(!policy.allows_lifecycle(ResourceLifecycle::Live));
161        assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
162        assert!(policy.requires_management_permissions());
163        assert!(!policy.has_runtime_cleanup_before_teardown());
164    }
165
166    #[test]
167    fn removed_resource_type_tags_are_not_normal_policy_entries() {
168        for resource_type in ["function", "container-cluster"] {
169            let policy = ownership_policy_for_resource_type(resource_type);
170            assert!(!policy.allows_lifecycle(ResourceLifecycle::Frozen));
171            assert!(!policy.allows_lifecycle(ResourceLifecycle::Live));
172            assert!(!policy.requires_management_permissions());
173            assert!(!policy.has_runtime_cleanup_before_teardown());
174        }
175    }
176
177    #[test]
178    fn data_resources_can_be_frozen_or_live() {
179        for resource_type in ["storage", "queue", "kv", "vault", "postgres", "ai"] {
180            let policy = ownership_policy_for_resource_type(resource_type);
181            assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Frozen);
182            assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
183            assert!(policy.allows_lifecycle(ResourceLifecycle::Live));
184            assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
185            assert!(!policy.should_emit_in_setup(ResourceLifecycle::Live));
186        }
187    }
188
189    #[test]
190    fn experimental_aws_opensearch_is_frozen_only() {
191        let policy = ownership_policy_for_resource_type("experimental/aws-opensearch");
192        assert_eq!(policy.default_lifecycle(), ResourceLifecycle::Frozen);
193        assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
194        assert!(!policy.allows_lifecycle(ResourceLifecycle::Live));
195        assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
196        assert!(!policy.requires_management_permissions());
197    }
198
199    #[test]
200    fn setup_resources_are_frozen_only() {
201        for resource_type in [
202            "build",
203            "network",
204            "remote-stack-management",
205            "resource-access",
206            "service-account",
207            "service_activation",
208            "azure_resource_group",
209            "azure_storage_account",
210            "azure_container_apps_environment",
211            "azure_service_bus_namespace",
212            "email",
213        ] {
214            let policy = ownership_policy_for_resource_type(resource_type);
215            assert!(policy.allows_lifecycle(ResourceLifecycle::Frozen));
216            assert!(!policy.allows_lifecycle(ResourceLifecycle::Live));
217            assert!(policy.should_emit_in_setup(ResourceLifecycle::Frozen));
218        }
219    }
220}