A new `swarm-builder` service, in its own container on the host that sets
`deploy.builder.enable` (defaulted on by `allSwarmServices` where the host
has the builder's store leaf, like the review classifier).
When an `agent-configs/<agent>` PR is opened, reopened or synchronized,
swarm-controller queues a `build_agent` jobq node. The node sets a
`swarm-builder` commit status to pending on the PR head, sends
`{agent, rev, pr}` to the builder over NATS request-reply
(`$SWARM.builder.build`), and writes success (store path), failure (last
line nix printed) or error (no builder / no reply) on the head. The node
fails when the build does, so the Jobs page matches the PR.
The builder builds nix/builder/check-flake's `check` with every input
overridden: hyperhive and nixpkgs to the host's own
`c0re.hyperhiveFlake`/`c0re.nixpkgsFlake` (what a hive there renders into
its meta flake), and the agent to the PR head on the forge. Evaluation is
pure and runs in the container; builds go to the host nix daemon as an
untrusted client. The private config repo is fetched with the token of a
new read-only `builder` forge account (in the `hives` team), which the
controller mints into `swarm/services/builder/forge/token`; git gets it
from a credential helper reading the child's environment, so it is in no
argument or URL.
Credentials: a `builder` store leaf minted by glue-bao-tls on the store
host, a `swarm-builder` reader policy (forge token + its own OIDC client
secret), an authelia machine client, and a `--builder-client` grant in the
queue's auth-callout responder. The controller gains a grant on the token
path and the secret publisher a deny on it.
This is the agent-config build check atlas scoped on #5081 (slice 3):
nothing is pushed to a cache, nothing is written to KV, and a red status
gates no deploy. The check composes agent-base + the config without the
values a hive adds (CA, docs, claude-code path, ports, input follows), so
it can disagree with a hive's own build.
102 lines
4.1 KiB
Rust
102 lines
4.1 KiB
Rust
//! The swarm's secret-store client: where a credential lives, and how both ends
|
|
//! reach it.
|
|
//!
|
|
//! The HTTP is [`vaultrs`]'s job. What this crate owns is the *agreements* —
|
|
//! the rules every path obeys ([`path`]), the translation from this
|
|
//! deployment's environment into a logged-in client ([`client`]), and, per kind
|
|
//! of secret, the path it lives at together with the fields it holds
|
|
//! ([`matrix`], [`queue`], [`mtls`], [`forge`], [`github`], [`acp`], [`ci`],
|
|
//! [`review_classifier`], [`builder`]). Each of those is a thing the controller
|
|
//! and a hive must say identically, so it is said once here.
|
|
//!
|
|
//! [`policy`] is the same kind of agreement seen from the other side: which of
|
|
//! those paths a given principal's own token may read — a hive's, and an
|
|
//! agent's, which are two documents because they are two shapes of grant rather
|
|
//! than one with a name in it. It belongs here rather than in the controller
|
|
//! because the grant and the path are one statement — spelled differently they
|
|
//! produce a 403 that names neither.
|
|
//!
|
|
//! [`client`] is deliberately ignorant of all of it: it moves whatever type a
|
|
//! caller names, so a second kind of secret is a new module beside [`matrix`]
|
|
//! and not another field on a struct shared with it.
|
|
//!
|
|
//! [`mtls`] is the one module about reaching the store rather than about a
|
|
//! value inside it, and its doc explains why that is not circular.
|
|
|
|
pub mod acp;
|
|
pub mod builder;
|
|
pub mod ci;
|
|
pub mod client;
|
|
pub mod forge;
|
|
pub mod github;
|
|
pub mod matrix;
|
|
pub mod mtls;
|
|
pub mod path;
|
|
pub mod policy;
|
|
pub mod queue;
|
|
pub mod review_classifier;
|
|
|
|
pub use client::SecretStore;
|
|
|
|
/// What can go wrong between "we have a client certificate" and "we have the
|
|
/// credential".
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum Error {
|
|
/// A name that would have addressed something other than what the caller
|
|
/// meant. See [`path`].
|
|
#[error("{kind} name {value:?} is not a single path segment of [A-Za-z0-9_-]")]
|
|
PathSegment {
|
|
/// Which name was rejected. A principal's kind in the singular
|
|
/// (`agent`, `hive`, `service`, `controller`) when the name addresses
|
|
/// one, or what the name is to the secret otherwise — `account`, for
|
|
/// a matrix credential.
|
|
kind: &'static str,
|
|
/// The offending value, quoted in the message because the caller
|
|
/// usually got it from config and needs to see which one.
|
|
value: String,
|
|
},
|
|
|
|
/// A variable the store's address or identity comes from is unset or
|
|
/// empty. Named rather than defaulted: a wrong store address fails much
|
|
/// later and much less clearly than a missing one.
|
|
#[error("{0} is unset or empty")]
|
|
MissingEnv(&'static str),
|
|
|
|
/// A client-certificate file named by the environment could not be read.
|
|
#[error("reading {path} (from {var}): {source}")]
|
|
Identity {
|
|
/// The variable that named the file.
|
|
var: &'static str,
|
|
/// The path it named.
|
|
path: String,
|
|
/// The underlying IO failure.
|
|
source: std::io::Error,
|
|
},
|
|
|
|
/// The address would not parse into a URL the client can use.
|
|
#[error("the store's settings are unusable: {0}")]
|
|
Settings(String),
|
|
|
|
/// The store refused us, was unreachable, did not answer within the
|
|
/// client's request timeout, or answered something we could not parse.
|
|
#[error(transparent)]
|
|
Vault(#[from] Box<vaultrs::error::ClientError>),
|
|
|
|
/// The client certificate and key did not form a usable identity, or the
|
|
/// CA bundle did not parse.
|
|
#[error("building the TLS identity: {0}")]
|
|
Tls(#[source] reqwest::Error),
|
|
|
|
/// The store answered an issue request with a field empty that a usable
|
|
/// identity needs. Names the field, never its value.
|
|
#[error("the store issued a certificate whose {0} is empty")]
|
|
IncompleteIssue(&'static str),
|
|
}
|
|
|
|
impl From<vaultrs::error::ClientError> for Error {
|
|
fn from(e: vaultrs::error::ClientError) -> Self {
|
|
// Boxed because `ClientError` is large enough that carrying it inline
|
|
// makes every `Result` in the crate pay for the rare arm.
|
|
Self::Vault(Box::new(e))
|
|
}
|
|
}
|