pub(crate) mod admin;
pub(crate) mod atproto;
#[doc(hidden)]
pub mod bitmap;
pub(crate) mod blobs;
pub(crate) mod cli;
pub(crate) mod cob_write;
pub(crate) mod config;
pub(crate) mod datastore;
pub(crate) mod firehose;
pub(crate) mod generate;
pub(crate) mod git_export;
pub(crate) mod git_xrpc;
pub(crate) mod identity;
pub(crate) mod index_page;
pub(crate) mod maintenance;
pub mod nginx;
pub(crate) mod oauth;
pub(crate) mod og_card;
pub(crate) mod spindle;
pub(crate) mod transport;
pub(crate) mod xrpc;
pub(crate) use admin::{init, keys, owner, provision, registry};
pub(crate) use atproto::{crawl, pds};
pub(crate) use datastore::{cas, output, repolock, store, tombstone};
pub(crate) use generate::{
activate, failpoint, gc, manifest, plan, profile, publish, release, snapshot,
};
#[cfg(any(test, feature = "bench"))]
pub(crate) use git_export::bench_fixture;
pub(crate) use git_export::{
commit_graph, consolidate, depth_one, midx_rev, packfile, partitions, reply_pack, spare,
};
pub(crate) use transport::{cgi, fork, git_ssh, http_push, inflate, out, repos, ssh_fetch, writes};
#[cfg(test)]
#[path = "../tests/common/bitmap.rs"]
mod test_bitmap;
#[cfg(test)]
#[path = "../tests/common/cobs.rs"]
mod test_cobs;
#[cfg(test)]
#[path = "../tests/common/rng.rs"]
mod test_rng;
#[cfg(test)]
#[path = "../tests/common/signing.rs"]
mod test_signing;
/// drops an inherited GIT_DIR from `git rebase -x`, and the user's gpgsign.
#[cfg(test)]
fn test_git() -> std::process::Command {
let mut command = std::process::Command::new("git");
git_export::without_repo_env(&mut command)
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_AUTHOR_NAME", "t")
.env("GIT_AUTHOR_EMAIL", "t@t")
.env("GIT_COMMITTER_NAME", "t")
.env("GIT_COMMITTER_EMAIL", "t@t")
.env("GIT_CONFIG_COUNT", "1")
.env("GIT_CONFIG_KEY_0", "maintenance.auto")
.env("GIT_CONFIG_VALUE_0", "false");
command
}
use std::sync::Arc;
use clap::Parser as _;
fn init_tracing() {
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.init();
}
/// cgi and ssh handle one request per process. a thread pool would only sit idle.
fn runtime(threaded: bool) -> tokio::runtime::Runtime {
let mut builder = if threaded {
tokio::runtime::Builder::new_multi_thread()
} else {
tokio::runtime::Builder::new_current_thread()
};
builder
.enable_all()
.build()
.expect("build the tokio runtime")
}
pub fn run() -> anyhow::Result<()> {
if cgi::invoked_as_cgi() {
init_tracing();
runtime(false).block_on(cgi::run(&cgi::config_path_from_env()?));
return Ok(());
}
let cli = cli::Cli::parse();
if !matches!(cli.command, cli::Command::GitSsh { .. }) || std::env::var_os("RUST_LOG").is_some()
{
init_tracing();
}
if matches!(cli.command, cli::Command::Cgi) {
runtime(false).block_on(cgi::run(&cli.config));
return Ok(());
}
let config_path = if cli.config == std::path::Path::new("knot-static.toml")
&& !cli.config.exists()
&& std::path::Path::new("/etc/knot-static/config.toml").is_file()
{
std::path::PathBuf::from("/etc/knot-static/config.toml")
} else {
cli.config.clone()
};
let config = Arc::new(config::ValidatedConfig::load(&config_path)?);
use cli::{Command, KeysCommand};
let threaded = match &cli.command {
Command::Keys {
command: KeysCommand::List { command },
} => {
print!("{}", keys::list(&config, command)?);
return Ok(());
}
Command::GitSsh { .. } | Command::Keys { .. } => false,
_ => true,
};
runtime(threaded).block_on(dispatch(cli, config))
}
async fn dispatch(cli: cli::Cli, config: Arc<config::ValidatedConfig>) -> anyhow::Result<()> {
use cli::{Command, PlcCommand, RepoCommand};
match cli.command {
Command::Init => init::run(config).await,
Command::Repo { command } => match command {
RepoCommand::Import(args) => provision::import(config, args).await,
RepoCommand::Create(args) => provision::create(config, args).await,
RepoCommand::List => registry::list(config).await,
RepoCommand::Settings(args) => owner::settings(config, args).await,
RepoCommand::Rename(args) => owner::rename(config, args).await,
RepoCommand::Collaborator { command } => owner::collaborator(config, command).await,
RepoCommand::Record(args) => pds::record(config, &args.repo_did, args.publish).await,
},
Command::Plc { command } => match command {
PlcCommand::Sync => registry::plc_sync(config).await,
PlcCommand::Handover(args) => registry::plc_handover(config, args).await,
},
Command::Cgi => unreachable!("cgi never loads a config before it answers"),
Command::Issue { command } => owner::issue(config, command).await,
Command::Label { command } => owner::label(config, command).await,
Command::Pull { command } => owner::pull(config, command).await,
Command::Generate { repo } => {
let outcome = generate::run(config, repo.as_deref()).await?;
if let generate::BuildOutcome::Unchanged { generation_id } = outcome {
eprintln!(
"generation {} unchanged, nothing to do",
manifest::short_id(&generation_id)
);
}
Ok(())
}
Command::Activate {
generation,
rollback,
} => activate::run(config, &generation, rollback).await,
Command::Gc { keep, force } => gc::run(config, keep, force).await,
Command::OAuth { command } => match command {
cli::OAuthCommand::Login { owner } => {
let owner_did = owner
.map(knot_types::AccountDid::new)
.transpose()?
.unwrap_or_else(|| config.owner.clone());
oauth::run_interactive_auth(&config, &owner_did).await?;
pds::publish_owned_by(config, &owner_did).await;
Ok(())
}
cli::OAuthCommand::Status => oauth::print_status(&config).await,
},
Command::GitSsh { actors, operator } => {
git_ssh::run(config, git_ssh::Pusher::from_flags(operator, actors)?).await
}
Command::Keys { command } => match command {
cli::KeysCommand::Sync { new } => keys::sync(&config, !new).await,
cli::KeysCommand::List { .. } => unreachable!("keys list answers without a runtime"),
},
Command::Write {
method,
repo,
actor,
} => write_record(config, &method, &repo, actor).await,
Command::PostActivate {
repos,
no_declarations,
} => {
if !repos.is_empty() {
activate::post_activate(config.clone(), &repos, !no_declarations).await;
}
// not in `activate`, which must not wait on every key or a down spindle.
let (_, _, keys) = tokio::join!(
spindle::drain(&config),
index_page::sync_handles(&config),
keys::sync(&config, false),
);
keys
}
Command::AfterActivation {
generation,
previous,
at,
} => activate::after_activation(&config, &generation, previous.as_deref(), at),
Command::Prepare => {
// a failure leaves the old generation serving, the deploy goes on
match git_xrpc::prepare(&config.output_root) {
Ok(written) => eprintln!("prepared the git read maps of {written} repos"),
Err(error) => tracing::error!("prepare the git read maps: {error:#}"),
}
let before = publish::current_generation(&config.output_root);
if let Err(error) = writes::publish_scoped(&config, None).await {
tracing::error!("rebuild with the new binary: {error:#}");
}
// an activation starts the hooks itself, a new or changed one alone needs a run of its own
let current = publish::current_generation(&config.output_root);
if let Some(current) = current.filter(|current| before.as_ref() == Some(current))
&& generate::hooks::changed(&config)
{
let at = publish::unix_now().to_string();
if let Err(error) =
git_ssh::spawn_detached(&config, ["after-activation", ¤t, "--at", &at])
{
tracing::error!("start the changed hooks: {error:#}");
}
}
Ok(())
}
Command::Crawl => {
crawl::request_all(&config).await;
Ok(())
}
Command::Maintenance { repo, command, .. } => {
let action = maintenance::Action::parse(&command)?;
match repo {
Some(repo) => maintenance::run(config, &repo, action).await,
None => maintenance::run_all(config, action).await,
}
}
}
}
async fn write_record(
config: Arc<config::ValidatedConfig>,
method: &str,
repo: &str,
actor: Option<String>,
) -> anyhow::Result<()> {
use std::io::Read as _;
let method = cob_write::Method::from_nsid(&format!(
"{}.{method}",
if method == "submitPatch" {
"org.tangled.review"
} else {
"com.atproto.repo"
}
))
.ok_or_else(|| anyhow::anyhow!("{method} is not a write method"))?;
let repo = knot_types::RepoDid::new(repo)?;
let actor = match actor {
Some(account) => writes::Actor::Account(knot_types::AccountDid::new(account)?),
None => writes::Actor::Owner,
};
let mut body = Vec::new();
std::io::stdin().read_to_end(&mut body)?;
match writes::perform(&config, &repo, actor, method, &body).await {
Ok(published) => {
println!("{}", published.response());
owner::announce(&config, &published).await;
Ok(())
}
Err(error) => anyhow::bail!("{}", error.body()),
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_git_reads_no_user_config() {
let dir = tempfile::tempdir().unwrap();
let (home, signed) = super::test_signing::signing_home(dir.path());
let repo = dir.path().join("repo");
std::fs::create_dir(&repo).unwrap();
let helpers: [(&str, &dyn Fn() -> std::process::Command); 2] = [
("test_git", &|| {
let mut command = super::test_git();
command.current_dir(&repo);
command
}),
// production git brings no identity, the test has to give it one.
("git_command", &|| {
let mut command = super::git_export::git_command(&repo);
command
.env("GIT_AUTHOR_NAME", "t")
.env("GIT_AUTHOR_EMAIL", "t@t")
.env("GIT_COMMITTER_NAME", "t")
.env("GIT_COMMITTER_EMAIL", "t@t");
command
}),
];
for (name, git) in helpers {
for args in [
&["init", "-q"][..],
// git won't guess an identity from the host, as on a fresh server.
&["config", "user.useConfigOnly", "true"],
&["commit", "-q", "--allow-empty", "-m", name],
] {
let output = git()
.args(args)
.env("HOME", &home)
.env("XDG_CONFIG_HOME", home.join(".config"))
.output()
.unwrap();
assert!(output.status.success(), "{name} {args:?}: {output:?}");
}
assert!(!signed.exists(), "{name} signed a commit");
let command = git();
let envs: Vec<_> = command.get_envs().collect();
for directing in super::git_export::REPO_LOCATING_ENV {
let dropped = (std::ffi::OsStr::new(directing), None);
assert!(envs.contains(&dropped), "{name} keeps {directing}");
}
}
}
}