mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 18:31:52 +00:00
compat
This commit is contained in:
1
appmgr/.gitignore
vendored
1
appmgr/.gitignore
vendored
@@ -3,3 +3,4 @@
|
||||
.DS_Store
|
||||
.vscode
|
||||
secrets.db
|
||||
*.s9pk
|
||||
22
appmgr/Cargo.lock
generated
22
appmgr/Cargo.lock
generated
@@ -710,6 +710,7 @@ dependencies = [
|
||||
"sha2 0.9.5",
|
||||
"simple-logging",
|
||||
"sqlx",
|
||||
"tar",
|
||||
"thiserror",
|
||||
"tokio 1.9.0",
|
||||
"tokio-compat-02",
|
||||
@@ -1563,6 +1564,15 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "111.15.0+1.1.1k"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1a5f6ae2ac04393b217ea9f700cd04fa9bf3d93fae2872069f3d15d908af70a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.65"
|
||||
@@ -1572,6 +1582,7 @@ dependencies = [
|
||||
"autocfg",
|
||||
"cc",
|
||||
"libc",
|
||||
"openssl-src",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
@@ -2538,6 +2549,17 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d779dc6aeff029314570f666ec83f19df7280bb36ef338442cfa8c604021b80"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.2.0"
|
||||
|
||||
@@ -64,7 +64,7 @@ lazy_static = "1.4"
|
||||
libc = "0.2.86"
|
||||
log = "0.4.11"
|
||||
nix = "0.20.0"
|
||||
openssl = "0.10.30"
|
||||
openssl = { version="0.10.30", features=["vendored"] }
|
||||
patch-db = { version="*", path="../../patch-db/patch-db" }
|
||||
pin-project = "1.0.6"
|
||||
prettytable-rs = "0.8.0"
|
||||
@@ -83,6 +83,7 @@ serde_yaml = "0.8.14"
|
||||
sha2 = "0.9.3"
|
||||
simple-logging = "2.0"
|
||||
sqlx = { version="0.5", features=["runtime-tokio-rustls", "sqlite", "offline"] }
|
||||
tar = "0.4.35"
|
||||
thiserror = "1.0.24"
|
||||
tokio = { version="1.5.0", features=["full"] }
|
||||
tokio-compat-02 = "0.2.0"
|
||||
|
||||
Binary file not shown.
@@ -77,6 +77,8 @@ impl DockerAction {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
cmd.stdout(std::process::Stdio::piped());
|
||||
cmd.stderr(std::process::Stdio::piped());
|
||||
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;
|
||||
@@ -133,6 +135,8 @@ impl DockerAction {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
cmd.stdout(std::process::Stdio::piped());
|
||||
cmd.stderr(std::process::Stdio::piped());
|
||||
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;
|
||||
@@ -177,9 +181,11 @@ impl DockerAction {
|
||||
format!("service_{}_{}", pkg_id, version)
|
||||
}
|
||||
|
||||
pub fn uncontainer_name(name: &str) -> Option<&str> {
|
||||
name.strip_prefix("service_")
|
||||
.and_then(|name| name.split("_").next())
|
||||
pub fn uncontainer_name(name: &str) -> Option<(&str, Version)> {
|
||||
name.trim_start_matches("/")
|
||||
.strip_prefix("service_")
|
||||
.and_then(|name| name.split_once("_"))
|
||||
.and_then(|(id, version)| Some((id, version.parse().ok()?)))
|
||||
}
|
||||
|
||||
fn docker_args<'a>(
|
||||
@@ -196,18 +202,20 @@ impl DockerAction {
|
||||
+ 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
|
||||
let volume = if let Some(v) = volumes.get(volume_id) {
|
||||
v
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let src = volume.path_for(pkg_id, pkg_version, volume_id);
|
||||
res.push(OsStr::new("--mount").into());
|
||||
res.push(
|
||||
OsString::from(format!(
|
||||
"type=bind,src={},dst={}",
|
||||
dbg!(OsString::from(format!(
|
||||
"type=bind,src={},dst={}{}",
|
||||
src.display(),
|
||||
dst.display()
|
||||
))
|
||||
dst.display(),
|
||||
if volume.readonly() { ",readonly" } else { "" }
|
||||
)))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use crate::util::Version;
|
||||
use crate::Error;
|
||||
|
||||
pub const SYSTEM_ID: Id<&'static str> = Id("SYSTEM");
|
||||
pub const SYSTEM_ID: Id<&'static str> = Id("x_system");
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Invalid ID")]
|
||||
|
||||
@@ -379,8 +379,6 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
let interface_info = manifest.interfaces.install(&mut sql_tx, pkg_id, ip).await?;
|
||||
log::info!("Install {}@{}: Installed interfaces", pkg_id, version);
|
||||
|
||||
log::info!("Install {}@{}: Complete", pkg_id, version);
|
||||
|
||||
let static_files = StaticFiles::local(pkg_id, version, manifest.assets.icon_type());
|
||||
let current_dependencies = manifest
|
||||
.dependencies
|
||||
@@ -489,11 +487,19 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Install {}@{}: Syncing Tor", pkg_id, version);
|
||||
ctx.tor_controller.sync(&mut tx, &mut sql_tx).await?;
|
||||
log::info!("Install {}@{}: Synced Tor", pkg_id, version);
|
||||
#[cfg(feature = "avahi")]
|
||||
ctx.mdns_controller.sync(&mut tx).await?;
|
||||
{
|
||||
log::info!("Install {}@{}: Syncing MDNS", pkg_id, version);
|
||||
ctx.mdns_controller.sync(&mut tx).await?;
|
||||
log::info!("Install {}@{}: Synced MDNS", pkg_id, version);
|
||||
}
|
||||
|
||||
tx.commit(None).await?;
|
||||
|
||||
log::info!("Install {}@{}: Complete", pkg_id, version);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct S9pkPacker<
|
||||
RInstructions: Read,
|
||||
RIcon: Read,
|
||||
RDockerImages: Read,
|
||||
RAssets: Read,
|
||||
> {
|
||||
writer: W,
|
||||
manifest: &'a Manifest,
|
||||
@@ -25,6 +26,7 @@ pub struct S9pkPacker<
|
||||
instructions: RInstructions,
|
||||
icon: RIcon,
|
||||
docker_images: RDockerImages,
|
||||
assets: RAssets,
|
||||
}
|
||||
impl<
|
||||
'a,
|
||||
@@ -33,7 +35,8 @@ impl<
|
||||
RInstructions: Read,
|
||||
RIcon: Read,
|
||||
RDockerImages: Read,
|
||||
> S9pkPacker<'a, W, RLicense, RInstructions, RIcon, RDockerImages>
|
||||
RAssets: Read,
|
||||
> S9pkPacker<'a, W, RLicense, RInstructions, RIcon, RDockerImages, RAssets>
|
||||
{
|
||||
/// BLOCKING
|
||||
pub fn pack(mut self, key: &ed25519_dalek::Keypair) -> Result<(), Error> {
|
||||
@@ -93,13 +96,22 @@ impl<
|
||||
position = new_pos;
|
||||
// docker_images
|
||||
std::io::copy(&mut self.docker_images, &mut writer)
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying App Image"))?;
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying Docker Images"))?;
|
||||
let new_pos = writer.stream_position()?;
|
||||
header.table_of_contents.docker_images = FileSection {
|
||||
position,
|
||||
length: new_pos - position,
|
||||
};
|
||||
position = new_pos;
|
||||
// docker_images
|
||||
std::io::copy(&mut self.assets, &mut writer)
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying Assets"))?;
|
||||
let new_pos = writer.stream_position()?;
|
||||
header.table_of_contents.assets = FileSection {
|
||||
position,
|
||||
length: new_pos - position,
|
||||
};
|
||||
position = new_pos;
|
||||
|
||||
// header
|
||||
let (hash, _) = writer.finish();
|
||||
|
||||
@@ -74,6 +74,7 @@ pub struct TableOfContents {
|
||||
pub instructions: FileSection,
|
||||
pub icon: FileSection,
|
||||
pub docker_images: FileSection,
|
||||
pub assets: FileSection,
|
||||
}
|
||||
impl TableOfContents {
|
||||
pub fn serialize<W: Write>(&self, mut writer: W) -> std::io::Result<()> {
|
||||
@@ -81,7 +82,8 @@ impl TableOfContents {
|
||||
+ (1 + "license".len() + 16)
|
||||
+ (1 + "instructions".len() + 16)
|
||||
+ (1 + "icon".len() + 16)
|
||||
+ (1 + "docker_images".len() + 16)) as u32;
|
||||
+ (1 + "docker_images".len() + 16)
|
||||
+ (1 + "assets".len() + 16)) as u32;
|
||||
writer.write_all(&u32::to_be_bytes(len))?;
|
||||
self.manifest.serialize_entry("manifest", &mut writer)?;
|
||||
self.license.serialize_entry("license", &mut writer)?;
|
||||
@@ -90,6 +92,7 @@ impl TableOfContents {
|
||||
self.icon.serialize_entry("icon", &mut writer)?;
|
||||
self.docker_images
|
||||
.serialize_entry("docker_images", &mut writer)?;
|
||||
self.assets.serialize_entry("assets", &mut writer)?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn deserialize<R: AsyncRead + Unpin>(mut reader: R) -> std::io::Result<Self> {
|
||||
@@ -126,6 +129,7 @@ impl TableOfContents {
|
||||
instructions: from_table(&table, "instructions")?,
|
||||
icon: from_table(&table, "icon")?,
|
||||
docker_images: from_table(&table, "docker_images")?,
|
||||
assets: from_table(&table, "assets")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,11 +139,13 @@ pub struct Assets {
|
||||
#[serde(default)]
|
||||
pub license: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub instructions: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub icon: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub docker_images: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub instructions: Option<PathBuf>,
|
||||
pub assets: Option<PathBuf>,
|
||||
}
|
||||
impl Assets {
|
||||
pub fn license_path(&self) -> &Path {
|
||||
@@ -152,6 +154,12 @@ impl Assets {
|
||||
.map(|a| a.as_path())
|
||||
.unwrap_or(Path::new("LICENSE.md"))
|
||||
}
|
||||
pub fn instructions_path(&self) -> &Path {
|
||||
self.instructions
|
||||
.as_ref()
|
||||
.map(|a| a.as_path())
|
||||
.unwrap_or(Path::new("INSTRUCTIONS.md"))
|
||||
}
|
||||
pub fn icon_path(&self) -> &Path {
|
||||
self.icon
|
||||
.as_ref()
|
||||
@@ -171,11 +179,11 @@ impl Assets {
|
||||
.map(|a| a.as_path())
|
||||
.unwrap_or(Path::new("image.tar"))
|
||||
}
|
||||
pub fn instructions_path(&self) -> &Path {
|
||||
self.instructions
|
||||
pub fn assets_path(&self) -> &Path {
|
||||
self.assets
|
||||
.as_ref()
|
||||
.map(|a| a.as_path())
|
||||
.unwrap_or(Path::new("INSTRUCTIONS.md"))
|
||||
.unwrap_or(Path::new("assets"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::s9pk::builder::S9pkPacker;
|
||||
use crate::s9pk::manifest::Manifest;
|
||||
use crate::s9pk::reader::S9pkReader;
|
||||
use crate::util::display_none;
|
||||
use crate::volume::Volume;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub mod builder;
|
||||
@@ -79,6 +80,22 @@ pub fn pack(#[context] ctx: EitherContext, #[arg] path: Option<PathBuf>) -> Resu
|
||||
)
|
||||
})?,
|
||||
)
|
||||
.assets({
|
||||
let mut assets = tar::Builder::new(Vec::new()); // TODO: Ideally stream this? best not to buffer in memory
|
||||
|
||||
for (asset_volume, _) in manifest
|
||||
.volumes
|
||||
.iter()
|
||||
.filter(|(_, v)| matches!(v, &&Volume::Assets {}))
|
||||
{
|
||||
assets.append_dir_all(
|
||||
asset_volume,
|
||||
path.join(manifest.assets.assets_path()).join(asset_volume),
|
||||
)?;
|
||||
}
|
||||
|
||||
std::io::Cursor::new(assets.into_inner()?)
|
||||
})
|
||||
.build()
|
||||
.pack(&ctx.developer_key()?)?;
|
||||
outfile.sync_all()?;
|
||||
|
||||
@@ -141,4 +141,8 @@ impl<R: AsyncRead + AsyncSeek + Unpin> S9pkReader<R> {
|
||||
pub async fn docker_images<'a>(&'a mut self) -> Result<ReadHandle<'a, R>, Error> {
|
||||
Ok(self.read_handle(self.toc.docker_images).await?)
|
||||
}
|
||||
|
||||
pub async fn assets<'a>(&'a mut self) -> Result<ReadHandle<'a, R>, Error> {
|
||||
Ok(self.read_handle(self.toc.assets).await?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ pub async fn synchronize_all(ctx: &RpcContext) -> Result<(), Error> {
|
||||
let mut fuckening = false;
|
||||
for summary in info {
|
||||
let id = if let Some(id) = summary.names.iter().flatten().find_map(|s| {
|
||||
DockerAction::uncontainer_name(s.as_str()).and_then(|id| pkg_ids.take(id))
|
||||
DockerAction::uncontainer_name(s.as_str()).and_then(|(id, _)| pkg_ids.take(id))
|
||||
}) {
|
||||
id
|
||||
} else {
|
||||
|
||||
@@ -518,6 +518,15 @@ impl std::fmt::Display for Version {
|
||||
write!(f, "{}", self.string)
|
||||
}
|
||||
}
|
||||
impl std::str::FromStr for Version {
|
||||
type Err = <emver::Version as FromStr>::Err;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Version {
|
||||
string: s.to_owned(),
|
||||
version: s.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl From<emver::Version> for Version {
|
||||
fn from(v: emver::Version) -> Self {
|
||||
Version {
|
||||
|
||||
@@ -9,6 +9,7 @@ use serde::{Deserialize, Deserializer, Serialize};
|
||||
use crate::id::{Id, IdUnchecked};
|
||||
use crate::net::interface::InterfaceId;
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::Version;
|
||||
|
||||
pub mod disk;
|
||||
|
||||
@@ -65,16 +66,18 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
|
||||
pub struct CustomVolumeId<S: AsRef<str> = String>(Id<S>);
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Volumes(IndexMap<VolumeId, Volume>);
|
||||
impl Volumes {
|
||||
pub fn get_path_for(&self, pkg_id: &PackageId, volume_id: &VolumeId) -> Option<PathBuf> {
|
||||
pub fn get_path_for(
|
||||
&self,
|
||||
pkg_id: &PackageId,
|
||||
version: &Version,
|
||||
volume_id: &VolumeId,
|
||||
) -> Option<PathBuf> {
|
||||
self.0
|
||||
.get(volume_id)
|
||||
.map(|volume| volume.path_for(pkg_id, volume_id))
|
||||
.map(|volume| volume.path_for(pkg_id, version, volume_id))
|
||||
}
|
||||
pub fn to_readonly(&self) -> Self {
|
||||
Volumes(
|
||||
@@ -121,6 +124,8 @@ pub enum Volume {
|
||||
readonly: bool,
|
||||
},
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
Assets {},
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
Pointer {
|
||||
package_id: PackageId,
|
||||
volume_id: VolumeId,
|
||||
@@ -134,22 +139,31 @@ pub enum Volume {
|
||||
Backup { readonly: bool },
|
||||
}
|
||||
impl Volume {
|
||||
pub fn path_for(&self, pkg_id: &PackageId, volume_id: &VolumeId) -> PathBuf {
|
||||
pub fn path_for(&self, pkg_id: &PackageId, version: &Version, volume_id: &VolumeId) -> PathBuf {
|
||||
match self {
|
||||
Volume::Data { .. } => Path::new(PKG_VOLUME_DIR)
|
||||
.join(pkg_id)
|
||||
.join("volumes")
|
||||
.join(volume_id),
|
||||
Volume::Assets {} => Path::new(PKG_VOLUME_DIR)
|
||||
.join(pkg_id)
|
||||
.join("assets")
|
||||
.join(version.as_str())
|
||||
.join(volume_id),
|
||||
Volume::Pointer {
|
||||
package_id,
|
||||
volume_id,
|
||||
path,
|
||||
..
|
||||
} => Path::new(PKG_VOLUME_DIR)
|
||||
} => dbg!(Path::new(PKG_VOLUME_DIR)
|
||||
.join(package_id)
|
||||
.join("volumes")
|
||||
.join(volume_id)
|
||||
.join(path),
|
||||
.join(if path.is_absolute() {
|
||||
path.strip_prefix("/").unwrap()
|
||||
} else {
|
||||
path.as_ref()
|
||||
})),
|
||||
Volume::Certificate { interface_id } => Path::new(PKG_VOLUME_DIR)
|
||||
.join(pkg_id)
|
||||
.join("certificates")
|
||||
@@ -174,6 +188,7 @@ impl Volume {
|
||||
pub fn readonly(&self) -> bool {
|
||||
match self {
|
||||
Volume::Data { readonly } => *readonly,
|
||||
Volume::Assets {} => true,
|
||||
Volume::Pointer { readonly, .. } => *readonly,
|
||||
Volume::Certificate { .. } => true,
|
||||
Volume::Backup { readonly } => *readonly,
|
||||
|
||||
4
compat/.gitignore
vendored
Normal file
4
compat/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.vscode
|
||||
3079
compat/Cargo.lock
generated
Normal file
3079
compat/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
compat/Cargo.toml
Normal file
13
compat/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "compat"
|
||||
version = "0.1.0"
|
||||
authors = ["Aiden McClelland <me@drbonez.dev>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.3"
|
||||
serde = { version="1.0.118", features=["derive", "rc"] }
|
||||
serde_yaml = "0.8.17"
|
||||
embassy-os = { path="../appmgr", default-features=false }
|
||||
5
compat/Dockerfile
Normal file
5
compat/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM alpine:latest
|
||||
|
||||
ADD ./target/aarch64-unknown-linux-musl/release/compat /usr/local/bin/compat
|
||||
|
||||
ENTRYPOINT ["compat"]
|
||||
45
compat/src/main.rs
Normal file
45
compat/src/main.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use std::{fs::File, io::stdout, path::Path};
|
||||
|
||||
use clap::{App, Arg, SubCommand};
|
||||
use embassy::config::action::{ConfigRes, SetResult};
|
||||
|
||||
fn main() {
|
||||
let app = App::new("compat").subcommand(
|
||||
SubCommand::with_name("config").subcommand(
|
||||
SubCommand::with_name("get")
|
||||
.arg(
|
||||
Arg::with_name("mountpoint")
|
||||
.help("The `mount` field from manifest.yaml")
|
||||
.required(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("spec")
|
||||
.help("The path to the config spec in the container")
|
||||
.required(true),
|
||||
),
|
||||
),
|
||||
);
|
||||
let matches = app.get_matches();
|
||||
match matches.subcommand() {
|
||||
("config", Some(sub_m)) => match sub_m.subcommand() {
|
||||
("get", Some(sub_m)) => {
|
||||
let cfg_path =
|
||||
Path::new(sub_m.value_of("mountpoint").unwrap()).join("start9/config.yaml");
|
||||
let cfg = if cfg_path.exists() {
|
||||
Some(serde_yaml::from_reader(File::open(cfg_path).unwrap()).unwrap())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let spec_path = Path::new(sub_m.value_of("spec").unwrap());
|
||||
let spec = serde_yaml::from_reader(File::open(spec_path).unwrap()).unwrap();
|
||||
serde_yaml::to_writer(stdout(), &ConfigRes { config: cfg, spec }).unwrap();
|
||||
}
|
||||
(subcmd, _) => {
|
||||
panic!("unknown subcommand: {}", subcmd);
|
||||
}
|
||||
},
|
||||
(subcmd, _) => {
|
||||
panic!("unknown subcommand: {}", subcmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user