feat(gateway): support runtime image env overrides (#3504)

Closes #3502

Resolve trusted OCI runtime images with driver TOML, process environment, and compiled-default precedence across Docker, Podman, and Kubernetes.

Signed-off-by: Evan Lezar <elezar@nvidia.com>
This commit is contained in:
Evan Lezar
2026-10-05 12:23:13 +00:00
committed by GitHub
parent e7fdd6beef
commit e9271cb313
20 changed files with 312 additions and 52 deletions
+48 -9
View File
@@ -89,24 +89,48 @@ pub const DEFAULT_SUPERVISOR_IMAGE_REPO: &str = "ghcr.io/nvidia/openshell/superv
/// Default OCI repository for the sandbox runtime image (no tag).
pub const DEFAULT_SANDBOX_RUNTIME_IMAGE_REPO: &str = "ghcr.io/nvidia/openshell/sandbox";
/// Return the default sandbox runtime image reference with a version-pinned tag.
#[must_use]
pub fn default_sandbox_runtime_image() -> String {
/// Process-level default for the trusted sandbox runtime image.
pub const SANDBOX_RUNTIME_IMAGE_ENV: &str = "OPENSHELL_SANDBOX_RUNTIME_IMAGE";
/// Process-level default for the trusted supervisor image.
pub const SUPERVISOR_IMAGE_ENV: &str = "OPENSHELL_SUPERVISOR_IMAGE";
fn compiled_sandbox_runtime_image() -> String {
format!(
"{DEFAULT_SANDBOX_RUNTIME_IMAGE_REPO}:{}",
default_supervisor_image_tag()
)
}
/// Return the default supervisor image reference with a version-pinned tag.
#[must_use]
pub fn default_supervisor_image() -> String {
fn compiled_supervisor_image() -> String {
format!(
"{DEFAULT_SUPERVISOR_IMAGE_REPO}:{}",
default_supervisor_image_tag()
)
}
fn runtime_image_default(environment_value: Option<String>, compiled_default: String) -> String {
environment_value.unwrap_or(compiled_default)
}
/// Return the process-configured sandbox runtime image, or the compiled default.
#[must_use]
pub fn default_sandbox_runtime_image() -> String {
runtime_image_default(
std::env::var(SANDBOX_RUNTIME_IMAGE_ENV).ok(),
compiled_sandbox_runtime_image(),
)
}
/// Return the process-configured supervisor image, or the compiled default.
#[must_use]
pub fn default_supervisor_image() -> String {
runtime_image_default(
std::env::var(SUPERVISOR_IMAGE_ENV).ok(),
compiled_supervisor_image(),
)
}
fn default_supervisor_image_tag() -> String {
resolve_supervisor_image_tag(&[
option_env!("OPENSHELL_IMAGE_TAG").unwrap_or(""),
@@ -1644,15 +1668,30 @@ mod tests {
#[test]
fn default_supervisor_image_is_version_pinned() {
use super::{default_sandbox_runtime_image, default_supervisor_image};
let image = default_supervisor_image();
use super::{compiled_sandbox_runtime_image, compiled_supervisor_image};
let image = compiled_supervisor_image();
assert!(image.starts_with("ghcr.io/nvidia/openshell/supervisor:"));
let tag = image.rsplit_once(':').unwrap().1;
assert!(!tag.is_empty());
let sandbox_image = default_sandbox_runtime_image();
let sandbox_image = compiled_sandbox_runtime_image();
assert!(sandbox_image.starts_with("ghcr.io/nvidia/openshell/sandbox:"));
let sandbox_tag = sandbox_image.rsplit_once(':').unwrap().1;
assert!(!sandbox_tag.is_empty());
}
#[test]
fn runtime_image_environment_value_replaces_compiled_default() {
use super::runtime_image_default;
let digest = format!("registry.example.com/sandbox@sha256:{}", "a".repeat(64));
assert_eq!(
runtime_image_default(Some(digest.clone()), "compiled:default".to_string()),
digest
);
assert_eq!(
runtime_image_default(None, "compiled:default".to_string()),
"compiled:default"
);
}
}
@@ -25,6 +25,14 @@ struct Args {
#[arg(long, env = "OPENSHELL_DOCKER_DRIVER_CONFIG")]
config: PathBuf,
/// OCI image containing the `openshell-sandbox` runtime binary.
#[arg(long, env = openshell_core::config::SANDBOX_RUNTIME_IMAGE_ENV)]
sandbox_runtime_image: Option<String>,
/// OCI image containing the `openshell-supervisor` control binary.
#[arg(long, env = openshell_core::config::SUPERVISOR_IMAGE_ENV)]
supervisor_image: Option<String>,
/// Gateway listener address used to derive the supervisor endpoint.
#[arg(
long,
@@ -63,6 +71,12 @@ async fn main() -> Result<()> {
docker_config.allow_driver_config = policy.allow_driver_config;
docker_config.resource_admission = policy.resource_admission;
}
if let Some(image) = args.sandbox_runtime_image {
docker_config.sandbox_runtime_image = Some(image);
}
if let Some(image) = args.supervisor_image {
docker_config.supervisor_image = Some(image);
}
let driver = DockerComputeDriver::new(args.gateway_bind, &args.log_level, &docker_config)
.await
.into_diagnostic()?;
@@ -2503,6 +2503,23 @@ fn validate_sandbox_rejects_unknown_driver_config_fields() {
assert!(err.message().contains("unknown field"));
}
#[test]
fn sandbox_driver_config_rejects_trusted_runtime_image_overrides() {
for field in ["sandbox_runtime_image", "supervisor_image"] {
let template = DriverSandboxTemplate {
driver_config: Some(json_struct(serde_json::json!({
(field): "registry.example.com/openshell/runtime:untrusted"
}))),
..Default::default()
};
let error = DockerSandboxDriverConfig::from_template(&template)
.expect_err("sandbox requests must not select trusted runtime images");
assert!(error.contains("unknown field"), "{error}");
assert!(error.contains(field), "{error}");
}
}
#[test]
fn validate_sandbox_accepts_gpu_count_request_shape() {
let mut config = runtime_config();
@@ -8865,6 +8865,23 @@ mod tests {
assert!(err.contains("unknown field"));
}
#[test]
fn sandbox_driver_config_rejects_trusted_runtime_image_overrides() {
for field in ["sandbox_runtime_image", "supervisor_image"] {
let template = SandboxTemplate {
driver_config: Some(json_struct(serde_json::json!({
(field): "registry.example.com/openshell/runtime:untrusted"
}))),
..Default::default()
};
let error = KubernetesSandboxDriverConfig::from_template(&template)
.expect_err("sandbox requests must not select trusted runtime images");
assert!(error.contains("unknown field"), "{error}");
assert!(error.contains(field), "{error}");
}
}
#[test]
fn driver_config_for_spec_rejects_unknown_fields() {
let sandbox = Sandbox {
@@ -116,13 +116,13 @@ struct Args {
#[arg(long, env = "OPENSHELL_HOST_GATEWAY_IP")]
host_gateway_ip: Option<String>,
#[arg(long, env = "OPENSHELL_SANDBOX_RUNTIME_IMAGE")]
#[arg(long, env = openshell_core::config::SANDBOX_RUNTIME_IMAGE_ENV)]
sandbox_runtime_image: Option<String>,
#[arg(long, env = "OPENSHELL_SANDBOX_RUNTIME_IMAGE_PULL_POLICY")]
sandbox_runtime_image_pull_policy: Option<KubernetesImagePullPolicy>,
#[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE")]
#[arg(long, env = openshell_core::config::SUPERVISOR_IMAGE_ENV)]
supervisor_image: Option<String>,
#[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE_PULL_POLICY")]
@@ -2313,6 +2313,25 @@ mod tests {
assert!(err.to_string().contains("unknown field"));
}
#[test]
fn sandbox_driver_config_rejects_trusted_runtime_image_overrides() {
use openshell_core::proto::compute::v1::DriverSandboxTemplate;
for field in ["sandbox_runtime_image", "supervisor_image"] {
let template = DriverSandboxTemplate {
driver_config: Some(json_struct(serde_json::json!({
(field): "registry.example.com/openshell/runtime:untrusted"
}))),
..Default::default()
};
let error = PodmanSandboxDriverConfig::from_template(&template)
.expect_err("sandbox requests must not select trusted runtime images");
assert!(error.to_string().contains("unknown field"), "{error}");
assert!(error.to_string().contains(field), "{error}");
}
}
#[test]
fn container_spec_defaults_drop_capabilities_and_keep_runtime_seccomp() {
let sandbox = test_sandbox("test-id", "test-name");
+2 -2
View File
@@ -110,11 +110,11 @@ struct Args {
health_check_interval_secs: Option<NonZeroU64>,
/// OCI image containing the `openshell-sandbox` runtime binary.
#[arg(long, env = "OPENSHELL_SANDBOX_RUNTIME_IMAGE")]
#[arg(long, env = openshell_core::config::SANDBOX_RUNTIME_IMAGE_ENV)]
sandbox_runtime_image: Option<String>,
/// OCI image containing the `openshell-supervisor` control binary.
#[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE")]
#[arg(long, env = openshell_core::config::SUPERVISOR_IMAGE_ENV)]
supervisor_image: Option<String>,
/// Host path to the CA certificate for supervisor-to-gateway TLS.
+67 -2
View File
@@ -42,6 +42,18 @@ use openshell_core::telemetry::TelemetryComputeDriver;
use openshell_server::ComputeDriverRegistration;
use openshell_server::ComputeDriverRegistry;
#[cfg(all(
not(target_os = "windows"),
any(
feature = "compute-driver-docker",
feature = "compute-driver-kubernetes",
feature = "compute-driver-podman"
)
))]
fn prefer_environment<T>(configured: T, environment: Option<T>) -> T {
environment.unwrap_or(configured)
}
/// Install every first-party compute driver linked into the standard gateway.
#[must_use]
pub fn install_default_compute_drivers() -> ComputeDriverRegistry {
@@ -272,6 +284,14 @@ fn kubernetes_config(
) -> openshell_core::Result<openshell_driver_kubernetes::KubernetesComputeConfig> {
let mut config: openshell_driver_kubernetes::KubernetesComputeConfig =
context.driver_config()?;
config.sandbox_runtime_image = prefer_environment(
config.sandbox_runtime_image,
std::env::var(openshell_core::config::SANDBOX_RUNTIME_IMAGE_ENV).ok(),
);
config.supervisor_image = prefer_environment(
config.supervisor_image,
std::env::var(openshell_core::config::SUPERVISOR_IMAGE_ENV).ok(),
);
if let Ok(size) = std::env::var("OPENSHELL_K8S_WORKSPACE_DEFAULT_STORAGE_SIZE") {
config.workspace_default_storage_size = size;
}
@@ -296,7 +316,7 @@ impl openshell_server::ComputeDriverFactory for DockerFactory {
&self,
context: openshell_server::ComputeDriverConfigContext<'_>,
) -> openshell_core::Result<()> {
let config: openshell_driver_docker::DockerComputeConfig = context.driver_config()?;
let config = docker_config(context)?;
config.validate_configuration(context.gateway_bind_address())
}
@@ -304,7 +324,7 @@ impl openshell_server::ComputeDriverFactory for DockerFactory {
&self,
context: openshell_server::ComputeDriverBuildContext<'_>,
) -> openshell_core::Result<openshell_server::ComputeDriverInstance> {
let mut config: openshell_driver_docker::DockerComputeConfig = context.driver_config()?;
let mut config = docker_config(context.config_context())?;
require_guest_tls_for_local_driver(&context, "docker")?;
apply_guest_tls(&mut config.guest_tls_ca, context.guest_tls_ca());
let driver = openshell_driver_docker::DockerComputeDriver::new(
@@ -321,6 +341,26 @@ impl openshell_server::ComputeDriverFactory for DockerFactory {
}
}
#[cfg(all(not(target_os = "windows"), feature = "compute-driver-docker"))]
fn docker_config(
context: openshell_server::ComputeDriverConfigContext<'_>,
) -> openshell_core::Result<openshell_driver_docker::DockerComputeConfig> {
let mut config: openshell_driver_docker::DockerComputeConfig = context.driver_config()?;
config.sandbox_runtime_image = prefer_environment(
config.sandbox_runtime_image,
std::env::var(openshell_core::config::SANDBOX_RUNTIME_IMAGE_ENV)
.ok()
.map(Some),
);
config.supervisor_image = prefer_environment(
config.supervisor_image,
std::env::var(openshell_core::config::SUPERVISOR_IMAGE_ENV)
.ok()
.map(Some),
);
Ok(config)
}
#[cfg(all(not(target_os = "windows"), feature = "compute-driver-podman"))]
#[derive(Clone, Copy)]
struct PodmanFactory;
@@ -364,6 +404,14 @@ fn podman_config(
) -> openshell_core::Result<openshell_driver_podman::PodmanComputeConfig> {
let mut config: openshell_driver_podman::PodmanComputeConfig = context.driver_config()?;
config.gateway_port = context.gateway_port();
config.sandbox_runtime_image = prefer_environment(
config.sandbox_runtime_image,
std::env::var(openshell_core::config::SANDBOX_RUNTIME_IMAGE_ENV).ok(),
);
config.supervisor_image = prefer_environment(
config.supervisor_image,
std::env::var(openshell_core::config::SUPERVISOR_IMAGE_ENV).ok(),
);
if let Ok(path) = std::env::var("OPENSHELL_PODMAN_SOCKET") {
config.socket_path = Some(path.into());
}
@@ -574,6 +622,23 @@ mod windows_tests {
mod tests {
use super::*;
#[test]
#[cfg(all(
not(target_os = "windows"),
any(
feature = "compute-driver-docker",
feature = "compute-driver-kubernetes",
feature = "compute-driver-podman"
)
))]
fn runtime_image_environment_overrides_configured_value() {
assert_eq!(
prefer_environment("driver-toml", Some("process-environment")),
"process-environment"
);
assert_eq!(prefer_environment("driver-toml", None), "driver-toml");
}
#[test]
fn default_registry_contains_exactly_the_enabled_compute_drivers() {
let expected: Vec<&str> = vec![
+9
View File
@@ -175,6 +175,15 @@ need a custom bundle location.
The gateway then starts from built-in defaults and reads
*~/.config/openshell/gateway.toml* when that file exists.
The user service also reads *~/.config/openshell/gateway.env* for both config
preflight and startup. **OPENSHELL_SANDBOX_RUNTIME_IMAGE** and
**OPENSHELL_SUPERVISOR_IMAGE** accept complete tagged or digest-pinned OCI
references for the built-in Docker, Podman, and Kubernetes drivers. These
variables take precedence over explicit image fields in the selected driver's
TOML table, which take precedence over compiled release defaults.
These are trusted gateway inputs and cannot be supplied by sandbox requests.
Do not embed registry credentials in image references.
To persist the service across logouts:
sudo loginctl enable-linger $USER
+17 -2
View File
@@ -51,6 +51,21 @@ editing the TOML file, add them to `~/.config/openshell/gateway.env`:
OPENSHELL_BIND_ADDRESS=192.168.1.10
```
To select exact trusted runtime artifacts across the built-in Docker, Podman,
or Kubernetes driver, set complete tagged or digest-pinned references:
```shell
OPENSHELL_SANDBOX_RUNTIME_IMAGE=registry.example.com/openshell/sandbox@sha256:<digest>
OPENSHELL_SUPERVISOR_IMAGE=registry.example.com/openshell/supervisor@sha256:<digest>
```
`OPENSHELL_SANDBOX_RUNTIME_IMAGE` and `OPENSHELL_SUPERVISOR_IMAGE` take
precedence over explicit image fields in the selected driver's TOML table; the
driver TOML takes precedence over the compiled release default. Preflight and
startup use the same resolution. These values are trusted operator inputs, not
sandbox request fields. Keep registry credentials in Podman's credential store
rather than embedding them in image references.
To override the path to the TOML config file entirely:
```shell
@@ -216,8 +231,8 @@ overrides that persist across package upgrades.
| `bind_address` | `127.0.0.1:17670` (gateway default) | Address for the primary gRPC/HTTP API listener. |
| `compute_driver` | `"podman"` (RPM default) | When unset, the gateway auto-detects Kubernetes, then Podman, then Docker. The RPM default pins to Podman; legacy `compute_drivers` lists are rejected. |
| `[openshell.drivers.podman].default_image` | `nvcr.io/nvidia/base/ubuntu:24.04` | Default sandbox image. |
| `[openshell.drivers.podman].sandbox_runtime_image` | `ghcr.io/nvidia/openshell/sandbox:latest` | Static musl sandbox runtime image mounted into Podman workloads. |
| `[openshell.drivers.podman].supervisor_image` | `ghcr.io/nvidia/openshell/supervisor:latest` | Dynamic glibc supervisor image used outside the workload. |
| `[openshell.drivers.podman].sandbox_runtime_image` | `ghcr.io/nvidia/openshell/sandbox:latest` | Trusted sandbox runtime image. `OPENSHELL_SANDBOX_RUNTIME_IMAGE` overrides this field. |
| `[openshell.drivers.podman].supervisor_image` | `ghcr.io/nvidia/openshell/supervisor:latest` | Trusted supervisor image. `OPENSHELL_SUPERVISOR_IMAGE` overrides this field. |
| `[openshell.gateway].guest_tls_ca` | auto-generated path | Gateway CA injected into the selected local driver for supervisor-to-gateway TLS. Sandbox identity uses a bearer token. |
| `[openshell.gateway.tls]` paths | auto-generated paths | Server TLS certificate, key, and client CA. |
| `disable_tls` | unset | Set to `true` to disable TLS. |
+15
View File
@@ -90,6 +90,21 @@ The script installs a Debian package on Debian and Ubuntu or an RPM package on F
The gateway runs as a systemd user service at `https://127.0.0.1:17670` and reads `~/.config/openshell/gateway.toml`.
Both Debian and RPM load `~/.config/openshell/gateway.env` for configuration
preflight and gateway startup. Use it to select exact trusted runtime artifacts
without creating driver-specific TOML:
```shell
OPENSHELL_SANDBOX_RUNTIME_IMAGE=registry.example.com/openshell/sandbox@sha256:<digest>
OPENSHELL_SUPERVISOR_IMAGE=registry.example.com/openshell/supervisor@sha256:<digest>
```
The environment variables take precedence over image fields in the selected
Docker, Podman, or Kubernetes TOML table. Restart the user service after
editing the file. Treat these variables as trusted operator configuration;
sandbox requests cannot set them, and registry credentials must remain in the
container runtime's credential store or Kubernetes image-pull secrets.
```shell
systemctl --user status openshell-gateway
systemctl --user restart openshell-gateway
@@ -20,6 +20,37 @@ Gateway CLI flag > gateway OPENSHELL_* env var > TOML file > built-in defa
On-disk SQLite databases run in WAL mode with `synchronous=FULL`: the gateway keeps `openshell.db-wal` and `openshell.db-shm` next to the database file, the file must live on a local filesystem, and a backup must use `sqlite3 openshell.db ".backup <copy>"` rather than copying `openshell.db` on its own. Every acknowledged write, including SSH session revocations, survives a power loss. The one exception is SSH session issuance, which skips the per-commit sync; a power loss can drop a just-issued session token, and the client must request a new one.
### Trusted runtime image overrides
Docker, Podman, and Kubernetes consume two gateway-owned trusted runtime
images. Set complete tagged or digest-pinned OCI references for the gateway
process when you need to qualify exact artifacts or use a registry mirror:
```shell
OPENSHELL_SANDBOX_RUNTIME_IMAGE=registry.example.com/openshell/sandbox@sha256:<digest>
OPENSHELL_SUPERVISOR_IMAGE=registry.example.com/openshell/supervisor:1.2.3
```
These settings follow the gateway's normal environment-over-file precedence:
```text
process environment > [openshell.drivers.<selected>] TOML > compiled release default
```
`openshell-gateway config preflight` and gateway startup use the same effective
references. Environment values are handled exactly like the corresponding TOML
fields and passed to the selected container runtime; this feature does not add
separate OCI-reference validation. Configure registry credentials in Docker,
Podman, or Kubernetes image-pull secrets instead of embedding them in a
reference.
The variables affect only the built-in Docker, Podman, and Kubernetes drivers.
They do not affect VM, MXC, or extension drivers. They are trusted gateway
operator inputs and are never accepted from sandbox create requests. For
Debian and RPM user services, place them in
`~/.config/openshell/gateway.env`; the service supplies that file to both
preflight and gateway startup.
`name` assigns an operator-facing identity to the gateway installation. Set it with `[openshell.gateway].name`, `--name`, or `OPENSHELL_GATEWAY_NAME`. It defaults to `openshell`; the Helm chart defaults it to the chart fullname so all replicas in one installation share a name. Chart fullnames are only unique within their Kubernetes namespace, so set `server.name` explicitly when one collector receives telemetry from multiple namespaces or clusters. This identity is independent of client-side gateway aliases, TLS names, and `gateway_jwt.gateway_id`.
## Package-Managed Locations
+8
View File
@@ -36,6 +36,14 @@ When `compute_driver` is unset, the gateway auto-detects Kubernetes, then Podman
Configure driver-specific values, such as images, endpoints, and sizing, under `[openshell.drivers.<name>]`. See the [Gateway Configuration File](/how-it-works/gateways/configuration) reference for every option.
Docker, Podman, and Kubernetes also accept process overrides for the trusted
sandbox-runtime and supervisor images through
`OPENSHELL_SANDBOX_RUNTIME_IMAGE` and `OPENSHELL_SUPERVISOR_IMAGE`. The
environment overrides values in the selected driver's TOML table, and the
driver TOML overrides the compiled release defaults. Helm renders Kubernetes
image fields into TOML, so they apply when these environment variables are
unset. Sandbox requests cannot override either trusted image.
For TLS-enabled Docker, Podman, and MicroVM gateways, set `guest_tls_ca` in
`[openshell.gateway]` or use the package-managed local CA. Remove the retired
`guest_tls_cert` and `guest_tls_key` fields. Supervisors receive only the
@@ -57,7 +57,6 @@
[openshell.drivers.podman]
default_image = "nvcr.io/nvidia/base/ubuntu:24.04"
supervisor_image = "{{ openshell_supervisor_image }}"
image_pull_policy = "always"
network_name = "openshell-test-guest"
grpc_endpoint = "http://host.containers.internal:8080"
@@ -97,6 +96,7 @@
Environment=XDG_CACHE_HOME={{ openshell_gateway_state_root }}/xdg/cache
Environment=XDG_DATA_HOME={{ openshell_gateway_state_root }}/xdg/data
Environment=XDG_STATE_HOME={{ openshell_gateway_state_root }}/xdg/state
Environment=OPENSHELL_SUPERVISOR_IMAGE={{ openshell_supervisor_image }}
ExecStart={{ openshell_gateway_bin }} --config {{ openshell_gateway_state_root }}/gateway.toml
Restart=on-failure
+36
View File
@@ -40,10 +40,20 @@ assert_file_exists() {
service="${ROOT}/deploy/deb/openshell-gateway.service"
control="${ROOT}/deploy/deb/control.in"
spec="${ROOT}/openshell.spec"
package_qualification="${ROOT}/tests/ansible/roles/openshell_packaged_gateway/tasks/main.yaml"
tmachine_gateway_service="${ROOT}/tests/ansible/roles/openshell_gateway/templates/gateway.service.j2"
tmachine_docker_config="${ROOT}/tests/ansible/roles/openshell_gateway/templates/gateway-docker.toml.j2"
tmachine_podman_config="${ROOT}/tests/ansible/roles/openshell_gateway/templates/gateway-podman.toml.j2"
development_gateway="${ROOT}/nix/test-guest/provisioners/roles/gateway-podman/tasks/development-gateway.yml"
assert_file_exists "$service"
assert_file_exists "$control"
assert_file_exists "$spec"
assert_file_exists "$package_qualification"
assert_file_exists "$tmachine_gateway_service"
assert_file_exists "$tmachine_docker_config"
assert_file_exists "$tmachine_podman_config"
assert_file_exists "$development_gateway"
# Debian control files are RFC822-style metadata. Older dpkg-deb releases
# reject comment lines as malformed fields, so keep SPDX metadata in the
@@ -60,6 +70,8 @@ fi
assert_contains \
"$service" \
'Environment=OPENSHELL_LOCAL_TLS_DIR=%h/.local/state/openshell/tls'
assert_contains "$service" 'EnvironmentFile=-%E/openshell/gateway.env'
assert_contains "$service" 'ExecStart=/usr/bin/openshell-gateway'
assert_contains \
"$service" \
'ExecStartPre=/usr/bin/openshell-gateway generate-certs --output-dir ${OPENSHELL_LOCAL_TLS_DIR} --server-san host.openshell.internal'
@@ -68,6 +80,8 @@ assert_not_contains "$service" '%S/openshell/tls'
assert_contains \
"$spec" \
'Environment=OPENSHELL_LOCAL_TLS_DIR=%%h/.local/state/openshell/tls'
assert_contains "$spec" 'EnvironmentFile=-%%E/openshell/gateway.env'
assert_contains "$spec" 'ExecStart=/usr/bin/openshell-gateway'
assert_contains \
"$spec" \
'ExecStartPre=/usr/bin/openshell-gateway generate-certs --output-dir ${OPENSHELL_LOCAL_TLS_DIR} --server-san host.openshell.internal'
@@ -77,6 +91,28 @@ assert_contains "$spec" '%files prover'
assert_contains "$spec" '%{_bindir}/%{name}-prover'
assert_not_contains "$spec" '%%S/openshell/tls'
# Installed-package qualification must select the candidate trusted runtime
# images through the same environment file consumed by preflight and startup,
# without generating operator-owned gateway TOML.
assert_contains "$package_qualification" 'dest: "{{ openshell_gateway_home }}/.config/openshell/gateway.env"'
assert_contains "$package_qualification" 'OPENSHELL_COMPUTE_DRIVER={{ openshell_gateway_driver }}'
assert_contains "$package_qualification" 'OPENSHELL_SANDBOX_RUNTIME_IMAGE=docker.io/openshell/sandbox:tmachine'
assert_contains "$package_qualification" 'OPENSHELL_SUPERVISOR_IMAGE=docker.io/openshell/supervisor:tmachine'
assert_not_contains "$package_qualification" 'OPENSHELL_GATEWAY_CONFIG='
assert_not_contains "$package_qualification" '/var/lib/openshell-qualification/gateway.toml'
# Direct tmachine gateway services select trusted runtime images through the
# process environment. Driver TOML remains available for settings that do not
# have a gateway startup environment variable.
assert_contains "$tmachine_gateway_service" 'Environment=OPENSHELL_SANDBOX_RUNTIME_IMAGE=docker.io/openshell/sandbox:tmachine'
assert_contains "$tmachine_gateway_service" 'Environment=OPENSHELL_SUPERVISOR_IMAGE=docker.io/openshell/supervisor:tmachine'
assert_not_contains "$tmachine_docker_config" 'sandbox_runtime_image ='
assert_not_contains "$tmachine_docker_config" 'supervisor_image ='
assert_not_contains "$tmachine_podman_config" 'sandbox_runtime_image ='
assert_not_contains "$tmachine_podman_config" 'supervisor_image ='
assert_contains "$development_gateway" 'Environment=OPENSHELL_SUPERVISOR_IMAGE={{ openshell_supervisor_image }}'
assert_not_contains "$development_gateway" 'supervisor_image = "{{ openshell_supervisor_image }}"'
# Schema-v2 package startup wiring.
snap_wrapper="${ROOT}/tasks/scripts/snap-gateway-wrapper.sh"
snapcraft="${ROOT}/snapcraft.yaml"
@@ -7,6 +7,4 @@ socket_path = "{{ openshell_runtime_socket }}"
image_pull_policy = "if_not_present"
sandbox_label = "tmachine"
grpc_endpoint = "http://127.0.0.1:17670"
sandbox_runtime_image = "docker.io/openshell/sandbox:tmachine"
supervisor_image = "docker.io/openshell/supervisor:tmachine"
{% endblock %}
@@ -5,8 +5,6 @@
[openshell.drivers.podman]
socket_path = "{{ openshell_runtime_socket }}"
image_pull_policy = "if_not_present"
sandbox_runtime_image = "docker.io/openshell/sandbox:tmachine"
supervisor_image = "docker.io/openshell/supervisor:tmachine"
network_name = "tmachine"
health_check_interval_secs = 10
stop_timeout_secs = 15
@@ -10,6 +10,8 @@ After=network-online.target
User={{ openshell_gateway_user }}
Group={{ openshell_gateway_user }}
Environment=HOME={{ openshell_gateway_home }}
Environment=OPENSHELL_SANDBOX_RUNTIME_IMAGE=docker.io/openshell/sandbox:tmachine
Environment=OPENSHELL_SUPERVISOR_IMAGE=docker.io/openshell/supervisor:tmachine
{% block service %}{% endblock %}
ExecStart=openshell-gateway --config /etc/openshell/gateway.toml
Restart=on-failure
@@ -21,26 +21,6 @@
name: openshell_gateway
tasks_from: "{{ openshell_gateway_driver }}.yaml"
# Package defaults select published images. Keep candidate image overrides
# separate from the operator-owned gateway.toml in both installers.
- name: Create qualification configuration directory
become: true
ansible.builtin.file:
path: /var/lib/openshell-qualification
state: directory
owner: root
group: root
mode: "0755"
- name: Configure candidate runtime images for qualification
become: true
ansible.builtin.template:
src: gateway.toml.j2
dest: /var/lib/openshell-qualification/gateway.toml
owner: root
group: root
mode: "0644"
- name: Create packaged gateway environment directory
become: true
ansible.builtin.file:
@@ -50,7 +30,10 @@
group: "{{ openshell_gateway_user }}"
mode: "0700"
- name: Select qualification gateway configuration
# Exercise the package-owned service's environment-file mechanism without
# generating operator-owned gateway TOML. Preflight and startup must resolve
# the same candidate runtime images.
- name: Configure candidate runtime images for qualification
become: true
ansible.builtin.copy:
dest: "{{ openshell_gateway_home }}/.config/openshell/gateway.env"
@@ -58,7 +41,9 @@
group: "{{ openshell_gateway_user }}"
mode: "0600"
content: |
OPENSHELL_GATEWAY_CONFIG=/var/lib/openshell-qualification/gateway.toml
OPENSHELL_COMPUTE_DRIVER={{ openshell_gateway_driver }}
OPENSHELL_SANDBOX_RUNTIME_IMAGE=docker.io/openshell/sandbox:tmachine
OPENSHELL_SUPERVISOR_IMAGE=docker.io/openshell/supervisor:tmachine
- name: Start gateway user manager
ansible.builtin.include_role:
@@ -1,8 +0,0 @@
{# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. #}
{# SPDX-License-Identifier: Apache-2.0 #}
[openshell]
version = 2
[openshell.drivers.{{ openshell_gateway_driver }}]
sandbox_runtime_image = "docker.io/openshell/sandbox:tmachine"
supervisor_image = "docker.io/openshell/supervisor:tmachine"