refactor!: remove workshop-pipeline crate and add repology support
BREAKING CHANGE: workshop-pipeline crate removed entirely - Remove workshop-pipeline/ (config structures, template resolution) - Add Repology package name resolution to workshop-baker - Add UPM system dependency collection for cross-pm resolution - Replace AGENTS.md with concise knowledge base format - Add crate-specific AGENTS.md files - Fix various clippy warnings and derive Default where applicable
This commit is contained in:
@@ -4,3 +4,4 @@ playground
|
||||
.ruff_cache
|
||||
|
||||
.codeartsdoer
|
||||
archived
|
||||
|
||||
@@ -1,145 +1,107 @@
|
||||
# Agent Guidelines for HoneyBiscuitWorkshop
|
||||
# PROJECT KNOWLEDGE BASE
|
||||
|
||||
This repository is a monorepo of independent Rust projects using unified coding style and conventions.
|
||||
**Generated:** 2026-04-04
|
||||
**Commit:** 2133f8b
|
||||
**Branch:** master
|
||||
|
||||
## Project Structure
|
||||
## OVERVIEW
|
||||
HoneyBiscuitWorkshop is a Rust-based CI/CD system ("蜜饼工坊") with LLM-powered pipeline auto-configuration. 9 independent Cargo crates, no workspace.
|
||||
|
||||
- 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)
|
||||
## STRUCTURE
|
||||
```
|
||||
./
|
||||
├── workshop-baker/ # Main orchestrator (CLI + daemon)
|
||||
├── workshop-agent/ # HTTP agent server
|
||||
├── workshop-pipeline/ # Config library (prebake/finalize)
|
||||
├── workshop-llm-detector/ # LLM repo analyzer
|
||||
├── workshop-vault/ # Secret management client
|
||||
├── workshop-cert/ # TLS certificate generator
|
||||
├── workshop-deviceid/ # Device ID library
|
||||
├── workshop-builder-native/ # Stub (unused)
|
||||
├── workshop-helper-mac/ # MAC address utility
|
||||
├── docs/ # mdBook documentation (zh-CN)
|
||||
├── playground/ # Experimentation
|
||||
└── third_party/ # Empty
|
||||
```
|
||||
|
||||
## Build and Test Commands
|
||||
## WHERE TO LOOK
|
||||
| Task | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| Baker (main) | `workshop-baker/src/{bake,prebake,engine}/` | Core pipeline logic |
|
||||
| Agent server | `workshop-agent/src/` | Actix-web HTTP + TLS |
|
||||
| Pipeline config | `workshop-pipeline/src/config/` | prebake/finalize YAML |
|
||||
| LLM integration | `workshop-llm-detector/src/llm/` | Ollama/OpenAI clients |
|
||||
| Secrets | `workshop-vault/src/` | Vault client wrapper |
|
||||
|
||||
## CODE MAP
|
||||
| Symbol | Type | Location | Role |
|
||||
|--------|------|----------|------|
|
||||
| PipelineConfig | struct | workshop-pipeline/src/lib.rs:27 | Full pipeline config |
|
||||
| CICDLLMHelper | struct | workshop-llm-detector/src/lib.rs:16 | LLM analysis entry |
|
||||
| Engine | struct | workshop-baker/src/engine.rs | Build executor |
|
||||
| VaultClient | struct | workshop-vault/src/client.rs | Secret ops |
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
### Rust
|
||||
- **Edition**: 2024 (non-standard, requires Rust 1.85+)
|
||||
- **Naming**: `snake_case` files/modules, `PascalCase` types, `SCREAMING_SNAKE_CASE` consts
|
||||
- **Error handling**: `thiserror` + `anyhow` combo
|
||||
- **Async**: `#[tokio::main]` + `tokio` with "full" features
|
||||
- **Imports**: `std` → `external_crate` → `crate::module`
|
||||
|
||||
### Testing
|
||||
- **Rust**: `#[test]` inline, `#[tokio::test]` async, `tests/*.rs` integration
|
||||
- **Python**: `pytest` with `tests/integration/test_*.py` (Docker-based)
|
||||
- **Benchmarks**: Criterion with `harness = false`
|
||||
|
||||
### Build Pipeline
|
||||
- **prebake.yml**: Environment setup (docker/firecracker/baremetal)
|
||||
- **bake.sh**: Build execution with `# @pipeline` decorators
|
||||
- **finalize.yml**: Packaging, deployment, notifications
|
||||
- **Annotations**: `@timeout()`, `@retry()`, `@parallel`, `@fallible`
|
||||
|
||||
## ANTI-PATTERNS (THIS PROJECT)
|
||||
|
||||
1. **Python in Rust project** — workshop-baker has Python tests (pytest.ini, test_prebake.py)
|
||||
2. **temp/ directory** — Non-standard in Rust projects
|
||||
3. **Certificates in source** — artifact workshop-cert/certs/ not excluded
|
||||
4. **Deprecated projects** — workshop-executor.old, workshop-monitor.old still present
|
||||
5. **Hardcoded mirrors** — Tsinghua University mirrors in configs
|
||||
6. **Rust 2024 edition** — May not work with stable toolchains
|
||||
|
||||
## UNIQUE STYLES
|
||||
|
||||
- **Pipeline DSL**: Shell scripts with `# @decorator` annotations
|
||||
- **Dual-mode baker**: CLI commands + daemon mode
|
||||
- **LLM cookbook system**: YAML cookbooks for language-specific builds
|
||||
- **Chinese docs** — Documentation primarily in zh-CN
|
||||
- **User "vulcan"** — Custom build user (not root/runner)
|
||||
|
||||
## 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
|
||||
# Build
|
||||
cargo build --release -p workshop-baker
|
||||
|
||||
# Test
|
||||
cargo test -p workshop-baker
|
||||
pytest tests/integration/test_prebake.py -v
|
||||
|
||||
# Lint
|
||||
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
|
||||
# Generate coverage report
|
||||
cargo install cargo-tarpaulin
|
||||
cargo tarpaulin --out Lcov
|
||||
cargo fmt --all
|
||||
|
||||
# Run baker
|
||||
cargo run --bin workshop-baker -- prebake config.yml
|
||||
cargo run --bin workshop-baker -- bake script.sh
|
||||
|
||||
# Run agent
|
||||
cargo run --bin workshop-agent
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
## NOTES
|
||||
|
||||
### Edition and Naming
|
||||
|
||||
- Workshop projects: Rust 2024 edition, RustyVault: Rust 2021 edition
|
||||
- **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()`)
|
||||
- **Constants**: `SCREAMING_SNAKE_CASE` (e.g., `EXIT_CODE_OK`, `VERSION`)
|
||||
|
||||
### Import Organization
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::mpsc;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use crate::module_name::Type;
|
||||
```
|
||||
|
||||
### Module Structure and Error Handling
|
||||
|
||||
```rust
|
||||
mod config;
|
||||
mod error;
|
||||
mod handler;
|
||||
pub use config::*;
|
||||
pub use error::*;
|
||||
use thiserror::Error;
|
||||
#[derive(Error, Debug)]
|
||||
pub enum VaultError {
|
||||
#[error("Vault operation failed: {0}")]
|
||||
OperationFailed(String),
|
||||
}
|
||||
pub type Result<T> = std::result::Result<T, VaultError>;
|
||||
```
|
||||
|
||||
### Async Patterns and Serialization
|
||||
|
||||
Use `tokio` with "full" features for async code:
|
||||
```rust
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
// async code...
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Artifact {
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String,
|
||||
}
|
||||
```
|
||||
|
||||
### Documentation and Testing
|
||||
|
||||
Add doc comments (`///`) for all public items. Place unit tests in `#[cfg(test)]` modules within source files.
|
||||
|
||||
Use `#[tokio::test]` for async tests, `#[ignore]` for tests requiring external resources. Use `maybe_async` crate for conditional async tests:
|
||||
```rust
|
||||
#[maybe_async::test(feature = "sync_handler", async(all(not(feature = "sync_handler")), tokio::test))]
|
||||
async fn test_with_feature_flag() {
|
||||
// test code...
|
||||
}
|
||||
```
|
||||
|
||||
### Formatting and Dependencies
|
||||
|
||||
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()`
|
||||
|
||||
### Cursor/Copilot Rules
|
||||
|
||||
No Cursor or Copilot rules found in the repository.
|
||||
- **Docker required** for integration tests
|
||||
- **workshop-builder-native** is a stub (3-line Hello World)
|
||||
- No root workspace Cargo.toml — each crate is independent
|
||||
- `AGENTS.md.old` at root — legacy documentation
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
temp
|
||||
@@ -0,0 +1,59 @@
|
||||
# workshop-baker/src
|
||||
|
||||
**Baker CLI + execution engine for HoneyBiscuitWorkshop pipelines.**
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
CLI tool and runtime for executing CI/CD pipelines with resource isolation, privilege dropping, and package management integration.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
src/
|
||||
├── bake.rs # Top-level bake orchestration
|
||||
├── bake/ # Script parsing and building
|
||||
│ ├── parser.rs # @decorator-aware shell parser
|
||||
│ ├── decorator.rs # @pipeline, @retry, @parallel, etc.
|
||||
│ ├── builder.rs # Script template builder
|
||||
│ └── schedule.rs # Function ordering via @after deps
|
||||
├── engine/ # Execution runtime
|
||||
│ ├── executor.rs # Script execution with cgroups
|
||||
│ ├── cgroups.rs # Linux cgroup resource limits
|
||||
│ ├── repology.rs # Package name resolution
|
||||
│ ├── pm.rs # Package manager detection
|
||||
│ └── upm.rs # User package manager (Nix/Guix)
|
||||
├── prebake/ # Build environment setup
|
||||
│ ├── config.rs # PrebakeConfig YAML schema
|
||||
│ ├── stage/ # Bootstrap, DepsSystem, DepsUser, Hooks
|
||||
│ └── security.rs # Privilege dropping logic
|
||||
├── types/ # Shared type definitions
|
||||
│ ├── repology.rs # RepologyEndpoint enum
|
||||
│ ├── builderconfig.rs
|
||||
│ ├── cache.rs
|
||||
│ └── memsize.rs
|
||||
├── monitor/ # Resource usage monitoring
|
||||
│ ├── cgroups.rs # cgroup-based usage collection
|
||||
│ └── jobobject.rs # Windows job objects (stub)
|
||||
├── daemon.rs # Unix socket daemon (TODO: refactor)
|
||||
└── lib.rs # CLI struct + module exports
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
|
||||
| Task | Location |
|
||||
|------|----------|
|
||||
| Script parsing with @decorators | `bake/parser.rs:29` - `parse_script()` |
|
||||
| Decorator types | `bake/decorator.rs` - `Decorator` enum |
|
||||
| Build execution | `engine/executor.rs` - `Executor::execute_script()` |
|
||||
| Resource limits | `engine/types.rs:56` - `ResourceLimits` struct |
|
||||
| Prebake stages | `prebake/stage/` subdirectories |
|
||||
| Package detection | `engine/pm.rs:detection()` |
|
||||
| CLI entry | `lib.rs:26` - `cli::Cli` struct |
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
- **ExecutionContext**: Passed through all stages, carries task_id, username, env_vars, timeout
|
||||
- **PrebakeStage**: Ordered enum controlling stage execution sequence (Bootstrap -> Ready)
|
||||
- **Decorator**: Attached to shell functions via `# @name(args)` comments above function declarations
|
||||
- **Engine**: Lightweight wrapper around cgroup manager for resource isolation
|
||||
- **Dual-mode**: CLI commands (prebake, bake, finalize) vs daemon mode via Unix socket
|
||||
@@ -60,7 +60,7 @@ pub async fn bake(
|
||||
let script = builder::build_script(
|
||||
"bake".to_string(),
|
||||
script_body,
|
||||
&bake_base_path,
|
||||
bake_base_path,
|
||||
&workspace,
|
||||
None,
|
||||
use_template,
|
||||
@@ -72,7 +72,7 @@ pub async fn bake(
|
||||
let script = builder::build_script(
|
||||
func.name,
|
||||
func.body,
|
||||
&bake_base_path,
|
||||
bake_base_path,
|
||||
&workspace,
|
||||
None,
|
||||
use_template,
|
||||
@@ -104,6 +104,6 @@ fn privileged(prebake_config: &PrebakeConfig) -> bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
let drop_after = get_drop_after(&prebake_config.security).unwrap_or(PrebakeStage::default());
|
||||
let drop_after = get_drop_after(&prebake_config.security).unwrap_or_default();
|
||||
PrebakeStage::Never == drop_after
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use tempfile::NamedTempFile;
|
||||
use crate::error::BakeError;
|
||||
|
||||
/// Template for use_template=false, which just executes the main body without any wrapping.
|
||||
const TRIVIAL_TEMPLATE: &'static str = "{{ main }}";
|
||||
const TRIVIAL_TEMPLATE: &str = "{{ main }}";
|
||||
|
||||
pub fn build_script(
|
||||
name: String,
|
||||
@@ -23,7 +23,7 @@ pub fn build_script(
|
||||
let target = target_dir.join(filename);
|
||||
|
||||
let bake_base_content = if use_template {
|
||||
std::fs::read_to_string(&bake_base).map_err(|e| BakeError::ScriptGenerationFailed {
|
||||
std::fs::read_to_string(bake_base).map_err(|e| BakeError::ScriptGenerationFailed {
|
||||
script: bake_base.display().to_string(),
|
||||
reason: e.to_string(),
|
||||
})?
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
pub mod cgroups;
|
||||
pub mod executor;
|
||||
pub mod pm;
|
||||
pub mod upm;
|
||||
pub mod repology;
|
||||
pub mod types;
|
||||
pub mod upm;
|
||||
|
||||
// Re-export commonly used types for convenience
|
||||
pub use types::{
|
||||
|
||||
@@ -32,7 +32,7 @@ impl CgroupManager {
|
||||
|
||||
pub fn set_cpu_weight(&self, weight: u64) -> io::Result<()> {
|
||||
let cpu_weight_path = self.path.join("cpu.weight");
|
||||
let weight = weight.max(1).min(10000);
|
||||
let weight = weight.clamp(1, 10000);
|
||||
fs::write(&cpu_weight_path, weight.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -51,7 +51,6 @@ impl CgroupManager {
|
||||
pub fn add_process(&self, pid: u32) -> io::Result<()> {
|
||||
let cgroup_procs_path = self.path.join("cgroup.procs");
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.append(true)
|
||||
.open(&cgroup_procs_path)?;
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
pub mod local;
|
||||
|
||||
use crate::types::repology::RepologyEndpoint;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RepologyError {
|
||||
Unimplemented,
|
||||
RemoteNotSupported,
|
||||
}
|
||||
|
||||
pub async fn resolve_package_names(
|
||||
packages: &[String],
|
||||
mapped: bool,
|
||||
endpoint: &RepologyEndpoint,
|
||||
target_pm: &str,
|
||||
) -> Result<HashMap<String, Vec<String>>, RepologyError> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
if packages.is_empty() {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
if mapped {
|
||||
result.insert(target_pm.to_string(), packages.to_vec());
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
match endpoint {
|
||||
RepologyEndpoint::Local | RepologyEndpoint::Default => {
|
||||
let resolved = local::resolve(packages, target_pm).await;
|
||||
if !resolved.is_empty() {
|
||||
result.insert(target_pm.to_string(), resolved);
|
||||
}
|
||||
}
|
||||
RepologyEndpoint::None => {
|
||||
result.insert(target_pm.to_string(), packages.to_vec());
|
||||
}
|
||||
RepologyEndpoint::Disabled => {}
|
||||
RepologyEndpoint::Remote | RepologyEndpoint::Server => {
|
||||
return Err(RepologyError::Unimplemented);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
type PmName = &'static str;
|
||||
type PkgName = &'static str;
|
||||
|
||||
fn pkg(packages: &[(PmName, PkgName)]) -> Vec<(PmName, PkgName)> {
|
||||
packages.to_vec()
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref MAPPING: HashMap<PkgName, Vec<(PmName, PkgName)>> = {
|
||||
let mut m = HashMap::new();
|
||||
|
||||
m.insert("rustup", pkg(&[
|
||||
("pacman", "rustup"),
|
||||
("apt", "rustup"),
|
||||
("dnf", "rustup"),
|
||||
]));
|
||||
|
||||
m.insert("cargo", pkg(&[
|
||||
("pacman", "cargo"),
|
||||
("apt", "cargo"),
|
||||
("dnf", "cargo"),
|
||||
]));
|
||||
|
||||
m.insert("rustc", pkg(&[
|
||||
("pacman", "rustc"),
|
||||
("apt", "rustc"),
|
||||
("dnf", "rustc"),
|
||||
]));
|
||||
|
||||
m.insert("go", pkg(&[
|
||||
("pacman", "go"),
|
||||
("apt", "golang-go"),
|
||||
("dnf", "golang"),
|
||||
]));
|
||||
|
||||
m.insert("gofmt", pkg(&[
|
||||
("pacman", "go"),
|
||||
("apt", "golang-go"),
|
||||
("dnf", "golang"),
|
||||
]));
|
||||
|
||||
m.insert("nodejs", pkg(&[
|
||||
("pacman", "nodejs"),
|
||||
("apt", "nodejs"),
|
||||
("dnf", "nodejs"),
|
||||
]));
|
||||
|
||||
m.insert("npm", pkg(&[
|
||||
("pacman", "npm"),
|
||||
("apt", "npm"),
|
||||
("dnf", "npm"),
|
||||
]));
|
||||
|
||||
m.insert("yarn", pkg(&[
|
||||
("pacman", "yarn"),
|
||||
("apt", "yarn"),
|
||||
("dnf", "yarn"),
|
||||
]));
|
||||
|
||||
m.insert("pnpm", pkg(&[
|
||||
("pacman", "pnpm"),
|
||||
("apt", "pnpm"),
|
||||
("dnf", "pnpm"),
|
||||
]));
|
||||
|
||||
m.insert("python", pkg(&[
|
||||
("pacman", "python"),
|
||||
("apt", "python3"),
|
||||
("dnf", "python3"),
|
||||
]));
|
||||
|
||||
m.insert("pip", pkg(&[
|
||||
("pacman", "python-pip"),
|
||||
("apt", "python3-pip"),
|
||||
("dnf", "python3-pip"),
|
||||
]));
|
||||
|
||||
m
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn resolve(
|
||||
packages: &[String],
|
||||
target_pm: &str,
|
||||
) -> Vec<String> {
|
||||
let mut result = Vec::new();
|
||||
for pkg in packages {
|
||||
if let Some(mappings) = MAPPING.get(pkg.as_str()) {
|
||||
for (pm, name) in mappings {
|
||||
if *pm == target_pm {
|
||||
result.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -6,11 +6,14 @@ mod custom;
|
||||
use crate::{ExecutionContext, error::DependencyError};
|
||||
|
||||
pub struct UPMSysDeps {
|
||||
/// Packages to be installed
|
||||
packages: Vec<String>,
|
||||
pub packages: Vec<String>,
|
||||
pub mapped: bool,
|
||||
}
|
||||
|
||||
/// Whether package name is mapped to distro
|
||||
mapped: bool,
|
||||
impl UPMSysDeps {
|
||||
pub fn new(packages: Vec<String>, mapped: bool) -> Self {
|
||||
Self { packages, mapped }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -24,10 +24,7 @@ impl UserPackageManager for Custom {
|
||||
}
|
||||
|
||||
fn system_dependency(&self, _config: &serde_yaml::Value) -> super::UPMSysDeps {
|
||||
UPMSysDeps {
|
||||
packages: vec![],
|
||||
mapped: true,
|
||||
}
|
||||
UPMSysDeps::new(vec![], true)
|
||||
}
|
||||
|
||||
async fn main(
|
||||
@@ -52,7 +49,7 @@ impl UserPackageManager for Custom {
|
||||
};
|
||||
log::info!("Executing custom command #{}: {}.", index, cmd.name);
|
||||
let result = engine
|
||||
.execute_command_with_delta(&cmd.command, &ctx, &event_tx, &deltactx)
|
||||
.execute_command_with_delta(&cmd.command, ctx, event_tx, &deltactx)
|
||||
.await;
|
||||
if let Err(e) = result {
|
||||
log::error!("Custom command {} failed: {:?}", index, e);
|
||||
|
||||
@@ -57,7 +57,7 @@ pub async fn monitor(
|
||||
_timestamp: default_internal_timestamp(),
|
||||
};
|
||||
loop {
|
||||
let usage: UsageStats = cgroups::get_usage(&name, prev_usage)?; // TODO: 多系统支持?
|
||||
let usage: UsageStats = cgroups::get_usage(name, prev_usage)?; // TODO: 多系统支持?
|
||||
prev_usage = usage.clone();
|
||||
// dbg!(&usage);
|
||||
let usage_info = ResourceUsageInfo {
|
||||
@@ -83,7 +83,7 @@ async fn send_usage_info<S>(stream: &mut S, usage_json: &str) -> anyhow::Result<
|
||||
where
|
||||
S: tokio::io::AsyncWrite + Unpin,
|
||||
{
|
||||
stream.write(usage_json.as_bytes()).await?;
|
||||
stream.write_all(usage_json.as_bytes()).await?;
|
||||
stream.write_all(b"\n").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ pub fn get_usage(
|
||||
let mut process_count: u64 = 0;
|
||||
|
||||
if let Some(cpu) = cgroup.controller_of::<CpuController>() {
|
||||
let stats = parse_cpustat(&cpu.cpu().stat.as_str())?;
|
||||
let stats = parse_cpustat(cpu.cpu().stat.as_str())?;
|
||||
cpu_usage_us = stats.usage_usec;
|
||||
} else {
|
||||
log::warn!("Failed to fetch CPU usage info!");
|
||||
|
||||
@@ -25,12 +25,12 @@ pub mod env;
|
||||
pub mod security;
|
||||
pub mod stage;
|
||||
|
||||
use std::path::Path;
|
||||
use crate::{
|
||||
cli::Cli,
|
||||
error::PrebakeError,
|
||||
prebake::{security::get_drop_after, stage::PrebakeStage},
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
pub use config::PrebakeConfig;
|
||||
pub use security::prebake_drop_privilege;
|
||||
@@ -115,11 +115,11 @@ pub async fn prebake(
|
||||
};
|
||||
// Wait for bakerd to finish Fetch-1
|
||||
let stage = stage.unwrap_or(PrebakeStage::Bootstrap);
|
||||
if let Some(env) = &prebake.envvars {
|
||||
if let Some(prebake) = &env.prebake {
|
||||
if let Some(env) = &prebake.envvars
|
||||
&& let Some(prebake) = &env.prebake
|
||||
{
|
||||
ctx.env_vars.extend(prebake.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap stage
|
||||
|
||||
@@ -145,24 +145,78 @@ pub async fn prebake(
|
||||
}
|
||||
|
||||
if let Some(dependencies) = prebake.dependencies {
|
||||
// DepsSystem stage
|
||||
let endpoint = dependencies
|
||||
.config
|
||||
.as_ref()
|
||||
.map(|c| c.repology_endpoint)
|
||||
.unwrap_or(crate::types::repology::RepologyEndpoint::Default);
|
||||
|
||||
let osinfo = os_info::get();
|
||||
let detected_pm = crate::engine::pm::detect(&osinfo).map(|pm| pm.name().to_string());
|
||||
|
||||
let upm_sys_deps = if let Some(user) = &dependencies.user {
|
||||
if stage <= PrebakeStage::DepsSystem {
|
||||
log::info!("Running PBStage: DepsSystem");
|
||||
prebake_drop_privilege(PrebakeStage::DepsSystem, dropafter, target_user, &mut ctx)?;
|
||||
ctx.task_id = format!("{}-{}-depssystem", cli.pipeline, cli.build_id);
|
||||
if let Some(system) = dependencies.system {
|
||||
stage::depssystem::depssystem(&system, &ctx, None).await?;
|
||||
stage::depsuser::collect_upm_sysdeps(user, &ctx, &endpoint).await?
|
||||
} else {
|
||||
stage::depsuser::CollectedUPMdeps::default()
|
||||
}
|
||||
} else {
|
||||
stage::depsuser::CollectedUPMdeps::default()
|
||||
};
|
||||
|
||||
let mut system_config = dependencies.system.clone().unwrap_or_default();
|
||||
|
||||
if !upm_sys_deps.deps.is_empty() {
|
||||
if let Some(pm_name) = &detected_pm {
|
||||
for (upm_name, sys_deps) in &upm_sys_deps.deps {
|
||||
let resolved = crate::engine::repology::resolve_package_names(
|
||||
&sys_deps.packages,
|
||||
sys_deps.mapped,
|
||||
&endpoint,
|
||||
pm_name,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Repology error: {:?}", e))?;
|
||||
|
||||
if !resolved.is_empty() {
|
||||
log::info!(
|
||||
"UPM {} resolved to system packages: {:?}",
|
||||
upm_name,
|
||||
resolved
|
||||
);
|
||||
}
|
||||
|
||||
for (target_pm, packages) in &resolved {
|
||||
system_config
|
||||
.entry(target_pm.clone())
|
||||
.or_insert_with(|| config::SystemDependency {
|
||||
packages: Vec::new(),
|
||||
repositories: None,
|
||||
mirror: None,
|
||||
})
|
||||
.packages
|
||||
.extend(packages.iter().cloned());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!("No package manager detected, cannot resolve UPM system dependencies");
|
||||
}
|
||||
}
|
||||
|
||||
// DepsUser stage
|
||||
if stage <= PrebakeStage::DepsUser {
|
||||
if !system_config.is_empty() && stage <= PrebakeStage::DepsSystem {
|
||||
log::info!("Running PBStage: DepsSystem");
|
||||
prebake_drop_privilege(PrebakeStage::DepsSystem, dropafter, target_user, &mut ctx)?;
|
||||
ctx.task_id = format!("{}-{}-depssystem", cli.pipeline, cli.build_id);
|
||||
stage::depssystem::depssystem(&system_config, &ctx, None).await?;
|
||||
}
|
||||
|
||||
if let Some(user) = dependencies.user
|
||||
&& stage <= PrebakeStage::DepsUser
|
||||
{
|
||||
log::info!("Running PBStage: DepsUser");
|
||||
prebake_drop_privilege(PrebakeStage::DepsUser, dropafter, target_user, &mut ctx)?;
|
||||
ctx.task_id = format!("{}-{}-depsuser", cli.pipeline, cli.build_id);
|
||||
if let Some(user) = dependencies.user {
|
||||
stage::depsuser::depsuser(&user, &ctx, None).await?;
|
||||
}
|
||||
stage::depsuser::depsuser(&user, &ctx, None, &endpoint).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::types::builderconfig::{
|
||||
use crate::types::cache::{CacheDirectory, CacheStrategy};
|
||||
use crate::types::command::CustomCommand;
|
||||
use crate::types::memsize::MemSize;
|
||||
use crate::types::repology::RepologyEndpoint;
|
||||
|
||||
pub const VERSION_REQUIREMENT: &str = "^1.0";
|
||||
|
||||
@@ -36,7 +37,7 @@ pub struct PrebakeConfig {
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct Bootstrap {
|
||||
#[serde(default="default_bootstrap_user")]
|
||||
#[serde(default = "default_bootstrap_user")]
|
||||
pub user: String,
|
||||
|
||||
#[serde(default)]
|
||||
@@ -66,10 +67,10 @@ impl Default for Bootstrap {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Workspace {
|
||||
#[serde(default="default_workspace")]
|
||||
#[serde(default = "default_workspace")]
|
||||
pub path: String,
|
||||
|
||||
#[serde(default="default_true")]
|
||||
#[serde(default = "default_true")]
|
||||
pub fallback: bool,
|
||||
}
|
||||
|
||||
@@ -230,6 +231,14 @@ pub struct Dependencies {
|
||||
pub system: Option<HashMap<String, SystemDependency>>,
|
||||
#[serde(default)]
|
||||
pub user: Option<HashMap<String, Value>>,
|
||||
#[serde(default)]
|
||||
pub config: Option<DependenciesConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct DependenciesConfig {
|
||||
#[serde(default)]
|
||||
pub repology_endpoint: RepologyEndpoint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
|
||||
@@ -182,14 +182,13 @@ pub fn prebake_drop_privilege(
|
||||
if stage_current > stage_expected {
|
||||
let current_uid = unsafe { libc::geteuid() };
|
||||
|
||||
let target_uid = lookup_uid_by_name(user).map_err(|e| {
|
||||
let target_uid = lookup_uid_by_name(user).inspect_err(|_e| {
|
||||
log::error!(
|
||||
"Cannot drop privileges: target user '{}' does not exist at stage {}",
|
||||
user,
|
||||
stage_current
|
||||
);
|
||||
log::error!("Aborting pipeline.");
|
||||
e
|
||||
})?;
|
||||
|
||||
if current_uid == target_uid {
|
||||
|
||||
@@ -6,11 +6,12 @@ pub mod hook;
|
||||
pub mod depssystem;
|
||||
pub mod depsuser;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||
pub enum PrebakeStage {
|
||||
Init,
|
||||
Bootstrap,
|
||||
EarlyHook,
|
||||
#[default]
|
||||
DepsSystem,
|
||||
DepsUser,
|
||||
LateHook,
|
||||
@@ -50,9 +51,3 @@ impl FromStr for PrebakeStage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PrebakeStage {
|
||||
fn default() -> Self {
|
||||
PrebakeStage::DepsSystem
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
//! and the target operating system, supporting multiple package managers.
|
||||
|
||||
use crate::ExecutionContext;
|
||||
use crate::constant::BOOTSTRAP_SCRIPT_PATH;
|
||||
use crate::engine::{Engine, EventSender, pm};
|
||||
use crate::error::BootstrapError;
|
||||
use crate::prebake::config::Bootstrap;
|
||||
use crate::prebake::config::PrebakeConfig;
|
||||
use crate::constant::BOOTSTRAP_SCRIPT_PATH;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
@@ -54,7 +54,7 @@ pub async fn bootstrap(
|
||||
let bootstrap_config = config.bootstrap.clone().unwrap_or_default();
|
||||
|
||||
if bootstrap_config.custom.is_none() {
|
||||
let script = generate_bootstrap(&bootstrap_config, &config, &osinfo)?;
|
||||
let script = generate_bootstrap(&bootstrap_config, config, osinfo)?;
|
||||
if ctx.dry_run {
|
||||
log::info!("[DRY_RUN] Generated bootstrap script:\n{}", script);
|
||||
return Ok(());
|
||||
@@ -74,9 +74,9 @@ pub async fn bootstrap(
|
||||
}
|
||||
|
||||
engine
|
||||
.execute_script(&bootstrap_path, ctx, &event_tx)
|
||||
.execute_script(&bootstrap_path, ctx, event_tx)
|
||||
.await
|
||||
.map_err(|e| BootstrapError::ExecutionError(e))?;
|
||||
.map_err(BootstrapError::ExecutionError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ pub async fn bootstrap(
|
||||
fn generate_bootstrap(
|
||||
bootstrap_config: &Bootstrap,
|
||||
config: &PrebakeConfig,
|
||||
osinfo: &os_info::Info
|
||||
osinfo: &os_info::Info,
|
||||
) -> Result<String, BootstrapError> {
|
||||
let mut script = String::new();
|
||||
// Generate header
|
||||
@@ -207,7 +207,10 @@ fn generate_bootstrap(
|
||||
///
|
||||
/// Returns `Some(Box<dyn PackageManager>)` if a suitable package manager is found,
|
||||
/// or `None` if no package manager is configured or detectable.
|
||||
fn get_package_manager(config: &PrebakeConfig, osinfo: &os_info::Info) -> Option<Box<dyn pm::PackageManager>> {
|
||||
fn get_package_manager(
|
||||
config: &PrebakeConfig,
|
||||
osinfo: &os_info::Info,
|
||||
) -> Option<Box<dyn pm::PackageManager>> {
|
||||
config
|
||||
.dependencies
|
||||
.as_ref()
|
||||
@@ -225,15 +228,15 @@ fn get_package_manager(config: &PrebakeConfig, osinfo: &os_info::Info) -> Option
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((name, _)) = sysdeps.iter().next() {
|
||||
if let Some(pm) = pm::select(name) {
|
||||
if let Some((name, _)) = sysdeps.iter().next()
|
||||
&& let Some(pm) = pm::select(name)
|
||||
{
|
||||
log::warn!(
|
||||
"Using package manager: {} (first configured, not detected)",
|
||||
name
|
||||
);
|
||||
return Some(pm);
|
||||
}
|
||||
}
|
||||
|
||||
log::warn!("No valid package manager found in configuration");
|
||||
None
|
||||
|
||||
@@ -42,7 +42,7 @@ pub async fn depssystem(
|
||||
let cmd = package_manager.change_mirror(mirror, Some(&osinfo));
|
||||
log::debug!("Executing: {:?}", &cmd);
|
||||
engine
|
||||
.execute(cmd, &ctx, &event_tx)
|
||||
.execute(cmd, ctx, &event_tx)
|
||||
.await
|
||||
.map_err(|e| DependencyError::StageFailed {
|
||||
stage: "MirrorChange",
|
||||
@@ -59,7 +59,7 @@ pub async fn depssystem(
|
||||
let cmds = package_manager.add_repository(repos, Some(&osinfo));
|
||||
for cmd in cmds {
|
||||
log::debug!("Executing: {:?}", &cmd);
|
||||
engine.execute(cmd, &ctx, &event_tx).await.map_err(|e| {
|
||||
engine.execute(cmd, ctx, &event_tx).await.map_err(|e| {
|
||||
DependencyError::StageFailed {
|
||||
stage: "CustomRepoAdd",
|
||||
source: e,
|
||||
@@ -74,7 +74,7 @@ pub async fn depssystem(
|
||||
let cmd = package_manager.update();
|
||||
log::debug!("Executing: {:?}", &cmd);
|
||||
engine
|
||||
.execute(cmd, &ctx, &event_tx)
|
||||
.execute(cmd, ctx, &event_tx)
|
||||
.await
|
||||
.map_err(|e| DependencyError::StageFailed {
|
||||
stage: "UpdateIndex",
|
||||
@@ -86,7 +86,7 @@ pub async fn depssystem(
|
||||
if !pm_config.packages.is_empty() {
|
||||
log::info!("Installing {} packages", pm_config.packages.len());
|
||||
engine
|
||||
.install_dependency(&pm_config.packages, &*package_manager, &ctx, &event_tx)
|
||||
.install_dependency(&pm_config.packages, &*package_manager, ctx, &event_tx)
|
||||
.await
|
||||
.map_err(|e| DependencyError::StageFailed {
|
||||
stage: "InstallPackage",
|
||||
|
||||
@@ -3,24 +3,73 @@ use serde_yaml::Value;
|
||||
use crate::ExecutionContext;
|
||||
use crate::engine::{EventSender, upm};
|
||||
use crate::error::DependencyError;
|
||||
use crate::types::repology::RepologyEndpoint;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CollectedUPMdeps {
|
||||
pub deps: Vec<(String, upm::UPMSysDeps)>,
|
||||
}
|
||||
|
||||
impl CollectedUPMdeps {
|
||||
pub fn extend(&mut self, other: Self) {
|
||||
self.deps.extend(other.deps);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn collect_upm_sysdeps(
|
||||
config: &HashMap<String, Value>,
|
||||
_ctx: &ExecutionContext,
|
||||
endpoint: &RepologyEndpoint,
|
||||
) -> Result<CollectedUPMdeps, DependencyError> {
|
||||
let mut collected = CollectedUPMdeps::default();
|
||||
|
||||
if *endpoint == RepologyEndpoint::Disabled {
|
||||
return Ok(collected);
|
||||
}
|
||||
|
||||
for (name, cfg) in config {
|
||||
let package_manager = match upm::select(name) {
|
||||
Some(pm) => pm,
|
||||
None => {
|
||||
log::warn!("Unknown user package manager: {}, skipping sysdep collection", name);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let sys_deps = package_manager.system_dependency(cfg);
|
||||
if !sys_deps.packages.is_empty() {
|
||||
log::info!(
|
||||
"UPM {} system deps: {:?} (mapped: {})",
|
||||
name,
|
||||
sys_deps.packages,
|
||||
sys_deps.mapped
|
||||
);
|
||||
collected.deps.push((name.clone(), sys_deps));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(collected)
|
||||
}
|
||||
|
||||
pub async fn depsuser(
|
||||
config: &HashMap<String, Value>,
|
||||
ctx: &ExecutionContext,
|
||||
event_tx: EventSender,
|
||||
_endpoint: &RepologyEndpoint,
|
||||
) -> Result<(), DependencyError> {
|
||||
for (name, cfg) in config {
|
||||
let package_manager = match upm::select(name){
|
||||
let package_manager = match upm::select(name) {
|
||||
Some(pm) => pm,
|
||||
None => {
|
||||
log::error!("Unknown user package manager: {}", name);
|
||||
return Err(DependencyError::UnknownManager(name.to_string()));
|
||||
}
|
||||
};
|
||||
log::info!("Running user package manager: {}", name);
|
||||
|
||||
log::info!("Running user package manager: {}", name);
|
||||
package_manager.main(cfg, ctx, &event_tx).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ pub async fn hook(
|
||||
};
|
||||
log::info!("Executing hook #{}: {}.", index, hook_cmd.name);
|
||||
let result = engine
|
||||
.execute_command_with_delta(&hook_cmd.command, &ctx, &event_tx, &deltactx)
|
||||
.execute_command_with_delta(&hook_cmd.command, ctx, &event_tx, &deltactx)
|
||||
.await;
|
||||
if let Err(e) = result {
|
||||
log::error!("Hook {} failed: {:?}", index, e);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod memsize;
|
||||
pub mod architecture;
|
||||
pub mod builderconfig;
|
||||
pub mod command;
|
||||
pub mod cache;
|
||||
pub mod command;
|
||||
pub mod memsize;
|
||||
pub mod repology;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
|
||||
pub enum Architecture {
|
||||
#[default]
|
||||
NoArch,
|
||||
X86_64,
|
||||
Amd64, // Same as X86_64, normalized when processing
|
||||
@@ -50,9 +51,3 @@ impl FromStr for Architecture {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Architecture {
|
||||
fn default() -> Self {
|
||||
Architecture::NoArch
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct CustomCommand {
|
||||
#[serde(default="default_name")]
|
||||
#[serde(default = "default_name")]
|
||||
pub name: String,
|
||||
pub command: String,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -48,7 +48,6 @@ impl MemSize {
|
||||
pub const fn from_gib(gib: u64) -> Self {
|
||||
Self(gib * 1024 * 1024 * 1024)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for MemSize {
|
||||
@@ -129,14 +128,19 @@ pub fn parse_mem_size(s: &str) -> Result<MemSize, String> {
|
||||
let (num_str, unit_raw) = split_number_and_unit(s)?;
|
||||
|
||||
// Parse the numeric part
|
||||
let num: f64 = num_str.parse().map_err(|e: ParseFloatError| e.to_string())?;
|
||||
let num: f64 = num_str
|
||||
.parse()
|
||||
.map_err(|e: ParseFloatError| e.to_string())?;
|
||||
|
||||
// Normalize unit: remove trailing 'B' if present, but preserve case for error detection
|
||||
let unit = if unit_raw.ends_with('B') || unit_raw.ends_with('b') {
|
||||
let without_b = &unit_raw[..unit_raw.len() - 1];
|
||||
// Check if the original unit ends with lowercase 'b' (bits) before stripping
|
||||
if unit_raw.ends_with('b') && !unit_raw.ends_with('B') {
|
||||
return Err(format!("bit unit 'b' is not allowed, use 'B' for bytes: {}", unit_raw));
|
||||
return Err(format!(
|
||||
"bit unit 'b' is not allowed, use 'B' for bytes: {}",
|
||||
unit_raw
|
||||
));
|
||||
}
|
||||
without_b
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub enum RepologyEndpoint {
|
||||
#[default]
|
||||
Default,
|
||||
Disabled,
|
||||
None,
|
||||
Local,
|
||||
Remote,
|
||||
Server,
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for RepologyEndpoint {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::{self, Visitor};
|
||||
use std::fmt;
|
||||
|
||||
struct RepologyEndpointVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for RepologyEndpointVisitor {
|
||||
type Value = RepologyEndpoint;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("disabled, none, local, remote, server, or default")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
match v {
|
||||
"disabled" => Ok(RepologyEndpoint::Disabled),
|
||||
"none" => Ok(RepologyEndpoint::None),
|
||||
"local" => Ok(RepologyEndpoint::Local),
|
||||
"remote" => Ok(RepologyEndpoint::Remote),
|
||||
"server" => Ok(RepologyEndpoint::Server),
|
||||
"default" | "" => Ok(RepologyEndpoint::Default),
|
||||
_ => Err(de::Error::unknown_variant(
|
||||
v,
|
||||
&["disabled", "none", "local", "remote", "server", "default"],
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(RepologyEndpointVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for RepologyEndpoint {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
RepologyEndpoint::Default => serializer.serialize_str("default"),
|
||||
RepologyEndpoint::Disabled => serializer.serialize_str("disabled"),
|
||||
RepologyEndpoint::None => serializer.serialize_str("none"),
|
||||
RepologyEndpoint::Local => serializer.serialize_str("local"),
|
||||
RepologyEndpoint::Remote => serializer.serialize_str("remote"),
|
||||
RepologyEndpoint::Server => serializer.serialize_str("server"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# workshop-llm-detector
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
LLM-powered repository analyzer that generates pipeline configurations by combining AI analysis with language-specific cookbook templates.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
src/
|
||||
├── llm/ # LLM client abstraction
|
||||
│ ├── client.rs # LLMClient trait (async_trait)
|
||||
│ ├── ollama.rs # OllamaClient implementation
|
||||
│ └── openai.rs # OpenAIClient implementation
|
||||
├── cookbook/ # Language-specific build configs
|
||||
│ └── mod.rs # CookbookManager + Cookbook struct
|
||||
├── llm_output.rs # LLMOutput parsed response struct
|
||||
└── lib.rs # CICDLLMHelper entry point
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
|
||||
| Symbol | Type | Location | Role |
|
||||
|--------|------|----------|------|
|
||||
| CICDLLMHelper | struct | lib.rs:16 | Main entry point; analyze_repository() orchestrates LLM + cookbook merge |
|
||||
| LLMClient | trait | llm/client.rs:5 | Async trait for generate(prompt) -> Result<String> |
|
||||
| OllamaClient | struct | llm/ollama.rs:8 | Ollama local LLM adapter |
|
||||
| OpenAIClient | struct | llm/openai.rs:8 | OpenAI API adapter |
|
||||
| CookbookManager | struct | cookbook/mod.rs:26 | Loads .cookbook YAML files; get_cookbook(project_type) |
|
||||
| Cookbook | struct | cookbook/mod.rs:7 | Language-specific deps, env, artifacts template |
|
||||
| LLMOutput | struct | llm_output.rs:5 | Parsed YAML from LLM (project_type, dependencies, build_steps, artifacts) |
|
||||
| AnalysisResult | struct | lib.rs:157 | Merged LLM + cookbook output |
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
- **Async trait**: Uses `async_trait::async_trait` macro (not native async fn in traits)
|
||||
- **Cookbook files**: `*.cookbook.yaml` in cookbook directory; maps project_type -> Cookbook
|
||||
- **LLM prompt**: Builds YAML requiring project_type, dependencies, build_steps, artifacts, environment fields
|
||||
- **Output templates**: Writes prebake.tmpl.yaml, finalize.tmpl.yaml, build.sh via serde_yaml
|
||||
- **Error handling**: thiserror for CookbookError, anyhow for runtime errors
|
||||
Generated
-375
@@ -1,375 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[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 = "regex"
|
||||
version = "1.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.110"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
|
||||
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 = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "workshop-pipeline"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"env_logger",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror",
|
||||
]
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "workshop-pipeline"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.5.0"
|
||||
log = "0.4.28"
|
||||
regex = "1.12.2"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
serde_yaml = "0.9.34"
|
||||
thiserror = "2.0.17"
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.11.8"
|
||||
@@ -1,217 +0,0 @@
|
||||
#!/bin/bash
|
||||
# .workshop/bake.sh
|
||||
# 构建阶段脚本 - 使用标准的bash语法
|
||||
|
||||
set -euo pipefail # 严格模式:错误退出、未定义变量检查、管道错误检查
|
||||
|
||||
# 切换到工作目录
|
||||
cd "$WORKSPACE_DIR"
|
||||
|
||||
# 创建必要的目录
|
||||
mkdir -p target
|
||||
mkdir -p "$CARGO_REGISTRY"
|
||||
mkdir -p "$CARGO_GIT"
|
||||
|
||||
# 配置Cargo使用缓存目录
|
||||
export CARGO_HOME="$BUILD_CACHE_DIR/cargo"
|
||||
export CARGO_TARGET_DIR="$WORKSPACE_DIR/target"
|
||||
|
||||
# 构建前检查
|
||||
log "Starting build process"
|
||||
log "Build ID: $BUILD_ID"
|
||||
log "Commit: $COMMIT_SHA"
|
||||
log "Branch: $BRANCH_NAME"
|
||||
log "Rust version: $RUST_VERSION"
|
||||
|
||||
# 检查必要的工具
|
||||
check_requirements() {
|
||||
log "Checking build requirements..."
|
||||
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
log_error "Cargo not found. Please ensure Rust is installed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v git &> /dev/null; then
|
||||
log_error "Git not found. Please install git."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "All requirements satisfied"
|
||||
}
|
||||
|
||||
# 代码质量检查
|
||||
run_lints() {
|
||||
log "Running code quality checks..."
|
||||
|
||||
# Rustfmt检查代码格式
|
||||
if command -v rustfmt &> /dev/null; then
|
||||
log "Checking code formatting with rustfmt..."
|
||||
cargo fmt -- --check || {
|
||||
log_warn "Code formatting issues found"
|
||||
# 非致命错误,继续构建
|
||||
}
|
||||
fi
|
||||
|
||||
# Clippy静态分析
|
||||
log "Running Clippy..."
|
||||
cargo clippy -- -D warnings || {
|
||||
log_warn "Clippy warnings found"
|
||||
# 非致命错误,继续构建
|
||||
}
|
||||
|
||||
# 安全检查
|
||||
if command -v cargo-audit &> /dev/null; then
|
||||
log "Checking for security vulnerabilities..."
|
||||
cargo audit || {
|
||||
log_warn "Security vulnerabilities found"
|
||||
# 非致命错误,继续构建
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
# 运行测试
|
||||
run_tests() {
|
||||
log "Running tests..."
|
||||
|
||||
# 单元测试
|
||||
cargo test --lib --bins --tests || {
|
||||
log_error "Unit tests failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 集成测试(如果存在)
|
||||
if [ -d "tests" ] && [ -n "$(find tests -name '*.rs' -print -quit)" ]; then
|
||||
cargo test --tests || {
|
||||
log_error "Integration tests failed"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
# 文档测试
|
||||
cargo test --doc || {
|
||||
log_warn "Documentation tests failed"
|
||||
# 文档测试失败不阻断构建
|
||||
}
|
||||
}
|
||||
|
||||
# 构建项目
|
||||
build_project() {
|
||||
log "Building project..."
|
||||
|
||||
case "$BUILD_TYPE" in
|
||||
debug)
|
||||
log "Building in debug mode"
|
||||
cargo build
|
||||
;;
|
||||
release)
|
||||
log "Building in release mode"
|
||||
cargo build --release
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown build type: $BUILD_TYPE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# 检查构建是否成功
|
||||
if [ $? -eq 0 ]; then
|
||||
log "Build completed successfully"
|
||||
else
|
||||
log_error "Build failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 生成文档
|
||||
generate_docs() {
|
||||
log "Generating documentation..."
|
||||
|
||||
if [ "$BUILD_TYPE" = "release" ]; then
|
||||
cargo doc --no-deps --release || {
|
||||
log_warn "Documentation generation failed"
|
||||
# 文档生成失败不阻断构建
|
||||
}
|
||||
else
|
||||
cargo doc --no-deps || {
|
||||
log_warn "Documentation generation failed"
|
||||
# 文档生成失败不阻断构建
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
# 创建构建产物清单
|
||||
create_artifact_manifest() {
|
||||
log "Creating artifact manifest..."
|
||||
|
||||
local manifest_file="$WORKSPACE_DIR/artifacts.json"
|
||||
|
||||
cat > "$manifest_file" << EOF
|
||||
{
|
||||
"build_id": "$BUILD_ID",
|
||||
"commit_sha": "$COMMIT_SHA",
|
||||
"branch": "$BRANCH_NAME",
|
||||
"build_type": "$BUILD_TYPE",
|
||||
"timestamp": "$(date -Iseconds)",
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "binary",
|
||||
"path": "target/$BUILD_TYPE/$(basename "$WORKSPACE_DIR")",
|
||||
"type": "executable"
|
||||
},
|
||||
{
|
||||
"name": "documentation",
|
||||
"path": "target/doc",
|
||||
"type": "directory"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
log "Artifact manifest created: $manifest_file"
|
||||
}
|
||||
|
||||
# 构建后清理
|
||||
cleanup() {
|
||||
log "Performing build cleanup..."
|
||||
|
||||
# 清理临时文件
|
||||
find . -name "*.tmp" -delete
|
||||
find . -name "*.log" -delete
|
||||
|
||||
# 保留构建缓存
|
||||
log "Build cache preserved in: $BUILD_CACHE_DIR"
|
||||
}
|
||||
|
||||
# 主构建流程
|
||||
main() {
|
||||
log "Starting bake phase"
|
||||
|
||||
check_requirements
|
||||
run_lints
|
||||
run_tests
|
||||
build_project
|
||||
generate_docs
|
||||
create_artifact_manifest
|
||||
cleanup
|
||||
|
||||
log "Bake phase completed successfully"
|
||||
|
||||
# 输出构建摘要
|
||||
echo "=========================================="
|
||||
log_info "Build Summary"
|
||||
echo "------------------------------------------"
|
||||
log_info "Build ID: $BUILD_ID"
|
||||
log_info "Status: SUCCESS"
|
||||
log_info "Artifacts:"
|
||||
log_info " - Binary: target/$BUILD_TYPE/$(basename "$WORKSPACE_DIR")"
|
||||
log_info " - Documentation: target/doc/"
|
||||
log_info " - Manifest: artifacts.json"
|
||||
echo "=========================================="
|
||||
}
|
||||
|
||||
# 信号处理
|
||||
trap 'log_error "Build interrupted"; exit 130' INT TERM
|
||||
|
||||
# 执行主函数
|
||||
main "$@"
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Color for output
|
||||
# 颜色定义用于输出
|
||||
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, 无颜色
|
||||
|
||||
# logging function
|
||||
# 日志辅助函数
|
||||
bake_log() {
|
||||
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $*${NC}"
|
||||
}
|
||||
|
||||
bake_log_warn() {
|
||||
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*${NC}"
|
||||
}
|
||||
|
||||
bake_log_error() {
|
||||
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*${NC}"
|
||||
}
|
||||
|
||||
bake_log_info() {
|
||||
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*${NC}"
|
||||
}
|
||||
|
||||
{{ main }}
|
||||
@@ -1,270 +0,0 @@
|
||||
# finalize.yaml.tmpl
|
||||
# 版本号
|
||||
version: "1.0"
|
||||
|
||||
# 产物定义
|
||||
artifacts:
|
||||
# 主要构建产物
|
||||
primary:
|
||||
- name: "application-binary"
|
||||
type: "executable"
|
||||
path: "target/release/{{.project_name}}"
|
||||
platform: "linux/x86_64"
|
||||
permissions: "755"
|
||||
description: "Main application binary"
|
||||
|
||||
- name: "static-assets"
|
||||
type: "directory"
|
||||
path: "static"
|
||||
include_patterns:
|
||||
- "**/*.css"
|
||||
- "**/*.js"
|
||||
- "**/*.png"
|
||||
- "**/*.jpg"
|
||||
exclude_patterns:
|
||||
- "**/*.test.*"
|
||||
- "**/node_modules/**"
|
||||
|
||||
# 辅助产物
|
||||
secondary:
|
||||
- name: "documentation"
|
||||
type: "directory"
|
||||
path: "target/doc"
|
||||
compress: true
|
||||
format: "tar.gz"
|
||||
|
||||
- name: "build-logs"
|
||||
type: "file"
|
||||
path: "build.log"
|
||||
retention_days: 30
|
||||
|
||||
- name: "test-results"
|
||||
type: "directory"
|
||||
path: "test-results"
|
||||
format: "junit" # 支持junit, tap, custom
|
||||
|
||||
# 打包配置
|
||||
packaging:
|
||||
# 主要包配置
|
||||
primary_package:
|
||||
format: "tar.gz" # tar.gz, zip, docker, oci
|
||||
output: "{{.project_name}}-{{.version}}-{{.platform}}.{{.format}}"
|
||||
include:
|
||||
- "artifacts/application-binary"
|
||||
- "artifacts/static-assets"
|
||||
- "LICENSE"
|
||||
- "README.md"
|
||||
exclude:
|
||||
- "**/*.log"
|
||||
- "**/*.tmp"
|
||||
|
||||
# Docker镜像配置
|
||||
docker:
|
||||
enabled: true
|
||||
registry: "registry.example.com"
|
||||
repository: "{{.project_name}}"
|
||||
tags:
|
||||
- "{{.version}}"
|
||||
- "latest"
|
||||
- "{{.branch}}"
|
||||
dockerfile: "Dockerfile.final"
|
||||
build_args:
|
||||
- "VERSION={{.version}}"
|
||||
- "BUILD_ID={{.build_id}}"
|
||||
labels:
|
||||
org.label-schema.version: "{{.version}}"
|
||||
org.label-schema.build-date: "{{.build_date}}"
|
||||
org.label-schema.vcs-ref: "{{.commit_sha}}"
|
||||
|
||||
# 发布配置
|
||||
distribution:
|
||||
# 本地存储
|
||||
local:
|
||||
directory: "/artifacts/{{.project_name}}/{{.version}}"
|
||||
retention_days: 90
|
||||
|
||||
# 对象存储
|
||||
s3:
|
||||
enabled: true
|
||||
bucket: "my-artifacts"
|
||||
prefix: "{{.project_name}}/{{.version}}"
|
||||
region: "us-east-1"
|
||||
acl: "private"
|
||||
storage_class: "STANDARD"
|
||||
|
||||
# 包管理器仓库
|
||||
package_registries:
|
||||
crates_io:
|
||||
enabled: true
|
||||
token: "{{env.CRATES_IO_TOKEN}}"
|
||||
|
||||
npm:
|
||||
enabled: false
|
||||
registry: "https://registry.npmjs.org/"
|
||||
token: "{{env.NPM_TOKEN}}"
|
||||
|
||||
docker_registry:
|
||||
enabled: true
|
||||
registry: "registry.example.com"
|
||||
username: "{{env.DOCKER_USERNAME}}"
|
||||
password: "{{env.DOCKER_PASSWORD}}"
|
||||
|
||||
# 质量门禁
|
||||
quality_gates:
|
||||
# 测试覆盖率要求
|
||||
|
||||
# 部署配置
|
||||
deployment:
|
||||
# 环境定义
|
||||
environments:
|
||||
staging:
|
||||
type: "kubernetes"
|
||||
namespace: "staging"
|
||||
cluster: "staging-cluster"
|
||||
manifests:
|
||||
- "k8s/deployment.yaml"
|
||||
- "k8s/service.yaml"
|
||||
health_check:
|
||||
endpoint: "/health"
|
||||
timeout: 30
|
||||
interval: 10
|
||||
|
||||
production:
|
||||
type: "kubernetes"
|
||||
namespace: "production"
|
||||
cluster: "production-cluster"
|
||||
manual_approval: true
|
||||
rollout_strategy: "blue-green"
|
||||
manifests:
|
||||
- "k8s/deployment.yaml"
|
||||
|
||||
# 部署策略
|
||||
strategy:
|
||||
type: "rolling" # rolling, blue-green, canary
|
||||
max_unavailable: "25%"
|
||||
max_surge: "25%"
|
||||
wait_timeout: 300
|
||||
|
||||
# 通知配置
|
||||
notifications:
|
||||
# 成功通知
|
||||
on_success:
|
||||
- type: "slack"
|
||||
webhook: "{{env.SLACK_WEBHOOK}}"
|
||||
channel: "#builds"
|
||||
message: "Build {{.build_id}} succeeded for {{.project_name}}"
|
||||
|
||||
- type: "email"
|
||||
to:
|
||||
- "team@example.com"
|
||||
subject: "Build Success: {{.project_name}} v{{.version}}"
|
||||
message: "Build {{.build_id}} succeeded for {{.project_name}} v{{.version}}"
|
||||
template: "success_email.html"
|
||||
|
||||
# 失败通知
|
||||
on_failure:
|
||||
- type: "slack"
|
||||
webhook: "{{env.SLACK_WEBHOOK}}"
|
||||
channel: "#build-alerts"
|
||||
message: "🚨 Build {{.build_id}} failed for {{.project_name}}"
|
||||
|
||||
- type: "pagerduty"
|
||||
service_key: "{{env.PAGERDUTY_KEY}}"
|
||||
message: "Build {{.build_id}} failed for {{.project_name}} v{{.version}}"
|
||||
severity: "error"
|
||||
|
||||
# 部署通知
|
||||
on_deploy:
|
||||
- type: "webhook"
|
||||
url: "https://api.example.com/deployments"
|
||||
method: "POST"
|
||||
headers:
|
||||
Authorization: "Bearer {{env.DEPLOY_WEBHOOK_TOKEN}}"
|
||||
message: ""
|
||||
|
||||
# 清理策略
|
||||
cleanup:
|
||||
# 构建产物清理
|
||||
artifacts:
|
||||
keep_latest: 10
|
||||
keep_tags:
|
||||
- "latest"
|
||||
- "stable"
|
||||
- "v*.*.*"
|
||||
older_than_days: 30
|
||||
|
||||
# 构建环境清理
|
||||
environment:
|
||||
containers: true
|
||||
volumes: true
|
||||
networks: true
|
||||
images: true
|
||||
|
||||
# 缓存清理
|
||||
cache:
|
||||
strategy: "lru"
|
||||
max_size_gb: 50
|
||||
ttl_days: 7
|
||||
|
||||
# 钩子脚本
|
||||
hooks:
|
||||
# 打包前钩子
|
||||
pre_package:
|
||||
- name: "validate-artifacts"
|
||||
command: "./scripts/validate-artifacts.sh"
|
||||
timeout: 60
|
||||
|
||||
- name: "generate-changelog"
|
||||
command: "git log --oneline {{.previous_version}}..{{.current_version}} > CHANGELOG.md"
|
||||
|
||||
# 打包后钩子
|
||||
post_package:
|
||||
- name: "verify-package"
|
||||
command: "./scripts/verify-package.sh {{.package_path}}"
|
||||
|
||||
# 发布前钩子
|
||||
pre_release:
|
||||
- name: "notify-stakeholders"
|
||||
command: "./scripts/notify-stakeholders.sh"
|
||||
|
||||
# 发布后钩子
|
||||
post_release:
|
||||
- name: "update-dashboard"
|
||||
command: "curl -X POST https://dashboard.example.com/api/deployments"
|
||||
|
||||
# 部署后钩子
|
||||
post_deploy:
|
||||
- name: "run-smoke-tests"
|
||||
command: "./scripts/smoke-tests.sh {{.deployment_url}}"
|
||||
timeout: 300
|
||||
|
||||
# 变量替换上下文
|
||||
variables:
|
||||
# 内置变量(由CI/CD系统提供)
|
||||
builtin:
|
||||
- "project_name"
|
||||
- "version"
|
||||
- "build_id"
|
||||
- "commit_sha"
|
||||
- "branch"
|
||||
- "build_date"
|
||||
- "platform"
|
||||
|
||||
# 自定义变量
|
||||
custom:
|
||||
deployment_url: "https://{{.project_name}}-{{.environment}}.example.com"
|
||||
release_notes: "Automated build {{.build_id}}"
|
||||
|
||||
# 环境变量映射
|
||||
environment:
|
||||
- "CRATES_IO_TOKEN"
|
||||
- "DOCKER_USERNAME"
|
||||
- "SLACK_WEBHOOK"
|
||||
- "PAGERDUTY_KEY"
|
||||
|
||||
# 元数据
|
||||
metadata:
|
||||
description: "Finalization configuration for {{.project_name}}"
|
||||
schema_version: "1.0"
|
||||
maintainer: "devops@example.com"
|
||||
last_updated: "2024-01-01"
|
||||
@@ -1,177 +0,0 @@
|
||||
# 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" # 自定义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:
|
||||
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"
|
||||
@@ -1,27 +0,0 @@
|
||||
use workshop_pipeline::*;
|
||||
|
||||
fn main() -> PipelineResult<()> {
|
||||
env_logger::init();
|
||||
// 创建变量提供者
|
||||
let variable_provider = DefaultVariableProvider::new()
|
||||
.with_variable("project_name", "my-awesome-project")
|
||||
.with_variable("version", "1.0.0")
|
||||
.with_secret("CRATES_IO_TOKEN", "super-secret-token");
|
||||
|
||||
// 加载流水线配置
|
||||
let config = PipelineConfig::load_from_dir(
|
||||
"./examples",
|
||||
"user123",
|
||||
"pipeline456",
|
||||
&variable_provider,
|
||||
)?;
|
||||
|
||||
// 输出完整配置(用于engine)
|
||||
println!("完整配置: {:#?}", config);
|
||||
|
||||
// 输出遮蔽后的配置(用于日志)
|
||||
let output_config = config.for_output();
|
||||
println!("遮蔽配置: {:#?}", output_config);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
pub mod prebake;
|
||||
pub mod finalize;
|
||||
pub mod scripts;
|
||||
|
||||
pub use prebake::*;
|
||||
pub use finalize::*;
|
||||
pub use scripts::*;
|
||||
@@ -1,99 +0,0 @@
|
||||
mod artifacts;
|
||||
mod packaging;
|
||||
mod distribution;
|
||||
mod deployment;
|
||||
mod notifications;
|
||||
mod quality;
|
||||
|
||||
pub use artifacts::*;
|
||||
pub use packaging::*;
|
||||
pub use distribution::*;
|
||||
pub use deployment::*;
|
||||
pub use notifications::*;
|
||||
pub use quality::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Finalize配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FinalizeConfig {
|
||||
pub version: String,
|
||||
pub artifacts: Option<ArtifactsConfig>,
|
||||
pub packaging: Option<PackagingConfig>,
|
||||
pub distribution: Option<DistributionConfig>,
|
||||
pub quality_gates: Option<QualityGates>,
|
||||
pub deployment: Option<DeploymentConfig>,
|
||||
pub notifications: Option<NotificationsConfig>,
|
||||
pub cleanup: Option<CleanupConfig>,
|
||||
pub hooks: Option<FinalizeHooks>,
|
||||
pub variables: Option<VariableContext>,
|
||||
pub metadata: Option<FinalizeMetadata>,
|
||||
}
|
||||
|
||||
/// 清理配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CleanupConfig {
|
||||
pub artifacts: ArtifactCleanup,
|
||||
pub environment: EnvironmentCleanup,
|
||||
pub cache: CacheCleanup,
|
||||
}
|
||||
|
||||
/// 产物清理配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ArtifactCleanup {
|
||||
pub keep_latest: u32,
|
||||
pub keep_tags: Vec<String>,
|
||||
pub older_than_days: u32,
|
||||
}
|
||||
|
||||
/// 环境清理配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EnvironmentCleanup {
|
||||
pub containers: bool,
|
||||
pub volumes: bool,
|
||||
pub networks: bool,
|
||||
pub images: bool,
|
||||
}
|
||||
|
||||
/// 缓存清理配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CacheCleanup {
|
||||
pub strategy: String,
|
||||
pub max_size_gb: u32,
|
||||
pub ttl_days: u32,
|
||||
}
|
||||
|
||||
/// Finalize钩子配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FinalizeHooks {
|
||||
pub pre_package: Option<Vec<FinalizeHook>>,
|
||||
pub post_package: Option<Vec<FinalizeHook>>,
|
||||
pub pre_release: Option<Vec<FinalizeHook>>,
|
||||
pub post_release: Option<Vec<FinalizeHook>>,
|
||||
pub post_deploy: Option<Vec<FinalizeHook>>,
|
||||
}
|
||||
|
||||
/// Finalize钩子
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FinalizeHook {
|
||||
pub name: String,
|
||||
pub command: String,
|
||||
pub timeout: Option<u32>,
|
||||
}
|
||||
|
||||
/// 变量上下文
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VariableContext {
|
||||
pub builtin: Vec<String>,
|
||||
pub custom: std::collections::HashMap<String, String>,
|
||||
pub environment: Vec<String>,
|
||||
}
|
||||
|
||||
/// Finalize元数据
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FinalizeMetadata {
|
||||
pub description: String,
|
||||
pub schema_version: String,
|
||||
pub maintainer: String,
|
||||
pub last_updated: String,
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 产物配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ArtifactsConfig {
|
||||
pub primary: Option<Vec<Artifact>>,
|
||||
pub secondary: Option<Vec<Artifact>>,
|
||||
}
|
||||
|
||||
/// 产物
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Artifact {
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String,
|
||||
pub path: String,
|
||||
pub platform: Option<String>,
|
||||
pub permissions: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub include_patterns: Option<Vec<String>>,
|
||||
pub exclude_patterns: Option<Vec<String>>,
|
||||
pub compress: Option<bool>,
|
||||
pub format: Option<String>,
|
||||
pub retention_days: Option<u32>,
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 部署配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeploymentConfig {
|
||||
pub environments: HashMap<String, Environment>,
|
||||
pub strategy: DeploymentStrategy,
|
||||
}
|
||||
|
||||
/// 环境配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Environment {
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String,
|
||||
pub namespace: String,
|
||||
pub cluster: String,
|
||||
pub manifests: Vec<String>,
|
||||
pub health_check: Option<HealthCheck>,
|
||||
pub manual_approval: Option<bool>,
|
||||
pub rollout_strategy: Option<String>,
|
||||
}
|
||||
|
||||
/// 健康检查配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HealthCheck {
|
||||
pub endpoint: String,
|
||||
pub timeout: u32,
|
||||
pub interval: u32,
|
||||
}
|
||||
|
||||
/// 部署策略
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeploymentStrategy {
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String,
|
||||
pub max_unavailable: String,
|
||||
pub max_surge: String,
|
||||
pub wait_timeout: u32,
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 发布配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DistributionConfig {
|
||||
pub local: Option<LocalStorage>,
|
||||
pub s3: Option<S3Storage>,
|
||||
pub package_registries: Option<PackageRegistries>,
|
||||
}
|
||||
|
||||
/// 本地存储配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LocalStorage {
|
||||
pub directory: String,
|
||||
pub retention_days: u32,
|
||||
}
|
||||
|
||||
/// S3存储配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct S3Storage {
|
||||
pub enabled: bool,
|
||||
pub bucket: String,
|
||||
pub prefix: String,
|
||||
pub region: String,
|
||||
pub acl: String,
|
||||
pub storage_class: String,
|
||||
}
|
||||
|
||||
/// 包注册表配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PackageRegistries {
|
||||
pub crates_io: Option<CratesIoConfig>,
|
||||
pub npm: Option<NpmConfig>,
|
||||
pub docker_registry: Option<DockerRegistryConfig>,
|
||||
}
|
||||
|
||||
/// Crates.io配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CratesIoConfig {
|
||||
pub enabled: bool,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
/// NPM配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NpmConfig {
|
||||
pub enabled: bool,
|
||||
pub registry: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
/// Docker注册表配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DockerRegistryConfig {
|
||||
pub enabled: bool,
|
||||
pub registry: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 通知配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NotificationsConfig {
|
||||
pub on_success: Vec<Notification>,
|
||||
pub on_failure: Vec<Notification>,
|
||||
pub on_deploy: Vec<Notification>,
|
||||
}
|
||||
|
||||
/// 通知
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Notification {
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String,
|
||||
pub webhook: Option<String>,
|
||||
pub channel: Option<String>,
|
||||
pub message: String,
|
||||
pub to: Option<Vec<String>>,
|
||||
pub subject: Option<String>,
|
||||
pub template: Option<String>,
|
||||
pub url: Option<String>,
|
||||
pub method: Option<String>,
|
||||
pub headers: Option<HashMap<String, String>>,
|
||||
pub service_key: Option<String>,
|
||||
pub severity: Option<String>,
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 打包配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PackagingConfig {
|
||||
pub primary_package: Option<PrimaryPackage>,
|
||||
pub docker: Option<DockerPackaging>,
|
||||
}
|
||||
|
||||
/// 主要包配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PrimaryPackage {
|
||||
pub format: String,
|
||||
pub output: String,
|
||||
pub include: Vec<String>,
|
||||
pub exclude: Vec<String>,
|
||||
}
|
||||
|
||||
/// Docker打包配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DockerPackaging {
|
||||
pub enabled: bool,
|
||||
pub registry: String,
|
||||
pub repository: String,
|
||||
pub tags: Vec<String>,
|
||||
pub dockerfile: String,
|
||||
pub build_args: Vec<String>,
|
||||
pub labels: HashMap<String, String>,
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 质量门禁
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct QualityGates {
|
||||
pub test_coverage: Option<TestCoverageGate>,
|
||||
pub security_scan: Option<SecurityScanGate>,
|
||||
pub code_quality: Option<CodeQualityGate>,
|
||||
}
|
||||
|
||||
/// 测试覆盖率门禁
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TestCoverageGate {
|
||||
pub enabled: bool,
|
||||
pub minimum: f32,
|
||||
pub file: String,
|
||||
}
|
||||
|
||||
/// 安全扫描门禁
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SecurityScanGate {
|
||||
pub enabled: bool,
|
||||
pub tools: Vec<String>,
|
||||
pub fail_on: String,
|
||||
pub exceptions: Vec<String>,
|
||||
}
|
||||
|
||||
/// 代码质量门禁
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CodeQualityGate {
|
||||
pub enabled: bool,
|
||||
pub maximum_duplication: f32,
|
||||
pub maximum_complexity: f32,
|
||||
pub minimum_maintainability: f32,
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
mod environment;
|
||||
mod dependencies;
|
||||
mod security;
|
||||
mod hooks;
|
||||
|
||||
use std::collections::HashMap;
|
||||
pub use environment::*;
|
||||
pub use dependencies::*;
|
||||
pub use security::*;
|
||||
pub use hooks::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Prebake配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PrebakeConfig {
|
||||
pub version: String,
|
||||
pub environment: EnvironmentConfig,
|
||||
pub dependencies: Option<DependenciesConfig>,
|
||||
pub environment_variables: Option<EnvironmentVariables>,
|
||||
pub cache: Option<CacheConfig>,
|
||||
pub security: Option<SecurityConfig>,
|
||||
pub hooks: Option<HooksConfig>,
|
||||
pub metadata: Option<Metadata>,
|
||||
}
|
||||
|
||||
/// 环境变量配置 - 调整阶段
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EnvironmentVariables {
|
||||
pub prebake: Option<HashMap<String, String>>,
|
||||
pub bake: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
/// 缓存配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CacheConfig {
|
||||
pub directories: Vec<CacheDirectory>,
|
||||
pub strategy: Option<CacheStrategy>,
|
||||
}
|
||||
|
||||
/// 缓存目录配置 - 更新字段
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CacheDirectory {
|
||||
pub path: String,
|
||||
#[serde(default = "default_cache_strategy")]
|
||||
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,
|
||||
}
|
||||
|
||||
/// 缓存策略
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CacheStrategy {
|
||||
pub ttl_days: u32,
|
||||
pub max_size_gb: u32,
|
||||
pub cleanup_policy: String,
|
||||
}
|
||||
|
||||
/// 安全配置 - 留空
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SecurityConfig {
|
||||
// 根据新模板,暂时留空
|
||||
}
|
||||
|
||||
/// 元数据
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Metadata {
|
||||
pub description: String,
|
||||
pub maintainer: String,
|
||||
pub tags: Vec<String>,
|
||||
pub project_type: String,
|
||||
}
|
||||
|
||||
// 默认值函数
|
||||
fn default_cache_strategy() -> CacheStrategyType {
|
||||
CacheStrategyType::Always
|
||||
}
|
||||
|
||||
fn default_cache_mode() -> CacheMode {
|
||||
CacheMode::Zstd
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 依赖配置 - 调整顺序
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DependenciesConfig {
|
||||
pub custom_pre: Option<Vec<CustomDependency>>,
|
||||
pub system: Option<SystemDependencies>,
|
||||
pub languages: Option<LanguageDependencies>,
|
||||
pub custom: Option<Vec<CustomDependency>>,
|
||||
}
|
||||
|
||||
/// 系统依赖 - 按发行版组织
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SystemDependencies {
|
||||
pub ubuntu: Option<DistributionDeps>,
|
||||
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)]
|
||||
pub struct LanguageDependencies {
|
||||
pub rust: Option<RustConfig>,
|
||||
// 其他语言暂时保持不变...
|
||||
pub node: Option<NodeConfig>,
|
||||
pub python: Option<PythonConfig>,
|
||||
pub go: Option<GoConfig>,
|
||||
}
|
||||
|
||||
/// Rust配置 - 简化
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RustConfig {
|
||||
pub toolchain: String,
|
||||
}
|
||||
|
||||
/// Node配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodeConfig {
|
||||
pub version: String,
|
||||
pub package_manager: String,
|
||||
pub registry: String,
|
||||
}
|
||||
|
||||
/// Python配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PythonConfig {
|
||||
pub version: String,
|
||||
pub package_manager: String,
|
||||
pub requirements_file: String,
|
||||
}
|
||||
|
||||
/// Go配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GoConfig {
|
||||
pub version: String,
|
||||
pub modules: bool,
|
||||
}
|
||||
|
||||
// TODO: 更多语言配置
|
||||
|
||||
/// 自定义依赖
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CustomDependency {
|
||||
pub name: String,
|
||||
pub command: String,
|
||||
pub environment: Option<HashMap<String, String>>,
|
||||
pub working_dir: Option<String>,
|
||||
#[serde(default = "default_timeout")]
|
||||
pub timeout: u32,
|
||||
}
|
||||
|
||||
fn default_timeout() -> u32 { 300 }
|
||||
@@ -1,117 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 环境配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EnvironmentConfig {
|
||||
pub builder: String,
|
||||
pub config: BuilderConfig,
|
||||
pub resources: ResourceRequirements,
|
||||
pub network: NetworkConfig,
|
||||
}
|
||||
|
||||
/// 构建器配置 - 现在各个配置是互斥的
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BuilderConfig {
|
||||
pub baremetal: Option<BaremetalConfig>,
|
||||
pub docker: Option<DockerConfig>,
|
||||
pub firecracker: Option<FirecrackerConfig>,
|
||||
pub custom: Option<CustomBuilderConfig>,
|
||||
}
|
||||
|
||||
/// 裸机配置 - 现在不需要具体配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BaremetalConfig {
|
||||
// 根据新模板,baremetal不需要配置项
|
||||
}
|
||||
|
||||
/// Docker配置 - 更新字段
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DockerConfig {
|
||||
/// 镜像名
|
||||
pub image: Option<String>,
|
||||
/// Dockerfile路径,相对于项目根目录
|
||||
pub dockerfile: Option<String>,
|
||||
/// Docker构建参数,可选,只能与dockerfile搭配使用
|
||||
pub build_args: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Firecracker配置 - 留空
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FirecrackerConfig {
|
||||
// 根据新模板,暂时留空
|
||||
}
|
||||
|
||||
/// 自定义构建器配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CustomBuilderConfig {
|
||||
pub name: String,
|
||||
pub setup_script: String,
|
||||
pub cleanup_script: String,
|
||||
}
|
||||
|
||||
/// 资源需求 - 更新字段
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
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)]
|
||||
pub gpu: bool,
|
||||
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)]
|
||||
pub struct NetworkConfig {
|
||||
#[serde(default = "default_network_enabled")]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_outbound")]
|
||||
pub outbound: bool,
|
||||
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 }
|
||||
@@ -1,17 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 钩子配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HooksConfig {
|
||||
pub pre_dependencies: Option<Vec<Hook>>,
|
||||
pub post_dependencies: Option<Vec<Hook>>,
|
||||
pub ready: Option<Vec<Hook>>,
|
||||
}
|
||||
|
||||
/// 钩子
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Hook {
|
||||
pub command: String,
|
||||
pub working_dir: Option<String>,
|
||||
pub timeout: Option<u32>,
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 安全配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SecurityConfig {
|
||||
pub capabilities: Vec<String>,
|
||||
pub seccomp: String,
|
||||
pub apparmor: bool,
|
||||
pub no_new_privileges: bool,
|
||||
pub limits: ResourceLimits,
|
||||
}
|
||||
|
||||
/// 资源限制
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResourceLimits {
|
||||
pub max_processes: u32,
|
||||
pub max_file_size: String,
|
||||
pub max_open_files: u32,
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Bake脚本
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BakeScript {
|
||||
pub content: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl BakeScript {
|
||||
pub fn new(content: String) -> Self {
|
||||
Self {
|
||||
content,
|
||||
path: ".workshop/bake.sh".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 为bench.sh预留的结构
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BenchScript {
|
||||
pub content: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl BenchScript {
|
||||
pub fn new(content: String) -> Self {
|
||||
Self {
|
||||
content,
|
||||
path: ".workshop/bench.sh".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 为test.sh预留的结构
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TestScript {
|
||||
pub content: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl TestScript {
|
||||
pub fn new(content: String) -> Self {
|
||||
Self {
|
||||
content,
|
||||
path: ".workshop/test.sh".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PipelineError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("YAML parse error: {0}")]
|
||||
Yaml(#[from] serde_yaml::Error),
|
||||
#[error("Variable error: {0}")]
|
||||
Variable(#[from] crate::variable::VariableError),
|
||||
#[error("Template error: {0}")]
|
||||
Template(String),
|
||||
#[error("Configuration error: {0}")]
|
||||
Config(String),
|
||||
#[error("Validation error: {0}")]
|
||||
Validation(String),
|
||||
}
|
||||
|
||||
pub type PipelineResult<T> = Result<T, PipelineError>;
|
||||
@@ -1,88 +0,0 @@
|
||||
mod error;
|
||||
mod variable;
|
||||
mod template;
|
||||
mod config;
|
||||
|
||||
use std::path::Path;
|
||||
use std::collections::HashMap;
|
||||
use serde::Serialize;
|
||||
use log::info;
|
||||
pub use error::*;
|
||||
pub use variable::*;
|
||||
pub use template::*;
|
||||
pub use config::*;
|
||||
|
||||
// 重新导出常用的配置类型
|
||||
#[allow(unused)]
|
||||
pub use config::prebake::*;
|
||||
#[allow(unused)]
|
||||
pub use config::finalize::*;
|
||||
#[allow(unused)]
|
||||
pub use config::scripts::*;
|
||||
|
||||
pub type PipelineResult<T> = Result<T, PipelineError>;
|
||||
|
||||
/// 完整的流水线配置
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PipelineConfig {
|
||||
pub prebake: PrebakeConfig,
|
||||
pub bake: BakeScript,
|
||||
pub finalize: FinalizeConfig,
|
||||
pub variables: HashMap<String, VariableValue>,
|
||||
}
|
||||
|
||||
impl PipelineConfig {
|
||||
/// 从文件系统加载完整配置
|
||||
pub fn load_from_dir<P: AsRef<Path>>(
|
||||
dir: P,
|
||||
user_id: &str,
|
||||
pipeline_id: &str,
|
||||
variable_provider: &dyn VariableProvider,
|
||||
) -> 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 = 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,353 +0,0 @@
|
||||
use crate::{
|
||||
config::finalize::FinalizeConfig,
|
||||
config::prebake::{
|
||||
Architecture, CacheMode, CacheStrategyType, DistributionDeps, GpuType, PrebakeConfig,
|
||||
SystemDependencies,
|
||||
},
|
||||
error::PipelineError,
|
||||
variable::VariableResolver,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
// use serde::{Deserialize, Serialize};
|
||||
|
||||
lazy_static! {
|
||||
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();
|
||||
}
|
||||
|
||||
pub trait TemplateResolvable {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
// 为基本类型实现模板解析
|
||||
impl TemplateResolvable for String {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
let mut result = self.clone();
|
||||
|
||||
// 处理 {{VARIABLE}} 格式
|
||||
for cap in HANDLEBARS_REGEX.captures_iter(result.clone().as_str()) {
|
||||
let var_name = &cap[1];
|
||||
if let Ok(value) = resolver.resolve_variable(var_name) {
|
||||
result = result.replace(&cap[0], &value);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理$VARIABLE格式,TODO
|
||||
|
||||
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 {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
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 {
|
||||
*env_vars = env_vars.resolve_template(resolver)?;
|
||||
}
|
||||
|
||||
// 解析缓存配置
|
||||
if let Some(ref mut cache) = config.cache {
|
||||
*cache = cache.resolve_template(resolver)?;
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
// 为EnvironmentConfig实现
|
||||
impl TemplateResolvable for crate::config::prebake::EnvironmentConfig {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(Self {
|
||||
builder: self.builder.resolve_template(resolver)?,
|
||||
config: self.config.resolve_template(resolver)?,
|
||||
resources: self.resources.resolve_template(resolver)?,
|
||||
network: self.network.resolve_template(resolver)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 为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的模板解析保持不变...
|
||||
@@ -1,128 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum VariableError {
|
||||
#[error("Variable not found: {name}")]
|
||||
NotFound { name: String },
|
||||
#[error("Variable resolution failed: {message}")]
|
||||
Resolution { message: String },
|
||||
}
|
||||
|
||||
/// 变量值类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum VariableValue {
|
||||
String(String),
|
||||
Secret(String),
|
||||
}
|
||||
|
||||
impl VariableValue {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
VariableValue::String(s) => s,
|
||||
VariableValue::Secret(s) => s,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_output(&self) -> Self {
|
||||
match self {
|
||||
VariableValue::String(s) => VariableValue::String(s.clone()),
|
||||
VariableValue::Secret(_) => VariableValue::Secret("***".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 变量提供者trait
|
||||
pub trait VariableProvider: Send + Sync {
|
||||
fn get_variable(&self, user_id: &str, pipeline_id: &str, name: &str) -> Option<VariableValue>;
|
||||
fn get_secret(&self, user_id: &str, pipeline_id: &str, name: &str) -> Option<VariableValue>;
|
||||
}
|
||||
|
||||
/// 默认的变量提供者实现(用于测试)
|
||||
pub struct DefaultVariableProvider {
|
||||
variables: HashMap<String, VariableValue>,
|
||||
secrets: HashMap<String, VariableValue>,
|
||||
}
|
||||
|
||||
impl DefaultVariableProvider {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
variables: HashMap::new(),
|
||||
secrets: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_variable(mut self, name: &str, value: &str) -> Self {
|
||||
self.variables.insert(name.to_string(), VariableValue::String(value.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_secret(mut self, name: &str, value: &str) -> Self {
|
||||
self.secrets.insert(name.to_string(), VariableValue::Secret(value.to_string()));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl VariableProvider for DefaultVariableProvider {
|
||||
fn get_variable(&self, _user_id: &str, _pipeline_id: &str, name: &str) -> Option<VariableValue> {
|
||||
self.variables.get(name).cloned()
|
||||
}
|
||||
|
||||
fn get_secret(&self, _user_id: &str, _pipeline_id: &str, name: &str) -> Option<VariableValue> {
|
||||
self.secrets.get(name).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DefaultVariableProvider {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// 变量解析器
|
||||
pub struct VariableResolver<'a> {
|
||||
user_id: String,
|
||||
pipeline_id: String,
|
||||
provider: &'a dyn VariableProvider,
|
||||
resolved_variables: HashMap<String, VariableValue>,
|
||||
}
|
||||
|
||||
impl<'a> VariableResolver<'a> {
|
||||
pub fn new(
|
||||
user_id: &str,
|
||||
pipeline_id: &str,
|
||||
provider: &'a dyn VariableProvider,
|
||||
) -> Self {
|
||||
Self {
|
||||
user_id: user_id.to_string(),
|
||||
pipeline_id: pipeline_id.to_string(),
|
||||
provider,
|
||||
resolved_variables: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_variable(&mut self, name: &str) -> Result<String, VariableError> {
|
||||
// 先尝试普通变量
|
||||
if let Some(value) = self.provider.get_variable(&self.user_id, &self.pipeline_id, name) {
|
||||
let value_str = value.as_str().to_string();
|
||||
self.resolved_variables.insert(name.to_string(), value);
|
||||
return Ok(value_str);
|
||||
}
|
||||
|
||||
// 再尝试secret
|
||||
if let Some(value) = self.provider.get_secret(&self.user_id, &self.pipeline_id, name) {
|
||||
let value_str = value.as_str().to_string();
|
||||
self.resolved_variables.insert(name.to_string(), value);
|
||||
return Ok(value_str);
|
||||
}
|
||||
|
||||
Err(VariableError::NotFound {
|
||||
name: name.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_resolved_variables(&self) -> HashMap<String, VariableValue> {
|
||||
self.resolved_variables.clone()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user