refactor: project structure and better interfaces (#100)

* refactor: basically rewrite all interface

* refactor: rename crates to make clear meaning; use tokio runtime and handle shutdown within Provider

* remove tracker in main

Signed-off-by: sparkzky <sparkhhhhhhhhhh@outlook.com>

* feat(provider): enhance Provider trait with list, update, and status methods; refactor existing methods to async

* fix(containerd): fetch handle from environment and initialize it.

* fix(init): BACKEND init add handle fetching

* fix: add test framework

* fix: move the snapshot logic into snapshot.rs

Signed-off-by: sparkzky <sparkhhhhhhhhhh@outlook.com>

* fix: change some spec setting

Signed-off-by: sparkzky <sparkhhhhhhhhhh@outlook.com>

* feat: add created_at field, add http status code convertion

* refactor(spec): use builder to generate spec

Signed-off-by: sparkzky <sparkhhhhhhhhhh@outlook.com>

* fix: clippy

Signed-off-by: sparkzky <sparkhhhhhhhhhh@outlook.com>

* manage reference, fix boot issue

* fix: ip parsing

* feat: add cleanup logic on fail

* fix style: clippy for return function

* feat: add response message

* fix:1.修复proxy和resolve的逻辑 2.spec内netns的路径问题以及传参顺序

* feat:add update list status  service implmentation

* fix: move some consts into consts.rs

Signed-off-by: sparkzky <sparkhhhhhhhhhh@outlook.com>

* fix: fmt & clippy

Signed-off-by: sparkzky <sparkhhhhhhhhhh@outlook.com>

* fix: update dependecy

Signed-off-by: sparkzky <sparkhhhhhhhhhh@outlook.com>

* feat: add function with_vm_network

Signed-off-by: sparkzky <sparkhhhhhhhhhh@outlook.com>

* feat: integrate cni into containerd crate

* fix:修复proxy的路径正则匹配并添加单元测试

* fix:fix proxy_path and add default namespace for Query::from

* fix: integration_test

* fix: path dispatch test

* fix: more specified url dispatch in proxy handle

* feat: add persistent container record for restart service

* feat: add task error type

* fix: delete error handle logic

---------

Signed-off-by: sparkzky <sparkhhhhhhhhhh@outlook.com>
Co-authored-by: sparkzky <sparkhhhhhhhhhh@outlook.com>
Co-authored-by: dolzhuying <1240800466@qq.com>
Co-authored-by: scutKKsix <1129332011@qq.com>
This commit is contained in:
Samuel Dai 2025-05-22 21:43:16 +08:00 committed by GitHub
parent ed6741cd8a
commit 308e9bcc5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 3451 additions and 3431 deletions

1022
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +0,0 @@
[package]
name = "faas-rs"
version = "0.1.0"
edition = "2024"
[dependencies]
actix-web = "4.5.1"
tokio = { version = "1", features = ["full"] }
service = { path = "../service" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
my-workspace-hack = { version = "0.1", path = "../my-workspace-hack" }
provider = { path = "../provider" }
dotenv = "0.15"
env_logger = "0.10"
log = "0.4.27"

View File

@ -1,60 +0,0 @@
use actix_web::{App, HttpServer, web};
use provider::{
handlers::{
delete::delete_handler, deploy::deploy_handler, function_list::function_list_handler,
},
proxy::proxy_handler::proxy_handler,
types::config::FaaSConfig,
};
use service::containerd_manager::ContainerdManager;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenv::dotenv().ok();
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let socket_path = std::env::var("SOCKET_PATH")
.unwrap_or_else(|_| "/run/containerd/containerd.sock".to_string());
ContainerdManager::init(&socket_path).await;
let faas_config = FaaSConfig::new();
log::info!("I'm running!");
let server = HttpServer::new(move || {
App::new()
.app_data(web::Data::new(faas_config.clone()))
.route("/system/functions", web::post().to(deploy_handler))
.route("/system/functions", web::delete().to(delete_handler))
.route("/function/{name}{path:/?.*}", web::to(proxy_handler))
.route(
"/system/functions/{namespace}",
web::get().to(function_list_handler),
)
// 更多路由配置...
})
.bind("0.0.0.0:8090")?;
log::info!("Running on 0.0.0.0:8090...");
server.run().await
}
// 测试env能够正常获取
#[cfg(test)]
mod tests {
#[test]
fn test_env() {
dotenv::dotenv().ok();
let result: Vec<(String, String)> = dotenv::vars().collect();
let bin = std::env::var("CNI_BIN_DIR").unwrap_or_else(|_| "Not set".to_string());
let conf = std::env::var("CNI_CONF_DIR").unwrap_or_else(|_| "Not set".to_string());
let tool = std::env::var("CNI_TOOL").unwrap_or_else(|_| "Not set".to_string());
log::debug!("CNI_BIN_DIR: {bin}");
log::debug!("CNI_CONF_DIR: {conf}");
log::debug!("CNI_TOOL: {tool}");
// for (key, value) in &result {
// println!("{}={}", key, value);
// }
assert!(!result.is_empty());
}
}

View File

@ -1,105 +0,0 @@
use actix_web::{App, web};
use provider::{
handlers::{delete::delete_handler, deploy::deploy_handler},
proxy::proxy_handler::proxy_handler,
types::config::FaaSConfig,
};
use service::containerd_manager::ContainerdManager;
mod integration_tests {
use super::*;
use actix_web::http::StatusCode;
use actix_web::test;
use serde_json::json;
use std::thread::sleep;
use std::time::Duration;
#[actix_web::test]
#[ignore]
async fn test_handlers_in_order() {
dotenv::dotenv().ok();
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let socket_path = std::env::var("SOCKET_PATH")
.unwrap_or_else(|_| "/run/containerd/containerd.sock".to_string());
ContainerdManager::init(&socket_path).await;
let faas_config = FaaSConfig::new();
let app = test::init_service(
App::new()
.app_data(web::Data::new(faas_config))
.route("/system/functions", web::post().to(deploy_handler))
.route("/system/functions", web::delete().to(delete_handler))
.route("/function/{name}{path:/?.*}", web::to(proxy_handler)),
)
.await;
// test proxy no-found-function in namespace 'default'
let req = test::TestRequest::get()
.uri("/function/test-no-found-function")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
let response_body = test::read_body(resp).await;
let response_str = std::str::from_utf8(&response_body).unwrap();
assert!(response_str.contains("Failed to get function"));
// test delete no-found-function in namespace 'default'
let req = test::TestRequest::delete()
.uri("/system/functions")
.set_json(json!({"function_name": "test-no-found-function"}))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let response_body = test::read_body(resp).await;
let response_str = std::str::from_utf8(&response_body).unwrap();
assert!(
response_str
.contains("Function 'test-no-found-function' not found in namespace 'default'")
);
// test deploy in namespace 'default'
let req = test::TestRequest::post()
.uri("/system/functions")
.set_json(json!({
"function_name": "test-function",
"image": "docker.io/library/nginx:alpine"
}))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(
resp.status(),
StatusCode::ACCEPTED,
"check whether the container has been existed"
);
let response_body = test::read_body(resp).await;
let response_str = std::str::from_utf8(&response_body).unwrap();
log::info!("{}", response_str);
assert!(response_str.contains("Function test-function deployment initiated successfully."));
sleep(Duration::from_secs(2));
// test proxy in namespace 'default'
let req = test::TestRequest::get()
.uri("/function/test-function")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let response_body = test::read_body(resp).await;
let response_str = std::str::from_utf8(&response_body).unwrap();
assert!(response_str.contains("Welcome to nginx!"));
// test delete in namespace 'default'
let req = test::TestRequest::delete()
.uri("/system/functions")
.set_json(json!({"function_name": "test-function"}))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let response_body = test::read_body(resp).await;
let response_str = std::str::from_utf8(&response_body).unwrap();
assert!(response_str.contains("Function test-function deleted successfully."));
}
}

View File

@ -1,16 +0,0 @@
[package]
name = "cni"
version = "0.1.0"
edition = "2024"
# version.workspace = true
# authors.workspace = true
[dependencies]
serde_json = "1.0"
my-workspace-hack = { version = "0.1", path = "../my-workspace-hack" }
log = "0.4.27"
dotenv = "0.15.0"
netns-rs = "0.1.0"
lazy_static = "1.4.0"
env_logger = "0.11.8"
defer = "0.2.1"

View File

@ -1,92 +0,0 @@
use std::{
io::Error,
process::{Command, Output},
};
lazy_static::lazy_static! {
static ref CNI_BIN_DIR: String =
std::env::var("CNI_BIN_DIR").expect("Environment variable CNI_BIN_DIR is not set");
static ref CNI_TOOL: String =
std::env::var("CNI_TOOL").expect("Environment variable CNI_TOOL is not set");
}
#[inline(always)]
fn netns_path(netns: &str) -> String {
"/var/run/netns/".to_string() + netns
}
pub(super) fn cni_add_bridge(netns: &str, bridge_network_name: &str) -> Result<Output, Error> {
Command::new(CNI_TOOL.as_str())
.arg("add")
.arg(bridge_network_name)
.arg(netns_path(netns))
.env("CNI_PATH", CNI_BIN_DIR.as_str())
.output()
}
pub(super) fn cni_del_bridge(netns: &str, bridge_network_name: &str) -> Result<Output, Error> {
Command::new(CNI_TOOL.as_str())
.arg("del")
.arg(bridge_network_name)
.arg(netns_path(netns))
.env("CNI_PATH", CNI_BIN_DIR.as_str())
.output()
}
/// THESE TESTS SHOULD BE RUN WITH ROOT PRIVILEGES
#[cfg(test)]
mod test {
use crate::{netns, util};
use std::path::Path;
use super::*;
const CNI_DATA_DIR: &str = "/var/run/cni";
const TEST_CNI_CONF_FILENAME: &str = "11-faasrstest.conflist";
const TEST_NETWORK_NAME: &str = "faasrstest-cni-bridge";
const TEST_BRIDGE_NAME: &str = "faasrstest0";
const TEST_SUBNET: &str = "10.99.0.0/16";
const CNI_CONF_DIR: &str = "/etc/cni/net.d";
fn init_test_net_fs() {
crate::util::init_net_fs(
Path::new(CNI_CONF_DIR),
TEST_CNI_CONF_FILENAME,
TEST_NETWORK_NAME,
TEST_BRIDGE_NAME,
TEST_SUBNET,
CNI_DATA_DIR,
)
.unwrap()
}
#[test]
#[ignore]
fn test_cni_resource() {
dotenv::dotenv().unwrap();
env_logger::init_from_env(env_logger::Env::new().default_filter_or("trace"));
init_test_net_fs();
let netns = util::netns_from_cid_and_cns("123456", "cns");
netns::create(&netns).unwrap();
defer::defer!({
let _ = netns::remove(&netns);
});
let result = cni_add_bridge(&netns, TEST_NETWORK_NAME);
log::debug!("add CNI result: {:?}", result);
assert!(
result.is_ok_and(|output| output.status.success()),
"Failed to add CNI"
);
defer::defer!({
let result = cni_del_bridge(&netns, TEST_NETWORK_NAME);
log::debug!("del CNI result: {:?}", result);
assert!(
result.is_ok_and(|output| output.status.success()),
"Failed to delete CNI"
);
});
}
}

View File

@ -1,91 +0,0 @@
type Err = Box<dyn std::error::Error>;
use lazy_static::lazy_static;
use serde_json::Value;
use std::{fmt::Error, net::IpAddr, path::Path};
mod command;
mod netns;
mod util;
use command as cmd;
lazy_static! {
static ref CNI_CONF_DIR: String =
std::env::var("CNI_CONF_DIR").expect("Environment variable CNI_CONF_DIR is not set");
}
// const NET_NS_PATH_FMT: &str = "/proc/{}/ns/net";
const CNI_DATA_DIR: &str = "/var/run/cni";
const DEFAULT_CNI_CONF_FILENAME: &str = "10-faasrs.conflist";
const DEFAULT_NETWORK_NAME: &str = "faasrs-cni-bridge";
const DEFAULT_BRIDGE_NAME: &str = "faasrs0";
const DEFAULT_SUBNET: &str = "10.66.0.0/16";
// const DEFAULT_IF_PREFIX: &str = "eth";
pub fn init_net_work() -> Result<(), Err> {
util::init_net_fs(
Path::new(CNI_CONF_DIR.as_str()),
DEFAULT_CNI_CONF_FILENAME,
DEFAULT_NETWORK_NAME,
DEFAULT_BRIDGE_NAME,
DEFAULT_SUBNET,
CNI_DATA_DIR,
)
}
//TODO: 创建网络和删除网络的错误处理
pub fn create_cni_network(cid: String, ns: String) -> Result<String, Err> {
let netns = util::netns_from_cid_and_cns(&cid, &ns);
let mut ip = String::new();
netns::create(&netns)?;
let output = cmd::cni_add_bridge(netns.as_str(), DEFAULT_NETWORK_NAME);
match output {
Ok(output) => {
if !output.status.success() {
return Err(Box::new(Error));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let json: Value = match serde_json::from_str(&stdout) {
Ok(json) => json,
Err(e) => {
return Err(Box::new(e));
}
};
if let Some(ips) = json.get("ips").and_then(|ips| ips.as_array()) {
if let Some(first_ip) = ips
.first()
.and_then(|ip| ip.get("address"))
.and_then(|addr| addr.as_str())
{
ip = first_ip.to_string();
}
}
}
Err(e) => {
return Err(Box::new(e));
}
}
Ok(ip)
}
pub fn delete_cni_network(ns: &str, cid: &str) {
let netns = util::netns_from_cid_and_cns(cid, ns);
let _ = cmd::cni_del_bridge(&netns, DEFAULT_NETWORK_NAME);
let _ = netns::remove(&netns);
}
#[allow(unused)]
fn cni_gateway() -> Result<String, Err> {
let ip: IpAddr = DEFAULT_SUBNET.parse().unwrap();
if let IpAddr::V4(ip) = ip {
let octets = &mut ip.octets();
octets[3] = 1;
return Ok(ip.to_string());
}
Err(Box::new(Error))
}

View File

@ -1,32 +0,0 @@
use netns_rs::{Error, NetNs};
pub(super) fn create(netns: &str) -> Result<NetNs, Error> {
NetNs::new(netns)
}
pub(super) fn remove(netns: &str) -> Result<(), Error> {
match NetNs::get(netns) {
Ok(ns) => {
ns.remove()?;
Ok(())
}
Err(e) => {
log::error!("Failed to get netns {}: {}", netns, e);
Err(e)
}
}
}
/// THESE TESTS SHOULD BE RUN WITH ROOT PRIVILEGES
#[cfg(test)]
mod test {
use super::*;
#[test]
#[ignore]
fn test_create_and_remove() {
let netns_name = "test_netns";
create(netns_name).unwrap();
assert!(remove(netns_name).is_ok());
}
}

View File

@ -0,0 +1,36 @@
[package]
name = "faas-containerd"
version = "0.1.0"
edition = "2024"
[dependencies]
containerd-client = "0.8"
futures = "0.3"
tokio = { version = "1", features = ["full"] }
tonic = "0.12"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
scopeguard = "1.2.0"
log = "0.4"
env_logger = "0.10"
prost-types = "0.13.4"
oci-spec = "0.6"
sha2 = "0.10"
hex = "0.4"
my-workspace-hack = { version = "0.1", path = "../my-workspace-hack" }
gateway = { path = "../gateway" }
handlebars = "4.1.0"
tokio-util = { version = "0.7.15", features = ["full"] }
container_image_dist_ref = "0.3.0"
url = "2.4"
chrono = { version = "0.4", features = ["serde"] }
dotenv = "0.15.0"
derive_more = { version = "2", features = ["full"] }
cidr = "0.3.1"
async-safe-defer = "0.1.2"
actix-http = "*"
netns-rs = "0.1.0"
sled = "0.34.7"
[dev-dependencies]
actix-web = "4.11.0"

View File

@ -0,0 +1,15 @@
#[allow(unused)]
pub const DEFAULT_FUNCTION_NAMESPACE: &str = "faasrs-default";
#[allow(unused)]
pub const DEFAULT_SNAPSHOTTER: &str = "overlayfs";
pub const DEFAULT_CTRD_SOCK: &str = "/run/containerd/containerd.sock";
pub const DEFAULT_FAASDRS_DATA_DIR: &str = "/var/lib/faasdrs";
// 定义版本的常量
pub const VERSION_MAJOR: u32 = 1;
pub const VERSION_MINOR: u32 = 1;
pub const VERSION_PATCH: u32 = 0;
pub const VERSION_DEV: &str = ""; // 对应开发分支

View File

@ -0,0 +1,148 @@
type Err = Box<dyn std::error::Error>;
use derive_more::{Display, Error};
use netns_rs::NetNs;
use scopeguard::{ScopeGuard, guard};
use serde_json::Value;
use std::{fmt::Error, net::IpAddr, path::Path, sync::LazyLock};
use super::{Endpoint, command as cmd, util};
static CNI_CONF_DIR: LazyLock<String> = LazyLock::new(|| {
std::env::var("CNI_CONF_DIR").unwrap_or_else(|_| "/etc/cni/net.d".to_string())
});
const CNI_DATA_DIR: &str = "/var/run/cni";
const DEFAULT_CNI_CONF_FILENAME: &str = "10-faasrs.conflist";
const DEFAULT_NETWORK_NAME: &str = "faasrs-cni-bridge";
const DEFAULT_BRIDGE_NAME: &str = "faasrs0";
const DEFAULT_SUBNET: &str = "10.66.0.0/16";
pub fn init_cni_network() -> Result<(), Err> {
util::init_net_fs(
Path::new(CNI_CONF_DIR.as_str()),
DEFAULT_CNI_CONF_FILENAME,
DEFAULT_NETWORK_NAME,
DEFAULT_BRIDGE_NAME,
DEFAULT_SUBNET,
CNI_DATA_DIR,
)
}
#[derive(Debug, Display, Error)]
pub struct NetworkError {
pub msg: String,
}
//TODO: 创建网络和删除网络的错误处理
pub fn create_cni_network(endpoint: &Endpoint) -> Result<(cidr::IpInet, NetNs), NetworkError> {
let net_ns = guard(
NetNs::new(endpoint.to_string()).map_err(|e| NetworkError {
msg: format!("Failed to create netns: {}", e),
})?,
|ns| ns.remove().unwrap(),
);
let output = cmd::cni_add_bridge(net_ns.path(), DEFAULT_NETWORK_NAME);
match output {
Ok(output) => {
if !output.status.success() {
return Err(NetworkError {
msg: format!(
"Failed to add CNI bridge: {}",
String::from_utf8_lossy(&output.stderr)
),
});
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut json: Value = match serde_json::from_str(&stdout) {
Ok(json) => json,
Err(e) => {
log::error!("Failed to parse JSON: {}", e);
return Err(NetworkError {
msg: format!("Failed to parse JSON: {}", e),
});
}
};
log::trace!("CNI add bridge output: {:?}", json);
if let serde_json::Value::Array(ips) = json["ips"].take() {
let mut ip_list = Vec::new();
for mut ip in ips {
if let serde_json::Value::String(ip_str) = ip["address"].take() {
let ipcidr = ip_str.parse::<cidr::IpInet>().map_err(|e| {
log::error!("Failed to parse IP address: {}", e);
NetworkError { msg: e.to_string() }
})?;
ip_list.push(ipcidr);
}
}
if ip_list.is_empty() {
return Err(NetworkError {
msg: "No IP address found in CNI output".to_string(),
});
}
if ip_list.len() > 1 {
log::warn!("Multiple IP addresses found in CNI output: {:?}", ip_list);
}
log::trace!("CNI network created with IP: {:?}", ip_list[0]);
Ok((ip_list[0], ScopeGuard::into_inner(net_ns)))
} else {
log::error!("Invalid JSON format: {:?}", json);
Err(NetworkError {
msg: "Invalid JSON format".to_string(),
})
}
}
Err(e) => {
log::error!("Failed to add CNI bridge: {}", e);
Err(NetworkError {
msg: format!("Failed to add CNI bridge: {}", e),
})
}
}
}
pub fn delete_cni_network(endpoint: Endpoint) -> Result<(), NetworkError> {
match NetNs::get(endpoint.to_string()) {
Ok(ns) => {
let e1 = cmd::cni_del_bridge(ns.path(), DEFAULT_NETWORK_NAME);
let e2 = ns.remove();
if e1.is_err() || e2.is_err() {
let err = format!(
"NetNS exists, but failed to delete CNI network, cni bridge: {:?}, netns: {:?}",
e1, e2
);
log::error!("{}", err);
return Err(NetworkError { msg: err });
}
Ok(())
}
Err(e) => {
let msg = format!("Failed to get netns {}: {}", endpoint, e);
log::warn!("{}", msg);
Err(NetworkError { msg })
}
}
}
#[inline]
pub fn check_network_exists(addr: IpAddr) -> bool {
util::CNI_CONFIG_FILE
.get()
.unwrap()
.data_dir
.join(addr.to_string())
.exists()
}
#[allow(unused)]
fn cni_gateway() -> Result<String, Err> {
let ip: IpAddr = DEFAULT_SUBNET.parse().unwrap();
if let IpAddr::V4(ip) = ip {
let octets = &mut ip.octets();
octets[3] = 1;
return Ok(ip.to_string());
}
Err(Box::new(Error))
}

View File

@ -0,0 +1,94 @@
use std::{
io::Error,
path::Path,
process::{Command, Output},
sync::LazyLock,
};
static CNI_BIN_DIR: LazyLock<String> =
LazyLock::new(|| std::env::var("CNI_BIN_DIR").unwrap_or_else(|_| "/opt/cni/bin".to_string()));
static CNI_TOOL: LazyLock<String> =
LazyLock::new(|| std::env::var("CNI_TOOL").unwrap_or_else(|_| "cni-tool".to_string()));
pub(super) fn cni_add_bridge(
netns_path: &Path,
bridge_network_name: &str,
) -> Result<Output, Error> {
Command::new(CNI_TOOL.as_str())
.arg("add")
.arg(bridge_network_name)
.arg(netns_path)
.env("CNI_PATH", CNI_BIN_DIR.as_str())
.output()
}
pub(super) fn cni_del_bridge(
netns_path: &Path,
bridge_network_name: &str,
) -> Result<Output, Error> {
Command::new(CNI_TOOL.as_str())
.arg("del")
.arg(bridge_network_name)
.arg(netns_path)
.env("CNI_PATH", CNI_BIN_DIR.as_str())
.output()
}
// /// THESE TESTS SHOULD BE RUN WITH ROOT PRIVILEGES
// #[cfg(test)]
// mod test {
// use crate::impls::cni::util;
// use std::path::Path;
// use super::*;
// const CNI_DATA_DIR: &str = "/var/run/cni";
// const TEST_CNI_CONF_FILENAME: &str = "11-faasrstest.conflist";
// const TEST_NETWORK_NAME: &str = "faasrstest-cni-bridge";
// const TEST_BRIDGE_NAME: &str = "faasrstest0";
// const TEST_SUBNET: &str = "10.99.0.0/16";
// const CNI_CONF_DIR: &str = "/etc/cni/net.d";
// fn init_test_net_fs() {
// util::init_net_fs(
// Path::new(CNI_CONF_DIR),
// TEST_CNI_CONF_FILENAME,
// TEST_NETWORK_NAME,
// TEST_BRIDGE_NAME,
// TEST_SUBNET,
// CNI_DATA_DIR,
// )
// .unwrap()
// }
// #[test]
// #[ignore]
// fn test_cni_resource() {
// dotenv::dotenv().unwrap();
// env_logger::init_from_env(env_logger::Env::new().default_filter_or("trace"));
// init_test_net_fs();
// let netns = util::netns_from_cid_and_cns("123456", "cns");
// let net_namespace = netns::create(&netns).unwrap();
// defer::defer!({
// let _ = netns::remove(&netns);
// });
// net_namespace.path()
// let result = cni_add_bridge(&netns, TEST_NETWORK_NAME);
// log::debug!("add CNI result: {:?}", result);
// assert!(
// result.is_ok_and(|output| output.status.success()),
// "Failed to add CNI"
// );
// defer::defer!({
// let result = cni_del_bridge(&netns, TEST_NETWORK_NAME);
// log::debug!("del CNI result: {:?}", result);
// assert!(
// result.is_ok_and(|output| output.status.success()),
// "Failed to delete CNI"
// );
// });
// }
// }

View File

@ -0,0 +1,55 @@
use crate::consts;
pub mod cni_impl;
mod command;
mod util;
pub use cni_impl::init_cni_network;
use gateway::types::function::Query;
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct Endpoint {
pub service: String,
pub namespace: String,
}
impl Endpoint {
pub fn new(service: &str, namespace: &str) -> Self {
Self {
service: service.to_string(),
namespace: namespace.to_string(),
}
}
}
/// format `<namespace>-<service>` as netns name, also the identifier of each function
impl std::fmt::Display for Endpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}-{}", self.namespace, self.service)
}
}
impl From<Query> for Endpoint {
fn from(query: Query) -> Self {
Self {
service: query.service,
namespace: query
.namespace
.unwrap_or(consts::DEFAULT_FUNCTION_NAMESPACE.to_string()),
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_ip_parsing() {
let raw_ip = "10.42.0.48/16";
let ipcidr = raw_ip.parse::<cidr::IpInet>().unwrap();
assert_eq!(
ipcidr.address(),
std::net::IpAddr::V4(std::net::Ipv4Addr::new(10, 42, 0, 48))
);
}
}

View File

@ -1,14 +1,15 @@
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
static mut CNI_CONFIG_FILE: Option<CniConfFile> = None;
pub static CNI_CONFIG_FILE: OnceLock<CniConfFile> = OnceLock::new();
/// Generate "cns-cid"
#[inline(always)]
pub fn netns_from_cid_and_cns(cid: &str, cns: &str) -> String {
format!("{}-{}", cns, cid)
}
// /// Generate "cns-cid"
// #[inline(always)]
// pub fn netns_name_from_cid_ns(cid: &str, cns: &str) -> String {
// format!("{}-{}", cns, cid)
// }
pub fn init_net_fs(
conf_dir: &Path,
@ -19,9 +20,9 @@ pub fn init_net_fs(
data_dir: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let conf_file = CniConfFile::new(conf_dir, conf_filename, net_name, bridge, subnet, data_dir)?;
unsafe {
CNI_CONFIG_FILE = Some(conf_file);
}
CNI_CONFIG_FILE
.set(conf_file)
.map_err(|_| "Failed to set CNI_CONFIG_FILE")?;
Ok(())
}
@ -56,9 +57,10 @@ fn cni_conf(name: &str, bridge: &str, subnet: &str, data_dir: &str) -> String {
)
}
struct CniConfFile {
conf_dir: PathBuf,
conf_filename: String,
pub(super) struct CniConfFile {
pub conf_dir: PathBuf,
pub conf_filename: String,
pub data_dir: PathBuf,
}
impl CniConfFile {
@ -80,9 +82,11 @@ impl CniConfFile {
let net_config = conf_dir.join(conf_filename);
File::create(&net_config)?
.write_all(cni_conf(net_name, bridge, subnet, data_dir).as_bytes())?;
let data_dir = PathBuf::from(data_dir);
Ok(Self {
conf_dir: conf_dir.to_path_buf(),
conf_filename: conf_filename.to_string(),
data_dir: data_dir.join(net_name),
})
}
}

View File

@ -0,0 +1,125 @@
use containerd_client::{
services::v1::{Container, DeleteContainerRequest, GetContainerRequest, ListContainersRequest},
with_namespace,
};
use derive_more::Display;
use containerd_client::services::v1::container::Runtime;
use super::{ContainerdService, backend, cni::Endpoint, function::ContainerStaticMetadata};
use tonic::Request;
#[derive(Debug, Display)]
pub enum ContainerError {
NotFound,
AlreadyExists,
Internal,
}
impl ContainerdService {
/// 创建容器
pub async fn create_container(
&self,
metadata: &ContainerStaticMetadata,
) -> Result<Container, ContainerError> {
let container = Container {
id: metadata.endpoint.service.clone(),
image: metadata.image.clone(),
runtime: Some(Runtime {
name: "io.containerd.runc.v2".to_string(),
options: None,
}),
spec: Some(backend().get_spec(metadata).await.map_err(|_| {
log::error!("Failed to get spec");
ContainerError::Internal
})?),
snapshotter: crate::consts::DEFAULT_SNAPSHOTTER.to_string(),
snapshot_key: metadata.endpoint.service.clone(),
..Default::default()
};
let mut cc = backend().client.containers();
let req = containerd_client::services::v1::CreateContainerRequest {
container: Some(container),
};
let resp = cc
.create(with_namespace!(req, metadata.endpoint.namespace))
.await
.map_err(|e| {
log::error!("Failed to create container: {}", e);
ContainerError::Internal
})?;
resp.into_inner().container.ok_or(ContainerError::Internal)
}
/// 删除容器
pub async fn delete_container(&self, endpoint: &Endpoint) -> Result<(), ContainerError> {
let Endpoint {
service: cid,
namespace: ns,
} = endpoint;
let mut cc = self.client.containers();
let delete_request = DeleteContainerRequest { id: cid.clone() };
cc.delete(with_namespace!(delete_request, ns))
.await
.map_err(|e| {
log::error!("Failed to delete container: {}", e);
ContainerError::Internal
})
.map(|_| ())
}
/// 根据查询条件加载容器参数
pub async fn load_container(&self, endpoint: &Endpoint) -> Result<Container, ContainerError> {
let mut cc = self.client.containers();
let request = GetContainerRequest {
id: endpoint.service.clone(),
};
let resp = cc
.get(with_namespace!(request, endpoint.namespace))
.await
.map_err(|e| {
log::error!("Failed to list containers: {}", e);
ContainerError::Internal
})?;
resp.into_inner().container.ok_or(ContainerError::NotFound)
}
/// 获取容器列表
pub async fn list_container(&self, namespace: &str) -> Result<Vec<Container>, ContainerError> {
let mut cc = self.client.containers();
let request = ListContainersRequest {
..Default::default()
};
let resp = cc
.list(with_namespace!(request, namespace))
.await
.map_err(|e| {
log::error!("Failed to list containers: {}", e);
ContainerError::Internal
})?;
Ok(resp.into_inner().containers)
}
/// 不儿,这也要单独一个函数?
#[deprecated]
pub async fn list_container_into_string(
&self,
ns: &str,
) -> Result<Vec<String>, ContainerError> {
self.list_container(ns)
.await
.map(|ctrs| ctrs.into_iter().map(|ctr| ctr.id).collect())
}
}

View File

@ -0,0 +1,17 @@
use derive_more::derive::Display;
#[derive(Debug, Display)]
pub enum ContainerdError {
CreateContainerError(String),
CreateSnapshotError(String),
GetParentSnapshotError(String),
GenerateSpecError(String),
DeleteContainerError(String),
GetContainerListError(String),
KillTaskError(String),
DeleteTaskError(String),
WaitTaskError(String),
CreateTaskError(String),
StartTaskError(String),
#[allow(dead_code)]
OtherError,
}

View File

@ -0,0 +1,97 @@
use gateway::types::function;
use crate::consts;
use super::cni::Endpoint;
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct ContainerStaticMetadata {
pub image: String,
pub endpoint: Endpoint,
}
impl From<function::Deployment> for ContainerStaticMetadata {
fn from(info: function::Deployment) -> Self {
ContainerStaticMetadata {
image: info.image,
endpoint: Endpoint::new(
&info.service,
&info
.namespace
.unwrap_or(consts::DEFAULT_FUNCTION_NAMESPACE.to_string()),
),
}
}
}
// impl From<ContainerStaticMetadata> for function::Query {
// fn from(metadata: ContainerStaticMetadata) -> Self {
// function::Query {
// service: metadata.container_id,
// namespace: Some(metadata.namespace),
// }
// }
// }
// /// A function is a container instance with correct cni connected
// #[derive(Debug)]
// pub struct FunctionInstance {
// container: containerd_client::services::v1::Container,
// namespace: String,
// // ip addr inside cni
// // network: CNIEndpoint,
// // port: Vec<u16>, default use 8080
// // manager: Weak<crate::provider::ContainerdProvider>,
// }
// impl FunctionInstance {
// pub async fn new(metadata: ContainerStaticMetadata) -> Result<Self, ContainerdError> {
// Ok(Self {
// container,
// namespace: metadata.namespace,
// // network,
// })
// }
// pub async fn delete(&self) -> Result<(), ContainerdError> {
// let container_id = self.container.id.clone();
// let namespace = self.namespace.clone();
// let kill_err = backend()
// .kill_task_with_timeout(&container_id, &namespace)
// .await
// .map_err(|e| {
// log::error!("Failed to kill task: {:?}", e);
// e
// });
// let del_ctr_err = backend()
// .delete_container(&container_id, &namespace)
// .await
// .map_err(|e| {
// log::error!("Failed to delete container: {:?}", e);
// e
// });
// let rm_snap_err = backend()
// .remove_snapshot(&container_id, &namespace)
// .await
// .map_err(|e| {
// log::error!("Failed to remove snapshot: {:?}", e);
// e
// });
// if kill_err.is_ok() && del_ctr_err.is_ok() && rm_snap_err.is_ok() {
// Ok(())
// } else {
// Err(ContainerdError::DeleteContainerError(format!(
// "{:?}, {:?}, {:?}",
// kill_err, del_ctr_err, rm_snap_err
// )))
// }
// }
// pub fn address(&self) -> IpAddr {
// self.network.address()
// }
// }

View File

@ -0,0 +1,30 @@
pub mod cni;
pub mod container;
pub mod error;
pub mod function;
pub mod oci_image;
pub mod snapshot;
pub mod spec;
pub mod task;
use std::sync::OnceLock;
pub static __BACKEND: OnceLock<ContainerdService> = OnceLock::new();
pub(crate) fn backend() -> &'static ContainerdService {
__BACKEND.get().unwrap()
}
/// TODO: Panic on failure, should be handled in a better way
pub async fn init_backend() {
let socket =
std::env::var("SOCKET_PATH").unwrap_or(crate::consts::DEFAULT_CTRD_SOCK.to_string());
let client = containerd_client::Client::from_path(socket).await.unwrap();
__BACKEND.set(ContainerdService { client }).ok().unwrap();
cni::init_cni_network().unwrap();
}
pub struct ContainerdService {
pub client: containerd_client::Client,
}

View File

@ -1,10 +1,7 @@
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use super::ContainerdService;
use container_image_dist_ref::ImgRef;
use containerd_client::{
Client,
services::v1::{GetImageRequest, ReadContentRequest, TransferOptions, TransferRequest},
to_any,
tonic::Request,
@ -16,36 +13,265 @@ use containerd_client::{
};
use oci_spec::image::{Arch, ImageConfiguration, ImageIndex, ImageManifest, MediaType, Os};
use crate::{containerd_manager::CLIENT, spec::DEFAULT_NAMESPACE};
impl ContainerdService {
async fn get_image(&self, image_name: &str, ns: &str) -> Result<(), ImageError> {
let mut c = self.client.images();
let req = GetImageRequest {
name: image_name.to_string(),
};
type ImagesMap = Arc<RwLock<HashMap<String, ImageConfiguration>>>;
lazy_static::lazy_static! {
static ref GLOBAL_IMAGE_MAP: ImagesMap = Arc::new(RwLock::new(HashMap::new()));
let resp = match c.get(with_namespace!(req, ns)).await {
Ok(response) => response.into_inner(),
Err(e) => {
return Err(ImageError::ImageNotFound(format!(
"Failed to get image {}: {}",
image_name, e
)));
}
};
if resp.image.is_none() {
self.pull_image(image_name, ns).await?;
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct ImageRuntimeConfig {
pub env: Vec<String>,
pub args: Vec<String>,
pub ports: Vec<String>,
pub cwd: String,
pub async fn pull_image(&self, image_name: &str, ns: &str) -> Result<(), ImageError> {
let ns = check_namespace(ns);
let namespace = ns.as_str();
let mut trans_cli = self.client.transfer();
let source = OciRegistry {
reference: image_name.to_string(),
resolver: Default::default(),
};
// 这里先写死linux amd64
let platform = Platform {
os: "linux".to_string(),
architecture: "amd64".to_string(),
..Default::default()
};
let dest = ImageStore {
name: image_name.to_string(),
platforms: vec![platform.clone()],
unpacks: vec![UnpackConfiguration {
platform: Some(platform),
..Default::default()
}],
..Default::default()
};
let anys = to_any(&source);
let anyd = to_any(&dest);
let req = TransferRequest {
source: Some(anys),
destination: Some(anyd),
options: Some(TransferOptions {
..Default::default()
}),
};
trans_cli
.transfer(with_namespace!(req, namespace))
.await
.map_err(|e| {
log::error!("Failed to pull image: {}", e);
ImageError::ImagePullFailed(format!("Failed to pull image {}: {}", image_name, e))
})
.map(|resp| {
log::trace!("Pull image response: {:?}", resp);
})
}
impl ImageRuntimeConfig {
pub fn new(env: Vec<String>, args: Vec<String>, ports: Vec<String>, cwd: String) -> Self {
ImageRuntimeConfig {
env,
args,
ports,
cwd,
}
pub async fn prepare_image(
&self,
image_name: &str,
ns: &str,
always_pull: bool,
) -> Result<(), ImageError> {
let _ = ImgRef::new(image_name).map_err(|e| {
ImageError::ImageNotFound(format!("Invalid image name: {:?}", e.kind()))
})?;
if always_pull {
self.pull_image(image_name, ns).await
} else {
let namespace = check_namespace(ns);
let namespace = namespace.as_str();
self.get_image(image_name, namespace).await
}
}
impl Drop for ImageManager {
fn drop(&mut self) {
let mut map = GLOBAL_IMAGE_MAP.write().unwrap();
map.clear();
pub async fn image_config(
&self,
img_name: &str,
ns: &str,
) -> Result<ImageConfiguration, ImageError> {
let mut img_cli = self.client.images();
let req = GetImageRequest {
name: img_name.to_string(),
};
let resp = match img_cli.get(with_namespace!(req, ns)).await {
Ok(response) => response.into_inner(),
Err(e) => {
return Err(ImageError::ImageNotFound(format!(
"Failed to get image {}: {}",
img_name, e
)));
}
};
let img_dscr = resp.image.unwrap().target.unwrap();
let media_type = MediaType::from(img_dscr.media_type.as_str());
let req = ReadContentRequest {
digest: img_dscr.digest,
..Default::default()
};
let mut content_cli = self.client.content();
let mut inner = match content_cli.read(with_namespace!(req, ns)).await {
Ok(response) => response.into_inner(),
Err(e) => {
return Err(ImageError::ReadContentFailed(format!(
"Failed to read content of image {}: {}",
img_name, e
)));
}
};
let resp = match inner.message().await {
Ok(response) => response.unwrap().data,
Err(e) => {
return Err(ImageError::ReadContentFailed(format!(
"Failed to get the inner content of image {}: {}",
img_name, e
)));
}
};
drop(content_cli);
match media_type {
MediaType::ImageIndex => self.handle_index(&resp, ns).await,
MediaType::ImageManifest => self.handle_manifest(&resp, ns).await,
MediaType::Other(val)
if val == "application/vnd.docker.distribution.manifest.list.v2+json" =>
{
self.handle_index(&resp, ns).await
}
MediaType::Other(val)
if val == "application/vnd.docker.distribution.manifest.v2+json" =>
{
self.handle_manifest(&resp, ns).await
}
_ => Err(ImageError::UnexpectedMediaType),
}
}
async fn handle_index(&self, data: &[u8], ns: &str) -> Result<ImageConfiguration, ImageError> {
let image_index: ImageIndex = ::serde_json::from_slice(data).map_err(|e| {
ImageError::DeserializationFailed(format!("Failed to parse JSON: {}", e))
})?;
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.content();
let mut inner = match c.read(with_namespace!(req, ns)).await {
Ok(response) => response.into_inner(),
Err(e) => {
return Err(ImageError::ReadContentFailed(format!(
"Failed to handler index : {}",
e
)));
}
};
let resp = match inner.message().await {
Ok(response) => response.unwrap().data,
Err(e) => {
return Err(ImageError::ReadContentFailed(format!(
"Failed to handle index inner : {}",
e
)));
}
};
drop(c);
self.handle_manifest(&resp, ns).await
}
async fn handle_manifest(
&self,
data: &[u8],
ns: &str,
) -> Result<ImageConfiguration, ImageError> {
let img_manifest: ImageManifest = match serde_json::from_slice(data) {
Ok(manifest) => manifest,
Err(e) => {
return Err(ImageError::DeserializationFailed(format!(
"Failed to deserialize image manifest: {}",
e
)));
}
};
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.content();
let mut inner = match c.read(with_namespace!(req, ns)).await {
Ok(response) => response.into_inner(),
Err(e) => {
return Err(ImageError::ReadContentFailed(format!(
"Failed to handler index : {}",
e
)));
}
};
let resp = match inner.message().await {
Ok(response) => response.unwrap().data,
Err(e) => {
return Err(ImageError::ReadContentFailed(format!(
"Failed to handle index inner : {}",
e
)));
}
};
serde_json::from_slice(&resp)
.map_err(|e| ImageError::DeserializationFailed(format!("Failed to parse JSON: {}", e)))
}
}
@ -81,346 +307,14 @@ impl std::fmt::Display for ImageError {
}
}
impl std::error::Error for ImageError {}
#[derive(Debug)]
pub struct ImageManager;
impl ImageManager {
async fn get_client() -> Arc<Client> {
CLIENT
.get()
.unwrap_or_else(|| panic!("Client not initialized, Please run init first"))
.clone()
}
pub async fn prepare_image(
image_name: &str,
ns: &str,
always_pull: bool,
) -> Result<(), ImageError> {
if always_pull {
Self::pull_image(image_name, ns).await?;
} else {
let namespace = check_namespace(ns);
let namespace = namespace.as_str();
Self::get_image(image_name, namespace).await?;
}
Self::save_img_config(image_name, ns).await
}
async fn get_image(image_name: &str, ns: &str) -> Result<(), ImageError> {
let mut c = Self::get_client().await.images();
let req = GetImageRequest {
name: image_name.to_string(),
};
let resp = match c.get(with_namespace!(req, ns)).await {
Ok(response) => response.into_inner(),
Err(e) => {
return Err(ImageError::ImageNotFound(format!(
"Failed to get image {}: {}",
image_name, e
)));
}
};
if resp.image.is_none() {
Self::pull_image(image_name, ns).await?;
}
Ok(())
}
pub async fn pull_image(image_name: &str, ns: &str) -> Result<(), ImageError> {
let client = Self::get_client().await;
let ns = check_namespace(ns);
let namespace = ns.as_str();
let mut c: containerd_client::services::v1::transfer_client::TransferClient<
tonic::transport::Channel,
> = client.transfer();
let source = OciRegistry {
reference: image_name.to_string(),
resolver: Default::default(),
};
// 这里先写死linux amd64
let platform = Platform {
os: "linux".to_string(),
architecture: "amd64".to_string(),
..Default::default()
};
let dest = ImageStore {
name: image_name.to_string(),
platforms: vec![platform.clone()],
unpacks: vec![UnpackConfiguration {
platform: Some(platform),
..Default::default()
}],
..Default::default()
};
let anys = to_any(&source);
let anyd = to_any(&dest);
let req = TransferRequest {
source: Some(anys),
destination: Some(anyd),
options: Some(TransferOptions {
..Default::default()
}),
};
if let Err(e) = c.transfer(with_namespace!(req, namespace)).await {
return Err(ImageError::ImagePullFailed(format!(
"Failed to pull image {}: {}",
image_name, e
)));
}
Ok(())
// Self::save_img_config(client, image_name, ns.as_str()).await
}
pub async fn save_img_config(img_name: &str, ns: &str) -> Result<(), ImageError> {
let client = Self::get_client().await;
let mut c = client.images();
let req = GetImageRequest {
name: img_name.to_string(),
};
let resp = match c.get(with_namespace!(req, ns)).await {
Ok(response) => response.into_inner(),
Err(e) => {
return Err(ImageError::ImageNotFound(format!(
"Failed to get image {}: {}",
img_name, e
)));
}
};
let img_dscr = resp.image.unwrap().target.unwrap();
let media_type = MediaType::from(img_dscr.media_type.as_str());
let req = ReadContentRequest {
digest: img_dscr.digest,
..Default::default()
};
let mut c = client.content();
let mut inner = match c.read(with_namespace!(req, ns)).await {
Ok(response) => response.into_inner(),
Err(e) => {
return Err(ImageError::ReadContentFailed(format!(
"Failed to read content of image {}: {}",
img_name, e
)));
}
};
let resp = match inner.message().await {
Ok(response) => response.unwrap().data,
Err(e) => {
return Err(ImageError::ReadContentFailed(format!(
"Failed to get the inner content of image {}: {}",
img_name, e
)));
}
};
drop(c);
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()
}
_ => {
return Err(ImageError::UnexpectedMediaType);
}
},
_ => {
return Err(ImageError::UnexpectedMediaType);
}
};
if img_config.is_none() {
return Err(ImageError::ImageConfigurationNotFound(format!(
"save_img_config: Image configuration not found for image {}",
img_name
)));
}
let img_config = img_config.unwrap();
Self::insert_image_config(img_name, img_config)
}
async fn handle_index(data: &[u8], ns: &str) -> Result<Option<ImageConfiguration>, ImageError> {
let image_index: ImageIndex = ::serde_json::from_slice(data).map_err(|e| {
ImageError::DeserializationFailed(format!("Failed to parse JSON: {}", e))
})?;
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::get_client().await.content();
let mut inner = match c.read(with_namespace!(req, ns)).await {
Ok(response) => response.into_inner(),
Err(e) => {
return Err(ImageError::ReadContentFailed(format!(
"Failed to handler index : {}",
e
)));
}
};
let resp = match inner.message().await {
Ok(response) => response.unwrap().data,
Err(e) => {
return Err(ImageError::ReadContentFailed(format!(
"Failed to handle index inner : {}",
e
)));
}
};
drop(c);
Self::handle_manifest(&resp, ns).await
}
async fn handle_manifest(
data: &[u8],
ns: &str,
) -> Result<Option<ImageConfiguration>, ImageError> {
let img_manifest: ImageManifest = match ::serde_json::from_slice(data) {
Ok(manifest) => manifest,
Err(e) => {
return Err(ImageError::DeserializationFailed(format!(
"Failed to deserialize image manifest: {}",
e
)));
}
};
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::get_client().await.content();
let mut inner = match c.read(with_namespace!(req, ns)).await {
Ok(response) => response.into_inner(),
Err(e) => {
return Err(ImageError::ReadContentFailed(format!(
"Failed to handler index : {}",
e
)));
}
};
let resp = match inner.message().await {
Ok(response) => response.unwrap().data,
Err(e) => {
return Err(ImageError::ReadContentFailed(format!(
"Failed to handle index inner : {}",
e
)));
}
};
Ok(::serde_json::from_slice(&resp).unwrap())
}
fn insert_image_config(image_name: &str, config: ImageConfiguration) -> Result<(), ImageError> {
let mut map = GLOBAL_IMAGE_MAP.write().unwrap();
map.insert(image_name.to_string(), config);
Ok(())
}
pub fn get_image_config(image_name: &str) -> Result<ImageConfiguration, ImageError> {
let map = GLOBAL_IMAGE_MAP.read().unwrap();
if let Some(config) = map.get(image_name) {
Ok(config.clone())
} else {
Err(ImageError::ImageConfigurationNotFound(format!(
"get_image_config: Image configuration not found for image {}",
image_name
)))
}
}
pub fn get_runtime_config(image_name: &str) -> Result<ImageRuntimeConfig, ImageError> {
let map = GLOBAL_IMAGE_MAP.read().unwrap();
if let Some(config) = map.get(image_name) {
if let Some(config) = config.config() {
let env = config
.env()
.clone()
.expect("Failed to get environment variables");
let args = config
.cmd()
.clone()
.expect("Failed to get command arguments");
let ports = config.exposed_ports().clone().unwrap_or_else(|| {
log::warn!("Exposed ports not found, using default port 8080/tcp");
vec!["8080/tcp".to_string()]
});
let cwd = config.working_dir().clone().unwrap_or_else(|| {
log::warn!("Working directory not found, using default /");
"/".to_string()
});
Ok(ImageRuntimeConfig::new(env, args, ports, cwd))
} else {
Err(ImageError::ImageConfigurationNotFound(format!(
"Image configuration is empty for image {}",
image_name
)))
}
} else {
Err(ImageError::ImageConfigurationNotFound(format!(
"get_runtime_config: Image configuration not found for image {}",
image_name
)))
}
}
// 不用这个也能拉取镜像?
pub fn get_resolver() {
todo!()
}
}
fn check_namespace(ns: &str) -> String {
match ns {
"" => DEFAULT_NAMESPACE.to_string(),
"" => crate::consts::DEFAULT_FUNCTION_NAMESPACE.to_string(),
_ => ns.to_string(),
}
}

View File

@ -0,0 +1,131 @@
use containerd_client::{
services::v1::snapshots::{MountsRequest, PrepareSnapshotRequest, RemoveSnapshotRequest},
types::Mount,
with_namespace,
};
use tonic::Request;
use crate::impls::error::ContainerdError;
use super::{ContainerdService, cni::Endpoint, function::ContainerStaticMetadata};
impl ContainerdService {
#[allow(unused)]
pub(super) async fn get_mounts(
&self,
cid: &str,
ns: &str,
) -> Result<Vec<Mount>, ContainerdError> {
let mut sc = self.client.snapshots();
let req = MountsRequest {
snapshotter: crate::consts::DEFAULT_SNAPSHOTTER.to_string(),
key: cid.to_string(),
};
let mounts = sc
.mounts(with_namespace!(req, ns))
.await
.map_err(|e| {
log::error!("Failed to get mounts: {}", e);
ContainerdError::CreateTaskError(e.to_string())
})?
.into_inner()
.mounts;
Ok(mounts)
}
pub async fn prepare_snapshot(
&self,
container: &ContainerStaticMetadata,
) -> Result<Vec<Mount>, ContainerdError> {
let parent_snapshot = self
.get_parent_snapshot(&container.image, &container.endpoint.namespace)
.await?;
self.do_prepare_snapshot(
&container.endpoint.service,
&container.endpoint.namespace,
parent_snapshot,
)
.await
}
async fn do_prepare_snapshot(
&self,
cid: &str,
ns: &str,
parent_snapshot: String,
) -> Result<Vec<Mount>, ContainerdError> {
let req = PrepareSnapshotRequest {
snapshotter: crate::consts::DEFAULT_SNAPSHOTTER.to_string(),
key: cid.to_string(),
parent: parent_snapshot,
..Default::default()
};
let mut client = self.client.snapshots();
let resp = client
.prepare(with_namespace!(req, ns))
.await
.map_err(|e| {
log::error!("Failed to prepare snapshot: {}", e);
ContainerdError::CreateSnapshotError(e.to_string())
})?;
log::trace!("Prepare snapshot response: {:?}", resp);
Ok(resp.into_inner().mounts)
}
async fn get_parent_snapshot(
&self,
image_name: &str,
namespace: &str,
) -> Result<String, ContainerdError> {
use sha2::Digest;
let config = self
.image_config(image_name, namespace)
.await
.map_err(|e| {
log::error!("Failed to get image config: {}", e);
ContainerdError::GetParentSnapshotError(e.to_string())
})?;
if config.rootfs().diff_ids().is_empty() {
log::error!("Image config has no diff_ids for image: {}", image_name);
return Err(ContainerdError::GetParentSnapshotError(
"No diff_ids found in image config".to_string(),
));
}
let mut iter = config.rootfs().diff_ids().iter();
let mut ret = iter
.next()
.map_or_else(String::new, |layer_digest| layer_digest.clone());
for layer_digest in iter {
let mut hasher = sha2::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)
}
pub async fn remove_snapshot(&self, endpoint: &Endpoint) -> Result<(), ContainerdError> {
let mut sc = self.client.snapshots();
let req = RemoveSnapshotRequest {
snapshotter: crate::consts::DEFAULT_SNAPSHOTTER.to_string(),
key: endpoint.service.clone(),
};
sc.remove(with_namespace!(req, endpoint.namespace))
.await
.map_err(|e| {
log::error!("Failed to delete snapshot: {}", e);
ContainerdError::DeleteContainerError(e.to_string())
})?;
Ok(())
}
}

View File

@ -0,0 +1,329 @@
use super::{
ContainerdService, cni::Endpoint, error::ContainerdError, function::ContainerStaticMetadata,
};
use crate::consts::{VERSION_DEV, VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH};
use oci_spec::{
image::ImageConfiguration,
runtime::{
Capability, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxDeviceCgroupBuilder,
LinuxNamespaceBuilder, LinuxNamespaceType, LinuxResourcesBuilder, MountBuilder,
PosixRlimitBuilder, PosixRlimitType, ProcessBuilder, RootBuilder, Spec, SpecBuilder,
UserBuilder,
},
};
use std::path::Path;
fn oci_version() -> String {
format!(
"{}.{}.{}{}",
VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, VERSION_DEV
)
}
pub(super) fn generate_default_unix_spec(
ns: &str,
cid: &str,
runtime_config: &RuntimeConfig,
) -> Result<oci_spec::runtime::Spec, ContainerdError> {
let caps = [
Capability::Chown,
Capability::DacOverride,
Capability::Fsetid,
Capability::Fowner,
Capability::Mknod,
Capability::NetRaw,
Capability::Setgid,
Capability::Setuid,
Capability::Setfcap,
Capability::Setpcap,
Capability::NetBindService,
Capability::SysChroot,
Capability::Kill,
Capability::AuditWrite,
];
let spec = SpecBuilder::default()
.version(oci_version())
.root(
RootBuilder::default()
.path("rootfs")
.readonly(true)
.build()
.unwrap(),
)
.process(
ProcessBuilder::default()
.cwd(runtime_config.cwd.clone())
.no_new_privileges(true)
.user(UserBuilder::default().uid(0u32).gid(0u32).build().unwrap())
.capabilities(
LinuxCapabilitiesBuilder::default()
.bounding(caps)
.permitted(caps)
.effective(caps)
.build()
.unwrap(),
)
.rlimits([PosixRlimitBuilder::default()
.typ(PosixRlimitType::RlimitNofile)
.hard(1024u64)
.soft(1024u64)
.build()
.unwrap()])
.args(runtime_config.args.clone())
.env(runtime_config.env.clone())
.build()
.unwrap(),
)
.linux(
LinuxBuilder::default()
.masked_paths([
"/proc/acpi".into(),
"/proc/asound".into(),
"/proc/kcore".into(),
"/proc/keys".into(),
"/proc/latency_stats".into(),
"/proc/timer_list".into(),
"/proc/timer_stats".into(),
"/proc/sched_debug".into(),
"/sys/firmware".into(),
"/proc/scsi".into(),
"/sys/devices/virtual/powercap".into(),
])
.readonly_paths([
"/proc/bus".into(),
"/proc/fs".into(),
"/proc/irq".into(),
"/proc/sys".into(),
"/proc/sysrq-trigger".into(),
])
.cgroups_path(Path::new("/").join(ns).join(cid))
.resources(
LinuxResourcesBuilder::default()
.devices([LinuxDeviceCgroupBuilder::default()
.allow(false)
.access("rwm")
.build()
.unwrap()])
.build()
.unwrap(),
)
.namespaces([
LinuxNamespaceBuilder::default()
.typ(LinuxNamespaceType::Pid)
.build()
.unwrap(),
LinuxNamespaceBuilder::default()
.typ(LinuxNamespaceType::Ipc)
.build()
.unwrap(),
LinuxNamespaceBuilder::default()
.typ(LinuxNamespaceType::Uts)
.build()
.unwrap(),
LinuxNamespaceBuilder::default()
.typ(LinuxNamespaceType::Mount)
.build()
.unwrap(),
LinuxNamespaceBuilder::default()
.typ(LinuxNamespaceType::Network)
.path(format!("/var/run/netns/{}", Endpoint::new(cid, ns)))
.build()
.unwrap(),
])
.build()
.unwrap(),
)
.mounts([
MountBuilder::default()
.destination("/proc")
.typ("proc")
.source("proc")
.options(["nosuid".into(), "noexec".into(), "nodev".into()])
.build()
.unwrap(),
MountBuilder::default()
.destination("/dev")
.typ("tmpfs")
.source("tmpfs")
.options([
"nosuid".into(),
"strictatime".into(),
"mode=755".into(),
"size=65536k".into(),
])
.build()
.unwrap(),
MountBuilder::default()
.destination("/dev/pts")
.typ("devpts")
.source("devpts")
.options([
"nosuid".into(),
"noexec".into(),
"newinstance".into(),
"ptmxmode=0666".into(),
"mode=0620".into(),
"gid=5".into(),
])
.build()
.unwrap(),
MountBuilder::default()
.destination("/dev/shm")
.typ("tmpfs")
.source("shm")
.options([
"nosuid".into(),
"noexec".into(),
"nodev".into(),
"mode=1777".into(),
"size=65536k".into(),
])
.build()
.unwrap(),
MountBuilder::default()
.destination("/dev/mqueue")
.typ("mqueue")
.source("mqueue")
.options(["nosuid".into(), "noexec".into(), "nodev".into()])
.build()
.unwrap(),
MountBuilder::default()
.destination("/sys")
.typ("sysfs")
.source("sysfs")
.options([
"nosuid".into(),
"noexec".into(),
"nodev".into(),
"ro".into(),
])
.build()
.unwrap(),
MountBuilder::default()
.destination("/run")
.typ("tmpfs")
.source("tmpfs")
.options([
"nosuid".into(),
"strictatime".into(),
"mode=755".into(),
"size=65536k".into(),
])
.build()
.unwrap(),
])
.build()
.map_err(|e| {
log::error!("Failed to generate spec: {}", e);
ContainerdError::GenerateSpecError(e.to_string())
})?;
Ok(spec)
}
#[allow(unused)]
pub(super) fn with_vm_network(spec: &mut Spec) -> Result<(), ContainerdError> {
let mounts = spec
.mounts()
.as_ref()
.expect("Spec's 'Mounts' field should not be None");
let mut new_mounts = mounts.clone();
new_mounts.extend([
MountBuilder::default()
.destination("/etc/resolv.conf")
.typ("bind")
.source("/etc/resolv.conf")
.options(["rbind".into(), "ro".into()])
.build()
.map_err(|e| {
log::error!("Failed to build OCI (resolv.conf) Mount: {}", e);
ContainerdError::GenerateSpecError(e.to_string())
})?,
MountBuilder::default()
.destination("/etc/hosts")
.typ("bind")
.source("/etc/hosts")
.options(["rbind".into(), "ro".into()])
.build()
.map_err(|e| {
log::error!("Failed to build OCI (hosts) Mount: {}", e);
ContainerdError::GenerateSpecError(e.to_string())
})?,
]);
let _ = spec.set_mounts(Some(new_mounts));
Ok(())
}
#[derive(Debug, Clone)]
pub struct RuntimeConfig {
pub env: Vec<String>,
pub args: Vec<String>,
pub ports: Vec<String>,
pub cwd: String,
}
impl TryFrom<ImageConfiguration> for RuntimeConfig {
type Error = ContainerdError;
fn try_from(value: ImageConfiguration) -> Result<Self, Self::Error> {
let conf_ref = value.config().as_ref();
let config = conf_ref.ok_or(ContainerdError::GenerateSpecError(
"Image configuration not found".to_string(),
))?;
let env = config.env().clone().ok_or_else(|| {
ContainerdError::GenerateSpecError("Environment variables not found".to_string())
})?;
let args = config.cmd().clone().ok_or_else(|| {
ContainerdError::GenerateSpecError("Command arguments not found".to_string())
})?;
let ports = config.exposed_ports().clone().unwrap_or_else(|| {
log::warn!("Exposed ports not found, using default port 8080/tcp");
vec!["8080/tcp".to_string()]
});
let cwd = config.working_dir().clone().unwrap_or_else(|| {
log::warn!("Working directory not found, using default /");
"/".to_string()
});
Ok(RuntimeConfig {
env,
args,
ports,
cwd,
})
}
}
impl ContainerdService {
pub async fn get_spec(
&self,
metadata: &ContainerStaticMetadata,
) -> Result<prost_types::Any, ContainerdError> {
let image_conf = self
.image_config(&metadata.image, &metadata.endpoint.namespace)
.await
.map_err(|e| {
log::error!("Failed to get image config: {}", e);
ContainerdError::GenerateSpecError(e.to_string())
})?;
let rt_conf = RuntimeConfig::try_from(image_conf)?;
let spec = generate_default_unix_spec(
&metadata.endpoint.namespace,
&metadata.endpoint.service,
&rt_conf,
)?;
let spec_json = serde_json::to_string(&spec).map_err(|e| {
log::error!("Failed to serialize spec to JSON: {}", e);
ContainerdError::GenerateSpecError(e.to_string())
})?;
let any_spec = prost_types::Any {
type_url: "types.containerd.io/opencontainers/runtime-spec/1/Spec".to_string(),
value: spec_json.into_bytes(),
};
Ok(any_spec)
}
}

View File

@ -0,0 +1,210 @@
use std::time::Duration;
use containerd_client::{
services::v1::{
CreateTaskRequest, DeleteTaskRequest, GetRequest, KillRequest, ListTasksRequest,
ListTasksResponse, StartRequest, WaitRequest, WaitResponse,
},
types::{Mount, v1::Process},
with_namespace,
};
use derive_more::Display;
use gateway::handlers::function::{DeleteError, DeployError};
use tonic::Request;
use super::{ContainerdService, cni::Endpoint};
#[derive(Debug, Clone, Hash, Eq, PartialEq, Display)]
pub enum TaskError {
NotFound,
AlreadyExists,
InvalidArgument,
// PermissionDenied,
Internal(String),
}
impl From<tonic::Status> for TaskError {
fn from(status: tonic::Status) -> Self {
use tonic::Code::*;
match status.code() {
NotFound => TaskError::NotFound,
AlreadyExists => TaskError::AlreadyExists,
InvalidArgument => TaskError::InvalidArgument,
// PermissionDenied => TaskError::PermissionDenied,
_ => TaskError::Internal(status.message().to_string()),
}
}
}
impl From<TaskError> for DeployError {
fn from(e: TaskError) -> DeployError {
match e {
TaskError::InvalidArgument => DeployError::Invalid(e.to_string()),
_ => DeployError::InternalError(e.to_string()),
}
}
}
impl From<TaskError> for DeleteError {
fn from(e: TaskError) -> DeleteError {
log::trace!("DeleteTaskError: {:?}", e);
match e {
TaskError::NotFound => DeleteError::NotFound(e.to_string()),
TaskError::InvalidArgument => DeleteError::Invalid(e.to_string()),
_ => DeleteError::Internal(e.to_string()),
}
}
}
impl ContainerdService {
/// 创建并启动任务
pub async fn new_task(&self, mounts: Vec<Mount>, endpoint: &Endpoint) -> Result<(), TaskError> {
let Endpoint {
service: cid,
namespace: ns,
} = endpoint;
// let mounts = self.get_mounts(cid, ns).await?;
self.do_create_task(cid, ns, mounts).await?;
self.do_start_task(cid, ns).await?;
Ok(())
}
async fn do_start_task(&self, cid: &str, ns: &str) -> Result<(), TaskError> {
let mut c: containerd_client::services::v1::tasks_client::TasksClient<
tonic::transport::Channel,
> = self.client.tasks();
let req = StartRequest {
container_id: cid.to_string(),
..Default::default()
};
let resp = c.start(with_namespace!(req, ns)).await?;
log::debug!("Task: {:?} started", cid);
log::trace!("Task start response: {:?}", resp);
Ok(())
}
async fn do_create_task(
&self,
cid: &str,
ns: &str,
rootfs: Vec<Mount>,
) -> Result<(), TaskError> {
let mut tc = self.client.tasks();
let create_request = CreateTaskRequest {
container_id: cid.to_string(),
rootfs,
..Default::default()
};
let _resp = tc.create(with_namespace!(create_request, ns)).await?;
Ok(())
}
pub async fn get_task(&self, endpoint: &Endpoint) -> Result<Process, TaskError> {
let Endpoint {
service: cid,
namespace: ns,
} = endpoint;
let mut tc = self.client.tasks();
let req = GetRequest {
container_id: cid.clone(),
..Default::default()
};
let resp = tc.get(with_namespace!(req, ns)).await?;
let task = resp.into_inner().process.ok_or(TaskError::NotFound)?;
Ok(task)
}
#[allow(dead_code)]
async fn list_task_by_cid(&self, cid: &str, ns: &str) -> Result<ListTasksResponse, TaskError> {
let mut c = self.client.tasks();
let request = ListTasksRequest {
filter: format!("container=={}", cid),
};
let response = c.list(with_namespace!(request, ns)).await?.into_inner();
Ok(response)
}
async fn do_kill_task(&self, cid: &str, ns: &str) -> Result<(), TaskError> {
let mut c = self.client.tasks();
let kill_request = KillRequest {
container_id: cid.to_string(),
signal: 15,
all: true,
..Default::default()
};
c.kill(with_namespace!(kill_request, ns)).await?;
Ok(())
}
async fn do_kill_task_force(&self, cid: &str, ns: &str) -> Result<(), TaskError> {
let mut c = self.client.tasks();
let kill_request = KillRequest {
container_id: cid.to_string(),
signal: 9,
all: true,
..Default::default()
};
c.kill(with_namespace!(kill_request, ns)).await?;
Ok(())
}
async fn do_delete_task(&self, cid: &str, ns: &str) -> Result<(), TaskError> {
let mut c = self.client.tasks();
let delete_request = DeleteTaskRequest {
container_id: cid.to_string(),
};
c.delete(with_namespace!(delete_request, ns)).await?;
Ok(())
}
async fn do_wait_task(&self, cid: &str, ns: &str) -> Result<WaitResponse, TaskError> {
let mut c = self.client.tasks();
let wait_request = WaitRequest {
container_id: cid.to_string(),
..Default::default()
};
let resp = c
.wait(with_namespace!(wait_request, ns))
.await?
.into_inner();
Ok(resp)
}
/// 杀死并删除任务
pub async fn kill_task_with_timeout(&self, endpoint: &Endpoint) -> Result<(), TaskError> {
let Endpoint {
service: cid,
namespace: ns,
} = endpoint;
let kill_timeout = Duration::from_secs(5);
let wait_future = self.do_wait_task(cid, ns);
self.do_kill_task(cid, ns).await?;
match tokio::time::timeout(kill_timeout, wait_future).await {
Ok(Ok(_)) => {
// 正常退出,尝试删除任务
self.do_delete_task(cid, ns).await?;
}
Ok(Err(e)) => {
// wait 报错
log::error!("Error while waiting for task {}: {:?}", cid, e);
return Err(e);
}
Err(_) => {
// 超时,强制 kill
log::warn!("Task {} did not exit in time, sending SIGKILL", cid);
self.do_kill_task_force(cid, ns).await?;
// 尝试删除任务
if let Err(e) = self.do_delete_task(cid, ns).await {
log::error!("Failed to delete task {} after SIGKILL: {:?}", cid, e);
}
}
}
Ok(())
}
}

View File

@ -0,0 +1,8 @@
#![feature(ip_from)]
#![feature(slice_as_array)]
pub mod consts;
pub mod impls;
pub mod provider;
pub mod systemd;
pub use impls::init_backend;

View File

@ -0,0 +1,36 @@
use faas_containerd::consts::DEFAULT_FAASDRS_DATA_DIR;
use tokio::signal::unix::{SignalKind, signal};
#[tokio::main]
async fn main() -> std::io::Result<()> {
dotenv::dotenv().ok();
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
faas_containerd::init_backend().await;
let provider = faas_containerd::provider::ContainerdProvider::new(DEFAULT_FAASDRS_DATA_DIR);
// leave for shutdown containers (stop tasks)
let _handle = provider.clone();
tokio::spawn(async move {
log::info!("Setting up signal handlers for graceful shutdown");
let mut sigint = signal(SignalKind::interrupt()).unwrap();
let mut sigterm = signal(SignalKind::terminate()).unwrap();
let mut sigquit = signal(SignalKind::quit()).unwrap();
tokio::select! {
_ = sigint.recv() => log::info!("SIGINT received, starting graceful shutdown..."),
_ = sigterm.recv() => log::info!("SIGTERM received, starting graceful shutdown..."),
_ = sigquit.recv() => log::info!("SIGQUIT received, starting graceful shutdown..."),
}
// for (_q, ctr) in handle.ctr_instance_map.lock().await.drain() {
// let _ = ctr.delete().await;
// }
log::info!("Successfully shutdown all containers");
});
gateway::bootstrap::serve(provider)
.unwrap_or_else(|e| {
log::error!("Failed to start server: {}", e);
std::process::exit(1);
})
.await
}

View File

@ -0,0 +1,35 @@
use crate::impls::cni::Endpoint;
use crate::impls::{backend, cni};
use crate::provider::ContainerdProvider;
use gateway::handlers::function::DeleteError;
use gateway::types::function::Query;
impl ContainerdProvider {
pub(crate) async fn _delete(&self, function: Query) -> Result<(), DeleteError> {
let endpoint: Endpoint = function.into();
log::trace!("Deleting function: {:?}", endpoint);
backend().kill_task_with_timeout(&endpoint).await?;
let del_ctr_err = backend().delete_container(&endpoint).await.map_err(|e| {
log::error!("Failed to delete container: {:?}", e);
e
});
let rm_snap_err = backend().remove_snapshot(&endpoint).await.map_err(|e| {
log::error!("Failed to remove snapshot: {:?}", e);
e
});
let del_net_err = cni::cni_impl::delete_cni_network(endpoint);
if del_ctr_err.is_ok() && rm_snap_err.is_ok() && del_net_err.is_ok() {
Ok(())
} else {
Err(DeleteError::Internal(format!(
"{:?}, {:?}, {:?}",
del_ctr_err, rm_snap_err, del_net_err
)))
}
}
}

View File

@ -0,0 +1,96 @@
use crate::impls::cni;
use crate::impls::{self, backend, function::ContainerStaticMetadata};
use crate::provider::ContainerdProvider;
use gateway::handlers::function::DeployError;
use gateway::types::function::Deployment;
use scopeguard::{ScopeGuard, guard};
impl ContainerdProvider {
pub(crate) async fn _deploy(&self, config: Deployment) -> Result<(), DeployError> {
let metadata = ContainerStaticMetadata::from(config);
log::trace!("Deploying function: {:?}", metadata);
// not going to check the conflict of namespace, should be handled by containerd backend
backend()
.prepare_image(&metadata.image, &metadata.endpoint.namespace, true)
.await
.map_err(|img_err| {
use impls::oci_image::ImageError;
log::error!("Image '{}' fetch failed: {}", &metadata.image, img_err);
match img_err {
ImageError::ImageNotFound(e) => DeployError::Invalid(e.to_string()),
_ => DeployError::InternalError(img_err.to_string()),
}
})?;
log::trace!("Image '{}' fetch ok", &metadata.image);
let mounts = backend().prepare_snapshot(&metadata).await.map_err(|e| {
log::error!("Failed to prepare snapshot: {:?}", e);
DeployError::InternalError(e.to_string())
})?;
let snapshot_defer = scopeguard::guard((), |()| {
log::trace!("Cleaning up snapshot");
let endpoint = metadata.endpoint.clone();
tokio::spawn(async move { backend().remove_snapshot(&endpoint).await });
});
// let network = CNIEndpoint::new(&metadata.container_id, &metadata.namespace)?;
let (ip, netns) = cni::cni_impl::create_cni_network(&metadata.endpoint).map_err(|e| {
log::error!("Failed to create CNI network: {}", e);
DeployError::InternalError(e.to_string())
})?;
let netns_defer = guard(netns, |ns| ns.remove().unwrap());
let _ = backend().create_container(&metadata).await.map_err(|e| {
log::error!("Failed to create container: {:?}", e);
DeployError::InternalError(e.to_string())
})?;
let container_defer = scopeguard::guard((), |()| {
let endpoint = metadata.endpoint.clone();
tokio::spawn(async move { backend().delete_container(&endpoint).await });
});
// TODO: Use ostree-ext
// let img_conf = BACKEND.get().unwrap().get_runtime_config(&metadata.image).unwrap();
backend().new_task(mounts, &metadata.endpoint).await?;
let task_defer = scopeguard::guard((), |()| {
let endpoint = metadata.endpoint.clone();
tokio::spawn(async move { backend().kill_task_with_timeout(&endpoint).await });
});
use std::net::IpAddr::*;
match ip.address() {
V4(addr) => {
if let Err(err) = self
.database
.insert(metadata.endpoint.to_string(), &addr.octets())
{
log::error!("Failed to insert into database: {:?}", err);
return Err(DeployError::InternalError(err.to_string()));
}
}
V6(addr) => {
if let Err(err) = self
.database
.insert(metadata.endpoint.to_string(), &addr.octets())
{
log::error!("Failed to insert into database: {:?}", err);
return Err(DeployError::InternalError(err.to_string()));
}
}
}
log::info!("container was created successfully: {}", metadata.endpoint);
ScopeGuard::into_inner(snapshot_defer);
ScopeGuard::into_inner(netns_defer);
ScopeGuard::into_inner(container_defer);
ScopeGuard::into_inner(task_defer);
Ok(())
}
}

View File

@ -0,0 +1,19 @@
use crate::provider::ContainerdProvider;
pub enum GetError {
NotFound,
InternalError,
}
impl ContainerdProvider {
// pub async fn getfn(
// &self,
// query: function::Query,
// ) -> Option<FunctionInstance> {
// let instance = self.ctr_instance_map
// .lock()
// .await
// .get(&query)
// .cloned();
// }
}

View File

@ -0,0 +1,69 @@
use gateway::{handlers::function::ListError, types::function::Status};
use crate::{
impls::{backend, cni::Endpoint, task::TaskError},
provider::ContainerdProvider,
};
impl ContainerdProvider {
pub(crate) async fn _list(&self, namespace: String) -> Result<Vec<Status>, ListError> {
let containers = backend().list_container(&namespace).await.map_err(|e| {
log::error!(
"failed to get container list for namespace {} because {:?}",
namespace,
e
);
ListError::Internal(e.to_string())
})?;
let mut statuses: Vec<Status> = Vec::new();
for container in containers {
let endpoint = Endpoint {
service: container.id.clone(),
namespace: namespace.clone(),
};
let created_at = container.created_at.unwrap().to_string();
let mut replicas = 0;
match backend().get_task(&endpoint).await {
Ok(task) => {
let status = task.status;
if status == 2 || status == 3 {
replicas = 1;
}
}
Err(TaskError::NotFound) => continue,
Err(e) => {
log::warn!(
"failed to get task for function {:?} because {:?}",
&endpoint,
e
);
}
}
// 大部分字段并未实现使用None填充
let status = Status {
name: endpoint.service,
namespace: Some(endpoint.namespace),
image: container.image,
env_process: None,
env_vars: None,
constraints: None,
secrets: None,
labels: None,
annotations: None,
limits: None,
requests: None,
read_only_root_filesystem: false,
invocation_count: None,
replicas: Some(replicas),
available_replicas: Some(replicas),
created_at: Some(created_at),
usage: None,
};
statuses.push(status);
}
Ok(statuses)
}
}

View File

@ -0,0 +1,6 @@
pub mod delete;
pub mod deploy;
pub mod list;
pub mod resolve;
pub mod status;
pub mod update;

View File

@ -0,0 +1,65 @@
use std::net::{IpAddr, Ipv4Addr};
use actix_http::uri::Builder;
use gateway::handlers::function::ResolveError;
use gateway::types::function::Query;
use crate::impls::cni::{self, Endpoint};
use crate::provider::ContainerdProvider;
fn upstream(addr: IpAddr) -> Builder {
actix_http::Uri::builder()
.scheme("http")
.authority(format!("{}:{}", addr, 8080))
}
impl ContainerdProvider {
pub(crate) async fn _resolve(
&self,
query: Query,
) -> Result<actix_http::uri::Builder, ResolveError> {
let endpoint = Endpoint::from(query);
log::trace!("Resolving function: {:?}", endpoint);
let addr_oct = self
.database
.get(endpoint.to_string())
.map_err(|e| {
log::error!("Failed to get container address: {:?}", e);
ResolveError::Internal(e.to_string())
})?
.ok_or(ResolveError::NotFound("container not found".to_string()))?;
log::trace!("Container address: {:?}", addr_oct.as_array::<4>());
// We force the address to be IPv4 here
let addr = IpAddr::V4(Ipv4Addr::from_octets(*addr_oct.as_array::<4>().unwrap()));
// Check if the coresponding netns is still alive
// We can achieve this by checking the /run/cni/faasrs-cni-bridge,
// if the ip filename is still there
if cni::cni_impl::check_network_exists(addr) {
log::trace!("CNI network exists for {}", addr);
Ok(upstream(addr))
} else {
log::error!("CNI network not exists for {}", addr);
let _ = self.database.remove(endpoint.to_string());
Err(ResolveError::Internal("CNI network not exists".to_string()))
}
}
}
#[cfg(test)]
mod tests {
use std::net::{IpAddr, Ipv4Addr};
#[test]
fn test_uri() {
let addr = IpAddr::V4(Ipv4Addr::new(10, 42, 2, 48));
let uri = super::upstream(addr).path_and_query("").build().unwrap();
assert_eq!(uri.scheme_str(), Some("http"));
assert_eq!(uri.authority().unwrap().host(), addr.to_string());
assert_eq!(uri.authority().unwrap().port_u16(), Some(8080));
assert_eq!(uri.to_string(), format!("http://{}:8080/", addr));
}
}

View File

@ -0,0 +1,69 @@
use gateway::{
handlers::function::ResolveError,
types::function::{Query, Status},
};
use crate::{
impls::{backend, cni::Endpoint, container::ContainerError},
provider::ContainerdProvider,
};
impl ContainerdProvider {
pub(crate) async fn _status(&self, function: Query) -> Result<Status, ResolveError> {
let endpoint: Endpoint = function.into();
let container = backend().load_container(&endpoint).await.map_err(|e| {
log::error!(
"failed to load container for function {:?} because {:?}",
endpoint,
e
);
match e {
ContainerError::NotFound => ResolveError::NotFound(e.to_string()),
ContainerError::Internal => ResolveError::Internal(e.to_string()),
_ => ResolveError::Invalid(e.to_string()),
}
})?;
let created_at = container.created_at.unwrap().to_string();
let mut replicas = 0;
match backend().get_task(&endpoint).await {
Ok(task) => {
let status = task.status;
if status == 2 || status == 3 {
replicas = 1;
}
}
Err(e) => {
log::warn!(
"failed to get task for function {:?} because {:?}",
&endpoint,
e
);
}
}
// 大部分字段并未实现使用None填充
let status = Status {
name: container.id,
namespace: Some(endpoint.namespace),
image: container.image,
env_process: None,
env_vars: None,
constraints: None,
secrets: None,
labels: None,
annotations: None,
limits: None,
requests: None,
read_only_root_filesystem: false,
invocation_count: None,
replicas: Some(replicas),
available_replicas: Some(replicas),
created_at: Some(created_at),
usage: None,
};
Ok(status)
}
}

View File

@ -0,0 +1,32 @@
use gateway::{
handlers::function::{DeleteError, DeployError, UpdateError},
types::function::{Deployment, Query},
};
use crate::provider::ContainerdProvider;
impl ContainerdProvider {
pub(crate) async fn _update(&self, param: Deployment) -> Result<(), UpdateError> {
let function = Query {
service: param.service.clone(),
namespace: param.namespace.clone(),
};
self._delete(function).await.map_err(|e| {
log::error!("failed to delete function when update because {:?}", e);
match e {
DeleteError::NotFound(e) => UpdateError::NotFound(e.to_string()),
DeleteError::Internal(e) => UpdateError::Internal(e.to_string()),
_ => UpdateError::Internal(e.to_string()),
}
})?;
self._deploy(param).await.map_err(|e| {
log::error!("failed to deploy function when update because {:?}", e);
match e {
DeployError::Invalid(e) => UpdateError::Invalid(e.to_string()),
DeployError::InternalError(e) => UpdateError::Internal(e.to_string()),
}
})?;
Ok(())
}
}

View File

@ -0,0 +1,49 @@
pub mod function;
use std::{path::Path, sync::Arc};
use gateway::{
handlers::function::{DeleteError, DeployError, ListError, ResolveError, UpdateError},
provider::Provider,
types::function::{Deployment, Query, Status},
};
pub struct ContainerdProvider {
// pub ctr_instance_map: tokio::sync::Mutex<HashMap<Query, FunctionInstance>>,
database: sled::Db,
}
impl ContainerdProvider {
pub fn new<P: AsRef<Path>>(path: P) -> Arc<Self> {
Arc::new(ContainerdProvider {
// ctr_instance_map: tokio::sync::Mutex::new(HashMap::new()),
database: sled::open(path).unwrap(),
})
}
}
impl Provider for ContainerdProvider {
async fn resolve(&self, function: Query) -> Result<actix_http::uri::Builder, ResolveError> {
self._resolve(function).await
}
async fn deploy(&self, param: Deployment) -> Result<(), DeployError> {
self._deploy(param).await
}
async fn delete(&self, function: Query) -> Result<(), DeleteError> {
self._delete(function).await
}
async fn list(&self, namespace: String) -> Result<Vec<Status>, ListError> {
self._list(namespace).await
}
async fn update(&self, param: Deployment) -> Result<(), UpdateError> {
self._update(param).await
}
async fn status(&self, function: Query) -> Result<Status, ResolveError> {
self._status(function).await
}
}

View File

@ -0,0 +1,150 @@
use actix_web::App;
use actix_web::http::StatusCode;
use actix_web::test;
use faas_containerd::consts::DEFAULT_FAASDRS_DATA_DIR;
use gateway::bootstrap::config_app;
use serde_json::json;
#[actix_web::test]
#[ignore]
async fn test_handlers_in_order() {
dotenv::dotenv().ok();
faas_containerd::init_backend().await;
let provider = faas_containerd::provider::ContainerdProvider::new(DEFAULT_FAASDRS_DATA_DIR);
let app = test::init_service(App::new().configure(config_app(provider))).await;
// test proxy no-found-function in namespace 'faasrs-test-namespace'
let req = test::TestRequest::get()
.uri("/function/test-no-found-function")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let response_body = test::read_body(resp).await;
let response_str = std::str::from_utf8(&response_body).unwrap();
assert!(response_str.contains("Invalid function name"));
// test update no-found-function in namespace 'faasrs-test-namespace'
let req = test::TestRequest::put()
.uri("/system/functions")
.set_json(json!({
"service": "test-no-found-function",
"image": "hub.scutosc.cn/dolzhuying/echo:latest",
"namespace": "faasrs-test-namespace"
}))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let response_body = test::read_body(resp).await;
let response_str = std::str::from_utf8(&response_body).unwrap();
assert!(response_str.contains("NotFound: container not found"));
// test delete no-found-function in namespace 'faasrs-test-namespace'
let req = test::TestRequest::delete()
.uri("/system/functions")
.set_json(json!({
"functionName": "test-no-found-function",
"namespace": "faasrs-test-namespace"
}))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
// test deploy test-function in namespace 'faasrs-test-namespace'
let req = test::TestRequest::post()
.uri("/system/functions")
.set_json(json!({
"service": "test-function",
"image": "hub.scutosc.cn/dolzhuying/echo:latest",
"namespace": "faasrs-test-namespace"
}))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(
resp.status(),
StatusCode::ACCEPTED,
"error: {:?}",
resp.response()
);
// test update test-function in namespace 'faasrs-test-namespace'
let req = test::TestRequest::put()
.uri("/system/functions")
.set_json(json!({
"service": "test-function",
"image": "hub.scutosc.cn/dolzhuying/echo:latest",
"namespace": "faasrs-test-namespace"
}))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::ACCEPTED);
let response_body = test::read_body(resp).await;
let response_str = std::str::from_utf8(&response_body).unwrap();
assert!(response_str.contains("function test-function was updated successfully"));
// test list
let req = test::TestRequest::get()
.uri("/system/functions?namespace=faasrs-test-namespace")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let response_body = test::read_body(resp).await;
let response_str = std::str::from_utf8(&response_body).unwrap();
let response_json: serde_json::Value = serde_json::from_str(response_str).unwrap();
if let Some(arr) = response_json.as_array() {
for item in arr {
assert_eq!(
item["name"],
serde_json::Value::String("test-function".to_string())
);
assert_eq!(
item["image"],
serde_json::Value::String("hub.scutosc.cn/dolzhuying/echo:latest".to_string())
);
assert_eq!(
item["namespace"],
serde_json::Value::String("faasrs-test-namespace".to_string())
);
}
}
// test status test-function in namespace 'faasrs-test-namespace'
let req = test::TestRequest::get()
.uri("/system/function/test-function?namespace=faasrs-test-namespace")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let response_body = test::read_body(resp).await;
let response_str = std::str::from_utf8(&response_body).unwrap();
let response_json: serde_json::Value = serde_json::from_str(response_str).unwrap();
if let Some(arr) = response_json.as_array() {
for item in arr {
assert_eq!(item["name"], "test-function");
assert_eq!(item["image"], "hub.scutosc.cn/dolzhuying/echo:latest");
assert_eq!(item["namespace"], "faasrs-test-namespace");
}
}
// test proxy test-function in namespace 'faasrs-test-namespace'
let req = test::TestRequest::get()
.uri("/function/test-function.faasrs-test-namespace")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let response_body = test::read_body(resp).await;
let response_str = std::str::from_utf8(&response_body).unwrap();
assert!(response_str.contains("Hello world!"));
// test delete test-function in namespace 'faasrs-test-namespace'
let req = test::TestRequest::delete()
.uri("/system/functions")
.set_json(json!({
"functionName": "test-function",
"namespace": "faasrs-test-namespace"
}))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let response_body = test::read_body(resp).await;
let response_str = std::str::from_utf8(&response_body).unwrap();
assert!(response_str.contains("function test-function was deleted successfully"));
}

View File

@ -1,35 +1,25 @@
[package]
name = "provider"
name = "gateway"
edition = "2024"
version.workspace = true
authors.workspace = true
[dependencies]
actix-web = "4.5.1"
actix-web = "4.11.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.37.0", features = ["full"] }
bollard = "0.13.0"
uuid = { version = "1.8.0", features = ["v4"] }
actix-web-httpauth = "0.6"
config = "0.11"
thiserror = "1.0"
awc = "3.6.0"
prometheus = "0.13"
tempfile = "3.2"
tower = "0.4"
regex = "1"
futures = "0.3"
actix-service = "2"
base64 = "0.13"
futures-util = "0.3"
service = { path = "../service" }
cni = { path = "../cni" }
async-trait = "0.1"
lazy_static = "1.4.0"
log = "0.4.27"
my-workspace-hack = { version = "0.1", path = "../my-workspace-hack" }
url = "2.4"
derive_more = { version = "2", features = ["full"] }
tonic = "0.12"
http = "1.3.1"
tokio-util = "*"
http = "*"
actix-http = "*"
chrono = "0.4.41"

View File

@ -0,0 +1,175 @@
use actix_web::{
App, HttpServer,
dev::Server,
web::{self, ServiceConfig},
};
use std::{collections::HashMap, sync::Arc};
use crate::{
handlers::{self, proxy::PROXY_DISPATCH_PATH},
// metrics::HttpMetrics,
provider::Provider,
types::config::FaaSConfig,
};
pub fn config_app<P: Provider>(provider: Arc<P>) -> impl FnOnce(&mut ServiceConfig) {
// let _registry = Registry::new();
let provider = web::Data::from(provider);
let app_state = web::Data::new(AppState {
// metrics: HttpMetrics::new(),
credentials: None,
});
move |cfg: &mut ServiceConfig| {
cfg.app_data(app_state)
.app_data(provider)
.service(
web::scope("/system")
.service(
web::resource("/functions")
.route(web::get().to(handlers::function::list::<P>))
.route(web::put().to(handlers::function::update::<P>))
.route(web::post().to(handlers::function::deploy::<P>))
.route(web::delete().to(handlers::function::delete::<P>)),
)
.service(
web::resource("/function/{functionName}")
.route(web::get().to(handlers::function::status::<P>)),
), // .service(
// web::resource("/scale-function/{name}")
// .route(web::post().to(handlers::scale_function)),
// )
// .service(web::resource("/info").route(web::get().to(handlers::info)))
// .service(
// web::resource("/secrets")
// .route(web::get().to(handlers::secrets))
// .route(web::post().to(handlers::secrets))
// .route(web::put().to(handlers::secrets))
// .route(web::delete().to(handlers::secrets)),
// )
// .service(web::resource("/logs").route(web::get().to(handlers::logs)))
// .service(
// web::resource("/namespaces")
// .route(web::get().to(handlers::list_namespaces))
// .route(web::post().to(handlers::mutate_namespace)),
// ),
// )
)
.service(web::scope("/function").service(
web::resource(PROXY_DISPATCH_PATH).route(web::to(handlers::proxy::proxy::<P>)),
));
// .route("/metrics", web::get().to(handlers::telemetry))
// .route("/healthz", web::get().to(handlers::health));
}
}
//应用程序状态,存储共享的数据,如配置、指标、认证信息等,为业务函数提供支持
#[derive(Clone)]
#[allow(dead_code)]
struct AppState {
// config: FaaSConfig, //应用程序的配置用于识别是否开启Basic Auth等
// metrics: HttpMetrics, //用于监视http请求的持续时间和总数
// metrics: HttpMetrics, //用于监视http请求的持续时间和总数
credentials: Option<HashMap<String, String>>, //当有认证信息的时候,获取认证信息
}
// this is a blocking serve function
pub fn serve<P: Provider>(provider: Arc<P>) -> std::io::Result<Server> {
log::info!("Checking config file");
let config = FaaSConfig::new();
let port = config.tcp_port.unwrap_or(8080);
// 如果启用了Basic Auth从指定路径读取认证凭证并存储在应用程序状态中
// TODO: Authentication Logic
let server = HttpServer::new(move || App::new().configure(config_app(provider.clone())))
.bind(("0.0.0.0", port))?
.run();
Ok(server)
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use crate::handlers::proxy::{PROXY_DISPATCH_PATH, ProxyQuery};
use actix_web::{App, HttpResponse, Responder, test, web};
async fn dispatcher(any: web::Path<String>) -> impl Responder {
let meta = ProxyQuery::from_str(&any).unwrap();
HttpResponse::Ok().body(format!(
"{}|{}|{}",
meta.query.service,
meta.query.namespace.unwrap_or_default(),
meta.path
))
}
#[actix_web::test]
async fn test_proxy() {
let app = test::init_service(
App::new().service(web::resource(PROXY_DISPATCH_PATH).route(web::get().to(dispatcher))),
)
.await;
let (unslash, slash, resp0, a0) = (
"/service.namespace/path",
"/service.namespace/path/",
"service|namespace|/path",
"service|namespace|/path/",
);
let (unslash1, slash1, resp1, a1) = (
"/service/path",
"/service/path/",
"service||/path",
"service||/path/",
);
let (unslash2, slash2, resp2, a2) = (
"/service.namespace",
"/service.namespace/",
"service|namespace|",
"service|namespace|/",
);
let (unslash3, slash3, resp3, a3) = ("/service", "/service/", "service||", "service||/");
let req = test::TestRequest::get().uri(unslash).to_request();
let resp = test::call_and_read_body(&app, req).await;
assert_eq!(resp, resp0);
let req = test::TestRequest::get().uri(slash).to_request();
let resp = test::call_and_read_body(&app, req).await;
assert_eq!(resp, a0);
let req = test::TestRequest::get().uri(unslash1).to_request();
let resp = test::call_and_read_body(&app, req).await;
assert_eq!(resp, resp1);
let req = test::TestRequest::get().uri(slash1).to_request();
let resp = test::call_and_read_body(&app, req).await;
assert_eq!(resp, a1);
let req = test::TestRequest::get().uri(unslash2).to_request();
let resp = test::call_and_read_body(&app, req).await;
assert_eq!(resp, resp2);
let req = test::TestRequest::get().uri(slash2).to_request();
let resp = test::call_and_read_body(&app, req).await;
assert_eq!(resp, a2);
let req = test::TestRequest::get().uri(unslash3).to_request();
let resp = test::call_and_read_body(&app, req).await;
assert_eq!(resp, resp3);
let req = test::TestRequest::get().uri(slash3).to_request();
let resp = test::call_and_read_body(&app, req).await;
assert_eq!(resp, a3);
// test with empty path
let req = test::TestRequest::get().uri("/").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), 404);
}
}

View File

@ -0,0 +1,173 @@
use crate::provider::Provider;
use crate::types::function::{Delete, Deployment, Query};
use actix_http::StatusCode;
use actix_web::ResponseError;
use actix_web::{HttpResponse, web};
use derive_more::derive::Display;
use serde::Deserialize;
// 参考响应状态 https://github.com/openfaas/faas/blob/7803ea1861f2a22adcbcfa8c79ed539bc6506d5b/api-docs/spec.openapi.yml#L121C1-L140C45
// 请求体反序列化失败自动返回400错误
pub async fn deploy<P: Provider>(
provider: web::Data<P>,
info: web::Json<Deployment>,
) -> Result<HttpResponse, DeployError> {
let service = info.0.service.clone();
(*provider).deploy(info.0).await.map(|()| {
HttpResponse::Accepted().body(format!("function {} was created successfully", service))
})
}
pub async fn update<P: Provider>(
provider: web::Data<P>,
info: web::Json<Deployment>,
) -> Result<HttpResponse, UpdateError> {
let service = info.0.service.clone();
(*provider).update(info.0).await.map(|()| {
HttpResponse::Accepted().body(format!("function {} was updated successfully", service))
})
}
pub async fn delete<P: Provider>(
provider: web::Data<P>,
info: web::Json<Delete>,
) -> Result<HttpResponse, DeleteError> {
let service = info.0.function_name.clone();
let query = Query {
service: service.clone(),
namespace: Some(info.0.namespace),
};
(*provider)
.delete(query)
.await
.map(|()| HttpResponse::Ok().body(format!("function {} was deleted successfully", service)))
}
#[derive(Debug, Deserialize)]
pub struct ListParam {
namespace: String,
}
pub async fn list<P: Provider>(
provider: web::Data<P>,
info: web::Query<ListParam>,
) -> Result<HttpResponse, ListError> {
(*provider)
.list(info.namespace.clone())
.await
.map(|functions| HttpResponse::Ok().json(functions))
}
#[derive(Debug, Deserialize)]
pub struct StatusParam {
namespace: Option<String>,
}
pub async fn status<P: Provider>(
provider: web::Data<P>,
name: web::Path<String>,
info: web::Query<StatusParam>,
) -> Result<HttpResponse, ResolveError> {
let query = Query {
service: name.into_inner(),
namespace: info.namespace.clone(),
};
let status = (*provider).status(query).await?;
Ok(HttpResponse::Ok().json(status))
}
// TODO: 为 Errors 添加错误信息
#[derive(Debug, Display)]
pub enum DeployError {
#[display("Invalid: {}", _0)]
Invalid(String),
#[display("Internal: {}", _0)]
InternalError(String),
}
#[derive(Debug, Display)]
pub enum DeleteError {
#[display("Invalid: {}", _0)]
Invalid(String),
#[display("NotFound: {}", _0)]
NotFound(String),
#[display("Internal: {}", _0)]
Internal(String),
}
#[derive(Debug, Display)]
pub enum ResolveError {
#[display("NotFound: {}", _0)]
NotFound(String),
#[display("Invalid: {}", _0)]
Invalid(String),
#[display("Internal: {}", _0)]
Internal(String),
}
#[derive(Debug, Display)]
pub enum ListError {
#[display("Internal: {}", _0)]
Internal(String),
#[display("NotFound: {}", _0)]
NotFound(String),
}
#[derive(Debug, Display)]
pub enum UpdateError {
#[display("Invalid: {}", _0)]
Invalid(String),
#[display("Internal: {}", _0)]
Internal(String),
#[display("NotFound: {}", _0)]
NotFound(String),
}
impl ResponseError for DeployError {
fn status_code(&self) -> StatusCode {
match self {
DeployError::Invalid(_) => StatusCode::BAD_REQUEST,
DeployError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl ResponseError for DeleteError {
fn status_code(&self) -> StatusCode {
match self {
DeleteError::Invalid(_) => StatusCode::BAD_REQUEST,
DeleteError::NotFound(_) => StatusCode::NOT_FOUND,
DeleteError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl ResponseError for ResolveError {
fn status_code(&self) -> StatusCode {
match self {
ResolveError::NotFound(_) => StatusCode::NOT_FOUND,
ResolveError::Invalid(_) => StatusCode::BAD_REQUEST,
ResolveError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl ResponseError for ListError {
fn status_code(&self) -> StatusCode {
match self {
ListError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
ListError::NotFound(_) => StatusCode::NOT_FOUND,
}
}
}
impl ResponseError for UpdateError {
fn status_code(&self) -> StatusCode {
match self {
UpdateError::Invalid(_) => StatusCode::BAD_REQUEST,
UpdateError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
UpdateError::NotFound(_) => StatusCode::NOT_FOUND,
}
}
}

View File

@ -0,0 +1,34 @@
pub mod function;
pub mod proxy;
#[derive(Debug, thiserror::Error)]
pub struct FaasError {
message: String,
error_type: FaasErrorType,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
#[derive(Debug)]
pub enum FaasErrorType {
ContainerFailure,
Timeout,
InternalError,
}
impl std::fmt::Display for FaasError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{:?}] {}", self.error_type, self.message)
}
}
// 实现从常见错误类型转换
impl From<std::io::Error> for FaasError {
fn from(err: std::io::Error) -> Self {
FaasError {
message: format!("IO error: {}", err),
error_type: FaasErrorType::InternalError,
source: Some(Box::new(err)),
}
}
}

View File

@ -0,0 +1,67 @@
use std::str::FromStr;
use actix_http::Method;
use actix_web::{HttpRequest, HttpResponse, error::ErrorMethodNotAllowed, web};
use crate::{provider::Provider, proxy::proxy_handler::proxy_request, types::function::Query};
pub const PROXY_DISPATCH_PATH: &str = "/{any:.+}";
pub struct ProxyQuery {
pub query: Query,
pub path: String,
}
impl FromStr for ProxyQuery {
type Err = ();
fn from_str(path: &str) -> Result<Self, Self::Err> {
let (identifier, rest_path) = if let Some((identifier, rest_path)) = path.split_once('/') {
match rest_path {
"" => (identifier, "/".to_owned()),
_ => (identifier, "/".to_owned() + rest_path),
}
} else {
(path, "".to_owned())
};
let (service, namespace) = identifier
.rsplit_once('.')
.map(|(s, n)| (s.to_string(), Some(n.to_string())))
.unwrap_or((identifier.to_string(), None));
Ok(ProxyQuery {
query: Query { service, namespace },
path: rest_path,
})
}
}
// 主要参考源码的响应设置
pub async fn proxy<P: Provider>(
req: HttpRequest,
payload: web::Payload,
provider: web::Data<P>,
any: web::Path<String>,
) -> actix_web::Result<HttpResponse> {
let meta = ProxyQuery::from_str(&any).map_err(|_| {
log::error!("Failed to parse path: {}", any);
ErrorMethodNotAllowed("Invalid path")
})?;
let function = meta.query;
log::trace!("proxy query: {:?}", function);
match *req.method() {
Method::POST
| Method::PUT
| Method::DELETE
| Method::GET
| Method::PATCH
| Method::HEAD
| Method::OPTIONS => {
let upstream = provider
.resolve(function)
.await
.map_err(|e| ErrorMethodNotAllowed(format!("Invalid function name {e}")))?;
log::trace!("upstream: {:?}", upstream);
proxy_request(&req, payload, upstream, &meta.path).await
}
_ => Err(ErrorMethodNotAllowed("Method not allowed")),
}
}

View File

@ -0,0 +1,6 @@
pub mod bootstrap;
pub mod handlers;
// pub mod metrics;
pub mod provider;
pub mod proxy;
pub mod types;

View File

@ -35,5 +35,3 @@ impl HttpMetrics {
}
}
}
pub const TEXT_CONTENT_TYPE: &str = "text/plain; version=0.0.4";

View File

@ -0,0 +1,45 @@
use crate::{
handlers::function::{DeleteError, DeployError, ListError, ResolveError, UpdateError},
types::function::{Deployment, Query, Status},
};
pub trait Provider: Send + Sync + 'static {
/// Should return a valid upstream url
fn resolve(
&self,
function: Query,
) -> impl std::future::Future<Output = Result<actix_http::uri::Builder, ResolveError>> + Send;
// `/system/functions` endpoint
/// Get a list of deployed functions
fn list(
&self,
namespace: String,
) -> impl std::future::Future<Output = Result<Vec<Status>, ListError>> + Send;
/// Deploy a new function
fn deploy(
&self,
param: Deployment,
) -> impl std::future::Future<Output = Result<(), DeployError>> + Send;
/// Update a function spec
fn update(
&self,
param: Deployment,
) -> impl std::future::Future<Output = Result<(), UpdateError>> + Send;
/// Delete a function
fn delete(
&self,
function: Query,
) -> impl std::future::Future<Output = Result<(), DeleteError>> + Send;
// `/system/function/{functionName}` endpoint
/// Get the status of a function by name
fn status(
&self,
function: Query,
) -> impl std::future::Future<Output = Result<Status, ResolveError>> + Send;
}

View File

@ -1,22 +1,14 @@
use actix_web::{HttpRequest, web};
use actix_web::{HttpRequest, http::Uri, web};
use awc::http::Uri;
use url::Url;
//根据URL和原始请求来构建转发请求并对请求头进行处理
pub fn create_proxy_request(
req: &HttpRequest,
base_url: &Url,
uri: Uri,
payload: web::Payload,
) -> awc::SendClientRequest {
let proxy_client = awc::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.finish();
let origin_url = base_url.join(req.uri().path()).unwrap();
let remaining_segments = origin_url.path_segments().unwrap().skip(2);
let rest_path = remaining_segments.collect::<Vec<_>>().join("/");
let url = base_url.join(&rest_path).unwrap();
let uri = url.as_str().parse::<Uri>().unwrap();
let mut proxy_req = proxy_client.request(req.method().clone(), uri);

View File

@ -1,3 +1,4 @@
pub mod builder;
pub mod proxy_handler;
mod proxy_handler_test;
// #[cfg(test)]
// mod test;

View File

@ -0,0 +1,28 @@
// use crate::handlers::invoke_resolver::InvokeResolver;
use crate::proxy::builder::create_proxy_request;
use actix_web::{HttpRequest, HttpResponse, error::ErrorInternalServerError, web};
pub async fn proxy_request(
req: &HttpRequest,
payload: web::Payload,
upstream: actix_http::uri::Builder,
path: &str,
) -> actix_web::Result<HttpResponse> {
let uri = upstream.path_and_query(path).build().map_err(|e| {
log::error!("Failed to build URI: {}", e);
ErrorInternalServerError("Failed to build URI")
})?;
log::trace!("Proxying request to: {}", uri);
// Handle the error conversion explicitly
let proxy_resp = create_proxy_request(req, uri, payload).await.map_err(|e| {
log::error!("Failed to create proxy request: {}", e);
ErrorInternalServerError("Failed to create proxy request")
})?;
// Now create an HttpResponse from the proxy response
let mut client_resp = HttpResponse::build(proxy_resp.status());
// Stream the response body
Ok(client_resp.streaming(proxy_resp))
}

View File

@ -0,0 +1,108 @@
use crate::handlers::proxy::proxy;
use actix_web::{
App, HttpRequest, HttpResponse, Responder, http,
test::{self},
web::{self, Bytes},
};
#[actix_web::test]
#[ignore]
async fn test_proxy_handler_success() {
todo!()
}
#[actix_web::test]
async fn test_path_parsing() {
let test_cases = vec![
("simple_name_match", "/function/echo", "echo", "", 200),
(
"simple_name_match",
"/function/echo.faasd-in-rs-fn",
"echo.faasd-in-rs-fn",
"",
200,
),
(
"simple_name_match_with_trailing_slash",
"/function/echo/",
"echo",
"",
200,
),
(
"name_match_with_additional_path_values",
"/function/echo/subPath/extras",
"echo",
"subPath/extras",
200,
),
(
"name_match_with_additional_path_values_and_querystring",
"/function/echo/subPath/extras?query=true",
"echo",
"subPath/extras",
200,
),
("not_found_if_no_name", "/function/", "", "", 404),
];
let app = test::init_service(
App::new()
.route("/function/{name}", web::get().to(var_handler))
.route("/function/{name}/", web::get().to(var_handler))
.route("/function/{name}/{params:.*}", web::get().to(var_handler)),
)
.await;
for (name, path, function_name, extra_path, status_code) in test_cases {
let req = test::TestRequest::get().uri(path).to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status().as_u16(), status_code, "Test case: {}", name);
if status_code == 200 {
let body = test::read_body(resp).await;
let expected_body = format!("name: {} params: {}", function_name, extra_path);
assert_eq!(body, expected_body.as_bytes(), "Test case: {}", name);
}
}
}
#[actix_web::test]
async fn test_invalid_method() {
let app = test::init_service(
App::new().route("/function/{name}{path:/?.*}", web::to(proxy)),
)
.await;
let req = test::TestRequest::with_uri("/function/test-service/path")
.method(http::Method::from_bytes(b"INVALID").unwrap())
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), http::StatusCode::METHOD_NOT_ALLOWED);
}
#[actix_web::test]
async fn test_empty_func_name() {
let app = test::init_service(
App::new().route("/function{name:/?}{path:/?.*}", web::to(proxy)),
)
.await;
let req = test::TestRequest::post()
.uri("/function")
.insert_header((http::header::CONTENT_TYPE, "application/json"))
.set_payload(Bytes::from_static(b"{\"key\":\"value\"}"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
}
async fn var_handler(req: HttpRequest) -> impl Responder {
let vars = req.match_info();
HttpResponse::Ok().body(format!(
"name: {} params: {}",
vars.get("name").unwrap_or(""),
vars.get("params").unwrap_or("")
))
}

View File

@ -0,0 +1,166 @@
// https://github.com/openfaas/faas/blob/7803ea1861f2a22adcbcfa8c79ed539bc6506d5b/api-docs/spec.openapi.yml
use std::{collections::HashMap, str::FromStr};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Deployment {
/// Service is the name of the function deployment
pub service: String,
/// Image is a fully-qualified container image
pub image: String,
/// Namespace for the function, if supported by the faas-provider
pub namespace: Option<String>,
/// EnvProcess overrides the fprocess environment variable and can be used
/// with the watchdog
pub env_process: Option<String>,
/// EnvVars can be provided to set environment variables for the function runtime.
pub env_vars: Option<HashMap<String, String>>,
/// Constraints are specific to the faas-provider.
pub constraints: Option<Vec<String>>,
/// Secrets list of secrets to be made available to function
pub secrets: Option<Vec<String>>,
/// Labels are metadata for functions which may be used by the
/// faas-provider or the gateway
pub labels: Option<HashMap<String, String>>,
/// Annotations are metadata for functions which may be used by the
/// faas-provider or the gateway
pub annotations: Option<HashMap<String, String>>,
/// Limits for function
pub limits: Option<Resources>,
/// Requests of resources requested by function
pub requests: Option<Resources>,
/// ReadOnlyRootFilesystem removes write-access from the root filesystem
/// mount-point.
#[serde(default = "default_read_only_root_filesystem")]
pub read_only_root_filesystem: bool,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Resources {
/// The amount of memory that is allocated for the function
pub memory: Option<String>,
/// The amount of CPU that is allocated for the function
pub cpu: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Usage {
/// CPU usage increase since the last measurement, equivalent to Kubernetes' concept of millicores
pub cpu: Option<f64>,
/// Total memory usage in bytes
pub total_memory_bytes: Option<f64>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Status {
/// The name of the function
pub name: String,
/// The fully qualified docker image name of the function
pub image: String,
/// The namespace of the function
pub namespace: Option<String>,
/// Process for watchdog to fork
pub env_process: Option<String>,
/// Environment variables for the function runtime
pub env_vars: Option<HashMap<String, String>>,
/// Constraints are specific to OpenFaaS Provider
pub constraints: Option<Vec<String>>,
/// An array of names of secrets that are made available to the function
pub secrets: Option<Vec<String>>,
/// A map of labels for making scheduling or routing decisions
pub labels: Option<HashMap<String, String>>,
/// A map of annotations for management, orchestration, events, and build tasks
pub annotations: Option<HashMap<String, String>>,
/// Limits for function resources
pub limits: Option<Resources>,
/// Requests for function resources
pub requests: Option<Resources>,
/// Removes write-access from the root filesystem mount-point
#[serde(default = "default_read_only_root_filesystem")]
pub read_only_root_filesystem: bool,
/// The amount of invocations for the specified function
pub invocation_count: Option<i32>,
/// Desired amount of replicas
pub replicas: Option<i32>,
/// The current available amount of replicas
pub available_replicas: Option<i32>,
/// The time read back from the faas backend's data store for when the function or its container was created
pub created_at: Option<String>,
/// Usage statistics for the function
pub usage: Option<Usage>,
}
#[derive(Eq, Hash, PartialEq, Clone, Debug)]
pub struct Query {
/// Name of deployed function
pub service: String,
/// Namespace of deployed function
pub namespace: Option<String>,
}
/// TODO: 其实应该是 try from, 排除非法的函数名
impl FromStr for Query {
type Err = ();
fn from_str(function_name: &str) -> Result<Self, Self::Err> {
Ok(if let Some(index) = function_name.rfind('.') {
Self {
service: function_name[..index].to_string(),
namespace: Some(function_name[index + 1..].to_string()),
}
} else {
Self {
service: function_name.to_string(),
namespace: Some("default".to_string()),
}
})
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Delete {
/// Name of deployed function
pub function_name: String,
pub namespace: String,
}
const fn default_read_only_root_filesystem() -> bool {
false
}

View File

@ -0,0 +1,2 @@
pub mod config;
pub mod function;

View File

@ -16,22 +16,24 @@ publish = false
### BEGIN HAKARI SECTION
[dependencies]
actix-router = { version = "0.5", default-features = false, features = ["http", "unicode"] }
byteorder = { version = "1" }
bytes = { version = "1" }
chrono = { version = "0.4", features = ["serde"] }
futures-channel = { version = "0.3", features = ["sink"] }
futures-task = { version = "0.3", default-features = false, features = ["std"] }
futures-util = { version = "0.3", features = ["channel", "io", "sink"] }
log = { version = "0.4", default-features = false, features = ["std"] }
memchr = { version = "2", features = ["use_std"] }
memchr = { version = "2" }
mio = { version = "1", features = ["net", "os-ext"] }
prost = { version = "0.13", features = ["prost-derive"] }
regex = { version = "1" }
regex-automata = { version = "0.4", default-features = false, features = ["dfa-onepass", "hybrid", "meta", "nfa-backtrack", "perf-inline", "perf-literal", "unicode"] }
regex-syntax = { version = "0.8" }
scopeguard = { version = "1" }
serde = { version = "1", features = ["derive"] }
smallvec = { version = "1", default-features = false, features = ["const_new"] }
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["codec", "io"] }
tower = { version = "0.4", features = ["balance", "buffer", "limit", "util"] }
tokio-util = { version = "0.7", features = ["full"] }
tracing = { version = "0.1", features = ["log"] }
tracing-core = { version = "0.1", default-features = false, features = ["std"] }
@ -39,7 +41,7 @@ tracing-core = { version = "0.1", default-features = false, features = ["std"] }
actix-router = { version = "0.5", default-features = false, features = ["http", "unicode"] }
bytes = { version = "1" }
log = { version = "0.4", default-features = false, features = ["std"] }
memchr = { version = "2", features = ["use_std"] }
memchr = { version = "2" }
prost = { version = "0.13", features = ["prost-derive"] }
regex = { version = "1" }
regex-automata = { version = "0.4", default-features = false, features = ["dfa-onepass", "hybrid", "meta", "nfa-backtrack", "perf-inline", "perf-literal", "unicode"] }
@ -50,18 +52,22 @@ tracing = { version = "0.1", features = ["log"] }
tracing-core = { version = "0.1", default-features = false, features = ["std"] }
[target.x86_64-unknown-linux-gnu.dependencies]
bitflags = { version = "2", default-features = false, features = ["std"] }
getrandom = { version = "0.2", default-features = false, features = ["std"] }
libc = { version = "0.2", features = ["extra_traits"] }
[target.x86_64-unknown-linux-gnu.build-dependencies]
bitflags = { version = "2", default-features = false, features = ["std"] }
getrandom = { version = "0.2", default-features = false, features = ["std"] }
libc = { version = "0.2", features = ["extra_traits"] }
[target.aarch64-unknown-linux-gnu.dependencies]
bitflags = { version = "2", default-features = false, features = ["std"] }
getrandom = { version = "0.2", default-features = false, features = ["std"] }
libc = { version = "0.2", features = ["extra_traits"] }
[target.aarch64-unknown-linux-gnu.build-dependencies]
bitflags = { version = "2", default-features = false, features = ["std"] }
getrandom = { version = "0.2", default-features = false, features = ["std"] }
libc = { version = "0.2", features = ["extra_traits"] }

View File

@ -1,103 +0,0 @@
use actix_web::{App, HttpServer, middleware, web};
use prometheus::Registry;
use std::collections::HashMap;
use crate::{
handlers,
metrics::{self, HttpMetrics},
//httputil,
//proxy,
types::config::FaaSConfig,
};
//用于函数/服务名称的表达式
#[allow(dead_code)]
const NAME_EXPRESSION: &str = r"-a-zA-Z_0-9\.";
//应用程序状态,存储共享的数据,如配置、指标、认证信息等,为业务函数提供支持
#[derive(Clone)]
#[allow(dead_code)]
struct AppState {
config: FaaSConfig, //应用程序的配置用于识别是否开启Basic Auth等
metrics: HttpMetrics, //用于监视http请求的持续时间和总数
credentials: Option<HashMap<String, String>>, //当有认证信息的时候,获取认证信息
}
//serve 把处理程序headlers load到正确路由规范。这个函数是阻塞的。
#[allow(dead_code)]
async fn serve() -> std::io::Result<()> {
let config = FaaSConfig::new(); //加载配置用于识别是否开启Basic Auth等
let _registry = Registry::new();
let metrics = metrics::HttpMetrics::new(); //metrics监视http请求的持续时间和总数
// 用于存储应用程序状态的结构体
let app_state = AppState {
config: config.clone(),
metrics: metrics.clone(),
credentials: None,
};
// 如果启用了Basic Auth从指定路径读取认证凭证并存储在应用程序状态中
if config.enable_basic_auth {
todo!("implement authentication");
}
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(app_state.clone())) // 将app_state存储在web::Data中以便在处理程序中访问
.wrap(middleware::Logger::default()) // 记录请求日志
.service(
web::scope("/system")
.service(
web::resource("/functions")
.route(web::get().to(handlers::function_lister))
.route(web::post().to(handlers::deploy_function))
.route(web::delete().to(handlers::delete_function))
.route(web::put().to(handlers::update_function)),
)
.service(
web::resource("/function/{name}")
.route(web::get().to(handlers::function_status)),
)
.service(
web::resource("/scale-function/{name}")
.route(web::post().to(handlers::scale_function)),
)
.service(web::resource("/info").route(web::get().to(handlers::info)))
.service(
web::resource("/secrets")
.route(web::get().to(handlers::secrets))
.route(web::post().to(handlers::secrets))
.route(web::put().to(handlers::secrets))
.route(web::delete().to(handlers::secrets)),
)
.service(web::resource("/logs").route(web::get().to(handlers::logs)))
.service(
web::resource("/namespaces")
.route(web::get().to(handlers::list_namespaces))
.route(web::post().to(handlers::mutate_namespace)),
),
)
.service(
web::scope("/function")
.service(
web::resource("/{name}")
.route(web::get().to(handlers::function_proxy))
.route(web::post().to(handlers::function_proxy)),
)
.service(
web::resource("/{name}/{params:.*}")
.route(web::get().to(handlers::function_proxy))
.route(web::post().to(handlers::function_proxy)),
),
)
.route("/metrics", web::get().to(handlers::telemetry))
.route("/healthz", web::get().to(handlers::health))
})
.bind(("0.0.0.0", config.tcp_port.unwrap_or(8080)))?
.run()
.await
}
//当上下文完成的时候关闭服务器
//无法关闭时候写进log,并且返回错误

View File

@ -1 +0,0 @@

View File

@ -1,14 +0,0 @@
#[allow(unused)]
pub const DEFAULT_FUNCTION_NAMESPACE: &str = "default";
#[allow(unused)]
pub const NAMESPACE_LABEL: &str = "faasrs";
#[allow(unused)]
pub const FAASRS_NAMESPACE: &str = "faasrs";
#[allow(unused)]
pub const FAASRS_SERVICE_PULL_ALWAYS: bool = false;
#[allow(unused)]
pub const DEFAULT_SNAPSHOTTER: &str = "overlayfs";

View File

@ -1,68 +0,0 @@
use crate::{
consts,
handlers::{function_get::get_function, utils::CustomError},
};
use actix_web::{HttpResponse, Responder, web};
use serde::{Deserialize, Serialize};
use service::containerd_manager::ContainerdManager;
use super::function_list::Function;
// 参考响应状态https://github.com/openfaas/faas/blob/7803ea1861f2a22adcbcfa8c79ed539bc6506d5b/api-docs/spec.openapi.yml#L141C2-L162C45
// 请求体反序列化失败自动返回400错误
pub async fn delete_handler(info: web::Json<DeleteContainerInfo>) -> impl Responder {
let function_name = info.function_name.clone();
let namespace = info
.namespace
.clone()
.unwrap_or_else(|| consts::DEFAULT_FUNCTION_NAMESPACE.to_string());
let namespaces = ContainerdManager::list_namespaces().await.unwrap();
if !namespaces.contains(&namespace.to_string()) {
return HttpResponse::NotFound().body(format!("Namespace '{}' does not exist", namespace));
}
let function = match get_function(&function_name, &namespace).await {
Ok(function) => function,
Err(e) => {
log::error!("Failed to get function: {}", e);
return HttpResponse::NotFound().body(format!(
"Function '{}' not found in namespace '{}'",
function_name, namespace
));
}
};
match delete(&function, &namespace).await {
Ok(()) => {
HttpResponse::Ok().body(format!("Function {} deleted successfully.", function_name))
}
Err(e) => {
HttpResponse::InternalServerError().body(format!("Failed to delete function: {}", e))
}
}
}
async fn delete(function: &Function, namespace: &str) -> Result<(), CustomError> {
let function_name = function.name.clone();
if function.replicas != 0 {
log::info!("function.replicas: {:?}", function.replicas);
cni::delete_cni_network(namespace, &function_name);
log::info!("delete_cni_network ok");
} else {
log::info!("function.replicas: {:?}", function.replicas);
}
ContainerdManager::delete_container(&function_name, namespace)
.await
.map_err(|e| {
log::error!("Failed to delete container: {}", e);
CustomError::OtherError(format!("Failed to delete container: {}", e))
})?;
Ok(())
}
#[derive(Serialize, Deserialize)]
pub struct DeleteContainerInfo {
pub function_name: String,
pub namespace: Option<String>,
}

View File

@ -1,77 +0,0 @@
use crate::{consts, handlers::utils::CustomError, types::function_deployment::DeployFunctionInfo};
use actix_web::{HttpResponse, Responder, web};
use service::{containerd_manager::ContainerdManager, image_manager::ImageManager};
// 参考响应状态 https://github.com/openfaas/faas/blob/7803ea1861f2a22adcbcfa8c79ed539bc6506d5b/api-docs/spec.openapi.yml#L121C1-L140C45
// 请求体反序列化失败自动返回400错误
pub async fn deploy_handler(info: web::Json<DeployFunctionInfo>) -> impl Responder {
let image = info.image.clone();
let function_name = info.function_name.clone();
let namespace = info
.namespace
.clone()
.unwrap_or(consts::DEFAULT_FUNCTION_NAMESPACE.to_string());
log::info!("Namespace '{}' validated.", &namespace);
let container_list = match ContainerdManager::list_container_into_string(&namespace).await {
Ok(container_list) => container_list,
Err(e) => {
log::error!("Failed to list container: {}", e);
return HttpResponse::InternalServerError()
.body(format!("Failed to list container: {}", e));
}
};
if container_list.contains(&function_name) {
return HttpResponse::BadRequest().body(format!(
"Function '{}' already exists in namespace '{}'",
function_name, namespace
));
}
match deploy(&function_name, &image, &namespace).await {
Ok(()) => HttpResponse::Accepted().body(format!(
"Function {} deployment initiated successfully.",
function_name
)),
Err(e) => HttpResponse::BadRequest().body(format!(
"failed to deploy function {}, because {}",
function_name, e
)),
}
}
async fn deploy(function_name: &str, image: &str, namespace: &str) -> Result<(), CustomError> {
ImageManager::prepare_image(image, namespace, true)
.await
.map_err(CustomError::from)?;
log::info!("Image '{}' validated ,", image);
ContainerdManager::create_container(image, function_name, namespace)
.await
.map_err(|e| CustomError::OtherError(format!("failed to create container:{}", e)))?;
log::info!(
"Container {} created using image {} in namespace {}",
function_name,
image,
namespace
);
ContainerdManager::new_task(function_name, namespace)
.await
.map_err(|e| {
CustomError::OtherError(format!(
"failed to start task for container {},{}",
function_name, e
))
})?;
log::info!(
"Task for container {} was created successfully",
function_name
);
Ok(())
}

View File

@ -1,146 +0,0 @@
use crate::handlers::function_list::Function;
// use service::spec::{ Mount, Spec};
use actix_web::cookie::time::Duration;
use service::{FunctionScope, containerd_manager::ContainerdManager, image_manager::ImageManager};
use std::{collections::HashMap, time::UNIX_EPOCH};
use thiserror::Error;
const ANNOTATION_LABEL_PREFIX: &str = "com.openfaas.annotations.";
#[derive(Error, Debug)]
pub enum FunctionError {
#[error("Function not found: {0}")]
FunctionNotFound(String),
#[error("Runtime Config not found: {0}")]
RuntimeConfigNotFound(String),
}
impl From<Box<dyn std::error::Error>> for FunctionError {
fn from(error: Box<dyn std::error::Error>) -> Self {
FunctionError::FunctionNotFound(error.to_string())
}
}
pub async fn get_function(function_name: &str, namespace: &str) -> Result<Function, FunctionError> {
let cid = function_name;
let function = FunctionScope {
function_name: cid.to_string(),
namespace: namespace.to_string(),
};
let address = ContainerdManager::get_address(&function);
let container = ContainerdManager::load_container(cid, namespace)
.await
.map_err(|e| FunctionError::FunctionNotFound(e.to_string()))?
.unwrap_or_default();
let container_name = container.id.to_string();
let image = container.image.clone();
let mut pid = 0;
let mut replicas = 0;
let all_labels = container.labels;
let (labels, _) = build_labels_and_annotations(all_labels);
let env = ImageManager::get_runtime_config(&image)
.map_err(|e| FunctionError::RuntimeConfigNotFound(e.to_string()))?
.env;
let (env_vars, env_process) = read_env_from_process_env(env);
// let secrets = read_secrets_from_mounts(&spec.mounts);
// let memory_limit = read_memory_limit_from_spec(&spec);
let timestamp = container.created_at.unwrap_or_default();
let created_at = UNIX_EPOCH + Duration::new(timestamp.seconds, timestamp.nanos);
let task = ContainerdManager::get_task(cid, namespace)
.await
.map_err(|e| FunctionError::FunctionNotFound(e.to_string()));
match task {
Ok(task) => {
let status = task.status;
if status == 2 || status == 3 {
pid = task.pid;
replicas = 1;
}
}
Err(e) => {
log::error!("Failed to get task: {}", e);
replicas = 0;
}
}
Ok(Function {
name: container_name,
namespace: namespace.to_string(),
image,
pid,
replicas,
address,
labels,
env_vars,
env_process,
created_at,
})
}
fn build_labels_and_annotations(
ctr_labels: HashMap<String, String>,
) -> (HashMap<String, String>, HashMap<String, String>) {
let mut labels = HashMap::new();
let mut annotations = HashMap::new();
for (k, v) in ctr_labels {
if k.starts_with(ANNOTATION_LABEL_PREFIX) {
annotations.insert(k.trim_start_matches(ANNOTATION_LABEL_PREFIX).to_string(), v);
} else {
labels.insert(k, v);
}
}
(labels, annotations)
}
fn read_env_from_process_env(env: Vec<String>) -> (HashMap<String, String>, String) {
let mut found_env = HashMap::new();
let mut fprocess = String::new();
for e in env {
let kv: Vec<&str> = e.splitn(2, '=').collect();
if kv.len() == 1 {
continue;
}
if kv[0] == "PATH" {
continue;
}
if kv[0] == "fprocess" {
fprocess = kv[1].to_string();
continue;
}
found_env.insert(kv[0].to_string(), kv[1].to_string());
}
(found_env, fprocess)
}
// fn read_secrets_from_mounts(mounts: &[Mount]) -> Vec<String> {
// let mut secrets = Vec::new();
// for mnt in mounts {
// let parts: Vec<&str> = mnt.destination.split("/var/openfaas/secrets/").collect();
// if parts.len() > 1 {
// secrets.push(parts[1].to_string());
// }
// }
// secrets
// }
// fn read_memory_limit_from_spec(spec: &Spec) -> i64 {
// match &spec.linux {
// linux => match &linux.resources {
// resources => match &resources.memory {
// Some(memory) => memory.limit.unwrap_or(0),
// None => 0,
// },
// _ => 0,
// },
// _ => 0,
// }
// }

View File

@ -1,74 +0,0 @@
use std::{collections::HashMap, time::SystemTime};
use actix_web::{HttpRequest, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use service::containerd_manager::ContainerdManager;
use super::{function_get::get_function, utils::CustomError};
#[derive(Debug, Serialize, Deserialize)]
pub struct Function {
pub name: String,
pub namespace: String,
pub image: String,
pub pid: u32,
pub replicas: i32,
pub address: String,
pub labels: HashMap<String, String>,
// pub annotations: HashMap<String, String>,
// pub secrets: Vec<String>,
pub env_vars: HashMap<String, String>,
pub env_process: String,
// pub memory_limit: i64,
pub created_at: SystemTime,
}
// openfaas API文档和faasd源码的响应不能完全对齐这里参考源码的响应码设置
// 考虑到部分操作可能返回500错误但是faasd并没有做internal server error的处理可能上层有中间件捕获这里应该需要做500的处理
pub async fn function_list_handler(req: HttpRequest) -> impl Responder {
let namespace = req.match_info().get("namespace").unwrap_or("");
if namespace.is_empty() {
return HttpResponse::BadRequest().body("provide namespace in path");
}
let namespaces = match ContainerdManager::list_namespaces().await {
Ok(namespace) => namespace,
Err(e) => {
return HttpResponse::InternalServerError()
.body(format!("Failed to list namespaces:{}", e));
}
};
if !namespaces.contains(&namespace.to_string()) {
return HttpResponse::BadRequest()
.body(format!("Namespace '{}' does not exist", namespace));
}
let container_list = match ContainerdManager::list_container_into_string(namespace).await {
Ok(container_list) => container_list,
Err(e) => {
return HttpResponse::InternalServerError()
.body(format!("Failed to list container:{}", e));
}
};
log::info!("container_list: {:?}", container_list);
match get_function_list(container_list, namespace).await {
Ok(functions) => HttpResponse::Ok().body(serde_json::to_string(&functions).unwrap()),
Err(e) => HttpResponse::BadRequest().body(format!("Failed to get function list: {}", e)),
}
}
async fn get_function_list(
container_list: Vec<String>,
namespace: &str,
) -> Result<Vec<Function>, CustomError> {
let mut functions: Vec<Function> = Vec::new();
for cid in container_list {
log::info!("cid: {}", cid);
let function = match get_function(&cid, namespace).await {
Ok(function) => function,
Err(e) => return Err(CustomError::FunctionError(e)),
};
functions.push(function);
}
Ok(functions)
}

View File

@ -1,54 +0,0 @@
use crate::consts::DEFAULT_FUNCTION_NAMESPACE;
use crate::handlers::function_get::get_function;
use actix_web::{Error, error::ErrorInternalServerError, error::ErrorServiceUnavailable};
use log;
use url::Url;
#[derive(Clone)]
pub struct InvokeResolver;
impl InvokeResolver {
pub async fn resolve_function_url(function_name: &str) -> Result<Url, Error> {
//根据函数名和containerd获取函数ip
//从函数名称中提取命名空间。如果函数名称中包含 .,则将其后的部分作为命名空间;否则使用默认命名空间
let mut actual_function_name = function_name;
let namespace =
extract_namespace_from_function_or_default(function_name, DEFAULT_FUNCTION_NAMESPACE);
if function_name.contains('.') {
actual_function_name = function_name.trim_end_matches(&format!(".{}", namespace));
}
let function = match get_function(actual_function_name, &namespace).await {
Ok(function) => function,
Err(e) => {
log::error!("Failed to get function:{}", e);
return Err(ErrorServiceUnavailable("Failed to get function"));
}
};
log::info!("Function:{:?}", function);
let address = function.address.clone();
let urlstr = format!("http://{}", address);
match Url::parse(&urlstr) {
Ok(url) => Ok(url),
Err(e) => {
log::error!("Failed to resolve url:{}", e);
Err(ErrorInternalServerError("Failed to resolve URL"))
}
}
}
}
fn extract_namespace_from_function_or_default(
function_name: &str,
default_namespace: &str,
) -> String {
let mut namespace = default_namespace.to_string();
if function_name.contains('.') {
if let Some(index) = function_name.rfind('.') {
namespace = function_name[index + 1..].to_string();
}
}
namespace
}

View File

@ -1,111 +0,0 @@
pub mod delete;
pub mod deploy;
pub mod function_get;
pub mod function_list;
pub mod invoke_resolver;
pub mod utils;
use actix_web::{HttpRequest, HttpResponse, Responder};
pub async fn function_lister(_req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body("函数列表")
}
pub async fn deploy_function(_req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body("部署函数")
}
pub async fn delete_function(_req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body("删除函数")
}
pub async fn update_function(_req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body("更新函数")
}
pub async fn function_status(_req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body("函数状态")
}
pub async fn scale_function(_req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body("扩展函数")
}
pub async fn info(_req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body("信息")
}
pub async fn secrets(_req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body("秘密")
}
pub async fn logs(_req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body("日志")
}
pub async fn list_namespaces(_req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body("命名空间列表")
}
pub async fn mutate_namespace(_req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body("变更命名空间")
}
pub async fn function_proxy(_req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body("函数代理")
}
pub async fn telemetry(_req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body("遥测")
}
pub async fn health(_req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body("健康检查")
}
// lazy_static! {
// pub static ref HANDLERS: HashMap<String, Box<dyn IAmHandler>> = {
// let mut map = HashMap::new();
// map.insert(
// "function_list".to_string(),
// Box::new(function_list::FunctionLister),
// );
// map.insert(
// "namespace_list".to_string(),
// Box::new(namespace_list::NamespaceLister),
// );
// map
// };
// }
#[derive(Debug, thiserror::Error)]
pub struct FaasError {
message: String,
error_type: FaasErrorType,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
#[derive(Debug)]
pub enum FaasErrorType {
ContainerFailure,
Timeout,
InternalError,
}
impl std::fmt::Display for FaasError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{:?}] {}", self.error_type, self.message)
}
}
// 实现从常见错误类型转换
impl From<std::io::Error> for FaasError {
fn from(err: std::io::Error) -> Self {
FaasError {
message: format!("IO error: {}", err),
error_type: FaasErrorType::InternalError,
source: Some(Box::new(err)),
}
}
}

View File

@ -1 +0,0 @@

View File

@ -1,9 +0,0 @@
pub mod bootstrap;
pub mod config;
pub mod consts;
pub mod handlers;
pub mod httputils;
pub mod logs;
pub mod metrics;
pub mod proxy;
pub mod types;

View File

@ -1 +0,0 @@

View File

@ -1,59 +0,0 @@
use crate::handlers::invoke_resolver::InvokeResolver;
use crate::proxy::builder::create_proxy_request;
use actix_web::{
HttpRequest, HttpResponse,
error::{ErrorBadRequest, ErrorInternalServerError, ErrorMethodNotAllowed},
http::Method,
web,
};
// 主要参考源码的响应设置
pub async fn proxy_handler(
req: HttpRequest,
payload: web::Payload,
) -> actix_web::Result<HttpResponse> {
match *req.method() {
Method::POST
| Method::PUT
| Method::DELETE
| Method::GET
| Method::PATCH
| Method::HEAD
| Method::OPTIONS => proxy_request(&req, payload).await,
_ => Err(ErrorMethodNotAllowed("Method not allowed")),
}
}
//根据原始请求解析url构建转发请求并转发获取响应
async fn proxy_request(
req: &HttpRequest,
payload: web::Payload,
) -> actix_web::Result<HttpResponse> {
let function_name = req.match_info().get("name").unwrap_or("");
if function_name.is_empty() {
return Err(ErrorBadRequest("Function name is required"));
}
let function_addr = InvokeResolver::resolve_function_url(function_name).await?;
let proxy_req = create_proxy_request(req, &function_addr, payload);
// Handle the error conversion explicitly
let proxy_resp = match proxy_req.await {
Ok(resp) => resp,
Err(e) => {
log::error!("Proxy request failed: {}", e);
return Err(ErrorInternalServerError(format!(
"Proxy request failed: {}",
e
)));
}
};
// Now create an HttpResponse from the proxy response
let mut client_resp = HttpResponse::build(proxy_resp.status());
// Stream the response body
Ok(client_resp.streaming(proxy_resp))
}

View File

@ -1,127 +0,0 @@
#[cfg(test)]
mod test {
use crate::proxy::proxy_handler::proxy_handler;
use actix_web::{
App, HttpRequest, HttpResponse, Responder, http,
test::{self},
web::{self, Bytes},
};
#[actix_web::test]
#[ignore]
async fn test_proxy_handler_success() {
todo!()
}
#[actix_web::test]
async fn test_path_parsing() {
let test_cases = vec![
("simple_name_match", "/function/echo", "echo", "", 200),
(
"simple_name_match",
"/function/echo.faasd-in-rs-fn",
"echo.faasd-in-rs-fn",
"",
200,
),
(
"simple_name_match_with_trailing_slash",
"/function/echo/",
"echo",
"",
200,
),
(
"name_match_with_additional_path_values",
"/function/echo/subPath/extras",
"echo",
"subPath/extras",
200,
),
(
"name_match_with_additional_path_values_and_querystring",
"/function/echo/subPath/extras?query=true",
"echo",
"subPath/extras",
200,
),
("not_found_if_no_name", "/function/", "", "", 404),
];
let app = test::init_service(
App::new()
.route("/function/{name}", web::get().to(var_handler))
.route("/function/{name}/", web::get().to(var_handler))
.route("/function/{name}/{params:.*}", web::get().to(var_handler)),
)
.await;
for (name, path, function_name, extra_path, status_code) in test_cases {
let req = test::TestRequest::get().uri(path).to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status().as_u16(), status_code, "Test case: {}", name);
if status_code == 200 {
let body = test::read_body(resp).await;
let expected_body = format!("name: {} params: {}", function_name, extra_path);
assert_eq!(body, expected_body.as_bytes(), "Test case: {}", name);
}
}
}
#[actix_web::test]
async fn test_handler_func_invalid_method() {
let app = test::init_service(
App::new().route("/function/{name}{path:/?.*}", web::to(proxy_handler)),
)
.await;
let req = test::TestRequest::with_uri("/function/test-service/path")
.method(http::Method::from_bytes(b"INVALID").unwrap())
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), http::StatusCode::METHOD_NOT_ALLOWED);
}
#[actix_web::test]
async fn test_handler_func_empty_function_nam() {
let app = test::init_service(
App::new().route("/function{name:/?}{path:/?.*}", web::to(proxy_handler)),
)
.await;
let req = test::TestRequest::post()
.uri("/function")
.insert_header((http::header::CONTENT_TYPE, "application/json"))
.set_payload(Bytes::from_static(b"{\"key\":\"value\"}"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
}
#[actix_web::test]
async fn test_handler_func_empty_function_name() {
let app = test::init_service(
App::new().route("/function{name:/?}{path:/?.*}", web::to(proxy_handler)),
)
.await;
let req = test::TestRequest::post()
.uri("/function")
.insert_header((http::header::CONTENT_TYPE, "application/json"))
.set_payload(Bytes::from_static(b"{\"key\":\"value\"}"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
}
async fn var_handler(req: HttpRequest) -> impl Responder {
let vars = req.match_info();
HttpResponse::Ok().body(format!(
"name: {} params: {}",
vars.get("name").unwrap_or(""),
vars.get("params").unwrap_or("")
))
}
}

View File

@ -1,71 +0,0 @@
use serde::{Deserialize, Serialize};
//use std::collections::HashMap;
#[derive(Serialize, Deserialize, Debug)]
pub struct FunctionDeployment {
/// Service is the name of the function deployment
pub service: String,
/// Image is a fully-qualified container image
pub image: String,
/// Namespace for the function, if supported by the faas-provider
#[serde(skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
// /// EnvProcess overrides the fprocess environment variable and can be used
// /// with the watchdog
// #[serde(rename = "envProcess", skip_serializing_if = "Option::is_none")]
// pub env_process: Option<String>,
// /// EnvVars can be provided to set environment variables for the function runtime.
// #[serde(skip_serializing_if = "Option::is_none")]
// pub env_vars: Option<HashMap<String, String>>,
// /// Constraints are specific to the faas-provider.
// #[serde(skip_serializing_if = "Option::is_none")]
// pub constraints: Option<Vec<String>>,
// /// Secrets list of secrets to be made available to function
// #[serde(skip_serializing_if = "Option::is_none")]
// pub secrets: Option<Vec<String>>,
// /// Labels are metadata for functions which may be used by the
// /// faas-provider or the gateway
// #[serde(skip_serializing_if = "Option::is_none")]
// pub labels: Option<HashMap<String, String>>,
// /// Annotations are metadata for functions which may be used by the
// /// faas-provider or the gateway
// #[serde(skip_serializing_if = "Option::is_none")]
// pub annotations: Option<HashMap<String, String>>,
// /// Limits for function
// #[serde(skip_serializing_if = "Option::is_none")]
// pub limits: Option<FunctionResources>,
// /// Requests of resources requested by function
// #[serde(skip_serializing_if = "Option::is_none")]
// pub requests: Option<FunctionResources>,
// /// ReadOnlyRootFilesystem removes write-access from the root filesystem
// /// mount-point.
// #[serde(rename = "readOnlyRootFilesystem", default)]
// pub read_only_root_filesystem: bool,
}
// #[derive(Debug, Serialize, Deserialize)]
// pub struct FunctionResources {
// #[serde(skip_serializing_if = "Option::is_none")]
// pub memory: Option<String>,
// #[serde(skip_serializing_if = "Option::is_none")]
// pub cpu: Option<String>,
// }
#[derive(serde::Deserialize)]
pub struct DeployFunctionInfo {
pub function_name: String,
pub image: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
}

View File

@ -1,2 +0,0 @@
pub mod config;
pub mod function_deployment;

View File

@ -1,21 +0,0 @@
[package]
name = "service"
version = "0.1.0"
edition = "2024"
[dependencies]
containerd-client = "0.8"
tokio = { version = "1", features = ["full"] }
tonic = "0.12"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
log = "0.4"
env_logger = "0.10"
prost-types = "0.13.4"
oci-spec = "0.6"
sha2 = "0.10"
hex = "0.4"
my-workspace-hack = { version = "0.1", path = "../my-workspace-hack" }
cni = { version = "0.1", path = "../cni" }
handlebars= "4.1.0"
lazy_static = "1.4"

View File

@ -1,599 +0,0 @@
use std::{fs, panic, sync::Arc};
use containerd_client::{
Client,
services::v1::{
Container, CreateContainerRequest, CreateTaskRequest, DeleteContainerRequest,
DeleteTaskRequest, KillRequest, ListContainersRequest, ListNamespacesRequest,
ListTasksRequest, ListTasksResponse, StartRequest, WaitRequest, WaitResponse,
container::Runtime,
snapshots::{MountsRequest, PrepareSnapshotRequest},
},
tonic::Request,
types::{Mount, v1::Process},
with_namespace,
};
use prost_types::Any;
use sha2::{Digest, Sha256};
use tokio::{
sync::OnceCell,
time::{Duration, timeout},
};
use crate::{
FunctionScope, GLOBAL_NETNS_MAP, NetworkConfig, image_manager::ImageManager,
spec::generate_spec,
};
pub(super) static CLIENT: OnceCell<Arc<Client>> = OnceCell::const_new();
#[derive(Debug)]
pub struct ContainerdManager;
impl ContainerdManager {
pub async fn init(socket_path: &str) {
if let Err(e) = CLIENT.set(Arc::new(Client::from_path(socket_path).await.unwrap())) {
panic!("Failed to set client: {}", e);
}
let _ = cni::init_net_work();
log::info!("ContainerdManager initialized");
}
async fn get_client() -> Arc<Client> {
CLIENT
.get()
.unwrap_or_else(|| panic!("Client not initialized, Please run init first"))
.clone()
}
/// 创建容器
pub async fn create_container(
image_name: &str,
cid: &str,
ns: &str,
) -> Result<(), ContainerdError> {
Self::prepare_snapshot(image_name, cid, ns)
.await
.map_err(|e| {
log::error!("Failed to create container: {}", e);
ContainerdError::CreateContainerError(e.to_string())
})?;
let spec = Self::get_spec(cid, ns, image_name).unwrap();
let container = Container {
id: cid.to_string(),
image: image_name.to_string(),
runtime: Some(Runtime {
name: "io.containerd.runc.v2".to_string(),
options: None,
}),
spec,
snapshotter: "overlayfs".to_string(),
snapshot_key: cid.to_string(),
..Default::default()
};
Self::do_create_container(container, ns).await?;
Self::prepare_cni_network(cid, ns, image_name)?;
Ok(())
}
async fn do_create_container(container: Container, ns: &str) -> Result<(), ContainerdError> {
let mut cc = Self::get_client().await.containers();
let req = CreateContainerRequest {
container: Some(container),
};
let _resp = cc.create(with_namespace!(req, ns)).await.map_err(|e| {
log::error!("Failed to create container: {}", e);
ContainerdError::CreateContainerError(e.to_string())
})?;
Ok(())
}
/// 删除容器
pub async fn delete_container(cid: &str, ns: &str) -> Result<(), ContainerdError> {
let container_list = Self::list_container(ns).await?;
if !container_list.iter().any(|container| container.id == cid) {
log::info!("Container {} not found", cid);
return Ok(());
}
let resp = Self::list_task_by_cid(cid, ns).await?;
if let Some(task) = resp.tasks.iter().find(|task| task.id == cid) {
log::info!("Task found: {}, Status: {}", task.id, task.status);
// TASK_UNKNOWN (0) — 未知状态
// TASK_CREATED (1) — 任务已创建
// TASK_RUNNING (2) — 任务正在运行
// TASK_STOPPED (3) — 任务已停止
// TASK_EXITED (4) — 任务已退出
// TASK_PAUSED (5) — 任务已暂停
// TASK_FAILED (6) — 任务失败
Self::kill_task_with_timeout(cid, ns).await?;
}
Self::do_delete_container(cid, ns).await?;
Self::remove_cni_network(cid, ns).map_err(|e| {
log::error!("Failed to remove CNI network: {}", e);
ContainerdError::CreateTaskError(e.to_string())
})?;
Ok(())
}
async fn do_delete_container(cid: &str, ns: &str) -> Result<(), ContainerdError> {
let mut cc = Self::get_client().await.containers();
let delete_request = DeleteContainerRequest {
id: cid.to_string(),
};
let _ = cc
.delete(with_namespace!(delete_request, ns))
.await
.expect("Failed to delete container");
Ok(())
}
/// 创建并启动任务
pub async fn new_task(cid: &str, ns: &str) -> Result<(), ContainerdError> {
let mounts = Self::get_mounts(cid, ns).await?;
Self::do_create_task(cid, ns, mounts).await?;
Self::do_start_task(cid, ns).await?;
Ok(())
}
async fn do_start_task(cid: &str, ns: &str) -> Result<(), ContainerdError> {
let mut c = Self::get_client().await.tasks();
let req = StartRequest {
container_id: cid.to_string(),
..Default::default()
};
let _resp = c.start(with_namespace!(req, ns)).await.map_err(|e| {
log::error!("Failed to start task: {}", e);
ContainerdError::StartTaskError(e.to_string())
})?;
log::info!("Task: {:?} started", cid);
Ok(())
}
async fn do_create_task(
cid: &str,
ns: &str,
rootfs: Vec<Mount>,
) -> Result<(), ContainerdError> {
let mut tc = Self::get_client().await.tasks();
let create_request = CreateTaskRequest {
container_id: cid.to_string(),
rootfs,
..Default::default()
};
let _resp = tc
.create(with_namespace!(create_request, ns))
.await
.map_err(|e| {
log::error!("Failed to create task: {}", e);
ContainerdError::CreateTaskError(e.to_string())
})?;
Ok(())
}
pub async fn get_task(cid: &str, ns: &str) -> Result<Process, ContainerdError> {
let mut tc = Self::get_client().await.tasks();
let request = ListTasksRequest {
filter: format!("container=={}", cid),
};
let response = tc.list(with_namespace!(request, ns)).await.map_err(|e| {
log::error!("Failed to list tasks: {}", e);
ContainerdError::GetContainerListError(e.to_string())
})?;
let tasks = response.into_inner().tasks;
let task =
tasks
.into_iter()
.find(|task| task.id == cid)
.ok_or_else(|| -> ContainerdError {
log::error!("Task not found for container: {}", cid);
ContainerdError::CreateTaskError("Task not found".to_string())
})?;
Ok(task)
}
async fn get_mounts(cid: &str, ns: &str) -> Result<Vec<Mount>, ContainerdError> {
let mut sc = Self::get_client().await.snapshots();
let req = MountsRequest {
snapshotter: "overlayfs".to_string(),
key: cid.to_string(),
};
let mounts = sc
.mounts(with_namespace!(req, ns))
.await
.map_err(|e| {
log::error!("Failed to get mounts: {}", e);
ContainerdError::CreateTaskError(e.to_string())
})?
.into_inner()
.mounts;
Ok(mounts)
}
async fn list_task_by_cid(cid: &str, ns: &str) -> Result<ListTasksResponse, ContainerdError> {
let mut c = Self::get_client().await.tasks();
let request = ListTasksRequest {
filter: format!("container=={}", cid),
};
let response = c
.list(with_namespace!(request, ns))
.await
.map_err(|e| {
log::error!("Failed to list tasks: {}", e);
ContainerdError::GetContainerListError(e.to_string())
})?
.into_inner();
Ok(response)
}
async fn do_kill_task(cid: &str, ns: &str) -> Result<(), ContainerdError> {
let mut c = Self::get_client().await.tasks();
let kill_request = KillRequest {
container_id: cid.to_string(),
signal: 15,
all: true,
..Default::default()
};
c.kill(with_namespace!(kill_request, ns))
.await
.map_err(|e| {
log::error!("Failed to kill task: {}", e);
ContainerdError::KillTaskError(e.to_string())
})?;
Ok(())
}
async fn do_kill_task_force(cid: &str, ns: &str) -> Result<(), ContainerdError> {
let mut c = Self::get_client().await.tasks();
let kill_request = KillRequest {
container_id: cid.to_string(),
signal: 9,
all: true,
..Default::default()
};
c.kill(with_namespace!(kill_request, ns))
.await
.map_err(|e| {
log::error!("Failed to force kill task: {}", e);
ContainerdError::KillTaskError(e.to_string())
})?;
Ok(())
}
async fn do_delete_task(cid: &str, ns: &str) -> Result<(), ContainerdError> {
let mut c = Self::get_client().await.tasks();
let delete_request = DeleteTaskRequest {
container_id: cid.to_string(),
};
c.delete(with_namespace!(delete_request, ns))
.await
.map_err(|e| {
log::error!("Failed to delete task: {}", e);
ContainerdError::DeleteTaskError(e.to_string())
})?;
Ok(())
}
async fn do_wait_task(cid: &str, ns: &str) -> Result<WaitResponse, ContainerdError> {
let mut c = Self::get_client().await.tasks();
let wait_request = WaitRequest {
container_id: cid.to_string(),
..Default::default()
};
let resp = c
.wait(with_namespace!(wait_request, ns))
.await
.map_err(|e| {
log::error!("wait error: {}", e);
ContainerdError::WaitTaskError(e.to_string())
})?
.into_inner();
Ok(resp)
}
/// 杀死并删除任务
pub async fn kill_task_with_timeout(cid: &str, ns: &str) -> Result<(), ContainerdError> {
let kill_timeout = Duration::from_secs(5);
let wait_future = Self::do_wait_task(cid, ns);
Self::do_kill_task(cid, ns).await?;
match timeout(kill_timeout, wait_future).await {
Ok(Ok(_)) => {
// 正常退出,尝试删除任务
Self::do_delete_task(cid, ns).await?;
}
Ok(Err(e)) => {
// wait 报错
log::error!("Error while waiting for task {}: {}", cid, e);
}
Err(_) => {
// 超时,强制 kill
log::warn!("Task {} did not exit in time, sending SIGKILL", cid);
Self::do_kill_task_force(cid, ns).await?;
// 尝试删除任务
if let Err(e) = Self::do_delete_task(cid, ns).await {
log::error!("Failed to delete task {} after SIGKILL: {}", cid, e);
}
}
}
Ok(())
}
/// 获取一个容器
pub async fn load_container(cid: &str, ns: &str) -> Result<Option<Container>, ContainerdError> {
let container_list = Self::list_container(ns).await?;
let container = container_list
.into_iter()
.find(|container| container.id == cid);
Ok(container)
}
/// 获取容器列表
pub async fn list_container(ns: &str) -> Result<Vec<Container>, ContainerdError> {
let mut cc = Self::get_client().await.containers();
let request = ListContainersRequest {
..Default::default()
};
let resp = cc.list(with_namespace!(request, ns)).await.map_err(|e| {
log::error!("Failed to list containers: {}", e);
ContainerdError::CreateContainerError(e.to_string())
})?;
Ok(resp.into_inner().containers)
}
pub async fn list_container_into_string(ns: &str) -> Result<Vec<String>, ContainerdError> {
let mut cc = Self::get_client().await.containers();
let request = ListContainersRequest {
..Default::default()
};
let resp = cc.list(with_namespace!(request, ns)).await.map_err(|e| {
log::error!("Failed to list containers: {}", e);
ContainerdError::CreateContainerError(e.to_string())
})?;
Ok(resp
.into_inner()
.containers
.into_iter()
.map(|container| container.id)
.collect())
}
async fn prepare_snapshot(
image_name: &str,
cid: &str,
ns: &str,
) -> Result<(), ContainerdError> {
let parent_snapshot = Self::get_parent_snapshot(image_name).await?;
Self::do_prepare_snapshot(cid, ns, parent_snapshot).await?;
Ok(())
}
async fn do_prepare_snapshot(
cid: &str,
ns: &str,
parent_snapshot: String,
) -> Result<(), ContainerdError> {
let req = PrepareSnapshotRequest {
snapshotter: "overlayfs".to_string(),
key: cid.to_string(),
parent: parent_snapshot,
..Default::default()
};
let client = Self::get_client().await;
let _resp = client
.snapshots()
.prepare(with_namespace!(req, ns))
.await
.map_err(|e| {
log::error!("Failed to prepare snapshot: {}", e);
ContainerdError::CreateSnapshotError(e.to_string())
})?;
Ok(())
}
async fn get_parent_snapshot(image_name: &str) -> Result<String, ContainerdError> {
let config = ImageManager::get_image_config(image_name).map_err(|e| {
log::error!("Failed to get image config: {}", e);
ContainerdError::GetParentSnapshotError(e.to_string())
})?;
if config.rootfs().diff_ids().is_empty() {
log::error!("Image config has no diff_ids for image: {}", image_name);
return Err(ContainerdError::GetParentSnapshotError(
"No diff_ids found in image config".to_string(),
));
}
let mut iter = config.rootfs().diff_ids().iter();
let mut ret = iter
.next()
.map_or_else(String::new, |layer_digest| layer_digest.clone());
for layer_digest in iter {
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)
}
fn get_spec(cid: &str, ns: &str, image_name: &str) -> Result<Option<Any>, ContainerdError> {
let config = ImageManager::get_runtime_config(image_name).unwrap();
let spec_path = generate_spec(cid, ns, &config).map_err(|e| {
log::error!("Failed to generate spec: {}", e);
ContainerdError::GenerateSpecError(e.to_string())
})?;
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(),
};
Ok(Some(spec))
}
/// 为一个容器准备cni网络并写入全局map中
fn prepare_cni_network(cid: &str, ns: &str, image_name: &str) -> Result<(), ContainerdError> {
let ip = cni::create_cni_network(cid.to_string(), ns.to_string()).map_err(|e| {
log::error!("Failed to create CNI network: {}", e);
ContainerdError::CreateTaskError(e.to_string())
})?;
let ports = ImageManager::get_runtime_config(image_name).unwrap().ports;
let network_config = NetworkConfig::new(ip, ports);
let function = FunctionScope {
function_name: cid.to_string(),
namespace: ns.to_string(),
};
Self::save_container_network_config(function, network_config);
Ok(())
}
/// 删除cni网络删除全局map中的网络配置
fn remove_cni_network(cid: &str, ns: &str) -> Result<(), ContainerdError> {
cni::delete_cni_network(ns, cid);
let function = FunctionScope {
function_name: cid.to_string(),
namespace: ns.to_string(),
};
Self::remove_container_network_config(&function);
Ok(())
}
fn save_container_network_config(function: FunctionScope, net_conf: NetworkConfig) {
let mut map = GLOBAL_NETNS_MAP.write().unwrap();
map.insert(function, net_conf);
}
pub fn get_address(function: &FunctionScope) -> String {
let map = GLOBAL_NETNS_MAP.read().unwrap();
let addr = map.get(function).map(|net_conf| net_conf.get_address());
addr.unwrap_or_default()
}
fn remove_container_network_config(function: &FunctionScope) {
let mut map = GLOBAL_NETNS_MAP.write().unwrap();
map.remove(function);
}
pub async fn list_namespaces() -> Result<Vec<String>, ContainerdError> {
let mut c = Self::get_client().await.namespaces();
let req = ListNamespacesRequest {
..Default::default()
};
let resp = c.list(req).await.map_err(|e| {
log::error!("Failed to list namespaces: {}", e);
ContainerdError::GetContainerListError(e.to_string())
})?;
Ok(resp
.into_inner()
.namespaces
.into_iter()
.map(|ns| ns.name)
.collect())
}
pub async fn pause_task() {
todo!()
}
pub async fn resume_task() {
todo!()
}
}
#[derive(Debug)]
pub enum ContainerdError {
CreateContainerError(String),
CreateSnapshotError(String),
GetParentSnapshotError(String),
GenerateSpecError(String),
DeleteContainerError(String),
GetContainerListError(String),
KillTaskError(String),
DeleteTaskError(String),
WaitTaskError(String),
CreateTaskError(String),
StartTaskError(String),
#[allow(dead_code)]
OtherError,
}
impl std::fmt::Display for ContainerdError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ContainerdError::CreateContainerError(msg) => {
write!(f, "Failed to create container: {}", msg)
}
ContainerdError::CreateSnapshotError(msg) => {
write!(f, "Failed to create snapshot: {}", msg)
}
ContainerdError::GetParentSnapshotError(msg) => {
write!(f, "Failed to get parent snapshot: {}", msg)
}
ContainerdError::GenerateSpecError(msg) => {
write!(f, "Failed to generate spec: {}", msg)
}
ContainerdError::DeleteContainerError(msg) => {
write!(f, "Failed to delete container: {}", msg)
}
ContainerdError::GetContainerListError(msg) => {
write!(f, "Failed to get container list: {}", msg)
}
ContainerdError::KillTaskError(msg) => {
write!(f, "Failed to kill task: {}", msg)
}
ContainerdError::DeleteTaskError(msg) => {
write!(f, "Failed to delete task: {}", msg)
}
ContainerdError::WaitTaskError(msg) => {
write!(f, "Failed to wait task: {}", msg)
}
ContainerdError::CreateTaskError(msg) => {
write!(f, "Failed to create task: {}", msg)
}
ContainerdError::StartTaskError(msg) => {
write!(f, "Failed to start task: {}", msg)
}
ContainerdError::OtherError => write!(f, "Other error happened"),
}
}
}
impl std::error::Error for ContainerdError {}

View File

@ -1,48 +0,0 @@
pub mod containerd_manager;
pub mod image_manager;
pub mod namespace_manager;
pub mod spec;
pub mod systemd;
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
// config.json,dockerhub密钥
// const DOCKER_CONFIG_DIR: &str = "/var/lib/faasd/.docker/";
type NetnsMap = Arc<RwLock<HashMap<FunctionScope, NetworkConfig>>>;
lazy_static::lazy_static! {
static ref GLOBAL_NETNS_MAP: NetnsMap = Arc::new(RwLock::new(HashMap::new()));
}
#[derive(Hash, Eq, PartialEq)]
pub struct FunctionScope {
pub function_name: String,
pub namespace: String,
}
#[derive(Debug, Clone)]
pub struct NetworkConfig {
ip: String,
ports: Vec<String>,
}
impl NetworkConfig {
pub fn new(ip: String, ports: Vec<String>) -> Self {
NetworkConfig { ip, ports }
}
pub fn get_ip(&self) -> String {
self.ip.clone()
}
pub fn get_address(&self) -> String {
format!(
"{}:{}",
self.ip.split('/').next().unwrap_or(""),
self.ports[0].split('/').next().unwrap_or("")
)
}
}

View File

@ -1,81 +0,0 @@
use crate::containerd_manager::CLIENT;
use containerd_client::{
Client,
services::v1::{CreateNamespaceRequest, DeleteNamespaceRequest, Namespace},
};
use std::sync::Arc;
pub struct NSManager {
namespaces: Vec<Namespace>,
}
impl NSManager {
async fn get_client() -> Arc<Client> {
CLIENT
.get()
.unwrap_or_else(|| panic!("Client not initialized, Please run init first"))
.clone()
}
pub async fn create_namespace(&mut self, name: &str) -> Result<(), NameSpaceError> {
let client = Self::get_client().await;
let mut ns_client = client.namespaces();
let request = CreateNamespaceRequest {
namespace: Some(Namespace {
name: name.to_string(),
..Default::default()
}),
};
let response = ns_client.create(request).await.map_err(|e| {
NameSpaceError::CreateError(format!("Failed to create namespace {}: {}", name, e))
})?;
self.namespaces
.push(response.into_inner().namespace.unwrap());
Ok(())
}
pub async fn delete_namespace(&mut self, name: &str) -> Result<(), NameSpaceError> {
let client = Self::get_client().await;
let mut ns_client = client.namespaces();
let req = DeleteNamespaceRequest {
name: name.to_string(),
};
ns_client.delete(req).await.map_err(|e| {
NameSpaceError::DeleteError(format!("Failed to delete namespace {}: {}", name, e))
})?;
self.namespaces.retain(|ns| ns.name != name);
Ok(())
}
pub async fn list_namespace(&self) -> Result<Vec<Namespace>, NameSpaceError> {
// 这里没有选择直接列举所有的命名空间,而是返回当前对象中存储的命名空间
// 觉得应该只能看见自己创建的命名空间而不能看见其他人的命名空间
// 是不是应该把namespaces做持久化存储作为一个用户自己的namespace
Ok(self.namespaces.clone())
}
}
#[derive(Debug)]
pub enum NameSpaceError {
CreateError(String),
DeleteError(String),
ListError(String),
}
impl std::fmt::Display for NameSpaceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NameSpaceError::CreateError(msg) => write!(f, "Create Namespace Error: {}", msg),
NameSpaceError::DeleteError(msg) => write!(f, "Delete Namespace Error: {}", msg),
NameSpaceError::ListError(msg) => write!(f, "List Namespace Error: {}", msg),
}
}
}
impl std::error::Error for NameSpaceError {}

View File

@ -1,339 +0,0 @@
use serde::{Deserialize, Serialize};
use std::fs::File;
use crate::image_manager::ImageRuntimeConfig;
// 定义版本的常量
const VERSION_MAJOR: u32 = 1;
const VERSION_MINOR: u32 = 1;
const VERSION_PATCH: u32 = 0;
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 PID_NAMESPACE: &str = "pid";
const NETWORK_NAMESPACE: &str = "network";
const MOUNT_NAMESPACE: &str = "mount";
const IPC_NAMESPACE: &str = "ipc";
const UTS_NAMESPACE: &str = "uts";
#[derive(Serialize, Deserialize, Debug)]
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)]
pub struct Root {
pub path: String,
}
#[derive(Serialize, Deserialize, Debug)]
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)]
pub struct User {
pub uid: u32,
pub gid: u32,
#[serde(rename = "additionalGids")]
pub additional_gids: Vec<u32>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Mount {
pub destination: String,
#[serde(rename = "type")]
pub type_: String,
pub source: String,
pub options: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct LinuxCapabilities {
pub bounding: Vec<String>,
pub permitted: Vec<String>,
pub effective: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct POSIXRlimit {
pub hard: u64,
pub soft: u64,
#[serde(rename = "type")]
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 path: Option<String>,
}
pub fn default_unix_caps() -> Vec<String> {
vec![
String::from("CAP_CHOWN"),
String::from("CAP_DAC_OVERRIDE"),
String::from("CAP_FSETID"),
String::from("CAP_FOWNER"),
String::from("CAP_MKNOD"),
String::from("CAP_NET_RAW"),
String::from("CAP_SETGID"),
String::from("CAP_SETUID"),
String::from("CAP_SETFCAP"),
String::from("CAP_SETPCAP"),
String::from("CAP_NET_BIND_SERVICE"),
String::from("CAP_SYS_CHROOT"),
String::from("CAP_KILL"),
String::from("CAP_AUDIT_WRITE"),
]
}
fn default_masked_parhs() -> Vec<String> {
vec![
String::from("/proc/acpi"),
String::from("/proc/asound"),
String::from("/proc/kcore"),
String::from("/proc/keys"),
String::from("/proc/latency_stats"),
String::from("/proc/timer_list"),
String::from("/proc/timer_stats"),
String::from("/proc/sched_debug"),
String::from("/proc/scsi"),
String::from("/sys/firmware"),
String::from("/sys/devices/virtual/powercap"),
]
}
fn default_readonly_paths() -> Vec<String> {
vec![
String::from("/proc/bus"),
String::from("/proc/fs"),
String::from("/proc/irq"),
String::from("/proc/sys"),
String::from("/proc/sysrq-trigger"),
]
}
fn default_unix_namespaces(ns: &str, cid: &str) -> Vec<LinuxNamespace> {
vec![
LinuxNamespace {
type_: String::from(PID_NAMESPACE),
path: None,
},
LinuxNamespace {
type_: String::from(IPC_NAMESPACE),
path: None,
},
LinuxNamespace {
type_: String::from(UTS_NAMESPACE),
path: None,
},
LinuxNamespace {
type_: String::from(MOUNT_NAMESPACE),
path: None,
},
LinuxNamespace {
type_: String::from(NETWORK_NAMESPACE),
path: Some(format!("/var/run/netns/{}", get_netns(ns, cid))),
},
]
}
fn default_mounts() -> Vec<Mount> {
vec![
Mount {
destination: "/proc".to_string(),
type_: "proc".to_string(),
source: "proc".to_string(),
options: vec![],
},
Mount {
destination: "/dev".to_string(),
type_: "tmpfs".to_string(),
source: "tmpfs".to_string(),
options: vec![
"nosuid".to_string(),
"strictatime".to_string(),
"mode=755".to_string(),
"size=65536k".to_string(),
],
},
Mount {
destination: "/dev/pts".to_string(),
type_: "devpts".to_string(),
source: "devpts".to_string(),
options: vec![
"nosuid".to_string(),
"noexec".to_string(),
"newinstance".to_string(),
"ptmxmode=0666".to_string(),
"mode=0620".to_string(),
"gid=5".to_string(),
],
},
Mount {
destination: "/dev/shm".to_string(),
type_: "tmpfs".to_string(),
source: "shm".to_string(),
options: vec![
"nosuid".to_string(),
"noexec".to_string(),
"nodev".to_string(),
"mode=1777".to_string(),
"size=65536k".to_string(),
],
},
Mount {
destination: "/dev/mqueue".to_string(),
type_: "mqueue".to_string(),
source: "mqueue".to_string(),
options: vec![
"nosuid".to_string(),
"noexec".to_string(),
"nodev".to_string(),
],
},
Mount {
destination: "/sys".to_string(),
type_: "sysfs".to_string(),
source: "sysfs".to_string(),
options: vec![
"nosuid".to_string(),
"noexec".to_string(),
"nodev".to_string(),
"ro".to_string(),
],
},
Mount {
destination: "/sys/fs/cgroup".to_string(),
type_: "cgroup".to_string(),
source: "cgroup".to_string(),
options: vec![
"nosuid".to_string(),
"noexec".to_string(),
"nodev".to_string(),
"relatime".to_string(),
"ro".to_string(),
],
},
]
}
fn get_version() -> String {
format!(
"{}.{}.{}{}",
VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, VERSION_DEV
)
}
pub fn populate_default_unix_spec(id: &str, ns: &str) -> Spec {
Spec {
oci_version: get_version(),
root: Root {
path: DEFAULT_ROOTFS_PATH.to_string(),
},
process: Process {
cwd: String::from("/"),
no_new_privileges: true,
user: User {
uid: 0,
gid: 0,
additional_gids: vec![],
},
capabilities: LinuxCapabilities {
bounding: default_unix_caps(),
permitted: default_unix_caps(),
effective: default_unix_caps(),
},
rlimits: vec![POSIXRlimit {
type_: String::from("RLIMIT_NOFILE"),
hard: 1024,
soft: 1024,
}],
args: vec![],
env: vec![],
},
linux: Linux {
masked_paths: default_masked_parhs(),
readonly_paths: default_readonly_paths(),
cgroups_path: format!("/{}/{}", ns, id),
resources: LinuxResources {
devices: vec![LinuxDeviceCgroup {
allow: false,
access: RWM.to_string(),
}],
},
namespaces: default_unix_namespaces(ns, id),
},
mounts: default_mounts(),
}
}
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(())
}
fn get_netns(ns: &str, cid: &str) -> String {
format!("{}-{}", ns, cid)
}
pub fn generate_spec(
id: &str,
ns: &str,
runtime_config: &ImageRuntimeConfig,
) -> Result<String, std::io::Error> {
let namespace = match ns {
"" => DEFAULT_NAMESPACE,
_ => ns,
};
let mut spec = populate_default_unix_spec(id, ns);
spec.process.args = runtime_config.args.clone();
spec.process.env = runtime_config.env.clone();
spec.process.cwd = runtime_config.cwd.clone();
let dir_path = format!("{}/{}", PATH_TO_SPEC_PREFIX, namespace);
let path = format!("{}/{}.json", dir_path, id);
std::fs::create_dir_all(&dir_path)?;
save_spec_to_file(&spec, &path)?;
Ok(path)
}

View File

@ -6,7 +6,7 @@ info:
name: GPL-3.0
version: 0.1.0
servers:
- url: "http://localhost:8090"
- url: "http://localhost:8080"
description: Local server
tags:
- name: internal

View File

@ -59,10 +59,8 @@
fileset = lib.fileset.unions [
./Cargo.toml
./Cargo.lock
(craneLib.fileset.commonCargoSources ./crates/app)
(craneLib.fileset.commonCargoSources ./crates/service)
(craneLib.fileset.commonCargoSources ./crates/provider)
(craneLib.fileset.commonCargoSources ./crates/cni)
(craneLib.fileset.commonCargoSources ./crates/faas-containerd)
(craneLib.fileset.commonCargoSources ./crates/gateway)
(craneLib.fileset.commonCargoSources ./crates/my-workspace-hack)
(craneLib.fileset.commonCargoSources crate)
];
@ -70,8 +68,8 @@
faas-rs-crate = craneLib.buildPackage ( individualCrateArgs // {
pname = "faas-rs";
cargoExtraArgs = "-p faas-rs";
src = fileSetForCrate ./crates/app;
cargoExtraArgs = "-p faas-containerd";
src = fileSetForCrate ./crates/faas-containerd;
});
in
with pkgs;