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
2 changes: 1 addition & 1 deletion .github/workflows/branch-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ jobs:
stats_bin="${SCCACHE_PATH:-sccache}"
"$stats_bin" --show-stats
status=$?
if [[ $status -ne 0 ]]; then
if [ "$status" -ne 0 ]; then
echo "::warning::sccache stats unavailable (exit $status)"
fi
exit 0
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ jobs:
docker buildx build \
--file deploy/docker/Dockerfile.gateway-macos \
--build-arg OPENSHELL_CARGO_VERSION="${{ needs.compute-versions.outputs.cargo_version }}" \
--build-arg OPENSHELL_IMAGE_TAG=dev \
--build-arg CARGO_TARGET_CACHE_SCOPE="${{ github.sha }}" \
--target binary \
--output type=local,dest=out/ \
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ jobs:
docker buildx build \
--file deploy/docker/Dockerfile.gateway-macos \
--build-arg OPENSHELL_CARGO_VERSION="${{ needs.compute-versions.outputs.cargo_version }}" \
--build-arg OPENSHELL_IMAGE_TAG="${{ needs.compute-versions.outputs.semver }}" \
--build-arg CARGO_TARGET_CACHE_SCOPE="${{ github.sha }}" \
--target binary \
--output type=local,dest=out/ \
Expand Down
7 changes: 7 additions & 0 deletions architecture/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ target architecture, stages them under `deploy/docker/.build/`, and then uses
Buildx to publish per-architecture images and multi-architecture tags.
Gateway image builds bake the corresponding supervisor image tag into the
gateway binary so Docker sandboxes do not depend on `:latest` by default.
Package formulas also pin Docker supervisor extraction to the matching release
image tag so standalone gateway binaries do not infer image tags from package
versions.
The Homebrew service keeps gateway TLS under the Homebrew state directory but
mirrors Docker sandbox client TLS into `$HOME/.local/state/openshell/homebrew/tls`
at service start, because Docker Desktop bind mounts must use paths visible to
the macOS user's shared home directory.

Local image work should use `mise` tasks rather than direct Docker commands so
the same staging and tagging assumptions are used locally and in CI.
Expand Down
158 changes: 134 additions & 24 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,60 @@ fn mtls_certs_exist_for_endpoint(name: &str, endpoint: &str) -> bool {
})
}

