feat: add workshop-{executor, helper-mac}
Signed-off-by: Catty Steve <4795515+Catty2014@user.noreply.gitee.com>
This commit is contained in:
Generated
+1008
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "workshop-executor"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.5.53", features = ["derive"] }
|
||||||
|
config = "0.15.19"
|
||||||
|
env_logger = "0.11.8"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
log = "0.4.28"
|
||||||
|
minijinja = "2.14.0"
|
||||||
|
regex = "1.12.2"
|
||||||
|
shlex = "1.3.0"
|
||||||
|
tempfile = "3.24.0"
|
||||||
|
thiserror = "2.0.17"
|
||||||
|
topological-sort = "0.2.2"
|
||||||
|
uuid = { version = "1.19.0", features = ["v4"] }
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::fs::Permissions;
|
||||||
|
use minijinja::{Environment, context};
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
use crate::error::BuilderError;
|
||||||
|
pub fn build_script(name: String, body: String, bake_base: PathBuf, temp_path: Option<String>) -> Result<PathBuf, BuilderError>{
|
||||||
|
let temp_path = temp_path.unwrap_or_else(|| String::from("/"));
|
||||||
|
let bake_base_content = std::fs::read_to_string(&bake_base)
|
||||||
|
.map_err(|e| {
|
||||||
|
BuilderError::IoError(format!(
|
||||||
|
"Error when reading bake_base({}): {}",
|
||||||
|
bake_base.display(), // 显示文件名(含路径)
|
||||||
|
e // 原 IoError 描述
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let mut env = Environment::new();
|
||||||
|
env.add_template("script", bake_base_content.as_str())?;
|
||||||
|
let script = env.get_template("script")?;
|
||||||
|
let script = script.render(context!(main => body))?;
|
||||||
|
|
||||||
|
let temp_filename = format!("bakefn_{}.sh", name);
|
||||||
|
let mut temp_file = NamedTempFile::new()
|
||||||
|
.map_err(|e| {
|
||||||
|
BuilderError::IoError(format!(
|
||||||
|
"Error when creating temporary file({}): {}",
|
||||||
|
temp_path, e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
temp_file.as_file_mut().set_permissions(Permissions::from_mode(0o700))?;
|
||||||
|
|
||||||
|
std::fs::write(&temp_file, script)?;
|
||||||
|
let target_path = PathBuf::from(format!("{}/{}", temp_path, temp_filename));
|
||||||
|
temp_file.persist(&target_path)
|
||||||
|
.map_err(|e| { // persist 返回 (错误, 临时文件)
|
||||||
|
BuilderError::IoError(format!(
|
||||||
|
"Error when writing persistent file({}): {}",
|
||||||
|
target_path.display(), e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Ok(target_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
// use std::os::unix::fs::MetadataExt;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
/// 辅助函数:创建临时的bake_base模板文件(含{{ main }}占位符)
|
||||||
|
fn create_test_bake_base(content: &str) -> Result<PathBuf, BuilderError> {
|
||||||
|
let temp_file = NamedTempFile::new()?;
|
||||||
|
fs::write(&temp_file, content)?;
|
||||||
|
let filename = "/tmp/bake_base.sh";
|
||||||
|
temp_file.persist(&filename)?;
|
||||||
|
Ok(PathBuf::from(filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试1:正常场景(传入temp_path,模板渲染正确,文件生成成功)
|
||||||
|
#[test]
|
||||||
|
fn test_build_script_success() {
|
||||||
|
// 1. 准备测试数据
|
||||||
|
let test_name = "test_success";
|
||||||
|
let test_body = "echo 'hello world'";
|
||||||
|
let bake_base_content = "#!/bin/bash\nmain() { {{ main }} }\nmain";
|
||||||
|
let bake_base_path = create_test_bake_base(bake_base_content).unwrap();
|
||||||
|
|
||||||
|
// 创建临时目录(避免权限问题)
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let temp_path = temp_dir.path().to_str().unwrap().to_string();
|
||||||
|
|
||||||
|
// 2. 调用函数
|
||||||
|
let target_path = build_script(
|
||||||
|
test_name.to_string(),
|
||||||
|
test_body.to_string(),
|
||||||
|
bake_base_path,
|
||||||
|
Some(temp_path.clone()),
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
// 3. 断言验证
|
||||||
|
// 3.1 验证文件存在
|
||||||
|
assert!(target_path.exists());
|
||||||
|
// 3.2 验证文件名正确
|
||||||
|
assert_eq!(
|
||||||
|
target_path.file_name().unwrap().to_str().unwrap(),
|
||||||
|
format!("bakefn_{}.sh", test_name)
|
||||||
|
);
|
||||||
|
// 3.3 验证文件内容(模板渲染正确)
|
||||||
|
let script_content = fs::read_to_string(&target_path).unwrap();
|
||||||
|
assert!(script_content.contains("#!/bin/bash"));
|
||||||
|
dbg!(&test_body, &script_content);
|
||||||
|
assert!(script_content.contains(&format!("main() {{ {} }}", test_body)));
|
||||||
|
assert!(script_content.contains("main"));
|
||||||
|
// 3.4 验证文件权限(0o700)
|
||||||
|
let metadata = fs::metadata(&target_path).unwrap();
|
||||||
|
assert_eq!(metadata.permissions().mode() & 0o777, 0o700);
|
||||||
|
|
||||||
|
// 4. 清理
|
||||||
|
fs::remove_file(&target_path).ok();
|
||||||
|
temp_dir.close().ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试2:默认temp_path场景(temp_path=None,默认用"/",但测试时替换为临时目录避免权限问题)
|
||||||
|
#[test]
|
||||||
|
fn test_build_script_default_temp_path() {
|
||||||
|
// 注意:直接往"/"写文件普通用户权限不足,因此测试时先修改函数逻辑的默认值为临时目录,
|
||||||
|
// 或在测试中mock,这里采用「替换默认值」的方式(实际测试需适配环境)
|
||||||
|
let test_name = "test_default_path";
|
||||||
|
let test_body = "echo 'default path'";
|
||||||
|
let bake_base_content = "{{ main }}";
|
||||||
|
let bake_base_path = create_test_bake_base(bake_base_content).unwrap();
|
||||||
|
|
||||||
|
// 手动指定temp_path为临时目录(模拟None的默认逻辑,但避免权限问题)
|
||||||
|
let temp_path = std::env::temp_dir().to_str().unwrap().to_string();
|
||||||
|
let target_path = build_script(
|
||||||
|
test_name.to_string(),
|
||||||
|
test_body.to_string(),
|
||||||
|
bake_base_path,
|
||||||
|
None, // 传入None,函数内默认用"/",但测试环境可能需要调整,此处仅验证逻辑
|
||||||
|
).unwrap_or_else(|e| {
|
||||||
|
// 若"/"权限不足,改用临时目录重试(兼容测试环境)
|
||||||
|
build_script(
|
||||||
|
test_name.to_string(),
|
||||||
|
test_body.to_string(),
|
||||||
|
create_test_bake_base(bake_base_content).unwrap(),
|
||||||
|
Some(temp_path.clone()),
|
||||||
|
).unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 断言
|
||||||
|
assert!(target_path.exists());
|
||||||
|
assert!(target_path.starts_with(if temp_path.is_empty() { "/" } else { &temp_path }));
|
||||||
|
assert_eq!(
|
||||||
|
target_path.file_name().unwrap().to_str().unwrap(),
|
||||||
|
format!("bakefn_{}.sh", test_name)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
fs::remove_file(&target_path).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试3:错误场景 - bake_base文件不存在
|
||||||
|
#[test]
|
||||||
|
fn test_build_script_bake_base_not_found() {
|
||||||
|
let non_exist_path = PathBuf::from("/non/exist/bake_base.sh");
|
||||||
|
let result = build_script(
|
||||||
|
"test_not_found".to_string(),
|
||||||
|
"echo 'test'".to_string(),
|
||||||
|
non_exist_path,
|
||||||
|
Some("/tmp".to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 断言返回IoError
|
||||||
|
assert!(matches!(result, Err(BuilderError::IoError(_))));
|
||||||
|
if let Err(BuilderError::IoError(msg)) = result {
|
||||||
|
assert!(msg.contains("No such file or directory") || msg.contains("不存在"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试4:错误场景 - 模板语法错误(minijinja渲染失败)
|
||||||
|
#[test]
|
||||||
|
fn test_build_script_invalid_template() {
|
||||||
|
// 无效模板:缺少闭合的}}
|
||||||
|
let invalid_template = "{{ main";
|
||||||
|
let bake_base_path = create_test_bake_base(invalid_template).unwrap();
|
||||||
|
|
||||||
|
let result = build_script(
|
||||||
|
"test_invalid_template".to_string(),
|
||||||
|
"echo 'test'".to_string(),
|
||||||
|
bake_base_path,
|
||||||
|
Some("/tmp".to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 断言返回TemplateError
|
||||||
|
dbg!(&result);
|
||||||
|
assert!(matches!(result, Err(BuilderError::TemplateError(_))));
|
||||||
|
if let Err(BuilderError::TemplateError(msg)) = result {
|
||||||
|
assert!(msg.contains("syntax error"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试5:验证文件权限为0o700
|
||||||
|
#[test]
|
||||||
|
fn test_build_script_permissions() {
|
||||||
|
let bake_base_path = create_test_bake_base("{{ main }}").unwrap();
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let temp_path = temp_dir.path().to_str().unwrap().to_string();
|
||||||
|
|
||||||
|
let target_path = build_script(
|
||||||
|
"test_permissions".to_string(),
|
||||||
|
"echo 'permissions'".to_string(),
|
||||||
|
bake_base_path,
|
||||||
|
Some(temp_path),
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
// 验证权限(0o700 = 448 in decimal)
|
||||||
|
let metadata = fs::metadata(&target_path).unwrap();
|
||||||
|
let mode = metadata.permissions().mode() & 0o777; // 只保留后三位
|
||||||
|
assert_eq!(mode, 0o700);
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
fs::remove_file(&target_path).ok();
|
||||||
|
temp_dir.close().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
use regex::Regex;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
// 解析装饰器的正则表达式
|
||||||
|
// 格式: # @decorator_name 或 # @decorator_name(arguments)
|
||||||
|
// 支持装饰器名称后的空格和括号内的参数
|
||||||
|
static ref DECORATOR_REGEX: Regex = Regex::new(r"^\s*#\s+@(\w+)\s?(\((.*)\))?").unwrap()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 装饰器类型定义
|
||||||
|
/// 这些装饰器用于增强 Bash 函数的行为,为 CI/CD 流水线提供元数据和控制逻辑
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Decorator {
|
||||||
|
Pipeline,
|
||||||
|
If(String),
|
||||||
|
Fallible,
|
||||||
|
Loop(usize),
|
||||||
|
After(String),
|
||||||
|
Timeout(u32),
|
||||||
|
Retry(usize, u32),
|
||||||
|
Export,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_decorator(line: &str) -> Option<Decorator>
|
||||||
|
{
|
||||||
|
DECORATOR_REGEX.captures(line).and_then( |caps| {
|
||||||
|
let name = caps.get(1)?.as_str();
|
||||||
|
let args = match caps.get(3){
|
||||||
|
Some(value) => value.as_str(),
|
||||||
|
None => ""
|
||||||
|
};
|
||||||
|
// dbg!(name, args);
|
||||||
|
let decorator = match name {
|
||||||
|
"pipeline" => Decorator::Pipeline,
|
||||||
|
"if" => Decorator::If(args.to_string()),
|
||||||
|
"fallible" => Decorator::Fallible,
|
||||||
|
"loop" => Decorator::Loop(args.parse().unwrap()),
|
||||||
|
"after" => Decorator::After(args.to_string()),
|
||||||
|
"timeout" => Decorator::Timeout(args.parse().unwrap()),
|
||||||
|
"retry" => {
|
||||||
|
let parts: Vec<&str> = args.split(',').collect();
|
||||||
|
Decorator::Retry(parts[0].trim().parse().unwrap(), parts[1].trim().parse().unwrap())
|
||||||
|
}
|
||||||
|
"export" => Decorator::Export,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(decorator)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests{
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_decorator() {
|
||||||
|
assert_eq!(parse_decorator("# @if(true)"), Some(Decorator::If("true".to_string())));
|
||||||
|
assert_eq!(parse_decorator("# @pipeline"), Some(Decorator::Pipeline));
|
||||||
|
assert_eq!(parse_decorator("# @fallible"), Some(Decorator::Fallible));
|
||||||
|
assert_eq!(parse_decorator("# @loop (10)"), Some(Decorator::Loop(10)));
|
||||||
|
assert_eq!(parse_decorator("# @after (build)"), Some(Decorator::After("build".to_string())));
|
||||||
|
assert_eq!(parse_decorator("# @timeout(5)"), Some(Decorator::Timeout(5)));
|
||||||
|
assert_eq!(parse_decorator("# @retry (3, 2)"), Some(Decorator::Retry(3, 2)));
|
||||||
|
assert_eq!(parse_decorator("# @export # comment"), Some(Decorator::Export));
|
||||||
|
assert_eq!(parse_decorator("# fallible"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug, Clone)]
|
||||||
|
pub enum ExecutionError {
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
IoError(String),
|
||||||
|
|
||||||
|
#[error("Parse error: {0}")]
|
||||||
|
ParseError(String),
|
||||||
|
|
||||||
|
#[error("Variable error: {0}")]
|
||||||
|
VariableError(String),
|
||||||
|
|
||||||
|
#[error("Execution error: {0}")]
|
||||||
|
ExecutionError(String),
|
||||||
|
|
||||||
|
#[error("Circular dependency detected")]
|
||||||
|
CircularDependency,
|
||||||
|
|
||||||
|
#[error("Dependency not satisfied: {0}")]
|
||||||
|
DependencyNotSatisfied(String),
|
||||||
|
|
||||||
|
#[error("Parallel execution error: {0}")]
|
||||||
|
ParallelExecutionError(String),
|
||||||
|
|
||||||
|
#[error("Timeout error: {0}")]
|
||||||
|
TimeoutError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug, Clone)]
|
||||||
|
pub enum BuilderError {
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
IoError(String),
|
||||||
|
|
||||||
|
#[error("Template error: {0}")]
|
||||||
|
TemplateError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for BuilderError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
BuilderError::IoError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<minijinja::Error> for BuilderError {
|
||||||
|
fn from(err: minijinja::Error) -> Self {
|
||||||
|
BuilderError::TemplateError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<tempfile::PersistError> for BuilderError {
|
||||||
|
fn from(err: tempfile::PersistError) -> Self {
|
||||||
|
BuilderError::IoError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
mod parser;
|
||||||
|
mod decorator;
|
||||||
|
mod schedule;
|
||||||
|
mod error;
|
||||||
|
mod variable;
|
||||||
|
mod builder;
|
||||||
|
use clap::{Parser};
|
||||||
|
use config::Config;
|
||||||
|
|
||||||
|
/// Honey Biscuit Workshop script executor
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about=None)]
|
||||||
|
struct Args {
|
||||||
|
/// Path to the script to execute
|
||||||
|
script: String,
|
||||||
|
|
||||||
|
/// Path to bake_base.sh
|
||||||
|
#[arg(short, long, default_value="./bake_base.sh")]
|
||||||
|
bake_base: String,
|
||||||
|
|
||||||
|
/// Username of the pipeline executor
|
||||||
|
#[arg(short, long)]
|
||||||
|
username: String,
|
||||||
|
|
||||||
|
/// Pipeline ID of this pipeline
|
||||||
|
#[arg(short, long)]
|
||||||
|
pipeline: String,
|
||||||
|
|
||||||
|
/// Parse the script without executing
|
||||||
|
#[arg(long, default_value="false")]
|
||||||
|
dry_run: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main(){
|
||||||
|
let mut settings = Config::builder()
|
||||||
|
// .add_source(config::File::with_name("_config.toml"))
|
||||||
|
.add_source(config::Environment::with_prefix("HBW_EXEC_"))
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
use crate::decorator::{Decorator, parse_decorator};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
|
use shlex::Shlex;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref FUNCTION_REGEX: Regex = Regex::new(r"^\s*(function\s*)([a-zA-Z0-9_-]+)\s*(\(\))?").unwrap();
|
||||||
|
static ref FUNCTION_REGEX_WITHOUT_KEYWORD: Regex = Regex::new(r"^\s*([a-zA-Z0-9_-]+)\s*(\(\))").unwrap();
|
||||||
|
static ref DECORATOR_HEADER_REGEX: Regex = Regex::new(r"\s*#\s+@").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Function{
|
||||||
|
pub name: String,
|
||||||
|
pub decorators: Vec<Decorator>,
|
||||||
|
pub body: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn parse_script(text: String) -> Result<Vec<Function>, ()>{
|
||||||
|
let mut functions: Vec<Function> = Vec::new();
|
||||||
|
let mut current_decorators: Vec<Decorator> = Vec::new();
|
||||||
|
let mut current_function_name = String::new();
|
||||||
|
let mut current_body: String = String::new();
|
||||||
|
let mut in_function = false;
|
||||||
|
let mut brace_count: i8 = 0;
|
||||||
|
for line in text.lines() {
|
||||||
|
// dbg!(&line);
|
||||||
|
// dbg!(&in_function, &brace_count);
|
||||||
|
if in_function {
|
||||||
|
current_body.push_str(line);
|
||||||
|
current_body.push('\n');
|
||||||
|
let tokens = Shlex::new(line).collect::<Vec<String>>();
|
||||||
|
let brace_delta: i8 = tokens.iter().filter(|&token| token == "{").count() as i8 - tokens.iter().filter(|&token| token == "}").count() as i8;
|
||||||
|
brace_count += brace_delta;
|
||||||
|
if brace_count == 0 {
|
||||||
|
functions.push(Function {
|
||||||
|
name: current_function_name.clone(),
|
||||||
|
decorators: current_decorators.clone(),
|
||||||
|
body: current_body.clone(),
|
||||||
|
});
|
||||||
|
current_decorators.clear();
|
||||||
|
current_function_name.clear();
|
||||||
|
current_body.clear();
|
||||||
|
in_function = false;
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if DECORATOR_HEADER_REGEX.find(line).is_some() {
|
||||||
|
if let Some(decorator) = parse_decorator(line) {
|
||||||
|
current_decorators.push(decorator);
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if FUNCTION_REGEX.find(line).is_some() || FUNCTION_REGEX_WITHOUT_KEYWORD.find(line).is_some() {
|
||||||
|
let tokens = Shlex::new(line).collect::<Vec<String>>();
|
||||||
|
// dbg!(&tokens);
|
||||||
|
if tokens[0]!="function"{
|
||||||
|
if FUNCTION_REGEX_WITHOUT_KEYWORD.find(line).is_none() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
in_function = true;
|
||||||
|
current_function_name = FUNCTION_REGEX_WITHOUT_KEYWORD.captures(line).unwrap()[1].to_string();
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
in_function = true;
|
||||||
|
current_function_name = FUNCTION_REGEX.captures(line).unwrap()[2].to_string();
|
||||||
|
}
|
||||||
|
// dbg!(¤t_function_name);
|
||||||
|
current_body.push_str(line);
|
||||||
|
current_body.push('\n');
|
||||||
|
let brace_left: i8 = tokens.iter().filter(|&token| token == "{").count() as i8;
|
||||||
|
let brace_right: i8 = tokens.iter().filter(|&token| token == "}").count() as i8;
|
||||||
|
let brace_delta: i8 = brace_left - brace_right;
|
||||||
|
brace_count += brace_delta;
|
||||||
|
if brace_count == 0 && brace_left != 0 {
|
||||||
|
functions.push(Function {
|
||||||
|
name: current_function_name.clone(),
|
||||||
|
decorators: current_decorators.clone(),
|
||||||
|
body: current_body.clone(),
|
||||||
|
});
|
||||||
|
current_decorators.clear();
|
||||||
|
current_function_name.clear();
|
||||||
|
current_body.clear();
|
||||||
|
in_function = false;
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
current_decorators.clear();
|
||||||
|
in_function = false;
|
||||||
|
}
|
||||||
|
Ok(functions)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_script() {
|
||||||
|
let script = r#"
|
||||||
|
# @pipeline
|
||||||
|
# @fallible
|
||||||
|
function my_function() {
|
||||||
|
echo "Hello"
|
||||||
|
{
|
||||||
|
echo "2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let functions = parse_script(script.to_string()).unwrap();
|
||||||
|
assert_eq!(functions.len(), 1);
|
||||||
|
assert_eq!(functions[0].name, "my_function");
|
||||||
|
assert_eq!(functions[0].decorators.len(), 2);
|
||||||
|
assert_eq!(functions[0].decorators[0], Decorator::Pipeline);
|
||||||
|
assert_eq!(functions[0].decorators[1], Decorator::Fallible);
|
||||||
|
// assert_eq!(functions[0].body, " echo \"Hello\"\n {\n echo \"2\"\n }\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_weird_script() {
|
||||||
|
let weird_script = r#"
|
||||||
|
# 注释行
|
||||||
|
# @fallible
|
||||||
|
|
||||||
|
# 空行
|
||||||
|
|
||||||
|
# @pipeline
|
||||||
|
function func1 {
|
||||||
|
echo "函数1"
|
||||||
|
# 字符串中的花括号 { 不应该影响计数
|
||||||
|
echo "This is a { in string"
|
||||||
|
if [ true ]; then
|
||||||
|
echo "嵌套代码块"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 单行函数
|
||||||
|
func2() { echo "单行函数"; }
|
||||||
|
|
||||||
|
# 装饰器后面有注释
|
||||||
|
# @if(some)
|
||||||
|
# 这是注释
|
||||||
|
func3() {
|
||||||
|
# 函数中的注释
|
||||||
|
echo "函数3"
|
||||||
|
# 复杂的括号嵌套
|
||||||
|
{
|
||||||
|
{
|
||||||
|
echo "深度嵌套"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
valid-function() {
|
||||||
|
echo "这个也应该被解析"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 无效的函数定义
|
||||||
|
another_invalid-function {
|
||||||
|
echo "你的括号呢"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 另一个有效函数
|
||||||
|
# @after(what)
|
||||||
|
function final_func()
|
||||||
|
{
|
||||||
|
# 多行定义
|
||||||
|
echo "最后函数"
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let functions = parse_script(weird_script.to_string()).unwrap();
|
||||||
|
|
||||||
|
// 验证解析的函数数量
|
||||||
|
assert_eq!(functions.len(), 5, "应该解析出5个有效函数");
|
||||||
|
|
||||||
|
// 验证函数1
|
||||||
|
let func1 = functions.iter().find(|f| f.name == "func1").unwrap();
|
||||||
|
assert_eq!(func1.decorators.len(), 1);
|
||||||
|
assert!(func1.body.contains("echo \"函数1\""));
|
||||||
|
assert!(func1.body.contains("echo \"This is a { in string\""));
|
||||||
|
|
||||||
|
// 验证函数2(单行函数)
|
||||||
|
let func2 = functions.iter().find(|f| f.name == "func2").unwrap();
|
||||||
|
assert_eq!(func2.decorators.len(), 0);
|
||||||
|
assert!(func2.body.trim().ends_with("{ echo \"单行函数\"; }"));
|
||||||
|
|
||||||
|
// 验证函数3
|
||||||
|
let func3 = functions.iter().find(|f| f.name == "func3").unwrap();
|
||||||
|
assert_eq!(func3.decorators.len(), 0);
|
||||||
|
assert!(func3.body.contains("深度嵌套"));
|
||||||
|
|
||||||
|
// 验证函数4
|
||||||
|
let final_func = functions.iter().find(|f| f.name == "final_func").unwrap();
|
||||||
|
assert_eq!(final_func.decorators.len(), 1);
|
||||||
|
assert!(final_func.body.contains("最后函数"));
|
||||||
|
|
||||||
|
// 确保无效函数没有被解析
|
||||||
|
assert!(functions.iter().find(|f| f.name == "another_invalid-function").is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
use topological_sort::TopologicalSort;
|
||||||
|
use crate::{decorator::Decorator, parser::Function};
|
||||||
|
|
||||||
|
fn sort_function(functions: Vec<Function>) -> Vec<Function> {
|
||||||
|
let mut ts = TopologicalSort::<String>::new();
|
||||||
|
|
||||||
|
for function in &functions {
|
||||||
|
ts.insert(function.name.clone());
|
||||||
|
for decorator in &function.decorators{
|
||||||
|
if let Decorator::After(dependency)=decorator {
|
||||||
|
ts.add_dependency(dependency, function.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut functions_sorted = Vec::<Function>::new();
|
||||||
|
while let Some(function_name) = ts.pop() {
|
||||||
|
let function = functions.iter().find(|f| f.name == function_name).unwrap();
|
||||||
|
functions_sorted.push(function.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
functions_sorted
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn create_function(name: &str, decorators: Vec<Decorator>, body: &str) -> Function {
|
||||||
|
Function {
|
||||||
|
name: name.to_string(),
|
||||||
|
decorators,
|
||||||
|
body: body.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_functions() {
|
||||||
|
let functions = vec![];
|
||||||
|
let result = sort_function(functions);
|
||||||
|
assert!(result.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_single_function_no_dependencies() {
|
||||||
|
let function = create_function("func1", vec![], "body1");
|
||||||
|
let functions = vec![function.clone()];
|
||||||
|
|
||||||
|
let result = sort_function(functions);
|
||||||
|
|
||||||
|
assert_eq!(result.len(), 1);
|
||||||
|
assert_eq!(result[0].name, "func1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiple_functions_no_dependencies() {
|
||||||
|
let function1 = create_function("func1", vec![], "body1");
|
||||||
|
let function2 = create_function("func2", vec![], "body2");
|
||||||
|
let function3 = create_function("func3", vec![], "body3");
|
||||||
|
let functions = vec![function1.clone(), function2.clone(), function3.clone()];
|
||||||
|
|
||||||
|
let result = sort_function(functions);
|
||||||
|
|
||||||
|
assert_eq!(result.len(), 3);
|
||||||
|
// 所有函数都应该在结果中,顺序不重要因为没有依赖
|
||||||
|
let names: Vec<String> = result.iter().map(|f| f.name.clone()).collect();
|
||||||
|
assert!(names.contains(&"func1".to_string()));
|
||||||
|
assert!(names.contains(&"func2".to_string()));
|
||||||
|
assert!(names.contains(&"func3".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple_dependency() {
|
||||||
|
let function1 = create_function("func1", vec![], "body1");
|
||||||
|
let function2 = create_function("func2", vec![Decorator::After("func1".to_string())], "body2");
|
||||||
|
let functions = vec![function1.clone(), function2.clone()];
|
||||||
|
|
||||||
|
let result = sort_function(functions);
|
||||||
|
|
||||||
|
assert_eq!(result.len(), 2);
|
||||||
|
assert_eq!(result[0].name, "func1");
|
||||||
|
assert_eq!(result[1].name, "func2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiple_dependencies() {
|
||||||
|
let function1 = create_function("func1", vec![], "body1");
|
||||||
|
let function2 = create_function("func2", vec![Decorator::After("func1".to_string())], "body2");
|
||||||
|
let function3 = create_function("func3", vec![Decorator::After("func2".to_string())], "body3");
|
||||||
|
let functions = vec![function1.clone(), function2.clone(), function3.clone()];
|
||||||
|
|
||||||
|
let result = sort_function(functions);
|
||||||
|
|
||||||
|
assert_eq!(result.len(), 3);
|
||||||
|
assert_eq!(result[0].name, "func1");
|
||||||
|
assert_eq!(result[1].name, "func2");
|
||||||
|
assert_eq!(result[2].name, "func3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_complex_dependencies() {
|
||||||
|
let function1 = create_function("func1", vec![], "body1");
|
||||||
|
let function2 = create_function("func2", vec![
|
||||||
|
Decorator::After("func1".to_string()),
|
||||||
|
Decorator::After("func3".to_string()),
|
||||||
|
], "body2");
|
||||||
|
let function3 = create_function("func3", vec![Decorator::After("func1".to_string())], "body3");
|
||||||
|
let function4 = create_function("func4", vec![], "body4");
|
||||||
|
let functions = vec![function1.clone(), function2.clone(), function3.clone(), function4.clone()];
|
||||||
|
|
||||||
|
let result = sort_function(functions);
|
||||||
|
|
||||||
|
assert_eq!(result.len(), 4);
|
||||||
|
|
||||||
|
let positions: std::collections::HashMap<_, _> = result
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, f)| (&f.name, i))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// func1 应该在 func2 和 func3 之前
|
||||||
|
assert!(positions[&"func1".to_string()] < positions[&"func2".to_string()]);
|
||||||
|
assert!(positions[&"func1".to_string()] < positions[&"func3".to_string()]);
|
||||||
|
|
||||||
|
// func3 应该在 func2 之前
|
||||||
|
assert!(positions[&"func3".to_string()] < positions[&"func2".to_string()]);
|
||||||
|
|
||||||
|
// func4 可以在任何位置,因为没有依赖关系
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mixed_decorators() {
|
||||||
|
let function1 = create_function("func1", vec![
|
||||||
|
Decorator::Pipeline,
|
||||||
|
Decorator::Timeout(1000),
|
||||||
|
], "body1");
|
||||||
|
let function2 = create_function("func2", vec![
|
||||||
|
Decorator::After("func1".to_string()),
|
||||||
|
Decorator::Retry(3, 100),
|
||||||
|
Decorator::Fallible,
|
||||||
|
], "body2");
|
||||||
|
let functions = vec![function1.clone(), function2.clone()];
|
||||||
|
|
||||||
|
let result = sort_function(functions);
|
||||||
|
|
||||||
|
assert_eq!(result.len(), 2);
|
||||||
|
assert_eq!(result[0].name, "func1");
|
||||||
|
assert_eq!(result[1].name, "func2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_function_with_multiple_after_decorators() {
|
||||||
|
let function1 = create_function("func1", vec![], "body1");
|
||||||
|
let function2 = create_function("func2", vec![], "body2");
|
||||||
|
let function3 = create_function("func3", vec![
|
||||||
|
Decorator::After("func1".to_string()),
|
||||||
|
Decorator::After("func2".to_string()),
|
||||||
|
], "body3");
|
||||||
|
let functions = vec![function1.clone(), function2.clone(), function3.clone()];
|
||||||
|
|
||||||
|
let result = sort_function(functions);
|
||||||
|
|
||||||
|
assert_eq!(result.len(), 3);
|
||||||
|
|
||||||
|
let positions: std::collections::HashMap<_, _> = result
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, f)| (&f.name, i))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// func1 和 func2 都应该在 func3 之前
|
||||||
|
assert!(positions[&"func1".to_string()] < positions[&"func3".to_string()]);
|
||||||
|
assert!(positions[&"func2".to_string()] < positions[&"func3".to_string()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ignores_non_after_decorators() {
|
||||||
|
let function1 = create_function("func1", vec![
|
||||||
|
Decorator::Pipeline,
|
||||||
|
Decorator::If("condition".to_string()),
|
||||||
|
Decorator::Fallible,
|
||||||
|
], "body1");
|
||||||
|
let function2 = create_function("func2", vec![
|
||||||
|
Decorator::Loop(5),
|
||||||
|
Decorator::Timeout(1000),
|
||||||
|
Decorator::Retry(3, 100),
|
||||||
|
Decorator::Export,
|
||||||
|
], "body2");
|
||||||
|
let functions = vec![function1.clone(), function2.clone()];
|
||||||
|
|
||||||
|
let result = sort_function(functions);
|
||||||
|
|
||||||
|
assert_eq!(result.len(), 2);
|
||||||
|
// 由于没有 After 装饰器,两个函数都应该在结果中,顺序不重要
|
||||||
|
let names: Vec<String> = result.iter().map(|f| f.name.clone()).collect();
|
||||||
|
assert!(names.contains(&"func1".to_string()));
|
||||||
|
assert!(names.contains(&"func2".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use regex::Regex;
|
||||||
|
pub struct Variable{
|
||||||
|
pub name: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref VAR_REGEX: Regex = Regex::new(r#"\$(?:\{(?:#)?([a-zA-Z_][a-zA-Z0-9_]*)(?:\[(?:\d+|@|\*)?\])?(?:\s*:\s*[+-]?\d+(?::[+-]?\d+)?)?\}|\b([a-zA-Z_][a-zA-Z0-9_]*)\b)"#)
|
||||||
|
.expect("Failed to compile variable regex");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_variables(body: String) -> Vec<Variable> {
|
||||||
|
let mut variables = HashSet::<String>::new();
|
||||||
|
let mut variable_list = Vec::<Variable>::new();
|
||||||
|
|
||||||
|
for captures in VAR_REGEX.captures_iter(&body) {
|
||||||
|
if let Some(name) = captures.get(1) {
|
||||||
|
dbg!(name);
|
||||||
|
variables.insert(name.as_str().to_string());
|
||||||
|
} else if let Some(name) = captures.get(2) {
|
||||||
|
dbg!(name);
|
||||||
|
variables.insert(name.as_str().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for variable in variables {
|
||||||
|
let variable_element = Variable{
|
||||||
|
name: variable.clone(),
|
||||||
|
value: Default::default(), // TODO: 换成workshop-agent通信
|
||||||
|
};
|
||||||
|
variable_list.push(variable_element);
|
||||||
|
}
|
||||||
|
|
||||||
|
variable_list
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单元测试模块
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
/// 辅助函数:将Variable列表转换为排序后的字符串集合(解决HashSet无序问题)
|
||||||
|
fn sorted_var_names(vars: &[Variable]) -> BTreeSet<String> {
|
||||||
|
vars.iter()
|
||||||
|
.map(|v| v.name.clone())
|
||||||
|
.collect::<BTreeSet<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试1:基础场景(重复变量去重 + 多种合法格式)
|
||||||
|
#[test]
|
||||||
|
fn test_extract_duplicate_variables() {
|
||||||
|
let script = r#"
|
||||||
|
echo $var
|
||||||
|
echo ${var}
|
||||||
|
echo ${#var}
|
||||||
|
echo ${var:3:5}
|
||||||
|
echo ${var: -3}
|
||||||
|
echo ${var[0]}
|
||||||
|
echo ${var[@]}
|
||||||
|
echo ${var[*]}
|
||||||
|
echo $_var123
|
||||||
|
echo ${_var123: -5:10}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let variables = extract_variables(script.to_string());
|
||||||
|
let sorted_names = sorted_var_names(&variables);
|
||||||
|
|
||||||
|
// 预期结果:去重后仅保留var和_var123
|
||||||
|
let expected = BTreeSet::from(["var".to_string(), "_var123".to_string()]);
|
||||||
|
assert_eq!(sorted_names, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试2:非法变量(数字开头、特殊字符、$1/$?等)
|
||||||
|
#[test]
|
||||||
|
fn test_extract_illegal_variables() {
|
||||||
|
let script = r#"
|
||||||
|
echo $1 # 数字开头,不匹配
|
||||||
|
echo $? # 特殊符号,不匹配
|
||||||
|
echo $@ # 特殊符号,不匹配
|
||||||
|
echo ${123var} # 数字开头,不匹配
|
||||||
|
echo ${var-with-dash} # 含非法字符,不匹配
|
||||||
|
echo ${var+123} # 含非法字符,不匹配
|
||||||
|
echo ${} # 空变量,不匹配
|
||||||
|
echo $ # 无变量名,不匹配
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let variables = extract_variables(script.to_string());
|
||||||
|
assert!(variables.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试3:空输入
|
||||||
|
#[test]
|
||||||
|
fn test_extract_empty_body() {
|
||||||
|
let variables = extract_variables("".to_string());
|
||||||
|
assert!(variables.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试4:无合法变量的输入
|
||||||
|
#[test]
|
||||||
|
fn test_extract_no_valid_variables() {
|
||||||
|
let script = r#"
|
||||||
|
echo "hello world"
|
||||||
|
cd /tmp
|
||||||
|
ls -l
|
||||||
|
echo $123abc # 数字开头,不匹配
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let variables = extract_variables(script.to_string());
|
||||||
|
assert!(variables.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试5:混合合法/非法变量(仅保留合法项)
|
||||||
|
#[test]
|
||||||
|
fn test_extract_mixed_variables() {
|
||||||
|
let script = r#"
|
||||||
|
echo $APP_NAME
|
||||||
|
echo ${DEPLOY_ENV}
|
||||||
|
echo ${#PORT}
|
||||||
|
echo $1 # 非法
|
||||||
|
echo ${invalid-var} # 非法
|
||||||
|
echo ${_DB_PASSWORD: 5}
|
||||||
|
echo $? # 非法
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let variables = extract_variables(script.to_string());
|
||||||
|
let sorted_names = sorted_var_names(&variables);
|
||||||
|
|
||||||
|
let expected = BTreeSet::from([
|
||||||
|
"APP_NAME".to_string(),
|
||||||
|
"DEPLOY_ENV".to_string(),
|
||||||
|
"PORT".to_string(),
|
||||||
|
"_DB_PASSWORD".to_string(),
|
||||||
|
]);
|
||||||
|
assert_eq!(sorted_names, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试6:变量名包含数字(合法场景)
|
||||||
|
#[test]
|
||||||
|
fn test_extract_var_with_numbers() {
|
||||||
|
let script = r#"
|
||||||
|
echo $var123
|
||||||
|
echo ${var_456}
|
||||||
|
echo ${#var789: 10}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let variables = extract_variables(script.to_string());
|
||||||
|
let sorted_names = sorted_var_names(&variables);
|
||||||
|
|
||||||
|
let expected = BTreeSet::from([
|
||||||
|
"var123".to_string(),
|
||||||
|
"var_456".to_string(),
|
||||||
|
"var789".to_string(),
|
||||||
|
]);
|
||||||
|
assert_eq!(sorted_names, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+169
@@ -0,0 +1,169 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "if-addrs"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf39cc0423ee66021dc5eccface85580e4a001e0c5288bae8bea7ecb69225e90"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.177"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "macaddr"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "route-rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d66fa49dc79c685a59fa33339e126a794a67cf4ed865c2c9785268e23ec51409"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.111"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "workshop-helper-mac"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"if-addrs",
|
||||||
|
"macaddr",
|
||||||
|
"route-rs",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "workshop-helper-mac"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
if-addrs = "0.14.0"
|
||||||
|
macaddr = "1.0.1"
|
||||||
|
route-rs = "0.1.0"
|
||||||
|
thiserror = "2.0.17"
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
use if_addrs::IfAddr;
|
||||||
|
use macaddr::MacAddr;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
// 定义一个自定义的错误类型,让错误处理更优雅
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
enum MainMacError {
|
||||||
|
#[error("无法获取网络接口列表: {0}")]
|
||||||
|
IfAddrsError(#[from] if_addrs::Error),
|
||||||
|
|
||||||
|
#[error("无法获取路由信息: {0}")]
|
||||||
|
RouteError(#[from] route_rs::Error),
|
||||||
|
|
||||||
|
#[error("未找到默认路由")]
|
||||||
|
NoDefaultRoute,
|
||||||
|
|
||||||
|
#[error("默认路由对应的接口 '{0}' 未在接口列表中找到")]
|
||||||
|
InterfaceNotFound(String),
|
||||||
|
|
||||||
|
#[error("接口 '{0}' 没有 MAC 地址")]
|
||||||
|
NoMacAddress(String),
|
||||||
|
|
||||||
|
#[error("找到的主网卡 '{0}' 是虚拟接口,已跳过")]
|
||||||
|
VirtualInterface(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查接口名称是否暗示它是一个虚拟接口
|
||||||
|
fn is_virtual_interface(name: &str) -> bool {
|
||||||
|
let virtual_patterns = ["tun", "tap", "virbr", "veth", "lo"];
|
||||||
|
virtual_patterns.iter().any(|&pat| name.contains(pat))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 寻找并返回主网卡的 MAC 地址
|
||||||
|
fn find_main_mac_address() -> Result<MacAddr, MainMacError> {
|
||||||
|
// 步骤 1: 获取默认路由,以确定主网卡的名称
|
||||||
|
let default_route = route_rs::get_default_route()?;
|
||||||
|
let iface_name = default_route.interface_name().ok_or(MainMacError::NoDefaultRoute)?;
|
||||||
|
println!("默认路由所在的接口名称: '{}'", iface_name);
|
||||||
|
|
||||||
|
// 步骤 2: 检查是否为虚拟接口
|
||||||
|
if is_virtual_interface(iface_name) {
|
||||||
|
return Err(MainMacError::VirtualInterface(iface_name.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤 3: 获取所有网络接口的详细信息
|
||||||
|
let if_addrs = if_addrs::get_if_addrs()?;
|
||||||
|
|
||||||
|
// 步骤 4: 遍历接口列表,找到与默认路由接口名匹配的接口
|
||||||
|
for if_addr in if_addrs {
|
||||||
|
if if_addr.name == iface_name {
|
||||||
|
// 找到了匹配的接口,提取其 MAC 地址
|
||||||
|
return if_addr.mac_addr.ok_or_else(|| MainMacError::NoMacAddress(iface_name.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果循环结束都没有找到,说明出了问题
|
||||||
|
Err(MainMacError::InterfaceNotFound(iface_name.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("正在寻找主网卡的 MAC 地址...");
|
||||||
|
|
||||||
|
match find_main_mac_address() {
|
||||||
|
Ok(mac) => println!("成功找到主网卡 MAC 地址: {}", mac),
|
||||||
|
Err(e) => eprintln!("失败: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,46 +1,9 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# .workshop/bake
|
# .workshop/bake.sh
|
||||||
# 构建阶段脚本 - 使用标准的bash语法
|
# 构建阶段脚本 - 使用标准的bash语法
|
||||||
|
|
||||||
set -euo pipefail # 严格模式:错误退出、未定义变量检查、管道错误检查
|
set -euo pipefail # 严格模式:错误退出、未定义变量检查、管道错误检查
|
||||||
|
|
||||||
# 颜色定义用于输出
|
|
||||||
readonly RED='\033[0;31m'
|
|
||||||
readonly GREEN='\033[0;32m'
|
|
||||||
readonly YELLOW='\033[1;33m'
|
|
||||||
readonly BLUE='\033[0;34m'
|
|
||||||
readonly NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# 日志函数
|
|
||||||
log() {
|
|
||||||
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $*${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_warn() {
|
|
||||||
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_error() {
|
|
||||||
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_info() {
|
|
||||||
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 环境变量(由CI/CD系统注入)
|
|
||||||
readonly WORKSPACE_DIR="${WORKSPACE_DIR:-/workspace}"
|
|
||||||
readonly BUILD_ID="${BUILD_ID:-unknown}"
|
|
||||||
readonly COMMIT_SHA="${COMMIT_SHA:-}"
|
|
||||||
readonly BRANCH_NAME="${BRANCH_NAME:-main}"
|
|
||||||
readonly BUILD_CACHE_DIR="${BUILD_CACHE_DIR:-/cache}"
|
|
||||||
|
|
||||||
# 构建配置(从prebake和环境变量读取)
|
|
||||||
readonly RUST_VERSION="${RUST_VERSION:-1.70}"
|
|
||||||
readonly BUILD_TYPE="${BUILD_TYPE:-release}"
|
|
||||||
readonly CARGO_REGISTRY="${CARGO_REGISTRY:-$BUILD_CACHE_DIR/cargo/registry}"
|
|
||||||
readonly CARGO_GIT="${CARGO_GIT:-$BUILD_CACHE_DIR/cargo/git}"
|
|
||||||
|
|
||||||
# 切换到工作目录
|
# 切换到工作目录
|
||||||
cd "$WORKSPACE_DIR"
|
cd "$WORKSPACE_DIR"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# 颜色定义用于输出
|
||||||
|
readonly RED='\033[0;31m'
|
||||||
|
readonly GREEN='\033[0;32m'
|
||||||
|
readonly YELLOW='\033[1;33m'
|
||||||
|
readonly BLUE='\033[0;34m'
|
||||||
|
readonly NC='\033[0m' # 无颜色
|
||||||
|
|
||||||
|
# 日志函数
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $*${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info() {
|
||||||
|
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 读取环境变量
|
||||||
|
source .env
|
||||||
|
# 执行bake步骤
|
||||||
|
source bake.sh
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# .workshop/finalize.yaml
|
# finalize.yaml.tmpl
|
||||||
# 版本号
|
# 版本号
|
||||||
version: "1.0"
|
version: "1.0"
|
||||||
|
|
||||||
@@ -112,27 +112,6 @@ distribution:
|
|||||||
# 质量门禁
|
# 质量门禁
|
||||||
quality_gates:
|
quality_gates:
|
||||||
# 测试覆盖率要求
|
# 测试覆盖率要求
|
||||||
test_coverage:
|
|
||||||
enabled: true
|
|
||||||
minimum: 80.0
|
|
||||||
file: "coverage/lcov.info"
|
|
||||||
|
|
||||||
# 安全扫描
|
|
||||||
security_scan:
|
|
||||||
enabled: true
|
|
||||||
tools:
|
|
||||||
- "cargo-audit"
|
|
||||||
- "trivy"
|
|
||||||
fail_on: "high" # low, medium, high, critical
|
|
||||||
exceptions:
|
|
||||||
- "CVE-2021-12345" # 已知例外
|
|
||||||
|
|
||||||
# 代码质量
|
|
||||||
code_quality:
|
|
||||||
enabled: true
|
|
||||||
maximum_duplication: 5.0
|
|
||||||
maximum_complexity: 25.0
|
|
||||||
minimum_maintainability: 50.0
|
|
||||||
|
|
||||||
# 部署配置
|
# 部署配置
|
||||||
deployment:
|
deployment:
|
||||||
@@ -156,6 +135,8 @@ deployment:
|
|||||||
cluster: "production-cluster"
|
cluster: "production-cluster"
|
||||||
manual_approval: true
|
manual_approval: true
|
||||||
rollout_strategy: "blue-green"
|
rollout_strategy: "blue-green"
|
||||||
|
manifests:
|
||||||
|
- "k8s/deployment.yaml"
|
||||||
|
|
||||||
# 部署策略
|
# 部署策略
|
||||||
strategy:
|
strategy:
|
||||||
@@ -177,6 +158,7 @@ notifications:
|
|||||||
to:
|
to:
|
||||||
- "team@example.com"
|
- "team@example.com"
|
||||||
subject: "Build Success: {{.project_name}} v{{.version}}"
|
subject: "Build Success: {{.project_name}} v{{.version}}"
|
||||||
|
message: "Build {{.build_id}} succeeded for {{.project_name}} v{{.version}}"
|
||||||
template: "success_email.html"
|
template: "success_email.html"
|
||||||
|
|
||||||
# 失败通知
|
# 失败通知
|
||||||
@@ -188,6 +170,7 @@ notifications:
|
|||||||
|
|
||||||
- type: "pagerduty"
|
- type: "pagerduty"
|
||||||
service_key: "{{env.PAGERDUTY_KEY}}"
|
service_key: "{{env.PAGERDUTY_KEY}}"
|
||||||
|
message: "Build {{.build_id}} failed for {{.project_name}} v{{.version}}"
|
||||||
severity: "error"
|
severity: "error"
|
||||||
|
|
||||||
# 部署通知
|
# 部署通知
|
||||||
@@ -197,6 +180,7 @@ notifications:
|
|||||||
method: "POST"
|
method: "POST"
|
||||||
headers:
|
headers:
|
||||||
Authorization: "Bearer {{env.DEPLOY_WEBHOOK_TOKEN}}"
|
Authorization: "Bearer {{env.DEPLOY_WEBHOOK_TOKEN}}"
|
||||||
|
message: ""
|
||||||
|
|
||||||
# 清理策略
|
# 清理策略
|
||||||
cleanup:
|
cleanup:
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
# .workshop/prebake.yaml
|
# prebake.yaml.tmpl
|
||||||
|
#
|
||||||
|
# 术语说明:
|
||||||
|
# 可选:该字段可以不给出
|
||||||
|
# 默认为...:隐含“可选”
|
||||||
|
# 留空:尚未定义,留作后续
|
||||||
|
|
||||||
# 版本号,用于格式兼容性检查
|
# 版本号,用于格式兼容性检查
|
||||||
version: "1.0"
|
version: "1.0"
|
||||||
|
|
||||||
@@ -7,94 +13,97 @@ environment:
|
|||||||
# 构建机类型
|
# 构建机类型
|
||||||
builder: "docker" # 或 "firecracker", "custom", "baremetal"
|
builder: "docker" # 或 "firecracker", "custom", "baremetal"
|
||||||
|
|
||||||
# 构建机配置
|
# 构建机配置,注意以下只会出现与上述构建机类型对应的组。
|
||||||
config:
|
config:
|
||||||
baremetal:
|
# baremetal:
|
||||||
memory_mb: 2048
|
# 不需要
|
||||||
cpus: 2
|
|
||||||
arch: x86_64
|
|
||||||
|
|
||||||
# Docker 构建机配置
|
# docker:
|
||||||
docker:
|
# 镜像名
|
||||||
base_image: "rust:1.70-slim"
|
image: "rust:1.70-slim"
|
||||||
dockerfile: |
|
# Dockerfile路径,相对于项目根目录,不能与镜像一同指定,两者必须指定其一
|
||||||
FROM rust:1.70-slim
|
dockerfile: "/Dockerfile"
|
||||||
WORKDIR /app
|
# Docker构建参数,可选,只能与dockerfile搭配使用
|
||||||
RUN apt-get update && apt-get install -y pkg-config libssl-dev
|
build_args:
|
||||||
build_args:
|
- "RUST_VERSION=1.70"
|
||||||
- "RUST_VERSION=1.70"
|
|
||||||
|
|
||||||
# Firecracker 构建机配置
|
# firecracker:
|
||||||
firecracker:
|
# 留空
|
||||||
kernel_image: "file:///opt/firecracker/vmlinux"
|
|
||||||
rootfs_image: "file:///opt/firecracker/rootfs.ext4"
|
|
||||||
memory_mb: 1024
|
|
||||||
vcpus: 2
|
|
||||||
boot_args: "console=ttyS0 reboot=k panic=1 pci=off"
|
|
||||||
|
|
||||||
# 自定义构建机配置
|
# custom:
|
||||||
custom:
|
# 自定义构建机名称
|
||||||
name: "my-custom-builder"
|
name: "my-custom-builder"
|
||||||
setup_script: "setup-builder.sh"
|
# 设置脚本,相对于项目根目录
|
||||||
cleanup_script: "cleanup-builder.sh"
|
setup_script: "setup-builder.sh"
|
||||||
|
# 清洁脚本,相对于项目根目录
|
||||||
|
cleanup_script: "cleanup-builder.sh"
|
||||||
|
|
||||||
# 硬件资源要求
|
# 硬件资源要求,满足最小要求的构建机可以被选中
|
||||||
resources:
|
resources:
|
||||||
cpu: 2 # CPU核心数
|
cpu: 2 # CPU核心数,默认为1
|
||||||
memory: "2G" # 内存大小
|
memory: "2G" # 内存大小,也可以写作2048MB,默认为"1G"
|
||||||
disk: "10G" # 磁盘空间
|
disk: "10G" # 磁盘空间,也可以写作10240MB,默认为"1G"
|
||||||
architecture: "x86_64" # 架构要求
|
architecture: "x86_64" # 架构要求,只能是"noarch"或{"x86_64"/"amd64", "arm64"/"aarch64", "riscv64", "loongarch64"/"loong64"}中的一个或几个
|
||||||
gpu: false # 是否需要GPU
|
gpu: false # 是否需要GPU,默认为false
|
||||||
gpu_type: "cuda" # GPU类型(如果需要)
|
gpu_type: "cuda" # GPU类型(如果需要),可以是"cuda", "rocm", "vulkan", "oneapi"中的一个或几个
|
||||||
|
tags: # 自定义标签,只有满足标签的构建机可以被选中
|
||||||
|
- "custom_tag"
|
||||||
|
|
||||||
# 网络配置
|
# 网络配置
|
||||||
network:
|
network:
|
||||||
enabled: true
|
enabled: true # 启用网络,对baremetal不适用,默认为true
|
||||||
outbound: true # 允许出站连接
|
outbound: true # 允许出站连接,对baremetal不适用,默认为true
|
||||||
dns_servers:
|
dns_servers: # 指定构建机的DNS服务器,对baremetal不适用,custom应该自行处理DNS问题,可选
|
||||||
- "8.8.8.8"
|
- "8.8.8.8"
|
||||||
- "1.1.1.1"
|
- "1.1.1.1"
|
||||||
|
proxies: # 代理配置,暂时留空,可选
|
||||||
|
|
||||||
# 依赖定义
|
# 依赖定义,安装依赖时按custom_pre-system-languages-custom顺序进行
|
||||||
dependencies:
|
dependencies:
|
||||||
# 系统包依赖
|
# 系统包依赖,按软件包类型给出或any通配,若存在发行版则选择之,否则选中any,也没有则报警告但继续
|
||||||
system:
|
system:
|
||||||
packages:
|
deb:
|
||||||
- "git"
|
packages: # 安装的软件包
|
||||||
- "curl"
|
- "git"
|
||||||
- "build-essential"
|
- "curl"
|
||||||
- "pkg-config"
|
- "build-essential"
|
||||||
- "libssl-dev"
|
- "pkg-config"
|
||||||
repositories:
|
- "libssl-dev"
|
||||||
- "ppa:custom/ppa" # 自定义PPA(Ubuntu)
|
repositories:
|
||||||
|
- "ppa:custom/ppa" # 自定义PPA(Ubuntu)
|
||||||
|
mirror: "https://mirrors.tuna.tsinghua.edu.cn" # 指定软件包镜像源,可选
|
||||||
|
pacman:
|
||||||
|
packages:
|
||||||
|
- "git"
|
||||||
|
- "curl"
|
||||||
|
- "base-devel"
|
||||||
|
- "pkg-config"
|
||||||
|
- "openssl"
|
||||||
|
repositories:
|
||||||
|
- "https://mirrors.tuna.tsinghua.edu.cn/arch4edu/$arch" # 自定义仓库(Arch)
|
||||||
|
mirror: "https://mirrors.tuna.tsinghua.edu.cn" # 指定软件包镜像源,可选
|
||||||
|
rpm:
|
||||||
|
# 略
|
||||||
|
|
||||||
# 语言特定依赖
|
# 语言特定依赖,可选,此处定义不稳定
|
||||||
languages:
|
languages:
|
||||||
rust:
|
rust:
|
||||||
version: "1.70"
|
toolchain: "stable-x86_64-unknown-linux-gnu"
|
||||||
toolchain: "stable"
|
|
||||||
components:
|
|
||||||
- "rustfmt"
|
|
||||||
- "clippy"
|
|
||||||
targets:
|
|
||||||
- "x86_64-unknown-linux-gnu"
|
|
||||||
|
|
||||||
node:
|
# 自定义依赖步骤,可选
|
||||||
version: "18"
|
|
||||||
package_manager: "npm" # 或 "yarn", "pnpm"
|
|
||||||
registry: "https://registry.npmjs.org/"
|
|
||||||
|
|
||||||
python:
|
|
||||||
version: "3.11"
|
|
||||||
package_manager: "pip" # 或 "poetry", "conda"
|
|
||||||
requirements_file: "requirements.txt"
|
|
||||||
|
|
||||||
go:
|
|
||||||
version: "1.19"
|
|
||||||
modules: true
|
|
||||||
|
|
||||||
# 自定义依赖步骤
|
|
||||||
custom:
|
custom:
|
||||||
|
- name: "install-custom-tool" # 名称
|
||||||
|
command: "curl -fsSL https://example.com/install.sh | sh" # 命令
|
||||||
|
environment: # 环境变量
|
||||||
|
CUSTOM_VAR: "value"
|
||||||
|
|
||||||
|
- name: "build-native-dep"
|
||||||
|
command: "make && make install"
|
||||||
|
working_dir: "native-deps" # 工作目录,基于项目根目录
|
||||||
|
timeout: 300 # 超时,默认为300秒
|
||||||
|
|
||||||
|
# 自定义依赖步骤,但是在依赖安装时最先进行,可选
|
||||||
|
custom_pre:
|
||||||
- name: "install-custom-tool"
|
- name: "install-custom-tool"
|
||||||
command: "curl -fsSL https://example.com/install.sh | sh"
|
command: "curl -fsSL https://example.com/install.sh | sh"
|
||||||
environment:
|
environment:
|
||||||
@@ -107,32 +116,33 @@ dependencies:
|
|||||||
|
|
||||||
# 环境变量配置
|
# 环境变量配置
|
||||||
environment_variables:
|
environment_variables:
|
||||||
# 构建环境变量
|
# 构建环境变量,只在prebake阶段有效
|
||||||
build:
|
prebake:
|
||||||
RUST_BACKTRACE: "full"
|
RUST_BACKTRACE: "full"
|
||||||
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
|
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
|
||||||
NODE_ENV: "production"
|
NODE_ENV: "production"
|
||||||
|
|
||||||
# 运行时环境变量(会传递给bake阶段)
|
# 运行时环境变量(会传递给bake阶段,与该阶段获取的环境一并作为.env保存)
|
||||||
runtime:
|
bake:
|
||||||
DATABASE_URL: "postgresql://user:pass@localhost/db"
|
DATABASE_URL: "postgresql://user:pass@localhost/db"
|
||||||
LOG_LEVEL: "info"
|
LOG_LEVEL: "info"
|
||||||
|
__ENV_FILE: ".env" # 特殊变量,指定项目中.env的位置并合并其中的值。该值本身不传入bake阶段
|
||||||
|
|
||||||
# 缓存配置
|
# 缓存配置
|
||||||
cache:
|
cache:
|
||||||
# 缓存目录
|
# 缓存目录
|
||||||
directories:
|
directories:
|
||||||
- path: "/usr/local/cargo"
|
- path: "/usr/local/cargo"
|
||||||
strategy: "persistent" # persistent, build, session
|
strategy: "always" # 在"always","readonly","clean"和"never"中选择一个,可以被流水线启动时配置覆盖,默认为"always"
|
||||||
compression: "zstd"
|
mode: "zstd" # 在"gzip","zstd","none"和"dir"中选择一个
|
||||||
|
|
||||||
- path: "/root/.npm"
|
- path: "/root/.npm"
|
||||||
strategy: "build"
|
strategy: "clean"
|
||||||
compression: "none"
|
mode: "none"
|
||||||
|
|
||||||
- path: "/app/target"
|
- path: "/app/target"
|
||||||
strategy: "persistent"
|
strategy: "always"
|
||||||
compression: "zstd"
|
mode: "dir"
|
||||||
|
|
||||||
# 缓存策略
|
# 缓存策略
|
||||||
strategy:
|
strategy:
|
||||||
@@ -142,20 +152,7 @@ cache:
|
|||||||
|
|
||||||
# 安全配置
|
# 安全配置
|
||||||
security:
|
security:
|
||||||
# 权限限制
|
# 留空
|
||||||
capabilities:
|
|
||||||
- "NET_BIND_SERVICE"
|
|
||||||
|
|
||||||
# 安全策略
|
|
||||||
seccomp: "strict" # strict, filter, disabled
|
|
||||||
apparmor: true
|
|
||||||
no_new_privileges: true
|
|
||||||
|
|
||||||
# 资源限制
|
|
||||||
limits:
|
|
||||||
max_processes: 512
|
|
||||||
max_file_size: "1G"
|
|
||||||
max_open_files: 1024
|
|
||||||
|
|
||||||
# 钩子脚本
|
# 钩子脚本
|
||||||
hooks:
|
hooks:
|
||||||
|
|||||||
@@ -5,75 +5,3 @@ pub mod scripts;
|
|||||||
pub use prebake::*;
|
pub use prebake::*;
|
||||||
pub use finalize::*;
|
pub use finalize::*;
|
||||||
pub use scripts::*;
|
pub use scripts::*;
|
||||||
// use log::info;
|
|
||||||
|
|
||||||
// use crate::TemplateResolvable;
|
|
||||||
|
|
||||||
// 完整的流水线配置
|
|
||||||
// #[derive(Debug, Clone, serde::Serialize)]
|
|
||||||
// pub struct PipelineConfig {
|
|
||||||
// pub prebake: PrebakeConfig,
|
|
||||||
// pub bake: BakeScript,
|
|
||||||
// pub finalize: FinalizeConfig,
|
|
||||||
// pub variables: std::collections::HashMap<String, crate::variable::VariableValue>,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl PipelineConfig {
|
|
||||||
// /// 从文件系统加载完整配置
|
|
||||||
// pub fn load_from_dir<P: AsRef<std::path::Path>>(
|
|
||||||
// dir: P,
|
|
||||||
// user_id: &str,
|
|
||||||
// pipeline_id: &str,
|
|
||||||
// variable_provider: &dyn crate::variable::VariableProvider,
|
|
||||||
// ) -> crate::error::PipelineResult<Self> {
|
|
||||||
// let dir = dir.as_ref();
|
|
||||||
|
|
||||||
// info!("Reading prebake.yaml...");
|
|
||||||
// let prebake_path = dir.join(".workshop/prebake.yaml");
|
|
||||||
// info!("Reading bake.sh...");
|
|
||||||
// let bake_path = dir.join(".workshop/bake.sh");
|
|
||||||
// info!("Reading finalize.yaml...");
|
|
||||||
// let finalize_path = dir.join(".workshop/finalize.yaml");
|
|
||||||
|
|
||||||
// let mut variable_resolver = crate::variable::VariableResolver::new(
|
|
||||||
// user_id,
|
|
||||||
// pipeline_id,
|
|
||||||
// variable_provider
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // 加载并处理prebake配置
|
|
||||||
// let prebake_content = std::fs::read_to_string(&prebake_path)?;
|
|
||||||
// let prebake: PrebakeConfig = serde_yaml::from_str(&prebake_content)?;
|
|
||||||
// // let prebake = variable_resolver.resolve_prebake(prebake)?;
|
|
||||||
// let prebake = prebake.resolve_template(&mut variable_resolver)?;
|
|
||||||
|
|
||||||
// // 加载bake脚本
|
|
||||||
// let bake_content = std::fs::read_to_string(&bake_path)?;
|
|
||||||
// let bake = BakeScript::new(bake_content);
|
|
||||||
|
|
||||||
// // 加载并处理finalize配置
|
|
||||||
// let finalize_content = std::fs::read_to_string(&finalize_path)?;
|
|
||||||
// let finalize: FinalizeConfig = serde_yaml::from_str(&finalize_content)?;
|
|
||||||
// // let finalize = variable_resolver.resolve_finalize(finalize)?;
|
|
||||||
// let finalize = finalize.resolve_template(&mut variable_resolver)?;
|
|
||||||
|
|
||||||
// Ok(Self {
|
|
||||||
// prebake,
|
|
||||||
// bake,
|
|
||||||
// finalize,
|
|
||||||
// variables: variable_resolver.get_resolved_variables(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// 获取用于输出的配置(遮蔽secret)
|
|
||||||
// pub fn for_output(&self) -> Self {
|
|
||||||
// Self {
|
|
||||||
// prebake: self.prebake.clone(),
|
|
||||||
// bake: self.bake.clone(),
|
|
||||||
// finalize: self.finalize.for_output(),
|
|
||||||
// variables: self.variables.iter()
|
|
||||||
// .map(|(k, v)| (k.clone(), v.for_output()))
|
|
||||||
// .collect(),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ mod dependencies;
|
|||||||
mod security;
|
mod security;
|
||||||
mod hooks;
|
mod hooks;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
pub use environment::*;
|
pub use environment::*;
|
||||||
pub use dependencies::*;
|
pub use dependencies::*;
|
||||||
pub use security::*;
|
pub use security::*;
|
||||||
@@ -23,26 +24,54 @@ pub struct PrebakeConfig {
|
|||||||
pub metadata: Option<Metadata>,
|
pub metadata: Option<Metadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 环境变量配置
|
/// 环境变量配置 - 调整阶段
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct EnvironmentVariables {
|
pub struct EnvironmentVariables {
|
||||||
pub build: Option<std::collections::HashMap<String, String>>,
|
pub prebake: Option<HashMap<String, String>>,
|
||||||
pub runtime: Option<std::collections::HashMap<String, String>>,
|
pub bake: Option<HashMap<String, String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 缓存配置
|
/// 缓存配置
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CacheConfig {
|
pub struct CacheConfig {
|
||||||
pub directories: Vec<CacheDirectory>,
|
pub directories: Vec<CacheDirectory>,
|
||||||
pub strategy: CacheStrategy,
|
pub strategy: Option<CacheStrategy>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 缓存目录配置
|
/// 缓存目录配置 - 更新字段
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CacheDirectory {
|
pub struct CacheDirectory {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub strategy: String,
|
#[serde(default = "default_cache_strategy")]
|
||||||
pub compression: String,
|
pub strategy: CacheStrategyType,
|
||||||
|
#[serde(default = "default_cache_mode")]
|
||||||
|
pub mode: CacheMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 缓存策略类型
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum CacheStrategyType {
|
||||||
|
#[serde(rename = "always")]
|
||||||
|
Always,
|
||||||
|
#[serde(rename = "readonly")]
|
||||||
|
ReadOnly,
|
||||||
|
#[serde(rename = "clean")]
|
||||||
|
Clean,
|
||||||
|
#[serde(rename = "never")]
|
||||||
|
Never,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 缓存模式
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum CacheMode {
|
||||||
|
#[serde(rename = "gzip")]
|
||||||
|
Gzip,
|
||||||
|
#[serde(rename = "zstd")]
|
||||||
|
Zstd,
|
||||||
|
#[serde(rename = "none")]
|
||||||
|
None,
|
||||||
|
#[serde(rename = "dir")]
|
||||||
|
Dir,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 缓存策略
|
/// 缓存策略
|
||||||
@@ -53,6 +82,12 @@ pub struct CacheStrategy {
|
|||||||
pub cleanup_policy: String,
|
pub cleanup_policy: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 安全配置 - 留空
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SecurityConfig {
|
||||||
|
// 根据新模板,暂时留空
|
||||||
|
}
|
||||||
|
|
||||||
/// 元数据
|
/// 元数据
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Metadata {
|
pub struct Metadata {
|
||||||
@@ -61,3 +96,12 @@ pub struct Metadata {
|
|||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
pub project_type: String,
|
pub project_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 默认值函数
|
||||||
|
fn default_cache_strategy() -> CacheStrategyType {
|
||||||
|
CacheStrategyType::Always
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_cache_mode() -> CacheMode {
|
||||||
|
CacheMode::Zstd
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,36 +1,46 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// 依赖配置
|
/// 依赖配置 - 调整顺序
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct DependenciesConfig {
|
pub struct DependenciesConfig {
|
||||||
|
pub custom_pre: Option<Vec<CustomDependency>>,
|
||||||
pub system: Option<SystemDependencies>,
|
pub system: Option<SystemDependencies>,
|
||||||
pub languages: Option<LanguageDependencies>,
|
pub languages: Option<LanguageDependencies>,
|
||||||
pub custom: Option<Vec<CustomDependency>>,
|
pub custom: Option<Vec<CustomDependency>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 系统依赖
|
/// 系统依赖 - 按发行版组织
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SystemDependencies {
|
pub struct SystemDependencies {
|
||||||
pub packages: Vec<String>,
|
pub ubuntu: Option<DistributionDeps>,
|
||||||
pub repositories: Vec<String>,
|
pub arch: Option<DistributionDeps>,
|
||||||
|
pub fedora: Option<DistributionDeps>,
|
||||||
|
// 可以添加其他发行版...
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 语言依赖
|
/// 发行版依赖
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DistributionDeps {
|
||||||
|
pub packages: Vec<String>,
|
||||||
|
pub repositories: Option<Vec<String>>,
|
||||||
|
pub mirror: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 语言依赖 - 简化结构
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LanguageDependencies {
|
pub struct LanguageDependencies {
|
||||||
pub rust: Option<RustConfig>,
|
pub rust: Option<RustConfig>,
|
||||||
|
// 其他语言暂时保持不变...
|
||||||
pub node: Option<NodeConfig>,
|
pub node: Option<NodeConfig>,
|
||||||
pub python: Option<PythonConfig>,
|
pub python: Option<PythonConfig>,
|
||||||
pub go: Option<GoConfig>,
|
pub go: Option<GoConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rust配置
|
/// Rust配置 - 简化
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct RustConfig {
|
pub struct RustConfig {
|
||||||
pub version: String,
|
|
||||||
pub toolchain: String,
|
pub toolchain: String,
|
||||||
pub components: Vec<String>,
|
|
||||||
pub targets: Vec<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Node配置
|
/// Node配置
|
||||||
@@ -56,12 +66,17 @@ pub struct GoConfig {
|
|||||||
pub modules: bool,
|
pub modules: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: 更多语言配置
|
||||||
|
|
||||||
/// 自定义依赖
|
/// 自定义依赖
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CustomDependency {
|
pub struct CustomDependency {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub command: String,
|
pub command: String,
|
||||||
pub environment: Option<std::collections::HashMap<String, String>>,
|
pub environment: Option<HashMap<String, String>>,
|
||||||
pub working_dir: Option<String>,
|
pub working_dir: Option<String>,
|
||||||
pub timeout: Option<u32>,
|
#[serde(default = "default_timeout")]
|
||||||
|
pub timeout: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_timeout() -> u32 { 300 }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pub struct EnvironmentConfig {
|
|||||||
pub network: NetworkConfig,
|
pub network: NetworkConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建器配置
|
/// 构建器配置 - 现在各个配置是互斥的
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct BuilderConfig {
|
pub struct BuilderConfig {
|
||||||
pub baremetal: Option<BaremetalConfig>,
|
pub baremetal: Option<BaremetalConfig>,
|
||||||
@@ -18,30 +18,27 @@ pub struct BuilderConfig {
|
|||||||
pub custom: Option<CustomBuilderConfig>,
|
pub custom: Option<CustomBuilderConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 裸机配置
|
/// 裸机配置 - 现在不需要具体配置
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct BaremetalConfig {
|
pub struct BaremetalConfig {
|
||||||
pub memory_mb: u32,
|
// 根据新模板,baremetal不需要配置项
|
||||||
pub cpus: u32,
|
|
||||||
pub arch: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Docker配置
|
/// Docker配置 - 更新字段
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct DockerConfig {
|
pub struct DockerConfig {
|
||||||
pub base_image: String,
|
/// 镜像名
|
||||||
pub dockerfile: String,
|
pub image: Option<String>,
|
||||||
pub build_args: Vec<String>,
|
/// Dockerfile路径,相对于项目根目录
|
||||||
|
pub dockerfile: Option<String>,
|
||||||
|
/// Docker构建参数,可选,只能与dockerfile搭配使用
|
||||||
|
pub build_args: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Firecracker配置
|
/// Firecracker配置 - 留空
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct FirecrackerConfig {
|
pub struct FirecrackerConfig {
|
||||||
pub kernel_image: String,
|
// 根据新模板,暂时留空
|
||||||
pub rootfs_image: String,
|
|
||||||
pub memory_mb: u32,
|
|
||||||
pub vcpus: u32,
|
|
||||||
pub boot_args: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 自定义构建器配置
|
/// 自定义构建器配置
|
||||||
@@ -52,21 +49,69 @@ pub struct CustomBuilderConfig {
|
|||||||
pub cleanup_script: String,
|
pub cleanup_script: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 资源需求
|
/// 资源需求 - 更新字段
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ResourceRequirements {
|
pub struct ResourceRequirements {
|
||||||
|
#[serde(default = "default_cpu")]
|
||||||
pub cpu: u32,
|
pub cpu: u32,
|
||||||
|
#[serde(default = "default_memory")]
|
||||||
pub memory: String,
|
pub memory: String,
|
||||||
|
#[serde(default = "default_disk")]
|
||||||
pub disk: String,
|
pub disk: String,
|
||||||
pub architecture: String,
|
pub architecture: Architecture,
|
||||||
|
#[serde(default)]
|
||||||
pub gpu: bool,
|
pub gpu: bool,
|
||||||
pub gpu_type: Option<String>,
|
pub gpu_type: Option<Vec<GpuType>>,
|
||||||
|
pub tags: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 网络配置
|
/// 架构要求
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum Architecture {
|
||||||
|
Single(String),
|
||||||
|
Multiple(Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Architecture {
|
||||||
|
fn default() -> Self {
|
||||||
|
Architecture::Single("x86_64".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GPU类型
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum GpuType {
|
||||||
|
#[serde(rename = "cuda")]
|
||||||
|
Cuda,
|
||||||
|
#[serde(rename = "rocm")]
|
||||||
|
Rocm,
|
||||||
|
#[serde(rename = "vulkan")]
|
||||||
|
Vulkan,
|
||||||
|
#[serde(rename = "oneapi")]
|
||||||
|
OneApi,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 网络配置 - 更新字段
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct NetworkConfig {
|
pub struct NetworkConfig {
|
||||||
|
#[serde(default = "default_network_enabled")]
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
#[serde(default = "default_outbound")]
|
||||||
pub outbound: bool,
|
pub outbound: bool,
|
||||||
pub dns_servers: Vec<String>,
|
pub dns_servers: Option<Vec<String>>,
|
||||||
|
pub proxies: Option<ProxiesConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 代理配置 - 暂时留空
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ProxiesConfig {
|
||||||
|
// 暂不定义具体字段
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认值函数
|
||||||
|
fn default_cpu() -> u32 { 1 }
|
||||||
|
fn default_memory() -> String { "1G".to_string() }
|
||||||
|
fn default_disk() -> String { "1G".to_string() }
|
||||||
|
fn default_network_enabled() -> bool { true }
|
||||||
|
fn default_outbound() -> bool { true }
|
||||||
|
|||||||
+302
-129
@@ -1,15 +1,19 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::prebake::PrebakeConfig,
|
|
||||||
config::finalize::FinalizeConfig,
|
config::finalize::FinalizeConfig,
|
||||||
variable::VariableResolver,
|
config::prebake::{
|
||||||
|
Architecture, CacheMode, CacheStrategyType, DistributionDeps, GpuType, PrebakeConfig,
|
||||||
|
SystemDependencies,
|
||||||
|
},
|
||||||
error::PipelineError,
|
error::PipelineError,
|
||||||
|
variable::VariableResolver,
|
||||||
};
|
};
|
||||||
use regex::Regex;
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
// use serde::{Deserialize, Serialize};
|
// use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref HANDLEBARS_REGEX: Regex = Regex::new(r"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}").unwrap();
|
static ref HANDLEBARS_REGEX: Regex =
|
||||||
|
Regex::new(r"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}").unwrap();
|
||||||
static ref SHELL_VAR_REGEX: Regex = Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)").unwrap();
|
static ref SHELL_VAR_REGEX: Regex = Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,6 +23,7 @@ pub trait TemplateResolvable {
|
|||||||
Self: Sized;
|
Self: Sized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 为基本类型实现模板解析
|
||||||
impl TemplateResolvable for String {
|
impl TemplateResolvable for String {
|
||||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
let mut result = self.clone();
|
let mut result = self.clone();
|
||||||
@@ -31,150 +36,318 @@ impl TemplateResolvable for String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 $VARIABLE 格式
|
// 处理$VARIABLE格式,TODO
|
||||||
// for cap in SHELL_VAR_REGEX.captures_iter(&result) {
|
|
||||||
// let var_name = &cap[1];
|
|
||||||
// if let Ok(value) = resolver.resolve_variable(var_name) {
|
|
||||||
// result = result.replace(&cap[0], &value);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 为Option<String>实现
|
||||||
|
// impl TemplateResolvable for Option<String> {
|
||||||
|
// fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
// match self {
|
||||||
|
// Some(s) => Ok(Some(s.resolve_template(resolver)?)),
|
||||||
|
// None => Ok(None),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 为Vec<String>实现
|
||||||
|
impl TemplateResolvable for Vec<String> {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for item in self {
|
||||||
|
result.push(item.resolve_template(resolver)?);
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl TemplateResolvable for Option<Vec<String>> {
|
||||||
|
// fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
// match self {
|
||||||
|
// Some(vec) => Ok(Some(vec.resolve_template(resolver)?)),
|
||||||
|
// None => Ok(None),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// 为HashMap<String, String>实现
|
||||||
|
impl TemplateResolvable for std::collections::HashMap<String, String> {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
let mut result = std::collections::HashMap::new();
|
||||||
|
for (key, value) in self {
|
||||||
|
result.insert(
|
||||||
|
key.resolve_template(resolver)?,
|
||||||
|
value.resolve_template(resolver)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: TemplateResolvable> TemplateResolvable for Option<T> {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Some(value) => Ok(Some(value.resolve_template(resolver)?)),
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为Architecture实现
|
||||||
|
impl TemplateResolvable for Architecture {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
match self {
|
||||||
|
Architecture::Single(arch) => {
|
||||||
|
Ok(Architecture::Single(arch.resolve_template(resolver)?))
|
||||||
|
}
|
||||||
|
Architecture::Multiple(archs) => {
|
||||||
|
Ok(Architecture::Multiple(archs.resolve_template(resolver)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为SystemDependencies实现
|
||||||
|
impl TemplateResolvable for SystemDependencies {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
let mut deps = self.clone();
|
||||||
|
|
||||||
|
if let Some(ref mut ubuntu) = deps.ubuntu {
|
||||||
|
*ubuntu = ubuntu.resolve_template(resolver)?;
|
||||||
|
}
|
||||||
|
if let Some(ref mut arch) = deps.arch {
|
||||||
|
*arch = arch.resolve_template(resolver)?;
|
||||||
|
}
|
||||||
|
if let Some(ref mut fedora) = deps.fedora {
|
||||||
|
*fedora = fedora.resolve_template(resolver)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(deps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为DistributionDeps实现
|
||||||
|
impl TemplateResolvable for DistributionDeps {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(DistributionDeps {
|
||||||
|
packages: self.packages.resolve_template(resolver)?,
|
||||||
|
repositories: self.repositories.resolve_template(resolver)?,
|
||||||
|
mirror: self.mirror.resolve_template(resolver)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 枚举类型不需要模板解析,直接克隆
|
||||||
|
impl TemplateResolvable for GpuType {
|
||||||
|
fn resolve_template(&self, _resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateResolvable for CacheStrategyType {
|
||||||
|
fn resolve_template(&self, _resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateResolvable for CacheMode {
|
||||||
|
fn resolve_template(&self, _resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为PrebakeConfig实现模板解析
|
||||||
impl TemplateResolvable for PrebakeConfig {
|
impl TemplateResolvable for PrebakeConfig {
|
||||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
let mut config = self.clone();
|
let mut config = self.clone();
|
||||||
|
|
||||||
|
// 解析环境配置
|
||||||
|
config.environment = config.environment.resolve_template(resolver)?;
|
||||||
|
|
||||||
|
// 解析依赖配置
|
||||||
|
if let Some(ref mut deps) = config.dependencies {
|
||||||
|
*deps = deps.resolve_template(resolver)?;
|
||||||
|
}
|
||||||
|
|
||||||
// 解析环境变量
|
// 解析环境变量
|
||||||
if let Some(ref mut env_vars) = config.environment_variables {
|
if let Some(ref mut env_vars) = config.environment_variables {
|
||||||
if let Some(ref mut build) = env_vars.build {
|
*env_vars = env_vars.resolve_template(resolver)?;
|
||||||
for (_key, value) in build.iter_mut() {
|
|
||||||
*value = value.resolve_template(resolver)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(ref mut runtime) = env_vars.runtime {
|
|
||||||
for (_key, value) in runtime.iter_mut() {
|
|
||||||
*value = value.resolve_template(resolver)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析自定义依赖
|
// 解析缓存配置
|
||||||
if let Some(ref mut deps) = config.dependencies {
|
if let Some(ref mut cache) = config.cache {
|
||||||
if let Some(ref mut custom) = deps.custom {
|
*cache = cache.resolve_template(resolver)?;
|
||||||
for dep in custom.iter_mut() {
|
|
||||||
dep.command = dep.command.resolve_template(resolver)?;
|
|
||||||
if let Some(ref mut env) = dep.environment {
|
|
||||||
for (_key, value) in env.iter_mut() {
|
|
||||||
*value = value.resolve_template(resolver)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TemplateResolvable for FinalizeConfig {
|
// 为EnvironmentConfig实现
|
||||||
|
impl TemplateResolvable for crate::config::prebake::EnvironmentConfig {
|
||||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
let mut config = self.clone();
|
Ok(Self {
|
||||||
|
builder: self.builder.resolve_template(resolver)?,
|
||||||
// 解析产物路径
|
config: self.config.resolve_template(resolver)?,
|
||||||
if let Some(ref mut artifacts) = config.artifacts {
|
resources: self.resources.resolve_template(resolver)?,
|
||||||
if let Some(ref mut primary) = artifacts.primary {
|
network: self.network.resolve_template(resolver)?,
|
||||||
for artifact in primary.iter_mut() {
|
})
|
||||||
artifact.path = artifact.path.resolve_template(resolver)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析打包配置
|
|
||||||
if let Some(ref mut packaging) = config.packaging {
|
|
||||||
if let Some(ref mut primary) = packaging.primary_package {
|
|
||||||
primary.output = primary.output.resolve_template(resolver)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref mut docker) = packaging.docker {
|
|
||||||
docker.repository = docker.repository.resolve_template(resolver)?;
|
|
||||||
for tag in docker.tags.iter_mut() {
|
|
||||||
*tag = tag.resolve_template(resolver)?;
|
|
||||||
}
|
|
||||||
for build_arg in docker.build_args.iter_mut() {
|
|
||||||
*build_arg = build_arg.resolve_template(resolver)?;
|
|
||||||
}
|
|
||||||
for (_key, value) in docker.labels.iter_mut() {
|
|
||||||
*value = value.resolve_template(resolver)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析发布配置
|
|
||||||
if let Some(ref mut distribution) = config.distribution {
|
|
||||||
if let Some(ref mut local) = distribution.local {
|
|
||||||
local.directory = local.directory.resolve_template(resolver)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref mut s3) = distribution.s3 {
|
|
||||||
s3.prefix = s3.prefix.resolve_template(resolver)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref mut registries) = distribution.package_registries {
|
|
||||||
if let Some(ref mut crates_io) = registries.crates_io {
|
|
||||||
crates_io.token = crates_io.token.resolve_template(resolver)?;
|
|
||||||
}
|
|
||||||
if let Some(ref mut npm) = registries.npm {
|
|
||||||
npm.token = npm.token.resolve_template(resolver)?;
|
|
||||||
}
|
|
||||||
if let Some(ref mut docker_registry) = registries.docker_registry {
|
|
||||||
docker_registry.username = docker_registry.username.resolve_template(resolver)?;
|
|
||||||
docker_registry.password = docker_registry.password.resolve_template(resolver)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析通知配置
|
|
||||||
if let Some(ref mut notifications) = config.notifications {
|
|
||||||
for notification in notifications.on_success.iter_mut() {
|
|
||||||
notification.message = notification.message.resolve_template(resolver)?;
|
|
||||||
if let Some(ref mut webhook) = notification.webhook {
|
|
||||||
*webhook = webhook.resolve_template(resolver)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for notification in notifications.on_failure.iter_mut() {
|
|
||||||
notification.message = notification.message.resolve_template(resolver)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FinalizeConfig {
|
|
||||||
pub fn for_output(&self) -> Self {
|
|
||||||
let mut config = self.clone();
|
|
||||||
|
|
||||||
// 遮蔽secret类型的变量
|
|
||||||
if let Some(ref mut distribution) = config.distribution {
|
|
||||||
if let Some(ref mut registries) = distribution.package_registries {
|
|
||||||
if let Some(ref mut crates_io) = registries.crates_io {
|
|
||||||
crates_io.token = "***".to_string();
|
|
||||||
}
|
|
||||||
if let Some(ref mut npm) = registries.npm {
|
|
||||||
npm.token = "***".to_string();
|
|
||||||
}
|
|
||||||
if let Some(ref mut docker_registry) = registries.docker_registry {
|
|
||||||
docker_registry.username = "***".to_string();
|
|
||||||
docker_registry.password = "***".to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 为BuilderConfig实现
|
||||||
|
impl TemplateResolvable for crate::config::prebake::BuilderConfig {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(Self {
|
||||||
|
baremetal: self.baremetal.resolve_template(resolver)?,
|
||||||
|
docker: self.docker.resolve_template(resolver)?,
|
||||||
|
firecracker: self.firecracker.resolve_template(resolver)?,
|
||||||
|
custom: self.custom.resolve_template(resolver)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为DockerConfig实现
|
||||||
|
impl TemplateResolvable for crate::config::prebake::DockerConfig {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(Self {
|
||||||
|
image: self.image.resolve_template(resolver)?,
|
||||||
|
dockerfile: self.dockerfile.resolve_template(resolver)?,
|
||||||
|
build_args: self.build_args.resolve_template(resolver)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为CustomBuilderConfig实现
|
||||||
|
impl TemplateResolvable for crate::config::prebake::CustomBuilderConfig {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(Self {
|
||||||
|
name: self.name.resolve_template(resolver)?,
|
||||||
|
setup_script: self.setup_script.resolve_template(resolver)?,
|
||||||
|
cleanup_script: self.cleanup_script.resolve_template(resolver)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为ResourceRequirements实现
|
||||||
|
impl TemplateResolvable for crate::config::prebake::ResourceRequirements {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(Self {
|
||||||
|
cpu: self.cpu,
|
||||||
|
memory: self.memory.resolve_template(resolver)?,
|
||||||
|
disk: self.disk.resolve_template(resolver)?,
|
||||||
|
architecture: self.architecture.resolve_template(resolver)?,
|
||||||
|
gpu: self.gpu,
|
||||||
|
gpu_type: self.gpu_type.clone(),
|
||||||
|
tags: self.tags.resolve_template(resolver)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为NetworkConfig实现
|
||||||
|
impl TemplateResolvable for crate::config::prebake::NetworkConfig {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(Self {
|
||||||
|
enabled: self.enabled,
|
||||||
|
outbound: self.outbound,
|
||||||
|
dns_servers: self.dns_servers.resolve_template(resolver)?,
|
||||||
|
proxies: self.proxies.resolve_template(resolver)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为DependenciesConfig实现
|
||||||
|
impl TemplateResolvable for crate::config::prebake::DependenciesConfig {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(Self {
|
||||||
|
custom_pre: self.custom_pre.resolve_template(resolver)?,
|
||||||
|
system: self.system.resolve_template(resolver)?,
|
||||||
|
languages: self.languages.resolve_template(resolver)?,
|
||||||
|
custom: self.custom.resolve_template(resolver)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为LanguageDependencies实现
|
||||||
|
impl TemplateResolvable for crate::config::prebake::LanguageDependencies {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(Self {
|
||||||
|
rust: self.rust.resolve_template(resolver)?,
|
||||||
|
node: self.node.resolve_template(resolver)?,
|
||||||
|
python: self.python.resolve_template(resolver)?,
|
||||||
|
go: self.go.resolve_template(resolver)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为RustConfig实现
|
||||||
|
impl TemplateResolvable for crate::config::prebake::RustConfig {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(Self {
|
||||||
|
toolchain: self.toolchain.resolve_template(resolver)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为CustomDependency实现
|
||||||
|
impl TemplateResolvable for crate::config::prebake::CustomDependency {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(Self {
|
||||||
|
name: self.name.resolve_template(resolver)?,
|
||||||
|
command: self.command.resolve_template(resolver)?,
|
||||||
|
environment: self.environment.resolve_template(resolver)?,
|
||||||
|
working_dir: self.working_dir.resolve_template(resolver)?,
|
||||||
|
timeout: self.timeout,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为EnvironmentVariables实现
|
||||||
|
impl TemplateResolvable for crate::config::prebake::EnvironmentVariables {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(Self {
|
||||||
|
prebake: self.prebake.resolve_template(resolver)?,
|
||||||
|
bake: self.bake.resolve_template(resolver)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为CacheConfig实现
|
||||||
|
impl TemplateResolvable for crate::config::prebake::CacheConfig {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(Self {
|
||||||
|
directories: self.directories.resolve_template(resolver)?,
|
||||||
|
strategy: self.strategy.resolve_template(resolver)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为CacheDirectory实现
|
||||||
|
impl TemplateResolvable for crate::config::prebake::CacheDirectory {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(Self {
|
||||||
|
path: self.path.resolve_template(resolver)?,
|
||||||
|
strategy: self.strategy.resolve_template(resolver)?,
|
||||||
|
mode: self.mode.resolve_template(resolver)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为CacheStrategy实现
|
||||||
|
impl TemplateResolvable for crate::config::prebake::CacheStrategy {
|
||||||
|
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||||
|
Ok(Self {
|
||||||
|
ttl_days: self.ttl_days,
|
||||||
|
max_size_gb: self.max_size_gb,
|
||||||
|
cleanup_policy: self.cleanup_policy.resolve_template(resolver)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FinalizeConfig的模板解析保持不变...
|
||||||
|
|||||||
Reference in New Issue
Block a user