feat(providers): serve sandbox config files on demand (#3832)

* feat(providers): serve sandbox config files on demand

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(providers): defer managed file documentation

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(providers): mark managed file api experimental

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* style(go): format provider profile fields

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(providers): preserve legacy environment with managed files

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

---------

Signed-off-by: Drew Newberry <anewberry@nvidia.com>
This commit is contained in:
Drew Newberry
2026-09-30 01:41:32 +00:00
committed by GitHub
parent 5c0c9e446e
commit 252882f37f
26 changed files with 2264 additions and 953 deletions
+5
View File
@@ -43,6 +43,11 @@ name = "policy_activation"
path = "tests/policy_activation.rs"
required-features = ["e2e-docker"]
[[test]]
name = "provider_files"
path = "tests/provider_files.rs"
required-features = ["e2e-docker"]
[[test]]
name = "oidc_pkce"
path = "tests/oidc_pkce.rs"
+159
View File
@@ -0,0 +1,159 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
#![cfg(feature = "e2e-docker")]
//! A provider profile serves read-only config on each workload open.
use std::io::Write as _;
use std::process::Stdio;
use std::time::Duration;
use openshell_e2e::harness::binary::openshell_bin;
use openshell_e2e::harness::cli::run_cli;
use openshell_e2e::harness::sandbox::SandboxGuard;
use tokio::time::sleep;
struct ProviderGuard {
profile: String,
provider: String,
}
impl Drop for ProviderGuard {
fn drop(&mut self) {
let binary = openshell_bin();
for _ in 0..20 {
let deleted = std::process::Command::new(&binary)
.args(["provider", "delete", &self.provider])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
if deleted.is_ok_and(|status| status.success()) {
break;
}
std::thread::sleep(Duration::from_millis(250));
}
let _ = std::process::Command::new(&binary)
.args(["profile", "delete", &self.profile])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
}
async fn cli_ok(args: &[&str]) -> Result<(), String> {
let (output, code) = run_cli(args).await;
if code == 0 {
Ok(())
} else {
Err(format!(
"{} failed (exit {code}):\n{output}",
args.join(" ")
))
}
}
#[tokio::test]
async fn provider_file_open_update_and_detach() -> Result<(), String> {
let suffix = format!("{}-{:08x}", std::process::id(), rand::random::<u32>());
let profile_id = format!("pf-{suffix}");
let provider = format!("pf-{suffix}");
let profile_yaml = include_str!("../../../examples/provider-managed-files/acme-config.yaml")
.replace("id: acme-config", &format!("id: {profile_id}"));
let mut profile_file = tempfile::Builder::new()
.suffix(".yaml")
.tempfile()
.map_err(|error| error.to_string())?;
profile_file
.write_all(profile_yaml.as_bytes())
.map_err(|error| error.to_string())?;
cli_ok(&[
"profile",
"import",
"--file",
profile_file
.path()
.to_str()
.ok_or("profile path is not UTF-8")?,
])
.await?;
let _provider_guard = ProviderGuard {
profile: profile_id.clone(),
provider: provider.clone(),
};
cli_ok(&[
"provider",
"create",
"--name",
&provider,
"--type",
&profile_id,
"--config",
"endpoint=https://api.acme.example",
"--config",
"project=production",
])
.await?;
let mut sandbox = SandboxGuard::create(&["--provider", &provider, "--no-tty"]).await?;
let path = format!("/run/openshell/providers/{provider}/client.toml");
let environment_path = sandbox.exec(&["printenv", "ACME_CONFIG_FILE"]).await?;
if !environment_path.contains(&path) {
return Err(format!(
"provider path environment variable missing: {environment_path}"
));
}
let before = sandbox.exec(&["cat", &path]).await?;
if !before.contains("project = \"production\"") {
return Err(format!("initial provider file content missing:\n{before}"));
}
cli_ok(&[
"provider",
"update",
&provider,
"--config",
"project=staging",
"--wait",
"--timeout",
"90",
])
.await?;
let after = sandbox.exec(&["cat", &path]).await?;
if !after.contains("project = \"staging\"") {
return Err(format!("updated provider file content missing:\n{after}"));
}
cli_ok(&[
"sandbox",
"provider",
"detach",
&sandbox.name,
&provider,
"--wait",
"--timeout",
"90",
])
.await?;
let (detached, code) = run_cli(&[
"sandbox",
"exec",
"--name",
&sandbox.name,
"--no-tty",
"--",
"cat",
&path,
])
.await;
if code == 0 || !detached.contains("No such file or directory") {
return Err(format!(
"detached provider path remained readable: {detached}"
));
}
sandbox.cleanup().await;
// Let the asynchronous sandbox deletion release the provider before
// ProviderGuard removes the provider record and its profile.
sleep(Duration::from_millis(250)).await;
Ok(())
}