Skip to main content

alien_core/import/data/
mod.rs

1//! Typed ImportData payloads by cloud and resource.
2
3use serde::{Deserialize, Deserializer};
4
5pub mod aws;
6pub mod azure;
7pub mod gcp;
8pub mod kubernetes_cluster;
9
10pub use aws::{
11    AwsAiImportData, AwsArtifactRegistryImportData, AwsBuildImportData,
12    AwsComputeClusterImportData, AwsEmailDkimTokenImportData, AwsEmailDomainImportData,
13    AwsEmailImportData, AwsKeyImportData, AwsKvImportData, AwsNetworkImportData,
14    AwsOpenSearchImportData, AwsPostgresImportData, AwsQueueImportData,
15    AwsRemoteBindingsImportData, AwsRemoteStackManagementImportData, AwsSandboxImportData,
16    AwsServiceAccountImportData, AwsStorageImportData, AwsVaultImportData, AwsWorkerImportData,
17};
18pub use azure::{
19    AzureAiImportData, AzureArtifactRegistryImportData, AzureBuildImportData,
20    AzureComputeClusterImportData, AzureContainerAppsEnvironmentImportData,
21    AzureFlexibleServerPostgresImportData, AzureKeyImportData, AzureKvImportData,
22    AzureNetworkImportData, AzureQueueImportData, AzureRemoteBindingsImportData,
23    AzureRemoteStackManagementImportData, AzureResourceGroupImportData, AzureSandboxImportData,
24    AzureServiceAccountImportData, AzureServiceActivationImportData,
25    AzureServiceBusNamespaceImportData, AzureStorageAccountImportData, AzureStorageImportData,
26    AzureVaultImportData, AzureWorkerImportData,
27};
28pub use gcp::{
29    GcpAiImportData, GcpArtifactRegistryImportData, GcpBuildImportData,
30    GcpComputeClusterImportData, GcpKeyImportData, GcpKvImportData, GcpNetworkImportData,
31    GcpPostgresImportData, GcpQueueImportData, GcpRemoteBindingsImportData,
32    GcpRemoteStackManagementImportData, GcpServiceAccountImportData,
33    GcpServiceActivationImportData, GcpStorageImportData, GcpVaultImportData, GcpWorkerImportData,
34};
35pub use kubernetes_cluster::{
36    AzureApplicationGatewayForContainersBootstrap, KubernetesClusterImportData,
37};
38
39pub(crate) fn deserialize_bool_from_bool_or_string<'de, D>(
40    deserializer: D,
41) -> Result<bool, D::Error>
42where
43    D: Deserializer<'de>,
44{
45    let value = serde_json::Value::deserialize(deserializer)?;
46    match value {
47        serde_json::Value::Bool(value) => Ok(value),
48        serde_json::Value::String(value) if value.eq_ignore_ascii_case("true") => Ok(true),
49        serde_json::Value::String(value) if value.eq_ignore_ascii_case("false") => Ok(false),
50        other => Err(serde::de::Error::custom(format!(
51            "expected boolean or boolean string, got {other}"
52        ))),
53    }
54}
55
56/// Same reason as the boolean helper: CloudFormation stringifies the leaves of a custom
57/// resource's properties, so a declared port list arrives as `["3000"]` rather than `[3000]`.
58pub(crate) fn deserialize_u16_vec_from_numbers_or_strings<'de, D>(
59    deserializer: D,
60) -> Result<Vec<u16>, D::Error>
61where
62    D: Deserializer<'de>,
63{
64    let values = Vec::<serde_json::Value>::deserialize(deserializer)?;
65    values
66        .into_iter()
67        .map(|value| match value {
68            serde_json::Value::Number(number) => number
69                .as_u64()
70                .and_then(|n| u16::try_from(n).ok())
71                .ok_or_else(|| serde::de::Error::custom(format!("port {number} is out of range"))),
72            serde_json::Value::String(text) => text.parse::<u16>().map_err(|_| {
73                serde::de::Error::custom(format!("expected a port number, got {text:?}"))
74            }),
75            other => Err(serde::de::Error::custom(format!(
76                "expected a port number or numeric string, got {other}"
77            ))),
78        })
79        .collect()
80}
81
82#[cfg(all(test, feature = "jsonschema"))]
83mod schema_snapshots {
84    use super::*;
85    use indexmap::IndexMap;
86
87    fn schema<T: schemars::JsonSchema>() -> serde_json::Value {
88        serde_json::to_value(schemars::schema_for!(T)).expect("schema should serialize")
89    }
90
91    #[test]
92    fn import_data_schema_snapshot() {
93        let schemas = IndexMap::from([
94            ("aws_ai", schema::<AwsAiImportData>()),
95            (
96                "aws_artifact_registry",
97                schema::<AwsArtifactRegistryImportData>(),
98            ),
99            ("aws_build", schema::<AwsBuildImportData>()),
100            (
101                "aws_compute_cluster",
102                schema::<AwsComputeClusterImportData>(),
103            ),
104            ("aws_email", schema::<AwsEmailImportData>()),
105            ("aws_function", schema::<AwsWorkerImportData>()),
106            ("aws_kv", schema::<AwsKvImportData>()),
107            ("aws_key", schema::<AwsKeyImportData>()),
108            ("aws_sandbox", schema::<AwsSandboxImportData>()),
109            ("aws_network", schema::<AwsNetworkImportData>()),
110            ("aws_open_search", schema::<AwsOpenSearchImportData>()),
111            ("aws_postgres", schema::<AwsPostgresImportData>()),
112            ("aws_queue", schema::<AwsQueueImportData>()),
113            (
114                "aws_remote_stack_management",
115                schema::<AwsRemoteStackManagementImportData>(),
116            ),
117            (
118                "aws_service_account",
119                schema::<AwsServiceAccountImportData>(),
120            ),
121            ("aws_storage", schema::<AwsStorageImportData>()),
122            ("aws_vault", schema::<AwsVaultImportData>()),
123            ("azure_ai", schema::<AzureAiImportData>()),
124            (
125                "azure_artifact_registry",
126                schema::<AzureArtifactRegistryImportData>(),
127            ),
128            ("azure_build", schema::<AzureBuildImportData>()),
129            (
130                "azure_container_apps_environment",
131                schema::<AzureContainerAppsEnvironmentImportData>(),
132            ),
133            (
134                "azure_compute_cluster",
135                schema::<AzureComputeClusterImportData>(),
136            ),
137            ("azure_function", schema::<AzureWorkerImportData>()),
138            ("azure_kv", schema::<AzureKvImportData>()),
139            ("azure_key", schema::<AzureKeyImportData>()),
140            ("azure_sandbox", schema::<AzureSandboxImportData>()),
141            ("azure_network", schema::<AzureNetworkImportData>()),
142            (
143                "azure_postgres",
144                schema::<AzureFlexibleServerPostgresImportData>(),
145            ),
146            ("azure_queue", schema::<AzureQueueImportData>()),
147            (
148                "azure_remote_stack_management",
149                schema::<AzureRemoteStackManagementImportData>(),
150            ),
151            (
152                "azure_resource_group",
153                schema::<AzureResourceGroupImportData>(),
154            ),
155            (
156                "azure_service_account",
157                schema::<AzureServiceAccountImportData>(),
158            ),
159            (
160                "azure_service_activation",
161                schema::<AzureServiceActivationImportData>(),
162            ),
163            (
164                "azure_service_bus_namespace",
165                schema::<AzureServiceBusNamespaceImportData>(),
166            ),
167            ("azure_storage", schema::<AzureStorageImportData>()),
168            (
169                "azure_storage_account",
170                schema::<AzureStorageAccountImportData>(),
171            ),
172            ("azure_vault", schema::<AzureVaultImportData>()),
173            ("gcp_ai", schema::<GcpAiImportData>()),
174            (
175                "gcp_artifact_registry",
176                schema::<GcpArtifactRegistryImportData>(),
177            ),
178            ("gcp_build", schema::<GcpBuildImportData>()),
179            (
180                "gcp_compute_cluster",
181                schema::<GcpComputeClusterImportData>(),
182            ),
183            ("gcp_function", schema::<GcpWorkerImportData>()),
184            ("gcp_kv", schema::<GcpKvImportData>()),
185            ("gcp_key", schema::<GcpKeyImportData>()),
186            ("gcp_network", schema::<GcpNetworkImportData>()),
187            ("gcp_postgres", schema::<GcpPostgresImportData>()),
188            ("gcp_queue", schema::<GcpQueueImportData>()),
189            (
190                "gcp_remote_stack_management",
191                schema::<GcpRemoteStackManagementImportData>(),
192            ),
193            (
194                "kubernetes_cluster",
195                schema::<KubernetesClusterImportData>(),
196            ),
197            (
198                "gcp_service_account",
199                schema::<GcpServiceAccountImportData>(),
200            ),
201            (
202                "gcp_service_activation",
203                schema::<GcpServiceActivationImportData>(),
204            ),
205            ("gcp_storage", schema::<GcpStorageImportData>()),
206            ("gcp_vault", schema::<GcpVaultImportData>()),
207        ]);
208
209        insta::assert_json_snapshot!("import_data_schemas", schemas);
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use serde_json::json;
217
218    #[test]
219    fn aws_import_data_accepts_cloudformation_stringified_leaves() {
220        let network: AwsNetworkImportData = serde_json::from_value(json!({
221            "vpcId": "vpc-123",
222            "cidrBlock": null,
223            "internetGatewayId": null,
224            "natGatewayId": null,
225            "eipAllocationId": null,
226            "publicSubnetIds": ["subnet-public"],
227            "privateSubnetIds": ["subnet-private"],
228            "publicRouteTableId": null,
229            "privateRouteTableId": null,
230            "securityGroupId": "sg-123",
231            "availabilityZones": [],
232            "isByoVpc": "true",
233        }))
234        .expect("network import data should parse");
235        assert!(network.is_byo_vpc);
236
237        let remote_stack_management: AwsRemoteStackManagementImportData =
238            serde_json::from_value(json!({
239                "roleName": "alien-manager",
240                "roleArn": "arn:aws:iam::123456789012:role/alien-manager",
241                "managementPermissionsApplied": "true",
242            }))
243            .expect("remote stack management import data should parse");
244        assert!(remote_stack_management.management_permissions_applied);
245
246        let service_account: AwsServiceAccountImportData = serde_json::from_value(json!({
247            "roleName": "alien-worker",
248            "roleArn": "arn:aws:iam::123456789012:role/alien-worker",
249            "stackPermissionsApplied": "false",
250        }))
251        .expect("service account import data should parse");
252        assert!(!service_account.stack_permissions_applied);
253
254        // CloudFormation stringifies every leaf of a custom resource's properties, so this is the
255        // shape the importer actually receives — a sandbox declared with open egress and a preview
256        // port reaches it as strings, and rejecting them fails the customer's stack at setup.
257        let sandbox: AwsSandboxImportData = serde_json::from_value(json!({
258            "imageIdentifier": "runner",
259            "imageArn": "arn:aws:lambda:us-east-2:123456789012:microvm-image/runner",
260            "imageVersion": "1",
261            "egressConnectorArns": [],
262            "allowEgress": "true",
263            "previewPorts": ["3000", "8080"],
264        }))
265        .expect("sandbox import data should parse CloudFormation's stringified leaves");
266        assert!(sandbox.allow_egress);
267        assert_eq!(sandbox.preview_ports, vec![3000, 8080]);
268
269        let native: AwsSandboxImportData = serde_json::from_value(json!({
270            "imageIdentifier": "runner",
271            "imageArn": "arn:aws:lambda:us-east-2:123456789012:microvm-image/runner",
272            "imageVersion": "1",
273            "egressConnectorArns": [],
274            "allowEgress": false,
275            "previewPorts": [3000],
276        }))
277        .expect("sandbox import data should still parse native JSON types");
278        assert!(!native.allow_egress);
279        assert_eq!(native.preview_ports, vec![3000]);
280    }
281
282    /// A runtime-provisioned sandbox registers without an image; the build role and bundle take its
283    /// place. A contract that silently drops those fields as unknown fails only after `terraform
284    /// apply` or stack creation has already succeeded, in the customer's own account.
285    #[test]
286    fn a_runtime_provisioned_sandbox_registers_without_an_image() {
287        let runtime: AwsSandboxImportData = serde_json::from_value(json!({
288            "egressConnectorArns": ["arn:aws:lambda:us-east-2:123456789012:network-connector/c-1"],
289            "allowEgress": "false",
290            "previewPorts": ["8080"],
291            "buildRoleArn": "arn:aws:iam::123456789012:role/acme-agents-build",
292            "bundleUri": "s3://acme-artifacts-us-east-2/agents/bundle.zip",
293        }))
294        .expect("a runtime-provisioned sandbox must register without image fields");
295
296        assert_eq!(runtime.image_arn, None);
297        assert_eq!(runtime.image_identifier, None);
298        assert_eq!(runtime.image_version, None);
299        assert_eq!(
300            runtime.build_role_arn.as_deref(),
301            Some("arn:aws:iam::123456789012:role/acme-agents-build")
302        );
303        assert_eq!(
304            runtime.bundle_uri.as_deref(),
305            Some("s3://acme-artifacts-us-east-2/agents/bundle.zip")
306        );
307        assert_eq!(runtime.preview_ports, vec![8080]);
308
309        // The setup-built shape carries no build inputs, so the two are distinguishable rather
310        // than merely both accepted.
311        let frozen: AwsSandboxImportData = serde_json::from_value(json!({
312            "imageIdentifier": "runner",
313            "imageArn": "arn:aws:lambda:us-east-2:123456789012:microvm-image/runner",
314            "imageVersion": "1.0",
315        }))
316        .expect("the setup-built shape still parses");
317        assert_eq!(frozen.build_role_arn, None);
318        assert_eq!(frozen.bundle_uri, None);
319        assert_eq!(frozen.image_version.as_deref(), Some("1.0"));
320    }
321}