1use 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, SANDBOX_BUNDLE_STORAGE_ID};
11use crate::sandbox_egress::{sandbox_egress_network, sandbox_needs_connector};
12use crate::{
13 parse_ecr_image_repository, ErrorData, ResourceEntry, ResourceLifecycle, Result, Sandbox,
14 SandboxCode, Stack, Storage, BUNDLE_REGION_TOKEN,
15};
16
17#[derive(Debug, Clone, Copy)]
19pub struct SetupAccount<'a> {
20 pub partition: &'a str,
21 pub account_id: &'a str,
22 pub region: &'a str,
23}
24
25pub const SETUP_INPUTS_COMPARISON_ACCOUNT: SetupAccount<'static> = SetupAccount {
28 partition: "aws",
29 account_id: "000000000000",
30 region: BUNDLE_REGION_TOKEN,
31};
32
33pub fn aws_sandbox_egress_network_id<'a>(
36 stack: &'a Stack,
37 sandbox: &Sandbox,
38) -> std::result::Result<Option<&'a str>, String> {
39 if !sandbox_needs_connector(stack, sandbox) {
40 return Ok(None);
41 }
42 let network = match sandbox_egress_network(stack) {
43 Ok(network) => network,
44 Err(refusal) => return Err(refusal.to_string()),
45 };
46 if network.entry.lifecycle != ResourceLifecycle::Frozen {
47 return Err(
48 "an AWS sandbox routes session traffic through a VPC egress connector; its network \
49 must be created by setup, and this one is created at runtime"
50 .to_string(),
51 );
52 }
53 Ok(Some(network.id))
54}
55
56pub fn aws_sandbox_renders_bundle(stack: &Stack, sandbox_id: &str) -> bool {
61 stack
62 .resources
63 .get(sandbox_id)
64 .is_some_and(aws_sandbox_entry_renders_bundle)
65}
66
67pub fn aws_sandbox_entry_renders_bundle(entry: &ResourceEntry) -> bool {
69 entry.dependencies.iter().any(|dependency| {
70 dependency.resource_type == Storage::RESOURCE_TYPE
71 && dependency.id == SANDBOX_BUNDLE_STORAGE_ID
72 })
73}
74
75pub fn aws_sandbox_build_role<'a>(
78 sandbox: &'a Sandbox,
79 bundle_uri: &'a str,
80 renders_bundle: bool,
81 lifecycle: ResourceLifecycle,
82 account: SetupAccount<'a>,
83) -> SandboxBuildRole<'a> {
84 SandboxBuildRole::builder()
85 .sandbox_id(&sandbox.id)
86 .partition(account.partition)
87 .account_id(account.account_id)
88 .region(account.region)
89 .bundle_uri(bundle_uri)
90 .runtime_built(lifecycle == ResourceLifecycle::Live)
91 .maybe_private_base_image(sandbox.private_base_image.as_deref())
92 .renders_bundle(renders_bundle)
93 .build()
94}
95
96pub fn aws_sandbox_remote_grant(
98 stack: &Stack,
99 sandbox: &Sandbox,
100) -> Option<&'static RemoteBindingDefinition> {
101 stack
102 .resources
103 .get(&sandbox.id)
104 .filter(|entry| remote_binding_is_deliverable(entry))
105 .and_then(remote_binding_for_entry)
106}
107
108const SETUP_GRANTED_STORE: &str = "setup-granted-store";
110const SETUP_GRANTED_BASE_IMAGE: &str =
112 "000000000000.dkr.ecr.us-east-1.amazonaws.com/setup-granted-repository:base";
113
114fn setup_granted_base_image(image: &str) -> std::result::Result<String, String> {
119 let repository = parse_ecr_image_repository(image)?;
120 Ok(match repository.repository.rsplit_once('/') {
121 Some((namespace, _)) => SETUP_GRANTED_BASE_IMAGE.replacen(
122 "/setup-granted-repository",
123 &format!("/{namespace}/setup-granted-repository"),
124 1,
125 ),
126 None => SETUP_GRANTED_BASE_IMAGE.to_string(),
127 })
128}
129
130fn setup_granted_bundle(image: &str) -> std::result::Result<String, String> {
132 image
133 .strip_prefix("s3://")
134 .and_then(|path| path.split_once('/'))
135 .map(|(_, key)| format!("s3://{SETUP_GRANTED_STORE}/{key}"))
136 .ok_or_else(|| format!("bundle '{image}' is not an s3://bucket/key URI"))
137}
138
139pub fn aws_sandbox_setup_inputs(
144 stack: &Stack,
145 sandbox: &Sandbox,
146 lifecycle: ResourceLifecycle,
147 account: SetupAccount<'_>,
148) -> Result<Vec<(&'static str, Value)>> {
149 let refuse = |reason: String| {
150 AlienError::new(ErrorData::OperationNotSupported {
151 operation: format!("setup inputs for sandbox '{}'", sandbox.id),
152 reason,
153 })
154 };
155 let SandboxCode::Image { image, .. } = &sandbox.code else {
156 return Err(refuse(
157 "an AWS sandbox is built from a prebuilt s3:// bundle, not from source".to_string(),
158 ));
159 };
160 let policy = if aws_sandbox_renders_bundle(stack, &sandbox.id) {
161 aws_sandbox_build_role(sandbox, image, true, lifecycle, account).policy()?
164 } else if lifecycle == ResourceLifecycle::Live {
165 let mut granted = sandbox.clone();
176 granted.private_base_image = sandbox
177 .private_base_image
178 .as_deref()
179 .map(setup_granted_base_image)
180 .transpose()
181 .map_err(refuse)?;
182 let bundle = setup_granted_bundle(image).map_err(refuse)?;
183 aws_sandbox_build_role(&granted, &bundle, false, lifecycle, account).policy()?
184 } else {
185 aws_sandbox_build_role(sandbox, image, false, lifecycle, account).policy()?
186 };
187 let network = aws_sandbox_egress_network_id(stack, sandbox).map_err(refuse)?;
188 let grant =
191 aws_sandbox_remote_grant(stack, sandbox).map(|definition| definition.permission_set);
192 Ok(vec![
193 ("egress", sandbox.setup_digest_value()),
194 ("egress network", serde_json::json!(network)),
195 (
196 "build role policy",
197 serde_json::to_value(policy).expect("a build role policy serializes to JSON"),
198 ),
199 ("remote grant", serde_json::json!(grant)),
200 ])
201}
202
203pub fn comparable_aws_sandbox_setup_inputs(
206 stack: &Stack,
207 sandbox: &Sandbox,
208 lifecycle: ResourceLifecycle,
209) -> Vec<(&'static str, Value)> {
210 aws_sandbox_setup_inputs(stack, sandbox, lifecycle, SETUP_INPUTS_COMPARISON_ACCOUNT)
211 .unwrap_or_else(|_| {
212 vec![("configuration", {
213 let mut value =
214 serde_json::to_value(sandbox).expect("a sandbox serializes to JSON");
215 value["egress"] = sandbox.setup_digest_value();
216 value
217 .as_object_mut()
218 .expect("sandbox config is an object")
219 .remove("defaultPolicy");
220 value
221 })]
222 })
223}