Skip to main content

alien_core/
sandbox_setup_inputs.rs

1//! The facts an AWS sandbox's setup renders its scaffolding from. Setup applies them and the
2//! runtime never re-reads them, so a change to any of them needs setup to run again.
3
4use alien_error::AlienError;
5use serde_json::Value;
6
7use crate::remote_bindings::{
8    remote_binding_for_entry, remote_binding_is_deliverable, RemoteBindingDefinition,
9};
10use crate::sandbox_build_role::SandboxBuildRole;
11use crate::sandbox_egress::sandbox_egress_network;
12use crate::{
13    ErrorData, ResourceLifecycle, Result, Sandbox, SandboxCode, Stack, BUNDLE_REGION_TOKEN,
14};
15
16/// The account a sandbox's scaffolding is rendered into.
17#[derive(Debug, Clone, Copy)]
18pub struct SetupAccount<'a> {
19    pub partition: &'a str,
20    pub account_id: &'a str,
21    pub region: &'a str,
22}
23
24/// For comparing two stacks without an account. The region stays the token so a `{region}` host
25/// and a literal region never render the same ARN.
26pub const SETUP_INPUTS_COMPARISON_ACCOUNT: SetupAccount<'static> = SetupAccount {
27    partition: "aws",
28    account_id: "000000000000",
29    region: BUNDLE_REGION_TOKEN,
30};
31
32/// The id of the network an AWS sandbox's egress connector attaches to, `None` for `allow`.
33/// The error is the refusal message, for the caller to wrap in its own error.
34pub fn aws_sandbox_egress_network_id<'a>(
35    stack: &'a Stack,
36    sandbox: &Sandbox,
37) -> std::result::Result<Option<&'a str>, String> {
38    let network = match sandbox_egress_network(stack, sandbox.cloud_egress()) {
39        Ok(Some(network)) => network,
40        Ok(None) => return Ok(None),
41        Err(refusal) => return Err(refusal.to_string()),
42    };
43    if network.entry.lifecycle != ResourceLifecycle::Frozen {
44        return Err(
45            "an AWS sandbox routes session traffic through a VPC egress connector; its network \
46             must be created by setup, and this one is created at runtime"
47                .to_string(),
48        );
49    }
50    Ok(Some(network.id))
51}
52
53/// The build role setup renders for this sandbox from its bundle.
54pub fn aws_sandbox_build_role<'a>(
55    sandbox: &'a Sandbox,
56    bundle_uri: &'a str,
57    lifecycle: ResourceLifecycle,
58    account: SetupAccount<'a>,
59) -> SandboxBuildRole<'a> {
60    SandboxBuildRole::builder()
61        .sandbox_id(&sandbox.id)
62        .partition(account.partition)
63        .account_id(account.account_id)
64        .region(account.region)
65        .bundle_uri(bundle_uri)
66        .runtime_built(lifecycle == ResourceLifecycle::Live)
67        .maybe_private_base_image(sandbox.private_base_image.as_deref())
68        .build()
69}
70
71/// The Remote Bindings grant setup renders for this sandbox, `None` when it publishes none.
72pub fn aws_sandbox_remote_grant(
73    stack: &Stack,
74    sandbox: &Sandbox,
75) -> Option<&'static RemoteBindingDefinition> {
76    stack
77        .resources
78        .get(&sandbox.id)
79        .filter(|entry| remote_binding_is_deliverable(entry))
80        .and_then(remote_binding_for_entry)
81}
82
83/// Each setup input with the name a refused update reports it by. A new bundle under the same
84/// stable prefix, or a new tag in the same private repository, leaves them unchanged.
85pub fn aws_sandbox_setup_inputs(
86    stack: &Stack,
87    sandbox: &Sandbox,
88    lifecycle: ResourceLifecycle,
89    account: SetupAccount<'_>,
90) -> Result<Vec<(&'static str, Value)>> {
91    let refuse = |reason: String| {
92        AlienError::new(ErrorData::OperationNotSupported {
93            operation: format!("setup inputs for sandbox '{}'", sandbox.id),
94            reason,
95        })
96    };
97    let SandboxCode::Image { image } = &sandbox.code else {
98        return Err(refuse(
99            "an AWS sandbox is built from a prebuilt s3:// bundle, not from source".to_string(),
100        ));
101    };
102    let policy = aws_sandbox_build_role(sandbox, image, lifecycle, account).policy()?;
103    let network = aws_sandbox_egress_network_id(stack, sandbox).map_err(refuse)?;
104    // An update that stops publishing keeps the setup-owned Remote Bindings role, so without
105    // this the grant would outlive the declaration.
106    let grant =
107        aws_sandbox_remote_grant(stack, sandbox).map(|definition| definition.permission_set);
108    Ok(vec![
109        (
110            "egress",
111            serde_json::to_value(&sandbox.egress).expect("sandbox egress serializes to JSON"),
112        ),
113        ("egress network", serde_json::json!(network)),
114        (
115            "build role policy",
116            serde_json::to_value(policy).expect("a build role policy serializes to JSON"),
117        ),
118        ("remote grant", serde_json::json!(grant)),
119    ])
120}
121
122/// [`aws_sandbox_setup_inputs`] for comparing stacks, which cannot fail: a sandbox whose inputs do
123/// not resolve counts as its whole configuration, so any change to it needs setup.
124pub fn comparable_aws_sandbox_setup_inputs(
125    stack: &Stack,
126    sandbox: &Sandbox,
127    lifecycle: ResourceLifecycle,
128) -> Vec<(&'static str, Value)> {
129    aws_sandbox_setup_inputs(stack, sandbox, lifecycle, SETUP_INPUTS_COMPARISON_ACCOUNT)
130        .unwrap_or_else(|_| {
131            vec![(
132                "configuration",
133                serde_json::to_value(sandbox).expect("a sandbox serializes to JSON"),
134            )]
135        })
136}