nomad_client_rs/api/
job.rs

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(&params);
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(&params);
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(), &params)
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    // let body = JobCreateRequest{
566    //     job: Some(Job{
567    //         id: Some("exitWithRaw2".into()),
568    //         r#type: Some("batch".into()),
569    //         datacenters: Some(vec!["dc1".into()]),
570    //         task_groups: Some(vec![
571    //             TaskGroup {
572    //                 name: Some("default".into()),
573    //                 tasks: Some(vec![
574    //                     Task{
575    //                         name: Some("main".into()),
576    //                         driver: Some("raw_exec".into()),
577    //                         config: Some(config),
578    //                         ..Task::default()
579    //                     }
580    //                 ]),
581    //                 ..TaskGroup::default()
582    //             }
583    //         ]),
584    //         ..Job::default()
585    //     }),
586    //     ..JobCreateRequest::default()
587    // };
588}