alien-core 3.3.20

Deploy software into your customers' cloud accounts and keep it fully managed
Documentation
//! Typed ImportData payloads by cloud and resource.

use serde::{Deserialize, Deserializer};

pub mod aws;
pub mod azure;
pub mod gcp;
pub mod kubernetes_cluster;

pub use aws::{
    AwsAiImportData, AwsArtifactRegistryImportData, AwsBuildImportData,
    AwsComputeClusterImportData, AwsEmailDkimTokenImportData, AwsEmailDomainImportData,
    AwsEmailImportData, AwsKeyImportData, AwsKvImportData, AwsNetworkImportData,
    AwsOpenSearchImportData, AwsPostgresImportData, AwsQueueImportData,
    AwsRemoteBindingsImportData, AwsRemoteStackManagementImportData, AwsSandboxImportData,
    AwsServiceAccountImportData, AwsStorageImportData, AwsVaultImportData, AwsWorkerImportData,
};
pub use azure::{
    AzureAiImportData, AzureArtifactRegistryImportData, AzureBuildImportData,
    AzureComputeClusterImportData, AzureContainerAppsEnvironmentImportData,
    AzureFlexibleServerPostgresImportData, AzureKeyImportData, AzureKvImportData,
    AzureNetworkImportData, AzureQueueImportData, AzureRemoteBindingsImportData,
    AzureRemoteStackManagementImportData, AzureResourceGroupImportData, AzureSandboxImportData,
    AzureServiceAccountImportData, AzureServiceActivationImportData,
    AzureServiceBusNamespaceImportData, AzureStorageAccountImportData, AzureStorageImportData,
    AzureVaultImportData, AzureWorkerImportData,
};
pub use gcp::{
    GcpAiImportData, GcpArtifactRegistryImportData, GcpBuildImportData,
    GcpComputeClusterImportData, GcpKeyImportData, GcpKvImportData, GcpNetworkImportData,
    GcpPostgresImportData, GcpQueueImportData, GcpRemoteBindingsImportData,
    GcpRemoteStackManagementImportData, GcpServiceAccountImportData,
    GcpServiceActivationImportData, GcpStorageImportData, GcpVaultImportData, GcpWorkerImportData,
};
pub use kubernetes_cluster::{
    AzureApplicationGatewayForContainersBootstrap, KubernetesClusterImportData,
};

pub(crate) fn deserialize_bool_from_bool_or_string<'de, D>(
    deserializer: D,
) -> Result<bool, D::Error>
where
    D: Deserializer<'de>,
{
    let value = serde_json::Value::deserialize(deserializer)?;
    match value {
        serde_json::Value::Bool(value) => Ok(value),
        serde_json::Value::String(value) if value.eq_ignore_ascii_case("true") => Ok(true),
        serde_json::Value::String(value) if value.eq_ignore_ascii_case("false") => Ok(false),
        other => Err(serde::de::Error::custom(format!(
            "expected boolean or boolean string, got {other}"
        ))),
    }
}

/// Same reason as the boolean helper: CloudFormation stringifies the leaves of a custom
/// resource's properties, so a declared port list arrives as `["3000"]` rather than `[3000]`.
pub(crate) fn deserialize_u16_vec_from_numbers_or_strings<'de, D>(
    deserializer: D,
) -> Result<Vec<u16>, D::Error>
where
    D: Deserializer<'de>,
{
    let values = Vec::<serde_json::Value>::deserialize(deserializer)?;
    values
        .into_iter()
        .map(|value| match value {
            serde_json::Value::Number(number) => number
                .as_u64()
                .and_then(|n| u16::try_from(n).ok())
                .ok_or_else(|| serde::de::Error::custom(format!("port {number} is out of range"))),
            serde_json::Value::String(text) => text.parse::<u16>().map_err(|_| {
                serde::de::Error::custom(format!("expected a port number, got {text:?}"))
            }),
            other => Err(serde::de::Error::custom(format!(
                "expected a port number or numeric string, got {other}"
            ))),
        })
        .collect()
}

#[cfg(all(test, feature = "jsonschema"))]
mod schema_snapshots {
    use super::*;
    use indexmap::IndexMap;

    fn schema<T: schemars::JsonSchema>() -> serde_json::Value {
        serde_json::to_value(schemars::schema_for!(T)).expect("schema should serialize")
    }

