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