alien_core/
gateability.rs1use crate::ownership_policy_for_resource_type;
15
16pub const SECRETS_VAULT_ID: &str = "secrets";
23
24const STACK_DERIVED_TYPES: &[&str] = &[
32 "build",
33 "artifact-registry",
34 "service-account",
35 "compute-cluster",
36 "kubernetes-cluster",
37 "network",
38 "remote-stack-management",
39 "service_activation",
40 "service-activation",
41 "azure_resource_group",
42 "azure-resource-group",
43 "azure_storage_account",
44 "azure-storage-account",
45 "azure_container_apps_environment",
46 "azure-container-apps-environment",
47 "azure_service_bus_namespace",
48 "azure-service-bus-namespace",
49];
50
51const NOT_YET_GENERIC_TYPES: &[&str] = &[];
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum GateRefusal {
60 ReservedSecretsVault,
62 DerivedFromStack,
64 NotYetGeneric,
66}
67
68impl GateRefusal {
69 pub fn reason(self) -> &'static str {
73 match self {
74 GateRefusal::ReservedSecretsVault => {
75 "it is the deployment secrets vault. Workers and compute clusters are wired to \
76 it automatically after compile-time checks run, so a deployer who says no would \
77 leave them resolving a binding for a vault that was never created. Its presence \
78 cannot be optional. Give a vault you want to gate a different id"
79 }
80 GateRefusal::DerivedFromStack => {
81 "Alien derives this resource from the stack itself, so it cannot be optional"
82 }
83 GateRefusal::NotYetGeneric => {
84 "this resource type's conditional setup render has not been validated yet, so \
85 the resource would be created regardless of the deployer's answer"
86 }
87 }
88 }
89}
90
91pub fn gate_refusal(resource_type: &str, resource_id: &str) -> Option<GateRefusal> {
95 if resource_id == SECRETS_VAULT_ID {
96 return Some(GateRefusal::ReservedSecretsVault);
97 }
98 if STACK_DERIVED_TYPES.contains(&resource_type) {
99 return Some(GateRefusal::DerivedFromStack);
100 }
101 if NOT_YET_GENERIC_TYPES.contains(&resource_type) {
107 return Some(GateRefusal::NotYetGeneric);
108 }
109 None
110}
111
112#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
116#[serde(rename_all = "camelCase")]
117pub struct TypeGateability {
118 pub frozen: bool,
120 pub live: bool,
122}
123
124pub fn type_gateability(resource_type: &str) -> TypeGateability {
126 let policy = ownership_policy_for_resource_type(resource_type);
127 let gateable = gate_refusal(resource_type, "").is_none();
130 TypeGateability {
131 frozen: gateable && policy.allows_frozen(),
132 live: gateable && policy.allows_live(),
133 }
134}
135
136pub const MANIFEST_TYPES: &[&str] = &[
139 "kv",
140 "storage",
141 "queue",
142 "vault",
143 "postgres",
144 "ai",
145 "worker",
146 "daemon",
147 "container",
148 "email",
149 "experimental/aws-opensearch",
150];
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn stores_are_gateable_in_both_lifecycles() {
158 for store in ["kv", "storage", "queue", "vault", "ai"] {
159 assert_eq!(gate_refusal(store, "analytics"), None, "{store}");
160 let gateability = type_gateability(store);
161 assert!(gateability.frozen && gateability.live, "{store}");
162 }
163 }
164
165 #[test]
166 fn postgres_is_live_gateable_only() {
167 assert_eq!(gate_refusal("postgres", "db"), None);
168 let gateability = type_gateability("postgres");
169 assert!(gateability.live);
173 }
174
175 #[test]
176 fn compute_is_live_gateable() {
177 for compute in ["worker", "daemon", "container"] {
178 assert_eq!(gate_refusal(compute, "api"), None, "{compute}");
179 let gateability = type_gateability(compute);
180 assert!(!gateability.frozen, "{compute} cannot be frozen");
181 assert!(gateability.live, "{compute} gates as a live resource");
182 }
183 }
184
185 #[test]
186 fn stack_derived_types_are_refused() {
187 for framework in STACK_DERIVED_TYPES {
188 assert_eq!(
189 gate_refusal(framework, "x"),
190 Some(GateRefusal::DerivedFromStack),
191 "{framework}"
192 );
193 }
194 }
195
196 #[test]
197 fn the_reserved_secrets_vault_is_refused_by_id() {
198 assert_eq!(
199 gate_refusal("vault", SECRETS_VAULT_ID),
200 Some(GateRefusal::ReservedSecretsVault)
201 );
202 assert_eq!(gate_refusal("vault", "app-tokens"), None);
203 }
204
205 #[test]
206 fn email_and_opensearch_gate_as_frozen_resources() {
207 for setup_owned in ["email", "experimental/aws-opensearch"] {
208 assert_eq!(gate_refusal(setup_owned, "x"), None, "{setup_owned}");
209 let gateability = type_gateability(setup_owned);
210 assert!(gateability.frozen, "{setup_owned} gates at setup");
211 assert!(!gateability.live, "{setup_owned} has no runtime controller");
212 }
213 }
214
215 #[test]
216 fn extension_types_default_to_gateable() {
217 assert_eq!(gate_refusal("acme-widgets", "widgets"), None);
218 let gateability = type_gateability("acme-widgets");
219 assert!(gateability.frozen && gateability.live);
220 }
221}