chore: purge retired git.tx1138.com registry host from the codebase
The tx1138 Gitea was retired as a release server 2026-06-13 and its registry frontend is fully dead (500 on every /v2 manifest read, observed 2026-07-10). Nothing may reference it anymore: - registry.rs: no longer a default registry, no longer force-enabled on load; saved configs are stripped of it on load (same one-time migration treatment as the decommissioned Hetzner mirror), with a regression test. - image_policy.rs: removed from TRUSTED_REGISTRIES — refs through the dead host are now refused at the pull site (rejection test added). - api/handler: dropped the legacy catalog-proxy fallback URL. - .gitmodules: indeedhub submodule repointed to the OVH Gitea. - scripts, image-recipe, app-catalog data, neode-ui strings, docs, and all test fixtures repointed to 146.59.87.168:3000 (or neutral example hosts). - image-versions.sh: ARCHY_REGISTRY_FALLBACK emptied (guarded consumers skip it); reconcile-containers.sh candidate guard hardened. The only remaining occurrences of the host string are the strip/reject enforcement paths and their regression tests — the code that guarantees it is never used again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -126,15 +126,15 @@ impl ApiHandler {
|
||||
}
|
||||
|
||||
/// Server-side fetch of the upstream app catalog so the browser can
|
||||
/// load it without fighting CORS (git.tx1138.com emits no ACAO) or
|
||||
/// load it without fighting CORS (upstream Gitea emits no ACAO) or
|
||||
/// CSP (the fallback IP-port URL isn't in `connect-src`). The upstream
|
||||
/// list is derived from the operator's configured container registries
|
||||
/// so switching mirrors in Settings changes the App Store source too —
|
||||
/// each active registry contributes one Gitea `raw/branch/main/catalog.json`
|
||||
/// URL (http or https per `tls_verify`), tried in priority order.
|
||||
/// If registry config can't be loaded, falls back to the legacy
|
||||
/// hardcoded pair so the App Store still renders on nodes that haven't
|
||||
/// persisted a registry config yet. 15s total timeout.
|
||||
/// If registry config can't be loaded, falls back to the hardcoded OVH
|
||||
/// URL so the App Store still renders on nodes that haven't persisted
|
||||
/// a registry config yet. 15s total timeout.
|
||||
async fn handle_app_catalog_proxy(&self) -> Result<Response<hyper::Body>> {
|
||||
let mut upstreams: Vec<String> = Vec::new();
|
||||
if let Ok(config) = crate::container::registry::load_registries(&self.config.data_dir).await
|
||||
@@ -155,10 +155,6 @@ impl ApiHandler {
|
||||
"http://146.59.87.168:3000/lfg2025/app-catalog/raw/branch/main/catalog.json"
|
||||
.to_string(),
|
||||
);
|
||||
upstreams.push(
|
||||
"https://git.tx1138.com/lfg2025/app-catalog/raw/branch/main/catalog.json"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let client = match reqwest::Client::builder()
|
||||
@@ -527,7 +523,7 @@ impl ApiHandler {
|
||||
|
||||
// App-catalog proxy — fetches catalog.json from the configured
|
||||
// upstream URLs server-side so the browser doesn't hit CORS
|
||||
// (git.tx1138.com has no ACAO header) or CSP (IP-port upstream
|
||||
// (upstream Gitea has no ACAO header) or CSP (IP-port upstream
|
||||
// falls outside `connect-src`). Session-authenticated so only
|
||||
// the logged-in node owner can spin up fetches.
|
||||
(Method::GET, "/api/app-catalog") => {
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
//! 1.8.0 hardening plan).
|
||||
|
||||
/// Registries images may be pulled from with an explicit host part.
|
||||
/// (git.tx1138.com was removed 2026-07-10: the host is retired and must
|
||||
/// never be pulled through again.)
|
||||
pub const TRUSTED_REGISTRIES: &[&str] = &[
|
||||
"docker.io",
|
||||
"ghcr.io",
|
||||
"localhost",
|
||||
"git.tx1138.com",
|
||||
"146.59.87.168:3000",
|
||||
];
|
||||
|
||||
@@ -58,13 +59,19 @@ mod tests {
|
||||
"docker.io/library/nginx:1.25",
|
||||
"ghcr.io/owner/app:latest",
|
||||
"localhost/archy-dev:1",
|
||||
"git.tx1138.com/lfg2025/x:2",
|
||||
"146.59.87.168:3000/archy/bitcoin-knots:28.1",
|
||||
] {
|
||||
assert!(is_valid_docker_image(img), "{img} should be accepted");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_retired_tx1138_registry() {
|
||||
// Retired 2026-07-10 — refs through the dead host must be refused
|
||||
// at the pull site, not time out against it.
|
||||
assert!(!is_valid_docker_image("git.tx1138.com/lfg2025/x:2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_docker_hub_shorthand() {
|
||||
for img in ["nginx", "grafana/grafana:11.2.0", "lightninglabs/lnd:v0.18"] {
|
||||
|
||||
@@ -234,7 +234,7 @@ pub fn available_update_for_images(pinned: &str, running_image: &str) -> Option<
|
||||
}
|
||||
|
||||
/// Extract version tag from a full image reference.
|
||||
/// e.g. "git.tx1138.com/lfg2025/lnd:v0.18.4-beta" → "v0.18.4-beta"
|
||||
/// e.g. "146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta" → "v0.18.4-beta"
|
||||
/// Returns "latest" if no tag or tag is empty.
|
||||
pub fn extract_version_from_image(image: &str) -> String {
|
||||
// Split off the tag after the last colon, but only if it comes after the last slash
|
||||
@@ -328,11 +328,11 @@ mod tests {
|
||||
#[test]
|
||||
fn test_extract_version() {
|
||||
assert_eq!(
|
||||
extract_version_from_image("git.tx1138.com/lfg2025/lnd:v0.18.4-beta"),
|
||||
extract_version_from_image("146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta"),
|
||||
"v0.18.4-beta"
|
||||
);
|
||||
assert_eq!(
|
||||
extract_version_from_image("git.tx1138.com/lfg2025/grafana:10.2.0"),
|
||||
extract_version_from_image("146.59.87.168:3000/lfg2025/grafana:10.2.0"),
|
||||
"10.2.0"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -340,7 +340,7 @@ mod tests {
|
||||
"latest"
|
||||
);
|
||||
assert_eq!(
|
||||
extract_version_from_image("git.tx1138.com/lfg2025/bitcoin-knots:latest"),
|
||||
extract_version_from_image("146.59.87.168:3000/lfg2025/bitcoin-knots:latest"),
|
||||
"latest"
|
||||
);
|
||||
}
|
||||
@@ -352,7 +352,7 @@ mod tests {
|
||||
"lfg2025/lnd"
|
||||
);
|
||||
assert_eq!(
|
||||
image_without_registry_or_tag("git.tx1138.com/lfg2025/lnd:v0.18.4-beta"),
|
||||
image_without_registry_or_tag("146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta"),
|
||||
"lfg2025/lnd"
|
||||
);
|
||||
}
|
||||
@@ -369,7 +369,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
available_update_for_images(
|
||||
"146.59.87.168:3000/lfg2025/nextcloud:29",
|
||||
"git.tx1138.com/lfg2025/nextcloud:29",
|
||||
"146.59.87.168:3000/lfg2025/nextcloud:29",
|
||||
),
|
||||
None
|
||||
);
|
||||
@@ -389,7 +389,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_parse_image_versions() {
|
||||
let content = r#"
|
||||
ARCHY_REGISTRY="git.tx1138.com/lfg2025"
|
||||
ARCHY_REGISTRY="146.59.87.168:3000/lfg2025"
|
||||
LND_IMAGE="$ARCHY_REGISTRY/lnd:v0.18.4-beta"
|
||||
GRAFANA_IMAGE="$ARCHY_REGISTRY/grafana:10.2.0"
|
||||
# comment
|
||||
@@ -398,11 +398,11 @@ NOT_AN_IMAGE="something"
|
||||
let parsed = parse_image_versions(content);
|
||||
assert_eq!(
|
||||
parsed.get("LND_IMAGE"),
|
||||
Some(&"git.tx1138.com/lfg2025/lnd:v0.18.4-beta".to_string())
|
||||
Some(&"146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
parsed.get("GRAFANA_IMAGE"),
|
||||
Some(&"git.tx1138.com/lfg2025/grafana:10.2.0".to_string())
|
||||
Some(&"146.59.87.168:3000/lfg2025/grafana:10.2.0".to_string())
|
||||
);
|
||||
assert!(!parsed.contains_key("NOT_AN_IMAGE"));
|
||||
assert!(!parsed.contains_key("ARCHY_REGISTRY"));
|
||||
|
||||
@@ -2251,7 +2251,7 @@ impl ProdContainerOrchestrator {
|
||||
// Reconcile recreates route through here too: an unreachable
|
||||
// registry must not brick an app whose exact image:tag is
|
||||
// already in local storage (boot reconcile of archy-btcpay-db
|
||||
// /archy-nbxplorer with git.tx1138.com dead, 2026-07-10).
|
||||
// /archy-nbxplorer with the (since-retired) upstream registry dead, 2026-07-10).
|
||||
// Same exists-first semantics as
|
||||
// ensure_resolved_source_available and the quadlets'
|
||||
// Pull=never.
|
||||
@@ -4848,7 +4848,7 @@ app:
|
||||
name: File Browser
|
||||
version: 1.0.0
|
||||
container:
|
||||
image: git.tx1138.com/lfg2025/filebrowser:v2.27.0
|
||||
image: 146.59.87.168:3000/lfg2025/filebrowser:v2.27.0
|
||||
custom_args:
|
||||
- --config
|
||||
- /data/.filebrowser.json
|
||||
@@ -4872,7 +4872,7 @@ app:
|
||||
name: LND
|
||||
version: 1.0.0
|
||||
container:
|
||||
image: git.tx1138.com/lfg2025/lnd:v0.18.4-beta
|
||||
image: 146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta
|
||||
secret_env:
|
||||
- key: BITCOIND_RPCPASS
|
||||
secret_file: bitcoin-rpc-password
|
||||
@@ -4959,7 +4959,7 @@ app:
|
||||
async fn install_fresh_skips_pull_when_image_local() {
|
||||
// An unreachable registry must not brick a reconcile recreate whose
|
||||
// exact image:tag is already in local storage (archy-btcpay-db /
|
||||
// archy-nbxplorer boot reconcile with git.tx1138.com dead,
|
||||
// archy-nbxplorer boot reconcile with the (since-retired) upstream registry dead,
|
||||
// 2026-07-10).
|
||||
let rt = Arc::new(MockRuntime::default());
|
||||
rt.mark_image_present("docker.io/bitcoin/knots:28");
|
||||
|
||||
@@ -11,12 +11,17 @@ use tokio::fs;
|
||||
|
||||
const REGISTRY_FILE: &str = "config/registries.json";
|
||||
const OVH_REGISTRY_URL: &str = "146.59.87.168:3000/lfg2025";
|
||||
const TX1138_REGISTRY_URL: &str = "git.tx1138.com/lfg2025";
|
||||
/// Retired registry host (release server retired 2026-06-13; the registry
|
||||
/// frontend was fully dead by 2026-07-10 — 500 on every /v2 manifest read).
|
||||
/// Never a default, never force-enabled; stripped from saved configs on
|
||||
/// load. The literal exists ONLY so the strip can match — nothing may pull
|
||||
/// through this host.
|
||||
const RETIRED_TX1138_HOST: &str = "git.tx1138.com";
|
||||
|
||||
/// A single container registry.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Registry {
|
||||
/// Registry URL (e.g., "git.tx1138.com/lfg2025" or "146.59.87.168:3000/lfg2025").
|
||||
/// Registry URL (e.g., "146.59.87.168:3000/lfg2025").
|
||||
pub url: String,
|
||||
/// Human-readable name.
|
||||
pub name: String,
|
||||
@@ -43,22 +48,13 @@ pub struct RegistryConfig {
|
||||
impl Default for RegistryConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
registries: vec![
|
||||
Registry {
|
||||
url: OVH_REGISTRY_URL.to_string(),
|
||||
name: "Server 1 (OVH)".to_string(),
|
||||
tls_verify: false,
|
||||
enabled: true,
|
||||
priority: 0,
|
||||
},
|
||||
Registry {
|
||||
url: TX1138_REGISTRY_URL.to_string(),
|
||||
name: "Server 2 (tx1138)".to_string(),
|
||||
tls_verify: true,
|
||||
enabled: true,
|
||||
priority: 10,
|
||||
},
|
||||
],
|
||||
registries: vec![Registry {
|
||||
url: OVH_REGISTRY_URL.to_string(),
|
||||
name: "Server 1 (OVH)".to_string(),
|
||||
tls_verify: false,
|
||||
enabled: true,
|
||||
priority: 0,
|
||||
}],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +68,7 @@ impl RegistryConfig {
|
||||
}
|
||||
|
||||
/// Rewrite an image reference to use a specific registry.
|
||||
/// E.g., "git.tx1138.com/lfg2025/bitcoin-knots:latest" with registry "146.59.87.168:3000/lfg2025"
|
||||
/// E.g., "docker.io/lfg2025/bitcoin-knots:latest" with registry "146.59.87.168:3000/lfg2025"
|
||||
/// becomes "146.59.87.168:3000/lfg2025/bitcoin-knots:latest".
|
||||
pub fn rewrite_image(&self, image: &str, registry: &Registry) -> String {
|
||||
// Extract the image name (last component after the org/namespace)
|
||||
@@ -83,7 +79,7 @@ impl RegistryConfig {
|
||||
}
|
||||
|
||||
/// Extract the image name from a full image reference.
|
||||
/// "git.tx1138.com/lfg2025/bitcoin-knots:latest" -> "bitcoin-knots:latest"
|
||||
/// "146.59.87.168:3000/lfg2025/bitcoin-knots:latest" -> "bitcoin-knots:latest"
|
||||
/// "docker.io/gitea/gitea:1.23" -> "gitea:1.23"
|
||||
fn extract_image_name(image: &str) -> &str {
|
||||
// Split by '/' and take the last segment (image:tag)
|
||||
@@ -118,6 +114,12 @@ pub async fn load_registries(data_dir: &Path) -> Result<RegistryConfig> {
|
||||
config
|
||||
.registries
|
||||
.retain(|r| !r.url.contains("23.182.128.160"));
|
||||
// Same treatment for the retired tx1138 registry (was Server 2 in older
|
||||
// defaults): strip it on load so nothing ever pulls through the dead
|
||||
// host again.
|
||||
config
|
||||
.registries
|
||||
.retain(|r| !r.url.contains(RETIRED_TX1138_HOST));
|
||||
let mut changed = config.registries.len() != before;
|
||||
|
||||
// Migrate: any default registry URL that isn't already in the
|
||||
@@ -178,12 +180,6 @@ fn force_ovh_registry_primary(config: &mut RegistryConfig) {
|
||||
registry.enabled = true;
|
||||
registry.priority = 0;
|
||||
}
|
||||
TX1138_REGISTRY_URL => {
|
||||
registry.name = "Server 2 (tx1138)".to_string();
|
||||
registry.tls_verify = true;
|
||||
registry.enabled = true;
|
||||
registry.priority = 10;
|
||||
}
|
||||
_ => {
|
||||
if registry.priority <= 10 {
|
||||
registry.priority = registry.priority.saturating_add(20);
|
||||
@@ -216,7 +212,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_extract_image_name() {
|
||||
assert_eq!(
|
||||
extract_image_name("git.tx1138.com/lfg2025/bitcoin-knots:latest"),
|
||||
extract_image_name("146.59.87.168:3000/lfg2025/bitcoin-knots:latest"),
|
||||
"bitcoin-knots:latest"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -229,11 +225,11 @@ mod tests {
|
||||
#[test]
|
||||
fn test_rewrite_image() {
|
||||
let config = RegistryConfig::default();
|
||||
// Default primary is now the OVH VPS (index 0). A tx1138-hardcoded
|
||||
// image rewrites to OVH when asked for the primary mirror.
|
||||
// An image hardcoded to some other registry rewrites to OVH when
|
||||
// asked for the primary mirror.
|
||||
let primary = &config.registries[0];
|
||||
assert_eq!(
|
||||
config.rewrite_image("git.tx1138.com/lfg2025/bitcoin-knots:latest", primary),
|
||||
config.rewrite_image("docker.io/lfg2025/bitcoin-knots:latest", primary),
|
||||
"146.59.87.168:3000/lfg2025/bitcoin-knots:latest"
|
||||
);
|
||||
}
|
||||
@@ -242,15 +238,51 @@ mod tests {
|
||||
fn test_active_registries_sorted() {
|
||||
let config = RegistryConfig::default();
|
||||
let active = config.active_registries();
|
||||
assert_eq!(active.len(), 2);
|
||||
assert!(active[0].priority <= active[1].priority);
|
||||
assert_eq!(active.len(), 1);
|
||||
assert_eq!(active[0].url, OVH_REGISTRY_URL);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_load_default() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config = load_registries(tmp.path()).await.unwrap();
|
||||
assert_eq!(config.registries.len(), 2);
|
||||
assert_eq!(config.registries.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_load_strips_retired_tx1138_registry() {
|
||||
// Nodes provisioned before the retirement have the tx1138 registry
|
||||
// baked into their saved config (was Server 2). It must be stripped
|
||||
// on load and never re-added by the defaults merge.
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config = RegistryConfig {
|
||||
registries: vec![
|
||||
Registry {
|
||||
url: format!("{RETIRED_TX1138_HOST}/lfg2025"),
|
||||
name: "Server 2 (tx1138)".into(),
|
||||
tls_verify: true,
|
||||
enabled: true,
|
||||
priority: 10,
|
||||
},
|
||||
Registry {
|
||||
url: OVH_REGISTRY_URL.into(),
|
||||
name: "Server 1 (OVH)".into(),
|
||||
tls_verify: false,
|
||||
enabled: true,
|
||||
priority: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
save_registries(tmp.path(), &config).await.unwrap();
|
||||
let loaded = load_registries(tmp.path()).await.unwrap();
|
||||
assert!(
|
||||
!loaded
|
||||
.registries
|
||||
.iter()
|
||||
.any(|r| r.url.contains(RETIRED_TX1138_HOST)),
|
||||
"retired tx1138 registry must be stripped on load; got {:?}",
|
||||
loaded.registries
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -266,6 +298,6 @@ mod tests {
|
||||
});
|
||||
save_registries(tmp.path(), &config).await.unwrap();
|
||||
let loaded = load_registries(tmp.path()).await.unwrap();
|
||||
assert_eq!(loaded.registries.len(), 3);
|
||||
assert_eq!(loaded.registries.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2132,9 +2132,9 @@ mod tests {
|
||||
fn test_manifest_origin_parses_https() {
|
||||
assert_eq!(
|
||||
manifest_origin(
|
||||
"https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/manifest.json"
|
||||
"https://releases.example.com/lfg2025/archy/raw/branch/main/releases/manifest.json"
|
||||
),
|
||||
Some("https://git.tx1138.com".to_string())
|
||||
Some("https://releases.example.com".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2151,7 +2151,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_manifest_origin_rejects_garbage() {
|
||||
assert_eq!(manifest_origin("not a url"), None);
|
||||
assert_eq!(manifest_origin("ftp://git.tx1138.com/x"), None);
|
||||
assert_eq!(manifest_origin("ftp://releases.example.com/x"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2165,7 +2165,7 @@ mod tests {
|
||||
name: "archipelago".into(),
|
||||
current_version: "1.7.25-alpha".into(),
|
||||
new_version: "1.7.26-alpha".into(),
|
||||
download_url: "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/archipelago".into(),
|
||||
download_url: "https://releases.example.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/archipelago".into(),
|
||||
sha256: "x".into(),
|
||||
size_bytes: 1,
|
||||
blake3: None,
|
||||
@@ -2174,7 +2174,7 @@ mod tests {
|
||||
name: "frontend".into(),
|
||||
current_version: "1.7.25-alpha".into(),
|
||||
new_version: "1.7.26-alpha".into(),
|
||||
download_url: "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/frontend.tar.gz".into(),
|
||||
download_url: "https://releases.example.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/frontend.tar.gz".into(),
|
||||
sha256: "y".into(),
|
||||
size_bytes: 2,
|
||||
blake3: None,
|
||||
@@ -2218,6 +2218,8 @@ mod tests {
|
||||
label: "Server 1 (OVH)".to_string(),
|
||||
},
|
||||
UpdateMirror {
|
||||
// Deliberately the retired host: this fixture exists to prove
|
||||
// load_mirrors strips it.
|
||||
url: "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/manifest.json"
|
||||
.to_string(),
|
||||
label: "Server 2 (tx1138)".to_string(),
|
||||
@@ -2406,7 +2408,7 @@ mod tests {
|
||||
rollback_available: false,
|
||||
schedule: UpdateSchedule::Manual,
|
||||
manifest_mirror: Some(
|
||||
"https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/manifest.json"
|
||||
"https://releases.example.com/lfg2025/archy/raw/branch/main/releases/manifest.json"
|
||||
.to_string(),
|
||||
),
|
||||
manifest_signed: false,
|
||||
|
||||
@@ -885,8 +885,9 @@ mod tests {
|
||||
assert!(!image_uses_insecure_registry(
|
||||
"23.182.128.160:3000/lfg2025/filebrowser:v2.27.0"
|
||||
));
|
||||
// HTTPS registries never match the insecure list.
|
||||
assert!(!image_uses_insecure_registry(
|
||||
"git.tx1138.com/lfg2025/bitcoin-knots:latest"
|
||||
"ghcr.io/lfg2025/bitcoin-knots:latest"
|
||||
));
|
||||
assert!(!image_uses_insecure_registry(
|
||||
"docker.io/library/nginx:latest"
|
||||
|
||||
Reference in New Issue
Block a user