Feature/new registry (#2612)

* wip

* overhaul boot process

* wip: new registry

* wip

* wip

* wip

* wip

* wip

* wip

* os registry complete

* ui fixes

* fixes

* fixes

* more fixes

* fix merkle archive
This commit is contained in:
Aiden McClelland
2024-05-06 10:20:44 -06:00
committed by GitHub
parent 8a38666105
commit 9b14d714ca
167 changed files with 6297 additions and 3190 deletions

View File

@@ -0,0 +1,341 @@
use std::collections::BTreeMap;
use std::panic::UnwindSafe;
use std::path::PathBuf;
use std::time::Duration;
use axum::response::Response;
use clap::Parser;
use futures::{FutureExt, TryStreamExt};
use helpers::NonDetachingJoinHandle;
use imbl_value::InternedString;
use itertools::Itertools;
use rpc_toolkit::{from_fn_async, CallRemote, Context, HandlerArgs, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha512};
use ts_rs::TS;
use url::Url;
use crate::context::CliContext;
use crate::prelude::*;
use crate::progress::{FullProgressTracker, PhasedProgressBar};
use crate::registry::asset::RegistryAsset;
use crate::registry::context::RegistryContext;
use crate::registry::os::index::OsVersionInfo;
use crate::registry::os::SIG_CONTEXT;
use crate::registry::signer::{Blake3Ed25519Signature, Signature, SignatureInfo, SignerKey};
use crate::rpc_continuations::{RequestGuid, RpcContinuation};
use crate::s9pk::merkle_archive::source::ArchiveSource;
use crate::util::{Apply, Version};
pub fn add_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"iso",
from_fn_async(add_iso)
.with_metadata("getSigner", Value::Bool(true))
.no_cli(),
)
.subcommand(
"img",
from_fn_async(add_img)
.with_metadata("getSigner", Value::Bool(true))
.no_cli(),
)
.subcommand(
"squashfs",
from_fn_async(add_squashfs)
.with_metadata("getSigner", Value::Bool(true))
.no_cli(),
)
}
#[derive(Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct AddAssetParams {
#[ts(type = "string")]
pub url: Url,
pub signature: Signature,
#[ts(type = "string")]
pub version: Version,
#[ts(type = "string")]
pub platform: InternedString,
#[serde(default)]
pub upload: bool,
#[serde(rename = "__auth_signer")]
pub signer: SignerKey,
}
async fn add_asset(
ctx: RegistryContext,
AddAssetParams {
url,
signature,
version,
platform,
upload,
signer,
}: AddAssetParams,
accessor: impl FnOnce(&mut Model<OsVersionInfo>) -> &mut Model<BTreeMap<InternedString, RegistryAsset>>
+ UnwindSafe
+ Send,
) -> Result<Option<RequestGuid>, Error> {
ensure_code!(
signature.signer() == signer,
ErrorKind::InvalidSignature,
"asset signature does not match request signer"
);
ctx.db
.mutate(|db| {
let signer_guid = db.as_index().as_signers().get_signer(&signer)?;
if db
.as_index()
.as_os()
.as_versions()
.as_idx(&version)
.or_not_found(&version)?
.as_signers()
.de()?
.contains(&signer_guid)
{
accessor(
db.as_index_mut()
.as_os_mut()
.as_versions_mut()
.as_idx_mut(&version)
.or_not_found(&version)?,
)
.upsert(&platform, || RegistryAsset {
url,
signature_info: SignatureInfo::new(SIG_CONTEXT),
})?
.as_signature_info_mut()
.mutate(|s| s.add_sig(&signature))?;
Ok(())
} else {
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
}
})
.await?;
let guid = if upload {
let guid = RequestGuid::new();
let auth_guid = guid.clone();
let signer = signature.signer();
let hostname = ctx.hostname.clone();
ctx.rpc_continuations
.add(
guid.clone(),
RpcContinuation::rest(
Box::new(|req| {
async move {
Ok(
if async move {
let auth_sig = base64::decode(
req.headers().get("X-StartOS-Registry-Auth-Sig")?,
)
.ok()?;
signer
.verify_message(
auth_guid.as_ref().as_bytes(),
&auth_sig,
&hostname,
)
.ok()?;
Some(())
}
.await
.is_some()
{
Response::builder()
.status(200)
.body(axum::body::Body::empty())
.with_kind(ErrorKind::Network)?
} else {
Response::builder()
.status(401)
.body(axum::body::Body::empty())
.with_kind(ErrorKind::Network)?
},
)
}
.boxed()
}),
Duration::from_secs(30),
),
)
.await;
Some(guid)
} else {
None
};
Ok(guid)
}
pub async fn add_iso(
ctx: RegistryContext,
params: AddAssetParams,
) -> Result<Option<RequestGuid>, Error> {
add_asset(ctx, params, |m| m.as_iso_mut()).await
}
pub async fn add_img(
ctx: RegistryContext,
params: AddAssetParams,
) -> Result<Option<RequestGuid>, Error> {
add_asset(ctx, params, |m| m.as_img_mut()).await
}
pub async fn add_squashfs(
ctx: RegistryContext,
params: AddAssetParams,
) -> Result<Option<RequestGuid>, Error> {
add_asset(ctx, params, |m| m.as_squashfs_mut()).await
}
#[derive(Debug, Deserialize, Serialize, Parser)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliAddAssetParams {
#[arg(short = 'p', long = "platform")]
pub platform: InternedString,
#[arg(short = 'v', long = "version")]
pub version: Version,
pub file: PathBuf,
pub url: Url,
#[arg(short = 'u', long = "upload")]
pub upload: bool,
}
pub async fn cli_add_asset(
HandlerArgs {
context: ctx,
parent_method,
method,
params:
CliAddAssetParams {
platform,
version,
file: path,
url,
upload,
},
..
}: HandlerArgs<CliContext, CliAddAssetParams>,
) -> Result<(), Error> {
let ext = match path.extension().and_then(|e| e.to_str()) {
Some("iso") => "iso",
Some("img") => "img",
Some("squashfs") => "squashfs",
_ => {
return Err(Error::new(
eyre!("Unknown extension"),
ErrorKind::InvalidRequest,
))
}
};
let file = tokio::fs::File::open(&path).await?.into();
let mut progress = FullProgressTracker::new();
let progress_handle = progress.handle();
let mut sign_phase =
progress_handle.add_phase(InternedString::intern("Signing File"), Some(10));
let mut index_phase = progress_handle.add_phase(
InternedString::intern("Adding File to Registry Index"),
Some(1),
);
let mut upload_phase = if upload {
Some(progress_handle.add_phase(InternedString::intern("Uploading File"), Some(100)))
} else {
None
};
let progress_task: NonDetachingJoinHandle<()> = tokio::spawn(async move {
let mut bar = PhasedProgressBar::new(&format!("Adding {} to registry...", path.display()));
loop {
let snap = progress.snapshot();
bar.update(&snap);
if snap.overall.is_complete() {
break;
}
progress.changed().await
}
})
.into();
sign_phase.start();
let blake3_sig =
Blake3Ed25519Signature::sign_file(ctx.developer_key()?, &file, SIG_CONTEXT).await?;
let size = blake3_sig.size;
let signature = Signature::Blake3Ed25519(blake3_sig);
sign_phase.complete();
index_phase.start();
let add_res = from_value::<Option<RequestGuid>>(
ctx.call_remote::<RegistryContext>(
&parent_method
.into_iter()
.chain(method)
.chain([ext])
.join("."),
imbl_value::json!({
"platform": platform,
"version": version,
"url": &url,
"signature": signature,
"upload": upload,
}),
)
.await?,
)?;
index_phase.complete();
if let Some(guid) = add_res {
upload_phase.as_mut().map(|p| p.start());
upload_phase.as_mut().map(|p| p.set_total(size));
let reg_url = ctx.registry_url.as_ref().or_not_found("--registry")?;
ctx.client
.post(url)
.header("X-StartOS-Registry-Token", guid.as_ref())
.header(
"X-StartOS-Registry-Auth-Sig",
base64::encode(
ctx.developer_key()?
.sign_prehashed(
Sha512::new_with_prefix(guid.as_ref().as_bytes()),
Some(
reg_url
.host()
.or_not_found("registry hostname")?
.to_string()
.as_bytes(),
),
)?
.to_bytes(),
),
)
.body(reqwest::Body::wrap_stream(
tokio_util::io::ReaderStream::new(file.fetch(0, size).await?).inspect_ok(
move |b| {
upload_phase
.as_mut()
.map(|p| *p += b.len() as u64)
.apply(|_| ())
},
),
))
.send()
.await?;
// upload_phase.as_mut().map(|p| p.complete());
}
progress_handle.complete();
progress_task.await.with_kind(ErrorKind::Unknown)?;
Ok(())
}

