move repos into monorepo
This commit is contained in:
24
lib/models/job/Cargo.toml
Normal file
24
lib/models/job/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "hero-job"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description = "Job types and models for Hero"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[lib]
|
||||
name = "hero_job"
|
||||
path = "lib.rs"
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
chrono.workspace = true
|
||||
uuid.workspace = true
|
||||
log.workspace = true
|
||||
hex.workspace = true
|
||||
sha2.workspace = true
|
||||
secp256k1.workspace = true
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen.workspace = true
|
||||
338
lib/models/job/lib.rs
Normal file
338
lib/models/job/lib.rs
Normal file
@@ -0,0 +1,338 @@
|
||||
use chrono::{Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
use log::{error};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Signature for a job - contains the signatory's public key and their signature
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JobSignature {
|
||||
/// Public key of the signatory (hex-encoded secp256k1 public key)
|
||||
pub public_key: String,
|
||||
/// Signature (hex-encoded secp256k1 signature)
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// Job status enumeration
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum JobStatus {
|
||||
Created,
|
||||
Dispatched,
|
||||
WaitingForPrerequisites,
|
||||
Started,
|
||||
Error,
|
||||
Stopping,
|
||||
Finished,
|
||||
}
|
||||
|
||||
/// Job result response for RPC calls
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum JobResult {
|
||||
Success { success: String },
|
||||
Error { error: String },
|
||||
}
|
||||
|
||||
impl JobStatus {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
JobStatus::Created => "created",
|
||||
JobStatus::Dispatched => "dispatched",
|
||||
JobStatus::WaitingForPrerequisites => "waiting_for_prerequisites",
|
||||
JobStatus::Started => "started",
|
||||
JobStatus::Error => "error",
|
||||
JobStatus::Stopping => "stopping",
|
||||
JobStatus::Finished => "finished",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"created" => Some(JobStatus::Created),
|
||||
"dispatched" => Some(JobStatus::Dispatched),
|
||||
"waiting_for_prerequisites" => Some(JobStatus::WaitingForPrerequisites),
|
||||
"started" => Some(JobStatus::Started),
|
||||
"error" => Some(JobStatus::Error),
|
||||
"stopping" => Some(JobStatus::Stopping),
|
||||
"finished" => Some(JobStatus::Finished),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Job structure representing a unit of work to be executed
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter_with_clone))]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Job {
|
||||
pub id: String,
|
||||
pub caller_id: String,
|
||||
pub context_id: String,
|
||||
pub payload: String,
|
||||
pub runner: String, // name of the runner to execute this job
|
||||
pub executor: String, // name of the executor the runner will use to execute this job
|
||||
pub timeout: u64, // timeout in seconds
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
|
||||
pub env_vars: HashMap<String, String>, // environment variables for script execution
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
|
||||
/// Signatures from authorized signatories (public keys are included in each signature)
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
|
||||
pub signatures: Vec<JobSignature>,
|
||||
}
|
||||
|
||||
/// Error types for job operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum JobError {
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
#[error("Job not found: {0}")]
|
||||
NotFound(String),
|
||||
#[error("Invalid data: {0}")]
|
||||
InvalidData(String),
|
||||
#[error("Validation error: {0}")]
|
||||
Validation(String),
|
||||
#[error("Signature verification failed: {0}")]
|
||||
SignatureVerification(String),
|
||||
}
|
||||
|
||||
impl Job {
|
||||
/// Create a new job with the given parameters
|
||||
pub fn new(
|
||||
caller_id: String,
|
||||
context_id: String,
|
||||
payload: String,
|
||||
runner: String,
|
||||
executor: String,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
caller_id,
|
||||
context_id,
|
||||
payload,
|
||||
runner,
|
||||
executor,
|
||||
timeout: 300, // 5 minutes default
|
||||
env_vars: HashMap::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
signatures: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the canonical representation of the job for signing
|
||||
/// This creates a deterministic string representation that can be hashed and signed
|
||||
/// Note: Signatures are excluded from the canonical representation
|
||||
pub fn canonical_representation(&self) -> String {
|
||||
// Create a deterministic representation excluding signatures
|
||||
// Sort env_vars keys for deterministic ordering
|
||||
let mut env_vars_sorted: Vec<_> = self.env_vars.iter().collect();
|
||||
env_vars_sorted.sort_by_key(|&(k, _)| k);
|
||||
|
||||
format!(
|
||||
"{}:{}:{}:{}:{}:{}:{}:{:?}",
|
||||
self.id,
|
||||
self.caller_id,
|
||||
self.context_id,
|
||||
self.payload,
|
||||
self.runner,
|
||||
self.executor,
|
||||
self.timeout,
|
||||
env_vars_sorted
|
||||
)
|
||||
}
|
||||
|
||||
/// Get list of signatory public keys from signatures
|
||||
pub fn signatories(&self) -> Vec<String> {
|
||||
self.signatures.iter()
|
||||
.map(|sig| sig.public_key.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Verify that all signatures are valid
|
||||
/// Returns Ok(()) if verification passes, Err otherwise
|
||||
/// Empty signatures list is allowed - loop simply won't execute
|
||||
pub fn verify_signatures(&self) -> Result<(), JobError> {
|
||||
use secp256k1::{Message, PublicKey, Secp256k1, ecdsa::Signature};
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
// Get the canonical representation and hash it
|
||||
let canonical = self.canonical_representation();
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(canonical.as_bytes());
|
||||
let hash = hasher.finalize();
|
||||
|
||||
let secp = Secp256k1::verification_only();
|
||||
let message = Message::from_digest_slice(&hash)
|
||||
.map_err(|e| JobError::SignatureVerification(format!("Invalid message: {}", e)))?;
|
||||
|
||||
// Verify each signature (if any)
|
||||
for sig_data in &self.signatures {
|
||||
// Decode public key
|
||||
let pubkey_bytes = hex::decode(&sig_data.public_key)
|
||||
.map_err(|e| JobError::SignatureVerification(format!("Invalid public key hex: {}", e)))?;
|
||||
let pubkey = PublicKey::from_slice(&pubkey_bytes)
|
||||
.map_err(|e| JobError::SignatureVerification(format!("Invalid public key: {}", e)))?;
|
||||
|
||||
// Decode signature
|
||||
let sig_bytes = hex::decode(&sig_data.signature)
|
||||
.map_err(|e| JobError::SignatureVerification(format!("Invalid signature hex: {}", e)))?;
|
||||
let signature = Signature::from_compact(&sig_bytes)
|
||||
.map_err(|e| JobError::SignatureVerification(format!("Invalid signature: {}", e)))?;
|
||||
|
||||
// Verify signature
|
||||
secp.verify_ecdsa(&message, &signature, &pubkey)
|
||||
.map_err(|e| JobError::SignatureVerification(format!("Signature verification failed: {}", e)))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for constructing job execution requests.
|
||||
pub struct JobBuilder {
|
||||
caller_id: String,
|
||||
context_id: String,
|
||||
payload: String,
|
||||
runner: String,
|
||||
executor: String,
|
||||
timeout: u64, // timeout in seconds
|
||||
env_vars: HashMap<String, String>,
|
||||
signatures: Vec<JobSignature>,
|
||||
}
|
||||
|
||||
impl JobBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
caller_id: "".to_string(),
|
||||
context_id: "".to_string(),
|
||||
payload: "".to_string(),
|
||||
runner: "".to_string(),
|
||||
executor: "".to_string(),
|
||||
timeout: 300, // 5 minutes default
|
||||
env_vars: HashMap::new(),
|
||||
signatures: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the caller ID for this job
|
||||
pub fn caller_id(mut self, caller_id: &str) -> Self {
|
||||
self.caller_id = caller_id.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the context ID for this job
|
||||
pub fn context_id(mut self, context_id: &str) -> Self {
|
||||
self.context_id = context_id.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the payload (script content) for this job
|
||||
pub fn payload(mut self, payload: &str) -> Self {
|
||||
self.payload = payload.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the runner name for this job
|
||||
pub fn runner(mut self, runner: &str) -> Self {
|
||||
self.runner = runner.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the executor for this job
|
||||
pub fn executor(mut self, executor: &str) -> Self {
|
||||
self.executor = executor.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timeout for job execution (in seconds)
|
||||
pub fn timeout(mut self, timeout: u64) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a single environment variable
|
||||
pub fn env_var(mut self, key: &str, value: &str) -> Self {
|
||||
self.env_vars.insert(key.to_string(), value.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set multiple environment variables from a HashMap
|
||||
pub fn env_vars(mut self, env_vars: HashMap<String, String>) -> Self {
|
||||
self.env_vars = env_vars;
|
||||
self
|
||||
}
|
||||
|
||||
/// Clear all environment variables
|
||||
pub fn clear_env_vars(mut self) -> Self {
|
||||
self.env_vars.clear();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a signature (public key and signature)
|
||||
pub fn signature(mut self, public_key: &str, signature: &str) -> Self {
|
||||
self.signatures.push(JobSignature {
|
||||
public_key: public_key.to_string(),
|
||||
signature: signature.to_string(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Set multiple signatures
|
||||
pub fn signatures(mut self, signatures: Vec<JobSignature>) -> Self {
|
||||
self.signatures = signatures;
|
||||
self
|
||||
}
|
||||
|
||||
/// Clear all signatures
|
||||
pub fn clear_signatures(mut self) -> Self {
|
||||
self.signatures.clear();
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the job
|
||||
pub fn build(self) -> Result<Job, JobError> {
|
||||
if self.caller_id.is_empty() {
|
||||
return Err(JobError::InvalidData("caller_id is required".to_string()));
|
||||
}
|
||||
if self.context_id.is_empty() {
|
||||
return Err(JobError::InvalidData("context_id is required".to_string()));
|
||||
}
|
||||
if self.payload.is_empty() {
|
||||
return Err(JobError::InvalidData("payload is required".to_string()));
|
||||
}
|
||||
if self.runner.is_empty() {
|
||||
return Err(JobError::InvalidData("runner is required".to_string()));
|
||||
}
|
||||
if self.executor.is_empty() {
|
||||
return Err(JobError::InvalidData("executor is required".to_string()));
|
||||
}
|
||||
|
||||
let mut job = Job::new(
|
||||
self.caller_id,
|
||||
self.context_id,
|
||||
self.payload,
|
||||
self.runner,
|
||||
self.executor,
|
||||
);
|
||||
|
||||
job.timeout = self.timeout;
|
||||
job.env_vars = self.env_vars;
|
||||
job.signatures = self.signatures;
|
||||
|
||||
Ok(job)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for JobBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user