|
|
|
@ -12,34 +12,14 @@ use diesel::{Identifiable, Insertable, Queryable}; |
|
|
|
|
use serde::{Deserialize, Serialize}; |
|
|
|
|
use std::borrow::Cow; |
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] |
|
|
|
|
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] |
|
|
|
|
#[cfg_attr(
|
|
|
|
|
feature = "server", |
|
|
|
|
derive(Queryable, Identifiable, Insertable, AsChangeset), |
|
|
|
|
diesel(table_name = jobs) |
|
|
|
|
diesel(table_name = jobs), |
|
|
|
|
diesel(treat_none_as_null = true) |
|
|
|
|
)] |
|
|
|
|
pub struct JobModel { |
|
|
|
|
pub alias: Option<String>, |
|
|
|
|
/// string like `bash -c {} -a 1 --arg2`,
|
|
|
|
|
/// where {} is replaced by executable's tmp path
|
|
|
|
|
pub argv: String, |
|
|
|
|
pub id: Id, |
|
|
|
|
pub exec_type: JobType, |
|
|
|
|
/// target triple
|
|
|
|
|
pub target_platforms: String, |
|
|
|
|
pub payload_id: Option<Id>, |
|
|
|
|
/// cron-like string
|
|
|
|
|
pub schedule: Option<String>, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] |
|
|
|
|
pub struct Job { |
|
|
|
|
pub job: JobModel, |
|
|
|
|
pub payload: Option<Payload>, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Clone)] |
|
|
|
|
pub struct RawJob<'p> { |
|
|
|
|
pub struct JobMeta { |
|
|
|
|
#[serde(default)] |
|
|
|
|
pub alias: Option<String>, |
|
|
|
|
|
|
|
|
@ -59,17 +39,14 @@ pub struct RawJob<'p> { |
|
|
|
|
pub target_platforms: String, |
|
|
|
|
|
|
|
|
|
#[serde(default)] |
|
|
|
|
pub payload_path: Option<String>, |
|
|
|
|
|
|
|
|
|
#[serde(default)] |
|
|
|
|
pub raw_payload: Option<Cow<'p, [u8]>>, |
|
|
|
|
pub payload_id: Option<Id>, |
|
|
|
|
|
|
|
|
|
/// cron-like string
|
|
|
|
|
#[serde(default)] |
|
|
|
|
pub schedule: Option<String>, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl Default for RawJob<'_> { |
|
|
|
|
impl Default for JobMeta { |
|
|
|
|
fn default() -> Self { |
|
|
|
|
Self { |
|
|
|
|
alias: None, |
|
|
|
@ -77,198 +54,181 @@ impl Default for RawJob<'_> { |
|
|
|
|
id: Id::new_v4(), |
|
|
|
|
exec_type: JobType::default(), |
|
|
|
|
target_platforms: String::new(), |
|
|
|
|
payload_path: None, |
|
|
|
|
raw_payload: None, |
|
|
|
|
payload_id: None, |
|
|
|
|
schedule: None, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl fmt::Debug for RawJob<'_> { |
|
|
|
|
impl fmt::Debug for JobMeta { |
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
|
|
|
|
f.debug_struct("RawJob") |
|
|
|
|
f.debug_struct("JobMeta") |
|
|
|
|
.field("alias", &self.alias) |
|
|
|
|
.field("argv", &self.argv) |
|
|
|
|
.field("id", &self.id.to_string()) |
|
|
|
|
.field("exec_type", &self.exec_type) |
|
|
|
|
.field("platform", &self.target_platforms) |
|
|
|
|
.field("payload_path", &self.payload_path) |
|
|
|
|
.field("raw_payload", &self.raw_payload) |
|
|
|
|
.field("target_platforms", &self.target_platforms) |
|
|
|
|
.field("payload_id", &self.payload_id.map(|id| id.to_string())) |
|
|
|
|
.field("schedule", &self.schedule) |
|
|
|
|
.finish() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl JobMeta { |
|
|
|
|
pub fn validate(mut self) -> UResult<Self> { |
|
|
|
|
fn mk_err(msg: impl Into<String>) -> UError { |
|
|
|
|
UError::JobBuildError(msg.into()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const ARGV_STR_LEN: usize = 2048; |
|
|
|
|
|
|
|
|
|
if self.argv.is_empty() { |
|
|
|
|
// TODO: fix detecting
|
|
|
|
|
self.argv = String::from("echo 'hello, world!'") |
|
|
|
|
} else if self.argv.len() > ARGV_STR_LEN { |
|
|
|
|
return Err(mk_err(format!( |
|
|
|
|
"argv length limit ({ARGV_STR_LEN}) exceeded" |
|
|
|
|
))); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let argv_parts = shlex::split(&self.argv).ok_or_else(|| mk_err("Shlex failed"))?; |
|
|
|
|
let empty_err = mk_err("Empty argv"); |
|
|
|
|
|
|
|
|
|
if argv_parts.get(0).ok_or(empty_err.clone())?.is_empty() { |
|
|
|
|
return Err(empty_err); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if self.payload_id.is_some() && !self.argv.contains("{}") { |
|
|
|
|
return Err(mk_err("Argv contains no executable placeholder")); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if self.argv.contains("{}") && self.payload_id.is_none() { |
|
|
|
|
return Err(mk_err( |
|
|
|
|
"No payload provided, but argv contains executable placeholder", |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if self.target_platforms.is_empty() { |
|
|
|
|
self.target_platforms = "*".to_string(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if !platform::is_valid_glob(&self.target_platforms) { |
|
|
|
|
return Err(mk_err(format!( |
|
|
|
|
"Unknown platform '{}'", |
|
|
|
|
self.target_platforms |
|
|
|
|
))); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Ok(self) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] |
|
|
|
|
pub struct Job { |
|
|
|
|
pub meta: JobMeta, |
|
|
|
|
pub payload: Option<Payload>, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Default)] |
|
|
|
|
pub struct RawJob<'p> { |
|
|
|
|
#[serde(default)] |
|
|
|
|
payload_path: Option<String>, |
|
|
|
|
|
|
|
|
|
#[serde(default)] |
|
|
|
|
raw_payload: Option<Cow<'p, [u8]>>, |
|
|
|
|
|
|
|
|
|
#[serde(default, flatten)] |
|
|
|
|
meta: JobMeta, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl From<Job> for RawJob<'_> { |
|
|
|
|
fn from(job: Job) -> Self { |
|
|
|
|
let Job { |
|
|
|
|
job, |
|
|
|
|
payload: payload_meta, |
|
|
|
|
} = job; |
|
|
|
|
let Job { meta, payload } = job; |
|
|
|
|
|
|
|
|
|
RawJob { |
|
|
|
|
alias: job.alias, |
|
|
|
|
argv: job.argv, |
|
|
|
|
id: job.id, |
|
|
|
|
exec_type: job.exec_type, |
|
|
|
|
target_platforms: job.target_platforms, |
|
|
|
|
payload_path: payload_meta.map(|m| m.name), |
|
|
|
|
payload_path: payload.map(|m| m.name), |
|
|
|
|
raw_payload: None, |
|
|
|
|
schedule: job.schedule, |
|
|
|
|
meta, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl<'p> RawJob<'p> { |
|
|
|
|
pub fn validated(self) -> UResult<Job> { |
|
|
|
|
JobBuilder { inner: self }.build() |
|
|
|
|
pub fn try_into_job(self) -> UResult<Job> { |
|
|
|
|
Job::try_from(self) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
pub fn from_shell(cmd: impl Into<String>) -> UResult<Job> { |
|
|
|
|
Self::builder().with_shell(cmd).build() |
|
|
|
|
Self::default().with_shell(cmd).try_into_job() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
pub fn builder() -> JobBuilder<'p> { |
|
|
|
|
JobBuilder::default() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[derive(Default)] |
|
|
|
|
pub struct JobBuilder<'p> { |
|
|
|
|
inner: RawJob<'p>, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl<'p> JobBuilder<'p> { |
|
|
|
|
pub fn with_shell(mut self, shell_cmd: impl Into<String>) -> Self { |
|
|
|
|
self.inner.argv = shell_cmd.into(); |
|
|
|
|
self.inner.exec_type = JobType::Shell; |
|
|
|
|
self.meta.argv = shell_cmd.into(); |
|
|
|
|
self.meta.exec_type = JobType::Shell; |
|
|
|
|
self |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
pub fn with_raw_payload(mut self, raw_payload: impl AsPayload<'p>) -> Self { |
|
|
|
|
self.inner.raw_payload = Some(raw_payload.as_payload()); |
|
|
|
|
self.inner.payload_path = None; |
|
|
|
|
pub fn with_alias(mut self, alias: impl Into<String>) -> Self { |
|
|
|
|
self.meta.alias = Some(alias.into()); |
|
|
|
|
self |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
pub fn with_payload_path(mut self, path: impl Into<String>) -> Self { |
|
|
|
|
self.inner.payload_path = Some(path.into()); |
|
|
|
|
self.inner.raw_payload = None; |
|
|
|
|
pub fn with_type(mut self, e_type: JobType) -> Self { |
|
|
|
|
self.meta.exec_type = e_type; |
|
|
|
|
self |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
pub fn with_alias(mut self, alias: impl Into<String>) -> Self { |
|
|
|
|
self.inner.alias = Some(alias.into()); |
|
|
|
|
pub fn with_target_platforms(mut self, platform: impl Into<String>) -> Self { |
|
|
|
|
self.meta.target_platforms = platform.into(); |
|
|
|
|
self |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
pub fn with_type(mut self, e_type: JobType) -> Self { |
|
|
|
|
self.inner.exec_type = e_type; |
|
|
|
|
pub fn with_raw_payload(mut self, raw_payload: impl AsPayload<'p>) -> Self { |
|
|
|
|
self.raw_payload = Some(raw_payload.as_payload()); |
|
|
|
|
self.payload_path = None; |
|
|
|
|
self |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
pub fn with_target_platforms(mut self, platform: impl Into<String>) -> Self { |
|
|
|
|
self.inner.target_platforms = platform.into(); |
|
|
|
|
pub fn with_payload_path(mut self, path: impl Into<String>) -> Self { |
|
|
|
|
self.payload_path = Some(path.into()); |
|
|
|
|
self.raw_payload = None; |
|
|
|
|
self |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
pub fn build(self) -> UResult<Job> { |
|
|
|
|
let mut inner = self.inner; |
|
|
|
|
impl TryFrom<RawJob<'_>> for Job { |
|
|
|
|
type Error = UError; |
|
|
|
|
|
|
|
|
|
fn _build(job: RawJob) -> UResult<Job> { |
|
|
|
|
let payload = { |
|
|
|
|
let payload_from_path = job |
|
|
|
|
.payload_path |
|
|
|
|
.as_ref() |
|
|
|
|
.map(|path| Payload::from_path(path)) |
|
|
|
|
.transpose()?; |
|
|
|
|
|
|
|
|
|
if payload_from_path.is_none() { |
|
|
|
|
job.raw_payload |
|
|
|
|
.as_ref() |
|
|
|
|
.map(|data| Payload::from_data(data, None)) |
|
|
|
|
.transpose()? |
|
|
|
|
} else { |
|
|
|
|
payload_from_path |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
Ok(Job { |
|
|
|
|
job: JobModel { |
|
|
|
|
alias: job.alias, |
|
|
|
|
argv: job.argv, |
|
|
|
|
id: job.id, |
|
|
|
|
exec_type: job.exec_type, |
|
|
|
|
target_platforms: job.target_platforms, |
|
|
|
|
payload_id: payload.as_ref().map(|p| p.id), |
|
|
|
|
schedule: job.schedule, |
|
|
|
|
}, |
|
|
|
|
payload, |
|
|
|
|
}) |
|
|
|
|
fn try_from(mut raw: RawJob) -> Result<Self, Self::Error> { |
|
|
|
|
if raw.raw_payload.is_some() && raw.payload_path.is_some() { |
|
|
|
|
return Err(UError::JobBuildError( |
|
|
|
|
"Can't use both raw payload with payload path".to_string(), |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
match inner.exec_type { |
|
|
|
|
JobType::Shell => { |
|
|
|
|
const ARGV_STR_LEN: usize = 2048; |
|
|
|
|
|
|
|
|
|
if inner.argv.is_empty() { |
|
|
|
|
// TODO: fix detecting
|
|
|
|
|
inner.argv = String::from("echo 'hello, world!'") |
|
|
|
|
} else if inner.argv.len() > ARGV_STR_LEN { |
|
|
|
|
return Err(UError::JobBuildError(format!( |
|
|
|
|
"argv length limit ({ARGV_STR_LEN}) exceeded" |
|
|
|
|
))); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let argv_parts = shlex::split(&inner.argv) |
|
|
|
|
.ok_or(UError::JobBuildError("Shlex failed".into()))?; |
|
|
|
|
let empty_err = UError::JobBuildError("Empty argv".into()); |
|
|
|
|
|
|
|
|
|
if argv_parts.get(0).ok_or(empty_err.clone())?.is_empty() { |
|
|
|
|
return Err(empty_err.into()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if inner.raw_payload.is_some() && inner.payload_path.is_some() { |
|
|
|
|
return Err(UError::JobBuildError( |
|
|
|
|
"Can't use both raw payload with payload path".to_string(), |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
match inner.payload_path.as_ref() { |
|
|
|
|
Some(_) | None if inner.raw_payload.is_some() => { |
|
|
|
|
if !inner.argv.contains("{}") { |
|
|
|
|
return Err(UError::JobBuildError( |
|
|
|
|
"Argv contains no executable placeholder".into(), |
|
|
|
|
) |
|
|
|
|
.into()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
None => { |
|
|
|
|
if inner.argv.contains("{}") && inner.raw_payload.is_none() { |
|
|
|
|
return Err(UError::JobBuildError( |
|
|
|
|
"No payload provided, but argv contains executable placeholder" |
|
|
|
|
.into(), |
|
|
|
|
) |
|
|
|
|
.into()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
_ => (), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
if inner.target_platforms.is_empty() { |
|
|
|
|
inner.target_platforms = "*".to_string(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if !platform::is_valid_glob(&inner.target_platforms) { |
|
|
|
|
return Err(UError::JobBuildError(format!( |
|
|
|
|
"Unknown platform '{}'", |
|
|
|
|
inner.target_platforms |
|
|
|
|
))); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
_build(inner) |
|
|
|
|
let payload = { |
|
|
|
|
let payload_from_path = raw |
|
|
|
|
.payload_path |
|
|
|
|
.as_ref() |
|
|
|
|
.map(|path| Payload::from_path(path)) |
|
|
|
|
.transpose()?; |
|
|
|
|
|
|
|
|
|
if payload_from_path.is_none() { |
|
|
|
|
raw.raw_payload |
|
|
|
|
.as_ref() |
|
|
|
|
.map(|data| Payload::from_data(data, None)) |
|
|
|
|
.transpose()? |
|
|
|
|
} else { |
|
|
|
|
payload_from_path |
|
|
|
|
} |
|
|
|
|
_ => _build(inner), |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
raw.meta.payload_id = payload.as_ref().map(|p| p.id); |
|
|
|
|
|
|
|
|
|
Ok(Job { |
|
|
|
|
meta: raw.meta.validate()?, |
|
|
|
|
payload, |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|