feat(cli): add environment variable support and restructure commands

- Add HBW_USERNAME, HBW_PIPELINE, HBW_BUILD_ID, HBW_DRY_RUN env vars
- Wrap prebake/bake/finalize in Bare subcommand for cleaner hierarchy
- Remove unimplemented Monitor command
This commit is contained in:
Catty Steve
2026-04-24 21:12:10 +08:00
parent 84b3b7d8e8
commit 901005639b
8 changed files with 307 additions and 327 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ anyhow = "1.0.100"
bollard = { version = "0.20.0", features = ["buildkit", "chrono", "ssh"] }
cgroups-rs = "0.5.0"
chrono = { version = "0.4.42", features = ["serde"] }
clap = { version = "4.5.53", features = ["derive"] }
clap = { version = "4.5.53", features = ["derive", "env"] }
config = "0.15.19"
duration-str = "0.21.0"
env_logger = "0.11.8"
+106 -199
View File
@@ -4,6 +4,7 @@
//! which handles artifact collection, publishing, and notifications.
use crate::types::command::CustomCommand;
use crate::types::compression::CompressionMethod;
use crate::types::time::deserialize_duration_ms;
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
@@ -102,6 +103,33 @@ pub struct NotificationMethod {
#[serde(default)]
pub fallible: bool,
/// Number of retries on failure (0 = no retry, -1 = infinite retries)
#[serde(default)]
pub retry: i8,
/// Timeout for notification execution in milliseconds (0 = no timeout)
#[serde(default)]
pub timeout_ms: u64,
/// Template definition
#[serde(default)]
pub template: NotificationTemplate,
/// Trigger definition
#[serde(default)]
pub trigger: Vec<TriggerItemRaw>,
/// Notification-specific configuration (url, token, to, from, etc.)
#[serde(flatten)]
pub config: Value,
/// Policy list for flexible trigger/template/config combinations.
#[serde(default)]
pub policy: Vec<NotifyPolicy>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct NotificationTemplate {
/// Payload schema: "default" (plugin-defined) or "custom" (requires on_* templates)
#[serde(default)]
pub schema: Option<String>,
@@ -117,10 +145,6 @@ pub struct NotificationMethod {
/// Stage-triggered notification configuration
#[serde(default)]
pub on_finish_of: Option<OnFinishOf>,
/// Notification-specific configuration (url, token, to, from, etc.)
#[serde(flatten)]
pub config: Value,
}
/// Notification configuration container
@@ -128,9 +152,8 @@ pub struct NotificationMethod {
pub struct NotificationConfig {
/// Template definitions for notification content
#[serde(default)]
pub templates: HashMap<String, HashMap<String, PluginConfig>>,
pub templates: HashMap<String, NotificationTemplateDef>,
/// Priority groups for notification delivery
#[serde(flatten)]
pub groups: HashMap<u32, HashMap<String, NotificationMethod>>,
}
@@ -146,6 +169,63 @@ pub struct OnFinishOf {
pub content: String,
}
/// Notification template definition.
/// Supports inline content (on_success/on_failure/on_finish_of) or external reference (use).
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct NotificationTemplateDef {
/// External template reference (git URL).
/// Cannot be combined with on_* fields in MVP (validation will reject).
#[serde(default, rename = "use")]
pub use_: Option<String>,
/// Content template for build success notification
#[serde(default)]
pub on_success: Option<String>,
/// Content template for build failure notification
#[serde(default)]
pub on_failure: Option<String>,
/// Content template for stage completion notification
#[serde(default)]
pub on_finish_of: Option<String>,
}
/// Notification trigger type.
#[derive(Debug, Clone)]
pub enum Trigger {
OnSuccess,
OnFailure,
OnFinishOf(String),
}
/// Raw trigger item for deserialization.
/// Supports `- on_success` (string) and `- on_finish_of: [...]` (map).
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(untagged)]
pub enum TriggerItemRaw {
Simple(String),
Map(std::collections::HashMap<String, Vec<String>>),
}
/// Notification policy: a set of triggers with optional template/config overrides.
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct NotifyPolicy {
/// Trigger conditions for this policy.
/// Empty means inheriting method-level triggers.
#[serde(default)]
pub trigger: Vec<TriggerItemRaw>,
/// Template configuration (schema, on_success, on_failure, on_finish_of, subject, body...).
/// Empty/null means inheriting method-level templates.
#[serde(default)]
pub template: Value,
/// Override fields: to, url, token, fallible, retry, timeout_ms, etc.
#[serde(flatten)]
pub overrides: Value,
}
/// Artifact definition
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ArtifactDef {
@@ -167,16 +247,11 @@ pub struct ArtifactDef {
pub retention_ms: u64,
/// Compression method: "zstd", "gzip", "none", "dir"
#[serde(default = "default_compression")]
#[serde(default)]
pub compression: CompressionMethod,
/// Trigger conditions: "success", "failure", "always" (WIP: failure, always)
#[serde(default = "default_on_conditions")]
pub on: Vec<ArtifactCondition>,
/// Publishing targets
#[serde(default)]
pub publish: HashMap<String, PublishTarget>,
// Publishing targets
// TODO: under heavy refactor
}
impl Default for ArtifactDef {
@@ -185,55 +260,19 @@ impl Default for ArtifactDef {
id: None,
path: String::new(),
retention_ms: default_retention_ms(),
compression: default_compression(),
on: default_on_conditions(),
publish: HashMap::new(),
compression: CompressionMethod::default(),
}
}
}
/// Compression method for artifacts
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
#[serde(rename_all = "lowercase")]
pub enum CompressionMethod {
#[default]
Zstd,
Gzip,
None,
Dir,
}
/// Artifact trigger condition
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum ArtifactCondition {
Success,
Failure,
Always,
}
/// Publish target configuration
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct PublishTarget {
/// Whether publish failure should not fail the build
#[serde(default)]
pub fallible: bool,
/// Conditional execution (WIP)
/// Format: "{{ env.BRANCH }} == 'main'"
#[serde(rename = "if", default)]
pub condition: Option<String>,
/// Plugin-specific configuration (image, bucket, etc.)
#[serde(flatten)]
pub config: Value,
}
/// Artifact
/// TODO: Under heavy refactor
/// Cleanup configuration
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct CleanupConfig {
/// Cleanup policy: "auto", "manual", "on_failure" (WIP: manual, on_failure)
#[serde(default = "default_cleanup_policy")]
#[serde(default)]
pub policy: CleanupPolicy,
}
@@ -244,7 +283,8 @@ pub enum CleanupPolicy {
#[default]
Auto,
Manual,
OnFailure,
OnSuccess,
Never,
}
/// Hooks configuration for finalize stage
@@ -264,18 +304,6 @@ fn default_retention_ms() -> u64 {
7 * 24 * 3600 * 1000
}
fn default_compression() -> CompressionMethod {
CompressionMethod::Zstd
}
fn default_on_conditions() -> Vec<ArtifactCondition> {
vec![ArtifactCondition::Success]
}
fn default_cleanup_policy() -> CleanupPolicy {
CleanupPolicy::Auto
}
impl FinalizeConfig {
/// Validate the finalize configuration
pub fn validate(&self) -> Result<(), Vec<String>> {
@@ -311,8 +339,7 @@ impl FinalizeConfig {
}
}
// TODO: Schema-based validation for plugin configs
// TODO: Validate that publish targets reference existing plugins
self.validate_notification(&mut errors);
if errors.is_empty() {
Ok(())
@@ -320,136 +347,16 @@ impl FinalizeConfig {
Err(errors)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_hooks_full() {
let yaml = r#"
version: "0.0.1"
hooks:
early:
- name: "test-early"
command: "echo early"
timeout: 10
late:
- name: "test-late"
command: "plugin:test-plugin"
working_dir: "/tmp"
"#;
let config: FinalizeConfig = serde_yaml::from_str(yaml).unwrap();
assert!(config.hooks.is_some());
let hooks = config.hooks.unwrap();
let early = hooks.early.as_ref().unwrap();
let late = hooks.late.as_ref().unwrap();
assert_eq!(early.len(), 1);
assert_eq!(late.len(), 1);
assert_eq!(early[0].command, "echo early");
assert_eq!(late[0].command, "plugin:test-plugin");
assert_eq!(late[0].working_dir, Some("/tmp".to_string()));
}
#[test]
fn test_deserialize_hooks_missing() {
let yaml = r#"
version: "0.0.1"
"#;
let config: FinalizeConfig = serde_yaml::from_str(yaml).unwrap();
assert!(config.hooks.is_none());
}
#[test]
fn test_deserialize_hooks_timeout_float() {
let yaml = r#"
version: "0.0.1"
hooks:
early:
- name: "float-timeout"
command: "echo ok"
timeout: 10.5
"#;
let config: FinalizeConfig = serde_yaml::from_str(yaml).unwrap();
let hooks = config.hooks.unwrap();
let early = hooks.early.unwrap();
assert_eq!(early[0].timeout_ms, 10500);
}
#[test]
fn test_deserialize_hooks_timeout_string() {
let yaml = r#"
version: "0.0.1"
hooks:
early:
- name: "string-timeout"
command: "echo ok"
timeout: "30s"
"#;
let config: FinalizeConfig = serde_yaml::from_str(yaml).unwrap();
let hooks = config.hooks.unwrap();
let early = hooks.early.unwrap();
assert_eq!(early[0].timeout_ms, 30000);
}
#[test]
fn test_validate_plugin_zero_urls() {
let config: FinalizeConfig = serde_yaml::from_str(
r#"
version: "0.0.1"
plugin:
show_file_list: {}
"#,
)
.unwrap();
let result = config.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(
errors
.iter()
.any(|e| e.contains("show_file_list") && e.contains("no URL"))
);
}
#[test]
fn test_validate_plugin_one_url_ok() {
let config: FinalizeConfig = serde_yaml::from_str(
r#"
version: "0.0.1"
plugin:
show_file_list:
"https://example.com/plugin.tar.gz":
fallible: false
"#,
)
.unwrap();
let result = config.validate();
assert!(result.is_ok());
}
#[test]
fn test_validate_plugin_multiple_urls() {
let config: FinalizeConfig = serde_yaml::from_str(
r#"
version: "0.0.1"
plugin:
show_file_list:
"https://example.com/plugin1.tar.gz":
fallible: false
"https://example.com/plugin2.tar.gz":
fallible: false
"#,
)
.unwrap();
let result = config.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(
errors
.iter()
.any(|e| e.contains("show_file_list") && e.contains("2 URL definitions"))
);
fn validate_notification(&self, errors: &mut Vec<String>) {
// Validate templates
for (name, tmpl) in &self.notification.templates {
if tmpl.use_.is_some() && (tmpl.on_success.is_some() || tmpl.on_failure.is_some() || tmpl.on_finish_of.is_some()) {
errors.push(format!(
"Template '{}' cannot combine 'use' with 'on_*' fields (MVP restriction)",
name
));
}
}
}
}
+18 -8
View File
@@ -34,19 +34,19 @@ pub mod cli {
#[command(version, about, long_about=None)]
pub struct Cli {
/// Username of the pipeline executor
#[arg(short, long)]
#[arg(short, long, env = "HBW_USERNAME")]
pub username: String,
/// Pipeline ID of this pipeline
#[arg(short, long)]
#[arg(short, long, env = "HBW_PIPELINE")]
pub pipeline: String,
/// Build ID of this build
#[arg(short, long)]
#[arg(short, long, env = "HBW_BUILD_ID")]
pub build_id: String,
/// Parse the script without executing
#[arg(long, default_value = "false")]
#[arg(long, default_value = "false", env = "HBW_DRY_RUN")]
pub dry_run: bool,
#[command(subcommand)]
@@ -55,12 +55,21 @@ pub mod cli {
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Monitor a running task
Monitor { group: String },
/// Prepare a task
Prebake { config: PathBuf },
/// Run pipeline stages in standalone (bare) mode
Bare {
#[command(subcommand)]
command: BareCommands,
},
/// Run in client mode
Client { config: String },
}
#[derive(Subcommand, Debug)]
pub enum BareCommands {
/// Prepare a task
Prebake { config: PathBuf },
/// Run a task
Bake {
/// Path to the script to execute
@@ -71,6 +80,7 @@ pub mod cli {
#[arg(short, long, default_value = "./prebake.yml")]
prebake: PathBuf,
},
/// Finish a task
Finalize { config: String },
}
+100 -98
View File
@@ -2,7 +2,7 @@ use std::path::Path;
use workshop_baker::{
bake::bake,
cli::{Cli, Commands, Parser},
cli::{BareCommands, Cli, Commands, Parser},
daemon,
engine::{EventSender, ExecutionContext},
error::PrebakeError,
@@ -18,111 +18,113 @@ async fn main() {
log::warn!("Dry run enabled, no changes will be made.");
}
match &cli.command {
Some(Commands::Monitor { group: _ }) => {
unimplemented!("executor-monitor should be moved to daemon instead.")
}
Some(Commands::Client { config: _ }) => {
unimplemented!("Client mode not yet implemented.")
}
Some(Commands::Prebake { config }) => {
log::info!("Running in standalone mode.");
let mut ctx = ExecutionContext {
standalone: true,
task_id: format!("{}-{}-init", cli.pipeline, cli.build_id),
pipeline_name: cli.pipeline.clone(),
username: cli.username.clone(),
dry_run: cli.dry_run,
privileged: true,
..Default::default()
};
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
let event_tx: EventSender = Some(tx);
let pipeline_name = cli.pipeline.clone();
let build_id = cli.build_id.clone();
let receiver_handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
event_rx,
pipeline_name,
build_id,
));
let result = prebake(config, &cli, None, &mut ctx, event_tx).await;
drop(ctx);
let _ = receiver_handle.await;
dbg!(&result);
if let Err(ref e) = result {
log::error!("Prebake stage failed: {}", e);
std::process::exit(
e.downcast_ref::<PrebakeError>()
.map(|pe| pe.exit_code())
.unwrap_or(1),
);
Some(Commands::Bare { command }) => match command {
BareCommands::Prebake { config } => {
log::info!("Running in standalone mode.");
let mut ctx = ExecutionContext {
standalone: true,
task_id: format!("{}-{}-init", cli.pipeline, cli.build_id),
pipeline_name: cli.pipeline.clone(),
username: cli.username.clone(),
dry_run: cli.dry_run,
privileged: true,
..Default::default()
};
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
let event_tx: EventSender = Some(tx);
let pipeline_name = cli.pipeline.clone();
let build_id = cli.build_id.clone();
let receiver_handle =
tokio::spawn(workshop_baker::prebake::event::event_receiver(
event_rx,
pipeline_name,
build_id,
));
let result = prebake(config, &cli, None, &mut ctx, event_tx).await;
drop(ctx);
let _ = receiver_handle.await;
dbg!(&result);
if let Err(ref e) = result {
log::error!("Prebake stage failed: {}", e);
std::process::exit(
e.downcast_ref::<PrebakeError>()
.map(|pe| pe.exit_code())
.unwrap_or(1),
);
}
}
}
Some(Commands::Bake {
script,
bake_base,
prebake,
}) => {
log::info!("Running in standalone mode.");
let mut ctx = ExecutionContext {
standalone: true,
task_id: format!("{}-{}-bake", cli.pipeline, cli.build_id),
pipeline_name: cli.pipeline.clone(),
username: cli.username.clone(),
dry_run: cli.dry_run,
privileged: false,
..Default::default()
};
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
let event_tx: EventSender = Some(tx);
let pipeline_name = cli.pipeline.clone();
let build_id = cli.build_id.clone();
let receiver_handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
event_rx,
pipeline_name,
build_id,
));
let result = bake(script, bake_base, prebake, &cli, &mut ctx, event_tx).await;
drop(ctx);
let _ = receiver_handle.await;
dbg!(&result);
if result.is_err() {
log::error!("Bake stage failed! Exiting...");
std::process::exit(1);
BareCommands::Bake {
script,
bake_base,
prebake: prebake_path,
} => {
log::info!("Running in standalone mode.");
let mut ctx = ExecutionContext {
standalone: true,
task_id: format!("{}-{}-bake", cli.pipeline, cli.build_id),
pipeline_name: cli.pipeline.clone(),
username: cli.username.clone(),
dry_run: cli.dry_run,
privileged: false,
..Default::default()
};
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
let event_tx: EventSender = Some(tx);
let pipeline_name = cli.pipeline.clone();
let build_id = cli.build_id.clone();
let receiver_handle =
tokio::spawn(workshop_baker::prebake::event::event_receiver(
event_rx,
pipeline_name,
build_id,
));
let result =
bake(script, bake_base, prebake_path, &cli, &mut ctx, event_tx).await;
drop(ctx);
let _ = receiver_handle.await;
dbg!(&result);
if result.is_err() {
log::error!("Bake stage failed! Exiting...");
std::process::exit(1);
}
}
}
Some(Commands::Finalize { config }) => {
log::info!("Running in standalone mode.");
let mut ctx = ExecutionContext {
standalone: true,
task_id: format!("{}-{}-finalize", cli.pipeline, cli.build_id),
pipeline_name: cli.pipeline.clone(),
username: cli.username.clone(),
dry_run: cli.dry_run,
privileged: false,
..Default::default()
};
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
let event_tx: EventSender = Some(tx);
let pipeline_name = cli.pipeline.clone();
let build_id = cli.build_id.clone();
let receiver_handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
event_rx,
pipeline_name,
build_id,
));
let result = finalize(Path::new(config), &cli, &mut ctx, event_tx).await;
drop(ctx);
let _ = receiver_handle.await;
dbg!(&result);
if let Err(ref e) = result {
log::error!("Finalize stage failed: {}", e);
std::process::exit(1);
BareCommands::Finalize { config } => {
log::info!("Running in standalone mode.");
let mut ctx = ExecutionContext {
standalone: true,
task_id: format!("{}-{}-finalize", cli.pipeline, cli.build_id),
pipeline_name: cli.pipeline.clone(),
username: cli.username.clone(),
dry_run: cli.dry_run,
privileged: false,
..Default::default()
};
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
let event_tx: EventSender = Some(tx);
let pipeline_name = cli.pipeline.clone();
let build_id = cli.build_id.clone();
let receiver_handle =
tokio::spawn(workshop_baker::prebake::event::event_receiver(
event_rx,
pipeline_name,
build_id,
));
let result =
finalize(Path::new(config), &cli, &mut ctx, event_tx).await;
drop(ctx);
let _ = receiver_handle.await;
dbg!(&result);
if let Err(ref e) = result {
log::error!("Finalize stage failed: {}", e);
std::process::exit(1);
}
}
}
},
None => {
// Running in daemon mode
log::info!("Executor will be running in daemon mode.");
let _result = daemon::daemon(&cli).await;
}
}
-1
View File
@@ -93,7 +93,6 @@ type NotifyEventReceiver = tokio::sync::mpsc::Receiver<BundledNotifyEvent>;
pub type NotifyEventSender = tokio::sync::mpsc::Sender<NotifyEvent>;
const NOTIFY_NOTREADY_DELAY: std::time::Duration = std::time::Duration::from_secs(5);
const NOTIFY_NOTREADY_PRIORITY: u32 = 99;
const NOTIFY_TEMPORARY_DELAY_FACTOR: f64 = 2.0;
const NOTIFY_TEMPORARY_DELAY_TIME: f64 = 5.0;
const NOTIFY_TEMPORARY_MAX_TIME: f64 = f64::MAX;
+70 -20
View File
@@ -16,7 +16,7 @@ use crate::prebake::config::Bootstrap;
use crate::prebake::config::PrebakeConfig;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::path::{PathBuf,Path};
use which::which;
/// Executes the bootstrap stage of the prebake workflow.
@@ -53,22 +53,28 @@ 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)?;
if let Some(custom_bootstrap) = bootstrap_config.custom {
if ctx.dry_run {
log::info!("[DRY_RUN] User provided bootstrap script:\n{}", custom_bootstrap);
return Ok(());
}
// TODO: Parse custom bootstrap URL and download them
log::debug!("User provided bootstrap script:\n{}", custom_bootstrap);
if ctx.standalone {
write_bootstrap_content(&bootstrap_path, custom_bootstrap)?;
}
else {
// Requires PIP(Pipeline Implicit Parameters) to be implemented
unimplemented!("Permission check is not implemented, failing...");
}
} else {
let script = generate_bootstrap(&bootstrap_config, config, osinfo, ctx.standalone)?;
if ctx.dry_run {
log::info!("[DRY_RUN] Generated bootstrap script:\n{}", script);
return Ok(());
}
log::debug!("Generated bootstrap script:\n{}", script);
let mut file = File::create(&bootstrap_path)?;
file.write_all(script.as_bytes())?;
std::fs::set_permissions(
&bootstrap_path,
std::os::unix::fs::PermissionsExt::from_mode(0o755),
)?;
} else {
// Requires PIP(Pipeline Implicit Parameters) to be implemented
unimplemented!("Not supporting custom bootstrap.sh at this time");
write_bootstrap_content(&bootstrap_path, script)?;
}
// Check if bootstrap script exists before execution
@@ -84,6 +90,16 @@ pub async fn bootstrap(
Ok(())
}
fn write_bootstrap_content(bootstrap_path: &Path, content: String) -> Result<(), BootstrapError> {
let mut file = File::create(bootstrap_path)?;
file.write_all(content.as_bytes())?;
std::fs::set_permissions(
&bootstrap_path,
std::os::unix::fs::PermissionsExt::from_mode(0o755),
)?;
Ok(())
}
/// Generates a bootstrap script based on configuration and target OS.
///
/// The generated script performs the following setup tasks:
@@ -117,6 +133,7 @@ fn generate_bootstrap(
bootstrap_config: &Bootstrap,
config: &PrebakeConfig,
osinfo: &os_info::Info,
standalone: bool,
) -> Result<String, BootstrapError> {
let mut script = String::new();
// Generate header
@@ -128,7 +145,12 @@ fn generate_bootstrap(
let user = &bootstrap_config.user;
if user == "root" {
// Requires PIP(Pipeline Implicit Parameters) to be implemented
unimplemented!("Permission check is not implemented, failing...");
// unimplemented!("Permission check is not implemented, failing...");
if standalone {
log::warn!("Running the whole pipeline with root could harm your pipeline or cause undefined behavior.")
} else {
unimplemented!("Permission check is not implemented, failing...");
}
} else {
script.push_str("# User creation\n");
script.push_str(&format!("if ! id -u {} >/dev/null 2>&1; then\n", user));
@@ -166,8 +188,19 @@ fn generate_bootstrap(
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(sudoers) = &bootstrap_config.sudoers {
if standalone {
log::warn!("Pipeline permission check disabled due to Standalone mode.");
// TODO: Introduce permission check
} else {
// In client mode, custom sudoers is constrainted by PIPELINE_CUSTOM_SUDOERS
// TODO: Introduce permission check
unimplemented!("Permission check is not implemented, failing...")
}
script.push_str(&format!(
"echo '{}' | tee /etc/sudoers.d/workshop > /dev/null\n",
sudoers
));
}
if let Some(pkg_manager) = pkg_manager {
let allowed_commands = format!("/usr/bin/{} *", pkg_manager.binary());
@@ -179,12 +212,29 @@ fn generate_bootstrap(
script.push_str("chmod 440 /etc/sudoers.d/workshop\n");
}
} else if which("doas").is_ok() {
if bootstrap_config.doas.is_some() {
// Requires PIP(Pipeline Implicit Parameters) to be implemented
unimplemented!("Permission check is not implemented, failing...")
if let Some(doas) = &bootstrap_config.doas {
if standalone {
log::warn!("Pipeline permission check disabled due to Standalone mode.");
// TODO: Introduce permission check
} else {
// In client mode, custom sudoers is constrainted by PIPELINE_CUSTOM_SUDOERS
// TODO: Introduce permission check
unimplemented!("Permission check is not implemented, failing...")
}
script.push_str(&format!(
"cat >> /etc/doas.conf << 'EOF'\n{}\nEOF\n",
doas
));
}
if let Some(pkg_manager) = pkg_manager {
let allowed_commands = format!("/usr/bin/{} *", pkg_manager.binary());
let content = format!("permit nopass {} as root cmd {}", user, allowed_commands);
script.push_str(&format!(
"cat >> /etc/doas.conf << 'EOF'\n{}\nEOF\n",
content
));
script.push_str("chmod 600 /etc/doas.conf\n");
}
// We do not know how to write doas.conf lol
unimplemented!("doas is not implemented");
}
script.push_str("echo 'Bootstrap complete!'\n");
+1
View File
@@ -6,3 +6,4 @@ pub mod command;
pub mod memsize;
pub mod repology;
pub mod time;
pub mod compression;
+11
View File
@@ -0,0 +1,11 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
#[serde(rename_all = "lowercase")]
pub enum CompressionMethod {
#[default]
Zstd,
Gzip,
None,
Dir,
}