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, 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
51pub(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
76pub(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 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 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 #[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 #[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 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}