huskies: merge 1169 story Gateway pulls signed artifacts from a release channel into its local store

This commit is contained in:
Huskies Agent
2026-07-16 14:03:31 +00:00
parent 1e0e581bd7
commit 77e0394195
23 changed files with 1643 additions and 7 deletions
+41 -1
View File
@@ -70,8 +70,34 @@ impl ProjectEntry {
}
}
/// Configuration for one signed release channel the gateway can `pull` from.
///
/// All three fields are optional in the TOML shape — a channel can be
/// partially configured while it's being set up — but `pull <channel>`
/// (story 1169) requires both `base_url` and `pubkey` to be present. There is
/// no unsigned-pull mode: a missing pubkey is always a hard error, never a
/// fallback to trusting whatever the channel serves.
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct ReleaseChannelConfig {
/// Base URL the channel is served from (e.g. `https://releases.example.com/stable`).
///
/// `pull <channel>` fetches `{base_url}/manifest.json` and, once verified,
/// `{base_url}/{manifest.artifact}`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
/// Pinned Ed25519 public key (hex) the channel's `manifest.json` signature
/// must verify against. Generated by `release-tool keygen` and never
/// derived automatically — an operator must paste it in explicitly.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pubkey: Option<String>,
/// Optional bearer token sent as `Authorization: Bearer <token>` on both
/// the manifest and artifact requests (e.g. for a private release channel).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bearer_token: Option<String>,
}
/// Top-level `projects.toml` config.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct GatewayConfig {
/// Map of project name → container configuration.
#[serde(default)]
@@ -86,6 +112,12 @@ pub struct GatewayConfig {
/// `/api/sled-uplink` using the given secret token as a bearer credential.
#[serde(default)]
pub sled_tokens: BTreeMap<String, String>,
/// Map of channel name → signed release channel configuration (story 1169).
///
/// Populated by an operator adding `[release_channels.<name>]` sections to
/// `projects.toml`. Read by the `pull <channel>` gateway chat command.
#[serde(default)]
pub release_channels: BTreeMap<String, ReleaseChannelConfig>,
}
/// Validate that a gateway config has at least one project.
@@ -206,6 +238,7 @@ auth_token = "secret"
let config = GatewayConfig {
projects: BTreeMap::new(),
sled_tokens: BTreeMap::new(),
release_channels: BTreeMap::new(),
};
assert!(validate_config(&config).is_err());
}
@@ -218,6 +251,7 @@ auth_token = "secret"
let config = GatewayConfig {
projects,
sled_tokens: BTreeMap::new(),
release_channels: BTreeMap::new(),
};
assert_eq!(validate_config(&config).unwrap(), "alpha");
}
@@ -238,6 +272,7 @@ auth_token = "secret"
let config = GatewayConfig {
projects,
sled_tokens: BTreeMap::new(),
release_channels: BTreeMap::new(),
};
assert!(validate_config(&config).is_ok());
}
@@ -340,6 +375,7 @@ auth_token = "secret"
let config = GatewayConfig {
projects,
sled_tokens: BTreeMap::new(),
release_channels: BTreeMap::new(),
};
let toml_str = toml::to_string_pretty(&config).unwrap();
let parsed: GatewayConfig = toml::from_str(&toml_str).unwrap();
@@ -367,6 +403,7 @@ auth_token = "secret"
let config = GatewayConfig {
projects,
sled_tokens: BTreeMap::new(),
release_channels: BTreeMap::new(),
};
let toml_str = toml::to_string_pretty(&config).unwrap();
let parsed: GatewayConfig = toml::from_str(&toml_str).unwrap();
@@ -392,6 +429,7 @@ auth_token = "secret"
let config = GatewayConfig {
projects,
sled_tokens: BTreeMap::new(),
release_channels: BTreeMap::new(),
};
let toml_str = toml::to_string_pretty(&config).unwrap();
assert!(toml_str.contains("expected_node_id"));
@@ -407,6 +445,7 @@ auth_token = "secret"
let config2 = GatewayConfig {
projects: projects2,
sled_tokens: BTreeMap::new(),
release_channels: BTreeMap::new(),
};
let toml_str2 = toml::to_string_pretty(&config2).unwrap();
assert!(
@@ -423,6 +462,7 @@ auth_token = "secret"
let config = GatewayConfig {
projects,
sled_tokens: BTreeMap::new(),
release_channels: BTreeMap::new(),
};
let toml_str = toml::to_string_pretty(&config).unwrap();
assert!(