1use std::{collections::HashMap, iter};
6
7use futures_util::stream::Stream;
8use hyper::Body;
9use serde::{Deserialize, Serialize};
10use serde_json::{json, Value};
11use url::form_urlencoded;
12
13use crate::{
14 container::LogsOptions,
15 docker::Docker,
16 errors::{Error, Result},
17 image::RegistryAuth,
18 tty,
19};
20
21#[cfg(feature = "chrono")]
22use chrono::{DateTime, Utc};
23
24pub struct Services<'docker> {
28 docker: &'docker Docker,
29}
30
31impl<'docker> Services<'docker> {
32 pub fn new(docker: &'docker Docker) -> Self {
34 Services { docker }
35 }
36
37 pub async fn list(
41 &self,
42 opts: &ServiceListOptions,
43 ) -> Result<Vec<ServiceInfo>> {
44 let mut path = vec!["/services".to_owned()];
45 if let Some(query) = opts.serialize() {
46 path.push(query);
47 }
48 self.docker
49 .get_json::<Vec<ServiceInfo>>(&path.join("?"))
50 .await
51 }
52
53 pub fn get(
55 &self,
56 name: &str,
57 ) -> Service<'docker> {
58 Service::new(self.docker, name)
59 }
60}
61
62pub struct Service<'docker> {
66 docker: &'docker Docker,
67 name: String,
68}
69
70impl<'docker> Service<'docker> {
71 pub fn new<S>(
73 docker: &'docker Docker,
74 name: S,
75 ) -> Self
76 where
77 S: Into<String>,
78 {
79 Service {
80 docker,
81 name: name.into(),
82 }
83 }
84
85 pub async fn create(
89 &self,
90 opts: &ServiceOptions,
91 ) -> Result<ServiceCreateInfo> {
92 let body: Body = opts.serialize()?.into();
93 let path = vec!["/service/create".to_owned()];
94
95 let headers = opts
96 .auth_header()
97 .map(|a| iter::once(("X-Registry-Auth", a)));
98
99 self.docker
100 .post_json_headers(
101 &path.join("?"),
102 Some((body, mime::APPLICATION_JSON)),
103 headers,
104 )
105 .await
106 }
107
108 pub async fn inspect(&self) -> Result<ServiceDetails> {
112 self.docker
113 .get_json(&format!("/services/{}", self.name)[..])
114 .await
115 }
116
117 pub async fn delete(&self) -> Result<()> {
121 self.docker
122 .delete_json(&format!("/services/{}", self.name)[..])
123 .await
124 }
125
126 pub fn logs(
130 &self,
131 opts: &LogsOptions,
132 ) -> impl Stream<Item = Result<tty::TtyChunk>> + Unpin + 'docker {
133 let mut path = vec![format!("/services/{}/logs", self.name)];
134 if let Some(query) = opts.serialize() {
135 path.push(query)
136 }
137
138 let stream = Box::pin(self.docker.stream_get(path.join("?")));
139
140 Box::pin(tty::decode(stream))
141 }
142}
143
144#[derive(Default, Debug)]
146pub struct ServiceListOptions {
147 params: HashMap<&'static str, String>,
148}
149
150impl ServiceListOptions {
151 pub fn builder() -> ServiceListOptionsBuilder {
153 ServiceListOptionsBuilder::default()
154 }
155
156 pub fn serialize(&self) -> Option<String> {
158 if self.params.is_empty() {
159 None
160 } else {
161 Some(
162 form_urlencoded::Serializer::new(String::new())
163 .extend_pairs(&self.params)
164 .finish(),
165 )
166 }
167 }
168}
169
170pub enum ServiceFilter {
172 Id(String),
173 Label(String),
174 ReplicatedMode,
175 GlobalMode,
176 Name(String),
177}
178
179#[derive(Default)]
181pub struct ServiceListOptionsBuilder {
182 params: HashMap<&'static str, String>,
183}
184
185impl ServiceListOptionsBuilder {
186 pub fn filter(
187 &mut self,
188 filters: Vec<ServiceFilter>,
189 ) -> &mut Self {
190 let mut param = HashMap::new();
191 for f in filters {
192 match f {
193 ServiceFilter::Id(i) => param.insert("id", vec![i]),
194 ServiceFilter::Label(l) => param.insert("label", vec![l]),
195 ServiceFilter::ReplicatedMode => {
196 param.insert("mode", vec!["replicated".to_string()])
197 }
198 ServiceFilter::GlobalMode => param.insert("mode", vec!["global".to_string()]),
199 ServiceFilter::Name(n) => param.insert("name", vec![n.to_string()]),
200 };
201 }
202 self.params
205 .insert("filters", serde_json::to_string(¶m).unwrap());
206 self
207 }
208
209 pub fn enable_status(&mut self) -> &mut Self {
210 self.params.insert("status", "true".to_owned());
211 self
212 }
213
214 pub fn build(&self) -> ServiceListOptions {
215 ServiceListOptions {
216 params: self.params.clone(),
217 }
218 }
219}
220
221#[derive(Default, Debug)]
222pub struct ServiceOptions {
223 auth: Option<RegistryAuth>,
224 params: HashMap<&'static str, Value>,
225}
226
227impl ServiceOptions {
228 pub fn builder() -> ServiceOptionsBuilder {
230 ServiceOptionsBuilder::default()
231 }
232
233 pub fn serialize(&self) -> Result<String> {
235 serde_json::to_string(&self.params).map_err(Error::from)
236 }
237
238 pub(crate) fn auth_header(&self) -> Option<String> {
239 self.auth.clone().map(|a| a.serialize())
240 }
241}
242
243#[derive(Default)]
244pub struct ServiceOptionsBuilder {
245 auth: Option<RegistryAuth>,
246 params: HashMap<&'static str, Result<Value>>,
247}
248
249impl ServiceOptionsBuilder {
250 pub fn name<S>(
251 &mut self,
252 name: S,
253 ) -> &mut Self
254 where
255 S: AsRef<str>,
256 {
257 self.params.insert("Name", Ok(json!(name.as_ref())));
258 self
259 }
260
261 pub fn labels<I>(
262 &mut self,
263 labels: I,
264 ) -> &mut Self
265 where
266 I: IntoIterator<Item = (String, String)>,
267 {
268 self.params.insert(
269 "Labels",
270 Ok(json!(labels
271 .into_iter()
272 .collect::<HashMap<String, String>>())),
273 );
274 self
275 }
276
277 pub fn task_template(
278 &mut self,
279 spec: &TaskSpec,
280 ) -> &mut Self {
281 self.params.insert("TaskTemplate", to_json_value(spec));
282 self
283 }
284
285 pub fn mode(
286 &mut self,
287 mode: &Mode,
288 ) -> &mut Self {
289 self.params.insert("Mode", to_json_value(mode));
290 self
291 }
292
293 pub fn update_config(
294 &mut self,
295 conf: &UpdateConfig,
296 ) -> &mut Self {
297 self.params.insert("UpdateConfig", to_json_value(conf));
298 self
299 }
300
301 pub fn rollback_config(
302 &mut self,
303 conf: &RollbackConfig,
304 ) -> &mut Self {
305 self.params.insert("RollbackConfig", to_json_value(conf));
306 self
307 }
308
309 pub fn networks<I>(
310 &mut self,
311 networks: I,
312 ) -> &mut Self
313 where
314 I: IntoIterator<Item = NetworkAttachmentConfig>,
315 {
316 self.params.insert(
317 "Networks",
318 to_json_value(
319 networks
320 .into_iter()
321 .collect::<Vec<NetworkAttachmentConfig>>(),
322 ),
323 );
324 self
325 }
326
327 pub fn endpoint_spec(
328 &mut self,
329 spec: &EndpointSpec,
330 ) -> &mut Self {
331 self.params.insert("EndpointSpec", to_json_value(spec));
332 self
333 }
334
335 pub fn auth(
336 &mut self,
337 auth: RegistryAuth,
338 ) -> &mut Self {
339 self.auth = Some(auth);
340 self
341 }
342
343 pub fn build(&mut self) -> Result<ServiceOptions> {
344 let params = std::mem::take(&mut self.params);
345 let mut new_params = HashMap::new();
346 for (k, v) in params.into_iter() {
347 new_params.insert(k, v?);
348 }
349 Ok(ServiceOptions {
350 auth: self.auth.take(),
351 params: new_params,
352 })
353 }
354}
355
356fn to_json_value<T>(value: T) -> Result<Value>
357where
358 T: Serialize,
359{
360 Ok(serde_json::to_value(value)?)
361}
362
363pub type ServicesInfo = Vec<ServiceInfo>;
364
365#[derive(Clone, Debug, Serialize, Deserialize)]
366#[serde(rename_all = "PascalCase")]
367pub struct ServiceInfo {
368 #[serde(rename = "ID")]
369 pub id: String,
370 pub version: ObjectVersion,
371 #[cfg(feature = "chrono")]
372 pub created_at: DateTime<Utc>,
373 #[cfg(not(feature = "chrono"))]
374 pub created_at: String,
375 #[cfg(feature = "chrono")]
376 pub updated_at: DateTime<Utc>,
377 #[cfg(not(feature = "chrono"))]
378 pub updated_at: String,
379 pub endpoint: Endpoint,
380 pub update_status: Option<UpdateStatus>,
381 pub service_status: Option<ServiceStatus>,
382 pub job_status: Option<JobStatus>,
383}
384
385#[derive(Clone, Debug, Serialize, Deserialize)]
386#[serde(rename_all = "PascalCase")]
387pub struct ObjectVersion {
388 pub index: u64,
389}
390
391#[derive(Clone, Debug, Serialize, Deserialize)]
392#[serde(rename_all = "PascalCase")]
393pub struct Endpoint {
394 pub spec: EndpointSpec,
395 pub ports: Option<Vec<EndpointPortConfig>>,
396 #[serde(rename = "VirtualIPs")]
397 pub virtual_ips: Option<serde_json::Value>,
398}
399
400#[derive(Clone, Debug, Serialize, Deserialize)]
401#[serde(rename_all = "PascalCase")]
402pub struct EndpointSpec {
403 pub mode: Option<String>,
404 pub ports: Option<Vec<EndpointPortConfig>>,
405}
406
407#[derive(Clone, Debug, Serialize, Deserialize)]
408#[serde(rename_all = "PascalCase")]
409pub struct EndpointPortConfig {
410 pub name: Option<String>,
411 pub protocol: String,
412 pub publish_mode: String,
413 pub published_port: Option<u64>,
414 pub target_port: u64,
415}
416
417#[derive(Clone, Debug, Serialize, Deserialize)]
418#[serde(rename_all = "PascalCase")]
419pub struct UpdateStatus {
420 pub state: String,
421 #[cfg(feature = "chrono")]
422 pub started_at: DateTime<Utc>,
423 #[cfg(not(feature = "chrono"))]
424 pub started_at: String,
425 #[cfg(feature = "chrono")]
426 pub completed_at: DateTime<Utc>,
427 #[cfg(not(feature = "chrono"))]
428 pub completed_at: String,
429 pub message: String,
430}
431
432#[derive(Clone, Debug, Serialize, Deserialize)]
433#[serde(rename_all = "PascalCase")]
434pub struct ServiceStatus {
435 pub running_tasks: u64,
436 pub desired_tasks: u64,
437 pub completed_tasks: u64,
438}
439
440#[derive(Clone, Debug, Serialize, Deserialize)]
441#[serde(rename_all = "PascalCase")]
442pub struct JobStatus {
443 pub job_iteration: ObjectVersion,
444 #[cfg(feature = "chrono")]
445 pub last_execution: DateTime<Utc>,
446 #[cfg(not(feature = "chrono"))]
447 pub last_execution: String,
448}
449
450#[derive(Clone, Debug, Serialize, Deserialize)]
451#[serde(rename_all = "PascalCase")]
452pub struct ServiceDetails {
453 #[serde(rename = "ID")]
454 pub id: String,
455 pub version: ObjectVersion,
456 #[cfg(feature = "chrono")]
457 pub created_at: DateTime<Utc>,
458 #[cfg(not(feature = "chrono"))]
459 pub created_at: String,
460 #[cfg(feature = "chrono")]
461 pub updated_at: DateTime<Utc>,
462 #[cfg(not(feature = "chrono"))]
463 pub updated_at: String,
464 pub spec: ServiceSpec,
465 pub endpoint: Endpoint,
466 pub update_status: Option<UpdateStatus>,
467 pub service_status: Option<ServiceStatus>,
468 pub job_status: Option<JobStatus>,
469}
470
471#[derive(Clone, Debug, Serialize, Deserialize)]
472#[serde(rename_all = "PascalCase")]
473pub struct ServiceSpec {
474 pub name: String,
475 pub labels: Option<serde_json::Value>,
476 pub task_template: TaskSpec,
477 pub mode: Mode,
478 pub update_config: Option<UpdateConfig>,
479 pub rollback_config: Option<RollbackConfig>,
480 pub networks: Option<Vec<NetworkAttachmentConfig>>,
481 pub endpoint_spec: EndpointSpec,
482}
483
484#[derive(Clone, Debug, Serialize, Deserialize)]
485#[serde(rename_all = "PascalCase")]
486pub struct TaskSpec {}
488
489#[derive(Clone, Debug, Serialize, Deserialize)]
490#[serde(rename_all = "PascalCase")]
491pub struct Mode {
492 pub replicated: Option<Replicated>,
493 pub global: Option<serde_json::Value>,
494 pub replicated_job: Option<ReplicatedJob>,
495 pub global_job: Option<serde_json::Value>,
496}
497
498#[derive(Clone, Debug, Serialize, Deserialize)]
499#[serde(rename_all = "PascalCase")]
500pub struct Replicated {
501 pub replicas: u64,
502}
503
504#[derive(Clone, Debug, Serialize, Deserialize)]
505#[serde(rename_all = "PascalCase")]
506pub struct ReplicatedJob {
507 pub max_concurrent: u64,
508 pub total_completions: u64,
509}
510
511#[derive(Clone, Debug, Serialize, Deserialize)]
512#[serde(rename_all = "PascalCase")]
513pub struct UpdateConfig {
514 pub parallelism: u64,
515 pub delay: u64,
516 pub failure_action: String,
517 pub monitor: u64,
518 pub max_failure_ratio: usize,
519 pub order: String,
520}
521
522pub type RollbackConfig = UpdateConfig;
523
524#[derive(Clone, Debug, Serialize, Deserialize)]
525#[serde(rename_all = "PascalCase")]
526pub struct NetworkAttachmentConfig {
527 pub target: String,
528 pub aliases: Vec<String>,
529 pub driver_opts: Option<serde_json::Value>,
530}
531
532#[derive(Clone, Debug, Serialize, Deserialize)]
533pub struct ServiceCreateInfo {
534 #[serde(rename = "ID")]
535 pub id: String,
536 #[serde(rename = "Warning")]
537 pub warning: Option<String>,
538}