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 "worker",
145 "daemon",
146 "container",
147 "email",
148 "experimental/aws-opensearch",
149];
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn stores_are_gateable_in_both_lifecycles() {
157 for store in ["kv", "storage", "queue", "vault"] {
158 assert_eq!(gate_refusal(store, "analytics"), None, "{store}");
159 let gateability = type_gateability(store);
160 assert!(gateability.frozen && gateability.live, "{store}");
161 }
162 }
163
164 #[test]
165 fn postgres_is_live_gateable_only() {
166 assert_eq!(gate_refusal("postgres", "db"), None);
167 let gateability = type_gateability("postgres");
168 assert!(gateability.live);
172 }
173
174 #[test]
175 fn compute_is_live_gateable() {
176 for compute in ["worker", "daemon", "container"] {
177 assert_eq!(gate_refusal(compute, "api"), None, "{compute}");
178 let gateability = type_gateability(compute);
179 assert!(!gateability.frozen, "{compute} cannot be frozen");
180 assert!(gateability.live, "{compute} gates as a live resource");
181 }
182 }
183
184 #[test]
185 fn stack_derived_types_are_refused() {
186 for framework in STACK_DERIVED_TYPES {
187 assert_eq!(
188 gate_refusal(framework, "x"),
189 Some(GateRefusal::DerivedFromStack),
190 "{framework}"
191 );
192 }
193 }
194
195 #[test]
196 fn the_reserved_secrets_vault_is_refused_by_id() {
197 assert_eq!(
198 gate_refusal("vault", SECRETS_VAULT_ID),
199 Some(GateRefusal::ReservedSecretsVault)
200 );
201 assert_eq!(gate_refusal("vault", "app-tokens"), None);
202 }
203
204 #[test]
205 fn email_and_opensearch_gate_as_frozen_resources() {
206 for setup_owned in ["email", "experimental/aws-opensearch"] {
207 assert_eq!(gate_refusal(setup_owned, "x"), None, "{setup_owned}");
208 let gateability = type_gateability(setup_owned);
209 assert!(gateability.frozen, "{setup_owned} gates at setup");
210 assert!(!gateability.live, "{setup_owned} has no runtime controller");
211 }
212 }
213
214 #[test]
215 fn extension_types_default_to_gateable() {
216 assert_eq!(gate_refusal("acme-widgets", "widgets"), None);
217 let gateability = type_gateability("acme-widgets");
218 assert!(gateability.frozen && gateability.live);
219 }
220}