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:
@@ -24,7 +24,7 @@ anyhow = "1.0.100"
|
|||||||
bollard = { version = "0.20.0", features = ["buildkit", "chrono", "ssh"] }
|
bollard = { version = "0.20.0", features = ["buildkit", "chrono", "ssh"] }
|
||||||
cgroups-rs = "0.5.0"
|
cgroups-rs = "0.5.0"
|
||||||
chrono = { version = "0.4.42", features = ["serde"] }
|
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"
|
config = "0.15.19"
|
||||||
duration-str = "0.21.0"
|
duration-str = "0.21.0"
|
||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
//! which handles artifact collection, publishing, and notifications.
|
//! which handles artifact collection, publishing, and notifications.
|
||||||
|
|
||||||
use crate::types::command::CustomCommand;
|
use crate::types::command::CustomCommand;
|
||||||
|
use crate::types::compression::CompressionMethod;
|
||||||
use crate::types::time::deserialize_duration_ms;
|
use crate::types::time::deserialize_duration_ms;
|
||||||
use semver::{Version, VersionReq};
|
use semver::{Version, VersionReq};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -102,6 +103,33 @@ pub struct NotificationMethod {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub fallible: bool,
|
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)
|
/// Payload schema: "default" (plugin-defined) or "custom" (requires on_* templates)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub schema: Option<String>,
|
pub schema: Option<String>,
|
||||||
@@ -117,10 +145,6 @@ pub struct NotificationMethod {
|
|||||||
/// Stage-triggered notification configuration
|
/// Stage-triggered notification configuration
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub on_finish_of: Option<OnFinishOf>,
|
pub on_finish_of: Option<OnFinishOf>,
|
||||||
|
|
||||||
/// Notification-specific configuration (url, token, to, from, etc.)
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub config: Value,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Notification configuration container
|
/// Notification configuration container
|
||||||
@@ -128,9 +152,8 @@ pub struct NotificationMethod {
|
|||||||
pub struct NotificationConfig {
|
pub struct NotificationConfig {
|
||||||
/// Template definitions for notification content
|
/// Template definitions for notification content
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub templates: HashMap<String, HashMap<String, PluginConfig>>,
|
pub templates: HashMap<String, NotificationTemplateDef>,
|
||||||
|
|
||||||
/// Priority groups for notification delivery
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub groups: HashMap<u32, HashMap<String, NotificationMethod>>,
|
pub groups: HashMap<u32, HashMap<String, NotificationMethod>>,
|
||||||
}
|
}
|
||||||
@@ -146,6 +169,63 @@ pub struct OnFinishOf {
|
|||||||
pub content: String,
|
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
|
/// Artifact definition
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct ArtifactDef {
|
pub struct ArtifactDef {
|
||||||
@@ -167,16 +247,11 @@ pub struct ArtifactDef {
|
|||||||
pub retention_ms: u64,
|
pub retention_ms: u64,
|
||||||
|
|
||||||
/// Compression method: "zstd", "gzip", "none", "dir"
|
/// Compression method: "zstd", "gzip", "none", "dir"
|
||||||
#[serde(default = "default_compression")]
|
#[serde(default)]
|
||||||
pub compression: CompressionMethod,
|
pub compression: CompressionMethod,
|
||||||
|
|
||||||
/// Trigger conditions: "success", "failure", "always" (WIP: failure, always)
|
// Publishing targets
|
||||||
#[serde(default = "default_on_conditions")]
|
// TODO: under heavy refactor
|
||||||
pub on: Vec<ArtifactCondition>,
|
|
||||||
|
|
||||||
/// Publishing targets
|
|
||||||
#[serde(default)]
|
|
||||||
pub publish: HashMap<String, PublishTarget>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ArtifactDef {
|
impl Default for ArtifactDef {
|
||||||
@@ -185,55 +260,19 @@ impl Default for ArtifactDef {
|
|||||||
id: None,
|
id: None,
|
||||||
path: String::new(),
|
path: String::new(),
|
||||||
retention_ms: default_retention_ms(),
|
retention_ms: default_retention_ms(),
|
||||||
compression: default_compression(),
|
compression: CompressionMethod::default(),
|
||||||
on: default_on_conditions(),
|
|
||||||
publish: HashMap::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compression method for artifacts
|
/// Artifact
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
/// TODO: Under heavy refactor
|
||||||
#[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,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cleanup configuration
|
/// Cleanup configuration
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||||
pub struct CleanupConfig {
|
pub struct CleanupConfig {
|
||||||
/// Cleanup policy: "auto", "manual", "on_failure" (WIP: manual, on_failure)
|
/// Cleanup policy: "auto", "manual", "on_failure" (WIP: manual, on_failure)
|
||||||
#[serde(default = "default_cleanup_policy")]
|
#[serde(default)]
|
||||||
pub policy: CleanupPolicy,
|
pub policy: CleanupPolicy,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +283,8 @@ pub enum CleanupPolicy {
|
|||||||
#[default]
|
#[default]
|
||||||
Auto,
|
Auto,
|
||||||
Manual,
|
Manual,
|
||||||
OnFailure,
|
OnSuccess,
|
||||||
|
Never,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hooks configuration for finalize stage
|
/// Hooks configuration for finalize stage
|
||||||
@@ -264,18 +304,6 @@ fn default_retention_ms() -> u64 {
|
|||||||
7 * 24 * 3600 * 1000
|
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 {
|
impl FinalizeConfig {
|
||||||
/// Validate the finalize configuration
|
/// Validate the finalize configuration
|
||||||
pub fn validate(&self) -> Result<(), Vec<String>> {
|
pub fn validate(&self) -> Result<(), Vec<String>> {
|
||||||
@@ -311,8 +339,7 @@ impl FinalizeConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Schema-based validation for plugin configs
|
self.validate_notification(&mut errors);
|
||||||
// TODO: Validate that publish targets reference existing plugins
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
if errors.is_empty() {
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -320,136 +347,16 @@ impl FinalizeConfig {
|
|||||||
Err(errors)
|
Err(errors)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
fn validate_notification(&self, errors: &mut Vec<String>) {
|
||||||
mod tests {
|
// Validate templates
|
||||||
use super::*;
|
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()) {
|
||||||
#[test]
|
errors.push(format!(
|
||||||
fn test_deserialize_hooks_full() {
|
"Template '{}' cannot combine 'use' with 'on_*' fields (MVP restriction)",
|
||||||
let yaml = r#"
|
name
|
||||||
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"))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,19 +34,19 @@ pub mod cli {
|
|||||||
#[command(version, about, long_about=None)]
|
#[command(version, about, long_about=None)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Username of the pipeline executor
|
/// Username of the pipeline executor
|
||||||
#[arg(short, long)]
|
#[arg(short, long, env = "HBW_USERNAME")]
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
|
||||||
/// Pipeline ID of this pipeline
|
/// Pipeline ID of this pipeline
|
||||||
#[arg(short, long)]
|
#[arg(short, long, env = "HBW_PIPELINE")]
|
||||||
pub pipeline: String,
|
pub pipeline: String,
|
||||||
|
|
||||||
/// Build ID of this build
|
/// Build ID of this build
|
||||||
#[arg(short, long)]
|
#[arg(short, long, env = "HBW_BUILD_ID")]
|
||||||
pub build_id: String,
|
pub build_id: String,
|
||||||
|
|
||||||
/// Parse the script without executing
|
/// Parse the script without executing
|
||||||
#[arg(long, default_value = "false")]
|
#[arg(long, default_value = "false", env = "HBW_DRY_RUN")]
|
||||||
pub dry_run: bool,
|
pub dry_run: bool,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
@@ -55,12 +55,21 @@ pub mod cli {
|
|||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
/// Monitor a running task
|
/// Run pipeline stages in standalone (bare) mode
|
||||||
Monitor { group: String },
|
Bare {
|
||||||
/// Prepare a task
|
#[command(subcommand)]
|
||||||
Prebake { config: PathBuf },
|
command: BareCommands,
|
||||||
|
},
|
||||||
|
|
||||||
/// Run in client mode
|
/// Run in client mode
|
||||||
Client { config: String },
|
Client { config: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
pub enum BareCommands {
|
||||||
|
/// Prepare a task
|
||||||
|
Prebake { config: PathBuf },
|
||||||
|
|
||||||
/// Run a task
|
/// Run a task
|
||||||
Bake {
|
Bake {
|
||||||
/// Path to the script to execute
|
/// Path to the script to execute
|
||||||
@@ -71,6 +80,7 @@ pub mod cli {
|
|||||||
#[arg(short, long, default_value = "./prebake.yml")]
|
#[arg(short, long, default_value = "./prebake.yml")]
|
||||||
prebake: PathBuf,
|
prebake: PathBuf,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Finish a task
|
/// Finish a task
|
||||||
Finalize { config: String },
|
Finalize { config: String },
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-16
@@ -2,7 +2,7 @@ use std::path::Path;
|
|||||||
|
|
||||||
use workshop_baker::{
|
use workshop_baker::{
|
||||||
bake::bake,
|
bake::bake,
|
||||||
cli::{Cli, Commands, Parser},
|
cli::{BareCommands, Cli, Commands, Parser},
|
||||||
daemon,
|
daemon,
|
||||||
engine::{EventSender, ExecutionContext},
|
engine::{EventSender, ExecutionContext},
|
||||||
error::PrebakeError,
|
error::PrebakeError,
|
||||||
@@ -18,13 +18,11 @@ async fn main() {
|
|||||||
log::warn!("Dry run enabled, no changes will be made.");
|
log::warn!("Dry run enabled, no changes will be made.");
|
||||||
}
|
}
|
||||||
match &cli.command {
|
match &cli.command {
|
||||||
Some(Commands::Monitor { group: _ }) => {
|
|
||||||
unimplemented!("executor-monitor should be moved to daemon instead.")
|
|
||||||
}
|
|
||||||
Some(Commands::Client { config: _ }) => {
|
Some(Commands::Client { config: _ }) => {
|
||||||
unimplemented!("Client mode not yet implemented.")
|
unimplemented!("Client mode not yet implemented.")
|
||||||
}
|
}
|
||||||
Some(Commands::Prebake { config }) => {
|
Some(Commands::Bare { command }) => match command {
|
||||||
|
BareCommands::Prebake { config } => {
|
||||||
log::info!("Running in standalone mode.");
|
log::info!("Running in standalone mode.");
|
||||||
let mut ctx = ExecutionContext {
|
let mut ctx = ExecutionContext {
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -39,7 +37,8 @@ async fn main() {
|
|||||||
let event_tx: EventSender = Some(tx);
|
let event_tx: EventSender = Some(tx);
|
||||||
let pipeline_name = cli.pipeline.clone();
|
let pipeline_name = cli.pipeline.clone();
|
||||||
let build_id = cli.build_id.clone();
|
let build_id = cli.build_id.clone();
|
||||||
let receiver_handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
let receiver_handle =
|
||||||
|
tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
||||||
event_rx,
|
event_rx,
|
||||||
pipeline_name,
|
pipeline_name,
|
||||||
build_id,
|
build_id,
|
||||||
@@ -57,11 +56,11 @@ async fn main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Commands::Bake {
|
BareCommands::Bake {
|
||||||
script,
|
script,
|
||||||
bake_base,
|
bake_base,
|
||||||
prebake,
|
prebake: prebake_path,
|
||||||
}) => {
|
} => {
|
||||||
log::info!("Running in standalone mode.");
|
log::info!("Running in standalone mode.");
|
||||||
let mut ctx = ExecutionContext {
|
let mut ctx = ExecutionContext {
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -76,12 +75,14 @@ async fn main() {
|
|||||||
let event_tx: EventSender = Some(tx);
|
let event_tx: EventSender = Some(tx);
|
||||||
let pipeline_name = cli.pipeline.clone();
|
let pipeline_name = cli.pipeline.clone();
|
||||||
let build_id = cli.build_id.clone();
|
let build_id = cli.build_id.clone();
|
||||||
let receiver_handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
let receiver_handle =
|
||||||
|
tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
||||||
event_rx,
|
event_rx,
|
||||||
pipeline_name,
|
pipeline_name,
|
||||||
build_id,
|
build_id,
|
||||||
));
|
));
|
||||||
let result = bake(script, bake_base, prebake, &cli, &mut ctx, event_tx).await;
|
let result =
|
||||||
|
bake(script, bake_base, prebake_path, &cli, &mut ctx, event_tx).await;
|
||||||
drop(ctx);
|
drop(ctx);
|
||||||
let _ = receiver_handle.await;
|
let _ = receiver_handle.await;
|
||||||
dbg!(&result);
|
dbg!(&result);
|
||||||
@@ -90,7 +91,7 @@ async fn main() {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Commands::Finalize { config }) => {
|
BareCommands::Finalize { config } => {
|
||||||
log::info!("Running in standalone mode.");
|
log::info!("Running in standalone mode.");
|
||||||
let mut ctx = ExecutionContext {
|
let mut ctx = ExecutionContext {
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -105,12 +106,14 @@ async fn main() {
|
|||||||
let event_tx: EventSender = Some(tx);
|
let event_tx: EventSender = Some(tx);
|
||||||
let pipeline_name = cli.pipeline.clone();
|
let pipeline_name = cli.pipeline.clone();
|
||||||
let build_id = cli.build_id.clone();
|
let build_id = cli.build_id.clone();
|
||||||
let receiver_handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
let receiver_handle =
|
||||||
|
tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
||||||
event_rx,
|
event_rx,
|
||||||
pipeline_name,
|
pipeline_name,
|
||||||
build_id,
|
build_id,
|
||||||
));
|
));
|
||||||
let result = finalize(Path::new(config), &cli, &mut ctx, event_tx).await;
|
let result =
|
||||||
|
finalize(Path::new(config), &cli, &mut ctx, event_tx).await;
|
||||||
drop(ctx);
|
drop(ctx);
|
||||||
let _ = receiver_handle.await;
|
let _ = receiver_handle.await;
|
||||||
dbg!(&result);
|
dbg!(&result);
|
||||||
@@ -119,10 +122,9 @@ async fn main() {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
None => {
|
None => {
|
||||||
// Running in daemon mode
|
|
||||||
log::info!("Executor will be running in daemon mode.");
|
log::info!("Executor will be running in daemon mode.");
|
||||||
|
|
||||||
let _result = daemon::daemon(&cli).await;
|
let _result = daemon::daemon(&cli).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ type NotifyEventReceiver = tokio::sync::mpsc::Receiver<BundledNotifyEvent>;
|
|||||||
pub type NotifyEventSender = tokio::sync::mpsc::Sender<NotifyEvent>;
|
pub type NotifyEventSender = tokio::sync::mpsc::Sender<NotifyEvent>;
|
||||||
|
|
||||||
const NOTIFY_NOTREADY_DELAY: std::time::Duration = std::time::Duration::from_secs(5);
|
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_FACTOR: f64 = 2.0;
|
||||||
const NOTIFY_TEMPORARY_DELAY_TIME: f64 = 5.0;
|
const NOTIFY_TEMPORARY_DELAY_TIME: f64 = 5.0;
|
||||||
const NOTIFY_TEMPORARY_MAX_TIME: f64 = f64::MAX;
|
const NOTIFY_TEMPORARY_MAX_TIME: f64 = f64::MAX;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use crate::prebake::config::Bootstrap;
|
|||||||
use crate::prebake::config::PrebakeConfig;
|
use crate::prebake::config::PrebakeConfig;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::{PathBuf,Path};
|
||||||
use which::which;
|
use which::which;
|
||||||
|
|
||||||
/// Executes the bootstrap stage of the prebake workflow.
|
/// 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();
|
let bootstrap_config = config.bootstrap.clone().unwrap_or_default();
|
||||||
|
|
||||||
if bootstrap_config.custom.is_none() {
|
if let Some(custom_bootstrap) = bootstrap_config.custom {
|
||||||
let script = generate_bootstrap(&bootstrap_config, config, osinfo)?;
|
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 {
|
if ctx.dry_run {
|
||||||
log::info!("[DRY_RUN] Generated bootstrap script:\n{}", script);
|
log::info!("[DRY_RUN] Generated bootstrap script:\n{}", script);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
log::debug!("Generated bootstrap script:\n{}", script);
|
log::debug!("Generated bootstrap script:\n{}", script);
|
||||||
let mut file = File::create(&bootstrap_path)?;
|
write_bootstrap_content(&bootstrap_path, script)?;
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if bootstrap script exists before execution
|
// Check if bootstrap script exists before execution
|
||||||
@@ -84,6 +90,16 @@ pub async fn bootstrap(
|
|||||||
Ok(())
|
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.
|
/// Generates a bootstrap script based on configuration and target OS.
|
||||||
///
|
///
|
||||||
/// The generated script performs the following setup tasks:
|
/// The generated script performs the following setup tasks:
|
||||||
@@ -117,6 +133,7 @@ fn generate_bootstrap(
|
|||||||
bootstrap_config: &Bootstrap,
|
bootstrap_config: &Bootstrap,
|
||||||
config: &PrebakeConfig,
|
config: &PrebakeConfig,
|
||||||
osinfo: &os_info::Info,
|
osinfo: &os_info::Info,
|
||||||
|
standalone: bool,
|
||||||
) -> Result<String, BootstrapError> {
|
) -> Result<String, BootstrapError> {
|
||||||
let mut script = String::new();
|
let mut script = String::new();
|
||||||
// Generate header
|
// Generate header
|
||||||
@@ -128,7 +145,12 @@ fn generate_bootstrap(
|
|||||||
let user = &bootstrap_config.user;
|
let user = &bootstrap_config.user;
|
||||||
if user == "root" {
|
if user == "root" {
|
||||||
// Requires PIP(Pipeline Implicit Parameters) to be implemented
|
// Requires PIP(Pipeline Implicit Parameters) to be implemented
|
||||||
|
// 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...");
|
unimplemented!("Permission check is not implemented, failing...");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
script.push_str("# User creation\n");
|
script.push_str("# User creation\n");
|
||||||
script.push_str(&format!("if ! id -u {} >/dev/null 2>&1; then\n", user));
|
script.push_str(&format!("if ! id -u {} >/dev/null 2>&1; then\n", user));
|
||||||
@@ -166,9 +188,20 @@ fn generate_bootstrap(
|
|||||||
script.push_str("# Setup privilege executor\n");
|
script.push_str("# Setup privilege executor\n");
|
||||||
let pkg_manager = get_package_manager(config, osinfo);
|
let pkg_manager = get_package_manager(config, osinfo);
|
||||||
if which("sudo").is_ok() {
|
if which("sudo").is_ok() {
|
||||||
if bootstrap_config.sudoers.is_some() {
|
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...")
|
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 {
|
if let Some(pkg_manager) = pkg_manager {
|
||||||
let allowed_commands = format!("/usr/bin/{} *", pkg_manager.binary());
|
let allowed_commands = format!("/usr/bin/{} *", pkg_manager.binary());
|
||||||
let content = format!("{} ALL=(ALL:ALL) NOPASSWD {}", user, allowed_commands);
|
let content = format!("{} ALL=(ALL:ALL) NOPASSWD {}", user, allowed_commands);
|
||||||
@@ -179,12 +212,29 @@ fn generate_bootstrap(
|
|||||||
script.push_str("chmod 440 /etc/sudoers.d/workshop\n");
|
script.push_str("chmod 440 /etc/sudoers.d/workshop\n");
|
||||||
}
|
}
|
||||||
} else if which("doas").is_ok() {
|
} else if which("doas").is_ok() {
|
||||||
if bootstrap_config.doas.is_some() {
|
if let Some(doas) = &bootstrap_config.doas {
|
||||||
// Requires PIP(Pipeline Implicit Parameters) to be implemented
|
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...")
|
unimplemented!("Permission check is not implemented, failing...")
|
||||||
}
|
}
|
||||||
// We do not know how to write doas.conf lol
|
script.push_str(&format!(
|
||||||
unimplemented!("doas is not implemented");
|
"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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
script.push_str("echo 'Bootstrap complete!'\n");
|
script.push_str("echo 'Bootstrap complete!'\n");
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ pub mod command;
|
|||||||
pub mod memsize;
|
pub mod memsize;
|
||||||
pub mod repology;
|
pub mod repology;
|
||||||
pub mod time;
|
pub mod time;
|
||||||
|
pub mod compression;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user