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