From 69b1d3d9014548086830520a37bcefc256403b39 Mon Sep 17 00:00:00 2001 From: plazmoid Date: Wed, 3 May 2023 16:34:15 +0300 Subject: [PATCH] add payload-overview component & use try_from rawjob instead of builder --- Cargo.lock | 8 +- bin/u_panel/src/argparse.rs | 14 +- bin/u_panel/src/gui/fe/src/app/app.module.ts | 4 +- .../job-info-dialog.component.html | 22 +- .../job-info-dialog.component.ts | 29 +- .../payload-info-dialog.component.html | 13 +- .../payload-info-dialog.component.ts | 5 +- .../payload-overview.component.html | 9 + .../payload-overview.component.less | 0 .../payload-overview.component.ts | 22 ++ .../agent-table/agent-table.component.ts | 4 +- .../tables/base-table/base-table.component.ts | 5 +- .../tables/job-table/job-table.component.ts | 76 +++-- .../payload-table/payload-table.component.ts | 6 +- .../result-table/result-table.component.ts | 4 +- .../src/gui/fe/src/app/models/index.ts | 4 - .../src/gui/fe/src/app/models/job.model.ts | 6 +- .../gui/fe/src/app/services/api.service.ts | 23 +- bin/u_server/src/db.rs | 20 +- bin/u_server/src/handlers.rs | 14 +- bin/u_server/src/u_server.rs | 8 +- integration-tests/tests/fixtures/agent.rs | 4 +- .../tests/integration_tests/behaviour.rs | 8 +- .../tests/integration_tests/endpoints.rs | 16 +- lib/u_lib/src/api.rs | 45 +-- lib/u_lib/src/cache.rs | 2 +- lib/u_lib/src/jobs.rs | 40 +-- lib/u_lib/src/messaging.rs | 2 +- lib/u_lib/src/models/jobs/assigned.rs | 6 +- lib/u_lib/src/models/jobs/meta.rs | 298 ++++++++---------- lib/u_lib/src/models/jobs/misc.rs | 3 +- lib/u_lib/src/models/payload.rs | 10 +- 32 files changed, 364 insertions(+), 366 deletions(-) create mode 100644 bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.html create mode 100644 bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.less create mode 100644 bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.ts diff --git a/Cargo.lock b/Cargo.lock index c65af22..06f20e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1700,9 +1700,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "platforms" @@ -2024,9 +2024,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.17" +version = "0.37.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc809f704c03a812ac71f22456c857be34185cac691a4316f27ab0f633bb9009" +checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433" dependencies = [ "bitflags", "errno 0.3.1", diff --git a/bin/u_panel/src/argparse.rs b/bin/u_panel/src/argparse.rs index e528aad..4a0b8cf 100644 --- a/bin/u_panel/src/argparse.rs +++ b/bin/u_panel/src/argparse.rs @@ -99,7 +99,7 @@ pub async fn process_cmd(client: HttpClient, args: Args) -> PanelResult { CRUD::Create { item: job } => { let raw_job = from_str::(&job) .map_err(|e| UError::DeserializeError(e.to_string(), job))?; - let mut job = raw_job.validated()?; + let mut job = raw_job.try_into_job()?; if let Some(payload) = &mut job.payload { payload.join_payload()?; @@ -112,13 +112,13 @@ pub async fn process_cmd(client: HttpClient, args: Args) -> PanelResult { None => into_value(client.get_jobs().await?), }, CRUD::RUD(RUD::Update { item }) => { - let raw_job = from_str::(&item) + let raw_job = from_str::(&item) .map_err(|e| UError::DeserializeError(e.to_string(), item))?; - let mut job = raw_job.validated()?; + let job = raw_job.validate()?; - if let Some(payload) = &mut job.payload { - payload.join_payload()?; - } + // if let Some(payload) = &mut job.payload { + // payload.join_payload()?; + // } into_value(client.update_job(&job).await?) } @@ -145,7 +145,7 @@ pub async fn process_cmd(client: HttpClient, args: Args) -> PanelResult { } PayloadCRUD::Read { id } => match id { None => into_value(client.get_payloads().await?), - Some(id) => into_value(client.get_payload(id, args.brief).await?), + Some(id) => into_value(vec![client.get_payload(id, args.brief).await?]), }, PayloadCRUD::Update { item } => { let payload = from_str::(&item) diff --git a/bin/u_panel/src/gui/fe/src/app/app.module.ts b/bin/u_panel/src/gui/fe/src/app/app.module.ts index 71d356f..43ce309 100644 --- a/bin/u_panel/src/gui/fe/src/app/app.module.ts +++ b/bin/u_panel/src/gui/fe/src/app/app.module.ts @@ -27,6 +27,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatListModule } from '@angular/material/list'; import { GlobalErrorComponent } from './components/global-error/global-error.component'; +import { PayloadOverviewComponent } from './components/payload-overview/payload-overview.component'; @NgModule({ declarations: [ @@ -40,7 +41,8 @@ import { GlobalErrorComponent } from './components/global-error/global-error.com AssignJobDialogComponent, PayloadComponent, PayloadInfoDialogComponent, - GlobalErrorComponent + GlobalErrorComponent, + PayloadOverviewComponent ], imports: [ BrowserModule, diff --git a/bin/u_panel/src/gui/fe/src/app/components/dialogs/job-info-dialog/job-info-dialog.component.html b/bin/u_panel/src/gui/fe/src/app/components/dialogs/job-info-dialog/job-info-dialog.component.html index dc79aa7..ffe8910 100644 --- a/bin/u_panel/src/gui/fe/src/app/components/dialogs/job-info-dialog/job-info-dialog.component.html +++ b/bin/u_panel/src/gui/fe/src/app/components/dialogs/job-info-dialog/job-info-dialog.component.html @@ -4,46 +4,40 @@
ID - + Alias - + Args - +
Type - + Platform - + Schedule - +
Payload - + {{ pld[1] }} - - Payload data - - -
+ diff --git a/bin/u_panel/src/gui/fe/src/app/components/dialogs/job-info-dialog/job-info-dialog.component.ts b/bin/u_panel/src/gui/fe/src/app/components/dialogs/job-info-dialog/job-info-dialog.component.ts index c1ab012..0f6eef0 100644 --- a/bin/u_panel/src/gui/fe/src/app/components/dialogs/job-info-dialog/job-info-dialog.component.ts +++ b/bin/u_panel/src/gui/fe/src/app/components/dialogs/job-info-dialog/job-info-dialog.component.ts @@ -1,7 +1,7 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { EventEmitter } from '@angular/core'; -import { Job } from '../../../models/job.model'; +import { Job, JobModel } from '../../../models/job.model'; import { ApiTableService } from 'src/app/services'; import { PayloadModel } from 'src/app/models'; @@ -11,36 +11,19 @@ import { PayloadModel } from 'src/app/models'; styleUrls: ['../base-info-dialog.component.less'] }) export class JobInfoDialogComponent { - isPreview = true; - isTooBigPayload = false; - decodedPayload = ""; //[id, name] - allPayloads: [string, string][] = []; + isPreview = true; + allPayloads: [string | null, string][] = [[null, "none"]]; - onSave = new EventEmitter(); + onSave = new EventEmitter(); constructor(@Inject(MAT_DIALOG_DATA) public data: Job, dataSource: ApiTableService) { - if (data.payload !== null) { - this.showPayload(data.payload) - } - dataSource.getPayloads().subscribe(resp => { - this.allPayloads = resp.map(r => [r.id, r.name]) + this.allPayloads = this.allPayloads.concat(resp.map(r => [r.id, r.name])) }) } - showPayload(payload: PayloadModel) { - if (payload.data !== null) { - this.decodedPayload = new TextDecoder().decode(new Uint8Array(payload.data)) - } else { - this.isTooBigPayload = true - } - } - updateJob() { - // if (this.decodedPayload.length > 0) { - // this.data.payload = Array.from(new TextEncoder().encode(this.decodedPayload)) - // } - // this.onSave.emit(this.data); + this.onSave.emit(this.data.meta); } } \ No newline at end of file diff --git a/bin/u_panel/src/gui/fe/src/app/components/dialogs/payload-info-dialog/payload-info-dialog.component.html b/bin/u_panel/src/gui/fe/src/app/components/dialogs/payload-info-dialog/payload-info-dialog.component.html index 5780429..fabd0b1 100644 --- a/bin/u_panel/src/gui/fe/src/app/components/dialogs/payload-info-dialog/payload-info-dialog.component.html +++ b/bin/u_panel/src/gui/fe/src/app/components/dialogs/payload-info-dialog/payload-info-dialog.component.html @@ -1,23 +1,26 @@ -

Result

+

Payload

ID - + Name - + MIME-type - + Size - +
+
+ +
diff --git a/bin/u_panel/src/gui/fe/src/app/components/dialogs/payload-info-dialog/payload-info-dialog.component.ts b/bin/u_panel/src/gui/fe/src/app/components/dialogs/payload-info-dialog/payload-info-dialog.component.ts index ba879a4..0cafab0 100644 --- a/bin/u_panel/src/gui/fe/src/app/components/dialogs/payload-info-dialog/payload-info-dialog.component.ts +++ b/bin/u_panel/src/gui/fe/src/app/components/dialogs/payload-info-dialog/payload-info-dialog.component.ts @@ -5,10 +5,9 @@ import { PayloadModel } from 'src/app/models/payload.model'; @Component({ selector: 'payload-info-dialog', templateUrl: 'payload-info-dialog.component.html', - styleUrls: [] + styleUrls: ['../base-info-dialog.component.less'] }) export class PayloadInfoDialogComponent { - constructor(@Inject(MAT_DIALOG_DATA) public data: PayloadModel) { } - + constructor(@Inject(MAT_DIALOG_DATA) public payload: PayloadModel) { } } \ No newline at end of file diff --git a/bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.html b/bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.html new file mode 100644 index 0000000..b73739b --- /dev/null +++ b/bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.html @@ -0,0 +1,9 @@ +
+ + Payload data + + + +
\ No newline at end of file diff --git a/bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.less b/bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.less new file mode 100644 index 0000000..e69de29 diff --git a/bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.ts b/bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.ts new file mode 100644 index 0000000..1a66c86 --- /dev/null +++ b/bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.ts @@ -0,0 +1,22 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { PayloadModel } from 'src/app/models'; + +@Component({ + selector: 'payload-overview', + templateUrl: './payload-overview.component.html', + styleUrls: ['./payload-overview.component.less'] +}) +export class PayloadOverviewComponent implements OnInit { + @Input() payload!: PayloadModel; + @Input("preview") isPreview = true; + isTooBigPayload = false; + decodedPayload = ""; + + ngOnInit() { + if (this.payload.data !== null) { + this.decodedPayload = new TextDecoder().decode(new Uint8Array(this.payload.data)) + } else { + this.isTooBigPayload = true + } + } +} diff --git a/bin/u_panel/src/gui/fe/src/app/components/tables/agent-table/agent-table.component.ts b/bin/u_panel/src/gui/fe/src/app/components/tables/agent-table/agent-table.component.ts index 2a98125..89e0b4b 100644 --- a/bin/u_panel/src/gui/fe/src/app/components/tables/agent-table/agent-table.component.ts +++ b/bin/u_panel/src/gui/fe/src/app/components/tables/agent-table/agent-table.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { TablesComponent } from '../base-table/base-table.component'; +import { TableComponent } from '../base-table/base-table.component'; import { AgentModel, Area } from '../../../models'; import { AssignJobDialogComponent, AgentInfoDialogComponent } from '../../dialogs'; @@ -8,7 +8,7 @@ import { AssignJobDialogComponent, AgentInfoDialogComponent } from '../../dialog templateUrl: './agent-table.component.html', styleUrls: ['../base-table/base-table.component.less'], }) -export class AgentComponent extends TablesComponent implements OnInit { +export class AgentComponent extends TableComponent implements OnInit { area = 'agents' as Area displayedColumns = ['id', 'alias', 'username', 'hostname', 'last_active', 'actions'] diff --git a/bin/u_panel/src/gui/fe/src/app/components/tables/base-table/base-table.component.ts b/bin/u_panel/src/gui/fe/src/app/components/tables/base-table/base-table.component.ts index aa2183a..5900a79 100644 --- a/bin/u_panel/src/gui/fe/src/app/components/tables/base-table/base-table.component.ts +++ b/bin/u_panel/src/gui/fe/src/app/components/tables/base-table/base-table.component.ts @@ -6,7 +6,7 @@ import { ApiModel, Area } from '../../../models'; import { ActivatedRoute, Router } from '@angular/router'; @Directive() -export abstract class TablesComponent implements OnInit { +export abstract class TableComponent implements OnInit { abstract area: Area; table_data: MatTableDataSource = new MatTableDataSource; isLoadingResults = true; @@ -49,7 +49,8 @@ export abstract class TablesComponent implements OnInit { deleteItem(id: string) { if (confirm(`Delete ${id}?`)) { - this.dataSource.delete(id, this.area) + this.dataSource.delete(id, this.area).subscribe(_ => { }) + this.loadTableData() } } diff --git a/bin/u_panel/src/gui/fe/src/app/components/tables/job-table/job-table.component.ts b/bin/u_panel/src/gui/fe/src/app/components/tables/job-table/job-table.component.ts index 4c77d3b..4419141 100644 --- a/bin/u_panel/src/gui/fe/src/app/components/tables/job-table/job-table.component.ts +++ b/bin/u_panel/src/gui/fe/src/app/components/tables/job-table/job-table.component.ts @@ -1,7 +1,8 @@ import { Component, OnInit } from '@angular/core'; -import { TablesComponent } from '../base-table/base-table.component'; -import { Area, JobModel } from '../../../models'; +import { TableComponent } from '../base-table/base-table.component'; +import { Area, JobModel, Job } from '../../../models'; import { JobInfoDialogComponent } from '../../dialogs'; +import { Observable } from 'rxjs'; @Component({ selector: 'job-table', @@ -9,42 +10,63 @@ import { JobInfoDialogComponent } from '../../dialogs'; styleUrls: ['../base-table/base-table.component.less'], providers: [{ provide: 'area', useValue: 'jobs' }] }) -export class JobComponent extends TablesComponent { +export class JobComponent extends TableComponent { area = 'jobs' as Area; displayedColumns = ['id', 'alias', 'platform', 'schedule', 'exec_type', 'actions'] showItemDialog(id: string | null) { - const show_dlg = (id: string, edit: boolean) => { - this.dataSource.getJob(id).subscribe(resp => { - var dialog = this.infoDialog.open(JobInfoDialogComponent, { - data: resp, - width: '1000px', - }); - if (edit) { - dialog.componentInstance.isPreview = false - } + const is_new_job = id === null; + + var dialogData$: Observable; + + if (is_new_job) { + dialogData$ = new Observable(subscriber => { + var defaultJob: Job = { + meta: { + alias: null, + argv: '', + exec_type: 'shell', + target_platforms: '*', + payload_id: null, + schedule: null + }, + payload: null + }; + subscriber.next(defaultJob) + }) + } else { + dialogData$ = this.dataSource.getJob(id) + } + + dialogData$.subscribe(dialogData => { + const dialog = this.infoDialog.open(JobInfoDialogComponent, { + data: dialogData, + width: '1000px', + }); + + dialog.componentInstance.isPreview = !is_new_job; - const saveSub = dialog.componentInstance.onSave.subscribe(result => { + const saveSub = dialog.componentInstance.onSave.subscribe(result => { + if (is_new_job) { + this.dataSource.create(dialogData.meta, this.area) + .subscribe(_ => { + alert("Created") + this.loadTableData() + }) + } else { this.dataSource.updateJob(result) .subscribe(_ => { - alert("Saved") + alert("Updated") this.loadTableData() }) - }) - - dialog.afterClosed().subscribe(result => { - saveSub.unsubscribe() - this.router.navigate(['.'], { relativeTo: this.route }) - }) + } + dialog.close() }) - } - if (id) { - show_dlg(id, false) - } else { - this.dataSource.create(null, this.area).subscribe(resp => { - show_dlg(resp[0], true) + dialog.afterClosed().subscribe(result => { + saveSub.unsubscribe() + this.router.navigate(['.'], { relativeTo: this.route }) }) - } + }) } } diff --git a/bin/u_panel/src/gui/fe/src/app/components/tables/payload-table/payload-table.component.ts b/bin/u_panel/src/gui/fe/src/app/components/tables/payload-table/payload-table.component.ts index 7cd3ae4..9e7b6da 100644 --- a/bin/u_panel/src/gui/fe/src/app/components/tables/payload-table/payload-table.component.ts +++ b/bin/u_panel/src/gui/fe/src/app/components/tables/payload-table/payload-table.component.ts @@ -2,7 +2,7 @@ import { Component } from '@angular/core'; import { Area } from 'src/app/models'; import { PayloadModel } from 'src/app/models/payload.model'; import { PayloadInfoDialogComponent } from '../../dialogs'; -import { TablesComponent } from '../base-table/base-table.component'; +import { TableComponent } from '../base-table/base-table.component'; @Component({ selector: 'payload-table', @@ -10,9 +10,9 @@ import { TablesComponent } from '../base-table/base-table.component'; styleUrls: ['../base-table/base-table.component.less'], providers: [{ provide: 'area', useValue: 'payloads' }] }) -export class PayloadComponent extends TablesComponent { +export class PayloadComponent extends TableComponent { area = 'payloads' as Area - displayedColumns = ["name", "mime_type", "size"]; + displayedColumns = ["name", "mime_type", "size", 'actions']; showItemDialog(id: string) { this.dataSource.getPayload(id).subscribe(resp => { diff --git a/bin/u_panel/src/gui/fe/src/app/components/tables/result-table/result-table.component.ts b/bin/u_panel/src/gui/fe/src/app/components/tables/result-table/result-table.component.ts index 5d1a4cf..511c62e 100644 --- a/bin/u_panel/src/gui/fe/src/app/components/tables/result-table/result-table.component.ts +++ b/bin/u_panel/src/gui/fe/src/app/components/tables/result-table/result-table.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { TablesComponent } from '../base-table/base-table.component'; +import { TableComponent } from '../base-table/base-table.component'; import { Area, ResultModel } from '../../../models'; import { ResultInfoDialogComponent } from '../../dialogs'; @@ -9,7 +9,7 @@ import { ResultInfoDialogComponent } from '../../dialogs'; styleUrls: ['../base-table/base-table.component.less'], providers: [{ provide: 'area', useValue: 'map' }] }) -export class ResultComponent extends TablesComponent { +export class ResultComponent extends TableComponent { area = 'map' as Area displayedColumns = [ 'id', diff --git a/bin/u_panel/src/gui/fe/src/app/models/index.ts b/bin/u_panel/src/gui/fe/src/app/models/index.ts index 3ff527f..dbe93ca 100644 --- a/bin/u_panel/src/gui/fe/src/app/models/index.ts +++ b/bin/u_panel/src/gui/fe/src/app/models/index.ts @@ -18,7 +18,3 @@ export type Area = "agents" | "jobs" | "map" | "payloads"; export type ApiModel = AgentModel | JobModel | ResultModel | PayloadModel | Empty; export interface Empty { } - -export function getAreaByModel(_: AgentModel): Area { - return "agents" -} \ No newline at end of file diff --git a/bin/u_panel/src/gui/fe/src/app/models/job.model.ts b/bin/u_panel/src/gui/fe/src/app/models/job.model.ts index 301ad5a..96d97b7 100644 --- a/bin/u_panel/src/gui/fe/src/app/models/job.model.ts +++ b/bin/u_panel/src/gui/fe/src/app/models/job.model.ts @@ -3,14 +3,14 @@ import { PayloadModel } from './' export interface JobModel { alias: string | null, argv: string, - id: string, + id?: string, exec_type: string, target_platforms: string, - payload: string | null, + payload_id: string | null, schedule: string | null, } export interface Job { - job: JobModel, + meta: JobModel, payload: PayloadModel | null, } \ No newline at end of file diff --git a/bin/u_panel/src/gui/fe/src/app/services/api.service.ts b/bin/u_panel/src/gui/fe/src/app/services/api.service.ts index 4f2dc8a..be9174c 100644 --- a/bin/u_panel/src/gui/fe/src/app/services/api.service.ts +++ b/bin/u_panel/src/gui/fe/src/app/services/api.service.ts @@ -1,7 +1,7 @@ import { environment } from 'src/environments/environment'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Observable, map, catchError, throwError } from 'rxjs'; -import { ApiModel, getAreaByModel, PayloadModel, Empty, Area, AgentModel, JobModel, ResultModel, Job } from '../models'; +import { ApiModel, PayloadModel, Empty, Area, AgentModel, JobModel, ResultModel, Job } from '../models'; import { Injectable, Inject } from '@angular/core'; import { ErrorService } from './error.service'; @@ -26,9 +26,7 @@ export class ApiTableService { requestUrl = `${environment.server}/cmd/`; req(cmd: string): Observable> { - return this.http.post>(this.requestUrl, cmd).pipe( - catchError(this.errorHandler) - ) + return this.http.post>(this.requestUrl, cmd) } getOne(id: string, area: Area, brief: 'yes' | 'no' | 'auto' | null = null): Observable { @@ -73,11 +71,11 @@ export class ApiTableService { return this.getMany('agents') } - getJobs(): Observable { + getJobs(): Observable { return this.getMany('jobs') } - getResults(): Observable { + getResults(): Observable { return this.getMany('map') } @@ -109,11 +107,12 @@ export class ApiTableService { return this.filterErrStatus(this.req(`${area} delete ${id}`)) } - create(item: string | null, area: Area): Observable { - if (!item) { - item = '"{}"' + create(item: T | null, area: Area): Observable { + var serialized = '"{}"' + if (item) { + serialized = JSON.stringify(item); } - return this.filterErrStatus(this.req(`${area} create ${item}`)) + return this.filterErrStatus(this.req(`${area} create '${serialized}'`)) } createResult(item: string): Observable { @@ -131,8 +130,8 @@ export class ApiTableService { catchError(this.errorHandler.bind(this))) } - errorHandler(err: HttpErrorResponse, _: R) { - this.errorService.handle(err.message); + errorHandler(err: HttpErrorResponse, caught: any) { + this.errorService.handle(caught.data ?? err.message); return throwError(() => new Error(err.message)); } } \ No newline at end of file diff --git a/bin/u_server/src/db.rs b/bin/u_server/src/db.rs index 9228529..81f3fd5 100644 --- a/bin/u_server/src/db.rs +++ b/bin/u_server/src/db.rs @@ -3,7 +3,7 @@ use diesel::{pg::PgConnection, prelude::*, result::Error as DslError, Connection use std::mem::drop; use u_lib::{ db::PgAsyncPool, - models::{schema, Agent, AssignedJob, Job, JobModel, JobState, Payload}, + models::{schema, Agent, AssignedJob, Job, JobMeta, JobState, Payload}, platform::Platform, types::Id, }; @@ -50,7 +50,7 @@ pub struct UDB<'c> { } impl UDB<'_> { - pub fn insert_jobs(&mut self, jobs: &[JobModel]) -> Result<()> { + pub fn insert_jobs(&mut self, jobs: &[JobMeta]) -> Result<()> { use schema::jobs; diesel::insert_into(jobs::table) @@ -67,7 +67,7 @@ impl UDB<'_> { .values(payloads) .execute(self.conn) .map(drop) - .map_err(with_err_ctx("Can't insert payloads")) + .map_err(with_err_ctx(format!("Can't insert payloads {payloads:?}"))) } pub fn get_job(&mut self, id: Id) -> Result> { @@ -76,14 +76,14 @@ impl UDB<'_> { let maybe_job_with_payload = jobs::table .left_join(payloads::table) .filter(jobs::id.eq(id)) - .first::<(JobModel, Option)>(self.conn) + .first::<(JobMeta, Option)>(self.conn) .optional() .map_err(with_err_ctx(format!("Can't get job {id}")))?; - Ok(maybe_job_with_payload.map(|(job, payload)| Job { job, payload })) + Ok(maybe_job_with_payload.map(|(job, payload)| Job { meta: job, payload })) } - pub fn get_jobs(&mut self) -> Result> { + pub fn get_jobs(&mut self) -> Result> { use schema::jobs; jobs::table @@ -125,12 +125,12 @@ impl UDB<'_> { let maybe_job_with_payload = jobs::table .left_join(payloads::table) .filter(jobs::alias.eq(alias)) - .first::<(JobModel, Option)>(self.conn) + .first::<(JobMeta, Option)>(self.conn) .optional() .map_err(with_err_ctx(format!("Can't get job by alias {alias}")))?; Ok(maybe_job_with_payload.map(|(job, payload_meta)| Job { - job, + meta: job, payload: payload_meta, })) } @@ -303,8 +303,8 @@ impl UDB<'_> { Ok(()) } - pub fn update_job(&mut self, job: &JobModel) -> Result<()> { - job.save_changes::(self.conn) + pub fn update_job(&mut self, job: &JobMeta) -> Result<()> { + job.save_changes::(self.conn) .map_err(with_err_ctx(format!("Can't update job {job:?}")))?; Ok(()) } diff --git a/bin/u_server/src/handlers.rs b/bin/u_server/src/handlers.rs index 2ce2e2e..4cb1b4a 100644 --- a/bin/u_server/src/handlers.rs +++ b/bin/u_server/src/handlers.rs @@ -134,7 +134,7 @@ impl Endpoints { .get_job_by_alias("agent_hello")? .expect("agent_hello job not found"); - db.set_jobs_for_agent(id, &[job.job.id])?; + db.set_jobs_for_agent(id, &[job.meta.id])?; } } @@ -165,7 +165,7 @@ impl Endpoints { .collect::, Error>>()?; let (jobs, payloads_opt): (Vec<_>, Vec<_>) = - jobs.into_iter().map(|j| (j.job, j.payload)).unzip(); + jobs.into_iter().map(|j| (j.meta, j.payload)).unzip(); let payloads = payloads_opt .into_iter() @@ -224,7 +224,7 @@ impl Endpoints { let job_from_db = db.get_job_by_alias(&ident); match job_from_db { Ok(job) => match job { - Some(j) => Ok(j.job.id), + Some(j) => Ok(j.meta.id), None => { Err(Error::ProcessingError(format!("unknown ident {ident}"))) } @@ -303,8 +303,8 @@ impl Endpoints { .map_err(From::from) } - pub async fn update_job(repo: Arc, job: Job) -> EndpResult { - repo.interact(move |mut db| db.update_job(&job.job)) + pub async fn update_job(repo: Arc, job: JobMeta) -> EndpResult { + repo.interact(move |mut db| db.update_job(&job.validate()?)) .await .map_err(From::from) } @@ -325,11 +325,9 @@ impl Endpoints { match payload.data { Some(data) => { let mut well_formed_payload = - Payload::from_data(data, Some(&payload.name)).map_err(Error::from)?; + Payload::from_data(data, Some(payload.name)).map_err(Error::from)?; well_formed_payload.id = payload.id; - debug!("wf payload: {well_formed_payload:?}"); - repo.interact(move |mut db| db.update_payload(&well_formed_payload)) .await .map_err(From::from) diff --git a/bin/u_server/src/u_server.rs b/bin/u_server/src/u_server.rs index 87dfe6a..42e9f36 100644 --- a/bin/u_server/src/u_server.rs +++ b/bin/u_server/src/u_server.rs @@ -115,7 +115,7 @@ pub fn init_endpoints( let update_job = path("update_job") .and(with_db.clone()) - .and(body::json::()) + .and(body::json::()) .and_then(Endpoints::update_job) .map(ok); @@ -183,12 +183,12 @@ pub async fn preload_jobs(repo: &PgRepo) -> Result<(), ServerError> { let job_alias = "agent_hello"; let if_job_exists = db.get_job_by_alias(job_alias)?; if if_job_exists.is_none() { - let agent_hello = RawJob::builder() + let agent_hello = RawJob::default() .with_type(JobType::Init) .with_alias(job_alias) - .build() + .try_into_job() .unwrap(); - db.insert_jobs(&[agent_hello.job])?; + db.insert_jobs(&[agent_hello.meta])?; } Ok(()) }) diff --git a/integration-tests/tests/fixtures/agent.rs b/integration-tests/tests/fixtures/agent.rs index e0dc8e2..b441d7a 100644 --- a/integration-tests/tests/fixtures/agent.rs +++ b/integration-tests/tests/fixtures/agent.rs @@ -23,9 +23,9 @@ pub fn registered_agent(client: &HttpClient) -> RegisteredAgent { let job_id = resp.job_id; let job = client.get_job(job_id, BriefMode::No).await.unwrap(); - assert_eq!(job.job.alias, Some("agent_hello".to_string())); + assert_eq!(job.meta.alias, Some("agent_hello".to_string())); - let mut agent_data = AssignedJob::from((&job.job, resp)); + let mut agent_data = AssignedJob::from((&job.meta, resp)); agent_data.set_result(&agent); client diff --git a/integration-tests/tests/integration_tests/behaviour.rs b/integration-tests/tests/integration_tests/behaviour.rs index f3d94ed..51726a1 100644 --- a/integration-tests/tests/integration_tests/behaviour.rs +++ b/integration-tests/tests/integration_tests/behaviour.rs @@ -21,12 +21,12 @@ async fn setup_tasks() { let agents: Vec = Panel::check_output("agents read"); let agent_id = agents[0].id; let job_alias = "passwd_contents"; - let job = RawJob::builder() + let job = RawJob::default() .with_alias(job_alias) .with_raw_payload("cat /etc/passwd") .with_shell("/bin/bash {}") .with_target_platforms("*linux*") - .build() + .try_into_job() .unwrap(); Panel::check_status(["jobs", "create", &to_string(&RawJob::from(job)).unwrap()]); @@ -54,12 +54,12 @@ async fn large_payload() { let agent = &Panel::check_output::>("agents read")[0]; let agent_id = agent.id; let job_alias = "large_payload"; - let job = RawJob::builder() + let job = RawJob::default() .with_alias(job_alias) .with_payload_path("./tests/bin/echoer") .with_shell("{} type echo") .with_target_platforms(&agent.platform) - .build() + .try_into_job() .unwrap(); Panel::check_status(["jobs", "create", &to_string(&RawJob::from(job)).unwrap()]); diff --git a/integration-tests/tests/integration_tests/endpoints.rs b/integration-tests/tests/integration_tests/endpoints.rs index 96bf960..40cddd1 100644 --- a/integration-tests/tests/integration_tests/endpoints.rs +++ b/integration-tests/tests/integration_tests/endpoints.rs @@ -21,30 +21,30 @@ use u_lib::models::{BriefMode, RawJob, RawPayload, MAX_READABLE_PAYLOAD_SIZE}; #[tokio::test] async fn jobs_upload_update_get_del(client_panel: &HttpClient) { let job_alias = "henlo"; - let mut job = RawJob::builder() + let mut job = RawJob::default() .with_shell("/bin/bash {}") .with_raw_payload("echo henlo") .with_alias(job_alias) - .build() + .try_into_job() .unwrap(); - let job_id = job.job.id; + let job_id = job.meta.id; client_panel.upload_jobs([&job]).await.unwrap(); let fetched_job = client_panel.get_full_job(job_id).await.unwrap(); assert_eq!(job, fetched_job); - // update job's payload by edit existing does nothing, - // editing is only allowed from payload itself - *job.payload.as_mut().unwrap().data.as_mut().unwrap() = b"echo henlo2".to_vec(); - client_panel.update_job(&job).await.unwrap(); + let new_alias = "henlo2".to_string(); + job.meta.alias = Some(new_alias.clone()); + client_panel.update_job(&job.meta).await.unwrap(); let fetched_job = client_panel.get_full_job(job_id).await.unwrap(); assert_eq!( fetched_job.payload.as_ref().unwrap().data.as_ref().unwrap(), b"echo henlo" ); + assert_eq!(fetched_job.meta.alias, Some(new_alias)); client_panel.del(job_id).await.unwrap(); @@ -58,7 +58,7 @@ async fn payloads_upload_update_get_del(client_panel: &HttpClient) { let name = "test1".to_string(); let data = b"qweasdzxc".to_vec(); let payload = RawPayload { - name: name.clone(), + name: Some(name.clone()), data: data.clone(), }; diff --git a/lib/u_lib/src/api.rs b/lib/u_lib/src/api.rs index deca48a..35a7048 100644 --- a/lib/u_lib/src/api.rs +++ b/lib/u_lib/src/api.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use std::net::SocketAddr; use std::{collections::HashMap, time::Duration}; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use reqwest::{header, header::HeaderMap, Certificate, Client, Identity, Method, Url}; use serde::de::DeserializeOwned; use serde_json::{from_str, Value}; @@ -26,7 +26,7 @@ pub mod retypes { pub type Report = (); pub type GetJob = Job; pub type GetBriefJob = Job; - pub type GetJobs = Vec; + pub type GetJobs = Vec; pub type GetAgents = Vec; pub type UpdateAgent = (); pub type UpdateJob = (); @@ -58,18 +58,17 @@ impl HttpClient { default_headers.insert(header::AUTHORIZATION, format!("Bearer {pwd}")); } - // todo: don't rely only on dns resolve - let client = { - let client = Client::builder() - .identity(identity) - .default_headers(HeaderMap::try_from(&default_headers).unwrap()) - .add_root_certificate(Certificate::from_pem(ROOT_CA_CERT).unwrap()) - .timeout(Duration::from_secs(20)); + let client = Client::builder() + .identity(identity) + .default_headers(HeaderMap::try_from(&default_headers).unwrap()) + .add_root_certificate(Certificate::from_pem(ROOT_CA_CERT).unwrap()) + .timeout(Duration::from_secs(20)); + async fn resolve(domain_name: &str) -> Result { let dns_response = Client::new() .request( Method::GET, - format!("https://1.1.1.1/dns-query?name={server}&type=A"), + format!("https://1.1.1.1/dns-query?name={domain_name}&type=A"), ) .header(header::ACCEPT, "application/dns-json") .send() @@ -77,16 +76,22 @@ impl HttpClient { .text() .await?; - match from_str::(&dns_response).unwrap()["Answer"] - .get(0) + let ip = from_str::(&dns_response)? + .get("Answer") + .and_then(|a| a.get(0)) .and_then(|a| a.get("data")) - { - Some(ip) => { - let raw_addr = format!("{}:{MASTER_PORT}", ip.as_str().unwrap()); - let addr: SocketAddr = raw_addr.parse().unwrap(); - client.resolve(server, addr) - } - None => client, + .map(|ip| ip.to_owned()) + .ok_or_else(|| anyhow!("can't extract dns answer"))?; + + let raw_addr = format!("{}:{MASTER_PORT}", ip.as_str().unwrap()); + Ok(raw_addr.parse().unwrap()) + } + + let client = match resolve(server).await { + Ok(addr) => client.resolve(server, addr), + Err(e) => { + warn!("DNS error: {e}"); + client } } .build() @@ -192,7 +197,7 @@ impl HttpClient { } /// update job - pub async fn update_job(&self, job: &Job) -> Result { + pub async fn update_job(&self, job: &JobMeta) -> Result { self.req_with_payload("update_job", job).await } diff --git a/lib/u_lib/src/cache.rs b/lib/u_lib/src/cache.rs index c81414c..3031b0f 100644 --- a/lib/u_lib/src/cache.rs +++ b/lib/u_lib/src/cache.rs @@ -15,7 +15,7 @@ pub struct JobCache; impl JobCache { pub fn insert(job: Val) { - JOB_CACHE.write().insert(job.job.id, job); + JOB_CACHE.write().insert(job.meta.id, job); } pub fn contains(id: Id) -> bool { diff --git a/lib/u_lib/src/jobs.rs b/lib/u_lib/src/jobs.rs index 4c433cc..899aa67 100644 --- a/lib/u_lib/src/jobs.rs +++ b/lib/u_lib/src/jobs.rs @@ -29,7 +29,7 @@ impl AnonymousJobBatch { let jobs_ids: Vec<_> = jobs .into_iter() .map(|job| { - let job_id = job.job.id; + let job_id = job.meta.id; ( job, AssignedJobById { @@ -79,7 +79,11 @@ impl NamedJobBatch { let jobs: Vec<_> = named_jobs .into_iter() .filter_map(|(alias, cmd)| { - match RawJob::builder().with_shell(cmd).with_alias(alias).build() { + match RawJob::default() + .with_shell(cmd) + .with_alias(alias) + .try_into_job() + { Ok(jpm) => Some(jpm), Err(e) => { result.push_err(e); @@ -95,7 +99,7 @@ impl NamedJobBatch { pub fn from_meta(named_jobs: Vec) -> Self { let (job_names, jobs): (Vec<_>, Vec<_>) = named_jobs .into_iter() - .map(|job| (job.job.alias.clone().unwrap(), job)) + .map(|job| (job.meta.alias.clone().unwrap(), job)) .unzip(); Self { runner: Some(AnonymousJobBatch::from_meta(jobs)), @@ -129,17 +133,17 @@ impl NamedJobBatch { } pub async fn run_assigned_job(job: Job, ids: AssignedJobById) -> ExecResult { - let Job { job, payload } = job; - let mut result = AssignedJob::from((&job, ids)); - match job.exec_type { + let Job { meta, payload } = job; + let mut result = AssignedJob::from((&meta, ids)); + match meta.exec_type { JobType::Shell => { let (argv, _prepared_payload) = { if let Some(payload) = payload { let (prep_exec, prep_exec_path) = payload.prepare_executable()?; - let argv_with_exec = job.argv.replace("{}", &prep_exec_path); + let argv_with_exec = meta.argv.replace("{}", &prep_exec_path); (argv_with_exec, Some(prep_exec)) } else { - (job.argv, None) + (meta.argv, None) } }; @@ -227,11 +231,11 @@ mod tests { #[case] payload: Option<&[u8]>, #[case] expected_result: &str, ) -> TestResult { - let mut job = RawJob::builder().with_shell(cmd); + let mut raw_job = RawJob::default().with_shell(cmd); if let Some(p) = payload { - job = job.with_raw_payload(p); + raw_job = raw_job.with_raw_payload(p); } - let job = job.build().unwrap(); + let job = raw_job.try_into_job().unwrap(); let result = AnonymousJobBatch::from_meta([job]) .wait_one() .await @@ -310,11 +314,11 @@ mod tests { #[case] payload: Option<&[u8]>, #[case] err_str: &str, ) -> TestResult { - let mut job = RawJob::builder().with_shell(cmd); + let mut raw_job = RawJob::default().with_shell(cmd); if let Some(p) = payload { - job = job.with_raw_payload(p); + raw_job = raw_job.with_raw_payload(p); } - let err = job.build().unwrap_err(); + let err = raw_job.try_into_job().unwrap_err(); let err_msg = unwrap_enum!(err, UError::JobBuildError); assert!(err_msg.contains(err_str)); Ok(()) @@ -323,15 +327,15 @@ mod tests { #[tokio::test] async fn test_different_job_types() -> TestResult { let mut jobs = NamedJobBatch::from_meta(vec![ - RawJob::builder() + RawJob::default() .with_shell("sleep 3") .with_alias("sleeper") - .build() + .try_into_job() .unwrap(), - RawJob::builder() + RawJob::default() .with_type(JobType::Init) .with_alias("gatherer") - .build() + .try_into_job() .unwrap(), ]) .wait() diff --git a/lib/u_lib/src/messaging.rs b/lib/u_lib/src/messaging.rs index b7a0318..b1e1ecf 100644 --- a/lib/u_lib/src/messaging.rs +++ b/lib/u_lib/src/messaging.rs @@ -10,7 +10,7 @@ pub trait AsMsg: Clone + Serialize + Debug {} impl AsMsg for Agent {} impl AsMsg for AssignedJob {} impl AsMsg for AssignedJobById {} -impl AsMsg for JobModel {} +impl AsMsg for JobMeta {} impl AsMsg for Reportable {} impl AsMsg for Payload {} impl AsMsg for RawPayload {} diff --git a/lib/u_lib/src/models/jobs/assigned.rs b/lib/u_lib/src/models/jobs/assigned.rs index 946fc4b..fef995f 100644 --- a/lib/u_lib/src/models/jobs/assigned.rs +++ b/lib/u_lib/src/models/jobs/assigned.rs @@ -1,4 +1,4 @@ -use super::{JobModel, JobState, JobType}; +use super::{JobMeta, JobState, JobType}; #[cfg(feature = "server")] use crate::models::schema::*; use crate::{ @@ -60,8 +60,8 @@ pub struct AssignedJobById { pub job_id: Id, } -impl From<(&JobModel, AssignedJobById)> for AssignedJob { - fn from((job, ids): (&JobModel, AssignedJobById)) -> Self { +impl From<(&JobMeta, AssignedJobById)> for AssignedJob { + fn from((job, ids): (&JobMeta, AssignedJobById)) -> Self { let AssignedJobById { agent_id, id, diff --git a/lib/u_lib/src/models/jobs/meta.rs b/lib/u_lib/src/models/jobs/meta.rs index c419cf5..3611ad7 100644 --- a/lib/u_lib/src/models/jobs/meta.rs +++ b/lib/u_lib/src/models/jobs/meta.rs @@ -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 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, - /// cron-like string - pub schedule: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct Job { - pub job: JobModel, - pub payload: Option, -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct RawJob<'p> { +pub struct JobMeta { #[serde(default)] pub alias: Option, @@ -59,17 +39,14 @@ pub struct RawJob<'p> { pub target_platforms: String, #[serde(default)] - pub payload_path: Option, - - #[serde(default)] - pub raw_payload: Option>, + pub payload_id: Option, /// cron-like string #[serde(default)] pub schedule: Option, } -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 { + fn mk_err(msg: impl Into) -> 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, +} + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct RawJob<'p> { + #[serde(default)] + payload_path: Option, + + #[serde(default)] + raw_payload: Option>, + + #[serde(default, flatten)] + meta: JobMeta, +} + impl From 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 { - JobBuilder { inner: self }.build() + pub fn try_into_job(self) -> UResult { + Job::try_from(self) } pub fn from_shell(cmd: impl Into) -> UResult { - 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) -> 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) -> Self { + self.meta.alias = Some(alias.into()); self } - pub fn with_payload_path(mut self, path: impl Into) -> 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) -> Self { - self.inner.alias = Some(alias.into()); + pub fn with_target_platforms(mut self, platform: impl Into) -> 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) -> Self { - self.inner.target_platforms = platform.into(); + pub fn with_payload_path(mut self, path: impl Into) -> Self { + self.payload_path = Some(path.into()); + self.raw_payload = None; self } +} - pub fn build(self) -> UResult { - let mut inner = self.inner; +impl TryFrom> for Job { + type Error = UError; - fn _build(job: RawJob) -> UResult { - 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 { + 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, + }) } } diff --git a/lib/u_lib/src/models/jobs/misc.rs b/lib/u_lib/src/models/jobs/misc.rs index 94de0b1..faf108e 100644 --- a/lib/u_lib/src/models/jobs/misc.rs +++ b/lib/u_lib/src/models/jobs/misc.rs @@ -26,7 +26,8 @@ pub enum JobState { Finished, } -#[derive(Default, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Display)] +#[derive(Default, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] #[cfg_attr( feature = "server", derive(DbEnum), diff --git a/lib/u_lib/src/models/payload.rs b/lib/u_lib/src/models/payload.rs index 211a5aa..9b982bf 100644 --- a/lib/u_lib/src/models/payload.rs +++ b/lib/u_lib/src/models/payload.rs @@ -12,13 +12,13 @@ use diesel::Identifiable; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RawPayload { - pub name: String, + pub name: Option, pub data: Vec, } impl RawPayload { pub fn into_payload(self) -> Result { - Payload::from_data(self.data, Some(&self.name)) + Payload::from_data(self.data, self.name) } } @@ -71,11 +71,11 @@ impl Payload { Ok(()) } - pub fn from_data(data: impl AsRef<[u8]>, name: Option<&str>) -> Result { + pub fn from_data(data: impl AsRef<[u8]>, name: Option) -> Result { let name = match name { Some(name) => { - ufs::put(name, data).context("fr_put")?; - name.to_string() + ufs::put(&name, data).context("fr_put")?; + name } None => ufs::create_anonymous(data).context("fr_anon")?, };