View File

@@ -0,0 +1,182 @@
use std::collections::BTreeMap;
use std::panic::UnwindSafe;
use std::path::{Path, PathBuf};
use clap::Parser;
use helpers::{AtomicFile, NonDetachingJoinHandle};
use imbl_value::{json, InternedString};
use itertools::Itertools;
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::context::CliContext;
use crate::prelude::*;
use crate::progress::{FullProgressTracker, PhasedProgressBar};
use crate::registry::asset::RegistryAsset;
use crate::registry::context::RegistryContext;
use crate::registry::os::index::OsVersionInfo;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::util::Version;
pub fn get_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand("iso", from_fn_async(get_iso).no_cli())
.subcommand("iso", from_fn_async(cli_get_os_asset).no_display())
.subcommand("img", from_fn_async(get_img).no_cli())
.subcommand("img", from_fn_async(cli_get_os_asset).no_display())
.subcommand("squashfs", from_fn_async(get_squashfs).no_cli())
.subcommand("squashfs", from_fn_async(cli_get_os_asset).no_display())
}
#[derive(Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct GetOsAssetParams {
#[ts(type = "string")]
pub version: Version,
#[ts(type = "string")]
pub platform: InternedString,
}
async fn get_os_asset(
ctx: RegistryContext,
GetOsAssetParams { version, platform }: GetOsAssetParams,
accessor: impl FnOnce(&Model<OsVersionInfo>) -> &Model<BTreeMap<InternedString, RegistryAsset>>
+ UnwindSafe
+ Send,
) -> Result<RegistryAsset, Error> {
accessor(
ctx.db
.peek()
.await
.as_index()
.as_os()
.as_versions()
.as_idx(&version)
.or_not_found(&version)?,
)
.as_idx(&platform)
.or_not_found(&platform)?
.de()
}
pub async fn get_iso(
ctx: RegistryContext,
params: GetOsAssetParams,
) -> Result<RegistryAsset, Error> {
get_os_asset(ctx, params, |info| info.as_iso()).await
}
pub async fn get_img(
ctx: RegistryContext,
params: GetOsAssetParams,
) -> Result<RegistryAsset, Error> {
get_os_asset(ctx, params, |info| info.as_img()).await
}
pub async fn get_squashfs(
ctx: RegistryContext,
params: GetOsAssetParams,
) -> Result<RegistryAsset, Error> {
get_os_asset(ctx, params, |info| info.as_squashfs()).await
}
#[derive(Debug, Deserialize, Serialize, Parser)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliGetOsAssetParams {
pub version: Version,
pub platform: InternedString,
#[arg(long = "download", short = 'd')]
pub download: Option<PathBuf>,
#[arg(
long = "reverify",
short = 'r',
help = "verify the hash of the file a second time after download"
)]
pub reverify: bool,
}
async fn cli_get_os_asset(
HandlerArgs {
context: ctx,
parent_method,
method,
params:
CliGetOsAssetParams {
version,
platform,
download,
reverify,
},
..
}: HandlerArgs<CliContext, CliGetOsAssetParams>,
) -> Result<RegistryAsset, Error> {
let res = from_value::<RegistryAsset>(
ctx.call_remote::<RegistryContext>(
&parent_method.into_iter().chain(method).join("."),
json!({
"version": version,
"platform": platform,
}),
)
.await?,
)?;
let validator = res.validate(res.signature_info.all_signers())?;
if let Some(download) = download {
let mut file = AtomicFile::new(&download, None::<&Path>)
.await
.with_kind(ErrorKind::Filesystem)?;
let mut progress = FullProgressTracker::new();
let progress_handle = progress.handle();
let mut download_phase =
progress_handle.add_phase(InternedString::intern("Downloading File"), Some(100));
download_phase.set_total(validator.size()?);
let reverify_phase = if reverify {
Some(progress_handle.add_phase(InternedString::intern("Reverifying File"), Some(10)))
} else {
None
};
let progress_task: NonDetachingJoinHandle<()> = tokio::spawn(async move {
let mut bar = PhasedProgressBar::new("Downloading...");
loop {
let snap = progress.snapshot();
bar.update(&snap);
if snap.overall.is_complete() {
break;
}
progress.changed().await
}
})
.into();
download_phase.start();
let mut download_writer = download_phase.writer(&mut *file);
res.download(ctx.client.clone(), &mut download_writer, &validator)
.await?;
let (_, mut download_phase) = download_writer.into_inner();
file.save().await.with_kind(ErrorKind::Filesystem)?;
download_phase.complete();
if let Some(mut reverify_phase) = reverify_phase {
reverify_phase.start();
validator
.validate_file(&MultiCursorFile::from(
tokio::fs::File::open(download).await?,
))
.await?;
reverify_phase.complete();
}
progress_handle.complete();
progress_task.await.with_kind(ErrorKind::Unknown)?;
}
Ok(res)
}

