feat(baker/prebake): Add configurable bootstrap stage with dynamic
script generation The bootstrap stage now reads configuration from prebake.yml to dynamically generate bootstrap scripts for user creation, workspace setup, and privilege executor configuration. Adds the `which` crate for binary detection.
This commit is contained in:
@@ -58,6 +58,42 @@ environment:
|
||||
- "1.1.1.1"
|
||||
proxies: # 代理配置,暂时留空,可选
|
||||
|
||||
# 构建环境初始化
|
||||
bootstrap:
|
||||
# 构建用户,默认为 vulcan(火神)
|
||||
# 设定为 "root" 表示忽略用户切换并禁用安全特性,需要 PIPELINE_PRIVILEGED 权限
|
||||
user: "vulcan"
|
||||
|
||||
# 工作空间
|
||||
workspace:
|
||||
# 工作目录路径
|
||||
path: "/home/vulcan/workspace"
|
||||
# 路径不可用时回退到 /workspace
|
||||
# 即使路径可用,也会在 /workspace 建立软链接
|
||||
fallback: true
|
||||
|
||||
# 自定义 sudoers 文件内容
|
||||
# 这会覆盖默认行为,可能绕过安全机制
|
||||
# 需要 PIPELINE_CUSTOM_SUDOERS 权限
|
||||
sudoers: "vulcan ALL=(ALL:ALL) NOPASSWD: ALL"
|
||||
|
||||
# 自定义 doas 文件内容
|
||||
# 这会覆盖默认行为,可能绕过安全机制
|
||||
# 与 sudoers 共享 PIPELINE_CUSTOM_SUDOERS 权限
|
||||
doas: "permit nopass vulcan as root"
|
||||
|
||||
# 自定义 bootstrap 脚本(与上述所有字段互斥)
|
||||
# 支持多种来源:
|
||||
# workspace:///.workshop/bootstrap.sh - 项目内文件
|
||||
# file:///bin/bootstrap.sh - 环境内文件
|
||||
# server://bootstrap.sh - 由服务器提供
|
||||
# https://example.com/bootstrap.sh - http/https网络下载,请确保构建环境可以连接到该URL
|
||||
# (content) - 直接给出内容,当文本开头无法匹配任何scheme时采用
|
||||
# 需要 PIPELINE_CUSTOM_BOOTSTRAP 权限
|
||||
custom: "server://bootstrap-alpine.sh"
|
||||
|
||||
|
||||
|
||||
# 依赖定义,安装依赖时按system-user顺序进行
|
||||
dependencies:
|
||||
# 系统包依赖,按包管理器枚举给出,只应该给出与环境契合的字段。
|
||||
|
||||
Generated
+10
@@ -3129,6 +3129,15 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "8.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -3412,6 +3421,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"topological-sort",
|
||||
"uuid",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -47,3 +47,4 @@ uuid = { version = "1.19.0", features = ["v4"] }
|
||||
libc = "0.2"
|
||||
os_info = "3.14.0"
|
||||
privdrop = "0.5.6"
|
||||
which = "8.0.2"
|
||||
|
||||
@@ -77,6 +77,22 @@ impl PrebakeError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BootstrapError {
|
||||
#[error("Failed to write bootstrap script: {0}")]
|
||||
IOError(#[from] std::io::Error),
|
||||
|
||||
#[error("Failed to find bootstrap.sh: {0}")]
|
||||
ScriptNotFound(PathBuf),
|
||||
|
||||
#[error("Failed to execute bootstrap.sh: {0}")]
|
||||
ExecutionError(#[from] ExecutionError),
|
||||
|
||||
#[error("Failed to create user {0}: {1}")]
|
||||
UserCreationFailed(String, String),
|
||||
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DependencyError {
|
||||
#[error("No supported package manager found for {0}")]
|
||||
|
||||
@@ -66,7 +66,8 @@ pub async fn prebake(
|
||||
prebake_drop_privilege(PrebakeStage::Bootstrap, dropafter, target_user, &mut ctx)?;
|
||||
ctx.task_id = format!("{}-{}-bootstrap", cli.pipeline, cli.build_id);
|
||||
// TODO: Pass real event_tx
|
||||
stage::bootstrap::bootstrap(&config, &ctx, &None).await?;
|
||||
let osinfo = os_info::get();
|
||||
stage::bootstrap::bootstrap(&config, &osinfo, &ctx, &None).await?;
|
||||
}
|
||||
|
||||
// EarlyHook stage
|
||||
|
||||
@@ -19,6 +19,8 @@ pub struct PrebakeConfig {
|
||||
pub version: String,
|
||||
pub environment: Environment,
|
||||
#[serde(default)]
|
||||
pub bootstrap: Option<Bootstrap>,
|
||||
#[serde(default)]
|
||||
pub dependencies: Option<Dependencies>,
|
||||
#[serde(default)]
|
||||
pub envvars: Option<EnvVars>,
|
||||
@@ -32,6 +34,66 @@ pub struct PrebakeConfig {
|
||||
pub metadata: Option<Metadata>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct Bootstrap {
|
||||
#[serde(default="default_bootstrap_user")]
|
||||
pub user: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub workspace: Option<Workspace>,
|
||||
|
||||
#[serde(default)]
|
||||
pub sudoers: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub doas: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub custom: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Bootstrap {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
user: default_bootstrap_user(),
|
||||
workspace: None,
|
||||
sudoers: None,
|
||||
doas: None,
|
||||
custom: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Workspace {
|
||||
#[serde(default="default_workspace")]
|
||||
pub path: String,
|
||||
|
||||
#[serde(default="default_true")]
|
||||
pub fallback: bool,
|
||||
}
|
||||
|
||||
impl Default for Workspace {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
path: default_workspace(),
|
||||
fallback: default_true(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_workspace() -> String {
|
||||
"/home/vulcan/workspace".to_string()
|
||||
}
|
||||
|
||||
fn default_bootstrap_user() -> String {
|
||||
"vulcan".to_string()
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct Environment {
|
||||
pub builder: BuilderType,
|
||||
|
||||
@@ -1,53 +1,147 @@
|
||||
use crate::ExecutionContext;
|
||||
use crate::engine::{Engine, EventSender, ExecutionError, ExecutionResult};
|
||||
use crate::engine::{Engine, EventSender, ExecutionResult, pm};
|
||||
use crate::error::BootstrapError;
|
||||
use crate::prebake::config::Bootstrap;
|
||||
use crate::prebake::config::PrebakeConfig;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use which::which;
|
||||
|
||||
/// Executes the bootstrap stage.
|
||||
///
|
||||
/// This function is responsible for running the system bootstrap script,
|
||||
/// typically used to initialize the environment or perform pre-configuration
|
||||
/// operations required for system startup.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `_config` - Prebake configuration parameters (not used currently)
|
||||
/// * `ctx` - Execution context containing state and information required
|
||||
/// for the execution environment
|
||||
/// * `event_tx` - Event sender for broadcasting execution events externally
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Result<ExecutionResult, ExecutionError>`:
|
||||
/// * `Ok(ExecutionResult)` - Result when script execution succeeds
|
||||
/// * `Err(ExecutionError)` - Error information when script execution fails
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// // Call bootstrap stage in prebake workflow
|
||||
/// let result = bootstrap(config, ctx, &event_tx).await;
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Possible errors:
|
||||
/// * Bootstrap script file does not exist
|
||||
/// * Insufficient permissions to execute script
|
||||
/// * Runtime errors during script execution
|
||||
pub async fn bootstrap(
|
||||
_config: &PrebakeConfig,
|
||||
config: &PrebakeConfig,
|
||||
osinfo: &os_info::Info,
|
||||
ctx: &ExecutionContext,
|
||||
event_tx: &EventSender,
|
||||
) -> Result<ExecutionResult, ExecutionError> {
|
||||
) -> Result<ExecutionResult, BootstrapError> {
|
||||
let engine = Engine::new();
|
||||
let bootstrap_path = PathBuf::from("/bootstrap.sh");
|
||||
|
||||
let bootstrap_config = config.bootstrap.clone().unwrap_or_default();
|
||||
|
||||
if bootstrap_config.custom.is_none() {
|
||||
let script = generate_bootstrap(&bootstrap_config, &config, &osinfo)?;
|
||||
let mut file = File::create(&bootstrap_path)?;
|
||||
file.write_all(script.as_bytes())?;
|
||||
} else {
|
||||
unimplemented!("Not supporting custom bootstrap.sh at this time");
|
||||
}
|
||||
|
||||
// Check if bootstrap script exists before execution
|
||||
if !bootstrap_path.exists() {
|
||||
log::error!("/bootstrap.sh does not exist");
|
||||
return Err(ExecutionError::ScriptNotFound(bootstrap_path));
|
||||
// return Err(ExecutionError::ScriptNotFound(bootstrap_path));
|
||||
return Err(BootstrapError::ScriptNotFound(bootstrap_path));
|
||||
}
|
||||
|
||||
engine.execute_script(&bootstrap_path, ctx, &event_tx).await
|
||||
engine
|
||||
.execute_script(&bootstrap_path, ctx, &event_tx)
|
||||
.await
|
||||
.map_err(|e| BootstrapError::ExecutionError(e))
|
||||
}
|
||||
|
||||
fn generate_bootstrap(
|
||||
bootstrap_config: &Bootstrap,
|
||||
config: &PrebakeConfig,
|
||||
osinfo: &os_info::Info
|
||||
) -> Result<String, BootstrapError> {
|
||||
let mut script = String::new();
|
||||
// Generate header
|
||||
script.push_str("#!/bin/sh\n");
|
||||
script.push_str("# Template from HBW baker - Generated bootstrap\n");
|
||||
script.push_str("set -e\n");
|
||||
|
||||
// Create user
|
||||
let user = &bootstrap_config.user;
|
||||
if user == "root" {
|
||||
unimplemented!("Permission check is not implemented, failing...");
|
||||
} else {
|
||||
script.push_str("# User creation\n");
|
||||
if which("useradd").is_ok() {
|
||||
script.push_str(&format!("useradd -m -s /bin/nologin {}\n", user));
|
||||
} else if which("adduser").is_ok() {
|
||||
script.push_str(&format!(
|
||||
"adduser --disabled-password --gecos '' --shell /bin/nologin {}\n",
|
||||
user
|
||||
));
|
||||
} else if which("busybox").is_ok() {
|
||||
script.push_str(&format!("busybox useradd -m -s /bin/nologin {}\n", user));
|
||||
} else {
|
||||
log::error!("None of useradd, adduser and busybox found to add user!");
|
||||
return Err(BootstrapError::UserCreationFailed(
|
||||
user.to_string(),
|
||||
"no available binary found".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Setup workspace
|
||||
let workspace = bootstrap_config.workspace.clone().unwrap_or_default();
|
||||
let workspace_path = workspace.path;
|
||||
let workspace_fallback = workspace.fallback;
|
||||
script.push_str("# Setup workspace\n");
|
||||
script.push_str(&format!("mkdir -p {}\n", workspace_path));
|
||||
script.push_str(&format!("chown {}:{} {}\n", user, user, workspace_path));
|
||||
if workspace_fallback {
|
||||
script.push_str(&format!("ln -sf {} /workspace\n", workspace_path));
|
||||
}
|
||||
|
||||
// Setup privilege executor
|
||||
script.push_str("# Setup privilege executor\n");
|
||||
let pkg_manager = get_package_manager(config, osinfo);
|
||||
if which("sudo").is_ok() {
|
||||
if bootstrap_config.sudoers.is_some() {
|
||||
unimplemented!("Permission check is not implemented, failing...")
|
||||
}
|
||||
if let Some(pkg_manager) = pkg_manager {
|
||||
let allowed_commands = format!("/usr/bin/{} *", pkg_manager.binary());
|
||||
let content = format!("{} ALL=(ALL:ALL) NOPASSWD {}", user, allowed_commands);
|
||||
script.push_str(&format!(
|
||||
"echo '{}' | tee /etc/sudoers.d/workshop\n",
|
||||
content
|
||||
));
|
||||
script.push_str("chmod 440 /etc/sudoers.d/workshop\n");
|
||||
}
|
||||
} else if which("doas").is_ok() {
|
||||
if bootstrap_config.doas.is_some() {
|
||||
unimplemented!("Permission check is not implemented, failing...")
|
||||
}
|
||||
unimplemented!("doas is not implemented");
|
||||
}
|
||||
|
||||
script.push_str("echo 'Bootstrap complete!'\n");
|
||||
Ok(script)
|
||||
}
|
||||
|
||||
fn get_package_manager(config: &PrebakeConfig, osinfo: &os_info::Info) -> Option<Box<dyn pm::PackageManager>> {
|
||||
config
|
||||
.dependencies
|
||||
.as_ref()
|
||||
.and_then(|deps| deps.system.as_ref())
|
||||
.and_then(|sysdeps| {
|
||||
if let Some(detected) = pm::detect(osinfo) {
|
||||
if sysdeps.contains_key(detected.name()) {
|
||||
log::info!("Using package manager: {} (detected)", detected.name());
|
||||
return Some(detected);
|
||||
} else {
|
||||
log::warn!(
|
||||
"Detected package manager '{}' but not configured in prebake.yml",
|
||||
detected.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((name, _)) = sysdeps.iter().next() {
|
||||
if let Some(pm) = pm::select(name) {
|
||||
log::warn!(
|
||||
"Using package manager: {} (first configured, not detected)",
|
||||
name
|
||||
);
|
||||
return Some(pm);
|
||||
}
|
||||
}
|
||||
|
||||
log::error!("No valid package manager found in configuration");
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ fi
|
||||
# Configure sudoers (allow package manager commands)
|
||||
mkdir -p /etc/sudoers.d/
|
||||
cat > /etc/sudoers.d/vulcan << 'EOF'
|
||||
vulcan ALL=(ALL) NOPASSWD: /usr/bin/pacman, /usr/bin/apt-get, /usr/bin/dnf, /usr/bin/yum, /usr/bin/apk
|
||||
vulcan ALL=(ALL:ALL) NOPASSWD: /usr/bin/pacman, /usr/bin/apt-get, /usr/bin/dnf, /usr/bin/yum, /usr/bin/apk
|
||||
EOF
|
||||
chmod 440 /etc/sudoers.d/vulcan
|
||||
|
||||
|
||||
Reference in New Issue
Block a user