fix(install): generate bitcoin RPC password before orchestrator install
Bitcoin containers were exiting in ms after start because the orchestrator install path skipped the credential-materialisation step the legacy path did. resolve_secret_env then failed to read /var/lib/archipelago/secrets/bitcoin-rpc-password, the container started with no password, and bitcoind crashed before logs were useful. Two changes: 1. install.rs — call bitcoin_rpc_credentials() for bitcoin/bitcoin-core/ bitcoin-knots before any install branch runs. The function generates + persists on first call (OnceCell-cached), so this is idempotent. 2. manifest.rs::resolve_secret_env — return ManifestError::Invalid when a resolved secret trims to empty, instead of silently producing `KEY=` env vars that crash auth. Adds a unit test for the empty-secret rejection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f9e34fd0c6
commit
27ff1d5b52
@ -115,6 +115,13 @@ impl RpcHandler {
|
||||
check_bitcoin_implementation_conflict(package_id).await?;
|
||||
let repaired_bitcoin_conf =
|
||||
if matches!(package_id, "bitcoin" | "bitcoin-core" | "bitcoin-knots") {
|
||||
// Materialise the RPC password file before any install path
|
||||
// runs. The orchestrator path resolves secret_env from
|
||||
// /var/lib/archipelago/secrets/bitcoin-rpc-password at start
|
||||
// time; if the file is missing, bitcoind exits within ms.
|
||||
// bitcoin_rpc_credentials() generates + persists on first
|
||||
// call (OnceCell-cached), so this is idempotent.
|
||||
let _ = crate::bitcoin_rpc::bitcoin_rpc_credentials().await;
|
||||
ensure_bitcoin_rpc_bindings().await?
|
||||
} else {
|
||||
false
|
||||
|
||||
@ -619,6 +619,14 @@ impl ContainerConfig {
|
||||
let mut out = Vec::with_capacity(self.secret_env.len());
|
||||
for e in &self.secret_env {
|
||||
let v = provider.read(&e.secret_file)?;
|
||||
// An empty secret produces e.g. `-rpcpassword=` and crashes
|
||||
// the container on auth before logs are useful. Fail loud.
|
||||
if v.trim().is_empty() {
|
||||
return Err(ManifestError::Invalid(format!(
|
||||
"secret_env {} resolved to empty value (file: {})",
|
||||
e.key, e.secret_file
|
||||
)));
|
||||
}
|
||||
out.push(format!("{}={}", e.key, v));
|
||||
}
|
||||
Ok(out)
|
||||
@ -1051,6 +1059,36 @@ app:
|
||||
assert_eq!(out[1], "FM_GATEWAY_PASSWORD=supersecret2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_secret_env_rejects_empty_value() {
|
||||
let c = ContainerConfig {
|
||||
image: Some("x:latest".to_string()),
|
||||
image_signature: None,
|
||||
pull_policy: "if-not-present".to_string(),
|
||||
build: None,
|
||||
network: None,
|
||||
custom_args: vec![],
|
||||
entrypoint: None,
|
||||
derived_env: vec![],
|
||||
secret_env: vec![SecretEnv {
|
||||
key: "BITCOIN_RPC_PASS".to_string(),
|
||||
secret_file: "bitcoin-rpc-password".to_string(),
|
||||
}],
|
||||
data_uid: None,
|
||||
};
|
||||
let p = MapSecretsProvider {
|
||||
data: HashMap::from([("bitcoin-rpc-password".to_string(), " \n".to_string())]),
|
||||
};
|
||||
let err = c.resolve_secret_env(&p).unwrap_err();
|
||||
match err {
|
||||
ManifestError::Invalid(msg) => assert!(
|
||||
msg.contains("BITCOIN_RPC_PASS") && msg.contains("bitcoin-rpc-password"),
|
||||
"msg should name the env key + file: {msg}"
|
||||
),
|
||||
other => panic!("expected Invalid, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_every_real_manifest() {
|
||||
let app_manifests = list_repo_manifests();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user