pub use super::*;
pub use azure_core::{test::TestMode, Result};
pub use serde_json::json;
pub use std::{env, io, path::Path, process::Stdio, time::Duration};
use std::{
io::{Seek, Write},
path::PathBuf,
};
pub use tokio::{
fs::File,
io::{AsyncBufReadExt, BufReader},
process::{ChildStdout, Command},
};
pub use tracing::Level;
use tracing::Span;
pub const KESTREL_CERT_PATH_ENV: &str = "ASPNETCORE_Kestrel__Certificates__Default__Path";
pub const KESTREL_CERT_PASSWORD_ENV: &str = "ASPNETCORE_Kestrel__Certificates__Default__Password";
pub const KESTREL_CERT_PASSWORD: &str = "password";
pub const MIN_VERSION: Version = Version {
major: 20241213,
minor: 1,
metadata: None,
};
const PROXY_MANUAL_START: &str = "PROXY_MANUAL_START";
const SYSTEM_TEAMPROJECTID: &str = "SYSTEM_TEAMPROJECTID";
#[tracing::instrument(level = "debug", fields(crate_dir = ?crate_dir.as_ref(), ?options), err)]
pub async fn start(
test_mode: Option<TestMode>,
crate_dir: impl AsRef<Path>,
options: Option<ProxyOptions>,
) -> Result<Proxy> {
if env::var(PROXY_MANUAL_START).is_ok_and(|v| v.eq_ignore_ascii_case("true")) {
tracing::warn!(
"environment variable {PROXY_MANUAL_START} is 'true'; not starting test-proxy"
);
let proxy = Proxy::existing()?;
proxy.initialize().await?;
return Ok(proxy);
}
let git_dir = crate::find_ancestor_file(crate_dir.as_ref(), ".git")?;
let git_dir = git_dir.parent().ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "parent git repository not found")
})?;
let executable_file_path = ensure_test_proxy(git_dir).await?;
ensure_assets_file(test_mode, crate_dir).await?;
let mut args: Vec<String> = Vec::new();
args.extend_from_slice(&[
"start".into(),
"--storage-location".into(),
git_dir
.to_str()
.ok_or_else(|| ErrorKind::Other.into_error())?
.into(),
"--universal".into(),
]);
options.unwrap_or_default().copy_to(&mut args);
tracing::debug!(
"starting test-proxy with storage location {git_dir}",
git_dir = git_dir.display()
);
let mut proxy = Proxy::default();
let max_seconds = Duration::from_secs(env::var(SYSTEM_TEAMPROJECTID).map_or(5, |_| 20));
tokio::select! {
result = proxy.start(git_dir, &executable_file_path, args.into_iter()) => {
result?;
proxy.endpoint()
}
_ = tokio::time::sleep(max_seconds) => {
proxy.stop().await?;
return Err(azure_core::Error::with_message(ErrorKind::Other, "timed out waiting for test-proxy to start"));
},
};
proxy.initialize().await?;
Ok(proxy)
}
async fn ensure_assets_file(
test_mode: Option<TestMode>,
crate_dir: impl AsRef<Path>,
) -> Result<()> {
if test_mode == Some(TestMode::Record)
&& matches!(crate::find_ancestor_file(crate_dir.as_ref(), "assets.json"), Err(err) if err.kind() == &ErrorKind::Io)
{
let assets_file = crate_dir.as_ref().join("assets.json");
tracing::debug!("creating {path}", path = assets_file.display());
let assets_dir = assets_file
.parent()
.and_then(Path::file_name)
.map(|dir| dir.to_ascii_lowercase())
.ok_or_else(|| {
azure_core::Error::with_message(
ErrorKind::Io,
"failed to get assets.json parent directory name",
)
})?;
let assets_dir = assets_dir.to_string_lossy();
let assets_content = json!({
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "rust",
"TagPrefix": format!("rust/{assets_dir}"),
"Tag": "",
});
let file = File::create_new(assets_file).await?;
return serde_json::to_writer_pretty(file.into_std().await, &assets_content)
.map_err(azure_core::Error::from);
}
Ok(())
}
pub fn trace_line(level: Level, line: &str) {
if !line.starts_with('[') {
let line = line.trim();
if line.is_empty() {
return;
}
match level {
Level::ERROR => tracing::error!(target: "test-proxy", "{line}"),
_ => tracing::trace!(target: "test-proxy", "{line}"),
}
}
}
async fn ensure_test_proxy(git_dir: &Path) -> Result<PathBuf> {
let output_dir = git_dir.join(".proxy");
let mut executable_file_path = output_dir.join("Azure.Sdk.Tools.TestProxy");
if cfg!(windows) {
let path = executable_file_path.as_mut_os_string();
path.push(".exe");
}
let required_version = required_proxy_version(git_dir).await?;
if let Ok(output) = Command::new(&executable_file_path)
.arg("--version")
.output()
.await
{
if let Ok(version) = String::from_utf8(output.stdout) {
let installed_version = String::from("1.0.0-dev.") + version.trim();
tracing::trace!("requires test-proxy {required_version}; found {installed_version}");
if installed_version == required_version {
return Ok(executable_file_path);
}
}
}
download_test_proxy(&required_version, &executable_file_path, &output_dir).await?;
Ok(executable_file_path)
}
async fn required_proxy_version(git_dir: &Path) -> Result<String> {
let mut path = git_dir.join("eng/target_proxy_version.txt");
if !path.exists() {
path = git_dir.join("eng/common/testproxy/target_version.txt");
}
Ok(tokio::fs::read_to_string(path).await?.trim().to_string())
}
fn download_file_name() -> Option<&'static str> {
if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
Some("test-proxy-standalone-linux-x64.tar.gz")
} else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
Some("test-proxy-standalone-linux-arm64.tar.gz")
} else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
Some("test-proxy-standalone-osx-x64.zip")
} else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
Some("test-proxy-standalone-osx-arm64.zip")
} else if cfg!(all(target_os = "windows", not(target_arch = "x86"))) {
Some("test-proxy-standalone-win-x64.zip")
} else {
None
}
}
#[tracing::instrument(level = "debug", fields(url), err)]
async fn download_test_proxy(
version: &str,
executable_file_path: &Path,
output_dir: &Path,
) -> Result<()> {
let download_file_name = download_file_name().ok_or_else(|| {
azure_core::Error::with_message(
ErrorKind::Other,
"test-proxy not supported on current platform",
)
})?;
let url: azure_core::http::Url = format!("https://github.com/Azure/azure-sdk-tools/releases/download/Azure.Sdk.Tools.TestProxy_{}/{}", version, download_file_name).parse()?;
Span::current().record("url", url.as_str());
let map_reqwest_err = |err: reqwest::Error| {
let url = err.url().cloned().unwrap();
azure_core::Error::with_error(ErrorKind::Other, err, format!("failed to download {url}"))
};
let archive = reqwest::get(url)
.await
.map_err(map_reqwest_err)?
.bytes()
.await
.map_err(map_reqwest_err)?;
let archive_file_path = output_dir.join(download_file_name);
tokio::fs::create_dir_all(&output_dir).await?;
let mut archive_file = std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.read(true)
.write(true)
.open(&archive_file_path)?;
archive_file.write_all(&archive)?;
archive_file.flush()?;
archive_file.seek(io::SeekFrom::Start(0))?;
extract_test_proxy(&archive_file, &archive_file_path, output_dir)?;
#[cfg(unix)]
{
use std::{fs::Permissions, os::unix::fs::PermissionsExt as _};
tokio::fs::set_permissions(executable_file_path, Permissions::from_mode(0o755)).await?;
}
Ok(())
}
fn extract_test_proxy(
archive_file: &std::fs::File,
archive_file_path: &Path,
output_dir: &Path,
) -> Result<()> {
tracing::trace!(
"extracting {} to {}",
archive_file_path.display(),
output_dir.display()
);
match archive_file_path.extension() {
Some(ext) if ext == "gz" => untar(archive_file, output_dir),
Some(ext) if ext == "zip" => unzip(archive_file, output_dir),
_ => {
return Err(azure_core::Error::with_message(
ErrorKind::Io,
format!("unsupported archive {}", archive_file_path.display()),
))
}
}
.map_err(|err| {
let message = format!(
"failed to extract {}: {:?}",
archive_file_path.display(),
err
);
azure_core::Error::with_error(ErrorKind::Io, err, message)
})
}
fn untar(
archive_file: &std::fs::File,
output_dir: &Path,
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
let reader = std::io::BufReader::new(archive_file);
let decoder = flate2::bufread::GzDecoder::new(reader);
let mut archive = tar::Archive::new(decoder);
archive.unpack(output_dir)?;
Ok(())
}
fn unzip(
archive_file: &std::fs::File,
output_dir: &Path,
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut zip = zip::ZipArchive::new(archive_file)?;
zip.extract(output_dir)?;
Ok(())
}