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