    #[test]
    fn import_data_schema_snapshot() {
        let schemas = IndexMap::from([
            ("aws_ai", schema::<AwsAiImportData>()),
            (
                "aws_artifact_registry",
                schema::<AwsArtifactRegistryImportData>(),
            ),
            ("aws_build", schema::<AwsBuildImportData>()),
            (
                "aws_compute_cluster",
                schema::<AwsComputeClusterImportData>(),
            ),
            ("aws_email", schema::<AwsEmailImportData>()),
            ("aws_function", schema::<AwsWorkerImportData>()),
            ("aws_kv", schema::<AwsKvImportData>()),
            ("aws_key", schema::<AwsKeyImportData>()),
            ("aws_sandbox", schema::<AwsSandboxImportData>()),
            ("aws_network", schema::<AwsNetworkImportData>()),
            ("aws_open_search", schema::<AwsOpenSearchImportData>()),
            ("aws_postgres", schema::<AwsPostgresImportData>()),
            ("aws_queue", schema::<AwsQueueImportData>()),
            (
                "aws_remote_stack_management",
                schema::<AwsRemoteStackManagementImportData>(),
            ),
            (
                "aws_service_account",
                schema::<AwsServiceAccountImportData>(),
            ),
            ("aws_storage", schema::<AwsStorageImportData>()),
            ("aws_vault", schema::<AwsVaultImportData>()),
            ("azure_ai", schema::<AzureAiImportData>()),
            (
                "azure_artifact_registry",
                schema::<AzureArtifactRegistryImportData>(),
            ),
            ("azure_build", schema::<AzureBuildImportData>()),
            (
                "azure_container_apps_environment",
                schema::<AzureContainerAppsEnvironmentImportData>(),
            ),
            (
                "azure_compute_cluster",
                schema::<AzureComputeClusterImportData>(),
            ),
            ("azure_function", schema::<AzureWorkerImportData>()),
            ("azure_kv", schema::<AzureKvImportData>()),
            ("azure_key", schema::<AzureKeyImportData>()),
            ("azure_sandbox", schema::<AzureSandboxImportData>()),
            ("azure_network", schema::<AzureNetworkImportData>()),
            (
                "azure_postgres",
                schema::<AzureFlexibleServerPostgresImportData>(),
            ),
            ("azure_queue", schema::<AzureQueueImportData>()),
            (
                "azure_remote_stack_management",
                schema::<AzureRemoteStackManagementImportData>(),
            ),
            (
                "azure_resource_group",
                schema::<AzureResourceGroupImportData>(),
            ),
            (
                "azure_service_account",
                schema::<AzureServiceAccountImportData>(),
            ),
            (
                "azure_service_activation",
                schema::<AzureServiceActivationImportData>(),
            ),
            (
                "azure_service_bus_namespace",
                schema::<AzureServiceBusNamespaceImportData>(),
            ),
            ("azure_storage", schema::<AzureStorageImportData>()),
            (
                "azure_storage_account",
                schema::<AzureStorageAccountImportData>(),
            ),
            ("azure_vault", schema::<AzureVaultImportData>()),
            ("gcp_ai", schema::<GcpAiImportData>()),
            (
                "gcp_artifact_registry",
                schema::<GcpArtifactRegistryImportData>(),
            ),
            ("gcp_build", schema::<GcpBuildImportData>()),
            (
                "gcp_compute_cluster",
                schema::<GcpComputeClusterImportData>(),
            ),
            ("gcp_function", schema::<GcpWorkerImportData>()),
            ("gcp_kv", schema::<GcpKvImportData>()),
            ("gcp_key", schema::<GcpKeyImportData>()),
            ("gcp_network", schema::<GcpNetworkImportData>()),
            ("gcp_postgres", schema::<GcpPostgresImportData>()),
            ("gcp_queue", schema::<GcpQueueImportData>()),
            (
                "gcp_remote_stack_management",
                schema::<GcpRemoteStackManagementImportData>(),
            ),
            (
                "kubernetes_cluster",
                schema::<KubernetesClusterImportData>(),
            ),
            (
                "gcp_service_account",
                schema::<GcpServiceAccountImportData>(),
            ),
            (
                "gcp_service_activation",
                schema::<GcpServiceActivationImportData>(),
            ),
            ("gcp_storage", schema::<GcpStorageImportData>()),
            ("gcp_vault", schema::<GcpVaultImportData>()),
        ]);

        insta::assert_json_snapshot!("import_data_schemas", schemas);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn aws_import_data_accepts_cloudformation_stringified_leaves() {
        let network: AwsNetworkImportData = serde_json::from_value(json!({
            "vpcId": "vpc-123",
            "cidrBlock": null,
            "internetGatewayId": null,
            "natGatewayId": null,
            "eipAllocationId": null,
            "publicSubnetIds": ["subnet-public"],
            "privateSubnetIds": ["subnet-private"],
            "publicRouteTableId": null,
            "privateRouteTableId": null,
            "securityGroupId": "sg-123",
            "availabilityZones": [],
            "isByoVpc": "true",
        }))
        .expect("network import data should parse");
        assert!(network.is_byo_vpc);

        let remote_stack_management: AwsRemoteStackManagementImportData =
            serde_json::from_value(json!({
                "roleName": "alien-manager",
                "roleArn": "arn:aws:iam::123456789012:role/alien-manager",
                "managementPermissionsApplied": "true",
            }))
            .expect("remote stack management import data should parse");
        assert!(remote_stack_management.management_permissions_applied);

        let service_account: AwsServiceAccountImportData = serde_json::from_value(json!({
            "roleName": "alien-worker",
            "roleArn": "arn:aws:iam::123456789012:role/alien-worker",
            "stackPermissionsApplied": "false",
        }))
        .expect("service account import data should parse");
        assert!(!service_account.stack_permissions_applied);

        // CloudFormation stringifies every leaf of a custom resource's properties, so this is the
        // shape the importer actually receives — a sandbox declared with open egress and a preview
        // port reaches it as strings, and rejecting them fails the customer's stack at setup.
        let sandbox: AwsSandboxImportData = serde_json::from_value(json!({
            "imageIdentifier": "runner",
            "imageArn": "arn:aws:lambda:us-east-2:123456789012:microvm-image/runner",
            "imageVersion": "1",
            "egressConnectorArns": [],
            "allowEgress": "true",
            "previewPorts": ["3000", "8080"],
        }))
        .expect("sandbox import data should parse CloudFormation's stringified leaves");
        assert!(sandbox.allow_egress);
        assert_eq!(sandbox.preview_ports, vec![3000, 8080]);

        let native: AwsSandboxImportData = serde_json::from_value(json!({
            "imageIdentifier": "runner",
            "imageArn": "arn:aws:lambda:us-east-2:123456789012:microvm-image/runner",
            "imageVersion": "1",
            "egressConnectorArns": [],
            "allowEgress": false,
            "previewPorts": [3000],
        }))
        .expect("sandbox import data should still parse native JSON types");
        assert!(!native.allow_egress);
        assert_eq!(native.preview_ports, vec![3000]);
    }

    /// A runtime-provisioned sandbox registers without an image; the build role and bundle take its
    /// place. A contract that silently drops those fields as unknown fails only after `terraform
    /// apply` or stack creation has already succeeded, in the customer's own account.
    #[test]
    fn a_runtime_provisioned_sandbox_registers_without_an_image() {
        let runtime: AwsSandboxImportData = serde_json::from_value(json!({
            "egressConnectorArns": ["arn:aws:lambda:us-east-2:123456789012:network-connector/c-1"],
            "allowEgress": "false",
            "previewPorts": ["8080"],
            "buildRoleArn": "arn:aws:iam::123456789012:role/acme-agents-build",
            "bundleUri": "s3://acme-artifacts-us-east-2/agents/bundle.zip",
        }))
        .expect("a runtime-provisioned sandbox must register without image fields");

        assert_eq!(runtime.image_arn, None);
        assert_eq!(runtime.image_identifier, None);
        assert_eq!(runtime.image_version, None);
        assert_eq!(
            runtime.build_role_arn.as_deref(),
            Some("arn:aws:iam::123456789012:role/acme-agents-build")
        );
        assert_eq!(
            runtime.bundle_uri.as_deref(),
            Some("s3://acme-artifacts-us-east-2/agents/bundle.zip")
        );
        assert_eq!(runtime.preview_ports, vec![8080]);

        // The setup-built shape carries no build inputs, so the two are distinguishable rather
        // than merely both accepted.
        let frozen: AwsSandboxImportData = serde_json::from_value(json!({
            "imageIdentifier": "runner",
            "imageArn": "arn:aws:lambda:us-east-2:123456789012:microvm-image/runner",
            "imageVersion": "1.0",
        }))
        .expect("the setup-built shape still parses");
        assert_eq!(frozen.build_role_arn, None);
        assert_eq!(frozen.bundle_uri, None);
        assert_eq!(frozen.image_version.as_deref(), Some("1.0"));
    }
}