mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-02 07:34:45 +08:00
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:
@@ -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"
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user