1use 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
49pub(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
74pub(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 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 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 #[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 #[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 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}