Compare commits
4 Commits
84b3b7d8e8
...
9e45bdbbfb
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e45bdbbfb | |||
| 5ce8b72900 | |||
| 2c9d32e954 | |||
| 901005639b |
+117
@@ -0,0 +1,117 @@
|
|||||||
|
diff --git a/workshop-baker/src/notify.rs b/workshop-baker/src/notify.rs
|
||||||
|
index 75bce82..ad56c66 100644
|
||||||
|
--- a/workshop-baker/src/notify.rs
|
||||||
|
+++ b/workshop-baker/src/notify.rs
|
||||||
|
@@ -3,15 +3,15 @@ use crate::finalize::FinalizeConfig;
|
||||||
|
use crate::finalize::parse;
|
||||||
|
use crate::finalize::plugin;
|
||||||
|
use crate::finalize::plugin::fetch;
|
||||||
|
-use crate::notify::types::BundledNotifyEvent;
|
||||||
|
+use crate::notify::types::BundledNotificationEvent;
|
||||||
|
use crate::notify::types::NOTIFY_TEMPORARY_DELAY_FACTOR;
|
||||||
|
use crate::notify::types::NOTIFY_TEMPORARY_DELAY_TIME;
|
||||||
|
use crate::notify::types::NOTIFY_TEMPORARY_MAX_TIME;
|
||||||
|
use crate::notify::types::NotificationRulesMap;
|
||||||
|
-use crate::notify::types::NotifyEvent;
|
||||||
|
-use crate::notify::types::NotifyEventReceiver;
|
||||||
|
-use crate::notify::types::NotifyEventSender;
|
||||||
|
-use crate::notify::types::NotifyPluginMap;
|
||||||
|
+use crate::notify::types::NotificationEvent;
|
||||||
|
+use crate::notify::types::NotificationEventReceiver;
|
||||||
|
+use crate::notify::types::NotificationEventSender;
|
||||||
|
+use crate::notify::types::NotificationPluginMap;
|
||||||
|
use crate::{EventSender, ExecutionContext};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::Path;
|
||||||
|
@@ -46,8 +46,8 @@ pub async fn notify(
|
||||||
|
finalize_path: &Path,
|
||||||
|
ctx: &mut ExecutionContext,
|
||||||
|
event_tx: EventSender,
|
||||||
|
- notifyevent_rx: &mut NotifyEventReceiver,
|
||||||
|
- notifyevent_tx: NotifyEventSender,
|
||||||
|
+ notifyevent_rx: &mut NotificationEventReceiver,
|
||||||
|
+ notifyevent_tx: NotificationEventSender,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut finalize = parse(finalize_path)?;
|
||||||
|
validate(&finalize)?;
|
||||||
|
@@ -114,10 +114,11 @@ pub async fn notify(
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notification_handler(
|
||||||
|
- plugins: &NotifyPluginMap,
|
||||||
|
+ plugins: &NotificationPluginMap,
|
||||||
|
config: &NotificationRulesMap,
|
||||||
|
- event: &BundledNotifyEvent,
|
||||||
|
+ event: &BundledNotificationEvent,
|
||||||
|
) -> Result<(), NotifyError> {
|
||||||
|
+
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -132,7 +133,7 @@ fn wait_time(group_period: u32, group_watermark: u32, count: u32) -> f32 {
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate p^(m-l)*t as notification delay
|
||||||
|
-fn notify_delay(event: &NotifyEvent) -> std::time::Duration {
|
||||||
|
+fn notify_delay(event: &NotificationEvent) -> std::time::Duration {
|
||||||
|
let exponent = (event.max_lives - event.lives) as i32;
|
||||||
|
if exponent < 0 {
|
||||||
|
log::warn!(
|
||||||
|
diff --git a/workshop-baker/src/notify/types.rs b/workshop-baker/src/notify/types.rs
|
||||||
|
index b852dfb..d6ccfb7 100644
|
||||||
|
--- a/workshop-baker/src/notify/types.rs
|
||||||
|
+++ b/workshop-baker/src/notify/types.rs
|
||||||
|
@@ -39,13 +39,13 @@ pub struct NotificationRule{
|
||||||
|
pub type Priority = u32;
|
||||||
|
pub type Method = String;
|
||||||
|
pub type NotificationRulesMap = HashMap<Priority, HashMap<Method, Vec<NotificationRule>>>;
|
||||||
|
-pub type BundledNotifyEvent = Vec<NotifyEvent>;
|
||||||
|
-pub type NotifyEventReceiver = tokio::sync::mpsc::Receiver<BundledNotifyEvent>;
|
||||||
|
-pub type NotifyEventSender = tokio::sync::mpsc::Sender<NotifyEvent>;
|
||||||
|
-pub type NotifyPluginMap = HashMap<String, FinalizePlugin>;
|
||||||
|
+pub type BundledNotificationEvent = Vec<NotificationEvent>;
|
||||||
|
+pub type NotificationEventReceiver = tokio::sync::mpsc::Receiver<BundledNotificationEvent>;
|
||||||
|
+pub type NotificationEventSender = tokio::sync::mpsc::Sender<NotificationEvent>;
|
||||||
|
+pub type NotificationPluginMap = HashMap<String, FinalizePlugin>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
-pub struct NotifyEvent {
|
||||||
|
+pub struct NotificationEvent {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub stage: StagePhase,
|
||||||
|
pub substage: String,
|
||||||
|
@@ -58,21 +58,21 @@ pub struct NotifyEvent {
|
||||||
|
pub target: HashSet<String>, // Target audience
|
||||||
|
}
|
||||||
|
|
||||||
|
-impl Hash for NotifyEvent {
|
||||||
|
+impl Hash for NotificationEvent {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.id.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-impl PartialEq for NotifyEvent {
|
||||||
|
+impl PartialEq for NotificationEvent {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id == other.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-impl Eq for NotifyEvent {}
|
||||||
|
+impl Eq for NotificationEvent {}
|
||||||
|
|
||||||
|
-impl PartialOrd for NotifyEvent {
|
||||||
|
+impl PartialOrd for NotificationEvent {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(
|
||||||
|
self.effective_priority
|
||||||
|
@@ -83,7 +83,7 @@ impl PartialOrd for NotifyEvent {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-impl Ord for NotifyEvent {
|
||||||
|
+impl Ord for NotificationEvent {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.partial_cmp(other).unwrap()
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
use os_info::Info;
|
use os_info::Info;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::fmt::Display;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
// Obsolete?
|
||||||
|
pub trait HasExitCode: Display + Debug + Send + Sync + Sized + 'static {
|
||||||
|
fn exit_code(&self) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
pub const EXITCODE_OK: i32 = 0;
|
pub const EXITCODE_OK: i32 = 0;
|
||||||
pub const EXITCODE_GENERAL_ERROR: i32 = 1;
|
pub const EXITCODE_GENERAL_ERROR: i32 = 1;
|
||||||
pub const EXITCODE_INVALID_ARGS: i32 = 2;
|
pub const EXITCODE_INVALID_ARGS: i32 = 2;
|
||||||
@@ -69,8 +76,8 @@ pub enum PrebakeError {
|
|||||||
ConfigError(String),
|
ConfigError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrebakeError {
|
impl HasExitCode for PrebakeError {
|
||||||
pub fn exit_code(&self) -> i32 {
|
fn exit_code(&self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
PrebakeError::PrivDropFailed(_) => EXITCODE_PRIV_DROP_FAILED,
|
PrebakeError::PrivDropFailed(_) => EXITCODE_PRIV_DROP_FAILED,
|
||||||
PrebakeError::UserNotFound(_) => EXITCODE_PRIV_DROP_FAILED,
|
PrebakeError::UserNotFound(_) => EXITCODE_PRIV_DROP_FAILED,
|
||||||
@@ -149,8 +156,23 @@ pub enum ExecutionError {
|
|||||||
PackageManagerNotFound,
|
PackageManagerNotFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExecutionError {
|
impl HasExitCode for BakeError {
|
||||||
pub fn exit_code(&self) -> i32 {
|
fn exit_code(&self) -> i32 {
|
||||||
|
match self {
|
||||||
|
BakeError::ScriptGenerationFailed { .. } => EXITCODE_IO_ERROR,
|
||||||
|
BakeError::TemplateError(_) => EXITCODE_PARSE_ERROR,
|
||||||
|
BakeError::CircularDependency(_) => EXITCODE_PARSE_ERROR,
|
||||||
|
BakeError::UnknownDependency(..) => EXITCODE_PARSE_ERROR,
|
||||||
|
BakeError::IoError(_) => EXITCODE_IO_ERROR,
|
||||||
|
BakeError::YamlParseError(_) => EXITCODE_PARSE_ERROR,
|
||||||
|
BakeError::DecoratorParseError { .. } => EXITCODE_PARSE_ERROR,
|
||||||
|
BakeError::ScriptExecutionError(e) => e.exit_code(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasExitCode for ExecutionError {
|
||||||
|
fn exit_code(&self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
ExecutionError::InvalidCommand(_) => EXITCODE_EXECUTION_ERROR,
|
ExecutionError::InvalidCommand(_) => EXITCODE_EXECUTION_ERROR,
|
||||||
ExecutionError::ExecutionFailed(_) => EXITCODE_EXECUTION_ERROR,
|
ExecutionError::ExecutionFailed(_) => EXITCODE_EXECUTION_ERROR,
|
||||||
|
|||||||
@@ -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};
|
||||||
@@ -98,10 +99,25 @@ pub struct PluginRuntimeConfig {
|
|||||||
/// Notification method configuration
|
/// Notification method configuration
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct NotificationMethod {
|
pub struct NotificationMethod {
|
||||||
/// Whether notification failure should not trigger next priority group
|
/// Template definition
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub fallible: bool,
|
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 +133,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 +140,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 +157,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 +235,10 @@ 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")]
|
|
||||||
pub compression: CompressionMethod,
|
|
||||||
|
|
||||||
/// Trigger conditions: "success", "failure", "always" (WIP: failure, always)
|
|
||||||
#[serde(default = "default_on_conditions")]
|
|
||||||
pub on: Vec<ArtifactCondition>,
|
|
||||||
|
|
||||||
/// Publishing targets
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub publish: HashMap<String, PublishTarget>,
|
pub compression: CompressionMethod,
|
||||||
|
// Publishing targets
|
||||||
|
// TODO: under heavy refactor
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ArtifactDef {
|
impl Default for ArtifactDef {
|
||||||
@@ -185,55 +247,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 +270,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 +291,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 +326,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 +334,20 @@ 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()
|
||||||
#[test]
|
&& (tmpl.on_success.is_some()
|
||||||
fn test_deserialize_hooks_full() {
|
|| tmpl.on_failure.is_some()
|
||||||
let yaml = r#"
|
|| tmpl.on_finish_of.is_some())
|
||||||
version: "0.0.1"
|
{
|
||||||
hooks:
|
errors.push(format!(
|
||||||
early:
|
"Template '{}' cannot combine 'use' with 'on_*' fields (MVP restriction)",
|
||||||
- name: "test-early"
|
name
|
||||||
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"))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use crate::{
|
|||||||
ExecutionError,
|
ExecutionError,
|
||||||
error::{
|
error::{
|
||||||
EXITCODE_EXECUTION_ERROR, EXITCODE_GENERAL_ERROR, EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR,
|
EXITCODE_EXECUTION_ERROR, EXITCODE_GENERAL_ERROR, EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR,
|
||||||
|
HasExitCode,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,8 +39,8 @@ pub enum FinalizeError {
|
|||||||
ExecutionError(#[from] ExecutionError),
|
ExecutionError(#[from] ExecutionError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FinalizeError {
|
impl HasExitCode for FinalizeError {
|
||||||
pub fn exit_code(&self) -> i32 {
|
fn exit_code(&self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
FinalizeError::YamlParseError(_) => EXITCODE_PARSE_ERROR,
|
FinalizeError::YamlParseError(_) => EXITCODE_PARSE_ERROR,
|
||||||
FinalizeError::ValidateError(_) => EXITCODE_PARSE_ERROR,
|
FinalizeError::ValidateError(_) => EXITCODE_PARSE_ERROR,
|
||||||
|
|||||||
@@ -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 },
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-83
@@ -2,10 +2,10 @@ 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::HasExitCode,
|
||||||
finalize::finalize,
|
finalize::finalize,
|
||||||
prebake::prebake,
|
prebake::prebake,
|
||||||
};
|
};
|
||||||
@@ -18,111 +18,75 @@ 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 }) => {
|
||||||
log::info!("Running in standalone mode.");
|
log::info!("Running in standalone mode.");
|
||||||
|
|
||||||
|
let (stage, privileged) = match command {
|
||||||
|
BareCommands::Prebake { .. } => ("init", true),
|
||||||
|
BareCommands::Bake { .. } => ("bake", false),
|
||||||
|
BareCommands::Finalize { .. } => ("finalize", false),
|
||||||
|
};
|
||||||
let mut ctx = ExecutionContext {
|
let mut ctx = ExecutionContext {
|
||||||
standalone: true,
|
standalone: true,
|
||||||
task_id: format!("{}-{}-init", cli.pipeline, cli.build_id),
|
task_id: format!("{}-{}-{}", cli.pipeline, cli.build_id, stage),
|
||||||
pipeline_name: cli.pipeline.clone(),
|
pipeline_name: cli.pipeline.clone(),
|
||||||
username: cli.username.clone(),
|
username: cli.username.clone(),
|
||||||
dry_run: cli.dry_run,
|
dry_run: cli.dry_run,
|
||||||
privileged: true,
|
privileged,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
|
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
|
||||||
let event_tx: EventSender = Some(tx);
|
let event_tx: EventSender = Some(tx);
|
||||||
let pipeline_name = cli.pipeline.clone();
|
let handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
||||||
let build_id = cli.build_id.clone();
|
|
||||||
let receiver_handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
|
||||||
event_rx,
|
event_rx,
|
||||||
pipeline_name,
|
cli.pipeline.clone(),
|
||||||
build_id,
|
cli.build_id.clone(),
|
||||||
));
|
));
|
||||||
let result = prebake(config, &cli, None, &mut ctx, event_tx).await;
|
|
||||||
|
let result: anyhow::Result<()> = match command {
|
||||||
|
BareCommands::Prebake { config } => {
|
||||||
|
prebake(config, &cli, None, &mut ctx, event_tx).await
|
||||||
|
}
|
||||||
|
BareCommands::Bake {
|
||||||
|
script,
|
||||||
|
bake_base,
|
||||||
|
prebake: prebake_path,
|
||||||
|
} => bake(script, bake_base, prebake_path, &cli, &mut ctx, event_tx)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.into()),
|
||||||
|
BareCommands::Finalize { config } => {
|
||||||
|
finalize(Path::new(config), &cli, &mut ctx, event_tx)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
drop(ctx);
|
drop(ctx);
|
||||||
let _ = receiver_handle.await;
|
let _ = handle.await;
|
||||||
dbg!(&result);
|
dbg!(&result);
|
||||||
if let Err(ref e) = result {
|
if let Err(ref e) = result {
|
||||||
log::error!("Prebake stage failed: {}", e);
|
let code = match command {
|
||||||
std::process::exit(
|
BareCommands::Prebake { .. } => e
|
||||||
e.downcast_ref::<PrebakeError>()
|
.downcast_ref::<workshop_baker::error::PrebakeError>()
|
||||||
.map(|pe| pe.exit_code())
|
.map(|pe| pe.exit_code())
|
||||||
.unwrap_or(1),
|
.unwrap_or(1),
|
||||||
);
|
BareCommands::Bake { .. } => e
|
||||||
}
|
.downcast_ref::<workshop_baker::error::BakeError>()
|
||||||
}
|
.map(|be| be.exit_code())
|
||||||
Some(Commands::Bake {
|
.unwrap_or(1),
|
||||||
script,
|
BareCommands::Finalize { .. } => e
|
||||||
bake_base,
|
.downcast_ref::<workshop_baker::finalize::FinalizeError>()
|
||||||
prebake,
|
.map(|fe| fe.exit_code())
|
||||||
}) => {
|
.unwrap_or(1),
|
||||||
log::info!("Running in standalone mode.");
|
};
|
||||||
let mut ctx = ExecutionContext {
|
log::error!("{} stage failed: {}", stage, e);
|
||||||
standalone: true,
|
std::process::exit(code);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-114
@@ -1,23 +1,24 @@
|
|||||||
use crate::constant::FINALIZE_STANDALONE_PLUGIN_PATH;
|
use crate::constant::FINALIZE_STANDALONE_PLUGIN_PATH;
|
||||||
use crate::finalize::FinalizeConfig;
|
use crate::finalize::FinalizeConfig;
|
||||||
use crate::finalize::NotificationConfig;
|
|
||||||
use crate::finalize::parse;
|
use crate::finalize::parse;
|
||||||
use crate::finalize::plugin;
|
use crate::finalize::plugin;
|
||||||
use crate::finalize::plugin::FinalizePlugin;
|
|
||||||
use crate::finalize::plugin::fetch;
|
use crate::finalize::plugin::fetch;
|
||||||
use crate::types::buildstatus::StagePhase;
|
use crate::notify::types::BundledNotificationEvent;
|
||||||
use crate::types::buildstatus::StageResult;
|
use crate::notify::types::NOTIFY_TEMPORARY_DELAY_FACTOR;
|
||||||
|
use crate::notify::types::NOTIFY_TEMPORARY_DELAY_TIME;
|
||||||
|
use crate::notify::types::NOTIFY_TEMPORARY_MAX_TIME;
|
||||||
|
use crate::notify::types::NotificationRulesMap;
|
||||||
|
use crate::notify::types::NotificationEvent;
|
||||||
|
use crate::notify::types::NotificationEventReceiver;
|
||||||
|
use crate::notify::types::NotificationEventSender;
|
||||||
|
use crate::notify::types::NotificationPluginMap;
|
||||||
use crate::{EventSender, ExecutionContext};
|
use crate::{EventSender, ExecutionContext};
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::hash::Hash;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
|
mod types;
|
||||||
use error::NotifyError;
|
use error::NotifyError;
|
||||||
|
|
||||||
fn validate(finalize: &FinalizeConfig) -> anyhow::Result<()> {
|
fn validate(finalize: &FinalizeConfig) -> anyhow::Result<()> {
|
||||||
@@ -31,8 +32,6 @@ fn validate(finalize: &FinalizeConfig) -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type NotifyPluginMap = HashMap<String, FinalizePlugin>;
|
|
||||||
|
|
||||||
fn extract_plugin(finalize: &FinalizeConfig) -> HashSet<String> {
|
fn extract_plugin(finalize: &FinalizeConfig) -> HashSet<String> {
|
||||||
finalize
|
finalize
|
||||||
.notification
|
.notification
|
||||||
@@ -43,67 +42,12 @@ fn extract_plugin(finalize: &FinalizeConfig) -> HashSet<String> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct NotifyEvent {
|
|
||||||
id: Uuid,
|
|
||||||
stage: StagePhase,
|
|
||||||
substage: String,
|
|
||||||
result: StageResult,
|
|
||||||
not_before: DateTime<Utc>,
|
|
||||||
priority: u32,
|
|
||||||
effective_priority: u32,
|
|
||||||
lives: u32,
|
|
||||||
max_lives: u32,
|
|
||||||
target: HashSet<String>, // Target audience
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hash for NotifyEvent {
|
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
||||||
self.id.hash(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for NotifyEvent {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.id == other.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for NotifyEvent {}
|
|
||||||
|
|
||||||
impl PartialOrd for NotifyEvent {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
||||||
Some(
|
|
||||||
self.effective_priority
|
|
||||||
.cmp(&other.effective_priority)
|
|
||||||
.then(self.not_before.cmp(&other.not_before))
|
|
||||||
.reverse(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ord for NotifyEvent {
|
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
||||||
self.partial_cmp(other).unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type BundledNotifyEvent = Vec<NotifyEvent>;
|
|
||||||
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;
|
|
||||||
|
|
||||||
pub async fn notify(
|
pub async fn notify(
|
||||||
finalize_path: &Path,
|
finalize_path: &Path,
|
||||||
ctx: &mut ExecutionContext,
|
ctx: &mut ExecutionContext,
|
||||||
event_tx: EventSender,
|
event_tx: EventSender,
|
||||||
notifyevent_rx: &mut NotifyEventReceiver,
|
notifyevent_rx: &mut NotificationEventReceiver,
|
||||||
notifyevent_tx: NotifyEventSender,
|
notifyevent_tx: NotificationEventSender,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let mut finalize = parse(finalize_path)?;
|
let mut finalize = parse(finalize_path)?;
|
||||||
validate(&finalize)?;
|
validate(&finalize)?;
|
||||||
@@ -125,54 +69,56 @@ pub async fn notify(
|
|||||||
}
|
}
|
||||||
let notify_plugins = plugin::register(finalize.plugin, Some(&plugin_list))?;
|
let notify_plugins = plugin::register(finalize.plugin, Some(&plugin_list))?;
|
||||||
|
|
||||||
loop {
|
// loop {
|
||||||
tokio::select! {
|
// tokio::select! {
|
||||||
Some(events) = notifyevent_rx.recv() => {
|
// Some(events) = notifyevent_rx.recv() => {
|
||||||
match notify_handler(¬ify_plugins, &finalize.notification, &events){
|
// match notify_handler(¬ify_plugins, &finalize.notification, &events){
|
||||||
Ok(_) => {}, // Notification succeeded / failed with fallible
|
// Ok(_) => {}, // Notification succeeded / failed with fallible
|
||||||
Err(e) => {
|
// Err(e) => {
|
||||||
log::debug!("Failed to send notification: {:?}", e);
|
// log::debug!("Failed to send notification: {:?}", e);
|
||||||
match e {
|
// match e {
|
||||||
NotifyError::PluginNotAvailable(_) => {
|
// NotifyError::PluginNotAvailable(_) => {
|
||||||
// Should keep lives, add static delay and give less priority
|
// // Should keep lives, add static delay and give less priority
|
||||||
for mut event in events {
|
// for mut event in events {
|
||||||
event.not_before = Utc::now() + NOTIFY_NOTREADY_DELAY;
|
// event.not_before = Utc::now() + NOTIFY_NOTREADY_DELAY;
|
||||||
event.effective_priority = event.effective_priority.saturating_add(1);
|
// event.effective_priority = event.effective_priority.saturating_add(1);
|
||||||
notifyevent_tx.send(event).await?;
|
// notifyevent_tx.send(event).await?;
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
NotifyError::TemporaryError(_) => {
|
// NotifyError::TemporaryError(_) => {
|
||||||
// Should cost 1 life, add dynamic (exponential) delay
|
// // Should cost 1 life, add dynamic (exponential) delay
|
||||||
for mut event in events {
|
// for mut event in events {
|
||||||
event.lives -= 1;
|
// event.lives -= 1;
|
||||||
event.not_before = Utc::now() + notify_delay(&event);
|
// event.not_before = Utc::now() + notify_delay(&event);
|
||||||
notifyevent_tx.send(event).await?;
|
// notifyevent_tx.send(event).await?;
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
NotifyError::PermanentError(_) => {
|
// NotifyError::PermanentError(_) => {
|
||||||
// Unexpected!
|
// // Unexpected!
|
||||||
// Should be dropped
|
// // Should be dropped
|
||||||
log::error!("None of the configured notification plugins works.");
|
// log::error!("None of the configured notification plugins works.");
|
||||||
log::error!("Message will be dropped.");
|
// log::error!("Message will be dropped.");
|
||||||
// TODO: Send audit/metric information
|
// // TODO: Send audit/metric information
|
||||||
}
|
// }
|
||||||
error => {
|
// error => {
|
||||||
// Unexpected!
|
// // Unexpected!
|
||||||
log::error!("Internal error: notify_handler returned unexpected error: {:?}", error);
|
// log::error!("Internal error: notify_handler returned unexpected error: {:?}", error);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
todo!();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notify_handler(
|
fn notification_handler(
|
||||||
plugins: &NotifyPluginMap,
|
plugins: &NotificationPluginMap,
|
||||||
config: &NotificationConfig,
|
config: &NotificationRulesMap,
|
||||||
event: &Vec<NotifyEvent>,
|
event: &BundledNotificationEvent,
|
||||||
) -> Result<(), NotifyError> {
|
) -> Result<(), NotifyError> {
|
||||||
|
|
||||||
todo!();
|
todo!();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +133,7 @@ fn wait_time(group_period: u32, group_watermark: u32, count: u32) -> f32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// calculate p^(m-l)*t as notification delay
|
// calculate p^(m-l)*t as notification delay
|
||||||
fn notify_delay(event: &NotifyEvent) -> std::time::Duration {
|
fn notify_delay(event: &NotificationEvent) -> std::time::Duration {
|
||||||
let exponent = (event.max_lives - event.lives) as i32;
|
let exponent = (event.max_lives - event.lives) as i32;
|
||||||
if exponent < 0 {
|
if exponent < 0 {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::time::Duration;
|
||||||
|
use crate::finalize::plugin::FinalizePlugin;
|
||||||
|
use crate::types::buildstatus::StagePhase;
|
||||||
|
use crate::types::buildstatus::StageResult;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::hash::Hash;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::finalize::NotificationTemplate;
|
||||||
|
use crate::finalize::config::Trigger;
|
||||||
|
/// Final runtime configuration for notification, restored and flattened from yaml
|
||||||
|
pub struct NotificationRule{
|
||||||
|
pub enabled: bool,
|
||||||
|
|
||||||
|
pub trigger: Vec<Trigger>,
|
||||||
|
|
||||||
|
pub template: NotificationTemplate,
|
||||||
|
|
||||||
|
/// Whether notification failure should not trigger next priority group
|
||||||
|
pub fallible: bool,
|
||||||
|
|
||||||
|
/// Maximum number of attempts
|
||||||
|
pub max_lives: u32,
|
||||||
|
|
||||||
|
/// Timeout for notification execution
|
||||||
|
pub timeout: Duration,
|
||||||
|
|
||||||
|
/// Plugin-specific configuration
|
||||||
|
pub config: serde_yaml::Value,
|
||||||
|
|
||||||
|
/// Factor of exponential backoff for retry delay
|
||||||
|
// Not provided for external use
|
||||||
|
pub retry_backoff: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Priority = u32;
|
||||||
|
pub type Method = String;
|
||||||
|
pub type NotificationRulesMap = HashMap<Priority, HashMap<Method, Vec<NotificationRule>>>;
|
||||||
|
pub type BundledNotificationEvent = Vec<NotificationEvent>;
|
||||||
|
pub type NotificationEventReceiver = tokio::sync::mpsc::Receiver<BundledNotificationEvent>;
|
||||||
|
pub type NotificationEventSender = tokio::sync::mpsc::Sender<NotificationEvent>;
|
||||||
|
pub type NotificationPluginMap = HashMap<String, FinalizePlugin>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct NotificationEvent {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub stage: StagePhase,
|
||||||
|
pub substage: String,
|
||||||
|
pub result: StageResult,
|
||||||
|
pub not_before: DateTime<Utc>,
|
||||||
|
pub priority: u32,
|
||||||
|
pub effective_priority: u32,
|
||||||
|
pub lives: u32,
|
||||||
|
pub max_lives: u32,
|
||||||
|
pub target: HashSet<String>, // Target audience
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for NotificationEvent {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.id.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for NotificationEvent {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id == other.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for NotificationEvent {}
|
||||||
|
|
||||||
|
impl PartialOrd for NotificationEvent {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(
|
||||||
|
self.effective_priority
|
||||||
|
.cmp(&other.effective_priority)
|
||||||
|
.then(self.not_before.cmp(&other.not_before))
|
||||||
|
.reverse(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for NotificationEvent {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.partial_cmp(other).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub const NOTIFY_NOTREADY_DELAY: std::time::Duration = std::time::Duration::from_secs(5);
|
||||||
|
pub const NOTIFY_TEMPORARY_DELAY_FACTOR: f64 = 2.0;
|
||||||
|
pub const NOTIFY_TEMPORARY_DELAY_TIME: f64 = 5.0;
|
||||||
|
pub 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::{Path, PathBuf};
|
||||||
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,30 @@ 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 +92,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 +135,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 +147,14 @@ 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...");
|
// 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 {
|
} 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,8 +192,19 @@ 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 {
|
||||||
unimplemented!("Permission check is not implemented, failing...")
|
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 {
|
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());
|
||||||
@@ -179,12 +216,26 @@ 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 {
|
||||||
unimplemented!("Permission check is not implemented, failing...")
|
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");
|
script.push_str("echo 'Bootstrap complete!'\n");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pub mod builderconfig;
|
|||||||
pub mod buildstatus;
|
pub mod buildstatus;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod command;
|
pub mod command;
|
||||||
|
pub mod compression;
|
||||||
pub mod memsize;
|
pub mod memsize;
|
||||||
pub mod repology;
|
pub mod repology;
|
||||||
pub mod time;
|
pub mod time;
|
||||||
|
|||||||
@@ -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