Feature/registry improvements (#2772)

* add build cli script for cross-building cli

* sdk alpha.13

* registry improvements
This commit is contained in:
Aiden McClelland
2024-11-05 20:38:52 -07:00
committed by GitHub
parent 020268fe67
commit b79c029f21
19 changed files with 636 additions and 89 deletions

View File

@@ -28,6 +28,16 @@ fn select_executable(name: &str) -> Option<fn(VecDeque<OsString>)> {
"embassy-sdk" => Some(|_| deprecated::renamed("embassy-sdk", "start-sdk")),
"embassyd" => Some(|_| deprecated::renamed("embassyd", "startd")),
"embassy-init" => Some(|_| deprecated::removed("embassy-init")),
"contents" => Some(|_| {
#[cfg(feature = "cli")]
println!("start-cli");
#[cfg(feature = "container-runtime")]
println!("start-cli (container)");
#[cfg(feature = "daemon")]
println!("startd");
#[cfg(feature = "registry")]
println!("registry");
}),
_ => None,
}
}

View File

@@ -115,8 +115,7 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
let api = ParentHandler::new()
.subcommand(
"git-info",
from_fn(|_: RpcContext| version::git_info())
.with_about("Display the githash of StartOS CLI"),
from_fn(|_: C| version::git_info()).with_about("Display the githash of StartOS CLI"),
)
.subcommand(
"echo",

View File

@@ -360,11 +360,7 @@ pub fn logs<
source: impl for<'a> LogSourceFn<'a, C, Extra>,
) -> ParentHandler<C, LogsParams<Extra>> {
ParentHandler::new()
.root_handler(
logs_nofollow::<C, Extra>(source.clone())
.with_inherited(|params, _| params)
.no_cli(),
)
.root_handler(logs_nofollow::<C, Extra>(source.clone()).no_cli())
.subcommand(
"follow",
logs_follow::<C, Extra>(source)
@@ -436,7 +432,7 @@ where
fn logs_nofollow<C, Extra>(
f: impl for<'a> LogSourceFn<'a, C, Extra>,
) -> impl HandlerFor<C, Params = Empty, InheritedParams = LogsParams<Extra>, Ok = LogResponse, Err = Error>
) -> impl HandlerFor<C, Params = LogsParams<Extra>, InheritedParams = Empty, Ok = LogResponse, Err = Error>
where
C: Context,
Extra: FromArgMatches + Args + Send + Sync + 'static,
@@ -444,7 +440,7 @@ where
from_fn_async(
move |HandlerArgs {
context,
inherited_params:
params:
LogsParams {
extra,
limit,
@@ -453,7 +449,7 @@ where
before,
},
..
}: HandlerArgs<C, Empty, LogsParams<Extra>>| {
}: HandlerArgs<C, LogsParams<Extra>>| {
let f = f.clone();
async move {
fetch_logs(

View File

@@ -60,6 +60,13 @@ fn signers_api<C: Context>() -> ParentHandler<C> {
"add",
from_fn_async(cli_add_signer).with_about("Add signer"),
)
.subcommand(
"edit",
from_fn_async(edit_signer)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_call_remote::<CliContext>(),
)
}
impl Model<BTreeMap<Guid, SignerInfo>> {
@@ -143,6 +150,64 @@ pub async fn add_signer(ctx: RegistryContext, signer: SignerInfo) -> Result<Guid
.await
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
#[ts(export)]
pub struct EditSignerParams {
pub id: Guid,
#[arg(short = 'n', long)]
pub set_name: Option<String>,
#[arg(short = 'c', long)]
pub add_contact: Vec<ContactInfo>,
#[arg(short = 'k', long)]
pub add_key: Vec<AnyVerifyingKey>,
#[arg(short = 'C', long)]
pub remove_contact: Vec<ContactInfo>,
#[arg(short = 'K', long)]
pub remove_key: Vec<AnyVerifyingKey>,
}
pub async fn edit_signer(
ctx: RegistryContext,
EditSignerParams {
id,
set_name,
add_contact,
add_key,
remove_contact,
remove_key,
}: EditSignerParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_index_mut()
.as_signers_mut()
.as_idx_mut(&id)
.or_not_found(&id)?
.mutate(|s| {
if let Some(name) = set_name {
s.name = name;
}
s.contact.extend(add_contact);
for rm in remove_contact {
let Some((idx, _)) = s.contact.iter().enumerate().find(|(_, c)| *c == &rm)
else {
continue;
};
s.contact.remove(idx);
}
s.keys.extend(add_key);
for rm in remove_key {
s.keys.remove(&rm);
}
Ok(())
})
})
.await
}
#[derive(Debug, Deserialize, Serialize, Parser)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]

View File

@@ -0,0 +1,126 @@
use std::collections::BTreeMap;
use std::path::PathBuf;
use clap::Parser;
use imbl_value::InternedString;
use itertools::Itertools;
use models::DataUrl;
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, 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::package::index::Category;
use crate::util::serde::{HandlerExtSerde, WithIoFormat};
pub fn info_api<C: Context>() -> ParentHandler<C, WithIoFormat<Empty>> {
ParentHandler::<C, WithIoFormat<Empty>>::new()
.root_handler(
from_fn_async(get_info)
.with_display_serializable()
.with_about("Display registry name, icon, and package categories")
.with_call_remote::<CliContext>(),
)
.subcommand(
"set-name",
from_fn_async(set_name)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Set the name for the registry")
.with_call_remote::<CliContext>(),
)
.subcommand(
"set-icon",
from_fn_async(set_icon)
.with_metadata("admin", Value::Bool(true))
.no_cli(),
)
.subcommand(
"set-icon",
from_fn_async(cli_set_icon)
.no_display()
.with_about("Set the icon for the registry"),
)
}
#[derive(Debug, Default, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RegistryInfo {
pub name: Option<String>,
pub icon: Option<DataUrl<'static>>,
#[ts(as = "BTreeMap::<String, Category>")]
pub categories: BTreeMap<InternedString, Category>,
}
pub async fn get_info(ctx: RegistryContext) -> Result<RegistryInfo, Error> {
let peek = ctx.db.peek().await.into_index();
Ok(RegistryInfo {
name: peek.as_name().de()?,
icon: peek.as_icon().de()?,
categories: peek.as_package().as_categories().de()?,
})
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct SetNameParams {
pub name: String,
}
pub async fn set_name(
ctx: RegistryContext,
SetNameParams { name }: SetNameParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| db.as_index_mut().as_name_mut().ser(&Some(name)))
.await
}
#[derive(Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct SetIconParams {
pub icon: DataUrl<'static>,
}
pub async fn set_icon(
ctx: RegistryContext,
SetIconParams { icon }: SetIconParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| db.as_index_mut().as_icon_mut().ser(&Some(icon)))
.await
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CliSetIconParams {
pub icon: PathBuf,
}
pub async fn cli_set_icon(
HandlerArgs {
context: ctx,
parent_method,
method,
params: CliSetIconParams { icon },
..
}: HandlerArgs<CliContext, CliSetIconParams>,
) -> Result<(), Error> {
let data_url = DataUrl::from_path(icon).await?;
ctx.call_remote::<RegistryContext>(
&parent_method.into_iter().chain(method).join("."),
imbl_value::json!({
"icon": data_url,
}),
)
.await?;
Ok(())
}

View File

@@ -28,6 +28,7 @@ pub mod auth;
pub mod context;
pub mod db;
pub mod device_info;
pub mod info;
pub mod os;
pub mod package;
pub mod signer;
@@ -57,25 +58,6 @@ pub async fn get_full_index(ctx: RegistryContext) -> Result<FullIndex, Error> {
ctx.db.peek().await.into_index().de()
}
#[derive(Debug, Default, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RegistryInfo {
pub name: Option<String>,
pub icon: Option<DataUrl<'static>>,
#[ts(as = "BTreeMap::<String, Category>")]
pub categories: BTreeMap<InternedString, Category>,
}
pub async fn get_info(ctx: RegistryContext) -> Result<RegistryInfo, Error> {
let peek = ctx.db.peek().await.into_index();
Ok(RegistryInfo {
name: peek.as_name().de()?,
icon: peek.as_icon().de()?,
categories: peek.as_package().as_categories().de()?,
})
}
pub fn registry_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
@@ -85,13 +67,8 @@ pub fn registry_api<C: Context>() -> ParentHandler<C> {
.with_about("List info including registry name and packages")
.with_call_remote::<CliContext>(),
)
.subcommand(
"info",
from_fn_async(get_info)
.with_display_serializable()
.with_about("Display registry name, icon, and package categories")
.with_call_remote::<CliContext>(),
)
.subcommand("info", info::info_api::<C>())
// set info and categories
.subcommand(
"os",
os::os_api::<C>().with_about("Commands related to OS assets and versions"),

View File

@@ -40,7 +40,12 @@ pub fn get_api<C: Context>() -> ParentHandler<C> {
.with_about("Download img"),
)
.subcommand("squashfs", from_fn_async(get_squashfs).no_cli())
.subcommand("squashfs", from_fn_async(cli_get_os_asset).no_display().with_about("Download squashfs"))
.subcommand(
"squashfs",
from_fn_async(cli_get_os_asset)
.no_display()
.with_about("Download squashfs"),
)
}
#[derive(Debug, Deserialize, Serialize, TS)]
@@ -104,7 +109,11 @@ pub async fn get_squashfs(
pub struct CliGetOsAssetParams {
pub version: Version,
pub platform: InternedString,
#[arg(long = "download", short = 'd')]
#[arg(
long = "download",
short = 'd',
help = "The path of the directory to download to"
)]
pub download: Option<PathBuf>,
#[arg(
long = "reverify",