View File

@@ -0,0 +1,14 @@
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
pub mod add;
pub mod get;
pub mod sign;
pub fn asset_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand("add", add::add_api::<C>())
.subcommand("add", from_fn_async(add::cli_add_asset).no_display())
.subcommand("sign", sign::sign_api::<C>())
.subcommand("sign", from_fn_async(sign::cli_sign_asset).no_display())
.subcommand("get", get::get_api::<C>())
}

View File

@@ -0,0 +1,188 @@
use std::collections::BTreeMap;
use std::panic::UnwindSafe;
use std::path::PathBuf;
use clap::Parser;
use helpers::NonDetachingJoinHandle;
use imbl_value::InternedString;
use itertools::Itertools;
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::context::CliContext;
use crate::prelude::*;
use crate::progress::{FullProgressTracker, PhasedProgressBar};
use crate::registry::asset::RegistryAsset;
use crate::registry::context::RegistryContext;
use crate::registry::os::index::OsVersionInfo;
use crate::registry::os::SIG_CONTEXT;
use crate::registry::signer::{Blake3Ed25519Signature, Signature};
use crate::util::Version;
pub fn sign_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand("iso", from_fn_async(sign_iso).no_cli())
.subcommand("img", from_fn_async(sign_img).no_cli())
.subcommand("squashfs", from_fn_async(sign_squashfs).no_cli())
}
#[derive(Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct SignAssetParams {
#[ts(type = "string")]
version: Version,
#[ts(type = "string")]
platform: InternedString,
signature: Signature,
}
async fn sign_asset(
ctx: RegistryContext,
SignAssetParams {
version,
platform,
signature,
}: SignAssetParams,
accessor: impl FnOnce(&mut Model<OsVersionInfo>) -> &mut Model<BTreeMap<InternedString, RegistryAsset>>
+ UnwindSafe
+ Send,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
let guid = db.as_index().as_signers().get_signer(&signature.signer())?;
if !db
.as_index()
.as_os()
.as_versions()
.as_idx(&version)
.or_not_found(&version)?
.as_signers()
.de()?
.contains(&guid)
{
return Err(Error::new(
eyre!("signer {guid} is not authorized"),
ErrorKind::Authorization,
));
}
accessor(
db.as_index_mut()
.as_os_mut()
.as_versions_mut()
.as_idx_mut(&version)
.or_not_found(&version)?,
)
.as_idx_mut(&platform)
.or_not_found(&platform)?
.as_signature_info_mut()
.mutate(|s| s.add_sig(&signature))?;
Ok(())
})
.await
}
pub async fn sign_iso(ctx: RegistryContext, params: SignAssetParams) -> Result<(), Error> {
sign_asset(ctx, params, |m| m.as_iso_mut()).await
}
pub async fn sign_img(ctx: RegistryContext, params: SignAssetParams) -> Result<(), Error> {
sign_asset(ctx, params, |m| m.as_img_mut()).await
}
pub async fn sign_squashfs(ctx: RegistryContext, params: SignAssetParams) -> Result<(), Error> {
sign_asset(ctx, params, |m| m.as_squashfs_mut()).await
}
#[derive(Debug, Deserialize, Serialize, Parser)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliSignAssetParams {
#[arg(short = 'p', long = "platform")]
pub platform: InternedString,
#[arg(short = 'v', long = "version")]
pub version: Version,
pub file: PathBuf,
}
pub async fn cli_sign_asset(
HandlerArgs {
context: ctx,
parent_method,
method,
params:
CliSignAssetParams {
platform,
version,
file: path,
},
..
}: HandlerArgs<CliContext, CliSignAssetParams>,
) -> Result<(), Error> {
let ext = match path.extension().and_then(|e| e.to_str()) {
Some("iso") => "iso",
Some("img") => "img",
Some("squashfs") => "squashfs",
_ => {
return Err(Error::new(
eyre!("Unknown extension"),
ErrorKind::InvalidRequest,
))
}
};
let file = tokio::fs::File::open(&path).await?.into();
let mut progress = FullProgressTracker::new();
let progress_handle = progress.handle();
let mut sign_phase =
progress_handle.add_phase(InternedString::intern("Signing File"), Some(10));
let mut index_phase = progress_handle.add_phase(
InternedString::intern("Adding Signature to Registry Index"),
Some(1),
);
let progress_task: NonDetachingJoinHandle<()> = tokio::spawn(async move {
let mut bar = PhasedProgressBar::new(&format!("Adding {} to registry...", path.display()));
loop {
let snap = progress.snapshot();
bar.update(&snap);
if snap.overall.is_complete() {
break;
}
progress.changed().await
}
})
.into();
sign_phase.start();
let blake3_sig =
Blake3Ed25519Signature::sign_file(ctx.developer_key()?, &file, SIG_CONTEXT).await?;
let signature = Signature::Blake3Ed25519(blake3_sig);
sign_phase.complete();
index_phase.start();
ctx.call_remote::<RegistryContext>(
&parent_method
.into_iter()
.chain(method)
.chain([ext])
.join("."),
imbl_value::json!({
"platform": platform,
"version": version,
"signature": signature,
}),
)
.await?;
index_phase.complete();
progress_handle.complete();
progress_task.await.with_kind(ErrorKind::Unknown)?;
Ok(())
}

