mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
cli improvements
fix group handling
This commit is contained in:
@@ -46,6 +46,7 @@ openssh-server
|
||||
podman
|
||||
psmisc
|
||||
qemu-guest-agent
|
||||
qemu-user-static
|
||||
rfkill
|
||||
rsync
|
||||
samba-common-bin
|
||||
|
||||
@@ -176,6 +176,7 @@ mio = "1"
|
||||
new_mime_guess = "4"
|
||||
nix = { version = "0.30.1", features = [
|
||||
"fs",
|
||||
"hostname",
|
||||
"mount",
|
||||
"net",
|
||||
"process",
|
||||
|
||||
@@ -1843,18 +1843,18 @@ service.mod.failed-to-parse-package-data-entry:
|
||||
pl_PL: "Nie udało się przeanalizować PackageDataEntry, znaleziono: %{error}"
|
||||
|
||||
service.mod.no-matching-subcontainers:
|
||||
en_US: "no matching subcontainers are running for %{id}; some possible choices are:\n%{subcontainers}"
|
||||
de_DE: "keine passenden Subcontainer laufen für %{id}; einige mögliche Optionen sind:\n%{subcontainers}"
|
||||
es_ES: "no hay subcontenedores coincidentes ejecutándose para %{id}; algunas opciones posibles son:\n%{subcontainers}"
|
||||
fr_FR: "aucun sous-conteneur correspondant n'est en cours d'exécution pour %{id} ; voici quelques choix possibles :\n%{subcontainers}"
|
||||
pl_PL: "nie działają pasujące podkontenery dla %{id}; niektóre możliwe wybory to:\n%{subcontainers}"
|
||||
en_US: "no matching subcontainers are running for %{id}; some possible choices are:"
|
||||
de_DE: "keine passenden Subcontainer laufen für %{id}; einige mögliche Optionen sind:"
|
||||
es_ES: "no hay subcontenedores coincidentes ejecutándose para %{id}; algunas opciones posibles son:"
|
||||
fr_FR: "aucun sous-conteneur correspondant n'est en cours d'exécution pour %{id} ; voici quelques choix possibles :"
|
||||
pl_PL: "nie działają pasujące podkontenery dla %{id}; niektóre możliwe wybory to:"
|
||||
|
||||
service.mod.multiple-subcontainers-found:
|
||||
en_US: "multiple subcontainers found for %{id}: \n%{subcontainer_ids}"
|
||||
de_DE: "mehrere Subcontainer für %{id} gefunden: \n%{subcontainer_ids}"
|
||||
es_ES: "se encontraron múltiples subcontenedores para %{id}: \n%{subcontainer_ids}"
|
||||
fr_FR: "plusieurs sous-conteneurs trouvés pour %{id} : \n%{subcontainer_ids}"
|
||||
pl_PL: "znaleziono wiele podkontenerów dla %{id}: \n%{subcontainer_ids}"
|
||||
en_US: "multiple subcontainers found for %{id}"
|
||||
de_DE: "mehrere Subcontainer für %{id} gefunden"
|
||||
es_ES: "se encontraron múltiples subcontenedores para %{id}"
|
||||
fr_FR: "plusieurs sous-conteneurs trouvés pour %{id}"
|
||||
pl_PL: "znaleziono wiele podkontenerów dla %{id}"
|
||||
|
||||
service.mod.invalid-byte-length-for-signal:
|
||||
en_US: "invalid byte length for signal: %{length}"
|
||||
@@ -3703,6 +3703,20 @@ help.arg.wireguard-config:
|
||||
fr_FR: "Configuration WireGuard"
|
||||
pl_PL: "Konfiguracja WireGuard"
|
||||
|
||||
help.s9pk-s3base:
|
||||
en_US: "Base URL for publishing s9pks"
|
||||
de_DE: "Basis-URL für die Veröffentlichung von s9pks"
|
||||
es_ES: "URL base para publicar s9pks"
|
||||
fr_FR: "URL de base pour publier les s9pks"
|
||||
pl_PL: "Bazowy URL do publikowania s9pks"
|
||||
|
||||
help.s9pk-s3bucket:
|
||||
en_US: "S3 bucket to publish s9pks to (should correspond to s3base)"
|
||||
de_DE: "S3-Bucket zum Veröffentlichen von s9pks (sollte mit s3base übereinstimmen)"
|
||||
es_ES: "Bucket S3 para publicar s9pks (debe corresponder con s3base)"
|
||||
fr_FR: "Bucket S3 pour publier les s9pks (doit correspondre à s3base)"
|
||||
pl_PL: "Bucket S3 do publikowania s9pks (powinien odpowiadać s3base)"
|
||||
|
||||
# CLI command descriptions (about.*)
|
||||
about.add-address-to-host:
|
||||
en_US: "Add an address to this host"
|
||||
@@ -4866,6 +4880,13 @@ about.persist-new-notification:
|
||||
fr_FR: "Persister une nouvelle notification"
|
||||
pl_PL: "Utrwal nowe powiadomienie"
|
||||
|
||||
about.publish-s9pk:
|
||||
en_US: "Publish s9pk to S3 bucket and index on registry"
|
||||
de_DE: "S9pk in S3-Bucket veröffentlichen und in Registry indizieren"
|
||||
es_ES: "Publicar s9pk en bucket S3 e indexar en el registro"
|
||||
fr_FR: "Publier s9pk dans le bucket S3 et indexer dans le registre"
|
||||
pl_PL: "Opublikuj s9pk do bucketu S3 i zindeksuj w rejestrze"
|
||||
|
||||
about.rebuild-service-container:
|
||||
en_US: "Rebuild service container"
|
||||
de_DE: "Dienst-Container neu erstellen"
|
||||
|
||||
@@ -38,6 +38,8 @@ pub struct CliContextSeed {
|
||||
pub registry_url: Option<Url>,
|
||||
pub registry_hostname: Vec<InternedString>,
|
||||
pub registry_listen: Option<SocketAddr>,
|
||||
pub s9pk_s3base: Option<Url>,
|
||||
pub s9pk_s3bucket: Option<InternedString>,
|
||||
pub tunnel_addr: Option<SocketAddr>,
|
||||
pub tunnel_listen: Option<SocketAddr>,
|
||||
pub client: Client,
|
||||
@@ -129,6 +131,8 @@ impl CliContext {
|
||||
.transpose()?,
|
||||
registry_hostname: config.registry_hostname.unwrap_or_default(),
|
||||
registry_listen: config.registry_listen,
|
||||
s9pk_s3base: config.s9pk_s3base,
|
||||
s9pk_s3bucket: config.s9pk_s3bucket,
|
||||
tunnel_addr: config.tunnel,
|
||||
tunnel_listen: config.tunnel_listen,
|
||||
client: {
|
||||
|
||||
@@ -68,6 +68,10 @@ pub struct ClientConfig {
|
||||
pub registry_hostname: Option<Vec<InternedString>>,
|
||||
#[arg(skip)]
|
||||
pub registry_listen: Option<SocketAddr>,
|
||||
#[arg(long, help = "help.s9pk-s3base")]
|
||||
pub s9pk_s3base: Option<Url>,
|
||||
#[arg(long, help = "help.s9pk-s3bucket")]
|
||||
pub s9pk_s3bucket: Option<InternedString>,
|
||||
#[arg(short = 't', long, help = "help.arg.tunnel-address")]
|
||||
pub tunnel: Option<SocketAddr>,
|
||||
#[arg(skip)]
|
||||
|
||||
@@ -579,6 +579,7 @@ impl RpcContext {
|
||||
pub async fn call_remote<RemoteContext>(
|
||||
&self,
|
||||
method: &str,
|
||||
metadata: OrdMap<&'static str, Value>,
|
||||
params: Value,
|
||||
) -> Result<Value, RpcError>
|
||||
where
|
||||
@@ -587,7 +588,7 @@ impl RpcContext {
|
||||
<Self as CallRemote<RemoteContext, Empty>>::call_remote(
|
||||
&self,
|
||||
method,
|
||||
OrdMap::new(),
|
||||
metadata,
|
||||
params,
|
||||
Empty {},
|
||||
)
|
||||
@@ -596,20 +597,15 @@ impl RpcContext {
|
||||
pub async fn call_remote_with<RemoteContext, T>(
|
||||
&self,
|
||||
method: &str,
|
||||
metadata: OrdMap<&'static str, Value>,
|
||||
params: Value,
|
||||
extra: T,
|
||||
) -> Result<Value, RpcError>
|
||||
where
|
||||
Self: CallRemote<RemoteContext, T>,
|
||||
{
|
||||
<Self as CallRemote<RemoteContext, T>>::call_remote(
|
||||
&self,
|
||||
method,
|
||||
OrdMap::new(),
|
||||
params,
|
||||
extra,
|
||||
)
|
||||
.await
|
||||
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, metadata, params, extra)
|
||||
.await
|
||||
}
|
||||
}
|
||||
impl AsRef<Client> for RpcContext {
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum::http::StatusCode;
|
||||
use axum::http::uri::InvalidUri;
|
||||
use color_eyre::eyre::eyre;
|
||||
use num_enum::TryFromPrimitive;
|
||||
use patch_db::Revision;
|
||||
use patch_db::Value;
|
||||
use rpc_toolkit::reqwest;
|
||||
use rpc_toolkit::yajrc::{
|
||||
INVALID_PARAMS_ERROR, INVALID_REQUEST_ERROR, METHOD_NOT_FOUND_ERROR, PARSE_ERROR, RpcError,
|
||||
@@ -16,6 +16,7 @@ use tokio_rustls::rustls;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::InvalidId;
|
||||
use crate::prelude::to_value;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
|
||||
#[repr(i32)]
|
||||
@@ -197,7 +198,7 @@ pub struct Error {
|
||||
pub source: color_eyre::eyre::Error,
|
||||
pub debug: Option<color_eyre::eyre::Error>,
|
||||
pub kind: ErrorKind,
|
||||
pub revision: Option<Revision>,
|
||||
pub info: Value,
|
||||
pub task: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
@@ -228,7 +229,7 @@ impl Error {
|
||||
source: source.into(),
|
||||
debug,
|
||||
kind,
|
||||
revision: None,
|
||||
info: Value::Null,
|
||||
task: None,
|
||||
}
|
||||
}
|
||||
@@ -237,7 +238,7 @@ impl Error {
|
||||
source: eyre!("{}", self.source),
|
||||
debug: self.debug.as_ref().map(|e| eyre!("{e}")),
|
||||
kind: self.kind,
|
||||
revision: self.revision.clone(),
|
||||
info: self.info.clone(),
|
||||
task: None,
|
||||
}
|
||||
}
|
||||
@@ -245,6 +246,10 @@ impl Error {
|
||||
self.task = Some(task);
|
||||
self
|
||||
}
|
||||
pub fn with_info(mut self, info: Value) -> Self {
|
||||
self.info = info;
|
||||
self
|
||||
}
|
||||
pub async fn wait(mut self) -> Self {
|
||||
if let Some(task) = &mut self.task {
|
||||
task.await.log_err();
|
||||
@@ -423,6 +428,8 @@ impl From<patch_db::value::Error> for Error {
|
||||
pub struct ErrorData {
|
||||
pub details: String,
|
||||
pub debug: String,
|
||||
#[serde(default)]
|
||||
pub info: Value,
|
||||
}
|
||||
impl Display for ErrorData {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
@@ -440,6 +447,7 @@ impl From<Error> for ErrorData {
|
||||
Self {
|
||||
details: value.to_string(),
|
||||
debug: format!("{:?}", value),
|
||||
info: value.info,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -470,40 +478,31 @@ impl From<&RpcError> for ErrorData {
|
||||
.or_else(|| d.as_str().map(|s| s.to_owned()))
|
||||
})
|
||||
.unwrap_or_else(|| value.message.clone().into_owned()),
|
||||
info: to_value(
|
||||
&value
|
||||
.data
|
||||
.as_ref()
|
||||
.and_then(|d| d.as_object().and_then(|d| d.get("info"))),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for RpcError {
|
||||
fn from(e: Error) -> Self {
|
||||
let mut data_object = serde_json::Map::with_capacity(3);
|
||||
data_object.insert("details".to_owned(), format!("{}", e.source).into());
|
||||
data_object.insert("debug".to_owned(), format!("{:?}", e.source).into());
|
||||
data_object.insert(
|
||||
"revision".to_owned(),
|
||||
match serde_json::to_value(&e.revision) {
|
||||
let kind = e.kind;
|
||||
let data = ErrorData::from(e);
|
||||
RpcError {
|
||||
code: kind as i32,
|
||||
message: kind.as_str().into(),
|
||||
data: Some(match serde_json::to_value(&data) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::warn!("Error serializing revision for Error object: {}", e);
|
||||
tracing::warn!("Error serializing ErrorData object: {}", e);
|
||||
serde_json::Value::Null
|
||||
}
|
||||
},
|
||||
);
|
||||
RpcError {
|
||||
code: e.kind as i32,
|
||||
message: e.kind.as_str().into(),
|
||||
data: Some(
|
||||
match serde_json::to_value(&ErrorData {
|
||||
details: format!("{}", e.source),
|
||||
debug: format!("{:?}", e.source),
|
||||
}) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::warn!("Error serializing revision for Error object: {}", e);
|
||||
serde_json::Value::Null
|
||||
}
|
||||
},
|
||||
),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -606,7 +605,7 @@ where
|
||||
kind,
|
||||
source,
|
||||
debug,
|
||||
revision: None,
|
||||
info: Value::Null,
|
||||
task: None,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -131,6 +131,9 @@ pub async fn install(
|
||||
let package: GetPackageResponse = from_value(
|
||||
ctx.call_remote_with::<RegistryContext, _>(
|
||||
"package.get",
|
||||
[("get_device_info", Value::Bool(true))]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
json!({
|
||||
"id": id,
|
||||
"targetVersion": VersionRange::exactly(version.deref().clone()),
|
||||
|
||||
@@ -540,7 +540,10 @@ pub fn package<C: Context>() -> ParentHandler<C> {
|
||||
.with_about("about.execute-commands-container")
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand("attach", from_fn_async(service::cli_attach).no_display())
|
||||
.subcommand(
|
||||
"attach",
|
||||
from_fn_async_local(service::cli_attach).no_display(),
|
||||
)
|
||||
.subcommand(
|
||||
"host",
|
||||
net::host::host_api::<C>().with_about("about.manage-network-hosts-package"),
|
||||
|
||||
@@ -135,20 +135,24 @@ pub struct CliAddPackageParams {
|
||||
}
|
||||
|
||||
pub async fn cli_add_package(
|
||||
HandlerArgs {
|
||||
context: ctx,
|
||||
parent_method,
|
||||
method,
|
||||
params:
|
||||
CliAddPackageParams {
|
||||
file,
|
||||
url,
|
||||
no_verify,
|
||||
},
|
||||
..
|
||||
}: HandlerArgs<CliContext, CliAddPackageParams>,
|
||||
ctx: CliContext,
|
||||
CliAddPackageParams {
|
||||
file,
|
||||
url,
|
||||
no_verify,
|
||||
}: CliAddPackageParams,
|
||||
) -> Result<(), Error> {
|
||||
let s9pk = S9pk::open(&file, None).await?;
|
||||
cli_add_package_impl(ctx, s9pk, url, no_verify).await
|
||||
}
|
||||
|
||||
pub async fn cli_add_package_impl(
|
||||
ctx: CliContext,
|
||||
s9pk: S9pk,
|
||||
url: Vec<Url>,
|
||||
no_verify: bool,
|
||||
) -> Result<(), Error> {
|
||||
let manifest = s9pk.as_manifest();
|
||||
|
||||
let progress = FullProgressTracker::new();
|
||||
let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(1));
|
||||
@@ -170,8 +174,16 @@ pub async fn cli_add_package(
|
||||
Some(1),
|
||||
);
|
||||
|
||||
let progress_task =
|
||||
progress.progress_bar_task(&format!("Adding {} to registry...", file.display()));
|
||||
let progress_task = progress.progress_bar_task(&format!(
|
||||
"Adding {}@{}{} to registry...",
|
||||
manifest.id,
|
||||
manifest.version,
|
||||
manifest
|
||||
.hardware_requirements
|
||||
.arch
|
||||
.as_ref()
|
||||
.map_or(String::new(), |a| format!(" ({})", a.iter().join("/")))
|
||||
));
|
||||
|
||||
sign_phase.start();
|
||||
let commitment = s9pk.as_archive().commitment().await?;
|
||||
@@ -188,7 +200,7 @@ pub async fn cli_add_package(
|
||||
|
||||
index_phase.start();
|
||||
ctx.call_remote::<RegistryContext>(
|
||||
&parent_method.into_iter().chain(method).join("."),
|
||||
"package.add",
|
||||
imbl_value::json!({
|
||||
"urls": &url,
|
||||
"signature": AnySignature::Ed25519(signature),
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use rpc_toolkit::{Empty, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::process::Command;
|
||||
use ts_rs::TS;
|
||||
use url::Url;
|
||||
|
||||
use crate::ImageId;
|
||||
use crate::context::CliContext;
|
||||
@@ -13,9 +16,9 @@ use crate::s9pk::manifest::Manifest;
|
||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||
use crate::s9pk::v2::SIG_CONTEXT;
|
||||
use crate::s9pk::v2::pack::ImageConfig;
|
||||
use crate::util::Apply;
|
||||
use crate::util::io::{TmpDir, create_file, open_file};
|
||||
use crate::util::serde::{HandlerExtSerde, apply_expr};
|
||||
use crate::util::{Apply, Invoke};
|
||||
|
||||
pub const SKIP_ENV: &[&str] = &["TERM", "container", "HOME", "HOSTNAME"];
|
||||
|
||||
@@ -61,6 +64,12 @@ pub fn s9pk() -> ParentHandler<CliContext> {
|
||||
.no_display()
|
||||
.with_about("about.convert-s9pk-v1-to-v2"),
|
||||
)
|
||||
.subcommand(
|
||||
"publish",
|
||||
from_fn_async(publish)
|
||||
.no_display()
|
||||
.with_about("about.publish-s9pk"),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
@@ -256,3 +265,60 @@ async fn convert(ctx: CliContext, S9pkPath { s9pk: s9pk_path }: S9pkPath) -> Res
|
||||
tokio::fs::rename(tmp_path, s9pk_path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn publish(ctx: CliContext, S9pkPath { s9pk: s9pk_path }: S9pkPath) -> Result<(), Error> {
|
||||
let filename = s9pk_path.file_name().unwrap().to_string_lossy();
|
||||
let s9pk = super::S9pk::open(&s9pk_path, None).await?;
|
||||
let manifest = s9pk.as_manifest();
|
||||
let path = [
|
||||
manifest.id.deref(),
|
||||
manifest.version.as_str(),
|
||||
filename.deref(),
|
||||
];
|
||||
let mut s3url = ctx
|
||||
.s9pk_s3base
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::new(eyre!("--s9pk-s3base required"), ErrorKind::InvalidRequest))?
|
||||
.clone();
|
||||
s3url
|
||||
.path_segments_mut()
|
||||
.map_err(|_| {
|
||||
Error::new(
|
||||
eyre!("s9pk-s3base is invalid (missing protocol?)"),
|
||||
ErrorKind::ParseUrl,
|
||||
)
|
||||
})?
|
||||
.pop_if_empty()
|
||||
.extend(path);
|
||||
|
||||
let mut s3dest = format!(
|
||||
"s3://{}",
|
||||
ctx.s9pk_s3bucket
|
||||
.as_deref()
|
||||
.or_else(|| s3url
|
||||
.host_str()
|
||||
.and_then(|h| h.split_once(".").map(|h| h.0)))
|
||||
.ok_or_else(|| {
|
||||
Error::new(eyre!("--s9pk-s3bucket required"), ErrorKind::InvalidRequest)
|
||||
})?,
|
||||
)
|
||||
.parse::<Url>()?;
|
||||
s3dest
|
||||
.path_segments_mut()
|
||||
.map_err(|_| {
|
||||
Error::new(
|
||||
eyre!("s9pk-s3base is invalid (missing protocol?)"),
|
||||
ErrorKind::ParseUrl,
|
||||
)
|
||||
})?
|
||||
.pop_if_empty()
|
||||
.extend(path);
|
||||
Command::new("s3cmd")
|
||||
.arg("put")
|
||||
.arg("-P")
|
||||
.arg(s9pk_path)
|
||||
.arg(s3dest.as_str())
|
||||
.invoke(ErrorKind::Network)
|
||||
.await?;
|
||||
crate::registry::package::add::cli_add_package_impl(ctx, s9pk, vec![s3url], false).await
|
||||
}
|
||||
|
||||
@@ -727,8 +727,9 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> {
|
||||
arches.iter().join("/"),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
c.arch = filtered;
|
||||
}
|
||||
c.arch = filtered;
|
||||
});
|
||||
manifest.hardware_requirements.arch = Some(arches);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::rpc_continuations::Guid;
|
||||
use crate::service::effects::prelude::*;
|
||||
use crate::service::persistent_container::Subcontainer;
|
||||
use crate::util::Invoke;
|
||||
use crate::util::io::write_file_owned_atomic;
|
||||
|
||||
pub const NVIDIA_OVERLAY_PATH: &str = "/var/tmp/startos/nvidia-overlay";
|
||||
pub const NVIDIA_OVERLAY_DEBIAN: &str = "/var/tmp/startos/nvidia-overlay/debian";
|
||||
@@ -94,7 +95,7 @@ pub async fn create_subcontainer_fs(
|
||||
.cloned()
|
||||
{
|
||||
let guid = Guid::new();
|
||||
let rootfs_dir = context
|
||||
let lxc_container = context
|
||||
.seed
|
||||
.persistent_container
|
||||
.lxc_container
|
||||
@@ -104,8 +105,9 @@ pub async fn create_subcontainer_fs(
|
||||
eyre!("PersistentContainer has been destroyed"),
|
||||
ErrorKind::Incoherent,
|
||||
)
|
||||
})?
|
||||
.rootfs_dir();
|
||||
})?;
|
||||
let container_guid = &lxc_container.guid;
|
||||
let rootfs_dir = lxc_container.rootfs_dir();
|
||||
let mountpoint = rootfs_dir
|
||||
.join("media/startos/subcontainers")
|
||||
.join(guid.as_ref());
|
||||
@@ -154,6 +156,20 @@ pub async fn create_subcontainer_fs(
|
||||
.arg(&mountpoint)
|
||||
.invoke(ErrorKind::Filesystem)
|
||||
.await?;
|
||||
write_file_owned_atomic(
|
||||
mountpoint.join("etc/hostname"),
|
||||
format!("{container_guid}\n"),
|
||||
100000,
|
||||
100000,
|
||||
)
|
||||
.await?;
|
||||
write_file_owned_atomic(
|
||||
mountpoint.join("etc/hosts"),
|
||||
format!("127.0.0.1\tlocalhost\n127.0.1.1\t{container_guid}\n::1\tlocalhost ip6-localhost ip6-loopback\n"),
|
||||
100000,
|
||||
100000,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!("Mounted overlay {guid} for {image_id}");
|
||||
context
|
||||
.seed
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::{OsStr, OsString, c_int};
|
||||
use std::fs::File;
|
||||
use std::io::{IsTerminal, Read};
|
||||
use std::io::{BufRead, BufReader, IsTerminal, Read};
|
||||
use std::os::unix::process::{CommandExt, ExitStatusExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command as StdCommand, Stdio};
|
||||
@@ -146,95 +145,160 @@ impl ExecParams {
|
||||
|
||||
let mut cmd = StdCommand::new(command);
|
||||
|
||||
let passwd = std::fs::read_to_string(chroot.join("etc/passwd"))
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, "read /etc/passwd"))
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
let mut home = None;
|
||||
let mut uid = Err(None);
|
||||
let mut gid = Err(None);
|
||||
let mut needs_home = true;
|
||||
|
||||
if let Some((uid, gid)) =
|
||||
if let Some(uid) = user.as_deref().and_then(|u| u.parse::<u32>().ok()) {
|
||||
Some((uid, uid))
|
||||
} else if let Some((uid, gid)) = user
|
||||
.as_deref()
|
||||
.and_then(|u| u.split_once(":"))
|
||||
.and_then(|(u, g)| Some((u.parse::<u32>().ok()?, g.parse::<u32>().ok()?)))
|
||||
{
|
||||
Some((uid, gid))
|
||||
} else if let Some(user) = user {
|
||||
Some(
|
||||
if let Some((uid, gid)) = passwd.lines().find_map(|l| {
|
||||
let l = l.trim();
|
||||
let mut split = l.split(":");
|
||||
if user != split.next()? {
|
||||
return None;
|
||||
}
|
||||
|
||||
split.next(); // throw away x
|
||||
let uid = split.next()?.parse().ok()?;
|
||||
let gid = split.next()?.parse().ok()?;
|
||||
split.next(); // throw away group name
|
||||
|
||||
home = split.next();
|
||||
|
||||
Some((uid, gid))
|
||||
// uid gid
|
||||
}) {
|
||||
(uid, gid)
|
||||
} else if user == "root" {
|
||||
(0, 0)
|
||||
} else {
|
||||
None.or_not_found(lazy_format!("{user} in /etc/passwd"))?
|
||||
},
|
||||
)
|
||||
if let Some(user) = user {
|
||||
if let Some((u, g)) = user.split_once(":") {
|
||||
uid = Err(Some(u));
|
||||
gid = Err(Some(g));
|
||||
} else {
|
||||
None
|
||||
uid = Err(Some(user));
|
||||
}
|
||||
{
|
||||
if home.is_none() {
|
||||
home = passwd.lines().find_map(|l| {
|
||||
let l = l.trim();
|
||||
let mut split = l.split(":");
|
||||
|
||||
split.next(); // throw away user name
|
||||
split.next(); // throw away x
|
||||
if split.next()?.parse::<u32>().ok()? != uid {
|
||||
return None;
|
||||
}
|
||||
split.next(); // throw away gid
|
||||
split.next(); // throw away group name
|
||||
|
||||
split.next()
|
||||
})
|
||||
};
|
||||
std::os::unix::fs::chown("/proc/self/fd/0", Some(uid), Some(gid)).ok();
|
||||
std::os::unix::fs::chown("/proc/self/fd/1", Some(uid), Some(gid)).ok();
|
||||
std::os::unix::fs::chown("/proc/self/fd/2", Some(uid), Some(gid)).ok();
|
||||
cmd.uid(uid);
|
||||
cmd.gid(gid);
|
||||
} else {
|
||||
home = Some("/root");
|
||||
}
|
||||
cmd.env("HOME", home.unwrap_or("/"));
|
||||
|
||||
let env_string = if let Some(env_file) = &env_file {
|
||||
std::fs::read_to_string(env_file)
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("read {env:?}")))?
|
||||
} else {
|
||||
Default::default()
|
||||
if let Some(u) = uid.err().flatten().and_then(|u| u.parse::<u32>().ok()) {
|
||||
uid = Ok(u);
|
||||
}
|
||||
if let Some(g) = gid.err().flatten().and_then(|g| g.parse::<u32>().ok()) {
|
||||
gid = Ok(g);
|
||||
}
|
||||
|
||||
let mut update_env = |line: &str| {
|
||||
if let Some((k, v)) = line.split_once("=") {
|
||||
needs_home &= k != "HOME";
|
||||
cmd.env(k, v);
|
||||
} else {
|
||||
tracing::warn!("Invalid line in env: {line}");
|
||||
}
|
||||
};
|
||||
let env = env_string
|
||||
.lines()
|
||||
.chain(env.iter().map(|l| l.as_str()))
|
||||
.map(|l| l.trim())
|
||||
.filter_map(|l| l.split_once("="))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
if let Some(f) = env_file {
|
||||
let mut lines = BufReader::new(
|
||||
File::open(&f).with_ctx(|_| (ErrorKind::Filesystem, format!("open r {f:?}")))?,
|
||||
)
|
||||
.lines();
|
||||
while let Some(line) = lines.next().transpose()? {
|
||||
update_env(&line);
|
||||
}
|
||||
}
|
||||
|
||||
for line in env {
|
||||
update_env(&line);
|
||||
}
|
||||
|
||||
let needs_gid = Err(None) == gid;
|
||||
let mut username = InternedString::intern("root");
|
||||
let mut handle_passwd_line = |line: &str| -> Option<()> {
|
||||
let l = line.trim();
|
||||
let mut split = l.split(":");
|
||||
let user = split.next()?;
|
||||
match uid {
|
||||
Err(Some(u)) if u != user => return None,
|
||||
_ => (),
|
||||
}
|
||||
split.next(); // throw away x
|
||||
let u: u32 = split.next()?.parse().ok()?;
|
||||
match uid {
|
||||
Err(Some(_)) => uid = Ok(u),
|
||||
Err(None) if u == 0 => uid = Ok(u),
|
||||
Ok(uid) if uid != u => return None,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
username = user.into();
|
||||
|
||||
if !needs_gid && !needs_home {
|
||||
return Some(());
|
||||
}
|
||||
let g = split.next()?;
|
||||
if needs_gid {
|
||||
gid = Ok(g.parse().ok()?);
|
||||
}
|
||||
|
||||
if needs_home {
|
||||
split.next(); // throw away group name
|
||||
|
||||
let home = split.next()?;
|
||||
|
||||
cmd.env("HOME", home);
|
||||
}
|
||||
|
||||
Some(())
|
||||
};
|
||||
|
||||
let mut lines = BufReader::new(
|
||||
File::open(chroot.join("etc/passwd"))
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, format!("open r /etc/passwd")))?,
|
||||
)
|
||||
.lines();
|
||||
while let Some(line) = lines.next().transpose()? {
|
||||
if handle_passwd_line(&line).is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut groups = Vec::new();
|
||||
let mut handle_group_line = |line: &str| -> Option<()> {
|
||||
let l = line.trim();
|
||||
let mut split = l.split(":");
|
||||
let name = split.next()?;
|
||||
split.next()?; // throw away x
|
||||
let g = split.next()?.parse::<u32>().ok()?;
|
||||
match gid {
|
||||
Err(Some(n)) if n == name => gid = Ok(g),
|
||||
_ => (),
|
||||
}
|
||||
let users = split.next()?;
|
||||
if users.split(",").any(|u| u == &*username) {
|
||||
groups.push(nix::unistd::Gid::from_raw(g));
|
||||
}
|
||||
Some(())
|
||||
};
|
||||
let mut lines = BufReader::new(
|
||||
File::open(chroot.join("etc/group"))
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, format!("open r /etc/group")))?,
|
||||
)
|
||||
.lines();
|
||||
while let Some(line) = lines.next().transpose()? {
|
||||
if handle_group_line(&line).is_none() {
|
||||
tracing::warn!("Invalid /etc/group line: {line}");
|
||||
}
|
||||
}
|
||||
|
||||
std::os::unix::fs::chroot(chroot)
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("chroot {chroot:?}")))?;
|
||||
cmd.args(args);
|
||||
for (k, v) in env {
|
||||
cmd.env(k, v);
|
||||
if let Ok(uid) = uid {
|
||||
if uid != 0 {
|
||||
std::os::unix::fs::chown("/proc/self/fd/0", Some(uid), gid.ok()).ok();
|
||||
std::os::unix::fs::chown("/proc/self/fd/1", Some(uid), gid.ok()).ok();
|
||||
std::os::unix::fs::chown("/proc/self/fd/2", Some(uid), gid.ok()).ok();
|
||||
}
|
||||
}
|
||||
// Handle credential changes in pre_exec to control the order:
|
||||
// setgroups must happen before setgid/setuid (requires CAP_SETGID)
|
||||
{
|
||||
let set_uid = uid.ok();
|
||||
let set_gid = gid.ok();
|
||||
unsafe {
|
||||
cmd.pre_exec(move || {
|
||||
if !groups.is_empty() {
|
||||
nix::unistd::setgroups(&groups)
|
||||
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
|
||||
}
|
||||
if let Some(gid) = set_gid {
|
||||
nix::unistd::setgid(nix::unistd::Gid::from_raw(gid))
|
||||
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
|
||||
}
|
||||
if let Some(uid) = set_uid {
|
||||
nix::unistd::setuid(nix::unistd::Uid::from_raw(uid))
|
||||
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
cmd.args(args);
|
||||
|
||||
if let Some(workdir) = workdir {
|
||||
cmd.current_dir(workdir);
|
||||
|
||||
@@ -50,6 +50,7 @@ use crate::util::io::{AsyncReadStream, AtomicFile, TermSize, delete_file};
|
||||
use crate::util::net::WebSocket;
|
||||
use crate::util::serde::Pem;
|
||||
use crate::util::sync::SyncMutex;
|
||||
use crate::util::tui::choose;
|
||||
use crate::volume::data_dir;
|
||||
use crate::{ActionId, CAP_1_KiB, DATA_DIR, HostId, ImageId, PackageId};
|
||||
|
||||
@@ -709,6 +710,19 @@ pub async fn rebuild(ctx: RpcContext, RebuildParams { id }: RebuildParams) -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct SubcontainerInfo {
|
||||
pub id: Guid,
|
||||
pub name: InternedString,
|
||||
pub image_id: ImageId,
|
||||
}
|
||||
impl std::fmt::Display for SubcontainerInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let SubcontainerInfo { id, name, image_id } = self;
|
||||
write!(f, "{id} => Name: {name}; Image: {image_id}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AttachParams {
|
||||
@@ -722,7 +736,7 @@ pub struct AttachParams {
|
||||
#[serde(rename = "__Auth_session")]
|
||||
session: Option<InternedString>,
|
||||
#[ts(type = "string | null")]
|
||||
subcontainer: Option<InternedString>,
|
||||
subcontainer: Option<Guid>,
|
||||
#[ts(type = "string | null")]
|
||||
name: Option<InternedString>,
|
||||
#[ts(type = "string | null")]
|
||||
@@ -745,7 +759,7 @@ pub async fn attach(
|
||||
user,
|
||||
}: AttachParams,
|
||||
) -> Result<Guid, Error> {
|
||||
let (container_id, subcontainer_id, image_id, workdir, root_command) = {
|
||||
let (container_id, subcontainer_id, image_id, user, workdir, root_command) = {
|
||||
let id = &id;
|
||||
|
||||
let service = ctx.services.get(id).await;
|
||||
@@ -786,13 +800,6 @@ pub async fn attach(
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let format_subcontainer_pair = |(guid, wrapper): (&Guid, &Subcontainer)| {
|
||||
format!(
|
||||
"{guid} imageId: {image_id} name: \"{name}\"",
|
||||
name = &wrapper.name,
|
||||
image_id = &wrapper.image_id
|
||||
)
|
||||
};
|
||||
let Some((subcontainer_id, image_id)) = subcontainer_ids
|
||||
.first()
|
||||
.map::<(Guid, ImageId), _>(|&x| (x.0.clone(), x.1.image_id.clone()))
|
||||
@@ -803,19 +810,17 @@ pub async fn attach(
|
||||
.lock()
|
||||
.await
|
||||
.iter()
|
||||
.map(format_subcontainer_pair)
|
||||
.join("\n");
|
||||
.map(|(g, s)| SubcontainerInfo {
|
||||
id: g.clone(),
|
||||
name: s.name.clone(),
|
||||
image_id: s.image_id.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"service.mod.no-matching-subcontainers",
|
||||
id = id,
|
||||
subcontainers = subcontainers
|
||||
)
|
||||
),
|
||||
eyre!("{}", t!("service.mod.no-matching-subcontainers", id = id)),
|
||||
ErrorKind::NotFound,
|
||||
));
|
||||
)
|
||||
.with_info(to_value(&subcontainers)?));
|
||||
};
|
||||
|
||||
let passwd = root_dir
|
||||
@@ -835,38 +840,39 @@ pub async fn attach(
|
||||
)
|
||||
.with_kind(ErrorKind::Deserialization)?;
|
||||
|
||||
let root_command = get_passwd_command(
|
||||
passwd,
|
||||
user.as_deref()
|
||||
.or_else(|| image_meta["user"].as_str())
|
||||
.unwrap_or("root"),
|
||||
)
|
||||
.await;
|
||||
let user = user
|
||||
.clone()
|
||||
.or_else(|| image_meta["user"].as_str().map(InternedString::intern))
|
||||
.unwrap_or_else(|| InternedString::intern("root"));
|
||||
|
||||
let root_command = get_passwd_command(passwd, &*user).await;
|
||||
|
||||
let workdir = image_meta["workdir"].as_str().map(|s| s.to_owned());
|
||||
|
||||
if subcontainer_ids.len() > 1 {
|
||||
let subcontainer_ids = subcontainer_ids
|
||||
let subcontainers = subcontainer_ids
|
||||
.into_iter()
|
||||
.map(format_subcontainer_pair)
|
||||
.join("\n");
|
||||
.map(|(g, s)| SubcontainerInfo {
|
||||
id: g.clone(),
|
||||
name: s.name.clone(),
|
||||
image_id: s.image_id.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"service.mod.multiple-subcontainers-found",
|
||||
id = id,
|
||||
subcontainer_ids = subcontainer_ids
|
||||
)
|
||||
t!("service.mod.multiple-subcontainers-found", id = id,)
|
||||
),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
)
|
||||
.with_info(to_value(&subcontainers)?));
|
||||
}
|
||||
|
||||
(
|
||||
service_ref.container_id()?,
|
||||
subcontainer_id,
|
||||
image_id,
|
||||
user.into(),
|
||||
workdir,
|
||||
root_command,
|
||||
)
|
||||
@@ -883,7 +889,7 @@ pub async fn attach(
|
||||
pty_size: Option<TermSize>,
|
||||
image_id: ImageId,
|
||||
workdir: Option<String>,
|
||||
user: Option<InternedString>,
|
||||
user: InternedString,
|
||||
root_command: &RootCommand,
|
||||
) -> Result<(), Error> {
|
||||
use axum::extract::ws::Message;
|
||||
@@ -904,11 +910,9 @@ pub async fn attach(
|
||||
Path::new("/media/startos/images")
|
||||
.join(image_id)
|
||||
.with_extension("env"),
|
||||
);
|
||||
|
||||
if let Some(user) = user {
|
||||
cmd.arg("--user").arg(&*user);
|
||||
}
|
||||
)
|
||||
.arg("--user")
|
||||
.arg(&*user);
|
||||
|
||||
if let Some(workdir) = workdir {
|
||||
cmd.arg("--workdir").arg(workdir);
|
||||
@@ -1091,45 +1095,6 @@ pub async fn attach(
|
||||
Ok(guid)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListSubcontainersParams {
|
||||
pub id: PackageId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubcontainerInfo {
|
||||
pub name: InternedString,
|
||||
pub image_id: ImageId,
|
||||
}
|
||||
|
||||
pub async fn list_subcontainers(
|
||||
ctx: RpcContext,
|
||||
ListSubcontainersParams { id }: ListSubcontainersParams,
|
||||
) -> Result<BTreeMap<Guid, SubcontainerInfo>, Error> {
|
||||
let service = ctx.services.get(&id).await;
|
||||
let service_ref = service.as_ref().or_not_found(&id)?;
|
||||
let container = &service_ref.seed.persistent_container;
|
||||
|
||||
let subcontainers = container.subcontainers.lock().await;
|
||||
|
||||
let result: BTreeMap<Guid, SubcontainerInfo> = subcontainers
|
||||
.iter()
|
||||
.map(|(guid, subcontainer)| {
|
||||
(
|
||||
guid.clone(),
|
||||
SubcontainerInfo {
|
||||
name: subcontainer.name.clone(),
|
||||
image_id: subcontainer.image_id.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn get_passwd_command(etc_passwd_path: PathBuf, user: &str) -> RootCommand {
|
||||
async {
|
||||
let mut file = tokio::fs::File::open(etc_passwd_path).await?;
|
||||
@@ -1210,23 +1175,34 @@ pub async fn cli_attach(
|
||||
None
|
||||
};
|
||||
|
||||
let method = parent_method.into_iter().chain(method).join(".");
|
||||
let mut params = json!({
|
||||
"id": params.id,
|
||||
"command": params.command,
|
||||
"tty": tty,
|
||||
"stderrTty": stderr.is_terminal(),
|
||||
"ptySize": if tty { TermSize::get_current() } else { None },
|
||||
"subcontainer": params.subcontainer,
|
||||
"imageId": params.image_id,
|
||||
"name": params.name,
|
||||
"user": params.user,
|
||||
});
|
||||
let guid: Guid = from_value(
|
||||
context
|
||||
.call_remote::<RpcContext>(
|
||||
&parent_method.into_iter().chain(method).join("."),
|
||||
json!({
|
||||
"id": params.id,
|
||||
"command": params.command,
|
||||
"tty": tty,
|
||||
"stderrTty": stderr.is_terminal(),
|
||||
"ptySize": if tty { TermSize::get_current() } else { None },
|
||||
"subcontainer": params.subcontainer,
|
||||
"imageId": params.image_id,
|
||||
"name": params.name,
|
||||
"user": params.user,
|
||||
}),
|
||||
)
|
||||
.await?,
|
||||
match context
|
||||
.call_remote::<RpcContext>(&method, params.clone())
|
||||
.await
|
||||
{
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
let prompt = e.to_string();
|
||||
let options: Vec<SubcontainerInfo> = from_value(e.info)?;
|
||||
let choice = choose(&prompt, &options).await?;
|
||||
params["subcontainer"] = to_value(&choice.id)?;
|
||||
context
|
||||
.call_remote::<RpcContext>(&method, params.clone())
|
||||
.await?
|
||||
}
|
||||
},
|
||||
)?;
|
||||
let mut ws = context.ws_continuation(guid).await?;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use clap::{ArgAction, Parser};
|
||||
use color_eyre::eyre::{Result, eyre};
|
||||
use exver::{Version, VersionRange};
|
||||
use futures::TryStreamExt;
|
||||
use imbl::OrdMap;
|
||||
use imbl_value::json;
|
||||
use itertools::Itertools;
|
||||
use patch_db::json_ptr::JsonPointer;
|
||||
@@ -245,6 +246,7 @@ async fn maybe_do_update(
|
||||
let mut available = from_value::<BTreeMap<Version, OsVersionInfo>>(
|
||||
ctx.call_remote_with::<RegistryContext, _>(
|
||||
"os.version.get",
|
||||
OrdMap::new(),
|
||||
json!({
|
||||
"source": current_version,
|
||||
"target": target,
|
||||
|
||||
Reference in New Issue
Block a user