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