Skip to main content

alien_core/
setup_package.rs

1use crate::{
2    ownership_policy_for_resource_type, ErrorData, Key, ManagementPermissions, Platform,
3    ResourceEntry, ResourceLifecycle, Result, Sandbox, SetupManagement, Stack, Storage,
4};
5use alien_error::AlienError;
6
7/// Refuse resources whose setup-owned scaffolding cannot yet be created by runtime.
8/// Check both package generation and deployment planning, including later additions.
9pub fn validate_automatic_setup_resources(
10    stack: &Stack,
11    platform: Platform,
12    management: SetupManagement,
13) -> Result<()> {
14    if management == SetupManagement::Automatic
15        && matches!(platform, Platform::Aws | Platform::Gcp | Platform::Azure)
16    {
17        let unsupported = |resource_type: &str, resource_id: &str| {
18            AlienError::new(ErrorData::AutomaticSetupResourceUnsupported {
19                resource_type: resource_type.to_string(),
20                resource_id: resource_id.to_string(),
21                platform,
22            })
23        };
24        match stack.management() {
25            // Setup must be able to grant whatever a later release adds.
26            ManagementPermissions::Override(_) => {
27                return Err(unsupported(
28                    "management permission override",
29                    "permissions.management",
30                ));
31            }
32            // The AWS management role may not change its own policies after setup, so a
33            // grant scoped to one resource could never be attached by the runtime.
34            ManagementPermissions::Extend(profile) if platform == Platform::Aws => {
35                if let Some(scope) = profile.0.keys().find(|scope| scope.as_str() != "*") {
36                    return Err(unsupported("resource-scoped management permission", scope));
37                }
38            }
39            _ => {}
40        }
41        for (id, entry) in stack.resources() {
42            if let Some(resource_type) = automatic_setup_unsupported_resource(entry, platform) {
43                return Err(unsupported(&resource_type, id));
44            }
45        }
46    }
47    if management == SetupManagement::Automatic && platform == Platform::Azure {
48        for (id, entry) in stack.resources() {
49            if entry.config.downcast_ref::<Sandbox>().is_some() {
50                return Err(AlienError::new(ErrorData::SandboxCapabilityUnsupported {
51                    capability: format!(
52                        "automatic infrastructure management for sandbox '{id}'; use manual setup to create and import its sandbox group"
53                    ),
54                    platform: platform.to_string(),
55                }));
56            }
57        }
58    }
59    Ok(())
60}
61
62/// Return resources whose declarations cannot be created by their runtime
63/// controllers when omitted from automatic setup.
64fn automatic_setup_unsupported_resource(
65    entry: &ResourceEntry,
66    platform: Platform,
67) -> Option<String> {
68    if entry.config.downcast_ref::<Key>().is_some() {
69        return Some("key".to_string());
70    }
71    let resource_type = entry.config.resource_type();
72    // These controllers refuse runtime creation: only a setup package can provision them.
73    if matches!(
74        resource_type.as_ref(),
75        "email" | "experimental/aws-opensearch" | "kubernetes-cluster"
76    ) {
77        return Some(resource_type.as_ref().to_string());
78    }
79    // Only the CloudFormation template applies S3 CORS rules. The runtime storage
80    // controller would create the bucket without them, so browser reads would fail.
81    if platform == Platform::Aws
82        && entry
83            .config
84            .downcast_ref::<Storage>()
85            .is_some_and(|storage| !storage.cors_allowed_origins.is_empty())
86    {
87        return Some("storage with CORS origins".to_string());
88    }
89    None
90}
91
92/// Controls configuration exposure, independently of installation authority.
93/// Existing generator entry points retain the complete Advanced interface.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum SetupConfigurationProfile {
96    Standard,
97    Advanced,
98}
99
100/// Automatic packages install access and secret storage, then let the runtime
101/// create application infrastructure. Cloud API activation and Azure's containing
102/// resource group must exist before their management identity can be installed.
103/// Azure also installs application identities so management can name their
104/// principals in the role-assignment conditions.
105pub fn emits_package_scaffolding(
106    entry: &ResourceEntry,
107    management: SetupManagement,
108    platform: Platform,
109) -> bool {
110    let resource_type = entry.config.resource_type();
111    if management == SetupManagement::Automatic {
112        is_automatic_setup_package_resource_type(resource_type.as_ref(), platform)
113    } else {
114        ownership_policy_for_resource_type(resource_type.as_ref())
115            .emits_setup_scaffolding(entry.lifecycle)
116    }
117}
118
119/// Resources whose state is replaced by an automatic setup package import.
120/// Application resources are managed through the installed identity and must
121/// retain their controller state when that package is reimported.
122pub fn is_automatic_setup_package_resource_type(resource_type: &str, platform: Platform) -> bool {
123    matches!(
124        resource_type,
125        "remote-stack-management" | "vault" | "azure_resource_group" | "service_activation"
126    ) || (platform == Platform::Azure
127        && matches!(resource_type, "service-account" | "resource-access"))
128}
129
130/// Whether deleting an installation through runtime cleanup removes this resource before the
131/// setup package's own teardown runs.
132///
133/// An automatic package creates only its scaffolding types (see
134/// [`is_automatic_setup_package_resource_type`]). The runtime created everything else, Frozen
135/// resources included, and the package's teardown cannot see those, so runtime cleanup removes
136/// them. It leaves the package's resources, Live vaults included, to that teardown. Any other
137/// package owns its Frozen resources, apart from types that need runtime cleanup first.
138pub fn removed_by_runtime_cleanup(
139    resource_type: &str,
140    lifecycle: ResourceLifecycle,
141    management: SetupManagement,
142    platform: Platform,
143) -> bool {
144    if management == SetupManagement::Automatic {
145        return !is_automatic_setup_package_resource_type(resource_type, platform);
146    }
147    lifecycle == ResourceLifecycle::Live
148        || ownership_policy_for_resource_type(resource_type).has_runtime_cleanup_before_teardown()
149}