View File

@@ -0,0 +1,44 @@
use std::collections::{BTreeMap, BTreeSet};
use emver::VersionRange;
use imbl_value::InternedString;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::prelude::*;
use crate::registry::asset::RegistryAsset;
use crate::registry::context::RegistryContext;
use crate::rpc_continuations::RequestGuid;
use crate::util::Version;
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct OsIndex {
#[ts(as = "BTreeMap::<String, OsVersionInfo>")]
pub versions: BTreeMap<Version, OsVersionInfo>,
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct OsVersionInfo {
pub headline: String,
pub release_notes: String,
#[ts(type = "string")]
pub source_version: VersionRange,
#[ts(type = "string[]")]
pub signers: BTreeSet<RequestGuid>,
#[ts(as = "BTreeMap::<String, RegistryAsset>")]
pub iso: BTreeMap<InternedString, RegistryAsset>, // platform (i.e. x86_64-nonfree) -> asset
#[ts(as = "BTreeMap::<String, RegistryAsset>")]
pub squashfs: BTreeMap<InternedString, RegistryAsset>, // platform (i.e. x86_64-nonfree) -> asset
#[ts(as = "BTreeMap::<String, RegistryAsset>")]
pub img: BTreeMap<InternedString, RegistryAsset>, // platform (i.e. raspberrypi) -> asset
}
pub async fn get_os_index(ctx: RegistryContext) -> Result<OsIndex, Error> {
ctx.db.peek().await.into_index().into_os().de()
}

View File

@@ -0,0 +1,22 @@
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
use crate::context::CliContext;
use crate::util::serde::HandlerExtSerde;
pub const SIG_CONTEXT: &str = "startos";
pub mod asset;
pub mod index;
pub mod version;
pub fn os_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"index",
from_fn_async(index::get_os_index)
.with_display_serializable()
.with_call_remote::<CliContext>(),
)
.subcommand("asset", asset::asset_api::<C>())
.subcommand("version", version::version_api::<C>())
}

