完善对ctr的包装

This commit is contained in:
sparkzky 2025-02-27 21:30:07 +08:00
parent ab94f96457
commit fa084b1f23
5 changed files with 458 additions and 163 deletions

View File

@ -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<RemoveContainerInfo>,
) -> 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<Arc<Service>>) -> impl Responder {
let container_list = service.get_container_list().await.unwrap();
/// 获取容器列表
pub async fn get_container_list(
service: web::Data<Arc<Service>>,
info: web::Json<GetContainerListQuery>,
) -> impl Responder {
let ns = info.ns.clone();
let container_list = service.get_container_list(&ns).await.unwrap();
HttpResponse::Ok().json(container_list)
}

View File

@ -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<String>,
pub ns: String,
}

View File

@ -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"
prost-types = "0.13.4"
oci-spec = "0.6"
sha2 = "0.10"
hex = "0.4"

View File

@ -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<Vec<Mount>, 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<Vec<String>, tonic::Status> {
let mut cc = self.client.lock().unwrap().containers();
pub async fn get_container_list(&self, ns: &str) -> Result<Vec<String>, 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<u8>, ns: &str) -> Option<ImageConfiguration> {
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<u8>, ns: &str) -> Option<ImageConfiguration> {
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<ImageConfiguration> {
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<String, Err> {
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然后才能删除。

View File

@ -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<Mount>,
pub struct Spec {
#[serde(rename = "ociVersion")]
pub oci_version: String,
pub root: Root,
pub process: Process,
pub linux: Linux,
pub mounts: Vec<Mount>,
}
#[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<POSIXRlimit>,
pub struct Process {
pub cwd: String,
#[serde(rename = "noNewPrivileges")]
pub no_new_privileges: bool,
pub user: User,
pub capabilities: LinuxCapabilities,
pub rlimits: Vec<POSIXRlimit>,
pub args: Vec<String>,
pub env: Vec<String>,
}
#[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<u32>,
}
#[derive(Serialize, Deserialize, Debug)]
struct Mount {
destination: String,
type_: String,
source: String,
options: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
struct LinuxCapabilities {
bounding: Vec<String>,
permitted: Vec<String>,
effective: Vec<String>,
}
#[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<String>,
}
#[derive(Serialize, Deserialize, Debug)]
struct Linux {
masked_paths: Vec<String>,
readonly_paths: Vec<String>,
cgroups_path: String,
resources: LinuxResources,
namespaces: Vec<LinuxNamespace>,
pub struct LinuxCapabilities {
pub bounding: Vec<String>,
pub permitted: Vec<String>,
pub effective: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
struct LinuxResources {
devices: Vec<LinuxDeviceCgroup>,
}
#[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<String>,
pub readonly_paths: Vec<String>,
pub cgroups_path: String,
pub resources: LinuxResources,
pub namespaces: Vec<LinuxNamespace>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct LinuxResources {
pub devices: Vec<LinuxDeviceCgroup>,
}
#[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<String> {
@ -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<String, std::io::Error> {
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)
}