1use super::BindingValue;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(tag = "service")]
17pub enum SandboxBinding {
18 #[serde(rename = "sandbox-aws")]
20 Aws(AwsSandboxBinding),
21 #[serde(rename = "sandbox-azure")]
23 Azure(AzureSandboxBinding),
24 #[serde(rename = "sandbox-gcp")]
26 Gcp(GcpSandboxBinding),
27 #[serde(rename = "sandbox-kubernetes")]
29 Kubernetes(KubernetesSandboxBinding),
30 #[serde(rename = "sandbox-local")]
32 Local(LocalSandboxBinding),
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct AwsSandboxBinding {
39 pub image_arn: BindingValue<String>,
41 pub image_version: BindingValue<String>,
44 pub region: BindingValue<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub execution_role_arn: Option<BindingValue<String>>,
49 #[serde(default, skip_serializing_if = "Vec::is_empty")]
55 pub egress_connector_arns: Vec<BindingValue<String>>,
56 #[serde(default, skip_serializing_if = "Vec::is_empty")]
62 pub preview_ports: Vec<u16>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub idle_suspend_seconds: Option<u32>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub max_lifetime_seconds: Option<u32>,
72 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
78 pub allow_egress: bool,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct AzureSandboxBinding {
85 pub sandbox_group: BindingValue<String>,
87 pub data_plane_endpoint: BindingValue<String>,
89 pub region: BindingValue<String>,
91 pub resource_group: BindingValue<String>,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct GcpSandboxBinding {
103 pub launcher_path: BindingValue<String>,
105 pub allow_egress: BindingValue<bool>,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct KubernetesSandboxBinding {
115 pub namespace: BindingValue<String>,
117 pub runtime_class: BindingValue<String>,
119 pub selector: BindingValue<String>,
121 pub broker_url: BindingValue<String>,
124 pub key_name: BindingValue<String>,
127 pub token_path: BindingValue<String>,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct LocalSandboxBinding {
136 pub manager_url: BindingValue<String>,
138 pub sandbox_key: BindingValue<String>,
140 pub token_path: BindingValue<String>,
143}
144
145impl SandboxBinding {
146 pub fn aws(
148 image_arn: impl Into<BindingValue<String>>,
149 image_version: impl Into<BindingValue<String>>,
150 region: impl Into<BindingValue<String>>,
151 ) -> Self {
152 Self::Aws(AwsSandboxBinding {
153 image_arn: image_arn.into(),
154 image_version: image_version.into(),
155 region: region.into(),
156 execution_role_arn: None,
157 egress_connector_arns: Vec::new(),
158 preview_ports: Vec::new(),
159 idle_suspend_seconds: None,
160 max_lifetime_seconds: None,
161 allow_egress: false,
162 })
163 }
164
165 pub fn azure(
167 sandbox_group: impl Into<BindingValue<String>>,
168 data_plane_endpoint: impl Into<BindingValue<String>>,
169 region: impl Into<BindingValue<String>>,
170 resource_group: impl Into<BindingValue<String>>,
171 ) -> Self {
172 Self::Azure(AzureSandboxBinding {
173 sandbox_group: sandbox_group.into(),
174 data_plane_endpoint: data_plane_endpoint.into(),
175 region: region.into(),
176 resource_group: resource_group.into(),
177 })
178 }
179
180 pub fn gcp(
182 launcher_path: impl Into<BindingValue<String>>,
183 allow_egress: impl Into<BindingValue<bool>>,
184 ) -> Self {
185 Self::Gcp(GcpSandboxBinding {
186 launcher_path: launcher_path.into(),
187 allow_egress: allow_egress.into(),
188 })
189 }
190
191 pub fn kubernetes(
193 namespace: impl Into<BindingValue<String>>,
194 runtime_class: impl Into<BindingValue<String>>,
195 selector: impl Into<BindingValue<String>>,
196 broker_url: impl Into<BindingValue<String>>,
197 key_name: impl Into<BindingValue<String>>,
198 token_path: impl Into<BindingValue<String>>,
199 ) -> Self {
200 Self::Kubernetes(KubernetesSandboxBinding {
201 namespace: namespace.into(),
202 runtime_class: runtime_class.into(),
203 selector: selector.into(),
204 broker_url: broker_url.into(),
205 key_name: key_name.into(),
206 token_path: token_path.into(),
207 })
208 }
209
210 pub fn local(
212 manager_url: impl Into<BindingValue<String>>,
213 sandbox_key: impl Into<BindingValue<String>>,
214 token_path: impl Into<BindingValue<String>>,
215 ) -> Self {
216 Self::Local(LocalSandboxBinding {
217 manager_url: manager_url.into(),
218 sandbox_key: sandbox_key.into(),
219 token_path: token_path.into(),
220 })
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use crate::bindings::{ContainerBinding, KvBinding};
228
229 #[test]
230 fn every_variant_roundtrips() {
231 let bindings = vec![
232 SandboxBinding::aws(
233 "arn:aws:lambda:us-east-2:1:microvm-image:sbx",
234 "3",
235 "us-east-2",
236 ),
237 SandboxBinding::azure(
238 "sbg1",
239 "https://management.swedencentral.azuredevcompute.io",
240 "swedencentral",
241 "rg",
242 ),
243 SandboxBinding::gcp("/usr/local/gcp/bin/sandbox", false),
244 SandboxBinding::kubernetes(
245 "alien-sandboxes",
246 "gvisor",
247 "alien.dev/sandbox=agent",
248 "http://alien-operator.alien.svc:8080",
249 "alien-sandbox-agent-capability",
250 "/var/run/secrets/kubernetes.io/serviceaccount/token",
251 ),
252 SandboxBinding::local(
253 "http://127.0.0.1:8931",
254 "agent",
255 "/state/sandbox-manager.token",
256 ),
257 ];
258
259 for binding in bindings {
260 let json = serde_json::to_string(&binding).expect("serializes");
261 let restored: SandboxBinding = serde_json::from_str(&json).expect("deserializes");
262 assert_eq!(binding, restored, "roundtrip changed the binding: {json}");
263 }
264 }
265
266 #[test]
267 fn service_tags_are_prefixed_and_distinct() {
268 let tags: Vec<String> = vec![
269 SandboxBinding::aws("a", "1", "r"),
270 SandboxBinding::azure("g", "e", "r", "rg"),
271 SandboxBinding::gcp("p", true),
272 SandboxBinding::kubernetes("n", "gvisor", "s", "http://op:8080", "k", "/t"),
273 SandboxBinding::local("u", "k", "t"),
274 ]
275 .iter()
276 .map(|binding| {
277 serde_json::to_value(binding).expect("serializes")["service"]
278 .as_str()
279 .expect("has a service tag")
280 .to_string()
281 })
282 .collect();
283
284 for tag in &tags {
285 assert!(tag.starts_with("sandbox-"), "tag '{tag}' is not namespaced");
286 }
287
288 let mut unique = tags.clone();
289 unique.sort();
290 unique.dedup();
291 assert_eq!(unique.len(), tags.len(), "duplicate service tags: {tags:?}");
292 }
293
294 #[test]
298 fn a_sandbox_binding_cannot_deserialize_as_another_resource() {
299 let json = serde_json::to_string(&SandboxBinding::local(
300 "http://127.0.0.1:8931",
301 "agent",
302 "/state/sandbox-manager.token",
303 ))
304 .expect("serializes");
305
306 serde_json::from_str::<KvBinding>(&json)
307 .expect_err("a sandbox binding must not parse as a KV binding");
308 serde_json::from_str::<ContainerBinding>(&json)
309 .expect_err("a sandbox binding must not parse as a container binding");
310 }
311
312 #[test]
313 fn another_resource_binding_cannot_deserialize_as_a_sandbox() {
314 let json = serde_json::to_string(&ContainerBinding::local("api", "http://api.svc:8080"))
315 .expect("serializes");
316
317 serde_json::from_str::<SandboxBinding>(&json)
318 .expect_err("a container binding must not parse as a sandbox binding");
319 }
320}