use reqwest::Method;
use crate::api::job::models::{
JobCreateRequest, JobCreateResponse, JobDispatchRequest, JobDispatchResponse,
JobListAllocationsParams, JobStopParams, JobStopResponse, JobsParseRequest,
};
use crate::models::allocation::Allocation;
use crate::models::job::Job;
use crate::{ClientError, NomadClient};
impl NomadClient {
pub async fn job_dispatch(
&self,
job_name: &str,
req: &JobDispatchRequest,
) -> Result<JobDispatchResponse, ClientError> {
let req = self
.request(Method::POST, &format!("/job/{}/dispatch", job_name))
.json(req);
self.send::<JobDispatchResponse>(req).await
}
pub async fn job_parse(&self, req: &JobsParseRequest) -> Result<Job, ClientError> {
let req = self.request(Method::POST, "/jobs/parse").json(req);
self.send::<Job>(req).await
}
pub async fn job_create(
&self,
req: &JobCreateRequest,
) -> Result<JobCreateResponse, ClientError> {
let req = self.request(Method::POST, "/jobs").json(req);
self.send::<JobCreateResponse>(req).await
}
pub async fn job_list_allocations(
&self,
job_name: &str,
params: &JobListAllocationsParams,
) -> Result<Vec<Allocation>, ClientError> {
let req = self
.request(Method::GET, &format!("/job/{}/allocations", job_name))
.query(¶ms);
self.send::<Vec<Allocation>>(req).await
}
pub async fn job_stop(
&self,
job_name: &str,
params: &JobStopParams,
) -> Result<JobStopResponse, ClientError> {
let req = self
.request(Method::DELETE, &format!("/job/{}", job_name))
.query(¶ms);
self.send::<JobStopResponse>(req).await
}
}
pub mod models {
use serde::{Deserialize, Serialize};
use crate::models::job::Job;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct JobStopResponse {
#[serde(rename = "EvalID")]
pub eval_id: Option<String>,
pub eval_create_index: Option<u32>,
pub job_modify_index: Option<u32>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JobDispatchRequest {
#[serde(rename = "JobID", skip_serializing_if = "Option::is_none")]
pub job_id: Option<String>,
#[serde(rename = "Meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<::std::collections::HashMap<String, String>>,
#[serde(rename = "Payload", skip_serializing_if = "Option::is_none")]
pub payload: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JobDispatchResponse {
#[serde(rename = "DispatchedJobID", skip_serializing_if = "Option::is_none")]
pub dispatched_job_id: Option<String>,
#[serde(rename = "EvalCreateIndex", skip_serializing_if = "Option::is_none")]
pub eval_create_index: Option<i32>,
#[serde(rename = "EvalID", skip_serializing_if = "Option::is_none")]
pub eval_id: Option<String>,
#[serde(rename = "JobCreateIndex", skip_serializing_if = "Option::is_none")]
pub job_create_index: Option<i32>,
#[serde(rename = "LastIndex", skip_serializing_if = "Option::is_none")]
pub last_index: Option<i32>,
#[serde(rename = "RequestTime", skip_serializing_if = "Option::is_none")]
pub request_time: Option<i64>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JobEvaluateRequest {
#[serde(rename = "EvalOptions", skip_serializing_if = "Option::is_none")]
pub eval_options: Option<crate::models::EvalOptions>,
#[serde(rename = "JobID", skip_serializing_if = "Option::is_none")]
pub job_id: Option<String>,
#[serde(rename = "Namespace", skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(rename = "Region", skip_serializing_if = "Option::is_none")]
pub region: Option<String>,
#[serde(rename = "SecretID", skip_serializing_if = "Option::is_none")]
pub secret_id: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JobPlanRequest {
#[serde(rename = "Diff", skip_serializing_if = "Option::is_none")]
pub diff: Option<bool>,
#[serde(rename = "Job", skip_serializing_if = "Option::is_none")]
pub job: Option<crate::models::Job>,
#[serde(rename = "Namespace", skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(rename = "PolicyOverride", skip_serializing_if = "Option::is_none")]
pub policy_override: Option<bool>,
#[serde(rename = "Region", skip_serializing_if = "Option::is_none")]
pub region: Option<String>,
#[serde(rename = "SecretID", skip_serializing_if = "Option::is_none")]
pub secret_id: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JobPlanResponse {
#[serde(rename = "Annotations", skip_serializing_if = "Option::is_none")]
pub annotations: Option<crate::models::PlanAnnotations>,
#[serde(rename = "CreatedEvals", skip_serializing_if = "Option::is_none")]
pub created_evals: Option<Vec<crate::models::Evaluation>>,
#[serde(rename = "Diff", skip_serializing_if = "Option::is_none")]
pub diff: Option<crate::models::JobDiff>,
#[serde(rename = "FailedTGAllocs", skip_serializing_if = "Option::is_none")]
pub failed_tg_allocs:
Option<::std::collections::HashMap<String, crate::models::AllocationMetric>>,
#[serde(rename = "JobModifyIndex", skip_serializing_if = "Option::is_none")]
pub job_modify_index: Option<i32>,
#[serde(rename = "NextPeriodicLaunch", skip_serializing_if = "Option::is_none")]
pub next_periodic_launch: Option<String>,
#[serde(rename = "Warnings", skip_serializing_if = "Option::is_none")]
pub warnings: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JobRegisterRequest {
#[serde(rename = "EnforceIndex", skip_serializing_if = "Option::is_none")]
pub enforce_index: Option<bool>,
#[serde(rename = "EvalPriority", skip_serializing_if = "Option::is_none")]
pub eval_priority: Option<i32>,
#[serde(rename = "Job", skip_serializing_if = "Option::is_none")]
pub job: Option<crate::models::Job>,
#[serde(rename = "JobModifyIndex", skip_serializing_if = "Option::is_none")]
pub job_modify_index: Option<i32>,
#[serde(rename = "Namespace", skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(rename = "PolicyOverride", skip_serializing_if = "Option::is_none")]
pub policy_override: Option<bool>,
#[serde(rename = "PreserveCounts", skip_serializing_if = "Option::is_none")]
pub preserve_counts: Option<bool>,
#[serde(rename = "Region", skip_serializing_if = "Option::is_none")]
pub region: Option<String>,
#[serde(rename = "SecretID", skip_serializing_if = "Option::is_none")]
pub secret_id: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JobRegisterResponse {
#[serde(rename = "EvalCreateIndex", skip_serializing_if = "Option::is_none")]
pub eval_create_index: Option<i32>,
#[serde(rename = "EvalID", skip_serializing_if = "Option::is_none")]
pub eval_id: Option<String>,
#[serde(rename = "JobModifyIndex", skip_serializing_if = "Option::is_none")]
pub job_modify_index: Option<i32>,
#[serde(rename = "KnownLeader", skip_serializing_if = "Option::is_none")]
pub known_leader: Option<bool>,
#[serde(rename = "LastContact", skip_serializing_if = "Option::is_none")]
pub last_contact: Option<i64>,
#[serde(rename = "LastIndex", skip_serializing_if = "Option::is_none")]
pub last_index: Option<i32>,
#[serde(rename = "NextToken", skip_serializing_if = "Option::is_none")]
pub next_token: Option<String>,
#[serde(rename = "RequestTime", skip_serializing_if = "Option::is_none")]
pub request_time: Option<i64>,
#[serde(rename = "Warnings", skip_serializing_if = "Option::is_none")]
pub warnings: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JobRevertRequest {
#[serde(rename = "ConsulToken", skip_serializing_if = "Option::is_none")]
pub consul_token: Option<String>,
#[serde(
rename = "EnforcePriorVersion",
skip_serializing_if = "Option::is_none"
)]
pub enforce_prior_version: Option<i32>,
#[serde(rename = "JobID", skip_serializing_if = "Option::is_none")]
pub job_id: Option<String>,
#[serde(rename = "JobVersion", skip_serializing_if = "Option::is_none")]
pub job_version: Option<i32>,
#[serde(rename = "Namespace", skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(rename = "Region", skip_serializing_if = "Option::is_none")]
pub region: Option<String>,
#[serde(rename = "SecretID", skip_serializing_if = "Option::is_none")]
pub secret_id: Option<String>,
#[serde(rename = "VaultToken", skip_serializing_if = "Option::is_none")]
pub vault_token: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JobScaleStatusResponse {
#[serde(rename = "JobCreateIndex", skip_serializing_if = "Option::is_none")]
pub job_create_index: Option<i32>,
#[serde(rename = "JobID", skip_serializing_if = "Option::is_none")]
pub job_id: Option<String>,
#[serde(rename = "JobModifyIndex", skip_serializing_if = "Option::is_none")]
pub job_modify_index: Option<i32>,
#[serde(rename = "JobStopped", skip_serializing_if = "Option::is_none")]
pub job_stopped: Option<bool>,
#[serde(rename = "Namespace", skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(rename = "TaskGroups", skip_serializing_if = "Option::is_none")]
pub task_groups:
Option<::std::collections::HashMap<String, crate::models::TaskGroupScaleStatus>>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JobStabilityRequest {
#[serde(rename = "JobID", skip_serializing_if = "Option::is_none")]
pub job_id: Option<String>,
#[serde(rename = "JobVersion", skip_serializing_if = "Option::is_none")]
pub job_version: Option<i32>,
#[serde(rename = "Namespace", skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(rename = "Region", skip_serializing_if = "Option::is_none")]
pub region: Option<String>,
#[serde(rename = "SecretID", skip_serializing_if = "Option::is_none")]
pub secret_id: Option<String>,
#[serde(rename = "Stable", skip_serializing_if = "Option::is_none")]
pub stable: Option<bool>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JobStabilityResponse {
#[serde(rename = "Index", skip_serializing_if = "Option::is_none")]
pub index: Option<i32>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JobValidateRequest {
#[serde(rename = "Job", skip_serializing_if = "Option::is_none")]
pub job: Option<crate::models::Job>,
#[serde(rename = "Namespace", skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(rename = "Region", skip_serializing_if = "Option::is_none")]
pub region: Option<String>,
#[serde(rename = "SecretID", skip_serializing_if = "Option::is_none")]
pub secret_id: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JobValidateResponse {
#[serde(
rename = "DriverConfigValidated",
skip_serializing_if = "Option::is_none"
)]
pub driver_config_validated: Option<bool>,
#[serde(rename = "Error", skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(rename = "ValidationErrors", skip_serializing_if = "Option::is_none")]
pub validation_errors: Option<Vec<String>>,
#[serde(rename = "Warnings", skip_serializing_if = "Option::is_none")]
pub warnings: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JobVersionsResponse {
#[serde(rename = "Diffs", skip_serializing_if = "Option::is_none")]
pub diffs: Option<Vec<crate::models::JobDiff>>,
#[serde(rename = "KnownLeader", skip_serializing_if = "Option::is_none")]
pub known_leader: Option<bool>,
#[serde(rename = "LastContact", skip_serializing_if = "Option::is_none")]
pub last_contact: Option<i64>,
#[serde(rename = "LastIndex", skip_serializing_if = "Option::is_none")]
pub last_index: Option<i32>,
#[serde(rename = "NextToken", skip_serializing_if = "Option::is_none")]
pub next_token: Option<String>,
#[serde(rename = "RequestTime", skip_serializing_if = "Option::is_none")]
pub request_time: Option<i64>,
#[serde(rename = "Versions", skip_serializing_if = "Option::is_none")]
pub versions: Option<Vec<crate::models::Job>>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct JobsParseRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub canonicalize: Option<bool>,
#[serde(rename = "JobHCL", skip_serializing_if = "Option::is_none")]
pub job_hcl: Option<String>,
#[serde(rename = "hclv1", skip_serializing_if = "Option::is_none")]
pub hclv1: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variables: Option<String>,
}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct JobCreateRequest {
pub enforce_index: Option<bool>,
pub eval_priority: Option<i32>,
pub job: Option<Job>,
pub job_modify_index: Option<i32>,
pub namespace: Option<String>,
pub policy_override: Option<bool>,
pub preserve_counts: Option<bool>,
pub region: Option<String>,
#[serde(rename = "SecretID")]
pub secret_id: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct JobCreateResponse {
pub eval_create_index: Option<i32>,
#[serde(rename = "EvalID")]
pub eval_id: Option<String>,
pub job_modify_index: Option<i32>,
pub known_leader: Option<bool>,
pub last_contact: Option<i64>,
pub last_index: Option<i32>,
pub next_token: Option<String>,
pub request_time: Option<i64>,
pub warnings: Option<String>,
}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JobListAllocationsParams {
pub all: Option<bool>,
}
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JobStopParams {
pub region: Option<String>,
pub global: Option<bool>,
pub purge: Option<bool>,
pub namespace: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct JobDeregisterResponse {
#[serde(rename = "EvalCreateIndex", skip_serializing_if = "Option::is_none")]
pub eval_create_index: Option<i32>,
#[serde(rename = "EvalID", skip_serializing_if = "Option::is_none")]
pub eval_id: Option<String>,
#[serde(rename = "JobModifyIndex", skip_serializing_if = "Option::is_none")]
pub job_modify_index: Option<i32>,
#[serde(rename = "KnownLeader", skip_serializing_if = "Option::is_none")]
pub known_leader: Option<bool>,
#[serde(rename = "LastContact", skip_serializing_if = "Option::is_none")]
pub last_contact: Option<i64>,
#[serde(rename = "LastIndex", skip_serializing_if = "Option::is_none")]
pub last_index: Option<i32>,
#[serde(rename = "NextToken", skip_serializing_if = "Option::is_none")]
pub next_token: Option<String>,
#[serde(rename = "RequestTime", skip_serializing_if = "Option::is_none")]
pub request_time: Option<i64>,
}
}
#[cfg(test)]
mod tests {
use crate::api::job::models::{
JobCreateRequest, JobCreateResponse, JobDispatchRequest, JobStopParams, JobsParseRequest,
};
use crate::models::job::Job;
use crate::{ClientError, NomadClient};
static HCL_JOB_BATCH: &str = r#"
job "exitWith" {
type = "batch"
datacenters = ["dc1"]
group "default" {
task "main" {
driver = "raw_exec"
config {
command = "sh"
args = ["-c", "echo 'exit with 0' && exit 0"]
}
}
}
}
"#;
static HCL_JOB_BATCH_PARAM: &str = r#"
job "exitWithParam" {
type = "batch"
datacenters = ["dc1"]
parameterized {
meta_required = []
meta_optional = []
payload = "forbidden"
}
group "default" {
task "main" {
driver = "raw_exec"
config {
command = "sh"
args = ["-c", "echo exit with 0 && exit 0"]
}
}
}
}
"#;
static HCL_JOB_BATCH_PARAM_REQUIRED: &str = r#"
job "exitWithParamReq" {
type = "batch"
datacenters = ["dc1"]
parameterized {
meta_required = ["exitCode"]
meta_optional = []
payload = "forbidden"
}
group "default" {
task "main" {
driver = "raw_exec"
config {
command = "sh"
args = ["-c", "echo \"exit with ${NOMAD_META_exitCode}\" && exit ${NOMAD_META_exitCode}"]
}
}
}
}
"#;
#[tokio::test]
async fn job_parse_should_return_valid_job() {
let client = NomadClient::default();
let body = JobsParseRequest {
job_hcl: HCL_JOB_BATCH.to_string().into(),
..JobsParseRequest::default()
};
match client.job_parse(&body).await {
Ok(job) => {
assert_eq!(job.name, Some("exitWith".into()));
assert_eq!(job._type, Some("batch".into()));
}
Err(e) => panic!("{:#?}", e),
}
}
#[tokio::test]
async fn job_create_should_create_job() {
let client = NomadClient::default();
match parse_and_setup_job(&client, HCL_JOB_BATCH).await {
Ok((resp, _)) => assert!(resp.eval_id.is_some()),
Err(e) => panic!("{:#?}", e),
}
}
#[tokio::test]
async fn job_stop_with_purge_should_delete_job() {
let client = NomadClient::default();
match parse_and_setup_job(&client, HCL_JOB_BATCH).await {
Ok((resp, job)) => {
let params = JobStopParams {
purge: Some(true),
..JobStopParams::default()
};
client
.job_stop(job.name.unwrap().as_str(), ¶ms)
.await
.expect("job should be deleted");
assert!(resp.eval_id.is_some())
}
Err(e) => panic!("{:#?}", e),
}
}
#[tokio::test]
async fn job_dispatch_should_return_err_when_job_doesnt_exist() {
let client = NomadClient::default();
match client
.job_dispatch("not-existing", &JobDispatchRequest::default())
.await
{
Ok(_) => panic!("dispatching non existing job was successful"),
Err(e) => match e {
ClientError::ServerError(_, msg) => assert_eq!(msg, "parameterized job not found"),
_ => panic!("unexpected error"),
},
}
}
#[tokio::test]
async fn job_dispatch_should_dispatch_existing_job() {
let client = NomadClient::default();
match parse_and_setup_job(&client, HCL_JOB_BATCH_PARAM).await {
Ok((_, job)) => {
match client
.job_dispatch(job.name.unwrap().as_str(), &JobDispatchRequest::default())
.await
{
Ok(_) => assert!(true),
Err(e) => panic!("{:#?}", e),
};
}
Err(e) => panic!("{:#?}", e),
}
}
async fn parse_and_setup_job(
client: &NomadClient,
hcl: &str,
) -> Result<(JobCreateResponse, Job), ClientError> {
let parse_req = JobsParseRequest {
job_hcl: hcl.to_string().into(),
..JobsParseRequest::default()
};
return match client.job_parse(&parse_req).await {
Ok(job) => {
let req = JobCreateRequest {
job: Some(job.clone()),
..JobCreateRequest::default()
};
match client.job_create(&req).await {
Ok(response) => Ok((response, job)),
Err(e) => Err(e),
}
}
Err(e) => Err(e),
};
}
}