alien_core/
sandbox_setup_inputs.rs1use 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, sandbox_needs_connector};
12use crate::{
13 parse_ecr_image_repository, ErrorData, ResourceLifecycle, Result, Sandbox, SandboxCode, Stack,
14 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_build_role<'a>(
58 sandbox: &'a Sandbox,
59 bundle_uri: &'a str,
60 lifecycle: ResourceLifecycle,
61 account: SetupAccount<'a>,
62) -> SandboxBuildRole<'a> {
63 SandboxBuildRole::builder()
64 .sandbox_id(&sandbox.id)
65 .partition(account.partition)
66 .account_id(account.account_id)
67 .region(account.region)
68 .bundle_uri(bundle_uri)
69 .runtime_built(lifecycle == ResourceLifecycle::Live)
70 .maybe_private_base_image(sandbox.private_base_image.as_deref())
71 .build()
72}
73
74pub fn aws_sandbox_remote_grant(
76 stack: &Stack,
77 sandbox: &Sandbox,
78) -> Option<&'static RemoteBindingDefinition> {
79 stack
80 .resources
81 .get(&sandbox.id)
82 .filter(|entry| remote_binding_is_deliverable(entry))
83 .and_then(remote_binding_for_entry)
84}
85
86const SETUP_GRANTED_STORE: &str = "setup-granted-store";
88const SETUP_GRANTED_BASE_IMAGE: &str =
90 "000000000000.dkr.ecr.us-east-1.amazonaws.com/setup-granted-repository:base";
91
92fn setup_granted_base_image(image: &str) -> std::result::Result<String, String> {
97 let repository = parse_ecr_image_repository(image)?;
98 Ok(match repository.repository.rsplit_once('/') {
99 Some((namespace, _)) => SETUP_GRANTED_BASE_IMAGE.replacen(
100 "/setup-granted-repository",
101 &format!("/{namespace}/setup-granted-repository"),
102 1,
103 ),
104 None => SETUP_GRANTED_BASE_IMAGE.to_string(),
105 })
106}
107
108fn setup_granted_bundle(image: &str) -> std::result::Result<String, String> {
110 image
111 .strip_prefix("s3://")
112 .and_then(|path| path.split_once('/'))
113 .map(|(_, key)| format!("s3://{SETUP_GRANTED_STORE}/{key}"))
114 .ok_or_else(|| format!("bundle '{image}' is not an s3://bucket/key URI"))
115}
116
117pub fn aws_sandbox_setup_inputs(
122 stack: &Stack,
123 sandbox: &Sandbox,
124 lifecycle: ResourceLifecycle,
125 account: SetupAccount<'_>,
126) -> Result<Vec<(&'static str, Value)>> {
127 let refuse = |reason: String| {
128 AlienError::new(ErrorData::OperationNotSupported {
129 operation: format!("setup inputs for sandbox '{}'", sandbox.id),
130 reason,
131 })
132 };
133 let SandboxCode::Image { image } = &sandbox.code else {
134 return Err(refuse(
135 "an AWS sandbox is built from a prebuilt s3:// bundle, not from source".to_string(),
136 ));
137 };
138 let policy = if lifecycle == ResourceLifecycle::Live {
139 let mut granted = sandbox.clone();
145 granted.private_base_image = sandbox
146 .private_base_image
147 .as_deref()
148 .map(setup_granted_base_image)
149 .transpose()
150 .map_err(refuse)?;
151 let bundle = setup_granted_bundle(image).map_err(refuse)?;
152 aws_sandbox_build_role(&granted, &bundle, lifecycle, account).policy()?
153 } else {
154 aws_sandbox_build_role(sandbox, image, lifecycle, account).policy()?
155 };
156 let network = aws_sandbox_egress_network_id(stack, sandbox).map_err(refuse)?;
157 let grant =
160 aws_sandbox_remote_grant(stack, sandbox).map(|definition| definition.permission_set);
161 Ok(vec![
162 ("egress", sandbox.setup_digest_value()),
163 ("egress network", serde_json::json!(network)),
164 (
165 "build role policy",
166 serde_json::to_value(policy).expect("a build role policy serializes to JSON"),
167 ),
168 ("remote grant", serde_json::json!(grant)),
169 ])
170}
171
172pub fn comparable_aws_sandbox_setup_inputs(
175 stack: &Stack,
176 sandbox: &Sandbox,
177 lifecycle: ResourceLifecycle,
178) -> Vec<(&'static str, Value)> {
179 aws_sandbox_setup_inputs(stack, sandbox, lifecycle, SETUP_INPUTS_COMPARISON_ACCOUNT)
180 .unwrap_or_else(|_| {
181 vec![("configuration", {
182 let mut value =
183 serde_json::to_value(sandbox).expect("a sandbox serializes to JSON");
184 value["egress"] = sandbox.setup_digest_value();
185 value
186 .as_object_mut()
187 .expect("sandbox config is an object")
188 .remove("defaultPolicy");
189 value
190 })]
191 })
192}