fn package_managed_tls_dirs() -> Vec<PathBuf> {
if let Some(path) = std::env::var_os("OPENSHELL_LOCAL_TLS_DIR") {
return vec![PathBuf::from(path)];
}

let mut dirs = Vec::new();

if cfg!(target_os = "macos") {
dirs.push(PathBuf::from("/opt/homebrew/var/openshell/tls"));
dirs.push(PathBuf::from("/usr/local/var/openshell/tls"));
}

let state_dir = std::env::var_os("XDG_STATE_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".local/state")));
if let Some(state_dir) = state_dir {
dirs.push(state_dir.join("openshell/tls"));
}

dirs
}

fn import_local_package_mtls_bundle(name: &str) -> Result<Option<PathBuf>> {
for dir in package_managed_tls_dirs() {
let ca = dir.join("ca.crt");
let cert = dir.join("client/tls.crt");
let key = dir.join("client/tls.key");
if !(ca.is_file() && cert.is_file() && key.is_file()) {
continue;
}

let bundle = openshell_bootstrap::pki::PkiBundle {
ca_cert_pem: std::fs::read_to_string(&ca)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read {}", ca.display()))?,
ca_key_pem: String::new(),
server_cert_pem: String::new(),
server_key_pem: String::new(),
client_cert_pem: std::fs::read_to_string(&cert)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read {}", cert.display()))?,
client_key_pem: std::fs::read_to_string(&key)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read {}", key.display()))?,
};
openshell_bootstrap::mtls::store_pki_bundle(name, &bundle)
.wrap_err_with(|| format!("failed to store mTLS bundle for gateway '{name}'"))?;

return Ok(Some(dir));
}

Ok(None)
}

fn plaintext_gateway_is_remote(endpoint: &str, remote: Option<&str>, local: bool) -> bool {
if local {
return false;
Expand Down Expand Up @@ -924,16 +978,13 @@ pub async fn gateway_add(

// Verify the gateway is reachable.
let tls = TlsOptions::default();
match http_health_check(&endpoint, &tls).await {
Ok(Some(status)) if status.is_success() => {}
_ => {
eprintln!(
"{} Gateway is not reachable at {endpoint}",
"⚠".yellow().bold(),
);
if !has_mtls_certs {
eprintln!(" Verify the gateway is running and the endpoint is correct.");
}
if !gateway_reachable(&endpoint, &tls).await {
eprintln!(
"{} Gateway is not reachable at {endpoint}",
"⚠".yellow().bold(),
);
if !has_mtls_certs {
eprintln!(" Verify the gateway is running and the endpoint is correct.");
}
}

Expand All @@ -951,7 +1002,13 @@ pub async fn gateway_add(

if remote.is_some() || local {
// mTLS gateway (remote or local).
let certs_on_disk = mtls_certs_exist_for_endpoint(name, &endpoint);
let imported_mtls_dir = if local {
import_local_package_mtls_bundle(name)?
} else {
None
};
let certs_on_disk =
imported_mtls_dir.is_some() || mtls_certs_exist_for_endpoint(name, &endpoint);
if !certs_on_disk {
return Err(miette::miette!(
"mTLS certificates for gateway '{name}' were not found.\n\
Expand Down Expand Up @@ -984,14 +1041,11 @@ pub async fn gateway_add(

// Verify the gateway is reachable over mTLS.
let tls = TlsOptions::default().with_gateway_name(name);
match http_health_check(&endpoint, &tls).await {
Ok(Some(status)) if status.is_success() => {}
_ => {
eprintln!(
"{} Gateway is not reachable at {endpoint}. Verify the gateway is running.",
"⚠".yellow().bold(),
);
}
if !gateway_reachable(&endpoint, &tls).await {
eprintln!(
"{} Gateway is not reachable at {endpoint}. Verify the gateway is running.",
"⚠".yellow().bold(),
);
}

eprintln!(
Expand Down Expand Up @@ -1252,6 +1306,16 @@ async fn http_health_check(server: &str, tls: &TlsOptions) -> Result<Option<Stat
Ok(Some(resp.status()))
}

async fn gateway_reachable(server: &str, tls: &TlsOptions) -> bool {
if let Ok(mut client) = grpc_client(server, tls).await
&& client.health(HealthRequest {}).await.is_ok()
{
return true;
}

matches!(http_health_check(server, tls).await, Ok(Some(status)) if status.is_success())
}

fn remove_gateway_registration(name: &str) {
if let Err(err) = openshell_bootstrap::edge_token::remove_edge_token(name) {
tracing::debug!("failed to remove edge token: {err}");
Expand Down Expand Up @@ -5391,18 +5455,18 @@ mod tests {
TlsOptions, dockerfile_sources_supported_for_gateway, format_gateway_select_header,
format_gateway_select_items, format_provider_attachment_table, gateway_add,
gateway_auth_label, gateway_env_override_warning, gateway_select_with, gateway_type_label,
git_sync_files, http_health_check, image_requests_gpu, inferred_provider_type,
parse_cli_setting_value, parse_credential_pairs, plaintext_gateway_is_remote,
provisioning_timeout_message, ready_false_condition_message, resolve_from,
sandbox_should_persist,
git_sync_files, http_health_check, image_requests_gpu, import_local_package_mtls_bundle,
inferred_provider_type, package_managed_tls_dirs, parse_cli_setting_value,
parse_credential_pairs, plaintext_gateway_is_remote, provisioning_timeout_message,
ready_false_condition_message, resolve_from, sandbox_should_persist,
};
use crate::TEST_ENV_LOCK;
use hyper::StatusCode;
use openshell_bootstrap::{load_active_gateway, load_gateway_metadata, store_gateway_metadata};
use std::fs;
use std::io::{Read, Write};
use std::net::TcpListener;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::thread;

Expand Down Expand Up @@ -6023,6 +6087,52 @@ mod tests {
assert_eq!(gateway_auth_label(&gateway), "mtls");
}

#[test]
fn package_managed_tls_dirs_respects_override() {
let _guard = TEST_ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _tls_dir = EnvVarGuard::set("OPENSHELL_LOCAL_TLS_DIR", "/tmp/openshell-test-tls");

assert_eq!(
package_managed_tls_dirs(),
vec![PathBuf::from("/tmp/openshell-test-tls")],
);
}

#[test]
fn import_local_package_mtls_bundle_copies_client_materials() {
let tmpdir = tempfile::tempdir().expect("create tmpdir");
let package_tls = tmpdir.path().join("package-tls");
fs::create_dir_all(package_tls.join("client")).expect("create package tls dir");
fs::write(package_tls.join("ca.crt"), "ca").expect("write ca");
fs::write(package_tls.join("client/tls.crt"), "client cert").expect("write cert");
fs::write(package_tls.join("client/tls.key"), "client key").expect("write key");

with_tmp_xdg(tmpdir.path(), || {
let _tls_dir = EnvVarGuard::set(
"OPENSHELL_LOCAL_TLS_DIR",
package_tls.to_str().expect("temp path should be utf-8"),
);

let imported =
import_local_package_mtls_bundle("openshell").expect("import local bundle");

assert_eq!(imported.as_deref(), Some(package_tls.as_path()));

let mtls = tmpdir.path().join("openshell/gateways/openshell/mtls");
assert_eq!(fs::read_to_string(mtls.join("ca.crt")).unwrap(), "ca");
assert_eq!(
fs::read_to_string(mtls.join("tls.crt")).unwrap(),
"client cert",
);
assert_eq!(
fs::read_to_string(mtls.join("tls.key")).unwrap(),
"client key",
);
});
}

#[test]
fn plaintext_gateway_locality_infers_loopback_endpoints_as_local() {
assert!(!plaintext_gateway_is_remote(
Expand Down
10 changes: 6 additions & 4 deletions crates/openshell-driver-docker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ pub fn default_docker_supervisor_image() -> String {
/// fallback covers image build wrappers that already tag the gateway and
/// supervisor together. Standalone release binaries also patch the Cargo
/// package version, so use it when it has been set to a real release value.
fn default_docker_supervisor_image_tag() -> &'static str {
fn default_docker_supervisor_image_tag() -> String {
resolve_default_docker_supervisor_image_tag(
option_env!("OPENSHELL_IMAGE_TAG"),
option_env!("IMAGE_TAG"),
Expand All @@ -101,8 +101,8 @@ fn resolve_default_docker_supervisor_image_tag(
openshell_image_tag: Option<&'static str>,
image_tag: Option<&'static str>,
cargo_pkg_version: &'static str,
) -> &'static str {
openshell_image_tag
) -> String {
let tag = openshell_image_tag
.filter(|tag| !tag.is_empty())
.or_else(|| image_tag.filter(|tag| !tag.is_empty()))
.unwrap_or_else(|| {
Expand All @@ -111,7 +111,9 @@ fn resolve_default_docker_supervisor_image_tag(
} else {
cargo_pkg_version
}
})
});

tag.replace('+', "-")
}

/// Queried by the Docker driver to decide when a sandbox's supervisor
Expand Down
16 changes: 16 additions & 0 deletions crates/openshell-driver-docker/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,22 @@ fn docker_supervisor_image_tag_prefers_explicit_build_tags() {
);
}

#[test]
fn docker_supervisor_image_tag_sanitizes_build_metadata_for_docker() {
assert_eq!(
resolve_default_docker_supervisor_image_tag(None, None, "0.0.37-dev.156+g1d3b741ee"),
"0.0.37-dev.156-g1d3b741ee",
);
assert_eq!(
resolve_default_docker_supervisor_image_tag(
Some("0.0.37-dev.156+g1d3b741ee"),
None,
"0.0.0",
),
"0.0.37-dev.156-g1d3b741ee",
);
}

#[test]
fn supervisor_cache_path_namespaces_by_digest_under_openshell_data_dir() {
let base = PathBuf::from("/var/cache/share");
Expand Down
Loading
Loading