1use 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 "sandbox",
151 "experimental/aws-opensearch",
152];
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn stores_are_gateable_in_both_lifecycles() {
160 for store in ["kv", "storage", "queue", "vault", "ai"] {
161 assert_eq!(gate_refusal(store, "analytics"), None, "{store}");
162 let gateability = type_gateability(store);
163 assert!(gateability.frozen && gateability.live, "{store}");
164 }
165 }
166
167 #[test]
168 fn postgres_is_live_gateable_only() {
169 assert_eq!(gate_refusal("postgres", "db"), None);
170 let gateability = type_gateability("postgres");
171 assert!(gateability.live);
175 }
176
177 #[test]
178 fn compute_is_live_gateable() {
179 for compute in ["worker", "daemon", "container"] {
180 assert_eq!(gate_refusal(compute, "api"), None, "{compute}");
181 let gateability = type_gateability(compute);
182 assert!(!gateability.frozen, "{compute} cannot be frozen");
183 assert!(gateability.live, "{compute} gates as a live resource");
184 }
185 }
186
187 #[test]
188 fn stack_derived_types_are_refused() {
189 for framework in STACK_DERIVED_TYPES {
190 assert_eq!(
191 gate_refusal(framework, "x"),
192 Some(GateRefusal::DerivedFromStack),
193 "{framework}"
194 );
195 }
196 }
197
198 #[test]
199 fn the_reserved_secrets_vault_is_refused_by_id() {
200 assert_eq!(
201 gate_refusal("vault", SECRETS_VAULT_ID),
202 Some(GateRefusal::ReservedSecretsVault)
203 );
204 assert_eq!(gate_refusal("vault", "app-tokens"), None);
205 }
206
207 #[test]
208 fn email_and_opensearch_gate_as_frozen_resources() {
209 for setup_owned in ["email", "experimental/aws-opensearch"] {
210 assert_eq!(gate_refusal(setup_owned, "x"), None, "{setup_owned}");
211 let gateability = type_gateability(setup_owned);
212 assert!(gateability.frozen, "{setup_owned} gates at setup");
213 assert!(!gateability.live, "{setup_owned} has no runtime controller");
214 }
215 }
216
217 #[test]
221 fn sandbox_gates_under_either_lifecycle() {
222 assert_eq!(gate_refusal("sandbox", "agents"), None);
223 let gateability = type_gateability("sandbox");
224 assert!(gateability.frozen, "a setup-baked image gates at setup");
225 assert!(
226 gateability.live,
227 "a runtime-built image gates in the runtime strip"
228 );
229 }
230
231 #[test]
235 fn every_manifest_type_is_asserted_somewhere_above() {
236 let asserted = [
237 "kv",
238 "storage",
239 "queue",
240 "vault",
241 "ai",
242 "postgres",
243 "worker",
244 "daemon",
245 "container",
246 "email",
247 "experimental/aws-opensearch",
248 "sandbox",
249 ];
250 for declared in MANIFEST_TYPES {
251 assert!(
252 asserted.contains(declared),
253 "{declared} is in the manifest but no test pins its gateability"
254 );
255 }
256 }
257
258 #[test]
259 fn extension_types_default_to_gateable() {
260 assert_eq!(gate_refusal("acme-widgets", "widgets"), None);
261 let gateability = type_gateability("acme-widgets");
262 assert!(gateability.frozen && gateability.live);
263 }
264}