Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

OpenShell ships a set of [pre-built sandbox images](https://github.com/NVIDIA/OpenShell-Community), but they are general-purpose. `openshell-image-builder` lets you build your own: lightweight, workspace-specific images that contain only what you need — without writing a Containerfile by hand.

- **Base image selection** — Ubuntu or Fedora, any tag.
- **Base image selection** — Ubuntu, Fedora, or Red Hat UBI, any tag.
- **Agent installation and configuration** — pre-installed in `PATH` with scoped network access to agent-specific endpoints. Settings files can be embedded into the image from a local directory.
- **Inference configuration** — scoped network access to LLM backends.
- **Dev Container Features** — install toolchains and utilities declared in your Kaiden workspace configuration.
Expand Down Expand Up @@ -61,15 +61,15 @@ If a directory is given explicitly (via `--config` or the environment variable)
version = 1

[openshell_image_builder.base_image]
image = "ubuntu" # or "fedora"
tag = "24.04" # ubuntu: "24.04", "22.04", … — fedora: "latest", "43", "42", …
image = "ubuntu" # "ubuntu", "fedora", or "ubi"
tag = "24.04" # ubuntu: "24.04", "22.04", … — fedora: "latest", "43", "42", … — ubi: "10.2-1780377767", …
```

| Field | Default | Description |
| ------------------------------------------ | -------- | ---------------------------- |
| `openshell_image_builder.version` | `1` | Configuration schema version |
| `openshell_image_builder.base_image.image` | `ubuntu` | Base image name (`ubuntu` or `fedora`) |
| `openshell_image_builder.base_image.tag` | `24.04` | Base image tag — Ubuntu: `24.04`, `22.04`, …; Fedora: `latest`, `43`, `42`, … |
| `openshell_image_builder.base_image.image` | `ubuntu` | Base image name (`ubuntu`, `fedora`, or `ubi`) |
| `openshell_image_builder.base_image.tag` | `24.04` | Base image tag — Ubuntu: `24.04`, `22.04`, …; Fedora: `latest`, `43`, `42`, …; UBI: `10.2-1780377767`, … |

### Loading from a specific config directory

Expand Down
116 changes: 100 additions & 16 deletions src/containerfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,40 @@ pub fn generate(
) -> Result<String, ContainerfileError> {
let tag = &config.base_image.tag;
let system_stage = match config.base_image.image.as_str() {
"fedora" => fedora_system_stage(tag),
"fedora" => dnf_system_stage(
"registry.fedoraproject.org/fedora",
tag,
&[
"bind-utils",
"ca-certificates",
"curl",
"iproute",
"iptables",
"iputils",
"net-tools",
"nftables",
"nmap-ncat",
"openssh-server",
"procps-ng",
"traceroute",
"which",
],
),
"ubi" => dnf_system_stage(
"registry.access.redhat.com/ubi10/ubi",
tag,
&[
"bind-utils",
"ca-certificates",
"iputils",
"net-tools",
"nftables",
"nmap-ncat",
"openssh-server",
"procps-ng",
"which",
],
),
"ubuntu" => ubuntu_system_stage(tag),
image => {
return Err(ContainerfileError::NotSupported {
Expand Down Expand Up @@ -166,28 +199,21 @@ RUN groupadd -r supervisor && useradd -r -g supervisor -s /usr/sbin/nologin supe
)
}

fn fedora_system_stage(tag: &str) -> String {
fn dnf_system_stage(base_image: &str, tag: &str, packages: &[&str]) -> String {
let pkg_lines = packages
.iter()
.map(|p| format!(" {p} \\"))
.collect::<Vec<_>>()
.join("\n");
format!(
r#"# System base
FROM registry.fedoraproject.org/fedora:{tag} AS system
FROM {base_image}:{tag} AS system

WORKDIR /sandbox

# Core system dependencies
RUN dnf install -y --setopt=install_weak_deps=False \
bind-utils \
ca-certificates \
curl \
iproute \
iptables \
iputils \
net-tools \
nftables \
nmap-ncat \
openssh-server \
procps-ng \
traceroute \
which \
{pkg_lines}
&& dnf clean all

RUN groupadd -r supervisor && useradd -r -g supervisor -s /usr/sbin/nologin supervisor && \
Expand Down Expand Up @@ -257,6 +283,16 @@ mod tests {
}
}

fn ubi_config() -> Config {
Config {
version: 1,
base_image: BaseImageConfig {
image: "ubi".to_string(),
tag: "10.2-1780377767".to_string(),
},
}
}

struct MockAgent;

impl Agent for MockAgent {
Expand Down Expand Up @@ -383,6 +419,54 @@ mod tests {
assert!(!content.contains("RUN echo mock-agent"));
}

#[test]
fn ubi_generates_successfully() {
assert!(generate(&ubi_config(), None, &[], false, &[]).is_ok());
}

#[test]
fn ubi_containerfile_contains_tag() {
let content = generate(&ubi_config(), None, &[], false, &[]).unwrap();
assert!(
content.contains("FROM registry.access.redhat.com/ubi10/ubi:10.2-1780377767 AS system")
);
}

#[test]
fn ubi_containerfile_tag_is_substituted() {
let content = generate(&ubi_config(), None, &[], false, &[]).unwrap();
assert!(!content.contains("{tag}"));
}

#[test]
fn ubi_with_agent_includes_install() {
let content = generate(&ubi_config(), Some(&MockAgent), &[], false, &[]).unwrap();
assert!(content.contains("RUN echo mock-agent"));
}

#[test]
fn ubi_agent_install_runs_as_sandbox_user() {
let content = generate(&ubi_config(), Some(&MockAgent), &[], false, &[]).unwrap();
let user_pos = content.find("USER sandbox").unwrap();
let install_pos = content.find("RUN echo mock-agent").unwrap();
assert!(
install_pos > user_pos,
"agent install must appear after USER sandbox"
);
}

#[test]
fn ubi_without_agent_omits_install() {
let content = generate(&ubi_config(), None, &[], false, &[]).unwrap();
assert!(!content.contains("RUN echo mock-agent"));
}

#[test]
fn ubi_copies_policy_yaml() {
let content = generate(&ubi_config(), None, &[], false, &[]).unwrap();
assert!(content.contains("COPY policy.yaml /etc/openshell/policy.yaml"));
}

#[test]
fn not_supported_error_message() {
let err = ContainerfileError::NotSupported {
Expand Down
Loading