936 lines
34 KiB
Rust
936 lines
34 KiB
Rust
// Worker-backed Runtime REST process wrapper.
|
|
//
|
|
// This binary starts a Runtime command API with a real worker execution backend.
|
|
// A REST Runtime process that cannot spawn Workers is not a valid Runtime for the
|
|
// Workspace Browser.
|
|
|
|
use std::collections::VecDeque;
|
|
use std::env;
|
|
use std::error::Error;
|
|
use std::fmt;
|
|
use std::net::SocketAddr;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::ExitCode;
|
|
use std::sync::Arc;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use worker_runtime::auth::{
|
|
RuntimeHttpAuthConfig, RuntimeIdentityMaterial, TrustedServerKey, decode_public_key,
|
|
};
|
|
use worker_runtime::error::RuntimeError;
|
|
use worker_runtime::fs_store::FsRuntimeStoreOptions;
|
|
use worker_runtime::http_server::{
|
|
RuntimeHttpServerConfig, RuntimeHttpServerError, RuntimeHttpStoreSelection,
|
|
};
|
|
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
|
|
use worker_runtime::working_directory::LocalGitWorktreeMaterializer;
|
|
use worker_runtime::{Runtime, RuntimeOptions};
|
|
|
|
fn main() -> ExitCode {
|
|
match run() {
|
|
Ok(()) => ExitCode::SUCCESS,
|
|
Err(error) => {
|
|
eprintln!("worker-runtime-rest-server: {error}");
|
|
if let ProcessError::Usage(_) = error {
|
|
eprintln!();
|
|
eprintln!("{}", usage());
|
|
ExitCode::from(2)
|
|
} else {
|
|
ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn run() -> Result<(), ProcessError> {
|
|
let args = env::args().skip(1).collect::<Vec<_>>();
|
|
if matches!(
|
|
args.first().map(String::as_str),
|
|
Some("identity" | "trust-server")
|
|
) {
|
|
return run_auth_command(args);
|
|
}
|
|
let Some(mut config) = parse_args(args)? else {
|
|
println!("{}", usage());
|
|
return Ok(());
|
|
};
|
|
config.http.auth = load_runtime_http_auth(&config)?;
|
|
|
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()?;
|
|
runtime.block_on(async move {
|
|
let listener = tokio::net::TcpListener::bind(config.http.bind_addr).await?;
|
|
let local_addr = listener.local_addr()?;
|
|
let worker_runtime = build_runtime(&config)?;
|
|
eprintln!(
|
|
"worker-runtime REST server listening on {local_addr}; intended client is a trusted backend/proxy, not a browser"
|
|
);
|
|
worker_runtime::http_server::serve_runtime_http_with_auth(
|
|
worker_runtime,
|
|
listener,
|
|
config.http.local_token,
|
|
config.http.auth,
|
|
)
|
|
.await
|
|
.map_err(ProcessError::from)
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
fn build_runtime(config: &ProcessConfig) -> Result<Runtime, ProcessError> {
|
|
let fs_paths = config.resolved_fs_paths();
|
|
let mut factory = ProfileRuntimeWorkerFactory::new(fs_paths.worker_dir.join("worker-root"))
|
|
.with_store_dir(fs_paths.worker_dir.join("sessions"))
|
|
.with_worker_metadata_dir(fs_paths.worker_dir.join("metadata"))
|
|
.with_runtime_base_dir(fs_paths.worker_dir.join("runtime"));
|
|
if let Some(endpoint) = config.backend_resource_endpoint.clone() {
|
|
factory = factory.with_resource_client(Arc::new(
|
|
worker_runtime::resource::HttpBackendResourceClient::new(
|
|
endpoint,
|
|
config.backend_resource_token.clone(),
|
|
),
|
|
));
|
|
}
|
|
if let Some(profile) = config.profile.clone() {
|
|
factory = factory.with_profile(profile);
|
|
}
|
|
let backend = Arc::new(
|
|
WorkerRuntimeExecutionBackend::new(factory)
|
|
.map_err(ProcessError::WorkerAdapter)?
|
|
.with_working_directory_materializer(LocalGitWorktreeMaterializer::new(
|
|
fs_paths.workdir_target.clone(),
|
|
)),
|
|
);
|
|
|
|
match &config.http.store {
|
|
RuntimeHttpStoreSelection::Memory => {
|
|
Runtime::with_execution_backend(runtime_options_from_http(&config.http), backend)
|
|
.map_err(ProcessError::Runtime)
|
|
}
|
|
RuntimeHttpStoreSelection::Fs { root } => {
|
|
let mut options = FsRuntimeStoreOptions::new(root.clone());
|
|
options.display_name = config.http.display_name.clone();
|
|
options.limits = config.http.limits.clone();
|
|
Runtime::with_fs_store_and_execution_backend(options, backend)
|
|
.map_err(ProcessError::Runtime)
|
|
}
|
|
_ => Err(ProcessError::usage(
|
|
"unsupported Runtime catalog store selection".to_string(),
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn runtime_options_from_http(config: &RuntimeHttpServerConfig) -> RuntimeOptions {
|
|
RuntimeOptions {
|
|
display_name: config.display_name.clone(),
|
|
limits: config.limits.clone(),
|
|
}
|
|
}
|
|
|
|
fn default_fs_root() -> PathBuf {
|
|
manifest::paths::data_dir().unwrap_or_else(|| env::temp_dir().join("yoi"))
|
|
}
|
|
|
|
fn parse_args<I, S>(args: I) -> Result<Option<ProcessConfig>, ProcessError>
|
|
where
|
|
I: IntoIterator<Item = S>,
|
|
S: Into<String>,
|
|
{
|
|
let mut config = ProcessConfig::default()?;
|
|
let mut args = args.into_iter().map(Into::into).collect::<VecDeque<_>>();
|
|
|
|
while let Some(arg) = args.pop_front() {
|
|
let (flag, inline_value) = split_flag_value(arg)?;
|
|
match flag.as_str() {
|
|
"--help" | "-h" => return Ok(None),
|
|
"--bind" => {
|
|
let value = take_value(&flag, inline_value, &mut args)?;
|
|
config.http.bind_addr = value.parse::<SocketAddr>().map_err(|error| {
|
|
ProcessError::usage(format!("invalid --bind socket address `{value}`: {error}"))
|
|
})?;
|
|
}
|
|
"--display-name" => {
|
|
config.http.display_name = Some(take_value(&flag, inline_value, &mut args)?);
|
|
}
|
|
"--fs-worker-dir" => {
|
|
config.fs_worker_dir =
|
|
Some(PathBuf::from(take_value(&flag, inline_value, &mut args)?));
|
|
}
|
|
"--fs-runtime-dir" => {
|
|
config.fs_runtime_dir =
|
|
Some(PathBuf::from(take_value(&flag, inline_value, &mut args)?));
|
|
}
|
|
"--workdir-target" => {
|
|
config.workdir_target =
|
|
Some(PathBuf::from(take_value(&flag, inline_value, &mut args)?));
|
|
}
|
|
"--backend-resource-endpoint" => {
|
|
config.backend_resource_endpoint =
|
|
Some(take_value(&flag, inline_value, &mut args)?);
|
|
}
|
|
"--backend-resource-token" => {
|
|
config.backend_resource_token = Some(take_value(&flag, inline_value, &mut args)?);
|
|
}
|
|
"--profile" => {
|
|
config.profile = Some(take_value(&flag, inline_value, &mut args)?);
|
|
}
|
|
"--store" => {
|
|
let value = take_value(&flag, inline_value, &mut args)?;
|
|
if !matches!(value.as_str(), "fs" | "fs-store") {
|
|
return Err(ProcessError::usage(format!(
|
|
"unsupported --store `{value}`; only `fs` is accepted (use --no-store for ephemeral mode)"
|
|
)));
|
|
}
|
|
}
|
|
"--no-store" => {
|
|
ensure_no_inline_value(&flag, inline_value.as_deref())?;
|
|
config.no_store = true;
|
|
}
|
|
"--fs-root" => {
|
|
let value = take_value(&flag, inline_value, &mut args)?;
|
|
config.fs_root = Some(PathBuf::from(value));
|
|
}
|
|
"--local-token" => {
|
|
let value = take_value(&flag, inline_value, &mut args)?;
|
|
if value.is_empty() {
|
|
return Err(ProcessError::usage(
|
|
"--local-token must not be empty when provided".to_string(),
|
|
));
|
|
}
|
|
config.http.local_token = Some(value);
|
|
}
|
|
"--local-token-env" => {
|
|
let name = take_value(&flag, inline_value, &mut args)?;
|
|
let value = env::var(&name).map_err(|error| {
|
|
ProcessError::usage(format!(
|
|
"failed to read --local-token-env `{name}`: {error}"
|
|
))
|
|
})?;
|
|
if value.is_empty() {
|
|
return Err(ProcessError::usage(format!(
|
|
"--local-token-env `{name}` resolved to an empty value"
|
|
)));
|
|
}
|
|
config.http.local_token = Some(value);
|
|
}
|
|
"--max-event-batch-items" => {
|
|
config.http.limits.max_event_batch_items =
|
|
parse_usize_flag(&flag, take_value(&flag, inline_value, &mut args)?)?;
|
|
}
|
|
_ => {
|
|
return Err(ProcessError::usage(format!("unknown argument `{flag}`")));
|
|
}
|
|
}
|
|
}
|
|
|
|
apply_store_selection(&mut config);
|
|
Ok(Some(config))
|
|
}
|
|
|
|
fn split_flag_value(arg: String) -> Result<(String, Option<String>), ProcessError> {
|
|
if !arg.starts_with('-') {
|
|
return Err(ProcessError::usage(format!(
|
|
"unexpected positional argument `{arg}`"
|
|
)));
|
|
}
|
|
if let Some((flag, value)) = arg.split_once('=') {
|
|
Ok((flag.to_string(), Some(value.to_string())))
|
|
} else {
|
|
Ok((arg, None))
|
|
}
|
|
}
|
|
|
|
fn take_value(
|
|
flag: &str,
|
|
inline_value: Option<String>,
|
|
args: &mut VecDeque<String>,
|
|
) -> Result<String, ProcessError> {
|
|
if let Some(value) = inline_value {
|
|
return Ok(value);
|
|
}
|
|
args.pop_front()
|
|
.ok_or_else(|| ProcessError::usage(format!("{flag} requires a value")))
|
|
}
|
|
|
|
fn ensure_no_inline_value(flag: &str, inline_value: Option<&str>) -> Result<(), ProcessError> {
|
|
if inline_value.is_some() {
|
|
return Err(ProcessError::usage(format!(
|
|
"{flag} does not accept a value"
|
|
)));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn parse_usize_flag(flag: &str, value: String) -> Result<usize, ProcessError> {
|
|
value
|
|
.parse::<usize>()
|
|
.map_err(|error| ProcessError::usage(format!("invalid {flag} value `{value}`: {error}")))
|
|
}
|
|
|
|
fn apply_store_selection(config: &mut ProcessConfig) {
|
|
if config.no_store {
|
|
config.http.store = RuntimeHttpStoreSelection::Memory;
|
|
} else {
|
|
let runtime_dir = config.resolved_fs_paths().runtime_dir;
|
|
config.http.store = RuntimeHttpStoreSelection::Fs { root: runtime_dir };
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct ProcessConfig {
|
|
http: RuntimeHttpServerConfig,
|
|
fs_root: Option<PathBuf>,
|
|
fs_worker_dir: Option<PathBuf>,
|
|
fs_runtime_dir: Option<PathBuf>,
|
|
workdir_target: Option<PathBuf>,
|
|
no_store: bool,
|
|
backend_resource_endpoint: Option<String>,
|
|
backend_resource_token: Option<String>,
|
|
profile: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct FsPathConfig {
|
|
root: PathBuf,
|
|
worker_dir: PathBuf,
|
|
runtime_dir: PathBuf,
|
|
workdir_target: PathBuf,
|
|
}
|
|
|
|
impl ProcessConfig {
|
|
fn default() -> Result<Self, ProcessError> {
|
|
Ok(Self {
|
|
http: RuntimeHttpServerConfig::default(),
|
|
fs_root: None,
|
|
fs_worker_dir: None,
|
|
fs_runtime_dir: None,
|
|
workdir_target: None,
|
|
no_store: false,
|
|
backend_resource_endpoint: None,
|
|
backend_resource_token: None,
|
|
profile: None,
|
|
})
|
|
}
|
|
|
|
fn resolved_fs_paths(&self) -> FsPathConfig {
|
|
let root = self.fs_root.clone().unwrap_or_else(default_fs_root);
|
|
FsPathConfig {
|
|
worker_dir: self
|
|
.fs_worker_dir
|
|
.clone()
|
|
.unwrap_or_else(|| root.join("worker")),
|
|
runtime_dir: self
|
|
.fs_runtime_dir
|
|
.clone()
|
|
.unwrap_or_else(|| root.join("runtime")),
|
|
workdir_target: self
|
|
.workdir_target
|
|
.clone()
|
|
.unwrap_or_else(|| root.join("workdirs")),
|
|
root,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum ProcessError {
|
|
Usage(String),
|
|
Server(RuntimeHttpServerError),
|
|
Runtime(RuntimeError),
|
|
WorkerAdapter(String),
|
|
Auth(String),
|
|
Io(std::io::Error),
|
|
}
|
|
|
|
impl ProcessError {
|
|
fn usage(message: String) -> Self {
|
|
Self::Usage(message)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for ProcessError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Usage(message) => message.fmt(f),
|
|
Self::Server(error) => error.fmt(f),
|
|
Self::Runtime(error) => error.fmt(f),
|
|
Self::WorkerAdapter(error) => error.fmt(f),
|
|
Self::Auth(error) => error.fmt(f),
|
|
Self::Io(error) => error.fmt(f),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Error for ProcessError {
|
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
|
match self {
|
|
Self::Usage(_) | Self::WorkerAdapter(_) | Self::Auth(_) => None,
|
|
Self::Server(error) => Some(error),
|
|
Self::Runtime(error) => Some(error),
|
|
Self::Io(error) => Some(error),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<RuntimeHttpServerError> for ProcessError {
|
|
fn from(error: RuntimeHttpServerError) -> Self {
|
|
Self::Server(error)
|
|
}
|
|
}
|
|
|
|
impl From<std::io::Error> for ProcessError {
|
|
fn from(error: std::io::Error) -> Self {
|
|
Self::Io(error)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
struct RuntimeAuthFile {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
identity: Option<RuntimeIdentityMaterial>,
|
|
#[serde(default)]
|
|
trusted_servers: Vec<TrustedServerKey>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
struct RuntimePublicIdentityView {
|
|
identity_id: String,
|
|
public_key: String,
|
|
}
|
|
|
|
fn runtime_public_identity_view(identity: &RuntimeIdentityMaterial) -> RuntimePublicIdentityView {
|
|
RuntimePublicIdentityView {
|
|
identity_id: identity.identity_id.clone(),
|
|
public_key: identity.public_key.clone(),
|
|
}
|
|
}
|
|
|
|
fn runtime_auth_path(config: &ProcessConfig) -> PathBuf {
|
|
config.resolved_fs_paths().runtime_dir.join("auth.toml")
|
|
}
|
|
|
|
fn read_runtime_auth_file(path: &Path) -> Result<RuntimeAuthFile, ProcessError> {
|
|
if !path.exists() {
|
|
return Ok(RuntimeAuthFile::default());
|
|
}
|
|
let contents = std::fs::read_to_string(path)?;
|
|
toml::from_str(&contents)
|
|
.map_err(|error| ProcessError::Auth(format!("failed to parse {}: {error}", path.display())))
|
|
}
|
|
|
|
fn write_runtime_auth_file(path: &Path, auth: &RuntimeAuthFile) -> Result<(), ProcessError> {
|
|
if let Some(parent) = path.parent() {
|
|
std::fs::create_dir_all(parent)?;
|
|
}
|
|
let contents = toml::to_string_pretty(auth).map_err(|error| {
|
|
ProcessError::Auth(format!("failed to serialize {}: {error}", path.display()))
|
|
})?;
|
|
write_secret_file(path, contents.as_bytes())
|
|
}
|
|
|
|
fn write_secret_file(path: &Path, contents: &[u8]) -> Result<(), ProcessError> {
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::OpenOptionsExt;
|
|
let mut options = std::fs::OpenOptions::new();
|
|
options.create(true).write(true).truncate(true).mode(0o600);
|
|
std::io::Write::write_all(&mut options.open(path)?, contents)?;
|
|
}
|
|
#[cfg(not(unix))]
|
|
{
|
|
std::fs::write(path, contents)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn load_runtime_http_auth(
|
|
config: &ProcessConfig,
|
|
) -> Result<Option<RuntimeHttpAuthConfig>, ProcessError> {
|
|
let path = runtime_auth_path(config);
|
|
let auth = read_runtime_auth_file(&path)?;
|
|
let Some(identity) = auth.identity else {
|
|
return Ok(None);
|
|
};
|
|
if auth.trusted_servers.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
Ok(Some(RuntimeHttpAuthConfig {
|
|
runtime_id: identity.identity_id,
|
|
trusted_servers: auth.trusted_servers,
|
|
}))
|
|
}
|
|
|
|
fn parse_auth_storage_flags(args: &mut VecDeque<String>) -> Result<ProcessConfig, ProcessError> {
|
|
let mut config = ProcessConfig::default()?;
|
|
while let Some(arg) = args.pop_front() {
|
|
let (flag, inline_value) = split_flag_value(arg)?;
|
|
match flag.as_str() {
|
|
"--fs-root" => {
|
|
config.fs_root = Some(PathBuf::from(take_value(&flag, inline_value, args)?));
|
|
}
|
|
"--fs-runtime-dir" => {
|
|
config.fs_runtime_dir = Some(PathBuf::from(take_value(&flag, inline_value, args)?));
|
|
}
|
|
_ => {
|
|
return Err(ProcessError::usage(format!(
|
|
"unknown auth command argument `{flag}`"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
Ok(config)
|
|
}
|
|
|
|
fn run_auth_command(args: Vec<String>) -> Result<(), ProcessError> {
|
|
let mut args = VecDeque::from(args);
|
|
let command = args.pop_front().unwrap_or_default();
|
|
match command.as_str() {
|
|
"identity" => run_identity_command(args),
|
|
"trust-server" => run_trust_server_command(args),
|
|
_ => Err(ProcessError::usage(format!(
|
|
"unknown auth command `{command}`"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn run_identity_command(mut args: VecDeque<String>) -> Result<(), ProcessError> {
|
|
let subcommand = args.pop_front().ok_or_else(|| {
|
|
ProcessError::usage("identity requires subcommand `init` or `show`".to_string())
|
|
})?;
|
|
match subcommand.as_str() {
|
|
"init" => {
|
|
let mut runtime_id = None;
|
|
let mut replace = false;
|
|
let mut rest = VecDeque::new();
|
|
while let Some(arg) = args.pop_front() {
|
|
let (flag, inline_value) = split_flag_value(arg)?;
|
|
match flag.as_str() {
|
|
"--runtime-id" => {
|
|
runtime_id = Some(take_value(&flag, inline_value, &mut args)?)
|
|
}
|
|
"--replace" => {
|
|
ensure_no_inline_value(&flag, inline_value.as_deref())?;
|
|
replace = true;
|
|
}
|
|
"--fs-root" | "--fs-runtime-dir" => {
|
|
rest.push_back(flag);
|
|
if let Some(value) = inline_value {
|
|
rest.push_back(value);
|
|
} else {
|
|
rest.push_back(args.pop_front().ok_or_else(|| {
|
|
ProcessError::usage(format!(
|
|
"{} requires a value",
|
|
rest.back().unwrap()
|
|
))
|
|
})?);
|
|
}
|
|
}
|
|
_ => {
|
|
return Err(ProcessError::usage(format!(
|
|
"unknown identity init argument `{flag}`"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
let config = parse_auth_storage_flags(&mut rest)?;
|
|
let path = runtime_auth_path(&config);
|
|
let mut auth = read_runtime_auth_file(&path)?;
|
|
if auth.identity.is_some() && !replace {
|
|
return Err(ProcessError::usage(format!(
|
|
"runtime identity already exists at {}; pass --replace to rotate it",
|
|
path.display()
|
|
)));
|
|
}
|
|
let runtime_id = runtime_id.ok_or_else(|| {
|
|
ProcessError::usage("identity init requires --runtime-id".to_string())
|
|
})?;
|
|
auth.identity = Some(
|
|
RuntimeIdentityMaterial::generate(runtime_id)
|
|
.map_err(|error| ProcessError::Auth(error.to_string()))?,
|
|
);
|
|
write_runtime_auth_file(&path, &auth)?;
|
|
let identity = auth.identity.as_ref().unwrap();
|
|
println!("runtime_id={}", identity.identity_id);
|
|
println!("public_key={}", identity.public_key);
|
|
println!("auth_file={}", path.display());
|
|
Ok(())
|
|
}
|
|
"show" => {
|
|
let mut json = false;
|
|
let mut rest = VecDeque::new();
|
|
while let Some(arg) = args.pop_front() {
|
|
let (flag, inline_value) = split_flag_value(arg)?;
|
|
match flag.as_str() {
|
|
"--json" => {
|
|
ensure_no_inline_value(&flag, inline_value.as_deref())?;
|
|
json = true;
|
|
}
|
|
"--fs-root" | "--fs-runtime-dir" => {
|
|
rest.push_back(flag);
|
|
if let Some(value) = inline_value {
|
|
rest.push_back(value);
|
|
} else {
|
|
rest.push_back(args.pop_front().ok_or_else(|| {
|
|
ProcessError::usage(format!(
|
|
"{} requires a value",
|
|
rest.back().unwrap()
|
|
))
|
|
})?);
|
|
}
|
|
}
|
|
_ => {
|
|
return Err(ProcessError::usage(format!(
|
|
"unknown identity show argument `{flag}`"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
let config = parse_auth_storage_flags(&mut rest)?;
|
|
let path = runtime_auth_path(&config);
|
|
let auth = read_runtime_auth_file(&path)?;
|
|
let Some(identity) = auth.identity else {
|
|
return Err(ProcessError::usage(format!(
|
|
"runtime identity is not initialized at {}",
|
|
path.display()
|
|
)));
|
|
};
|
|
let view = runtime_public_identity_view(&identity);
|
|
if json {
|
|
println!(
|
|
"{}",
|
|
serde_json::to_string_pretty(&view)
|
|
.map_err(|error| ProcessError::Auth(error.to_string()))?
|
|
);
|
|
} else {
|
|
println!("runtime_id={}", view.identity_id);
|
|
println!("public_key={}", view.public_key);
|
|
println!("auth_file={}", path.display());
|
|
}
|
|
Ok(())
|
|
}
|
|
_ => Err(ProcessError::usage(format!(
|
|
"unknown identity subcommand `{subcommand}`"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn run_trust_server_command(mut args: VecDeque<String>) -> Result<(), ProcessError> {
|
|
let subcommand = args.pop_front().ok_or_else(|| {
|
|
ProcessError::usage(
|
|
"trust-server requires subcommand `add`, `list`, or `revoke`".to_string(),
|
|
)
|
|
})?;
|
|
match subcommand.as_str() {
|
|
"add" => {
|
|
let mut server_id = None;
|
|
let mut public_key = None;
|
|
let mut display_name = None;
|
|
let mut replace = false;
|
|
let mut rest = VecDeque::new();
|
|
while let Some(arg) = args.pop_front() {
|
|
let (flag, inline_value) = split_flag_value(arg)?;
|
|
match flag.as_str() {
|
|
"--server-id" => server_id = Some(take_value(&flag, inline_value, &mut args)?),
|
|
"--public-key" => {
|
|
public_key = Some(take_value(&flag, inline_value, &mut args)?)
|
|
}
|
|
"--display-name" => {
|
|
display_name = Some(take_value(&flag, inline_value, &mut args)?)
|
|
}
|
|
"--replace" => {
|
|
ensure_no_inline_value(&flag, inline_value.as_deref())?;
|
|
replace = true;
|
|
}
|
|
"--fs-root" | "--fs-runtime-dir" => {
|
|
rest.push_back(flag);
|
|
if let Some(value) = inline_value {
|
|
rest.push_back(value);
|
|
} else {
|
|
rest.push_back(args.pop_front().ok_or_else(|| {
|
|
ProcessError::usage(format!(
|
|
"{} requires a value",
|
|
rest.back().unwrap()
|
|
))
|
|
})?);
|
|
}
|
|
}
|
|
_ => {
|
|
return Err(ProcessError::usage(format!(
|
|
"unknown trust-server add argument `{flag}`"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
let config = parse_auth_storage_flags(&mut rest)?;
|
|
let path = runtime_auth_path(&config);
|
|
let mut auth = read_runtime_auth_file(&path)?;
|
|
let server_id = server_id.ok_or_else(|| {
|
|
ProcessError::usage("trust-server add requires --server-id".to_string())
|
|
})?;
|
|
let public_key = public_key.ok_or_else(|| {
|
|
ProcessError::usage("trust-server add requires --public-key".to_string())
|
|
})?;
|
|
decode_public_key(&public_key)
|
|
.map_err(|error| ProcessError::usage(error.to_string()))?;
|
|
if auth
|
|
.trusted_servers
|
|
.iter()
|
|
.any(|server| server.server_id == server_id)
|
|
&& !replace
|
|
{
|
|
return Err(ProcessError::usage(format!(
|
|
"trusted server `{server_id}` already exists; pass --replace to update it"
|
|
)));
|
|
}
|
|
auth.trusted_servers
|
|
.retain(|server| server.server_id != server_id);
|
|
auth.trusted_servers.push(TrustedServerKey {
|
|
server_id: server_id.clone(),
|
|
public_key,
|
|
display_name,
|
|
});
|
|
write_runtime_auth_file(&path, &auth)?;
|
|
println!("trusted_server_id={server_id}");
|
|
println!("auth_file={}", path.display());
|
|
Ok(())
|
|
}
|
|
"list" => {
|
|
let mut json = false;
|
|
let mut rest = VecDeque::new();
|
|
while let Some(arg) = args.pop_front() {
|
|
let (flag, inline_value) = split_flag_value(arg)?;
|
|
match flag.as_str() {
|
|
"--json" => {
|
|
ensure_no_inline_value(&flag, inline_value.as_deref())?;
|
|
json = true;
|
|
}
|
|
"--fs-root" | "--fs-runtime-dir" => {
|
|
rest.push_back(flag);
|
|
if let Some(value) = inline_value {
|
|
rest.push_back(value);
|
|
} else {
|
|
rest.push_back(args.pop_front().ok_or_else(|| {
|
|
ProcessError::usage(format!(
|
|
"{} requires a value",
|
|
rest.back().unwrap()
|
|
))
|
|
})?);
|
|
}
|
|
}
|
|
_ => {
|
|
return Err(ProcessError::usage(format!(
|
|
"unknown trust-server list argument `{flag}`"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
let config = parse_auth_storage_flags(&mut rest)?;
|
|
let auth = read_runtime_auth_file(&runtime_auth_path(&config))?;
|
|
if json {
|
|
println!(
|
|
"{}",
|
|
serde_json::to_string_pretty(&auth.trusted_servers)
|
|
.map_err(|error| ProcessError::Auth(error.to_string()))?
|
|
);
|
|
} else {
|
|
for server in auth.trusted_servers {
|
|
println!(
|
|
"server_id={} public_key={} display_name={}",
|
|
server.server_id,
|
|
server.public_key,
|
|
server.display_name.unwrap_or_default()
|
|
);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
"revoke" => {
|
|
let mut server_id = None;
|
|
let mut rest = VecDeque::new();
|
|
while let Some(arg) = args.pop_front() {
|
|
let (flag, inline_value) = split_flag_value(arg)?;
|
|
match flag.as_str() {
|
|
"--server-id" => server_id = Some(take_value(&flag, inline_value, &mut args)?),
|
|
"--fs-root" | "--fs-runtime-dir" => {
|
|
rest.push_back(flag);
|
|
if let Some(value) = inline_value {
|
|
rest.push_back(value);
|
|
} else {
|
|
rest.push_back(args.pop_front().ok_or_else(|| {
|
|
ProcessError::usage(format!(
|
|
"{} requires a value",
|
|
rest.back().unwrap()
|
|
))
|
|
})?);
|
|
}
|
|
}
|
|
_ => {
|
|
return Err(ProcessError::usage(format!(
|
|
"unknown trust-server revoke argument `{flag}`"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
let config = parse_auth_storage_flags(&mut rest)?;
|
|
let path = runtime_auth_path(&config);
|
|
let mut auth = read_runtime_auth_file(&path)?;
|
|
let server_id = server_id.ok_or_else(|| {
|
|
ProcessError::usage("trust-server revoke requires --server-id".to_string())
|
|
})?;
|
|
let before = auth.trusted_servers.len();
|
|
auth.trusted_servers
|
|
.retain(|server| server.server_id != server_id);
|
|
if auth.trusted_servers.len() == before {
|
|
return Err(ProcessError::usage(format!(
|
|
"trusted server `{server_id}` is not registered"
|
|
)));
|
|
}
|
|
write_runtime_auth_file(&path, &auth)?;
|
|
println!("revoked_server_id={server_id}");
|
|
Ok(())
|
|
}
|
|
_ => Err(ProcessError::usage(format!(
|
|
"unknown trust-server subcommand `{subcommand}`"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn usage() -> &'static str {
|
|
r#"Usage: worker-runtime-rest-server [OPTIONS]
|
|
|
|
Starts a worker-backed Runtime REST command API for a trusted backend/proxy.
|
|
Browsers must not connect to this Runtime process directly.
|
|
|
|
Options:
|
|
--bind <ADDR> Bind socket address (default: 127.0.0.1:38800)
|
|
--display-name <NAME> Runtime display name
|
|
--profile <SELECTOR> Force spawned Workers to use a Profile selector
|
|
--fs-root <PATH> Filesystem-backed storage root (default: user data dir)
|
|
--fs-worker-dir <PATH> Worker storage directory (default: <fs-root>/worker)
|
|
--fs-runtime-dir <PATH> Runtime catalog directory (default: <fs-root>/runtime)
|
|
--workdir-target <PATH> Workdir materialization target (default: <fs-root>/workdirs)
|
|
--backend-resource-endpoint <URL> Internal Backend resource fetch endpoint for resource handles
|
|
--backend-resource-token <TOKEN> Optional bearer token for the Backend resource fetch endpoint
|
|
--store <fs> Runtime catalog store kind (default: fs)
|
|
--no-store Disable Runtime catalog persistence for ephemeral runs
|
|
--local-token <TOKEN> Minimal local bearer token placeholder
|
|
--local-token-env <ENV> Read local bearer token placeholder from env
|
|
--max-event-batch-items <N> Override event batch limit
|
|
-h, --help Show this help
|
|
|
|
Auth commands:
|
|
identity init --runtime-id ID [--replace] [--fs-root PATH] [--fs-runtime-dir PATH]
|
|
identity show [--json] [--fs-root PATH] [--fs-runtime-dir PATH]
|
|
trust-server add --server-id ID --public-key KEY [--display-name NAME] [--replace] [--fs-root PATH] [--fs-runtime-dir PATH]
|
|
trust-server list [--json] [--fs-root PATH] [--fs-runtime-dir PATH]
|
|
trust-server revoke --server-id ID [--fs-root PATH] [--fs-runtime-dir PATH]"#
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn help_does_not_start_server() {
|
|
assert!(parse_args(["--help"]).unwrap().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn default_bind_addr_is_stable_for_local_dev() {
|
|
let config = parse_args([] as [&str; 0]).unwrap().unwrap();
|
|
|
|
assert_eq!(
|
|
config.http.bind_addr,
|
|
"127.0.0.1:38800".parse::<SocketAddr>().unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_removed_workspace_cwd_and_worker_path_options() {
|
|
assert!(parse_args(["--workspace", "/tmp/workspace"]).is_err());
|
|
assert!(parse_args(["--cwd", "/tmp/workspace"]).is_err());
|
|
assert!(parse_args(["--worker-store-dir", "/tmp/sessions"]).is_err());
|
|
assert!(parse_args(["--worker-metadata-dir", "/tmp/metadata"]).is_err());
|
|
assert!(parse_args(["--worker-runtime-base-dir", "/tmp/runtime"]).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_store_memory_in_favor_of_no_store() {
|
|
assert!(parse_args(["--store", "memory"]).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parses_runtime_process_config() {
|
|
let config = parse_args([
|
|
"--bind",
|
|
"127.0.0.1:0",
|
|
"--display-name",
|
|
"Runtime Alpha",
|
|
"--profile",
|
|
"builtin:coder",
|
|
"--local-token-env",
|
|
"TEST_RUNTIME_TOKEN",
|
|
]);
|
|
assert!(config.is_err());
|
|
|
|
unsafe {
|
|
env::set_var("TEST_RUNTIME_TOKEN", "secret-token");
|
|
}
|
|
let config = parse_args([
|
|
"--bind",
|
|
"127.0.0.1:0",
|
|
"--display-name",
|
|
"Runtime Alpha",
|
|
"--profile",
|
|
"builtin:coder",
|
|
"--local-token-env",
|
|
"TEST_RUNTIME_TOKEN",
|
|
])
|
|
.unwrap()
|
|
.unwrap();
|
|
unsafe {
|
|
env::remove_var("TEST_RUNTIME_TOKEN");
|
|
}
|
|
|
|
assert_eq!(config.http.bind_addr, "127.0.0.1:0".parse().unwrap());
|
|
assert_eq!(config.http.display_name.as_deref(), Some("Runtime Alpha"));
|
|
assert_eq!(config.profile.as_deref(), Some("builtin:coder"));
|
|
assert_eq!(config.http.local_token.as_deref(), Some("secret-token"));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_fs_store_runtime_process_config() {
|
|
let config = parse_args([
|
|
"--store",
|
|
"fs",
|
|
"--fs-root",
|
|
"/tmp/yoi",
|
|
"--fs-worker-dir",
|
|
"/tmp/yoi-worker",
|
|
"--fs-runtime-dir",
|
|
"/tmp/yoi-runtime",
|
|
"--workdir-target",
|
|
"/tmp/yoi-workdirs",
|
|
])
|
|
.unwrap()
|
|
.unwrap();
|
|
assert!(matches!(
|
|
config.http.store,
|
|
RuntimeHttpStoreSelection::Fs { ref root } if root == &PathBuf::from("/tmp/yoi-runtime")
|
|
));
|
|
let paths = config.resolved_fs_paths();
|
|
assert_eq!(paths.worker_dir, PathBuf::from("/tmp/yoi-worker"));
|
|
assert_eq!(paths.workdir_target, PathBuf::from("/tmp/yoi-workdirs"));
|
|
}
|
|
|
|
#[test]
|
|
fn no_store_disables_runtime_catalog_persistence() {
|
|
let config = parse_args(["--no-store"]).unwrap().unwrap();
|
|
assert!(matches!(
|
|
config.http.store,
|
|
RuntimeHttpStoreSelection::Memory
|
|
));
|
|
}
|
|
}
|