1use reqwest::Method;
2
3use crate::api::job::models::{
4 JobCreateRequest, JobCreateResponse, JobDispatchRequest, JobDispatchResponse,
5 JobListAllocationsParams, JobStopParams, JobStopResponse, JobsParseRequest,
6};
7use crate::models::allocation::Allocation;
8use crate::models::job::Job;
9use crate::{ClientError, NomadClient};
10
11impl NomadClient {
12 pub async fn job_dispatch(
13 &self,
14 job_name: &str,
15 req: &JobDispatchRequest,
16 ) -> Result<JobDispatchResponse, ClientError> {
17 let req = self
18 .request(Method::POST, &format!("/job/{}/dispatch", job_name))
19 .json(req);
20
21 self.send::<JobDispatchResponse>(req).await
22 }
23
24 pub async fn job_parse(&self, req: &JobsParseRequest) -> Result<Job, ClientError> {
25 let req = self.request(Method::POST, "/jobs/parse").json(req);
26
27 self.send::<Job>(req).await
28 }
29
30 pub async fn job_create(
31 &self,
32 req: &JobCreateRequest,
33 ) -> Result<JobCreateResponse, ClientError> {
34 let req = self.request(Method::POST, "/jobs").json(req);
35
36 self.send::<JobCreateResponse>(req).await
37 }
38
39 pub async fn job_list_allocations(
40 &self,
41 job_name: &str,
42 params: &JobListAllocationsParams,
43 ) -> Result<Vec<Allocation>, ClientError> {
44 let req = self
45 .request(Method::GET, &format!("/job/{}/allocations", job_name))
46 .query(¶ms);
47
48 self.send::<Vec<Allocation>>(req).await
49 }
50
51 pub async fn job_stop(
52 &self,
53 job_name: &str,
54 params: &JobStopParams,
55 ) -> Result<JobStopResponse, ClientError> {
56 let req = self
57 .request(Method::DELETE, &format!("/job/{}", job_name))
58 .query(¶ms);
59
60 self.send::<JobStopResponse>(req).await
61 }
62}
63
64pub mod models {
65 use serde::{Deserialize, Serialize};
66
67 use crate::models::job::Job;
68
69 #[derive(Debug, Deserialize)]
70 #[serde(rename_all = "PascalCase")]
71 pub struct JobStopResponse {
72 #[serde(rename = "EvalID")]
73 pub eval_id: Option<String>,
74 pub eval_create_index: Option<u32>,
75 pub job_modify_index: Option<u32>,
76 }
77
78 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
79 pub struct JobDispatchRequest {
80 #[serde(rename = "JobID", skip_serializing_if = "Option::is_none")]
81 pub job_id: Option<String>,
82 #[serde(rename = "Meta", skip_serializing_if = "Option::is_none")]
83 pub meta: Option<::std::collections::HashMap<String, String>>,
84 #[serde(rename = "Payload", skip_serializing_if = "Option::is_none")]
85 pub payload: Option<String>,
86 }
87
88 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
89 pub struct JobDispatchResponse {
90 #[serde(rename = "DispatchedJobID", skip_serializing_if = "Option::is_none")]
91 pub dispatched_job_id: Option<String>,
92 #[serde(rename = "EvalCreateIndex", skip_serializing_if = "Option::is_none")]
93 pub eval_create_index: Option<i32>,
94 #[serde(rename = "EvalID", skip_serializing_if = "Option::is_none")]
95 pub eval_id: Option<String>,
96 #[serde(rename = "JobCreateIndex", skip_serializing_if = "Option::is_none")]
97 pub job_create_index: Option<i32>,
98 #[serde(rename = "LastIndex", skip_serializing_if = "Option::is_none")]
99 pub last_index: Option<i32>,
100 #[serde(rename = "RequestTime", skip_serializing_if = "Option::is_none")]
101 pub request_time: Option<i64>,
102 }
103
104 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
105 pub struct JobEvaluateRequest {
106 #[serde(rename = "EvalOptions", skip_serializing_if = "Option::is_none")]
107 pub eval_options: Option<crate::models::EvalOptions>,
108 #[serde(rename = "JobID", skip_serializing_if = "Option::is_none")]
109 pub job_id: Option<String>,
110 #[serde(rename = "Namespace", skip_serializing_if = "Option::is_none")]
111 pub namespace: Option<String>,
112 #[serde(rename = "Region", skip_serializing_if = "Option::is_none")]
113 pub region: Option<String>,
114 #[serde(rename = "SecretID", skip_serializing_if = "Option::is_none")]
115 pub secret_id: Option<String>,
116 }
117
118 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
119 pub struct JobPlanRequest {
120 #[serde(rename = "Diff", skip_serializing_if = "Option::is_none")]
121 pub diff: Option<bool>,
122 #[serde(rename = "Job", skip_serializing_if = "Option::is_none")]
123 pub job: Option<crate::models::Job>,
124 #[serde(rename = "Namespace", skip_serializing_if = "Option::is_none")]
125 pub namespace: Option<String>,
126 #[serde(rename = "PolicyOverride", skip_serializing_if = "Option::is_none")]
127 pub policy_override: Option<bool>,
128 #[serde(rename = "Region", skip_serializing_if = "Option::is_none")]
129 pub region: Option<String>,
130 #[serde(rename = "SecretID", skip_serializing_if = "Option::is_none")]
131 pub secret_id: Option<String>,
132 }
133
134 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
135 pub struct JobPlanResponse {
136 #[serde(rename = "Annotations", skip_serializing_if = "Option::is_none")]
137 pub annotations: Option<crate::models::PlanAnnotations>,
138 #[serde(rename = "CreatedEvals", skip_serializing_if = "Option::is_none")]
139 pub created_evals: Option<Vec<crate::models::Evaluation>>,
140 #[serde(rename = "Diff", skip_serializing_if = "Option::is_none")]
141 pub diff: Option<crate::models::JobDiff>,
142 #[serde(rename = "FailedTGAllocs", skip_serializing_if = "Option::is_none")]
143 pub failed_tg_allocs:
144 Option<::std::collections::HashMap<String, crate::models::AllocationMetric>>,
145 #[serde(rename = "JobModifyIndex", skip_serializing_if = "Option::is_none")]
146 pub job_modify_index: Option<i32>,
147 #[serde(rename = "NextPeriodicLaunch", skip_serializing_if = "Option::is_none")]
148 pub next_periodic_launch: Option<String>,
149 #[serde(rename = "Warnings", skip_serializing_if = "Option::is_none")]
150 pub warnings: Option<String>,
151 }
152
153 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
154 pub struct JobRegisterRequest {
155 #[serde(rename = "EnforceIndex", skip_serializing_if = "Option::is_none")]
156 pub enforce_index: Option<bool>,
157 #[serde(rename = "EvalPriority", skip_serializing_if = "Option::is_none")]
158 pub eval_priority: Option<i32>,
159 #[serde(rename = "Job", skip_serializing_if = "Option::is_none")]
160 pub job: Option<crate::models::Job>,
161 #[serde(rename = "JobModifyIndex", skip_serializing_if = "Option::is_none")]
162 pub job_modify_index: Option<i32>,
163 #[serde(rename = "Namespace", skip_serializing_if = "Option::is_none")]
164 pub namespace: Option<String>,
165 #[serde(rename = "PolicyOverride", skip_serializing_if = "Option::is_none")]
166 pub policy_override: Option<bool>,
167 #[serde(rename = "PreserveCounts", skip_serializing_if = "Option::is_none")]
168 pub preserve_counts: Option<bool>,
169 #[serde(rename = "Region", skip_serializing_if = "Option::is_none")]
170 pub region: Option<String>,
171 #[serde(rename = "SecretID", skip_serializing_if = "Option::is_none")]
172 pub secret_id: Option<String>,
173 }
174
175 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
176 pub struct JobRegisterResponse {
177 #[serde(rename = "EvalCreateIndex", skip_serializing_if = "Option::is_none")]
178 pub eval_create_index: Option<i32>,
179 #[serde(rename = "EvalID", skip_serializing_if = "Option::is_none")]
180 pub eval_id: Option<String>,
181 #[serde(rename = "JobModifyIndex", skip_serializing_if = "Option::is_none")]
182 pub job_modify_index: Option<i32>,
183 #[serde(rename = "KnownLeader", skip_serializing_if = "Option::is_none")]
184 pub known_leader: Option<bool>,
185 #[serde(rename = "LastContact", skip_serializing_if = "Option::is_none")]
186 pub last_contact: Option<i64>,
187 #[serde(rename = "LastIndex", skip_serializing_if = "Option::is_none")]
188 pub last_index: Option<i32>,
189 #[serde(rename = "NextToken", skip_serializing_if = "Option::is_none")]
190 pub next_token: Option<String>,
191 #[serde(rename = "RequestTime", skip_serializing_if = "Option::is_none")]
192 pub request_time: Option<i64>,
193 #[serde(rename = "Warnings", skip_serializing_if = "Option::is_none")]
194 pub warnings: Option<String>,
195 }
196
197 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
198 pub struct JobRevertRequest {
199 #[serde(rename = "ConsulToken", skip_serializing_if = "Option::is_none")]
200 pub consul_token: Option<String>,
201 #[serde(
202 rename = "EnforcePriorVersion",
203 skip_serializing_if = "Option::is_none"
204 )]
205 pub enforce_prior_version: Option<i32>,
206 #[serde(rename = "JobID", skip_serializing_if = "Option::is_none")]
207 pub job_id: Option<String>,
208 #[serde(rename = "JobVersion", skip_serializing_if = "Option::is_none")]
209 pub job_version: Option<i32>,
210 #[serde(rename = "Namespace", skip_serializing_if = "Option::is_none")]
211 pub namespace: Option<String>,
212 #[serde(rename = "Region", skip_serializing_if = "Option::is_none")]
213 pub region: Option<String>,
214 #[serde(rename = "SecretID", skip_serializing_if = "Option::is_none")]
215 pub secret_id: Option<String>,
216 #[serde(rename = "VaultToken", skip_serializing_if = "Option::is_none")]
217 pub vault_token: Option<String>,
218 }
219
220 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
221 pub struct JobScaleStatusResponse {
222 #[serde(rename = "JobCreateIndex", skip_serializing_if = "Option::is_none")]
223 pub job_create_index: Option<i32>,
224 #[serde(rename = "JobID", skip_serializing_if = "Option::is_none")]
225 pub job_id: Option<String>,
226 #[serde(rename = "JobModifyIndex", skip_serializing_if = "Option::is_none")]
227 pub job_modify_index: Option<i32>,
228 #[serde(rename = "JobStopped", skip_serializing_if = "Option::is_none")]
229 pub job_stopped: Option<bool>,
230 #[serde(rename = "Namespace", skip_serializing_if = "Option::is_none")]
231 pub namespace: Option<String>,
232 #[serde(rename = "TaskGroups", skip_serializing_if = "Option::is_none")]
233 pub task_groups:
234 Option<::std::collections::HashMap<String, crate::models::TaskGroupScaleStatus>>,
235 }
236
237 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
238 pub struct JobStabilityRequest {
239 #[serde(rename = "JobID", skip_serializing_if = "Option::is_none")]
240 pub job_id: Option<String>,
241 #[serde(rename = "JobVersion", skip_serializing_if = "Option::is_none")]
242 pub job_version: Option<i32>,
243 #[serde(rename = "Namespace", skip_serializing_if = "Option::is_none")]
244 pub namespace: Option<String>,
245 #[serde(rename = "Region", skip_serializing_if = "Option::is_none")]
246 pub region: Option<String>,
247 #[serde(rename = "SecretID", skip_serializing_if = "Option::is_none")]
248 pub secret_id: Option<String>,
249 #[serde(rename = "Stable", skip_serializing_if = "Option::is_none")]
250 pub stable: Option<bool>,
251 }
252
253 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
254 pub struct JobStabilityResponse {
255 #[serde(rename = "Index", skip_serializing_if = "Option::is_none")]
256 pub index: Option<i32>,
257 }
258
259 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
260 pub struct JobValidateRequest {
261 #[serde(rename = "Job", skip_serializing_if = "Option::is_none")]
262 pub job: Option<crate::models::Job>,
263 #[serde(rename = "Namespace", skip_serializing_if = "Option::is_none")]
264 pub namespace: Option<String>,
265 #[serde(rename = "Region", skip_serializing_if = "Option::is_none")]
266 pub region: Option<String>,
267 #[serde(rename = "SecretID", skip_serializing_if = "Option::is_none")]
268 pub secret_id: Option<String>,
269 }
270
271 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
272 pub struct JobValidateResponse {
273 #[serde(
274 rename = "DriverConfigValidated",
275 skip_serializing_if = "Option::is_none"
276 )]
277 pub driver_config_validated: Option<bool>,
278 #[serde(rename = "Error", skip_serializing_if = "Option::is_none")]
279 pub error: Option<String>,
280 #[serde(rename = "ValidationErrors", skip_serializing_if = "Option::is_none")]
281 pub validation_errors: Option<Vec<String>>,
282 #[serde(rename = "Warnings", skip_serializing_if = "Option::is_none")]
283 pub warnings: Option<String>,
284 }
285
286 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
287 pub struct JobVersionsResponse {
288 #[serde(rename = "Diffs", skip_serializing_if = "Option::is_none")]
289 pub diffs: Option<Vec<crate::models::JobDiff>>,
290 #[serde(rename = "KnownLeader", skip_serializing_if = "Option::is_none")]
291 pub known_leader: Option<bool>,
292 #[serde(rename = "LastContact", skip_serializing_if = "Option::is_none")]
293 pub last_contact: Option<i64>,
294 #[serde(rename = "LastIndex", skip_serializing_if = "Option::is_none")]
295 pub last_index: Option<i32>,
296 #[serde(rename = "NextToken", skip_serializing_if = "Option::is_none")]
297 pub next_token: Option<String>,
298 #[serde(rename = "RequestTime", skip_serializing_if = "Option::is_none")]
299 pub request_time: Option<i64>,
300 #[serde(rename = "Versions", skip_serializing_if = "Option::is_none")]
301 pub versions: Option<Vec<crate::models::Job>>,
302 }
303
304 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
305 #[serde(rename_all = "PascalCase")]
306 pub struct JobsParseRequest {
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub canonicalize: Option<bool>,
309 #[serde(rename = "JobHCL", skip_serializing_if = "Option::is_none")]
310 pub job_hcl: Option<String>,
311 #[serde(rename = "hclv1", skip_serializing_if = "Option::is_none")]
312 pub hclv1: Option<bool>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub variables: Option<String>,
315 }
316
317 #[derive(Debug, Default, Serialize)]
318 #[serde(rename_all = "PascalCase")]
319 pub struct JobCreateRequest {
320 pub enforce_index: Option<bool>,
321 pub eval_priority: Option<i32>,
322 pub job: Option<Job>,
323 pub job_modify_index: Option<i32>,
324 pub namespace: Option<String>,
325 pub policy_override: Option<bool>,
326 pub preserve_counts: Option<bool>,
327 pub region: Option<String>,
328 #[serde(rename = "SecretID")]
329 pub secret_id: Option<String>,
330 }
331
332 #[derive(Debug, Deserialize)]
333 #[serde(rename_all = "PascalCase")]
334 pub struct JobCreateResponse {
335 pub eval_create_index: Option<i32>,
336 #[serde(rename = "EvalID")]
337 pub eval_id: Option<String>,
338 pub job_modify_index: Option<i32>,
339 pub known_leader: Option<bool>,
340 pub last_contact: Option<i64>,
341 pub last_index: Option<i32>,
342 pub next_token: Option<String>,
343 pub request_time: Option<i64>,
344 pub warnings: Option<String>,
345 }
346
347 #[derive(Debug, Default, Serialize)]
348 #[serde(rename_all = "camelCase")]
349 pub struct JobListAllocationsParams {
350 pub all: Option<bool>,
351 }
352
353 #[derive(Debug, Default, Serialize)]
354 #[serde(rename_all = "camelCase")]
355 pub struct JobStopParams {
356 pub region: Option<String>,
357 pub global: Option<bool>,
358 pub purge: Option<bool>,
359 pub namespace: Option<String>,
360 }
361
362 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
363 pub struct JobDeregisterResponse {
364 #[serde(rename = "EvalCreateIndex", skip_serializing_if = "Option::is_none")]
365 pub eval_create_index: Option<i32>,
366 #[serde(rename = "EvalID", skip_serializing_if = "Option::is_none")]
367 pub eval_id: Option<String>,
368 #[serde(rename = "JobModifyIndex", skip_serializing_if = "Option::is_none")]
369 pub job_modify_index: Option<i32>,
370 #[serde(rename = "KnownLeader", skip_serializing_if = "Option::is_none")]
371 pub known_leader: Option<bool>,
372 #[serde(rename = "LastContact", skip_serializing_if = "Option::is_none")]
373 pub last_contact: Option<i64>,
374 #[serde(rename = "LastIndex", skip_serializing_if = "Option::is_none")]
375 pub last_index: Option<i32>,
376 #[serde(rename = "NextToken", skip_serializing_if = "Option::is_none")]
377 pub next_token: Option<String>,
378 #[serde(rename = "RequestTime", skip_serializing_if = "Option::is_none")]
379 pub request_time: Option<i64>,
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use crate::api::job::models::{
386 JobCreateRequest, JobCreateResponse, JobDispatchRequest, JobStopParams, JobsParseRequest,
387 };
388 use crate::models::job::Job;
389 use crate::{ClientError, NomadClient};
390
391 static HCL_JOB_BATCH: &str = r#"
392 job "exitWith" {
393 type = "batch"
394 datacenters = ["dc1"]
395
396 group "default" {
397 task "main" {
398 driver = "raw_exec"
399
400 config {
401 command = "sh"
402 args = ["-c", "echo 'exit with 0' && exit 0"]
403 }
404 }
405 }
406 }
407 "#;
408
409 static HCL_JOB_BATCH_PARAM: &str = r#"
410 job "exitWithParam" {
411 type = "batch"
412 datacenters = ["dc1"]
413
414 parameterized {
415 meta_required = []
416 meta_optional = []
417 payload = "forbidden"
418 }
419
420 group "default" {
421 task "main" {
422 driver = "raw_exec"
423
424 config {
425 command = "sh"
426 args = ["-c", "echo exit with 0 && exit 0"]
427 }
428 }
429 }
430 }
431 "#;
432
433 static HCL_JOB_BATCH_PARAM_REQUIRED: &str = r#"
434 job "exitWithParamReq" {
435 type = "batch"
436 datacenters = ["dc1"]
437
438 parameterized {
439 meta_required = ["exitCode"]
440 meta_optional = []
441 payload = "forbidden"
442 }
443
444 group "default" {
445 task "main" {
446 driver = "raw_exec"
447
448 config {
449 command = "sh"
450 args = ["-c", "echo \"exit with ${NOMAD_META_exitCode}\" && exit ${NOMAD_META_exitCode}"]
451 }
452 }
453 }
454 }
455 "#;
456
457 #[tokio::test]
458 async fn job_parse_should_return_valid_job() {
459 let client = NomadClient::default();
460
461 let body = JobsParseRequest {
462 job_hcl: HCL_JOB_BATCH.to_string().into(),
463 ..JobsParseRequest::default()
464 };
465
466 match client.job_parse(&body).await {
467 Ok(job) => {
468 assert_eq!(job.name, Some("exitWith".into()));
469 assert_eq!(job._type, Some("batch".into()));
470 }
471 Err(e) => panic!("{:#?}", e),
472 }
473 }
474
475 #[tokio::test]
476 async fn job_create_should_create_job() {
477 let client = NomadClient::default();
478
479 match parse_and_setup_job(&client, HCL_JOB_BATCH).await {
480 Ok((resp, _)) => assert!(resp.eval_id.is_some()),
481 Err(e) => panic!("{:#?}", e),
482 }
483 }
484
485 #[tokio::test]
486 async fn job_stop_with_purge_should_delete_job() {
487 let client = NomadClient::default();
488
489 match parse_and_setup_job(&client, HCL_JOB_BATCH).await {
490 Ok((resp, job)) => {
491 let params = JobStopParams {
492 purge: Some(true),
493 ..JobStopParams::default()
494 };
495 client
496 .job_stop(job.name.unwrap().as_str(), ¶ms)
497 .await
498 .expect("job should be deleted");
499
500 assert!(resp.eval_id.is_some())
501 }
502 Err(e) => panic!("{:#?}", e),
503 }
504 }
505
506 #[tokio::test]
507 async fn job_dispatch_should_return_err_when_job_doesnt_exist() {
508 let client = NomadClient::default();
509
510 match client
511 .job_dispatch("not-existing", &JobDispatchRequest::default())
512 .await
513 {
514 Ok(_) => panic!("dispatching non existing job was successful"),
515 Err(e) => match e {
516 ClientError::ServerError(_, msg) => assert_eq!(msg, "parameterized job not found"),
517 _ => panic!("unexpected error"),
518 },
519 }
520 }
521
522 #[tokio::test]
523 async fn job_dispatch_should_dispatch_existing_job() {
524 let client = NomadClient::default();
525
526 match parse_and_setup_job(&client, HCL_JOB_BATCH_PARAM).await {
527 Ok((_, job)) => {
528 match client
529 .job_dispatch(job.name.unwrap().as_str(), &JobDispatchRequest::default())
530 .await
531 {
532 Ok(_) => assert!(true),
533 Err(e) => panic!("{:#?}", e),
534 };
535 }
536 Err(e) => panic!("{:#?}", e),
537 }
538 }
539
540 async fn parse_and_setup_job(
541 client: &NomadClient,
542 hcl: &str,
543 ) -> Result<(JobCreateResponse, Job), ClientError> {
544 let parse_req = JobsParseRequest {
545 job_hcl: hcl.to_string().into(),
546 ..JobsParseRequest::default()
547 };
548
549 return match client.job_parse(&parse_req).await {
550 Ok(job) => {
551 let req = JobCreateRequest {
552 job: Some(job.clone()),
553 ..JobCreateRequest::default()
554 };
555
556 match client.job_create(&req).await {
557 Ok(response) => Ok((response, job)),
558 Err(e) => Err(e),
559 }
560 }
561 Err(e) => Err(e),
562 };
563 }
564
565 }