View File

@@ -0,0 +1,147 @@
use std::collections::BTreeMap;
use clap::Parser;
use imbl_value::InternedString;
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::package::index::Category;
use crate::s9pk::manifest::Description;
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
pub fn category_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"add",
from_fn_async(add_category)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Add a category to the registry")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove_category)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Remove a category from the registry")
.with_call_remote::<CliContext>(),
)
.subcommand(
"list",
from_fn_async(list_categories)
.with_display_serializable()
.with_custom_display_fn(|params, categories| {
Ok(display_categories(params.params, categories))
})
.with_call_remote::<CliContext>(),
)
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct AddCategoryParams {
#[ts(type = "string")]
pub id: InternedString,
pub name: String,
#[arg(short, long, help = "Short description for the category")]
pub short: String,
#[arg(short, long, help = "Long description for the category")]
pub long: String,
}
pub async fn add_category(
ctx: RegistryContext,
AddCategoryParams {
id,
name,
short,
long,
}: AddCategoryParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_index_mut()
.as_package_mut()
.as_categories_mut()
.insert(
&id,
&Category {
name,
description: Description { short, long },
},
)
})
.await?;
Ok(())
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemoveCategoryParams {
#[ts(type = "string")]
pub id: InternedString,
}
pub async fn remove_category(
ctx: RegistryContext,
RemoveCategoryParams { id }: RemoveCategoryParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_index_mut()
.as_package_mut()
.as_categories_mut()
.remove(&id)
})
.await?;
Ok(())
}
pub async fn list_categories(
ctx: RegistryContext,
) -> Result<BTreeMap<InternedString, Category>, Error> {
ctx.db
.peek()
.await
.into_index()
.into_package()
.into_categories()
.de()
}
pub fn display_categories<T>(
params: WithIoFormat<T>,
categories: BTreeMap<InternedString, Category>,
) {
use prettytable::*;
if let Some(format) = params.format {
return display_serializable(format, categories);
}
let mut table = Table::new();
table.add_row(row![bc =>
"ID",
"NAME",
"SHORT DESCRIPTION",
"LONG DESCRIPTION",
]);
for (id, info) in categories {
table.add_row(row![
&*id,
&info.name,
&info.description.short,
&info.description.long,
]);
}
table.print_tty(false).unwrap();
}

