diff --git a/app/src/handlers.rs b/app/src/handlers.rs index 3b5ca5c..4e6d55c 100644 --- a/app/src/handlers.rs +++ b/app/src/handlers.rs @@ -10,7 +10,8 @@ pub async fn create_container( ) -> impl Responder { let cid = info.container_id.clone(); let image = info.image.clone(); - service.create_container(image, cid).await; + let ns = info.ns.clone(); + service.create_container(&image, &cid, &ns).await.unwrap(); HttpResponse::Ok().json("Container created successfully!") } @@ -20,12 +21,18 @@ pub async fn remove_container( info: web::Json, ) -> impl Responder { let container_id = info.container_id.clone(); - service.remove_container(container_id).await; + let ns = info.ns.clone(); + service.remove_container(&container_id, &ns).await.unwrap(); HttpResponse::Ok().json("Container removed successfully!") } -pub async fn get_container_list(service: web::Data>) -> impl Responder { - let container_list = service.get_container_list().await.unwrap(); +/// 获取容器列表 +pub async fn get_container_list( + service: web::Data>, + info: web::Json, +) -> impl Responder { + let ns = info.ns.clone(); + let container_list = service.get_container_list(&ns).await.unwrap(); HttpResponse::Ok().json(container_list) } diff --git a/app/src/types.rs b/app/src/types.rs index d3b52df..9b843e6 100644 --- a/app/src/types.rs +++ b/app/src/types.rs @@ -4,14 +4,17 @@ use serde::{Deserialize, Serialize}; pub struct CreateContainerInfo { pub container_id: String, pub image: String, + pub ns: String, } #[derive(Serialize, Deserialize)] pub struct RemoveContainerInfo { pub container_id: String, + pub ns: String, } #[derive(Deserialize)] pub struct GetContainerListQuery { pub status: Option, + pub ns: String, } diff --git a/service/Cargo.toml b/service/Cargo.toml index 481b9a4..09a441c 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -11,4 +11,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" log = "0.4" env_logger = "0.10" -prost-types = "0.13.4" \ No newline at end of file +prost-types = "0.13.4" +oci-spec = "0.6" +sha2 = "0.10" +hex = "0.4" \ No newline at end of file diff --git a/service/src/lib.rs b/service/src/lib.rs index f63c7a5..212fe0b 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -1,25 +1,30 @@ -pub mod spec; +pub mod spec; use containerd_client::{ services::v1::{ - container::Runtime, Container, CreateContainerRequest, CreateTaskRequest, - DeleteContainerRequest, DeleteTaskRequest, GetImageRequest, KillRequest, - ListContainersRequest, ListTasksRequest, StartRequest, WaitRequest, + container::Runtime, + snapshots::{MountsRequest, PrepareSnapshotRequest}, + Container, CreateContainerRequest, CreateTaskRequest, DeleteContainerRequest, + DeleteTaskRequest, GetImageRequest, KillRequest, ListContainersRequest, ListTasksRequest, + ReadContentRequest, StartRequest, WaitRequest, }, tonic::Request, + types::Mount, with_namespace, Client, }; +use oci_spec::image::{Arch, ImageConfiguration, ImageIndex, ImageManifest, MediaType, Os}; use prost_types::Any; +use sha2::{Digest, Sha256}; +use spec::generate_spec; use std::{ - collections::HashMap, - fs::{self, File}, + fs, sync::{Arc, Mutex}, time::Duration, }; use tokio::time::timeout; // config.json,dockerhub密钥 -const DOCKER_CONFIG_DIR: &str = "/var/lib/faasd/.docker/"; +// const DOCKER_CONFIG_DIR: &str = "/var/lib/faasd/.docker/"; // 命名空间(容器的) const NAMESPACE: &str = "default"; @@ -37,21 +42,59 @@ impl Service { }) } - pub async fn create_container(&self, image: String, cid: String) { - let spec = include_str!("../../container_spec.json").to_string(); + async fn prepare_snapshot(&self, cid: &str, ns: &str) -> Result, Err> { + let parent_snapshot = self.get_parent_snapshot(cid, ns).await?; + let req = PrepareSnapshotRequest { + snapshotter: "overlayfs".to_string(), + key: cid.to_string(), + parent: parent_snapshot, + ..Default::default() + }; + let resp = self + .client + .lock() + .unwrap() + .snapshots() + .prepare(with_namespace!(req, ns)) + .await? + .into_inner() + .mounts; + + Ok(resp) + } + + pub async fn create_container( + &self, + image_name: &str, + cid: &str, + ns: &str, + ) -> Result<(), Err> { + let namespace = match ns { + "" => spec::DEFAULT_NAMESPACE, + _ => ns, + }; + + let _mount = self.prepare_snapshot(cid, ns).await?; + + let spec_path = generate_spec(&cid, ns).unwrap(); + let spec = fs::read_to_string(spec_path).unwrap(); + let spec = Any { type_url: "types.containerd.io/opencontainers/runtime-spec/1/Spec".to_string(), value: spec.into_bytes(), }; + let mut containers_client = self.client.lock().unwrap().containers(); let container = Container { id: cid.to_string(), - image, + image: image_name.to_string(), runtime: Some(Runtime { name: "io.containerd.runc.v2".to_string(), options: None, }), spec: Some(spec), + snapshotter: "overlayfs".to_string(), + snapshot_key: cid.to_string(), ..Default::default() }; @@ -59,38 +102,49 @@ impl Service { container: Some(container), }; - let req = with_namespace!(req, NAMESPACE); + let req = with_namespace!(req, namespace); let _resp = containers_client .create(req) .await .expect("Failed to create container"); - println!("Container: {:?} created", cid); + // println!("Container: {:?} created", cid); + Ok(()) } - pub async fn remove_container(&self, container_id: String) { + pub async fn remove_container(&self, cid: &str, ns: &str) -> Result<(), Err> { + let namespace = match ns { + "" => NAMESPACE, + _ => ns, + }; let c = self.client.lock().unwrap(); - let mut containers_client = c.containers(); - let request = Request::new(ListContainersRequest { + let request = ListContainersRequest { ..Default::default() - }); + }; + let mut cc = c.containers(); - let responce = containers_client.list(request).await.unwrap().into_inner(); + let responce = cc + .list(with_namespace!(request, namespace)) + .await? + .into_inner(); let container = responce .containers .iter() - .find(|container| container.id == container_id); + .find(|container| container.id == cid); if let Some(container) = container { - let mut tasks_client = c.tasks(); + let mut tc = c.tasks(); - let request = Request::new(ListTasksRequest { - filter: format!("container=={}", container_id), + let request = ListTasksRequest { + filter: format!("container=={}", cid), ..Default::default() - }); - let responce = tasks_client.list(request).await.unwrap().into_inner(); - drop(tasks_client); + }; + let responce = tc + .list(with_namespace!(request, namespace)) + .await? + .into_inner(); + drop(tc); if let Some(task) = responce .tasks .iter() @@ -104,79 +158,104 @@ impl Service { // TASK_EXITED (4) — 任务已退出 // TASK_PAUSED (5) — 任务已暂停 // TASK_FAILED (6) — 任务失败 - self.delete_task(&task.container_id).await; + self.delete_task(&task.container_id, ns).await; } let delete_request = DeleteContainerRequest { id: container.id.clone(), ..Default::default() }; - let delete_request = with_namespace!(delete_request, NAMESPACE); - let _ = containers_client - .delete(delete_request) + let _ = cc + .delete(with_namespace!(delete_request, namespace)) .await .expect("Failed to delete container"); - println!("Container: {:?} deleted", containers_client); + // println!("Container: {:?} deleted", cc); } else { todo!("Container not found"); } - drop(containers_client); + drop(cc); + Ok(()) } - pub async fn create_and_start_task(&self, container_id: String) { - let tmp = std::env::temp_dir().join("containerd-client-test"); - println!("Temp dir: {:?}", tmp); - fs::create_dir_all(&tmp).expect("Failed to create temp directory"); - let stdin = tmp.join("stdin"); - let stdout = tmp.join("stdout"); - let stderr = tmp.join("stderr"); - File::create(&stdin).expect("Failed to create stdin"); - File::create(&stdout).expect("Failed to create stdout"); - File::create(&stderr).expect("Failed to create stderr"); + pub async fn create_and_start_task(&self, cid: &str, ns: &str) -> Result<(), Err> { + // let tmp = std::env::temp_dir().join("containerd-client-test"); + // println!("Temp dir: {:?}", tmp); + // fs::create_dir_all(&tmp).expect("Failed to create temp directory"); + // let stdin = tmp.join("stdin"); + // let stdout = tmp.join("stdout"); + // let stderr = tmp.join("stderr"); + // File::create(&stdin).expect("Failed to create stdin"); + // File::create(&stdout).expect("Failed to create stdout"); + // File::create(&stderr).expect("Failed to create stderr"); - let mut tasks_client = self.client.lock().unwrap().tasks(); + let namespace = match ns { + "" => spec::DEFAULT_NAMESPACE, + _ => ns, + }; + self.create_task(cid, namespace).await?; + self.start_task(cid, namespace).await?; + Ok(()) + } + async fn create_task(&self, cid: &str, ns: &str) -> Result<(), Err> { + let c = self.client.lock().unwrap(); + let mut sc = c.snapshots(); + let req = MountsRequest { + snapshotter: "overlayfs".to_string(), + key: cid.to_string(), + }; + let mounts = sc + .mounts(with_namespace!(req, ns)) + .await? + .into_inner() + .mounts; + drop(sc); + let mut tc = c.tasks(); let req = CreateTaskRequest { - container_id: container_id.clone(), - stdin: stdin.to_str().unwrap().to_string(), - stdout: stdout.to_str().unwrap().to_string(), - stderr: stderr.to_str().unwrap().to_string(), + container_id: cid.to_string(), + rootfs: mounts, ..Default::default() }; - let req = with_namespace!(req, NAMESPACE); + let _resp = tc.create(with_namespace!(req, ns)).await?; - let _resp = tasks_client - .create(req) - .await - .expect("Failed to create task"); - - println!("Task: {:?} created", container_id); - - let req = StartRequest { - container_id: container_id.to_string(), - ..Default::default() - }; - let req = with_namespace!(req, NAMESPACE); - - let _resp = tasks_client.start(req).await.expect("Failed to start task"); - - println!("Task: {:?} started", container_id); + Ok(()) } - pub async fn kill_task(&self, container_id: String) { - let mut tasks_client = self.client.lock().unwrap().tasks(); - let kill_request = Request::new(KillRequest { - container_id: container_id.to_string(), + async fn start_task(&self, cid: &str, ns: &str) -> Result<(), Err> { + let req = StartRequest { + container_id: cid.to_string(), + ..Default::default() + }; + let _resp = self + .client + .lock() + .unwrap() + .tasks() + .start(with_namespace!(req, ns)) + .await?; + + Ok(()) + } + + pub async fn kill_task(&self, cid: String, ns: &str) -> Result<(), Err> { + let namespace = match ns { + "" => NAMESPACE, + _ => ns, + }; + let mut c = self.client.lock().unwrap().tasks(); + let kill_request = KillRequest { + container_id: cid.to_string(), signal: 15, all: true, ..Default::default() - }); - tasks_client - .kill(kill_request) + }; + c.kill(with_namespace!(kill_request, namespace)) .await .expect("Failed to kill task"); + + Ok(()) } pub async fn pause_task() { todo!() @@ -184,62 +263,74 @@ impl Service { pub async fn resume_task() { todo!() } - pub async fn delete_task(&self, container_id: &str) { + pub async fn delete_task(&self, cid: &str, ns: &str) { + let namespace = match ns { + "" => NAMESPACE, + _ => ns, + }; + let mut c = self.client.lock().unwrap().tasks(); let time_out = Duration::from_secs(30); - let mut tc = self.client.lock().unwrap().tasks(); let wait_result = timeout(time_out, async { - let wait_request = Request::new(WaitRequest { - container_id: container_id.to_string(), + let wait_request = WaitRequest { + container_id: cid.to_string(), ..Default::default() - }); + }; - let _ = tc.wait(wait_request).await?; + let _ = c.wait(with_namespace!(wait_request, namespace)).await?; Ok::<(), Err>(()) }) .await; - let kill_request = Request::new(KillRequest { - container_id: container_id.to_string(), + let kill_request = KillRequest { + container_id: cid.to_string(), signal: 15, all: true, ..Default::default() - }); - tc.kill(kill_request).await.expect("Failed to kill task"); + }; + c.kill(with_namespace!(kill_request, namespace)) + .await + .expect("Failed to kill task"); match wait_result { Ok(Ok(_)) => { let req = DeleteTaskRequest { - container_id: container_id.to_string(), + container_id: cid.to_string(), }; - let req = with_namespace!(req, NAMESPACE); - let _resp = tc.delete(req).await.expect("Failed to delete task"); - println!("Task: {:?} deleted", container_id); + let _resp = c + .delete(with_namespace!(req, namespace)) + .await + .expect("Failed to delete task"); + println!("Task: {:?} deleted", cid); } _ => { - let kill_request = Request::new(KillRequest { - container_id: container_id.to_string(), + let kill_request = KillRequest { + container_id: cid.to_string(), signal: 9, all: true, ..Default::default() - }); - tc.kill(kill_request) + }; + c.kill(with_namespace!(kill_request, namespace)) .await .expect("Failed to FORCE kill task"); } } } - pub async fn get_container_list(&self) -> Result, tonic::Status> { - let mut cc = self.client.lock().unwrap().containers(); + pub async fn get_container_list(&self, ns: &str) -> Result, tonic::Status> { + let namespace = match ns { + "" => NAMESPACE, + _ => ns, + }; + let mut c = self.client.lock().unwrap().containers(); let request = ListContainersRequest { ..Default::default() }; - let request = with_namespace!(request, NAMESPACE); + let request = with_namespace!(request, namespace); - let response = cc.list(request).await?; + let response = c.list(request).await?; Ok(response .into_inner() @@ -249,7 +340,9 @@ impl Service { .collect()) } - pub async fn get_task_list() {} + pub async fn get_task_list() { + todo!() + } pub fn prepare_image(&self) { todo!() @@ -261,6 +354,174 @@ impl Service { pub fn get_resolver(&self) { todo!() } + + async fn handle_index(&self, data: &Vec, ns: &str) -> Option { + let image_index: ImageIndex = ::serde_json::from_slice(&data).unwrap(); + let img_manifest_dscr = image_index + .manifests() + .iter() + .find(|manifest_entry| match manifest_entry.platform() { + Some(p) => { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + { + matches!(p.architecture(), &Arch::Amd64) && matches!(p.os(), &Os::Linux) + } + #[cfg(target_arch = "aarch64")] + { + matches!(p.architecture(), &Arch::ARM64) && matches!(p.os(), Os::Linux) + //&& matches!(p.variant().as_ref().map(|s| s.as_str()), Some("v8")) + } + } + None => false, + }) + .unwrap(); + + let req = ReadContentRequest { + digest: img_manifest_dscr.digest().to_owned(), + offset: 0, + size: 0, + }; + + let mut c = self.client.lock().unwrap().content(); + let resp = c + .read(with_namespace!(req, ns)) + .await + .expect("Failed to read content") + .into_inner() + .message() + .await + .expect("Failed to read content message") + .unwrap() + .data; + + self.handle_manifest(&resp, ns).await + } + + async fn handle_manifest(&self, data: &Vec, ns: &str) -> Option { + let img_manifest: ImageManifest = ::serde_json::from_slice(&data).unwrap(); + let img_manifest_dscr = img_manifest.config(); + + let req = ReadContentRequest { + digest: img_manifest_dscr.digest().to_owned(), + offset: 0, + size: 0, + }; + let mut c = self.client.lock().unwrap().content(); + + let resp = c + .read(with_namespace!(req, ns)) + .await + .unwrap() + .into_inner() + .message() + .await + .unwrap() + .unwrap() + .data; + + ::serde_json::from_slice(&resp).unwrap() + } + + pub async fn get_img_config(&self, name: &str, ns: &str) -> Option { + let mut c = self.client.lock().unwrap().images(); + + let req = GetImageRequest { + name: name.to_string(), + }; + let resp = c + .get(with_namespace!(req, ns)) + .await + .map_err(|e| { + eprintln!( + "Failed to get the config of {} in namespace {}: {}", + name, ns, e + ); + e + }) + .ok()? + .into_inner(); + + let img_dscr = resp.image?.target?; + let media_type = MediaType::from(img_dscr.media_type.as_str()); + + let req = ReadContentRequest { + digest: img_dscr.digest, + ..Default::default() + }; + + let mut c = self.client.lock().unwrap().content(); + + let resp = c + .read(with_namespace!(req, ns)) + .await + .map_err(|e| { + eprintln!( + "Failed to read content for {} in namespace {}: {}", + name, ns, e + ); + e + }) + .ok()? + .into_inner() + .message() + .await + .map_err(|e| { + eprintln!( + "Failed to read message for {} in namespace {}: {}", + name, ns, e + ); + e + }) + .ok()? + .ok_or_else(|| { + eprintln!("No data found for {} in namespace {}", name, ns); + std::io::Error::new(std::io::ErrorKind::NotFound, "No data found") + }) + .ok()? + .data; + + let img_config = match media_type { + MediaType::ImageIndex => self.handle_index(&resp, ns).await.unwrap(), + MediaType::ImageManifest => self.handle_manifest(&resp, ns).await.unwrap(), + MediaType::Other(media_type) => match media_type.as_str() { + "application/vnd.docker.distribution.manifest.list.v2+json" => { + self.handle_index(&resp, ns).await.unwrap() + } + "application/vnd.docker.distribution.manifest.v2+json" => { + self.handle_manifest(&resp, ns).await.unwrap() + } + _ => { + eprintln!("Unexpected media type '{}'", media_type); + return None; + } + }, + _ => { + eprintln!("Unexpected media type '{}'", media_type); + return None; + } + }; + Some(img_config) + } + + async fn get_parent_snapshot(&self, name: &str, ns: &str) -> Result { + let img_config = self.get_img_config(name, ns).await.unwrap(); + + let mut iter = img_config.rootfs().diff_ids().iter(); + let mut ret = iter + .next() + .map_or_else(String::new, |layer_digest| layer_digest.clone()); + + while let Some(layer_digest) = iter.next() { + let mut hasher = Sha256::new(); + hasher.update(ret.as_bytes()); + ret.push_str(&format!(",{}", layer_digest)); + hasher.update(" "); + hasher.update(layer_digest); + let digest = ::hex::encode(hasher.finalize()); + ret = format!("sha256:{digest}"); + } + Ok(ret) + } } //容器是容器,要先启动,然后才能运行任务 //要想删除一个正在运行的Task,必须先kill掉这个task,然后才能删除。 diff --git a/service/src/spec.rs b/service/src/spec.rs index 0cc482b..6469009 100644 --- a/service/src/spec.rs +++ b/service/src/spec.rs @@ -9,95 +9,100 @@ const VERSION_DEV: &str = ""; // 对应开发分支 const RWM: &str = "rwm"; const DEFAULT_ROOTFS_PATH: &str = "rootfs"; +pub const DEFAULT_NAMESPACE: &str = "default"; +const PATH_TO_SPEC_PREFIX: &str = "/tmp/containerd-spec"; -const DEFAULT_UNIX_ENV:&str="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; +// const DEFAULT_UNIX_ENV: &str = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; const PID_NAMESPACE: &str = "pid"; const NETWORK_NAMESPACE: &str = "network"; const MOUNT_NAMESPACE: &str = "mount"; const IPC_NAMESPACE: &str = "ipc"; const UTS_NAMESPACE: &str = "uts"; -const USER_NAMESPACE: &str = "user"; -const CGROUP_NAMESPACE: &str = "cgroup"; -const TIME_NAMESPACE: &str = "time"; - #[derive(Serialize, Deserialize, Debug)] -struct Spec { - ociVersion: String, - root: Root, - process: Process, - linux: Linux, - mounts: Vec, +pub struct Spec { + #[serde(rename = "ociVersion")] + pub oci_version: String, + pub root: Root, + pub process: Process, + pub linux: Linux, + pub mounts: Vec, } #[derive(Serialize, Deserialize, Debug)] -struct Root { - path: String, +pub struct Root { + pub path: String, } #[derive(Serialize, Deserialize, Debug)] -struct Process { - cwd: String, - noNewPrivileges: bool, - user: User, - capabilities: LinuxCapabilities, - rlimits: Vec, +pub struct Process { + pub cwd: String, + #[serde(rename = "noNewPrivileges")] + pub no_new_privileges: bool, + pub user: User, + pub capabilities: LinuxCapabilities, + pub rlimits: Vec, + pub args: Vec, + pub env: Vec, } #[derive(Serialize, Deserialize, Debug)] -struct User { - uid: u32, - gid: u32, +pub struct User { + pub uid: u32, + pub gid: u32, + #[serde(rename = "additionalGids")] + pub additional_gids: Vec, } #[derive(Serialize, Deserialize, Debug)] -struct Mount { - destination: String, - type_: String, - source: String, - options: Vec, -} - -#[derive(Serialize, Deserialize, Debug)] -struct LinuxCapabilities { - bounding: Vec, - permitted: Vec, - effective: Vec, -} - -#[derive(Serialize, Deserialize, Debug)] -struct POSIXRlimit { +pub struct Mount { + pub destination: String, #[serde(rename = "type")] - type_: String, - hard: u64, - soft: u64, + pub type_: String, + pub source: String, + pub options: Vec, } #[derive(Serialize, Deserialize, Debug)] -struct Linux { - masked_paths: Vec, - readonly_paths: Vec, - cgroups_path: String, - resources: LinuxResources, - namespaces: Vec, +pub struct LinuxCapabilities { + pub bounding: Vec, + pub permitted: Vec, + pub effective: Vec, } #[derive(Serialize, Deserialize, Debug)] -struct LinuxResources { - devices: Vec, -} - -#[derive(Serialize, Deserialize, Debug)] -struct LinuxDeviceCgroup { - allow: bool, - access: String, -} - -#[derive(Serialize, Deserialize, Debug)] -struct LinuxNamespace { +pub struct POSIXRlimit { + pub hard: u64, + pub soft: u64, #[serde(rename = "type")] - type_: String, + pub type_: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Linux { + pub masked_paths: Vec, + pub readonly_paths: Vec, + pub cgroups_path: String, + pub resources: LinuxResources, + pub namespaces: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct LinuxResources { + pub devices: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct LinuxDeviceCgroup { + pub allow: bool, + pub access: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct LinuxNamespace { + #[serde(rename = "type")] + pub type_: String, } pub fn default_unix_caps() -> Vec { @@ -252,16 +257,20 @@ fn get_version() -> String { ) } -fn populate_default_unix_spec(id: &str, ns: &str) -> Spec { +pub fn populate_default_unix_spec(id: &str, ns: &str) -> Spec { Spec { - ociVersion: get_version(), + oci_version: get_version(), root: Root { path: DEFAULT_ROOTFS_PATH.to_string(), }, process: Process { cwd: String::from("/"), - noNewPrivileges: true, - user: User { uid: 0, gid: 0 }, + no_new_privileges: true, + user: User { + uid: 0, + gid: 0, + additional_gids: vec![], + }, capabilities: LinuxCapabilities { bounding: default_unix_caps(), permitted: default_unix_caps(), @@ -272,6 +281,8 @@ fn populate_default_unix_spec(id: &str, ns: &str) -> Spec { hard: 1024, soft: 1024, }], + args: vec![], + env: vec![], }, linux: Linux { masked_paths: default_masked_parhs(), @@ -280,7 +291,7 @@ fn populate_default_unix_spec(id: &str, ns: &str) -> Spec { resources: LinuxResources { devices: vec![LinuxDeviceCgroup { allow: false, - access: String::from("rwm"), + access: RWM.to_string(), }], }, namespaces: default_unix_namespaces(), @@ -289,9 +300,19 @@ fn populate_default_unix_spec(id: &str, ns: &str) -> Spec { } } -fn save_spec_to_file(spec: &Spec, path: &str) -> Result<(), std::io::Error> { +pub fn save_spec_to_file(spec: &Spec, path: &str) -> Result<(), std::io::Error> { let file = File::create(path)?; serde_json::to_writer(file, spec)?; Ok(()) } +pub fn generate_spec(id: &str, ns: &str) -> Result { + let namespace = match ns { + "" => DEFAULT_NAMESPACE, + _ => ns, + }; + let spec = populate_default_unix_spec(id, ns); + let path = format!("{}/{}/{}.json", PATH_TO_SPEC_PREFIX, namespace, id); + save_spec_to_file(&spec, &path)?; + Ok(path) +}