🛰️ export from upstream (9298ca3)
This commit is contained in:
@@ -80,6 +80,8 @@ struct RawConfig {
|
||||
services: Option<HashMap<String, ServiceOverride>>,
|
||||
#[serde(default)]
|
||||
default_services: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
|
||||
/// Absorb everything else so we can warn about unknown fields.
|
||||
#[serde(flatten)]
|
||||
@@ -121,13 +123,15 @@ pub struct SandcageConfig {
|
||||
pub services: Option<HashMap<String, ServiceOverride>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_services: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image: Option<String>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Known keys — used to filter the flattened `extra` map
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const KNOWN_KEYS: &[&str] = &["env", "packages", "toolchains", "mounts", "shell", "justfile", "dockerfiles", "trusted_projects", "container_workspace", "agent_args", "ssh_mode", "ssh_keys", "services", "default_services"];
|
||||
const KNOWN_KEYS: &[&str] = &["env", "packages", "toolchains", "mounts", "shell", "justfile", "dockerfiles", "trusted_projects", "container_workspace", "agent_args", "ssh_mode", "ssh_keys", "services", "default_services", "image"];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Validation helpers
|
||||
@@ -184,6 +188,7 @@ fn from_raw(raw: RawConfig) -> SandcageConfig {
|
||||
ssh_keys: raw.ssh_keys,
|
||||
services: raw.services,
|
||||
default_services: raw.default_services,
|
||||
image: raw.image,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,4 +723,17 @@ services:
|
||||
"project should override global"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_image_field() {
|
||||
let yaml = "image: my-custom\n";
|
||||
let cfg = load_from_str(yaml).expect("parse image field");
|
||||
assert_eq!(cfg.image.as_deref(), Some("my-custom"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_defaults_to_none() {
|
||||
let cfg = load_from_str("").expect("parse empty");
|
||||
assert!(cfg.image.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,12 +190,15 @@ pub fn build_compose_context(workspace: &Path, config: &SandcageConfig) -> Resul
|
||||
None => default_container_dir(workspace),
|
||||
};
|
||||
|
||||
let image = config.image.as_deref().unwrap_or("sandcage").to_string();
|
||||
|
||||
Ok(crate::service::ComposeContext {
|
||||
uid,
|
||||
gid,
|
||||
workspace: workspace.to_path_buf(),
|
||||
container_dir,
|
||||
sandcage_home,
|
||||
image,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -220,10 +223,6 @@ fn write_compose_tempfile(content: &str) -> Result<tempfile::NamedTempFile> {
|
||||
Ok(tmp)
|
||||
}
|
||||
|
||||
fn image_for_service(_service: &str) -> &'static str {
|
||||
"sandcage"
|
||||
}
|
||||
|
||||
fn require_image(docker: &Path, image: &str) -> Result<()> {
|
||||
let output = Command::new(docker)
|
||||
.args(["image", "inspect", image])
|
||||
@@ -355,8 +354,9 @@ pub fn run_service(
|
||||
});
|
||||
}
|
||||
|
||||
let image = image_for_service(service);
|
||||
require_image(&docker, image)?;
|
||||
let ctx = build_compose_context(workspace, config)?;
|
||||
let image_tag = format!("{}:latest", ctx.image);
|
||||
require_image(&docker, &image_tag)?;
|
||||
|
||||
if config.ssh_mode.as_deref() == Some("volume") {
|
||||
let vol_check = Command::new(&docker)
|
||||
@@ -369,7 +369,6 @@ pub fn run_service(
|
||||
}
|
||||
}
|
||||
|
||||
let ctx = build_compose_context(workspace, config)?;
|
||||
let compose_content = crate::service::compose::generate_compose(registry, &ctx);
|
||||
let compose_file = write_compose_tempfile(&compose_content)?;
|
||||
let compose_path = compose_file.path().to_string_lossy().into_owned();
|
||||
@@ -605,8 +604,9 @@ pub fn build_images(force: bool, service_filter: &[String]) -> Result<()> {
|
||||
}
|
||||
|
||||
|
||||
let images: &[(&str, &str)] = &[
|
||||
("sandcage", DOCKERFILE),
|
||||
let image_name = global_cfg.image.as_deref().unwrap_or("sandcage");
|
||||
let images: Vec<(&str, &str)> = vec![
|
||||
(image_name, DOCKERFILE),
|
||||
];
|
||||
|
||||
let hashes_path = build_hashes_path()?;
|
||||
@@ -622,7 +622,7 @@ pub fn build_images(force: bool, service_filter: &[String]) -> Result<()> {
|
||||
sha256_hex(bundled_dockerfile)
|
||||
};
|
||||
|
||||
let needs_build = force || stored.get(*image) != Some(¤t_hash);
|
||||
let needs_build = force || stored.get(image) != Some(¤t_hash);
|
||||
|
||||
if needs_build {
|
||||
if force {
|
||||
@@ -672,6 +672,7 @@ mod tests {
|
||||
workspace: PathBuf::from("/tmp/test"),
|
||||
container_dir: "/workspace/test".to_string(),
|
||||
sandcage_home: PathBuf::from("/home/user/.sandcage"),
|
||||
image: "sandcage".to_string(),
|
||||
};
|
||||
let yaml = generate_compose(®istry, &ctx);
|
||||
|
||||
@@ -860,14 +861,6 @@ mod tests {
|
||||
assert!(path.exists(), "hash file should have been created");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_for_service_maps_correctly() {
|
||||
assert_eq!(image_for_service("claude"), "sandcage");
|
||||
assert_eq!(image_for_service("codex"), "sandcage");
|
||||
assert_eq!(image_for_service("shell"), "sandcage");
|
||||
assert_eq!(image_for_service("anything-else"), "sandcage");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn require_image_fails_for_nonexistent_image() {
|
||||
let docker = match require_docker() {
|
||||
|
||||
@@ -37,6 +37,7 @@ mod tests {
|
||||
workspace: PathBuf::from("/home/user/projects/my-app"),
|
||||
container_dir: "/workspace/my-app".to_string(),
|
||||
sandcage_home: PathBuf::from("/home/user/.sandcage"),
|
||||
image: "sandcage".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ pub struct ComposeContext {
|
||||
pub workspace: PathBuf,
|
||||
pub container_dir: String,
|
||||
pub sandcage_home: PathBuf,
|
||||
pub image: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -96,7 +97,7 @@ impl Service for ServiceDef {
|
||||
};
|
||||
|
||||
ComposeServiceDef {
|
||||
image: "sandcage:latest".to_string(),
|
||||
image: format!("{}:latest", ctx.image),
|
||||
entrypoint,
|
||||
working_dir: ctx.container_dir.clone(),
|
||||
user: format!("{}:{}", ctx.uid, ctx.gid),
|
||||
@@ -130,6 +131,7 @@ mod tests {
|
||||
workspace: PathBuf::from("/home/user/projects/my-app"),
|
||||
container_dir: "/workspace/my-app".to_string(),
|
||||
sandcage_home: PathBuf::from("/home/user/.sandcage"),
|
||||
image: "sandcage".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,4 +233,13 @@ mod tests {
|
||||
let compose = svc.compose_service(&ctx);
|
||||
assert_eq!(compose.entrypoint, vec!["/bin/zsh"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compose_service_uses_configured_image() {
|
||||
let svc = agent_service();
|
||||
let mut ctx = test_context();
|
||||
ctx.image = "custom-img".to_string();
|
||||
let compose = svc.compose_service(&ctx);
|
||||
assert_eq!(compose.image, "custom-img:latest");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +267,26 @@ default_services:
|
||||
|
||||
---
|
||||
|
||||
### image
|
||||
|
||||
The Docker image name used for building and running containers.
|
||||
|
||||
- **Type:** String
|
||||
- **Valid in:** Global, Project, Local
|
||||
- **Default:** `"sandcage"`
|
||||
|
||||
```yaml
|
||||
# .sandcage.yml
|
||||
image: my-custom-sandcage
|
||||
```
|
||||
|
||||
```toml
|
||||
# ~/.sandcage/config.toml
|
||||
image = "my-custom-sandcage"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Services
|
||||
|
||||
Sandcage ships with four built-in services, all enabled by default:
|
||||
|
||||
Reference in New Issue
Block a user