mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
appmgr 0.3.0 rewrite pt 1
appmgr: split bins update cargo.toml and .gitignore context appmgr: refactor error module appmgr: context begin new s9pk format appmgr: add fields to manifest appmgr: start action abstraction appmgr: volume abstraction appmgr: improved volumes appmgr: install wip appmgr: health daemon appmgr: health checks appmgr: wip config get appmgr: secret store wip appmgr: config rewritten appmgr: delete non-reusable code appmgr: wip appmgr: please the borrow-checker appmgr: technically runs now appmgr: cli appmgr: clean up cli appmgr: rpc-toolkit in action appmgr: wrap up config appmgr: account for updates during install appmgr: fix: #308 appmgr: impl Display for Version appmgr: cleanup appmgr: set dependents on install appmgr: dependency health checks
This commit is contained in:
committed by
Aiden McClelland
parent
5741cf084f
commit
8954e3e338
234
appmgr/src/action/docker.rs
Normal file
234
appmgr/src/action/docker.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
use std::borrow::Cow;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::id::ImageId;
|
||||
use crate::net::host::Hosts;
|
||||
use crate::s9pk::manifest::{PackageId, SYSTEM_PACKAGE_ID};
|
||||
use crate::util::{Invoke, IoFormat, Version};
|
||||
use crate::volume::{VolumeId, Volumes};
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DockerAction {
|
||||
pub image: ImageId,
|
||||
#[serde(default)]
|
||||
pub system: bool,
|
||||
pub entrypoint: String,
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub mounts: IndexMap<VolumeId, PathBuf>,
|
||||
#[serde(default)]
|
||||
pub io_format: Option<IoFormat>,
|
||||
#[serde(default)]
|
||||
pub inject: bool,
|
||||
#[serde(default)]
|
||||
pub shm_size_mb: Option<usize>, // TODO: use postfix sizing? like 1k vs 1m vs 1g
|
||||
}
|
||||
impl DockerAction {
|
||||
pub async fn create(
|
||||
&self,
|
||||
pkg_id: &PackageId,
|
||||
pkg_version: &Version,
|
||||
volumes: &Volumes,
|
||||
ip: Ipv4Addr,
|
||||
) -> Result<(), Error> {
|
||||
tokio::process::Command::new("docker")
|
||||
.arg("create")
|
||||
.arg("--net")
|
||||
.arg("start9")
|
||||
.arg("--ip")
|
||||
.arg(format!("{}", ip))
|
||||
.arg("--name")
|
||||
.arg(Self::container_name(pkg_id, pkg_version))
|
||||
.args(self.docker_args(pkg_id, pkg_version, volumes, false))
|
||||
.invoke(crate::ErrorKind::Docker)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn execute<I: Serialize, O: for<'de> Deserialize<'de>>(
|
||||
&self,
|
||||
pkg_id: &PackageId,
|
||||
pkg_version: &Version,
|
||||
volumes: &Volumes,
|
||||
hosts: &Hosts,
|
||||
input: Option<I>,
|
||||
allow_inject: bool,
|
||||
) -> Result<Result<O, (i32, String)>, Error> {
|
||||
let mut cmd = tokio::process::Command::new("docker");
|
||||
if self.inject && allow_inject {
|
||||
cmd.arg("exec");
|
||||
} else {
|
||||
cmd.arg("run").arg("--rm");
|
||||
cmd.args(hosts.docker_args());
|
||||
}
|
||||
cmd.args(self.docker_args(pkg_id, pkg_version, volumes, allow_inject));
|
||||
let input_buf = if let (Some(input), Some(format)) = (&input, &self.io_format) {
|
||||
cmd.stdin(std::process::Stdio::piped());
|
||||
Some(format.to_vec(input)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut handle = cmd.spawn().with_kind(crate::ErrorKind::Docker)?;
|
||||
if let (Some(input), Some(stdin)) = (&input_buf, &mut handle.stdin) {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
stdin
|
||||
.write_all(input)
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Docker)?;
|
||||
}
|
||||
let res = handle
|
||||
.wait_with_output()
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Docker)?;
|
||||
Ok(if res.status.success() {
|
||||
Ok(if let Some(format) = &self.io_format {
|
||||
match format.from_slice(&res.stdout) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to deserialize stdout from {}: {}, falling back to UTF-8 string.",
|
||||
format,
|
||||
e
|
||||
);
|
||||
serde_json::from_value(String::from_utf8(res.stdout)?.into())
|
||||
.with_kind(crate::ErrorKind::Deserialization)?
|
||||
}
|
||||
}
|
||||
} else if res.stdout.is_empty() {
|
||||
serde_json::from_value(Value::Null).with_kind(crate::ErrorKind::Deserialization)?
|
||||
} else {
|
||||
serde_json::from_value(String::from_utf8(res.stdout)?.into())
|
||||
.with_kind(crate::ErrorKind::Deserialization)?
|
||||
})
|
||||
} else {
|
||||
Err((
|
||||
res.status.code().unwrap_or_default(),
|
||||
String::from_utf8(res.stderr)?,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn sandboxed<I: Serialize, O: for<'de> Deserialize<'de>>(
|
||||
&self,
|
||||
pkg_id: &PackageId,
|
||||
pkg_version: &Version,
|
||||
input: Option<I>,
|
||||
) -> Result<Result<O, (i32, String)>, Error> {
|
||||
let mut cmd = tokio::process::Command::new("docker");
|
||||
cmd.arg("run").arg("--rm");
|
||||
cmd.arg("--network=none");
|
||||
cmd.args(self.docker_args(pkg_id, pkg_version, &Volumes::default(), false));
|
||||
let input_buf = if let (Some(input), Some(format)) = (&input, &self.io_format) {
|
||||
cmd.stdin(std::process::Stdio::piped());
|
||||
Some(format.to_vec(input)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut handle = cmd.spawn().with_kind(crate::ErrorKind::Docker)?;
|
||||
if let (Some(input), Some(stdin)) = (&input_buf, &mut handle.stdin) {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
stdin
|
||||
.write_all(input)
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Docker)?;
|
||||
}
|
||||
let res = handle
|
||||
.wait_with_output()
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Docker)?;
|
||||
Ok(if res.status.success() {
|
||||
Ok(if let Some(format) = &self.io_format {
|
||||
match format.from_slice(&res.stdout) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to deserialize stdout from {}: {}, falling back to UTF-8 string.",
|
||||
format,
|
||||
e
|
||||
);
|
||||
serde_json::from_value(String::from_utf8(res.stdout)?.into())
|
||||
.with_kind(crate::ErrorKind::Deserialization)?
|
||||
}
|
||||
}
|
||||
} else if res.stdout.is_empty() {
|
||||
serde_json::from_value(Value::Null).with_kind(crate::ErrorKind::Deserialization)?
|
||||
} else {
|
||||
serde_json::from_value(String::from_utf8(res.stdout)?.into())
|
||||
.with_kind(crate::ErrorKind::Deserialization)?
|
||||
})
|
||||
} else {
|
||||
Err((
|
||||
res.status.code().unwrap_or_default(),
|
||||
String::from_utf8(res.stderr)?,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn container_name(pkg_id: &PackageId, version: &Version) -> String {
|
||||
format!("service_{}_{}", pkg_id, version)
|
||||
}
|
||||
|
||||
pub fn uncontainer_name(name: &str) -> Option<&str> {
|
||||
name.strip_prefix("service_")
|
||||
.and_then(|name| name.split("_").next())
|
||||
}
|
||||
|
||||
fn docker_args<'a>(
|
||||
&'a self,
|
||||
pkg_id: &PackageId,
|
||||
pkg_version: &Version,
|
||||
volumes: &Volumes,
|
||||
allow_inject: bool,
|
||||
) -> Vec<Cow<'a, OsStr>> {
|
||||
let mut res = Vec::with_capacity(
|
||||
(2 * self.mounts.len()) // --mount <MOUNT_ARG>
|
||||
+ (2 * self.shm_size_mb.is_some() as usize) // --shm-size <SHM_SIZE>
|
||||
+ 3 // --entrypoint <ENTRYPOINT> <IMAGE>
|
||||
+ self.args.len(), // [ARG...]
|
||||
);
|
||||
for (volume_id, dst) in &self.mounts {
|
||||
let src = if let Some(path) = volumes.get_path_for(pkg_id, volume_id) {
|
||||
path
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
res.push(OsStr::new("--mount").into());
|
||||
res.push(
|
||||
OsString::from(format!(
|
||||
"type=bind,src={},dst={}",
|
||||
src.display(),
|
||||
dst.display()
|
||||
))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if let Some(shm_size_mb) = self.shm_size_mb {
|
||||
res.push(OsStr::new("--shm-size").into());
|
||||
res.push(OsString::from(format!("{}m", shm_size_mb)).into());
|
||||
}
|
||||
if self.inject && allow_inject {
|
||||
res.push(OsString::from(Self::container_name(pkg_id, pkg_version)).into());
|
||||
res.push(OsStr::new(&self.entrypoint).into());
|
||||
} else {
|
||||
res.push(OsStr::new("--entrypoint").into());
|
||||
res.push(OsStr::new(&self.entrypoint).into());
|
||||
if self.system {
|
||||
res.push(OsString::from(self.image.for_package(SYSTEM_PACKAGE_ID, None)).into());
|
||||
} else {
|
||||
res.push(OsString::from(self.image.for_package(pkg_id, Some(pkg_version))).into());
|
||||
}
|
||||
}
|
||||
res.extend(self.args.iter().map(|s| OsStr::new(s).into()));
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
160
appmgr/src/action/mod.rs
Normal file
160
appmgr/src/action/mod.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use patch_db::HasModel;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use self::docker::DockerAction;
|
||||
use crate::config::{Config, ConfigSpec};
|
||||
use crate::id::Id;
|
||||
use crate::net::host::Hosts;
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::{ValuePrimative, Version};
|
||||
use crate::volume::Volumes;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub mod docker;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
|
||||
pub struct ActionId<S: AsRef<str> = String>(Id<S>);
|
||||
impl<S: AsRef<str>> AsRef<ActionId<S>> for ActionId<S> {
|
||||
fn as_ref(&self) -> &ActionId<S> {
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> std::fmt::Display for ActionId<S> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.0)
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<str> for ActionId<S> {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<Path> for ActionId<S> {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref().as_ref()
|
||||
}
|
||||
}
|
||||
impl<'de, S> Deserialize<'de> for ActionId<S>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
Id<S>: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
Ok(ActionId(Deserialize::deserialize(deserializer)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Actions(pub IndexMap<ActionId, Action>);
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "version")]
|
||||
pub enum ActionResult {
|
||||
#[serde(rename = "0")]
|
||||
V0(ActionResultV0),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ActionResultV0 {
|
||||
pub message: String,
|
||||
pub value: ValuePrimative,
|
||||
pub copyable: bool,
|
||||
pub qr: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum DockerStatus {
|
||||
Running,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Action {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub warning: Option<String>,
|
||||
pub implementation: ActionImplementation,
|
||||
pub allowed_statuses: IndexSet<DockerStatus>,
|
||||
#[serde(default)]
|
||||
pub input_spec: ConfigSpec,
|
||||
}
|
||||
impl Action {
|
||||
pub async fn execute(
|
||||
&self,
|
||||
pkg_id: &PackageId,
|
||||
pkg_version: &Version,
|
||||
volumes: &Volumes,
|
||||
hosts: &Hosts,
|
||||
input: Config,
|
||||
) -> Result<ActionResult, Error> {
|
||||
self.input_spec
|
||||
.matches(&input)
|
||||
.with_kind(crate::ErrorKind::ConfigSpecViolation)?;
|
||||
self.implementation
|
||||
.execute(pkg_id, pkg_version, volumes, hosts, Some(input), true)
|
||||
.await?
|
||||
.map_err(|e| Error::new(anyhow!("{}", e.1), crate::ErrorKind::Action))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename = "kebab-case")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ActionImplementation {
|
||||
Docker(DockerAction),
|
||||
}
|
||||
impl ActionImplementation {
|
||||
pub async fn install(
|
||||
&self,
|
||||
pkg_id: &PackageId,
|
||||
pkg_version: &Version,
|
||||
volumes: &Volumes,
|
||||
ip: Ipv4Addr,
|
||||
) -> Result<(), Error> {
|
||||
match self {
|
||||
ActionImplementation::Docker(action) => {
|
||||
action.create(pkg_id, pkg_version, volumes, ip).await
|
||||
}
|
||||
}
|
||||
}
|
||||
pub async fn execute<I: Serialize, O: for<'de> Deserialize<'de>>(
|
||||
&self,
|
||||
pkg_id: &PackageId,
|
||||
pkg_version: &Version,
|
||||
volumes: &Volumes,
|
||||
hosts: &Hosts,
|
||||
input: Option<I>,
|
||||
allow_inject: bool,
|
||||
) -> Result<Result<O, (i32, String)>, Error> {
|
||||
match self {
|
||||
ActionImplementation::Docker(action) => {
|
||||
action
|
||||
.execute(pkg_id, pkg_version, volumes, hosts, input, allow_inject)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
pub async fn sandboxed<I: Serialize, O: for<'de> Deserialize<'de>>(
|
||||
&self,
|
||||
pkg_id: &PackageId,
|
||||
pkg_version: &Version,
|
||||
input: Option<I>,
|
||||
) -> Result<Result<O, (i32, String)>, Error> {
|
||||
match self {
|
||||
ActionImplementation::Docker(action) => {
|
||||
action.sandboxed(pkg_id, pkg_version, input).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user