hive-c0re copies the hive's store read token to /run/hyperhive/forge-hive-token for nix's git credential helper, and no longer mints, reads or falls back to the core admin token. drops the core user sweep, avatar upload, webhook cleanup and hive-priv's RunForgeAdmin.
137 lines
4.5 KiB
Rust
137 lines
4.5 KiB
Rust
//! This hive's reads of `internal/meta` and of an agent's config head, as
|
|
//! [`crate::meta_rev::MetaSource`], under the hive's forge read token.
|
|
|
|
use forgejo_api::structs::RepoGetRawFileQuery;
|
|
use forgejo_api::{ApiErrorKind, Forgejo, ForgejoError};
|
|
use reqwest::StatusCode;
|
|
|
|
use super::reader::ReaderToken;
|
|
use super::{CONFIG_ORG, bounded};
|
|
use crate::meta_rev::MetaSource;
|
|
|
|
/// The org and repo the swarm commits its meta flake to.
|
|
const META_OWNER: &str = "internal";
|
|
const META_REPO: &str = "meta";
|
|
|
|
/// The branch an agent's config is deployed from, which `fetch_forge_main`
|
|
/// and the swarm controller's pins both read.
|
|
const CONFIG_BRANCH: &str = "main";
|
|
|
|
/// [`MetaSource`] over the forge API.
|
|
pub(crate) struct ForgeSource {
|
|
api: Forgejo,
|
|
/// Whether the credential file nix's `git` reads holds this hive's token.
|
|
nix_credential: bool,
|
|
}
|
|
|
|
/// A source reading as this hive, or `None` when it holds no forge token. It
|
|
/// also refreshes the credential file nix's `git` fetches with, which the
|
|
/// build after this read needs to be current.
|
|
pub(crate) async fn internal_meta_source() -> Option<ForgeSource> {
|
|
let token: ReaderToken = super::reader_token().await?;
|
|
match token.api() {
|
|
Ok(api) => Some(ForgeSource {
|
|
api,
|
|
nix_credential: token.publish_for_nix(),
|
|
}),
|
|
Err(e) => {
|
|
tracing::warn!(error = ?e, "internal/meta: cannot build a forge client");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_not_found(e: &ForgejoError) -> bool {
|
|
match e {
|
|
ForgejoError::ApiError(api) => match api.error_kind() {
|
|
ApiErrorKind::NotFound { .. } => true,
|
|
ApiErrorKind::Other(s) => *s == StatusCode::NOT_FOUND,
|
|
_ => false,
|
|
},
|
|
ForgejoError::UnexpectedStatusCode(s) => *s == StatusCode::NOT_FOUND,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
impl ForgeSource {
|
|
/// The head commit of `branch` in `owner/repo`; `None` when the repo or
|
|
/// the branch is not there.
|
|
async fn branch_head(
|
|
&self,
|
|
owner: &str,
|
|
repo: &str,
|
|
branch: &str,
|
|
) -> anyhow::Result<Option<String>> {
|
|
let op = format!("read branch {branch} of {owner}/{repo}");
|
|
match bounded(&op, self.api.repo_get_branch(owner, repo, branch)).await? {
|
|
Ok(branch) => Ok(branch.commit.and_then(|c| c.id)),
|
|
Err(e) if is_not_found(&e) => Ok(None),
|
|
Err(e) => Err(anyhow::Error::new(e).context(op)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MetaSource for ForgeSource {
|
|
fn nix_can_fetch(&self) -> bool {
|
|
self.nix_credential
|
|
}
|
|
|
|
async fn meta_head(&self) -> anyhow::Result<Option<String>> {
|
|
use anyhow::Context as _;
|
|
|
|
let op = format!("read repo {META_OWNER}/{META_REPO}");
|
|
let repo = match bounded(&op, self.api.repo_get(META_OWNER, META_REPO)).await? {
|
|
Ok(repo) => repo,
|
|
Err(e) if is_not_found(&e) => return Ok(None),
|
|
Err(e) => return Err(anyhow::Error::new(e).context(op)),
|
|
};
|
|
if repo.empty.unwrap_or(false) {
|
|
return Ok(None);
|
|
}
|
|
let branch = repo
|
|
.default_branch
|
|
.with_context(|| format!("{META_OWNER}/{META_REPO} names no default branch"))?;
|
|
self.branch_head(META_OWNER, META_REPO, &branch).await
|
|
}
|
|
|
|
async fn flake_at(&self, rev: &str) -> anyhow::Result<String> {
|
|
let op = format!("read flake.nix of {META_OWNER}/{META_REPO} at {rev}");
|
|
let query = RepoGetRawFileQuery {
|
|
r#ref: Some(rev.to_owned()),
|
|
};
|
|
let raw = bounded(
|
|
&op,
|
|
self.api
|
|
.repo_get_raw_file(META_OWNER, META_REPO, "flake.nix", query),
|
|
)
|
|
.await?
|
|
.map_err(|e| anyhow::Error::new(e).context(op))?;
|
|
Ok(String::from_utf8_lossy(&raw).into_owned())
|
|
}
|
|
|
|
async fn config_head(&self, agent: &str) -> anyhow::Result<Option<String>> {
|
|
self.branch_head(CONFIG_ORG, agent, CONFIG_BRANCH).await
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use forgejo_api::Auth;
|
|
|
|
fn source(nix_credential: bool) -> ForgeSource {
|
|
let url = url::Url::parse("http://forge.test").expect("url");
|
|
ForgeSource {
|
|
api: Forgejo::new(Auth::None, url).expect("client"),
|
|
nix_credential,
|
|
}
|
|
}
|
|
|
|
/// The control (`true`) shows the answer follows the field rather than
|
|
/// being a constant.
|
|
#[test]
|
|
fn nix_fetches_only_where_its_credential_was_published() {
|
|
assert!(source(true).nix_can_fetch());
|
|
assert!(!source(false).nix_can_fetch());
|
|
}
|
|
}
|