View File

@@ -0,0 +1,183 @@
use std::collections::BTreeMap;
use clap::Parser;
use emver::VersionRange;
use itertools::Itertools;
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::context::CliContext;
use crate::prelude::*;
use crate::registry::context::RegistryContext;
use crate::registry::os::index::OsVersionInfo;
use crate::registry::signer::SignerKey;
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
use crate::util::Version;
pub mod signer;
pub fn version_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"add",
from_fn_async(add_version)
.with_metadata("admin", Value::Bool(true))
.with_metadata("getSigner", Value::Bool(true))
.no_display()
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove_version)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_call_remote::<CliContext>(),
)
.subcommand("signer", signer::signer_api::<C>())
.subcommand(
"get",
from_fn_async(get_version)
.with_display_serializable()
.with_custom_display_fn(|handle, result| {
Ok(display_version_info(handle.params, result))
})
.with_call_remote::<CliContext>(),
)
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct AddVersionParams {
#[ts(type = "string")]
pub version: Version,
pub headline: String,
pub release_notes: String,
#[ts(type = "string")]
pub source_version: VersionRange,
#[arg(skip)]
#[ts(skip)]
#[serde(rename = "__auth_signer")]
pub signer: Option<SignerKey>,
}
pub async fn add_version(
ctx: RegistryContext,
AddVersionParams {
version,
headline,
release_notes,
source_version,
signer,
}: AddVersionParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
let signer = signer
.map(|s| db.as_index().as_signers().get_signer(&s))
.transpose()?;
db.as_index_mut()
.as_os_mut()
.as_versions_mut()
.upsert(&version, || OsVersionInfo::default())?
.mutate(|i| {
i.headline = headline;
i.release_notes = release_notes;
i.source_version = source_version;
i.signers.extend(signer);
Ok(())
})
})
.await
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemoveVersionParams {
#[ts(type = "string")]
pub version: Version,
}
pub async fn remove_version(
ctx: RegistryContext,
RemoveVersionParams { version }: RemoveVersionParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_index_mut()
.as_os_mut()
.as_versions_mut()
.remove(&version)?;
Ok(())
})
.await
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct GetVersionParams {
#[ts(type = "string | null")]
#[arg(long = "src")]
pub source: Option<Version>,
#[ts(type = "string | null")]
#[arg(long = "target")]
pub target: Option<VersionRange>,
}
pub async fn get_version(
ctx: RegistryContext,
GetVersionParams { source, target }: GetVersionParams,
) -> Result<BTreeMap<Version, OsVersionInfo>, Error> {
let target = target.unwrap_or(VersionRange::Any);
ctx.db
.peek()
.await
.into_index()
.into_os()
.into_versions()
.into_entries()?
.into_iter()
.map(|(v, i)| i.de().map(|i| (v, i)))
.filter_ok(|(version, info)| {
version.satisfies(&target)
&& source
.as_ref()
.map_or(true, |s| s.satisfies(&info.source_version))
})
.collect()
}
pub fn display_version_info<T>(params: WithIoFormat<T>, info: BTreeMap<Version, OsVersionInfo>) {
use prettytable::*;
if let Some(format) = params.format {
return display_serializable(format, info);
}
let mut table = Table::new();
table.add_row(row![bc =>
"VERSION",
"HEADLINE",
"RELEASE NOTES",
"ISO PLATFORMS",
"IMG PLATFORMS",
"SQUASHFS PLATFORMS",
]);
for (version, info) in &info {
table.add_row(row![
version.as_str(),
&info.headline,
&info.release_notes,
&info.iso.keys().into_iter().join(", "),
&info.img.keys().into_iter().join(", "),
&info.squashfs.keys().into_iter().join(", "),
]);
}
table.print_tty(false).unwrap();
}

