launchpad: drive the endpoints from data/launchpad.yml
This commit is contained in:
@@ -0,0 +1,26 @@
|
|||||||
|
## Launchpad service endpoints: the REST API, the PPA SFTP upload queue,
|
||||||
|
## the PPA package-content host and the Ubuntu source-package git web UI.
|
||||||
|
## Like host_keys.yml, this file exists so that static endpoints are data,
|
||||||
|
## updatable in one reviewable place, instead of hardcoded in the source —
|
||||||
|
## the API and content URLs were previously triplicated across modules.
|
||||||
|
##
|
||||||
|
## Templates carry their variable parts as {name} placeholders ({owner},
|
||||||
|
## {ppa}, {package}), substituted by the accessors of src/launchpad.rs
|
||||||
|
## with plain string replacement.
|
||||||
|
##
|
||||||
|
## Where the values come from:
|
||||||
|
## api_base: the Launchpad REST API root (https://launchpad.net/docs/api/)
|
||||||
|
## ssh_*: the PPA upload queue, as expanded by dput-ng's
|
||||||
|
## ppa:user/ppa profile (ppa.launchpad.net:22, incoming
|
||||||
|
## ~<user>/<ppa>)
|
||||||
|
## content_host_template: ppa.launchpadcontent.net serves PPA apt
|
||||||
|
## repositories since the 2022 move off ppa.launchpad.net
|
||||||
|
## git_web_template: Launchpad's CGit mirrors of Ubuntu source packages
|
||||||
|
## (git.launchpad.net/ubuntu/+source/<package>)
|
||||||
|
|
||||||
|
api_base: https://api.launchpad.net/1.0
|
||||||
|
ssh_host: ppa.launchpad.net
|
||||||
|
ssh_port: 22
|
||||||
|
incoming_template: "~{owner}/{ppa}"
|
||||||
|
content_host_template: https://ppa.launchpadcontent.net/{owner}/{ppa}/ubuntu
|
||||||
|
git_web_template: https://git.launchpad.net/ubuntu/+source/{package}
|
||||||
+2
-2
@@ -851,7 +851,7 @@ fn parse_ppa_url(ppa_base_url: &str) -> Option<(String, String)> {
|
|||||||
.strip_prefix("https://")
|
.strip_prefix("https://")
|
||||||
.or_else(|| ppa_base_url.strip_prefix("http://"))?;
|
.or_else(|| ppa_base_url.strip_prefix("http://"))?;
|
||||||
let (host, path) = rest.split_once('/')?;
|
let (host, path) = rest.split_once('/')?;
|
||||||
if host != "ppa.launchpadcontent.net" {
|
if host != crate::launchpad::ppa_content_host() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -886,7 +886,7 @@ pub async fn ppa_keyring_bytes(
|
|||||||
return Ok(cached.clone());
|
return Ok(cached.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let api_url = format!("https://api.launchpad.net/1.0/~{owner}/+archive/ubuntu/{name}");
|
let api_url = crate::launchpad::archive_url(&owner, &name);
|
||||||
let response = crate::distro_info::http_get_retried(&api_url).await?;
|
let response = crate::distro_info::http_get_retried(&api_url).await?;
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
|
|||||||
+3
-2
@@ -7,8 +7,9 @@
|
|||||||
//!
|
//!
|
||||||
//! This module is deliberately not a central registry: each file is
|
//! This module is deliberately not a central registry: each file is
|
||||||
//! embedded by the module that owns it (distro_info.rs owns
|
//! embedded by the module that owns it (distro_info.rs owns
|
||||||
//! data/distro_info.yml, put/ssh.rs owns data/host_keys.yml, quirks.rs
|
//! data/distro_info.yml, launchpad.rs owns data/launchpad.yml, put/ssh.rs
|
||||||
//! owns data/quirks.yml) through the [`embed_data!`] macro below, so data
|
//! owns data/host_keys.yml, quirks.rs owns data/quirks.yml) through the
|
||||||
|
//! [`embed_data!`] macro below, so data
|
||||||
//! and its accessors stay together and a diff touching one domain cannot
|
//! and its accessors stay together and a diff touching one domain cannot
|
||||||
//! half-touch another. The macro embeds the file at compile time and
|
//! half-touch another. The macro embeds the file at compile time and
|
||||||
//! parses it once into a `lazy_static` on first use; since the data ships
|
//! parses it once into a `lazy_static` on first use; since the data ships
|
||||||
|
|||||||
+84
-8
@@ -23,13 +23,69 @@ use std::path::Path;
|
|||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::data::embed_data;
|
||||||
use crate::put::target::UploadTarget;
|
use crate::put::target::UploadTarget;
|
||||||
|
|
||||||
/// Git configuration key holding the Launchpad account name
|
/// Git configuration key holding the Launchpad account name
|
||||||
const LP_USER_KEY: &str = "lp.user";
|
const LP_USER_KEY: &str = "lp.user";
|
||||||
|
|
||||||
|
/// Launchpad service endpoints, loaded from the bundled `launchpad.yml`
|
||||||
|
/// data file (same pattern as `distro_info.yml`): static endpoints that
|
||||||
|
/// change with Launchpad, not with the code, are data — several of them
|
||||||
|
/// were previously duplicated across three modules.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct LaunchpadData {
|
||||||
|
/// Base URL of the Launchpad REST API
|
||||||
|
api_base: String,
|
||||||
|
/// Host of the PPA SFTP upload server
|
||||||
|
ssh_host: String,
|
||||||
|
/// Port of the PPA SFTP upload server
|
||||||
|
ssh_port: u16,
|
||||||
|
/// Upload queue incoming directory template (`{owner}`/`{ppa}`)
|
||||||
|
incoming_template: String,
|
||||||
|
/// PPA package-content (apt repository) URL template
|
||||||
|
content_host_template: String,
|
||||||
|
/// Ubuntu source-package git web URL template (`{package}`)
|
||||||
|
git_web_template: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
embed_data! {
|
||||||
|
static ref LAUNCHPAD_DATA: LaunchpadData = "../data/launchpad.yml"
|
||||||
|
}
|
||||||
|
|
||||||
/// Base URL of the Launchpad REST API
|
/// Base URL of the Launchpad REST API
|
||||||
const API_BASE: &str = "https://api.launchpad.net/1.0";
|
fn api_base() -> &'static str {
|
||||||
|
&LAUNCHPAD_DATA.api_base
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Host serving PPA package content, derived from the content-host
|
||||||
|
/// template so the URL builders and the URL parsers of PPA addresses
|
||||||
|
/// cannot drift apart
|
||||||
|
pub(crate) fn ppa_content_host() -> &'static str {
|
||||||
|
let template = LAUNCHPAD_DATA.content_host_template.as_str();
|
||||||
|
let after_scheme = template
|
||||||
|
.split_once("://")
|
||||||
|
.map_or(template, |(_, rest)| rest);
|
||||||
|
after_scheme.split('/').next().unwrap_or(after_scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Base URL of the apt repository serving a PPA's packages
|
||||||
|
/// (e.g. `https://ppa.launchpadcontent.net/user/ppa/ubuntu`)
|
||||||
|
pub(crate) fn ppa_content_url(owner: &str, ppa: &str) -> String {
|
||||||
|
LAUNCHPAD_DATA
|
||||||
|
.content_host_template
|
||||||
|
.replace("{owner}", owner)
|
||||||
|
.replace("{ppa}", ppa)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// URL of the Launchpad git repository of an Ubuntu source package
|
||||||
|
/// (`git.launchpad.net/ubuntu/+source/<package>`), the preferred VCS of
|
||||||
|
/// Ubuntu packages
|
||||||
|
pub(crate) fn ubuntu_source_git_url(package: &str) -> String {
|
||||||
|
LAUNCHPAD_DATA
|
||||||
|
.git_web_template
|
||||||
|
.replace("{package}", package)
|
||||||
|
}
|
||||||
|
|
||||||
/// Page size (`ws.size`) asked from Launchpad collections. Launchpad
|
/// Page size (`ws.size`) asked from Launchpad collections. Launchpad
|
||||||
/// truncates collection answers at 75 entries by default and rejects
|
/// truncates collection answers at 75 entries by default and rejects
|
||||||
@@ -88,27 +144,30 @@ fn split_ppa(ppa: &str) -> Result<(String, String), String> {
|
|||||||
|
|
||||||
/// URL of the Launchpad API resource of a Launchpad account
|
/// URL of the Launchpad API resource of a Launchpad account
|
||||||
fn person_url(user: &str) -> String {
|
fn person_url(user: &str) -> String {
|
||||||
format!("{API_BASE}/~{user}")
|
format!("{}/~{user}", api_base())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// URL of the Launchpad API resource of a PPA (`~user/+archive/ubuntu/name`
|
/// URL of the Launchpad API resource of a PPA (`~user/+archive/ubuntu/name`
|
||||||
/// covers the default `ppa` archive and named archives alike); shared by the
|
/// covers the default `ppa` archive and named archives alike); shared by the
|
||||||
/// put-side pre-flight checks and the apt keyring's fingerprint lookup
|
/// put-side pre-flight checks and the apt keyring's fingerprint lookup
|
||||||
pub(crate) fn archive_url(user: &str, ppa: &str) -> String {
|
pub(crate) fn archive_url(user: &str, ppa: &str) -> String {
|
||||||
format!("{API_BASE}/~{user}/+archive/ubuntu/{ppa}")
|
format!("{}/~{user}/+archive/ubuntu/{ppa}", api_base())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a `user/ppa_name` PPA argument into its upload target
|
/// Resolve a `user/ppa_name` PPA argument into its upload target (the
|
||||||
/// (`ppa.launchpad.net`, incoming `~user/ppa_name`), like dput-ng's
|
/// SFTP host and incoming template of `launchpad.yml`), like dput-ng's
|
||||||
/// `ppa:user/ppa` profile expansion.
|
/// `ppa:user/ppa` profile expansion.
|
||||||
pub fn ppa_target(ppa: &str) -> Result<UploadTarget, String> {
|
pub fn ppa_target(ppa: &str) -> Result<UploadTarget, String> {
|
||||||
let (user, name) = split_ppa(ppa)?;
|
let (user, name) = split_ppa(ppa)?;
|
||||||
|
|
||||||
Ok(UploadTarget {
|
Ok(UploadTarget {
|
||||||
fqdn: "ppa.launchpad.net".to_string(),
|
fqdn: LAUNCHPAD_DATA.ssh_host.clone(),
|
||||||
port: 22,
|
port: LAUNCHPAD_DATA.ssh_port,
|
||||||
login: None,
|
login: None,
|
||||||
incoming: format!("~{user}/{name}"),
|
incoming: LAUNCHPAD_DATA
|
||||||
|
.incoming_template
|
||||||
|
.replace("{owner}", &user)
|
||||||
|
.replace("{ppa}", &name),
|
||||||
label: format!("ppa:{ppa}"),
|
label: format!("ppa:{ppa}"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -425,6 +484,23 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The data-driven endpoint accessors build the same addresses the
|
||||||
|
/// former hardcoded constants carried (each verified against the live
|
||||||
|
/// service), and the content host is derived from the same template
|
||||||
|
/// the content URLs are built from
|
||||||
|
#[test]
|
||||||
|
fn data_driven_endpoints_match_the_service() {
|
||||||
|
assert_eq!(
|
||||||
|
ppa_content_url("vhaudiquet", "noctalia"),
|
||||||
|
"https://ppa.launchpadcontent.net/vhaudiquet/noctalia/ubuntu"
|
||||||
|
);
|
||||||
|
assert_eq!(ppa_content_host(), "ppa.launchpadcontent.net");
|
||||||
|
assert_eq!(
|
||||||
|
ubuntu_source_git_url("hello"),
|
||||||
|
"https://git.launchpad.net/ubuntu/+source/hello"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// The API answer carries many unrelated fields; deserialization must
|
/// The API answer carries many unrelated fields; deserialization must
|
||||||
/// pick the relevant ones and tolerate a null `enabled`
|
/// pick the relevant ones and tolerate a null `enabled`
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+2
-2
@@ -18,11 +18,11 @@ use log::{debug, warn};
|
|||||||
/// # Returns
|
/// # Returns
|
||||||
/// * The base URL for the PPA (e.g., "https://ppa.launchpadcontent.net/user/ppa_name/ubuntu/")
|
/// * The base URL for the PPA (e.g., "https://ppa.launchpadcontent.net/user/ppa_name/ubuntu/")
|
||||||
pub fn ppa_to_base_url(user: &str, name: &str) -> String {
|
pub fn ppa_to_base_url(user: &str, name: &str) -> String {
|
||||||
format!("https://ppa.launchpadcontent.net/{}/{}/ubuntu", user, name)
|
crate::launchpad::ppa_content_url(user, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_launchpad_repo_sync(package: &str) -> Result<Option<String>, String> {
|
fn check_launchpad_repo_sync(package: &str) -> Result<Option<String>, String> {
|
||||||
let url = format!("https://git.launchpad.net/ubuntu/+source/{}", package);
|
let url = crate::launchpad::ubuntu_source_git_url(package);
|
||||||
|
|
||||||
// Use libgit2 to check if the remote repository exists
|
// Use libgit2 to check if the remote repository exists
|
||||||
// This is more reliable than HTTP HEAD requests when CGIt is disabled
|
// This is more reliable than HTTP HEAD requests when CGIt is disabled
|
||||||
|
|||||||
Reference in New Issue
Block a user