View File

@@ -5,8 +5,10 @@ use crate::prelude::*;
use crate::util::serde::HandlerExtSerde;
pub mod add;
pub mod category;
pub mod get;
pub mod index;
pub mod signer;
pub fn package_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
@@ -29,6 +31,10 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
.no_display()
.with_about("Add package to registry index"),
)
.subcommand(
"signer",
signer::signer_api::<C>().with_about("Add, remove, and list package signers"),
)
.subcommand(
"get",
from_fn_async(get::get_package)
@@ -40,4 +46,9 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
.with_about("List installation candidate package(s)")
.with_call_remote::<CliContext>(),
)
.subcommand(
"category",
category::category_api::<C>()
.with_about("Update the categories for packages on the registry"),
)
}

View File

@@ -0,0 +1,133 @@
use std::collections::BTreeMap;
use clap::Parser;
use models::PackageId;
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::Guid;
use crate::util::serde::HandlerExtSerde;
pub fn signer_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"add",
from_fn_async(add_package_signer)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Add package signer")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove_package_signer)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Remove package signer")
.with_call_remote::<CliContext>(),
)
.subcommand(
"list",
from_fn_async(list_package_signers)
.with_display_serializable()
.with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result)))
.with_about("List package signers and related signer info")
.with_call_remote::<CliContext>(),
)
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct PackageSignerParams {
pub id: PackageId,
pub signer: Guid,
}
pub async fn add_package_signer(
ctx: RegistryContext,
PackageSignerParams { id, signer }: PackageSignerParams,
) -> 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_package_mut()
.as_packages_mut()
.as_idx_mut(&id)
.or_not_found(&id)?
.as_authorized_mut()
.mutate(|s| Ok(s.insert(signer)))?;
Ok(())
})
.await
}
pub async fn remove_package_signer(
ctx: RegistryContext,
PackageSignerParams { id, signer }: PackageSignerParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
if !db
.as_index_mut()
.as_package_mut()
.as_packages_mut()
.as_idx_mut(&id)
.or_not_found(&id)?
.as_authorized_mut()
.mutate(|s| Ok(s.remove(&signer)))?
{
return Err(Error::new(
eyre!("signer {signer} is not authorized to sign for {id}"),
ErrorKind::NotFound,
));
}
Ok(())
})
.await
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct ListPackageSignersParams {
pub id: PackageId,
}
pub async fn list_package_signers(
ctx: RegistryContext,
ListPackageSignersParams { id }: ListPackageSignersParams,
) -> Result<BTreeMap<Guid, SignerInfo>, Error> {
let db = ctx.db.peek().await;
db.as_index()
.as_package()
.as_packages()
.as_idx(&id)
.or_not_found(&id)?
.as_authorized()
.de()?
.into_iter()
.filter_map(|guid| {
db.as_index()
.as_signers()
.as_idx(&guid)
.map(|s| s.de().map(|s| (guid, s)))
})
.collect()
}

View File

@@ -25,7 +25,7 @@ pub struct SignerInfo {
pub keys: HashSet<AnyVerifyingKey>,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
// TODO: better types