mirror of
https://github.com/faas-rs/faasd-in-rust.git
synced 2025-06-08 07:55:04 +00:00
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:
parent
ed6741cd8a
commit
308e9bcc5d
1022
Cargo.lock
generated
1022
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
@ -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());
|
||||
}
|
||||
}
|
@ -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."));
|
||||
}
|
||||
}
|
@ -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"
|
@ -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"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
36
crates/faas-containerd/Cargo.toml
Normal file
36
crates/faas-containerd/Cargo.toml
Normal 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"
|
15
crates/faas-containerd/src/consts.rs
Normal file
15
crates/faas-containerd/src/consts.rs
Normal 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 = ""; // 对应开发分支
|
148
crates/faas-containerd/src/impls/cni/cni_impl.rs
Normal file
148
crates/faas-containerd/src/impls/cni/cni_impl.rs
Normal 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))
|
||||
}
|
94
crates/faas-containerd/src/impls/cni/command.rs
Normal file
94
crates/faas-containerd/src/impls/cni/command.rs
Normal 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"
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
// }
|
55
crates/faas-containerd/src/impls/cni/mod.rs
Normal file
55
crates/faas-containerd/src/impls/cni/mod.rs
Normal 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))
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
})
|
||||
}
|
||||
}
|
125
crates/faas-containerd/src/impls/container.rs
Normal file
125
crates/faas-containerd/src/impls/container.rs
Normal 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())
|
||||
}
|
||||
}
|
17
crates/faas-containerd/src/impls/error.rs
Normal file
17
crates/faas-containerd/src/impls/error.rs
Normal 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,
|
||||
}
|
97
crates/faas-containerd/src/impls/function.rs
Normal file
97
crates/faas-containerd/src/impls/function.rs
Normal 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()
|
||||
// }
|
||||
// }
|
30
crates/faas-containerd/src/impls/mod.rs
Normal file
30
crates/faas-containerd/src/impls/mod.rs
Normal 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,
|
||||
}
|
@ -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();
|
||||
|
||||
impl ImageRuntimeConfig {
|
||||
pub fn new(env: Vec<String>, args: Vec<String>, ports: Vec<String>, cwd: String) -> Self {
|
||||
ImageRuntimeConfig {
|
||||
env,
|
||||
args,
|
||||
ports,
|
||||
cwd,
|
||||
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);
|
||||
})
|
||||
}
|
||||
|
||||
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!()
|
||||
}
|
||||
// 不用这个也能拉取镜像?
|
||||
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(),
|
||||
}
|
||||
}
|
131
crates/faas-containerd/src/impls/snapshot.rs
Normal file
131
crates/faas-containerd/src/impls/snapshot.rs
Normal 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(())
|
||||
}
|
||||
}
|
329
crates/faas-containerd/src/impls/spec.rs
Normal file
329
crates/faas-containerd/src/impls/spec.rs
Normal 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)
|
||||
}
|
||||
}
|
210
crates/faas-containerd/src/impls/task.rs
Normal file
210
crates/faas-containerd/src/impls/task.rs
Normal 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(())
|
||||
}
|
||||
}
|
8
crates/faas-containerd/src/lib.rs
Normal file
8
crates/faas-containerd/src/lib.rs
Normal 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;
|
36
crates/faas-containerd/src/main.rs
Normal file
36
crates/faas-containerd/src/main.rs
Normal 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
|
||||
}
|
35
crates/faas-containerd/src/provider/function/delete.rs
Normal file
35
crates/faas-containerd/src/provider/function/delete.rs
Normal 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
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
96
crates/faas-containerd/src/provider/function/deploy.rs
Normal file
96
crates/faas-containerd/src/provider/function/deploy.rs
Normal 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(())
|
||||
}
|
||||
}
|
19
crates/faas-containerd/src/provider/function/get.rs
Normal file
19
crates/faas-containerd/src/provider/function/get.rs
Normal 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();
|
||||
// }
|
||||
}
|
69
crates/faas-containerd/src/provider/function/list.rs
Normal file
69
crates/faas-containerd/src/provider/function/list.rs
Normal 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)
|
||||
}
|
||||
}
|
6
crates/faas-containerd/src/provider/function/mod.rs
Normal file
6
crates/faas-containerd/src/provider/function/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub mod delete;
|
||||
pub mod deploy;
|
||||
pub mod list;
|
||||
pub mod resolve;
|
||||
pub mod status;
|
||||
pub mod update;
|
65
crates/faas-containerd/src/provider/function/resolve.rs
Normal file
65
crates/faas-containerd/src/provider/function/resolve.rs
Normal 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));
|
||||
}
|
||||
}
|
69
crates/faas-containerd/src/provider/function/status.rs
Normal file
69
crates/faas-containerd/src/provider/function/status.rs
Normal 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)
|
||||
}
|
||||
}
|
32
crates/faas-containerd/src/provider/function/update.rs
Normal file
32
crates/faas-containerd/src/provider/function/update.rs
Normal 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(())
|
||||
}
|
||||
}
|
49
crates/faas-containerd/src/provider/mod.rs
Normal file
49
crates/faas-containerd/src/provider/mod.rs
Normal 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
|
||||
}
|
||||
}
|
150
crates/faas-containerd/tests/integration_test.rs
Normal file
150
crates/faas-containerd/tests/integration_test.rs
Normal 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"));
|
||||
}
|
@ -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"
|
175
crates/gateway/src/bootstrap/mod.rs
Normal file
175
crates/gateway/src/bootstrap/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
173
crates/gateway/src/handlers/function.rs
Normal file
173
crates/gateway/src/handlers/function.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
34
crates/gateway/src/handlers/mod.rs
Normal file
34
crates/gateway/src/handlers/mod.rs
Normal 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)),
|
||||
}
|
||||
}
|
||||
}
|
67
crates/gateway/src/handlers/proxy.rs
Normal file
67
crates/gateway/src/handlers/proxy.rs
Normal 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")),
|
||||
}
|
||||
}
|
6
crates/gateway/src/lib.rs
Normal file
6
crates/gateway/src/lib.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub mod bootstrap;
|
||||
pub mod handlers;
|
||||
// pub mod metrics;
|
||||
pub mod provider;
|
||||
pub mod proxy;
|
||||
pub mod types;
|
@ -35,5 +35,3 @@ impl HttpMetrics {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const TEXT_CONTENT_TYPE: &str = "text/plain; version=0.0.4";
|
45
crates/gateway/src/provider/mod.rs
Normal file
45
crates/gateway/src/provider/mod.rs
Normal 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;
|
||||
}
|
@ -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);
|
||||
|
@ -1,3 +1,4 @@
|
||||
pub mod builder;
|
||||
pub mod proxy_handler;
|
||||
mod proxy_handler_test;
|
||||
// #[cfg(test)]
|
||||
// mod test;
|
28
crates/gateway/src/proxy/proxy_handler.rs
Normal file
28
crates/gateway/src/proxy/proxy_handler.rs
Normal 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))
|
||||
}
|
108
crates/gateway/src/proxy/test.rs
Normal file
108
crates/gateway/src/proxy/test.rs
Normal 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("")
|
||||
))
|
||||
}
|
166
crates/gateway/src/types/function.rs
Normal file
166
crates/gateway/src/types/function.rs
Normal 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
|
||||
}
|
2
crates/gateway/src/types/mod.rs
Normal file
2
crates/gateway/src/types/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod config;
|
||||
pub mod function;
|
@ -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"] }
|
||||
|
||||
|
@ -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,并且返回错误
|
@ -1 +0,0 @@
|
||||
|
@ -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";
|
@ -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>,
|
||||
}
|
@ -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(())
|
||||
}
|
@ -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,
|
||||
// }
|
||||
// }
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
|
@ -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;
|
@ -1 +0,0 @@
|
||||
|
@ -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))
|
||||
}
|
@ -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("")
|
||||
))
|
||||
}
|
||||
}
|
@ -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>,
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
pub mod config;
|
||||
pub mod function_deployment;
|
@ -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"
|
@ -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 {}
|
@ -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("")
|
||||
)
|
||||
}
|
||||
}
|
@ -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 {}
|
@ -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)
|
||||
}
|
@ -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
|
||||
|
10
flake.nix
10
flake.nix
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user