feat: implement docker env creation for prebake

Also we introduced opencode for code production.
This commit is contained in:
Catty Steve
2026-03-10 11:59:58 +08:00
parent d7ba65fbf4
commit c80500f6ac
17 changed files with 3163 additions and 109 deletions
+2
View File
@@ -1,3 +1,5 @@
target
playground
*.old
.codeartsdoer
+223
View File
@@ -0,0 +1,223 @@
# Agent Guidelines for HoneyBiscuitWorkshop
This repository is a monorepo of independent Rust projects using a unified coding style and conventions.
## Project Structure
- Original projects use `workshop-{name}` naming convention (e.g., `workshop-executor`, `workshop-vault`)
- Submodules: `RustyVault` and `rust-tongsuo` are external projects
- Each project is independently built with Cargo (no workspace)
## Build and Test Commands
```bash
# Build specific project
cd workshop-executor && cargo build --release
# Run all tests for a project
cargo test
# Run a single test
cargo test test_function_name
# Run tests in a specific module
cargo test module_name
# Run tests with verbose output
cargo test --verbose
# Run ignored tests
cargo test -- --ignored
# Lint with clippy
cargo clippy -- -D warnings
# Format code
cargo fmt
# Check formatting without making changes
cargo fmt --all -- --check
# Run tests with specific features (RustyVault)
cd RustyVault && cargo test --features sync_handler --verbose
```
## Code Style Guidelines
### Edition and Rust Version
- All workshop projects use Rust 2024 edition
- RustyVault uses Rust 2021 edition
### Naming Conventions
- **Crates/Packages**: `workshop-{name}` (snake_case)
- **Modules/Files**: `snake_case` (e.g., `error.rs`, `config.rs`)
- **Structs/Enums**: `PascalCase` (e.g., `VaultError`, `CertificateBundle`)
- **Functions/Methods**: `snake_case` (e.g., `generate_device_id()`, `store_cicd_variable()`)
- **Constants**: `SCREAMING_SNAKE_CASE` (e.g., `EXIT_CODE_OK`, `VERSION`)
### Import Organization
```rust
// Standard library imports first
use std::collections::HashMap;
use std::path::PathBuf;
// External crates (grouped)
use tokio::sync::mpsc;
use serde::{Serialize, Deserialize};
use anyhow::Result;
use log::info;
// Local crate modules
use crate::module_name::Type;
```
### Module Structure
```rust
// lib.rs or main.rs
mod config;
mod error;
mod handler;
mod state;
// Re-export commonly used types
pub use config::*;
pub use error::*;
```
### Error Handling
Use `thiserror` for custom error types and `anyhow` for generic error handling:
```rust
use thiserror::Error;
#[derive(Error, Debug)]
pub enum VaultError {
#[error("Vault operation failed: {0}")]
OperationFailed(String),
#[error("Authentication failed: {0}")]
AuthFailed(String),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
}
// Create type alias for convenience
pub type Result<T> = std::result::Result<T, VaultError>;
// Implement From trait for error conversions
impl From<std::io::Error> for VaultError {
fn from(err: std::io::Error) -> Self {
VaultError::IoError(err.to_string())
}
}
```
### Async Patterns
Use `tokio` with "full" features for async code:
```rust
#[tokio::main]
async fn main() {
env_logger::init();
// async code...
}
#[tokio::test]
async fn test_async_function() {
// test code...
}
```
### Serialization
Use `serde` derive macros with `#[serde(rename)]` for reserved words:
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Artifact {
pub name: String,
#[serde(rename = "type")]
pub type_: String,
pub path: String,
}
```
### Documentation
Add doc comments (`///`) for all public items:
```rust
/// Creates a new Vault client with the specified address and token
pub fn create_vault_client(address: String, token: String) -> Result<RustyVaultClient> {
// implementation...
}
```
### Testing
- Place unit tests in `#[cfg(test)]` modules within source files
- Use `#[tokio::test]` for async tests
- Use `#[ignore]` for tests requiring external resources (e.g., Vault instances)
- Use descriptive test names
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_device_id_generation() {
let device_id = DeviceIdGenerator::generate_device_id().unwrap();
assert_eq!(device_id.len(), 64);
}
#[tokio::test]
#[ignore]
async fn test_vault_operations() {
let client = create_vault_client_from_env().unwrap();
// test code...
}
}
```
### Formatting
Follow rustfmt settings from RustyVault:
- Max width: 120 characters
- Comment width: 120 characters
- Group imports: `StdExternalCrate`
- Imports granularity: `Crate`
- Trailing comma: `Vertical`
### Common Dependencies
- `tokio` (v1.40-1.48) with "full" features
- `serde` and `serde_json` for serialization
- `thiserror` for custom errors
- `anyhow` for error propagation
- `minijinja` for templating
- `clap` for CLI parsing (derive features)
- `log` and `env_logger` for logging
### Clippy Lints
The following clippy lints are allowed in RustyVault:
- `result_large_err`, `ptr_arg`, `let_and_return`, `should_implement_trait`
- `new_without_default`, `field_reassign_with_default`, `await_holding_lock`
- `too_many_arguments`, `unnecessary_unwrap`, `collapsible_match`
- `large_enum_variant`, `unnecessary_map_or`
Use similar allowances for workshop projects when appropriate.
### Environment Variables
Use environment variable prefixes for configuration:
- `WSEXECUTOR_` for workshop-executor
- Common pattern: `env_logger::init()` at the start of `main()`
+227
View File
@@ -0,0 +1,227 @@
对以上信息进行了补充
我们正在实现一个基于Rust的CI/CD系统,名为'蜜饼工坊'(HoneyBiscuitWorkshop),取材于明日方舟世界观中小刻爱吃的蜜饼和她的管理者火神大姐的工坊。
这个CI/CD的最终用途如下:
0. 一个合格的CICD当然可以DevOps
1. 软件包自动化,例如接入AOSC的autobuild+abbs树,或者Arch Linux的ABS,实现软件包的自动构建与更新
2. 机器学习支持,支持CUDA/ROCm/Others方案运行机器学习/深度学习程序
3. 对于部分工业软件/行业软件的支持,例如允许通过Vivado实现硬件设计远程综合
这个CI/CD的特色如下:
1. 基于LLM的流水线自配置
LLM可以读取关键文件/定义的文件/预设规则/上次生成的报错来生成流水线配置
2. 流水线进度感知
具体说来程序可以通过读取的流水线日志来判断进行的进度,以估计剩余时间,或给出超时警告
3. 与koishi(Koishi.js,一个聊天机器人管理项目,https://koishi.chat/zh-CN/)等结合,实现消息推送,流水线简单管理
通过koishijs与QQ机器人/Telegram机器人等配合,实现流水线成功/失败推送和快捷的重试等功能
4. 基于cgroups/JobObject的任务资源管理与计数
资源管理可以对任务资源进行限制,资源计数可以对不同权限的账号计算配额
5. prebake.yml, bake.sh, finalize.yml的最简流水线配置,声明式+命令式结合,采用@decorator进行装饰,以增强流水线行为
细节见下
6. 单二进制发布,支持各种架构,系统与C库
当然,程序不支持没有操作系统的裸机(nostd)
7. 构建机支持裸机,容器与FireCracker虚拟机
8. 可配置的构建缓存与环境缓存
9. 本地流水线快捷验证
可以在不借助服务器的情况下快速验证流水线语法的正确性,在借助本机容器的情况下进行试构建
10. 本地文件夹推送构建
可以推送本地的任何一个文件夹到服务器,其会根据预设规则/.workshop中的文件/LLM自生成进行构建,支持增量传输
CI/CD采用Web+Server+Multi (Agent+Executor)设计。当然也有可能的CLI客户端用于上述目的。
这个CI/CD可能会结合koishi, rustyvaulthttps://rustyvault.net,使用标准的Vault也可)等外部项目构建,当然我们希望减少对外部项目的依赖,尤其是数据库(如MySQL,意即,我们尽可能在SQLite解决问题)和消息队列(如RabbitMQ),但是不阻止部署者使用这些。
bake.sh的基础说明如下:
#!/bin/bash
# This is a helper function lying in all task functions
load() {
echo "loading..."
log Hello!
}
# This is a task function with some example decorators
# @pipeline
# @fallible
# @if PIPELINE_BUILD_ENABLE
# @retry 3 10s
build() {
load
echo "Build process"
}
示例prebake.yml如下:
# prebake.yaml.tmpl
#
# 术语说明:
# 可选:该字段可以不给出
# 默认为...:隐含“可选”
# 留空:尚未定义,留作后续
# 版本号,用于格式兼容性检查
version: "1.0"
# 构建环境定义
environment:
# 构建机类型
builder: "docker" # 或 "firecracker", "custom", "baremetal"
# 构建机配置,注意以下只会出现与上述构建机类型对应的组。
config:
# baremetal:
# 不需要
# docker:
# 镜像名
image: "rust:1.70-slim"
# Dockerfile路径,相对于项目根目录,不能与镜像一同指定,两者必须指定其一
dockerfile: "/Dockerfile"
# Docker构建参数,可选,只能与dockerfile搭配使用
build_args:
- "RUST_VERSION=1.70"
# firecracker:
# 留空
# custom:
# 自定义构建机名称
name: "my-custom-builder"
# 设置脚本,相对于项目根目录
setup_script: "setup-builder.sh"
# 清洁脚本,相对于项目根目录
cleanup_script: "cleanup-builder.sh"
# 硬件资源要求,满足最小要求的构建机可以被选中
resources:
cpu: 2 # CPU核心数,默认为1
memory: "2G" # 内存大小,也可以写作2048MB,默认为"1G"
disk: "10G" # 磁盘空间,也可以写作10240MB,默认为"1G"
architecture: "x86_64" # 架构要求,只能是"noarch"或{"x86_64"/"amd64", "arm64"/"aarch64", "riscv64", "loongarch64"/"loong64"}中的一个或几个
gpu: false # 是否需要GPU,默认为false
gpu_type: "cuda" # GPU类型(如果需要),可以是"cuda", "rocm", "vulkan", "oneapi"中的一个或几个
tags: # 自定义标签,只有满足标签的构建机可以被选中
- "custom_tag"
# 网络配置
network:
enabled: true # 启用网络,对baremetal不适用,默认为true
outbound: true # 允许出站连接,对baremetal不适用,默认为true
dns_servers: # 指定构建机的DNS服务器,对baremetal不适用,custom应该自行处理DNS问题,可选
- "8.8.8.8"
- "1.1.1.1"
proxies: # 代理配置,暂时留空,可选
# 依赖定义,安装依赖时按custom_pre-system-languages-custom顺序进行
dependencies:
# 系统包依赖,按软件包类型给出或any通配,若存在发行版则选择之,否则选中any,也没有则报警告但继续
system:
deb:
packages: # 安装的软件包
- "git"
- "curl"
- "build-essential"
- "pkg-config"
- "libssl-dev"
repositories:
- "ppa:custom/ppa" # 自定义PPAUbuntu
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:
rust:
toolchain: "stable-x86_64-unknown-linux-gnu"
# 自定义依赖步骤,可选
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"
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
# 环境变量配置
environment_variables:
# 构建环境变量,只在prebake阶段有效
prebake:
RUST_BACKTRACE: "full"
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
NODE_ENV: "production"
# 运行时环境变量(会传递给bake阶段,与该阶段获取的环境一并作为.env保存)
bake:
DATABASE_URL: "postgresql://user:pass@localhost/db"
LOG_LEVEL: "info"
__ENV_FILE: ".env" # 特殊变量,指定项目中.env的位置并合并其中的值。该值本身不传入bake阶段
# 缓存配置
cache:
# 缓存目录
directories:
- path: "/usr/local/cargo"
strategy: "always" # 在"always","readonly","clean"和"never"中选择一个,可以被流水线启动时配置覆盖,默认为"always"
mode: "zstd" # 在"gzip","zstd","none"和"dir"中选择一个
- path: "/root/.npm"
strategy: "clean"
mode: "none"
- path: "/app/target"
strategy: "always"
mode: "dir"
# 缓存策略
strategy:
ttl_days: 30 # 缓存生存时间
max_size_gb: 20 # 最大缓存大小
cleanup_policy: "lru" # 清理策略:lru, fifo, size
# 安全配置
security:
# 留空
# 钩子脚本
hooks:
# 依赖安装前
pre_dependencies:
- command: "echo 'Starting dependency installation'"
# 依赖安装后
post_dependencies:
- command: "cargo fetch"
working_dir: "/app"
# 环境准备完成
ready:
- command: "echo 'Environment ready for build'"
# 元数据
metadata:
description: "Rust project build environment"
maintainer: "team@example.com"
tags: ["rust", "web", "api"]
project_type: "rust-cargo"
finalize.yml的语法尚在设计
对于通信
请对其进行(创新性?可行性?实用性?等等)评估和设计,不需要写代码,但作为示例写一点也可以。
+1501 -11
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -5,18 +5,23 @@ edition = "2024"
[dependencies]
anyhow = "1.0.100"
bollard = { version = "0.20.0", features = ["buildkit", "chrono", "ssh"] }
cgroups-rs = "0.5.0"
chrono = { version = "0.4.42", features = ["serde"] }
clap = { version = "4.5.53", features = ["derive"] }
config = "0.15.19"
env_logger = "0.11.8"
futures-util = "0.3.31"
lazy_static = "1.5.0"
log = "0.4.28"
minijinja = "2.14.0"
regex = "1.12.2"
semver = "1.0.27"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.148"
serde_yaml = "0.9.34"
shlex = "1.3.0"
tar = "0.4.44"
tempfile = "3.24.0"
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["full"] }
+5
View File
@@ -0,0 +1,5 @@
FROM alpine:latest
RUN echo "Hello from Docker!" && \
aps add --no-cache curl && \
curl --version
CMD ["sh", "-c", "echo 'Container started' && sleep 3600"]
+166
View File
@@ -0,0 +1,166 @@
# prebake.yml - 蜜饼工坊 CI/CD 示例配置
# 核心特性:使用 Docker pull 形态 image,适配 Rust + CUDA 机器学习场景
version: "1.0.0"
# 构建环境定义(核心:使用 Docker pull 镜像,不指定 Dockerfile
environment:
# 构建机类型:docker(使用容器构建)
builder: "docker"
# Docker 构建机配置(仅指定 image,不指定 dockerfile
config:
# docker:
# 核心:Docker pull 形态的镜像(NVIDIA CUDA + Rust 基础镜像)
image: "docker.1ms.run/library/ubuntu:latest"
# 注意:dockerfile 字段未指定(与 image 二选一),build_args 也无需配置
# 硬件资源要求(适配 CUDA 机器学习场景)
resources:
cpu: 4 # 机器学习编译需要更多CPU核心
memory: "16G" # 大内存支持CUDA编译
disk: "50G" # 足够的磁盘空间存放CUDA依赖
architecture:
- "x86_64" # 优先x86_64CUDA主力架构)
- "aarch64" # 兼容ARM64Jetson设备)
gpu: true # 启用GPU(机器学习场景必需)
gpu_type:
- "cuda" # 支持CUDA
- "rocm" # 兼容ROCm
tags: # 仅选择带GPU标签的构建机
- "gpu-enabled"
- "cuda-12.2"
# 依赖定义(适配 Rust + CUDA 开发环境)
dependencies:
# 系统包依赖(按发行版分类)
system:
deb:
packages:
- "git"
- "curl"
- "build-essential"
- "pkg-config"
- "libssl-dev"
- "nvidia-cuda-toolkit" # CUDA工具链
- "libcudnn8" # cuDNN库(深度学习必需)
- "python3-pip" # Python依赖(ML场景)
repositories:
- "ppa:deadsnakes/ppa" # Python版本仓库
mirror: "https://mirrors.aliyun.com" # 阿里云镜像(国内加速)
pacman:
packages:
- "git"
- "curl"
- "base-devel"
- "pkg-config"
- "openssl"
- "cuda" # Arch Linux CUDA包
- "cudnn"
- "python-pip"
repositories:
- "https://mirrors.tuna.tsinghua.edu.cn/arch4edu/$arch"
mirror: "https://mirrors.tuna.tsinghua.edu.cn"
# 语言特定依赖(Rust + Python
languages:
rust:
toolchain: "stable-x86_64-unknown-linux-gnu"
components: # Rust 额外组件
- "clippy"
- "rustfmt"
targets: # 交叉编译目标
- "aarch64-unknown-linux-gnu"
# python:
# version: "3.10"
# packages: # Python ML依赖
# - "torch==2.2.0"
# - "torchvision==0.17.0"
# - "numpy==1.26.4"
# 自定义依赖(优先执行)
custom_pre:
- name: "configure-cuda-env"
command: "echo 'export PATH=/usr/local/cuda/bin:$PATH' >> /etc/profile && ldconfig"
environment:
CUDA_HOME: "/usr/local/cuda"
# 自定义依赖(常规执行)
custom:
- name: "install-rust-cuda-bindings"
command: "cargo install cargo-cuda"
working_dir: "/app"
timeout: 600 # 延长超时(编译CUDA绑定耗时)
- name: "setup-ml-deps"
command: "pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple"
working_dir: "/app/ml"
environment:
PIP_NO_CACHE_DIR: "1"
# 环境变量配置(分阶段)
environment_variables:
# prebake阶段专属环境变量
prebake:
RUST_BACKTRACE: "full"
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
CUDA_VERBOSE: "1" # CUDA编译详细日志
RUSTFLAGS: "-C target-cpu=native" # 优化CPU编译
# bake阶段传递的环境变量(合并到.env)
bake:
DATABASE_URL: "sqlite:///app/data/honeybiscuit.db" # 优先SQLite(符合轻量化要求)
LOG_LEVEL: "debug"
CUDA_DEVICE_ORDER: "PCI_BUS_ID"
CUDA_VISIBLE_DEVICES: "0" # 指定使用第0块GPU
__ENV_FILE: ".env.prod" # 合并项目中的.env.prod文件
# 缓存配置(优化构建速度)
cache:
directories:
# Rust Cargo缓存(长期保留)
- path: "/usr/local/cargo/registry"
strategy: "always" # 始终缓存
mode: "zstd" # zstd压缩(比gzip高效)
# Rust 编译目标缓存
- path: "/app/target"
strategy: "always"
mode: "dir" # 直接目录缓存(不压缩,加速访问)
# Python pip缓存
- path: "/root/.cache/pip"
strategy: "clean" # 每次构建前清理
mode: "gzip"
# CUDA编译缓存(大文件,不压缩)
- path: "/usr/local/cuda/bin/cache"
strategy: "always"
mode: "none"
# 缓存全局策略
strategy:
ttl_days: 15 # 缓存保留15天(ML依赖更新快)
max_size_gb: 100 # 更大缓存空间(CUDA依赖体积大)
cleanup_policy: "size" # 按大小清理(优先删除大文件)
# 钩子脚本(生命周期管理)
hooks:
pre_dependencies:
- command: "echo '=== 蜜饼工坊 - 开始安装依赖 ===' && apt update"
post_dependencies:
- command: "cargo cache clean --git --registry" # 清理无用Cargo缓存
- command: "nvcc --version && rustc --version" # 验证环境
ready:
- command: "echo '=== 蜜饼工坊 - 环境准备完成,CUDA可用 ==='"
- command: "python3 -c 'import torch; print(f\"CUDA可用: {torch.cuda.is_available()}\")'"
# 安全配置(基础)
security:
seccomp_profile: "default" # 默认seccomp配置
capabilities: # 容器权限(GPU需要)
- "CAP_SYS_ADMIN"
- "CAP_NET_ADMIN"
read_only_fs: false # 非只读文件系统(需要写入编译产物)
# 元数据(蜜饼工坊专属标识)
metadata:
description: "蜜饼工坊 - Rust + CUDA 机器学习项目构建环境"
maintainer: "honeybiscuit@arknights.com"
tags: ["rust", "cuda", "machine-learning", "ml", "nvidia"]
project_type: "rust-cuda-ml"
@@ -0,0 +1,166 @@
# prebake.yml - 蜜饼工坊 CI/CD 示例配置
# 核心特性:使用 Docker pull 形态 image,适配 Rust + CUDA 机器学习场景
version: "1.0.0"
# 构建环境定义(核心:使用 Docker pull 镜像,不指定 Dockerfile
environment:
# 构建机类型:docker(使用容器构建)
builder: "docker"
# Docker 构建机配置(仅指定 image,不指定 dockerfile
config:
# docker:
# 核心:Docker pull 形态的镜像(NVIDIA CUDA + Rust 基础镜像)
dockerfile: "examples/Dockerfile_1"
# 注意:dockerfile 字段未指定(与 image 二选一),build_args 也无需配置
# 硬件资源要求(适配 CUDA 机器学习场景)
resources:
cpu: 4 # 机器学习编译需要更多CPU核心
memory: "16G" # 大内存支持CUDA编译
disk: "50G" # 足够的磁盘空间存放CUDA依赖
architecture:
- "x86_64" # 优先x86_64CUDA主力架构)
- "aarch64" # 兼容ARM64Jetson设备)
gpu: true # 启用GPU(机器学习场景必需)
gpu_type:
- "cuda" # 支持CUDA
- "rocm" # 兼容ROCm
tags: # 仅选择带GPU标签的构建机
- "gpu-enabled"
- "cuda-12.2"
# 依赖定义(适配 Rust + CUDA 开发环境)
dependencies:
# 系统包依赖(按发行版分类)
system:
deb:
packages:
- "git"
- "curl"
- "build-essential"
- "pkg-config"
- "libssl-dev"
- "nvidia-cuda-toolkit" # CUDA工具链
- "libcudnn8" # cuDNN库(深度学习必需)
- "python3-pip" # Python依赖(ML场景)
repositories:
- "ppa:deadsnakes/ppa" # Python版本仓库
mirror: "https://mirrors.aliyun.com" # 阿里云镜像(国内加速)
pacman:
packages:
- "git"
- "curl"
- "base-devel"
- "pkg-config"
- "openssl"
- "cuda" # Arch Linux CUDA包
- "cudnn"
- "python-pip"
repositories:
- "https://mirrors.tuna.tsinghua.edu.cn/arch4edu/$arch"
mirror: "https://mirrors.tuna.tsinghua.edu.cn"
# 语言特定依赖(Rust + Python
languages:
rust:
toolchain: "stable-x86_64-unknown-linux-gnu"
components: # Rust 额外组件
- "clippy"
- "rustfmt"
targets: # 交叉编译目标
- "aarch64-unknown-linux-gnu"
# python:
# version: "3.10"
# packages: # Python ML依赖
# - "torch==2.2.0"
# - "torchvision==0.17.0"
# - "numpy==1.26.4"
# 自定义依赖(优先执行)
custom_pre:
- name: "configure-cuda-env"
command: "echo 'export PATH=/usr/local/cuda/bin:$PATH' >> /etc/profile && ldconfig"
environment:
CUDA_HOME: "/usr/local/cuda"
# 自定义依赖(常规执行)
custom:
- name: "install-rust-cuda-bindings"
command: "cargo install cargo-cuda"
working_dir: "/app"
timeout: 600 # 延长超时(编译CUDA绑定耗时)
- name: "setup-ml-deps"
command: "pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple"
working_dir: "/app/ml"
environment:
PIP_NO_CACHE_DIR: "1"
# 环境变量配置(分阶段)
environment_variables:
# prebake阶段专属环境变量
prebake:
RUST_BACKTRACE: "full"
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
CUDA_VERBOSE: "1" # CUDA编译详细日志
RUSTFLAGS: "-C target-cpu=native" # 优化CPU编译
# bake阶段传递的环境变量(合并到.env)
bake:
DATABASE_URL: "sqlite:///app/data/honeybiscuit.db" # 优先SQLite(符合轻量化要求)
LOG_LEVEL: "debug"
CUDA_DEVICE_ORDER: "PCI_BUS_ID"
CUDA_VISIBLE_DEVICES: "0" # 指定使用第0块GPU
__ENV_FILE: ".env.prod" # 合并项目中的.env.prod文件
# 缓存配置(优化构建速度)
cache:
directories:
# Rust Cargo缓存(长期保留)
- path: "/usr/local/cargo/registry"
strategy: "always" # 始终缓存
mode: "zstd" # zstd压缩(比gzip高效)
# Rust 编译目标缓存
- path: "/app/target"
strategy: "always"
mode: "dir" # 直接目录缓存(不压缩,加速访问)
# Python pip缓存
- path: "/root/.cache/pip"
strategy: "clean" # 每次构建前清理
mode: "gzip"
# CUDA编译缓存(大文件,不压缩)
- path: "/usr/local/cuda/bin/cache"
strategy: "always"
mode: "none"
# 缓存全局策略
strategy:
ttl_days: 15 # 缓存保留15天(ML依赖更新快)
max_size_gb: 100 # 更大缓存空间(CUDA依赖体积大)
cleanup_policy: "size" # 按大小清理(优先删除大文件)
# 钩子脚本(生命周期管理)
hooks:
pre_dependencies:
- command: "echo '=== 蜜饼工坊 - 开始安装依赖 ===' && apt update"
post_dependencies:
- command: "cargo cache clean --git --registry" # 清理无用Cargo缓存
- command: "nvcc --version && rustc --version" # 验证环境
ready:
- command: "echo '=== 蜜饼工坊 - 环境准备完成,CUDA可用 ==='"
- command: "python3 -c 'import torch; print(f\"CUDA可用: {torch.cuda.is_available()}\")'"
# 安全配置(基础)
security:
seccomp_profile: "default" # 默认seccomp配置
capabilities: # 容器权限(GPU需要)
- "CAP_SYS_ADMIN"
- "CAP_NET_ADMIN"
read_only_fs: false # 非只读文件系统(需要写入编译产物)
# 元数据(蜜饼工坊专属标识)
metadata:
description: "蜜饼工坊 - Rust + CUDA 机器学习项目构建环境"
maintainer: "honeybiscuit@arknights.com"
tags: ["rust", "cuda", "machine-learning", "ml", "nvidia"]
project_type: "rust-cuda-ml"
View File
+36 -30
View File
@@ -1,6 +1,7 @@
use crate::Cli;
use crate::{Cli, TaskSpec};
use serde::Serialize;
use serde_json::Value;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::net::{UnixListener, UnixStream};
@@ -8,6 +9,7 @@ use tokio::process::{Child, Command};
use tokio::signal;
use tokio::sync::RwLock;
use tokio::time::{Duration, interval};
use config::Config;
#[derive(Debug, Clone, Serialize)]
struct Heartbeat {
@@ -19,10 +21,12 @@ struct Heartbeat {
subtasks: Vec<String>,
}
pub async fn daemon(cli: &Cli) {
let _ = std::fs::remove_file("/tmp/wsexecutor.sock");
pub async fn daemon(cli: &Cli, settings: Config) {
let socket = PathBuf::from(settings.get_string("socket").unwrap());
let agentsocket = settings.get_string("agentsocket").unwrap();
let _ = std::fs::remove_file(&socket);
let mut heartbeat = Arc::new(RwLock::new(Heartbeat {
let heartbeat = Arc::new(RwLock::new(Heartbeat {
pipeline_id: cli.pipeline.clone(),
build_id: cli.build_id.clone(),
stage: String::new(),
@@ -50,23 +54,28 @@ pub async fn daemon(cli: &Cli) {
})
};
let socket_task = tokio::spawn(async move {
log::info!("Creating Unix socket at /tmp/wsexecutor.sock");
let listener =
UnixListener::bind("/tmp/wsexecutor.sock").expect("Failed to bind Unix socket");
let socket_task = {
let socket = socket.clone();
tokio::spawn(async move {
log::info!("Creating Unix socket at {}", socket.display());
let listener =
UnixListener::bind(&socket).expect("Failed to bind Unix socket");
loop {
match listener.accept().await {
Ok((stream, _)) => {
tokio::spawn(handle_connection(stream));
}
Err(e) => {
log::error!("Accept error: {}", e);
break;
loop {
match listener.accept().await {
Ok((stream, _)) => {
tokio::spawn(handle_connection(stream));
}
Err(e) => {
log::error!("Accept error: {}", e);
break;
}
}
}
}
});
})
};
let mut tasks: Vec<TaskSpec> = Vec::new();
let execution_task = {
let hb = heartbeat.clone();
@@ -74,14 +83,15 @@ pub async fn daemon(cli: &Cli) {
let mut hb = hb.write().await;
let mut children: Vec<(String, Child)> = vec![];
for _i in 1..=3 {
let child = Command::new("bash")
.arg("-c")
.arg("'echo running!'")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
let executable = std::env::current_exe().unwrap();
for (index, task) in tasks.iter().enumerate() {
let mut command = task.command(&executable);
command.stdout(std::process::Stdio::piped());
command.stderr(std::process::Stdio::piped());
let child = command
.spawn()
.expect("Failed to spawn child");
.expect(format!("Failed to spawn child {}", index).as_str());
let pid = child.id().unwrap_or(0).to_string();
children.push((pid.clone(), child));
@@ -112,17 +122,13 @@ pub async fn daemon(cli: &Cli) {
}
_ = signal::ctrl_c() => {
log::info!("Shutting down...");
// 清理子进程
}
}
// 清理子进程(防止僵尸)
for pid in heartbeat.read().await.subtasks.clone() {
// 发送SIGTERM
let _ = Command::new("kill").arg("-TERM").arg(&pid).status().await;
}
// 清理socket
let _ = std::fs::remove_file("/tmp/wsexecutor.sock");
let _ = std::fs::remove_file(&socket);
}
async fn handle_connection(stream: UnixStream) {
+45 -18
View File
@@ -1,4 +1,5 @@
mod builder;
mod daemon;
mod decorator;
mod engine;
mod error;
@@ -6,13 +7,18 @@ mod monitor;
mod parser;
mod schedule;
mod variable;
mod daemon;
mod socket;
mod prebake;
use clap::{Parser, Subcommand};
use config::Config;
use env_logger;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::PathBuf;
use crate::monitor::monitor;
use crate::daemon::daemon;
use crate::monitor::monitor;
use crate::prebake::prebake;
/// Honey Biscuit Workshop script executor
#[derive(Parser, Debug)]
@@ -43,54 +49,75 @@ enum Commands {
/// Monitor a running task
Monitor { group: String },
/// Prepare a task
Prebake {
config: String,
},
Prebake { config: String },
/// Run a task
Bake {
/// Path to the script to execute
script: String,
/// Path to bake_base.sh
#[arg(short, long, default_value = "./bake_base.sh")]
bake_base: String,
},
/// Finish a task
Finalize {
config: String,
Finalize { config: String },
}
#[derive(Debug, Deserialize)]
struct TaskSpec {
// pub stage: String,
pub subcommand: String,
pub arguments: Vec<String>,
pub options: HashMap<String, String>,
}
impl TaskSpec {
fn command(&self, executable: &PathBuf) -> tokio::process::Command {
let mut command = tokio::process::Command::new(executable);
command.arg(&self.subcommand);
for arg in &self.arguments {
command.arg(arg);
}
for (flag, value) in self.options.iter() {
if value.is_empty() {
command.arg(format!("--{}", flag));
} else {
command.arg(format!("--{}={}", flag, value));
}
}
command
}
}
#[tokio::main]
async fn main() {
env_logger::init();
let mut settings = Config::builder()
// .add_source(config::File::with_name("_config.toml"))
.add_source(config::Environment::with_prefix("HBW_EXEC_"))
let cli = Cli::parse();
let settings = Config::builder()
.set_default("pipeline", cli.pipeline.clone()).unwrap()
.add_source(config::Environment::with_prefix("WSEXECUTOR_"))
.build()
.unwrap();
let cli = Cli::parse();
match &cli.command {
Some(Commands::Monitor { group }) => {
log::info!("Starting monitor for group: {}", group);
let result = monitor(group, Some(1000)).await;
let result = monitor(group, Some(1000), settings).await;
result.unwrap();
}
Some(Commands::Prebake { config }) => {
// builder::prebake(config);
let result = prebake(config, &settings).await;
result.unwrap();
}
Some(Commands::Bake {script, bake_base}) => {
Some(Commands::Bake { script, bake_base }) => {
// builder::bake(script, bake_base, username, pipeline, *dry_run);
}
Some(Commands::Finalize {config}) => {
Some(Commands::Finalize { config }) => {
// builder::finalize(config);
}
None => {
// Running in daemon mode
log::info!("Executor will be running in daemon mode.");
let result = daemon(&cli).await;
let result = daemon(&cli, settings).await;
}
}
}
+5 -42
View File
@@ -1,8 +1,10 @@
mod cgroups;
mod jobobject;
use config::Config;
use serde::{Deserialize, Serialize};
use std::time::Instant;
use tokio::io::AsyncWriteExt;
use crate::socket::{establish_connection, get_socket_addr};
#[derive(Debug, Serialize, Deserialize)]
pub struct ResourceUsageInfo {
@@ -20,11 +22,6 @@ pub enum OsType {
MacOS,
}
enum AgentSocket {
Unix(String),
Tcp(String),
DryRun
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UsageStats {
@@ -43,8 +40,9 @@ fn default_internal_timestamp() -> Instant{
Instant::now()
}
pub async fn monitor(name: &String, interval_ms: Option<u64>) -> anyhow::Result<()> {
let socket_addr = get_wsagent_socket_addr()?;
pub async fn monitor(name: &String, interval_ms: Option<u64>, settings: Config) -> anyhow::Result<()> {
// let socket_addr = get_wsagent_socket_addr()?;
let socket_addr = get_socket_addr(settings.get_string("socket")?)?;
let interval = tokio::time::Duration::from_millis(interval_ms.unwrap_or(1000));
let mut socket = establish_connection(&socket_addr).await?;
let mut prev_usage = UsageStats{
@@ -76,23 +74,6 @@ pub async fn monitor(name: &String, interval_ms: Option<u64>) -> anyhow::Result<
}
}
fn get_wsagent_socket_addr() -> anyhow::Result<AgentSocket> {
let socket_addr = std::env::var("WSAGENT_SOCK")
.map_err(|_| anyhow::anyhow!("WSAGENT_SOCK is not set!"))?;
let socket = if socket_addr.starts_with("unix://") {
AgentSocket::Unix(socket_addr.strip_prefix("unix://").unwrap().to_string())
} else if socket_addr.starts_with("/") {
AgentSocket::Unix(socket_addr)
} else if socket_addr.starts_with("tcp://") {
AgentSocket::Tcp(socket_addr.strip_prefix("tcp://").unwrap().to_string())
} else if socket_addr.starts_with("dryrun") {
AgentSocket::DryRun
} else { // TCP by default
AgentSocket::Tcp(socket_addr)
};
Ok(socket)
}
async fn send_usage_info<S>(stream: &mut S, usage_json: &str) -> anyhow::Result<()>
where S: tokio::io::AsyncWrite + Unpin
{
@@ -100,21 +81,3 @@ where S: tokio::io::AsyncWrite + Unpin
stream.write_all(b"\n").await?;
Ok(())
}
async fn establish_connection(socket_addr: &AgentSocket) -> anyhow::Result<Box<dyn tokio::io::AsyncWrite + Unpin>> {
match &socket_addr {
AgentSocket::Unix(path) => {
let socket = tokio::net::UnixStream::connect(path).await?;
Ok(Box::new(socket) as Box<dyn tokio::io::AsyncWrite + Unpin>)
}
AgentSocket::Tcp(path) => {
let socket = tokio::net::TcpStream::connect(path).await?;
Ok(Box::new(socket) as Box<dyn tokio::io::AsyncWrite + Unpin>)
}
AgentSocket::DryRun => {
Ok(Box::new(tokio::io::sink()) as Box<dyn tokio::io::AsyncWrite + Unpin>)
}
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
use cgroups_rs;
use cgroups_rs::{fs::{cpu::CpuController, memory::MemController, pid::PidController}, stats::CpuAcctStats};
use cgroups_rs::{fs::{cpu::CpuController, memory::MemController, pid::PidController}};
use log;
use cgroups_rs::fs::Cgroup;
use std::time::{Instant, Duration};
+518
View File
@@ -0,0 +1,518 @@
mod environment;
use serde::{Deserialize, Serialize};
// use serde_with::{serde_as, DisplayFromStr, NoneAsEmptyString};
use std::collections::HashMap;
// use std::path::PathBuf;
use semver::{Version, VersionReq};
use config::Config;
use crate::prebake::environment::prepare_environment;
const VERSION_REQUIREMENT: &str = "^1.0";
// 顶层配置结构
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct PrebakeConfig {
pub version: String,
pub environment: Environment,
#[serde(default)]
pub dependencies: Option<Dependencies>,
#[serde(default)]
pub environment_variables: Option<EnvironmentVariables>,
#[serde(default)]
pub cache: Option<Cache>,
#[serde(default)]
pub security: Option<Security>,
#[serde(default)]
pub hooks: Option<Hooks>,
#[serde(default)]
pub metadata: Option<Metadata>,
}
// 构建环境配置
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Environment {
pub builder: BuilderType,
#[serde(default)]
pub config: Option<BuilderConfig>,
#[serde(default)]
pub resources: Option<ResourceRequirements>,
#[serde(default)]
pub network: Option<NetworkConfig>,
}
// 构建器类型枚举
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum BuilderType {
Docker,
Firecracker,
Custom,
Baremetal,
}
// 构建器配置 - 使用枚举来处理不同类型的配置
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(untagged)]
pub enum BuilderConfig {
Docker(DockerConfig),
Firecracker(FirecrackerConfig),
Custom(CustomConfig),
Baremetal(BaremetalConfig),
}
// 为每种构建器类型实现From trait以便转换
impl From<DockerConfig> for BuilderConfig {
fn from(config: DockerConfig) -> Self {
BuilderConfig::Docker(config)
}
}
impl From<FirecrackerConfig> for BuilderConfig {
fn from(config: FirecrackerConfig) -> Self {
BuilderConfig::Firecracker(config)
}
}
impl From<CustomConfig> for BuilderConfig {
fn from(config: CustomConfig) -> Self {
BuilderConfig::Custom(config)
}
}
impl From<BaremetalConfig> for BuilderConfig {
fn from(_: BaremetalConfig) -> Self {
BuilderConfig::Baremetal(BaremetalConfig {})
}
}
// Docker配置
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct DockerConfig {
#[serde(default)]
pub image: Option<String>,
#[serde(default)]
pub dockerfile: Option<String>,
#[serde(default)]
pub build_args: Option<Vec<String>>,
}
// Firecracker配置(目前为空)
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct FirecrackerConfig {
// 留空,根据文档说明
}
// 自定义构建器配置
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct CustomConfig {
pub name: String,
pub setup_script: String,
pub cleanup_script: String,
}
// Baremetal配置(空结构体)
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct BaremetalConfig {}
// 资源要求
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ResourceRequirements {
#[serde(default = "default_cpu")]
pub cpu: u32,
#[serde(default = "default_memory")]
pub memory: String,
#[serde(default = "default_disk")]
pub disk: String,
pub architecture: Architecture,
#[serde(default = "default_gpu")]
pub gpu: bool,
#[serde(default)]
pub gpu_type: Option<Vec<GpuType>>,
#[serde(default)]
pub tags: Option<Vec<String>>,
}
fn default_cpu() -> u32 {
1
}
fn default_memory() -> String {
"1G".to_string()
}
fn default_disk() -> String {
"1G".to_string()
}
fn default_gpu() -> bool {
false
}
// 架构类型
#[derive(Debug, Deserialize, Serialize, Clone)]
#[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, Deserialize, Serialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum GpuType {
Cuda,
Rocm,
Vulkan,
Oneapi,
}
// 网络配置
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct NetworkConfig {
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default = "default_outbound")]
pub outbound: bool,
#[serde(default)]
pub dns_servers: Option<Vec<String>>,
#[serde(default)]
pub proxies: Option<Vec<ProxyConfig>>,
}
fn default_enabled() -> bool {
true
}
fn default_outbound() -> bool {
true
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ProxyConfig {
// 代理配置结构,根据需求扩展
}
// 依赖配置
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Dependencies {
#[serde(default)]
pub system: Option<SystemDependencies>,
#[serde(default)]
pub languages: Option<HashMap<String, LanguageDependency>>,
#[serde(default)]
pub custom: Option<Vec<CustomDependency>>,
#[serde(default, rename = "custom_pre")]
pub custom_pre: Option<Vec<CustomDependency>>,
}
// 系统依赖
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct SystemDependencies {
#[serde(default)]
pub deb: Option<PackageManagerConfig>,
#[serde(default)]
pub pacman: Option<PackageManagerConfig>,
#[serde(default)]
pub rpm: Option<PackageManagerConfig>,
// 可以添加其他包管理器
}
// 包管理器配置
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct PackageManagerConfig {
pub packages: Vec<String>,
#[serde(default)]
pub repositories: Option<Vec<String>>,
#[serde(default)]
pub mirror: Option<String>,
}
// 语言依赖
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct LanguageDependency {
pub toolchain: String,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub features: Option<Vec<String>>,
}
// 自定义依赖
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct CustomDependency {
pub name: String,
pub command: String,
#[serde(default)]
pub environment: Option<HashMap<String, String>>,
#[serde(default)]
pub working_dir: Option<String>,
#[serde(default = "default_timeout")]
pub timeout: u32,
}
fn default_timeout() -> u32 {
300
}
// 环境变量配置
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct EnvironmentVariables {
#[serde(default)]
pub prebake: Option<HashMap<String, String>>,
#[serde(default)]
pub bake: Option<HashMap<String, String>>,
}
// 缓存配置
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Cache {
pub directories: Vec<CacheDirectory>,
#[serde(default)]
pub strategy: Option<CacheStrategy>,
}
// 缓存目录
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct CacheDirectory {
pub path: String,
#[serde(default = "default_strategy")]
pub strategy: CacheStrategyType,
#[serde(default = "default_mode")]
pub mode: CacheMode,
}
fn default_strategy() -> CacheStrategyType {
CacheStrategyType::Always
}
fn default_mode() -> CacheMode {
CacheMode::Zstd
}
// 缓存策略类型
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum CacheStrategyType {
Always,
Readonly,
Clean,
Never,
}
// 缓存模式
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum CacheMode {
Gzip,
Zstd,
None,
Dir,
}
// 缓存策略
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct CacheStrategy {
#[serde(default = "default_ttl_days", rename = "ttl_days")]
pub ttl_days: u32,
#[serde(default = "default_max_size_gb", rename = "max_size_gb")]
pub max_size_gb: u32,
#[serde(default = "default_cleanup_policy", rename = "cleanup_policy")]
pub cleanup_policy: CleanupPolicy,
}
fn default_ttl_days() -> u32 {
30
}
fn default_max_size_gb() -> u32 {
20
}
fn default_cleanup_policy() -> CleanupPolicy {
CleanupPolicy::Lru
}
// 清理策略
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum CleanupPolicy {
Lru,
Fifo,
Size,
}
// 安全配置(目前为空)
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Security {
// 根据文档留空,后续扩展
}
// 钩子脚本
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Hooks {
#[serde(default, rename = "pre_dependencies")]
pub pre_dependencies: Option<Vec<HookCommand>>,
#[serde(default, rename = "post_dependencies")]
pub post_dependencies: Option<Vec<HookCommand>>,
#[serde(default)]
pub ready: Option<Vec<HookCommand>>,
}
// 钩子命令
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct HookCommand {
pub command: String,
#[serde(default)]
pub working_dir: Option<String>,
#[serde(default)]
pub environment: Option<HashMap<String, String>>,
#[serde(default = "default_hook_timeout")]
pub timeout: u32,
}
fn default_hook_timeout() -> u32 {
300
}
// 元数据
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Metadata {
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub maintainer: Option<String>,
#[serde(default)]
pub tags: Option<Vec<String>>,
#[serde(default, rename = "project_type")]
pub project_type: Option<String>,
}
// 反序列化函数
pub fn parse_prebake_config(yaml_content: &str) -> Result<PrebakeConfig, serde_yaml::Error> {
serde_yaml::from_str(yaml_content)
}
// 序列化函数
pub fn serialize_prebake_config(config: &PrebakeConfig) -> Result<String, serde_yaml::Error> {
serde_yaml::to_string(config)
}
// 验证函数示例
impl PrebakeConfig {
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
let version_requirement =
VersionReq::parse(VERSION_REQUIREMENT).expect("Invalid version requirement");
match Version::parse(&self.version) {
Ok(version) if version_requirement.matches(&version) => {},
Ok(version) => errors.push(format!(
"Version {} does not satisfy requirement {}",
version, VERSION_REQUIREMENT
)),
Err(e) => errors.push(format!("Invalid version format: {}", e)),
}
match (&self.environment.builder, &self.environment.config) {
(BuilderType::Docker, Some(BuilderConfig::Docker(docker))) => {
if docker.image.is_some() && docker.dockerfile.is_some() {
errors.push("At most one of image and dockerfile can be specified".to_string());
}
if docker.image.is_none() && docker.dockerfile.is_none() {
errors.push("At least one of image or dockerfile must be specified".to_string());
}
}
(BuilderType::Firecracker, Some(BuilderConfig::Firecracker(firecracker))) => {
errors.push("Firecracker is not supported".to_string());
}
(BuilderType::Custom, Some(BuilderConfig::Custom(custom))) => {
if custom.name.is_empty() {
errors.push("Custom builder name cannot be empty".to_string());
}
}
_ => {}
}
// TODO: 完成强劲的Validator口牙
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
// 辅助方法:获取构建器类型对应的配置
pub fn get_builder_config(&self) -> Option<&BuilderConfig> {
self.environment.config.as_ref()
}
// 辅助方法:获取Docker配置(如果存在)
pub fn get_docker_config(&self) -> Option<&DockerConfig> {
match &self.environment.config {
Some(BuilderConfig::Docker(docker)) => Some(docker),
_ => None,
}
}
}
pub async fn prebake(config: &String, settings: &Config) -> anyhow::Result<()> {
let config = std::fs::read_to_string(config)?;
let config = parse_prebake_config(config.as_str())?;
let result = config.validate();
if result.is_err() {
for error in result.err().unwrap() {
println!("Error: {}", error);
}
}
// Begin Stage 1 Prebake
// Stage 1 Prebake means preparation that do not need project file.
let result = prepare_environment(&config, settings).await;
// if result.is_err() {
// for error in result.err().unwrap() {
// println!("Error: {}", error);
// }
// }
dbg!(&result);
result.unwrap();
todo!();
}
@@ -0,0 +1,216 @@
use crate::prebake::*;
use anyhow::Context;
use bollard::{
API_DEFAULT_VERSION, Docker, body_full, query_parameters::{BuildImageOptionsBuilder, CreateImageOptionsBuilder}, secret::{BuildInfo, CreateImageInfo, ProgressDetail}
};
use config::Config;
use futures_util::stream::StreamExt;
// use futures_util::stream::TryStreamExt;
use log::{debug, error, info, warn};
use uuid::Uuid;
use tar;
use std::fs;
#[derive(Debug)]
enum ContainerBuildResult {
PullResults(CreateImageInfo),
BuildResults(BuildInfo),
}
pub async fn prepare_environment(config: &PrebakeConfig, settings: &Config) -> anyhow::Result<()> {
match config.environment.builder {
BuilderType::Baremetal => {
info!("Nothing to be done for baremetal environment.");
}
BuilderType::Docker => {
let container = container(config, settings).await?;
dbg!(container);
}
BuilderType::Firecracker => {
error!("Firecracker is not supported yet.");
}
BuilderType::Custom => {
custom(config, settings)?;
}
}
todo!();
}
async fn container(config: &PrebakeConfig, settings: &Config) -> anyhow::Result<String> {
let container_options = match config.environment.config.as_ref() {
Some(BuilderConfig::Docker(docker)) => docker,
Some(_) => {
// No way it is going to happen
return Err(anyhow::anyhow!(
"Unexpected builder configuration: {:?}",
config.environment.builder
));
}
None => {
return Err(anyhow::anyhow!("Missing Docker configuration"));
}
};
info!("Connecting to container engine");
let container_url = settings
.get_string("container.url")
.unwrap_or_else(|_| "__DEFAULT".to_string());
let container_url = container_url.as_str();
let container_connection = if container_url.starts_with("unix://") {
Docker::connect_with_socket(container_url, 20, API_DEFAULT_VERSION)?
} else if container_url.starts_with("http://") {
warn!("It is suggested to run WS-Executor and Docker/Podman on the same machine.");
Docker::connect_with_http(container_url, 20, API_DEFAULT_VERSION)?
} else if container_url.starts_with("ssh://") {
warn!("It is suggested to run WS-Executor and Docker/Podman on the same machine.");
Docker::connect_with_ssh(container_url, 20, API_DEFAULT_VERSION, None)?
} else if container_url.contains("__DEFAULT") {
Docker::connect_with_local_defaults()?
} else {
return Err(anyhow::anyhow!(
"Unsupported container URL scheme: {}",
container_url
));
};
if container_options.image.is_none() {
// Using Dockerfile
return container_build(container_options, container_connection, settings).await;
} else {
// Pulling from repo
return container_pull(container_options, container_connection).await;
}
// todo!();
}
fn single_dockerfile_tar(dockerfile: &str) -> anyhow::Result<Vec<u8>> {
let mut tar_buffer = Vec::new();
let mut tar_builder = tar::Builder::new(&mut tar_buffer);
let dockerfile_content = fs::read_to_string(dockerfile)?;
let mut header = tar::Header::new_gnu();
header.set_path("Dockerfile")?;
header.set_size(dockerfile_content.len() as u64);
header.set_mode(0o644);
header.set_entry_type(tar::EntryType::Regular);
header.set_cksum();
tar_builder.append(&header, dockerfile_content.as_bytes())?;
tar_builder.finish()?;
tar_builder.into_inner()?;
Ok(tar_buffer)
}
async fn container_build(
container_options: &DockerConfig,
container_connection: Docker,
settings: &Config,
) -> anyhow::Result<String> {
info!("Building image at Prebake Stage 1...");
// Build process may fail at this time, it is somehow acceptable.
let dockerfile = container_options.dockerfile.as_ref().unwrap(); // No way to fail
let tag = format!("HoneyBiscuitWorkshop/{}_{}", settings.get_string("pipeline").unwrap().as_str(), Uuid::new_v4());
let tar_buffer = match single_dockerfile_tar(dockerfile) {
Ok(buffer) => buffer,
Err(error) => {
if error.to_string().contains("No such file") {
error!("Couldn't find specified Dockerfile: {}", dockerfile);
} else {
error!("Failed to prepare Dockerfile: {}", error);
}
return Err(error);
}
};
let build_options = BuildImageOptionsBuilder::default()
.dockerfile("Dockerfile")
.t(&tag)
.build();
let mut build_result = container_connection.build_image(build_options, None, Some(body_full(tar_buffer.into())));
while let Some(stream_item) = build_result.next().await {
let build_info = stream_item.context("Unexpected build result.");
dbg!(&build_info);
match build_info {
Ok(BuildInfo { id, stream, error_detail, status, progress_detail, aux }) => {},
Err(error) => {
let error_text = error.chain()
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join(" ");
error!("Failed to build image: {}", error_text);
return Err(error);
},
}
}
info!("Successfully built the image {}", tag);
Ok(tag)
}
async fn container_pull(
container_options: &DockerConfig,
container_connection: Docker,
) -> anyhow::Result<String> {
let image_name = container_options
.image
.as_ref()
.expect("Unexpected empty image name");
info!("Pulling image: {}", image_name);
let pull_options = CreateImageOptionsBuilder::default()
.from_image(image_name)
.build();
// TODO(@Catty2014): 2026-03-09 - Implement credentials
// Credentials are required to pull from docker private registry.
let mut pull_result = container_connection.create_image(Some(pull_options), None, None);
while let Some(stream_item) = pull_result.next().await {
let pull_info = stream_item.context("Unexpected pull result.");
// dbg!(&pull_info);
match pull_info {
Ok(CreateImageInfo{ id, error_detail, status, progress_detail }) => {
// Still unknown to continue or to fail
if let Some(error_detail) = error_detail {
warn!("Error when pulling: {:?}", error_detail);
}
if let Some(status) = status {
if status.contains("Downloading") || status.contains("Extracting") {
// if let Some(progress_detail) = progress_detail {
// info!("{}: {:?}", status, progress_detail);
// } else {
// info!("{}", status);
// }
} else if status.contains("Pull complete") {
info!("Layer complete: {}", id.unwrap_or_default());
} else if status.contains("Already exists") {
info!("Layer already exists: {}", id.unwrap_or_default());
} else if status.contains("Image is up to date") {
info!("Up to date image: {}", image_name);
} else if status.contains("Retrying") {
info!("Warning: {}", status);
} else if status.contains("Downloaded newer image") {
info!("Downloaded newer image: {}", image_name);
} else {
// info!("Status: {}", status);
}
}
},
Err(error) => {
let error_text = error.chain()
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join(" ");
error!("Failed to pull image: {}", error_text);
return Err(error);
}
}
}
info!("Successfully pulled image: {}", image_name);
Ok(image_name.clone())
}
fn custom(config: &PrebakeConfig, settings: &Config) -> anyhow::Result<()> {
todo!();
}
+40
View File
@@ -0,0 +1,40 @@
pub enum ExecutorSocket {
Unix(String),
Tcp(String),
DryRun,
}
pub fn get_socket_addr(socket_addr: String) -> anyhow::Result<ExecutorSocket> {
let socket = if socket_addr.starts_with("unix://") {
ExecutorSocket::Unix(socket_addr.strip_prefix("unix://").unwrap().to_string())
} else if socket_addr.starts_with("/") {
ExecutorSocket::Unix(socket_addr)
} else if socket_addr.starts_with("tcp://") {
ExecutorSocket::Tcp(socket_addr.strip_prefix("tcp://").unwrap().to_string())
} else if socket_addr.starts_with("dryrun") {
ExecutorSocket::DryRun
} else {
// TCP by default
ExecutorSocket::Tcp(socket_addr)
};
Ok(socket)
}
pub async fn establish_connection(
socket_addr: &ExecutorSocket,
) -> anyhow::Result<Box<dyn tokio::io::AsyncWrite + Unpin>> {
match &socket_addr {
ExecutorSocket::Unix(path) => {
let socket = tokio::net::UnixStream::connect(path).await?;
Ok(Box::new(socket) as Box<dyn tokio::io::AsyncWrite + Unpin>)
}
ExecutorSocket::Tcp(path) => {
let socket = tokio::net::TcpStream::connect(path).await?;
Ok(Box::new(socket) as Box<dyn tokio::io::AsyncWrite + Unpin>)
}
ExecutorSocket::DryRun => {
Ok(Box::new(tokio::io::sink()) as Box<dyn tokio::io::AsyncWrite + Unpin>)
}
}
}
@@ -28,7 +28,7 @@ environment:
- "RUST_VERSION=1.70"
# firecracker:
# 留空
# 留空,还没做
# custom:
# 自定义构建机名称
@@ -38,7 +38,7 @@ environment:
# 清洁脚本,相对于项目根目录
cleanup_script: "cleanup-builder.sh"
# 硬件资源要求,满足最小要求的构建机可以被选中
# 硬件资源要求,满足最小要求的构建机可以被选中,可选
resources:
cpu: 2 # CPU核心数,默认为1
memory: "2G" # 内存大小,也可以写作2048MB,默认为"1G"
@@ -49,7 +49,7 @@ environment:
tags: # 自定义标签,只有满足标签的构建机可以被选中
- "custom_tag"
# 网络配置
# 网络配置,可选
network:
enabled: true # 启用网络,对baremetal不适用,默认为true
outbound: true # 允许出站连接,对baremetal不适用,默认为true
@@ -58,7 +58,7 @@ environment:
- "1.1.1.1"
proxies: # 代理配置,暂时留空,可选
# 依赖定义,安装依赖时按custom_pre-system-languages-custom顺序进行
# 依赖定义,安装依赖时按custom_pre-system-languages-custom顺序进行,可选
dependencies:
# 系统包依赖,按软件包类型给出或any通配,若存在发行版则选择之,否则选中any,也没有则报警告但继续
system:
@@ -114,7 +114,7 @@ dependencies:
working_dir: "native-deps"
timeout: 300
# 环境变量配置
# 环境变量配置,可选
environment_variables:
# 构建环境变量,只在prebake阶段有效
prebake:
@@ -128,7 +128,7 @@ environment_variables:
LOG_LEVEL: "info"
__ENV_FILE: ".env" # 特殊变量,指定项目中.env的位置并合并其中的值。该值本身不传入bake阶段
# 缓存配置
# 缓存配置,可选
cache:
# 缓存目录
directories:
@@ -154,7 +154,7 @@ cache:
security:
# 留空
# 钩子脚本
# 钩子脚本,可选
hooks:
# 依赖安装前
pre_dependencies: