opencode_cloud_core/docker/
profile.rs1use super::container::CONTAINER_NAME;
8use super::dockerfile::IMAGE_TAG_DEFAULT;
9use super::volume::{
10 VOLUME_CACHE, VOLUME_CONFIG, VOLUME_PROJECTS, VOLUME_SESSION, VOLUME_SSH, VOLUME_STATE,
11 VOLUME_USERS,
12};
13use std::env;
14
15pub const SANDBOX_INSTANCE_ENV: &str = "OPENCODE_SANDBOX_INSTANCE";
17
18pub const INSTANCE_LABEL_KEY: &str = "opencode-cloud.instance";
20
21const PREVIOUS_TAG_DEFAULT: &str = "previous";
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct DockerResourceNames {
26 pub instance_id: Option<String>,
27 pub suffix: Option<String>,
28 pub container_name: String,
29 pub hostname: String,
30 pub image_tag: String,
31 pub previous_image_tag: String,
32 pub volume_session: String,
33 pub volume_state: String,
34 pub volume_cache: String,
35 pub volume_projects: String,
36 pub volume_config: String,
37 pub volume_users: String,
38 pub volume_ssh: String,
39 pub image_state_file: String,
40}
41
42impl DockerResourceNames {
43 pub fn volume_names(&self) -> [&str; 7] {
44 [
45 &self.volume_session,
46 &self.volume_state,
47 &self.volume_cache,
48 &self.volume_projects,
49 &self.volume_config,
50 &self.volume_users,
51 &self.volume_ssh,
52 ]
53 }
54}
55
56pub fn active_resource_names() -> DockerResourceNames {
58 resource_names_for_instance(env_instance_id().as_deref())
59}
60
61pub fn resource_names_for_instance(instance_id: Option<&str>) -> DockerResourceNames {
63 if let Some(instance_id) = instance_id {
64 let suffix = format!("-{instance_id}");
65 DockerResourceNames {
67 instance_id: Some(instance_id.to_string()),
68 suffix: Some(suffix.clone()),
69 container_name: format!("{CONTAINER_NAME}{suffix}"),
70 hostname: format!("{CONTAINER_NAME}{suffix}"),
71 image_tag: format!("instance-{instance_id}"),
72 previous_image_tag: format!("instance-{instance_id}-previous"),
73 volume_session: format!("{VOLUME_SESSION}{suffix}"),
74 volume_state: format!("{VOLUME_STATE}{suffix}"),
75 volume_cache: format!("{VOLUME_CACHE}{suffix}"),
76 volume_projects: format!("{VOLUME_PROJECTS}{suffix}"),
77 volume_config: format!("{VOLUME_CONFIG}{suffix}"),
78 volume_users: format!("{VOLUME_USERS}{suffix}"),
79 volume_ssh: format!("{VOLUME_SSH}{suffix}"),
80 image_state_file: format!("image-state-{instance_id}.json"),
81 }
82 } else {
83 DockerResourceNames {
84 instance_id: None,
85 suffix: None,
86 container_name: CONTAINER_NAME.to_string(),
87 hostname: CONTAINER_NAME.to_string(),
88 image_tag: IMAGE_TAG_DEFAULT.to_string(),
89 previous_image_tag: PREVIOUS_TAG_DEFAULT.to_string(),
90 volume_session: VOLUME_SESSION.to_string(),
91 volume_state: VOLUME_STATE.to_string(),
92 volume_cache: VOLUME_CACHE.to_string(),
93 volume_projects: VOLUME_PROJECTS.to_string(),
94 volume_config: VOLUME_CONFIG.to_string(),
95 volume_users: VOLUME_USERS.to_string(),
96 volume_ssh: VOLUME_SSH.to_string(),
97 image_state_file: "image-state.json".to_string(),
98 }
99 }
100}
101
102pub fn remap_container_name(name: &str) -> String {
104 if name == CONTAINER_NAME {
105 return active_resource_names().container_name;
106 }
107 name.to_string()
108}
109
110pub fn remap_image_tag(tag: &str) -> String {
112 let names = active_resource_names();
113 if tag == IMAGE_TAG_DEFAULT {
114 return names.image_tag;
115 }
116 if tag == PREVIOUS_TAG_DEFAULT {
117 return names.previous_image_tag;
118 }
119 tag.to_string()
120}
121
122pub fn env_instance_id() -> Option<String> {
124 let raw = env::var(SANDBOX_INSTANCE_ENV).ok()?;
125 let trimmed = raw.trim().to_ascii_lowercase();
126 if is_valid_instance_id(&trimmed) {
127 Some(trimmed)
128 } else {
129 None
130 }
131}
132
133fn is_valid_instance_id(value: &str) -> bool {
134 let bytes = value.as_bytes();
135 if bytes.is_empty() || bytes.len() > 32 {
136 return false;
137 }
138 if !bytes[0].is_ascii_lowercase() && !bytes[0].is_ascii_digit() {
139 return false;
140 }
141 bytes
142 .iter()
143 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || *b == b'-')
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn legacy_names_remain_unchanged() {
152 let names = resource_names_for_instance(None);
153 assert_eq!(names.container_name, CONTAINER_NAME);
154 assert_eq!(names.image_tag, IMAGE_TAG_DEFAULT);
155 assert_eq!(names.volume_users, VOLUME_USERS);
156 assert_eq!(names.image_state_file, "image-state.json");
157 assert!(names.instance_id.is_none());
158 }
159
160 #[test]
161 fn isolated_names_include_suffixes() {
162 let names = resource_names_for_instance(Some("foo"));
163 assert_eq!(names.container_name, "opencode-cloud-sandbox-foo");
164 assert_eq!(names.image_tag, "instance-foo");
165 assert_eq!(names.previous_image_tag, "instance-foo-previous");
166 assert_eq!(names.volume_users, "opencode-users-foo");
167 assert_eq!(names.volume_ssh, "opencode-ssh-foo");
168 assert_eq!(names.image_state_file, "image-state-foo.json");
169 assert_eq!(names.instance_id.as_deref(), Some("foo"));
170 }
171
172 #[test]
173 fn env_instance_id_rejects_invalid_values() {
174 assert!(is_valid_instance_id("foo-123"));
175 assert!(!is_valid_instance_id(""));
176 assert!(!is_valid_instance_id("-foo"));
177 assert!(!is_valid_instance_id("foo_bar"));
178 assert!(!is_valid_instance_id("Foo"));
179 }
180}