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, AwsRedisImportData,
15    AwsRemoteBindingsImportData, AwsRemoteStackManagementImportData, AwsSandboxImportData,
16    AwsSandboxNetwork, AwsServiceAccountImportData, AwsStorageImportData, AwsVaultImportData,
17    AwsWorkerImportData,
18};
19pub use azure::{
20    AzureAiImportData, AzureArtifactRegistryImportData, AzureBuildImportData,
21    AzureComputeClusterImportData, AzureContainerAppsEnvironmentImportData,
22    AzureFlexibleServerPostgresImportData, AzureKeyImportData, AzureKvImportData,
23    AzureManagedRedisImportData, AzureNetworkImportData, AzureQueueImportData,
24    AzureRemoteBindingsImportData, AzureRemoteStackManagementImportData,
25    AzureResourceGroupImportData, AzureSandboxImportData, AzureSandboxNetwork,
26    AzureServiceAccountImportData, AzureServiceActivationImportData,
27    AzureServiceBusNamespaceImportData, AzureStorageAccountImportData, AzureStorageImportData,
28    AzureVaultImportData, AzureWorkerImportData,
29};
30pub use gcp::{
31    GcpAgentPlatformEngineImportData, GcpAiImportData, GcpArtifactRegistryImportData,
32    GcpBuildImportData, GcpComputeClusterImportData, GcpKeyImportData, GcpKvImportData,
33    GcpMemorystoreImportData, GcpNetworkImportData, GcpPostgresImportData, GcpQueueImportData,
34    GcpRemoteBindingsImportData, GcpRemoteStackManagementImportData, GcpSandboxImportData,
35    GcpServiceAccountImportData, GcpServiceActivationImportData, GcpStorageImportData,
36    GcpVaultImportData, GcpWorkerImportData,
37};
38pub use kubernetes_cluster::{
39    AzureApplicationGatewayForContainersBootstrap, KubernetesClusterImportData,
40};
41
42pub(crate) fn deserialize_bool_from_bool_or_string<'de, D>(
43    deserializer: D,
44) -> Result<bool, D::Error>
45where
46    D: Deserializer<'de>,
47{
48    bool_from_bool_or_string(serde_json::Value::deserialize(deserializer)?)
49}
50
51/// Optional form of [`deserialize_bool_from_bool_or_string`]: `null` stays `None`. Pair it with
52/// `#[serde(default)]` so an absent field is also `None`.
53pub(crate) fn deserialize_optional_bool_from_bool_or_string<'de, D>(
54    deserializer: D,
55) -> Result<Option<bool>, D::Error>
56where
57    D: Deserializer<'de>,
58{
59    match serde_json::Value::deserialize(deserializer)? {
60        serde_json::Value::Null => Ok(None),
61        value => bool_from_bool_or_string(value).map(Some),
62    }
63}
64
65fn bool_from_bool_or_string<E: serde::de::Error>(value: serde_json::Value) -> Result<bool, E> {
66    match value {
67        serde_json::Value::Bool(value) => Ok(value),
68        serde_json::Value::String(value) if value.eq_ignore_ascii_case("true") => Ok(true),
69        serde_json::Value::String(value) if value.eq_ignore_ascii_case("false") => Ok(false),
70        other => Err(E::custom(format!(
71            "expected boolean or boolean string, got {other}"
72        ))),
73    }
74}
75
76/// Same reason as the boolean helper: CloudFormation stringifies the leaves of a custom
77/// resource's properties, so a declared port list arrives as `["3000"]` rather than `[3000]`.
78pub(crate) fn deserialize_u16_vec_from_numbers_or_strings<'de, D>(
79    deserializer: D,
80) -> Result<Vec<u16>, D::Error>
81where
82    D: Deserializer<'de>,
83{
84    let values = Vec::<serde_json::Value>::deserialize(deserializer)?;
85    values
86        .into_iter()
87        .map(|value| match value {
88            serde_json::Value::Number(number) => number
89                .as_u64()
90                .and_then(|n| u16::try_from(n).ok())
91                .ok_or_else(|| serde::de::Error::custom(format!("port {number} is out of range"))),
92            serde_json::Value::String(text) => text.parse::<u16>().map_err(|_| {
93                serde::de::Error::custom(format!("expected a port number, got {text:?}"))
94            }),
95            other => Err(serde::de::Error::custom(format!(
96                "expected a port number or numeric string, got {other}"
97            ))),
98        })
99        .collect()
100}
101
102#[cfg(all(test, feature = "jsonschema"))]
103mod schema_snapshots {
104    use super::*;
105    use indexmap::IndexMap;
106
107    fn schema<T: schemars::JsonSchema>() -> serde_json::Value {
108        let mut schema =
109            serde_json::to_value(schemars::schema_for!(T)).expect("schema should serialize");
110        // Dependency features can enable preserve_order; object key order is not schema data.
111        schema.sort_all_objects();
112        schema
113    }
114
115    #[test]
116    fn import_data_schema_snapshot() {
117        let schemas = IndexMap::from([
118            ("aws_ai", schema::<AwsAiImportData>()),
119            (
120                "aws_artifact_registry",
121                schema::<AwsArtifactRegistryImportData>(),
122            ),
123            ("aws_build", schema::<AwsBuildImportData>()),
124            (
125                "aws_compute_cluster",
126                schema::<AwsComputeClusterImportData>(),
127            ),
128            ("aws_email", schema::<AwsEmailImportData>()),
129            ("aws_function", schema::<AwsWorkerImportData>()),
130            ("aws_kv", schema::<AwsKvImportData>()),
131            ("aws_key", schema::<AwsKeyImportData>()),
132            ("aws_sandbox", schema::<AwsSandboxImportData>()),
133            ("aws_network", schema::<AwsNetworkImportData>()),
134            ("aws_open_search", schema::<AwsOpenSearchImportData>()),
135            ("aws_postgres", schema::<AwsPostgresImportData>()),
136            ("aws_redis", schema::<AwsRedisImportData>()),
137            ("aws_queue", schema::<AwsQueueImportData>()),
138            (
139                "aws_remote_stack_management",
140                schema::<AwsRemoteStackManagementImportData>(),
141            ),
142            (
143                "aws_service_account",
144                schema::<AwsServiceAccountImportData>(),
145            ),
146            ("aws_storage", schema::<AwsStorageImportData>()),
147            ("aws_vault", schema::<AwsVaultImportData>()),
148            ("azure_ai", schema::<AzureAiImportData>()),
149            (
150                "azure_artifact_registry",
151                schema::<AzureArtifactRegistryImportData>(),
152            ),
153            ("azure_build", schema::<AzureBuildImportData>()),
154            (
155                "azure_container_apps_environment",
156                schema::<AzureContainerAppsEnvironmentImportData>(),
157            ),
158            (
159                "azure_compute_cluster",
160                schema::<AzureComputeClusterImportData>(),
161            ),
162            ("azure_function", schema::<AzureWorkerImportData>()),
163            ("azure_kv", schema::<AzureKvImportData>()),
164            ("azure_key", schema::<AzureKeyImportData>()),
165            ("azure_sandbox", schema::<AzureSandboxImportData>()),
166            ("azure_network", schema::<AzureNetworkImportData>()),
167            (
168                "azure_postgres",
169                schema::<AzureFlexibleServerPostgresImportData>(),
170            ),
171            ("azure_queue", schema::<AzureQueueImportData>()),
172            ("azure_redis", schema::<AzureManagedRedisImportData>()),
173            (
174                "azure_remote_stack_management",
175                schema::<AzureRemoteStackManagementImportData>(),
176            ),
177            (
178                "azure_resource_group",
179                schema::<AzureResourceGroupImportData>(),
180            ),
181            (
182                "azure_service_account",
183                schema::<AzureServiceAccountImportData>(),
184            ),
185            (
186                "azure_service_activation",
187                schema::<AzureServiceActivationImportData>(),
188            ),
189            (
190                "azure_service_bus_namespace",
191                schema::<AzureServiceBusNamespaceImportData>(),
192            ),
193            ("azure_storage", schema::<AzureStorageImportData>()),
194            (
195                "azure_storage_account",
196                schema::<AzureStorageAccountImportData>(),
197            ),
198            ("azure_vault", schema::<AzureVaultImportData>()),
199            (
200                "gcp_agent_platform_engine",
201                schema::<GcpAgentPlatformEngineImportData>(),
202            ),
203            ("gcp_ai", schema::<GcpAiImportData>()),
204            (
205                "gcp_artifact_registry",
206                schema::<GcpArtifactRegistryImportData>(),
207            ),
208            ("gcp_build", schema::<GcpBuildImportData>()),
209            (
210                "gcp_compute_cluster",
211                schema::<GcpComputeClusterImportData>(),
212            ),
213            ("gcp_function", schema::<GcpWorkerImportData>()),
214            ("gcp_kv", schema::<GcpKvImportData>()),
215            ("gcp_key", schema::<GcpKeyImportData>()),
216            ("gcp_network", schema::<GcpNetworkImportData>()),
217            ("gcp_postgres", schema::<GcpPostgresImportData>()),
218            ("gcp_redis", schema::<GcpMemorystoreImportData>()),
219            ("gcp_queue", schema::<GcpQueueImportData>()),
220            (
221                "gcp_remote_stack_management",
222                schema::<GcpRemoteStackManagementImportData>(),
223            ),
224            (
225                "kubernetes_cluster",
226                schema::<KubernetesClusterImportData>(),
227            ),
228            (
229                "gcp_service_account",
230                schema::<GcpServiceAccountImportData>(),
231            ),
232            (
233                "gcp_service_activation",
234                schema::<GcpServiceActivationImportData>(),
235            ),
236            ("gcp_sandbox", schema::<GcpSandboxImportData>()),
237            ("gcp_storage", schema::<GcpStorageImportData>()),
238            ("gcp_vault", schema::<GcpVaultImportData>()),
239        ]);
240
241        insta::assert_json_snapshot!("import_data_schemas", schemas);
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use serde_json::json;
249
250    #[test]
251    fn aws_import_data_accepts_cloudformation_stringified_leaves() {
252        let network: AwsNetworkImportData = serde_json::from_value(json!({
253            "vpcId": "vpc-123",
254            "cidrBlock": null,
255            "internetGatewayId": null,
256            "natGatewayId": null,
257            "eipAllocationId": null,
258            "publicSubnetIds": ["subnet-public"],
259            "privateSubnetIds": ["subnet-private"],
260            "publicRouteTableId": null,
261            "privateRouteTableId": null,
262            "securityGroupId": "sg-123",
263            "availabilityZones": [],
264            "isByoVpc": "true",
265        }))
266        .expect("network import data should parse");
267        assert!(network.is_byo_vpc);
268
269        let remote_stack_management: AwsRemoteStackManagementImportData =
270            serde_json::from_value(json!({
271                "roleName": "alien-manager",
272                "roleArn": "arn:aws:iam::123456789012:role/alien-manager",
273                "managementPermissionsApplied": "true",
274            }))
275            .expect("remote stack management import data should parse");
276        assert!(remote_stack_management.management_permissions_applied);
277
278        let service_account: AwsServiceAccountImportData = serde_json::from_value(json!({
279            "roleName": "alien-worker",
280            "roleArn": "arn:aws:iam::123456789012:role/alien-worker",
281            "stackPermissionsApplied": "false",
282        }))
283        .expect("service account import data should parse");
284        assert!(!service_account.stack_permissions_applied);
285
286        // CloudFormation stringifies every leaf of a custom resource's properties, so this is the
287        // shape the importer actually receives — a sandbox declared with open egress and a preview
288        // port reaches it as strings, and rejecting them fails the customer's stack at setup.
289        let sandbox: AwsSandboxImportData = serde_json::from_value(json!({
290            "imageIdentifier": "runner",
291            "imageArn": "arn:aws:lambda:us-east-2:123456789012:microvm-image/runner",
292            "imageVersion": "1",
293            "egressConnectorArns": [],
294            "allowEgress": "true",
295            "previewPorts": ["3000", "8080"],
296        }))
297        .expect("sandbox import data should parse CloudFormation's stringified leaves");
298        assert!(sandbox.allow_egress);
299        assert_eq!(sandbox.preview_ports, vec![3000, 8080]);
300
301        let native: AwsSandboxImportData = serde_json::from_value(json!({
302            "imageIdentifier": "runner",
303            "imageArn": "arn:aws:lambda:us-east-2:123456789012:microvm-image/runner",
304            "imageVersion": "1",
305            "egressConnectorArns": [],
306            "allowEgress": false,
307            "previewPorts": [3000],
308        }))
309        .expect("sandbox import data should still parse native JSON types");
310        assert!(!native.allow_egress);
311        assert_eq!(native.preview_ports, vec![3000]);
312    }
313
314    /// CloudFormation delivers every boolean property as a string, so a setup that declares a
315    /// customer encryption key or a CloudFormation-created EKS cluster registers with "true" or
316    /// "false" here. Rejecting the string form fails registration and rolls the customer's stack back.
317    #[test]
318    fn stringified_key_and_kubernetes_cluster_booleans_parse() {
319        let key = |probe: serde_json::Value| -> Result<AwsKeyImportData, serde_json::Error> {
320            let mut data = json!({ "keyArn": "arn:aws:kms:us-east-1:123456789012:key/k-1" });
321            if !probe.is_null() {
322                data["availabilityProbe"] = probe;
323            }
324            serde_json::from_value(data)
325        };
326        assert!(key(json!("true")).expect("string true").availability_probe);
327        assert!(
328            !key(json!("false"))
329                .expect("string false")
330                .availability_probe
331        );
332        assert!(key(json!(true)).expect("native true").availability_probe);
333        assert!(!key(json!(false)).expect("native false").availability_probe);
334        assert!(
335            !key(serde_json::Value::Null)
336                .expect("an absent probe defaults to false")
337                .availability_probe
338        );
339        assert!(
340            key(json!("yes")).is_err(),
341            "a non-boolean string must be rejected"
342        );
343        assert!(key(json!(1)).is_err(), "a number must be rejected");
344
345        let cluster = |ready: Option<serde_json::Value>| -> Result<
346            KubernetesClusterImportData,
347            serde_json::Error,
348        > {
349            let mut data = json!({
350                "provider": "eks",
351                "ownership": "managed",
352                "namespace": "alien",
353                "clusterName": "acme",
354            });
355            if let Some(ready) = ready {
356                data["cloudMetadataReady"] = ready;
357            }
358            serde_json::from_value(data)
359        };
360        assert_eq!(
361            cluster(Some(json!("true")))
362                .expect("string true")
363                .cloud_metadata_ready,
364            Some(true)
365        );
366        assert_eq!(
367            cluster(Some(json!("false")))
368                .expect("string false")
369                .cloud_metadata_ready,
370            Some(false)
371        );
372        assert_eq!(
373            cluster(Some(json!(true)))
374                .expect("native true")
375                .cloud_metadata_ready,
376            Some(true)
377        );
378        assert_eq!(
379            cluster(Some(json!(false)))
380                .expect("native false")
381                .cloud_metadata_ready,
382            Some(false)
383        );
384        assert_eq!(
385            cluster(Some(serde_json::Value::Null))
386                .expect("null")
387                .cloud_metadata_ready,
388            None
389        );
390        assert_eq!(cluster(None).expect("absent").cloud_metadata_ready, None);
391        assert!(
392            cluster(Some(json!("ready"))).is_err(),
393            "a non-boolean string must be rejected"
394        );
395    }
396
397    /// A runtime-provisioned sandbox registers without an image; the build role and bundle take its
398    /// place. A contract that silently drops those fields as unknown fails only after `terraform
399    /// apply` or stack creation has already succeeded, in the customer's own account.
400    #[test]
401    fn a_runtime_provisioned_sandbox_registers_without_an_image() {
402        let runtime: AwsSandboxImportData = serde_json::from_value(json!({
403            "egressConnectorArns": ["arn:aws:lambda:us-east-2:123456789012:network-connector/c-1"],
404            "allowEgress": "false",
405            "previewPorts": ["8080"],
406            "buildRoleArn": "arn:aws:iam::123456789012:role/acme-agents-build",
407            "bundleUri": "s3://acme-artifacts-us-east-2/agents/bundle.zip",
408        }))
409        .expect("a runtime-provisioned sandbox must register without image fields");
410
411        assert_eq!(runtime.image_arn, None);
412        assert_eq!(runtime.image_identifier, None);
413        assert_eq!(runtime.image_version, None);
414        assert_eq!(
415            runtime.build_role_arn.as_deref(),
416            Some("arn:aws:iam::123456789012:role/acme-agents-build")
417        );
418        assert_eq!(
419            runtime.bundle_uri.as_deref(),
420            Some("s3://acme-artifacts-us-east-2/agents/bundle.zip")
421        );
422        assert_eq!(runtime.preview_ports, vec![8080]);
423
424        // The setup-built shape carries no build inputs, so the two are distinguishable rather
425        // than merely both accepted.
426        let frozen: AwsSandboxImportData = serde_json::from_value(json!({
427            "imageIdentifier": "runner",
428            "imageArn": "arn:aws:lambda:us-east-2:123456789012:microvm-image/runner",
429            "imageVersion": "1.0",
430        }))
431        .expect("the setup-built shape still parses");
432        assert_eq!(frozen.build_role_arn, None);
433        assert_eq!(frozen.bundle_uri, None);
434        assert_eq!(frozen.image_version.as_deref(), Some("1.0"));
435    }
436}