View File

@@ -0,0 +1,133 @@
use std::collections::BTreeMap;
use clap::Parser;
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::context::CliContext;
use crate::prelude::*;
use crate::registry::admin::display_signers;
use crate::registry::context::RegistryContext;
use crate::registry::signer::SignerInfo;
use crate::rpc_continuations::RequestGuid;
use crate::util::serde::HandlerExtSerde;
use crate::util::Version;
pub fn signer_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"add",
from_fn_async(add_version_signer)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove_version_signer)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_call_remote::<CliContext>(),
)
.subcommand(
"list",
from_fn_async(list_version_signers)
.with_display_serializable()
.with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result)))
.with_call_remote::<CliContext>(),
)
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct VersionSignerParams {
#[ts(type = "string")]
pub version: Version,
#[ts(type = "string")]
pub signer: RequestGuid,
}
pub async fn add_version_signer(
ctx: RegistryContext,
VersionSignerParams { version, signer }: VersionSignerParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
ensure_code!(
db.as_index().as_signers().contains_key(&signer)?,
ErrorKind::InvalidRequest,
"unknown signer {signer}"
);
db.as_index_mut()
.as_os_mut()
.as_versions_mut()
.as_idx_mut(&version)
.or_not_found(&version)?
.as_signers_mut()
.mutate(|s| Ok(s.insert(signer)))?;
Ok(())
})
.await
}
pub async fn remove_version_signer(
ctx: RegistryContext,
VersionSignerParams { version, signer }: VersionSignerParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
if !db
.as_index_mut()
.as_os_mut()
.as_versions_mut()
.as_idx_mut(&version)
.or_not_found(&version)?
.as_signers_mut()
.mutate(|s| Ok(s.remove(&signer)))?
{
return Err(Error::new(
eyre!("signer {signer} is not authorized to sign for v{version}"),
ErrorKind::NotFound,
));
}
Ok(())
})
.await
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct ListVersionSignersParams {
#[ts(type = "string")]
pub version: Version,
}
pub async fn list_version_signers(
ctx: RegistryContext,
ListVersionSignersParams { version }: ListVersionSignersParams,
) -> Result<BTreeMap<RequestGuid, SignerInfo>, Error> {
let db = ctx.db.peek().await;
db.as_index()
.as_os()
.as_versions()
.as_idx(&version)
.or_not_found(&version)?
.as_signers()
.de()?
.into_iter()
.filter_map(|guid| {
db.as_index()
.as_signers()
.as_idx(&guid)
.map(|s| s.de().map(|s| (guid, s)))
})
.collect()
}