mirror of
https://github.com/moghtech/komodo.git
synced 2025-12-05 19:17:36 -06:00
periphery swarm stack deploy
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use std::{borrow::Cow, path::PathBuf};
|
||||
use std::{borrow::Cow, fmt::Write, path::PathBuf};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use command::{
|
||||
@@ -12,8 +12,9 @@ use komodo_client::{
|
||||
entities::{
|
||||
FileContents, RepoExecutionResponse, all_logs_success,
|
||||
stack::{
|
||||
ComposeFile, ComposeService, ComposeServiceDeploy,
|
||||
StackRemoteFileContents, StackServiceNames,
|
||||
AdditionalEnvFile, ComposeFile, ComposeService,
|
||||
ComposeServiceDeploy, StackRemoteFileContents,
|
||||
StackServiceNames,
|
||||
},
|
||||
to_path_compatible_name,
|
||||
update::Log,
|
||||
@@ -29,13 +30,12 @@ use crate::{
|
||||
config::periphery_config,
|
||||
docker::compose::docker_compose,
|
||||
helpers::{format_extra_args, format_log_grep},
|
||||
stack::{
|
||||
maybe_login_registry, pull_or_clone_stack, validate_files,
|
||||
write::write_stack,
|
||||
},
|
||||
};
|
||||
|
||||
mod helpers;
|
||||
mod write;
|
||||
|
||||
use helpers::*;
|
||||
|
||||
impl Resolve<crate::api::Args> for GetComposeLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
@@ -287,7 +287,7 @@ impl Resolve<crate::api::Args> for ComposePull {
|
||||
.push_logs(&mut res.logs);
|
||||
replacers.extend(interpolator.secret_replacers);
|
||||
|
||||
let (run_directory, env_file_path) = match write::stack(
|
||||
let (run_directory, env_file_path) = match write_stack(
|
||||
&stack,
|
||||
repo.as_ref(),
|
||||
git_token,
|
||||
@@ -396,12 +396,6 @@ impl Resolve<crate::api::Args> for ComposeUp {
|
||||
mut replacers,
|
||||
} = self;
|
||||
|
||||
if !stack.config.swarm_id.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"This method should only be called for Compose Stacks. This is an internal error and should not happen."
|
||||
));
|
||||
}
|
||||
|
||||
let mut res = DeployStackResponse::default();
|
||||
|
||||
let mut interpolator =
|
||||
@@ -413,7 +407,7 @@ impl Resolve<crate::api::Args> for ComposeUp {
|
||||
.push_logs(&mut res.logs);
|
||||
replacers.extend(interpolator.secret_replacers);
|
||||
|
||||
let (run_directory, env_file_path) = match write::stack(
|
||||
let (run_directory, env_file_path) = match write_stack(
|
||||
&stack,
|
||||
repo.as_ref(),
|
||||
git_token,
|
||||
@@ -451,7 +445,7 @@ impl Resolve<crate::api::Args> for ComposeUp {
|
||||
if !stack.config.pre_deploy.is_none() {
|
||||
let pre_deploy_path =
|
||||
run_directory.join(&stack.config.pre_deploy.path);
|
||||
let span = info_span!("RunPreDeploy");
|
||||
let span = info_span!("ExecutePreDeploy");
|
||||
if let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Pre Deploy",
|
||||
pre_deploy_path.as_path(),
|
||||
@@ -562,7 +556,7 @@ impl Resolve<crate::api::Args> for ComposeUp {
|
||||
let command = format!(
|
||||
"{docker_compose} -p {project_name} -f {file_args}{env_file_args} build{build_extra_args}{service_args}",
|
||||
);
|
||||
let span = info_span!("RunComposeBuild");
|
||||
let span = info_span!("ExecuteComposeBuild");
|
||||
let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Compose Build",
|
||||
run_directory.as_path(),
|
||||
@@ -606,11 +600,11 @@ impl Resolve<crate::api::Args> for ComposeUp {
|
||||
// Also check if project name changed, which also requires taking down.
|
||||
|| last_project_name != project_name
|
||||
{
|
||||
// Take down the existing containers.
|
||||
// Take down the existing compose stack.
|
||||
// This one tries to use the previously deployed service name, to ensure the right stack is taken down.
|
||||
helpers::compose_down(&last_project_name, &services, &mut res)
|
||||
compose_down(&last_project_name, &services, &mut res)
|
||||
.await
|
||||
.context("failed to destroy existing containers")?;
|
||||
.context("Failed to take down existing compose stack")?;
|
||||
}
|
||||
|
||||
// Run compose up
|
||||
@@ -634,7 +628,7 @@ impl Resolve<crate::api::Args> for ComposeUp {
|
||||
compose_cmd_wrapper.replace("[[COMPOSE_COMMAND]]", &command);
|
||||
}
|
||||
|
||||
let span = info_span!("RunComposeUp");
|
||||
let span = info_span!("ExecuteComposeUp");
|
||||
let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Compose Up",
|
||||
run_directory.as_path(),
|
||||
@@ -654,7 +648,7 @@ impl Resolve<crate::api::Args> for ComposeUp {
|
||||
if res.deployed && !stack.config.post_deploy.is_none() {
|
||||
let post_deploy_path =
|
||||
run_directory.join(&stack.config.post_deploy.path);
|
||||
let span = info_span!("RunPostDeploy");
|
||||
let span = info_span!("ExecutePostDeploy");
|
||||
if let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Post Deploy",
|
||||
post_deploy_path.as_path(),
|
||||
@@ -747,7 +741,7 @@ impl Resolve<crate::api::Args> for ComposeRun {
|
||||
replacers.extend(interpolator.secret_replacers);
|
||||
|
||||
let mut res = ComposeRunResponse::default();
|
||||
let (run_directory, env_file_path) = match write::stack(
|
||||
let (run_directory, env_file_path) = match write_stack(
|
||||
&stack,
|
||||
repo.as_ref(),
|
||||
git_token,
|
||||
@@ -864,3 +858,59 @@ impl Resolve<crate::api::Args> for ComposeRun {
|
||||
Ok(log)
|
||||
}
|
||||
}
|
||||
|
||||
fn env_file_args(
|
||||
env_file_path: Option<&str>,
|
||||
additional_env_files: &[AdditionalEnvFile],
|
||||
) -> anyhow::Result<String> {
|
||||
let mut res = String::new();
|
||||
|
||||
// Add additional env files (except komodo's own, which comes last)
|
||||
for file in additional_env_files
|
||||
.iter()
|
||||
.filter(|f| env_file_path != Some(f.path.as_str()))
|
||||
{
|
||||
let path = &file.path;
|
||||
write!(res, " --env-file {path}").with_context(|| {
|
||||
format!("Failed to write --env-file arg for {path}")
|
||||
})?;
|
||||
}
|
||||
|
||||
// Add komodo's env file last for highest priority
|
||||
if let Some(file) = env_file_path {
|
||||
write!(res, " --env-file {file}").with_context(|| {
|
||||
format!("Failed to write --env-file arg for {file}")
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[instrument("ComposeDown", skip(res))]
|
||||
async fn compose_down(
|
||||
project: &str,
|
||||
services: &[String],
|
||||
res: &mut DeployStackResponse,
|
||||
) -> anyhow::Result<()> {
|
||||
let docker_compose = docker_compose();
|
||||
let service_args = if services.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" {}", services.join(" "))
|
||||
};
|
||||
let log = run_komodo_standard_command(
|
||||
"Compose Down",
|
||||
None,
|
||||
format!("{docker_compose} -p {project} down{service_args}"),
|
||||
)
|
||||
.await;
|
||||
let success = log.success;
|
||||
res.logs.push(log);
|
||||
if !success {
|
||||
return Err(anyhow!(
|
||||
"Failed to bring down existing container(s) with docker compose down. Stopping run."
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,13 +1,32 @@
|
||||
use command::run_komodo_standard_command;
|
||||
use komodo_client::entities::{
|
||||
docker::stack::SwarmStack, update::Log,
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use command::{
|
||||
KomodoCommandMode, run_komodo_command_with_sanitization,
|
||||
run_komodo_standard_command,
|
||||
};
|
||||
use periphery_client::api::swarm::{
|
||||
DeploySwarmStack, InspectSwarmStack, RemoveSwarmStacks,
|
||||
use formatting::format_serror;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
entities::{
|
||||
all_logs_success,
|
||||
docker::stack::SwarmStack,
|
||||
stack::{ComposeFile, ComposeService, StackServiceNames},
|
||||
update::Log,
|
||||
},
|
||||
parsers::parse_multiline_command,
|
||||
};
|
||||
use periphery_client::api::{
|
||||
DeployStackResponse,
|
||||
swarm::{DeploySwarmStack, InspectSwarmStack, RemoveSwarmStacks},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use tracing::Instrument as _;
|
||||
|
||||
use crate::docker::stack::inspect_swarm_stack;
|
||||
use crate::{
|
||||
config::periphery_config,
|
||||
docker::stack::inspect_swarm_stack,
|
||||
helpers::push_extra_args,
|
||||
stack::{maybe_login_registry, validate_files, write::write_stack},
|
||||
};
|
||||
|
||||
impl Resolve<crate::api::Args> for InspectSwarmStack {
|
||||
async fn resolve(
|
||||
@@ -67,6 +86,221 @@ impl Resolve<crate::api::Args> for DeploySwarmStack {
|
||||
self,
|
||||
args: &crate::api::Args,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
todo!()
|
||||
let DeploySwarmStack {
|
||||
mut stack,
|
||||
repo,
|
||||
git_token,
|
||||
registry_token,
|
||||
mut replacers,
|
||||
} = self;
|
||||
|
||||
let mut res = DeployStackResponse::default();
|
||||
|
||||
let mut interpolator =
|
||||
Interpolator::new(None, &periphery_config().secrets);
|
||||
// Only interpolate Stack. Repo interpolation will be handled
|
||||
// by the CloneRepo / PullOrCloneRepo call.
|
||||
interpolator
|
||||
.interpolate_stack(&mut stack)?
|
||||
.push_logs(&mut res.logs);
|
||||
replacers.extend(interpolator.secret_replacers);
|
||||
|
||||
// Env files are not supported by docker stack deploy so are ignored.
|
||||
let (run_directory, _) = match write_stack(
|
||||
&stack,
|
||||
repo.as_ref(),
|
||||
git_token,
|
||||
replacers.clone(),
|
||||
&mut res,
|
||||
args,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
res
|
||||
.logs
|
||||
.push(Log::error("Write Stack", format_serror(&e.into())));
|
||||
return Ok(res);
|
||||
}
|
||||
};
|
||||
|
||||
// Canonicalize the path to ensure it exists, and is the cleanest path to the run directory.
|
||||
let run_directory = run_directory.canonicalize().context(
|
||||
"Failed to validate run directory on host after stack write (canonicalize error)",
|
||||
)?;
|
||||
|
||||
validate_files(&stack, &run_directory, &mut res).await;
|
||||
if !all_logs_success(&res.logs) {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
let use_with_registry_auth =
|
||||
maybe_login_registry(&stack, registry_token, &mut res.logs)
|
||||
.await;
|
||||
if !all_logs_success(&res.logs) {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
// Pre deploy
|
||||
if !stack.config.pre_deploy.is_none() {
|
||||
let pre_deploy_path =
|
||||
run_directory.join(&stack.config.pre_deploy.path);
|
||||
let span = info_span!("ExecutePreDeploy");
|
||||
if let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Pre Deploy",
|
||||
pre_deploy_path.as_path(),
|
||||
&stack.config.pre_deploy.command,
|
||||
KomodoCommandMode::Multiline,
|
||||
&replacers,
|
||||
)
|
||||
.instrument(span)
|
||||
.await
|
||||
{
|
||||
res.logs.push(log);
|
||||
if !all_logs_success(&res.logs) {
|
||||
return Ok(res);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let file_args = stack.compose_file_paths().join(" -c ");
|
||||
|
||||
// This will be the last project name, which is the one that needs to be destroyed.
|
||||
// Might be different from the current project name, if user renames stack / changes to custom project name.
|
||||
let last_project_name = stack.project_name(false);
|
||||
let project_name = stack.project_name(true);
|
||||
|
||||
// Uses 'docker stack config' command to extract services (including image)
|
||||
// after performing interpolation
|
||||
{
|
||||
let command = format!("docker stack config -c {file_args}",);
|
||||
let span = info_span!("GetStackConfig", command);
|
||||
let Some(config_log) = run_komodo_command_with_sanitization(
|
||||
"Stack Config",
|
||||
run_directory.as_path(),
|
||||
command,
|
||||
KomodoCommandMode::Standard,
|
||||
&replacers,
|
||||
)
|
||||
.instrument(span)
|
||||
.await
|
||||
else {
|
||||
// Only reachable if command is empty,
|
||||
// not the case since it is provided above.
|
||||
unreachable!()
|
||||
};
|
||||
if !config_log.success {
|
||||
res.logs.push(config_log);
|
||||
return Ok(res);
|
||||
}
|
||||
let compose =
|
||||
serde_yaml_ng::from_str::<ComposeFile>(&config_log.stdout)
|
||||
.context("Failed to parse compose contents")?;
|
||||
// Store sanitized stack config output
|
||||
res.merged_config = Some(config_log.stdout);
|
||||
for (service_name, ComposeService { image, .. }) in
|
||||
compose.services
|
||||
{
|
||||
let image = image.unwrap_or_default();
|
||||
res.services.push(StackServiceNames {
|
||||
container_name: format!("{project_name}-{service_name}"),
|
||||
service_name,
|
||||
image,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if stack.config.destroy_before_deploy
|
||||
// Also check if project name changed, which also requires taking down.
|
||||
|| last_project_name != project_name
|
||||
{
|
||||
// Take down the existing stack.
|
||||
// This one tries to use the previously deployed project name, to ensure the right stack is taken down.
|
||||
remove_stack(&last_project_name, &mut res)
|
||||
.await
|
||||
.context("Failed to destroy existing stack")?;
|
||||
}
|
||||
|
||||
// Run stack deploy
|
||||
let mut command =
|
||||
format!("docker stack deploy --detach=false -c {file_args}");
|
||||
if use_with_registry_auth {
|
||||
command += " --with-registry-auth";
|
||||
}
|
||||
push_extra_args(&mut command, &stack.config.extra_args)?;
|
||||
|
||||
// Apply compose cmd wrapper if configured
|
||||
let compose_cmd_wrapper =
|
||||
parse_multiline_command(&stack.config.compose_cmd_wrapper);
|
||||
if !compose_cmd_wrapper.is_empty() {
|
||||
if !compose_cmd_wrapper.contains("[[COMPOSE_COMMAND]]") {
|
||||
res.logs.push(Log::error(
|
||||
"Compose Command Wrapper",
|
||||
"compose_cmd_wrapper is configured but does not contain [[COMPOSE_COMMAND]] placeholder. The placeholder is required to inject the compose command.".to_string(),
|
||||
));
|
||||
return Ok(res);
|
||||
}
|
||||
command =
|
||||
compose_cmd_wrapper.replace("[[COMPOSE_COMMAND]]", &command);
|
||||
}
|
||||
|
||||
let span = info_span!("ExecuteStackDeploy");
|
||||
let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Stack Deploy",
|
||||
run_directory.as_path(),
|
||||
command,
|
||||
KomodoCommandMode::Shell,
|
||||
&replacers,
|
||||
)
|
||||
.instrument(span)
|
||||
.await
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
res.deployed = log.success;
|
||||
res.logs.push(log);
|
||||
|
||||
if res.deployed && !stack.config.post_deploy.is_none() {
|
||||
let post_deploy_path =
|
||||
run_directory.join(&stack.config.post_deploy.path);
|
||||
let span = info_span!("ExecutePostDeploy");
|
||||
if let Some(log) = run_komodo_command_with_sanitization(
|
||||
"Post Deploy",
|
||||
post_deploy_path.as_path(),
|
||||
&stack.config.post_deploy.command,
|
||||
KomodoCommandMode::Multiline,
|
||||
&replacers,
|
||||
)
|
||||
.instrument(span)
|
||||
.await
|
||||
{
|
||||
res.logs.push(log);
|
||||
};
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument("RemoveStack", skip(res))]
|
||||
async fn remove_stack(
|
||||
stack: &str,
|
||||
res: &mut DeployStackResponse,
|
||||
) -> anyhow::Result<()> {
|
||||
let log = run_komodo_standard_command(
|
||||
"Remove Stack",
|
||||
None,
|
||||
format!("docker stack rm --detach=false {stack}"),
|
||||
)
|
||||
.await;
|
||||
let success = log.success;
|
||||
res.logs.push(log);
|
||||
if !success {
|
||||
return Err(anyhow!(
|
||||
"Failed to remove existing stack with docker stack rm. Stopping run."
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ mod config;
|
||||
mod connection;
|
||||
mod docker;
|
||||
mod helpers;
|
||||
mod stack;
|
||||
mod state;
|
||||
mod stats;
|
||||
mod terminal;
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
//! Module to handle common parts of deploying Compose and Swarm Stacks.
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use command::run_komodo_standard_command;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use formatting::format_serror;
|
||||
use komodo_client::entities::{
|
||||
FileContents, RepoExecutionArgs,
|
||||
repo::Repo,
|
||||
stack::{AdditionalEnvFile, Stack, StackRemoteFileContents},
|
||||
stack::{Stack, StackRemoteFileContents},
|
||||
to_path_compatible_name,
|
||||
update::Log,
|
||||
};
|
||||
use periphery_client::api::{
|
||||
DeployStackResponse, git::PullOrCloneRepo,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use tokio::fs;
|
||||
use resolver_api::Resolve as _;
|
||||
|
||||
use crate::{
|
||||
api::Args, config::periphery_config, docker::docker_login,
|
||||
};
|
||||
|
||||
use super::docker_compose;
|
||||
pub mod write;
|
||||
|
||||
#[instrument(
|
||||
"MaybeLoginRegistry",
|
||||
@@ -34,10 +31,11 @@ pub async fn maybe_login_registry(
|
||||
stack: &Stack,
|
||||
registry_token: Option<String>,
|
||||
logs: &mut Vec<Log>,
|
||||
) {
|
||||
) -> bool {
|
||||
if !stack.config.registry_provider.is_empty()
|
||||
&& !stack.config.registry_account.is_empty()
|
||||
&& let Err(e) = docker_login(
|
||||
{
|
||||
if let Err(e) = docker_login(
|
||||
&stack.config.registry_provider,
|
||||
&stack.config.registry_account,
|
||||
registry_token.as_deref(),
|
||||
@@ -50,68 +48,16 @@ pub async fn maybe_login_registry(
|
||||
)
|
||||
})
|
||||
.context("Failed to login to image registry")
|
||||
{
|
||||
logs.push(Log::error(
|
||||
"Login to Registry",
|
||||
format_serror(&e.into()),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn env_file_args(
|
||||
env_file_path: Option<&str>,
|
||||
additional_env_files: &[AdditionalEnvFile],
|
||||
) -> anyhow::Result<String> {
|
||||
let mut res = String::new();
|
||||
|
||||
// Add additional env files (except komodo's own, which comes last)
|
||||
for file in additional_env_files
|
||||
.iter()
|
||||
.filter(|f| env_file_path != Some(f.path.as_str()))
|
||||
{
|
||||
let path = &file.path;
|
||||
write!(res, " --env-file {path}").with_context(|| {
|
||||
format!("Failed to write --env-file arg for {path}")
|
||||
})?;
|
||||
}
|
||||
|
||||
// Add komodo's env file last for highest priority
|
||||
if let Some(file) = env_file_path {
|
||||
write!(res, " --env-file {file}").with_context(|| {
|
||||
format!("Failed to write --env-file arg for {file}")
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[instrument("ComposeDown", skip(res))]
|
||||
pub async fn compose_down(
|
||||
project: &str,
|
||||
services: &[String],
|
||||
res: &mut DeployStackResponse,
|
||||
) -> anyhow::Result<()> {
|
||||
let docker_compose = docker_compose();
|
||||
let service_args = if services.is_empty() {
|
||||
String::new()
|
||||
{
|
||||
logs.push(Log::error(
|
||||
"Login to Registry",
|
||||
format_serror(&e.into()),
|
||||
));
|
||||
}
|
||||
true
|
||||
} else {
|
||||
format!(" {}", services.join(" "))
|
||||
};
|
||||
let log = run_komodo_standard_command(
|
||||
"Compose Down",
|
||||
None,
|
||||
format!("{docker_compose} -p {project} down{service_args}"),
|
||||
)
|
||||
.await;
|
||||
let success = log.success;
|
||||
res.logs.push(log);
|
||||
if !success {
|
||||
return Err(anyhow!(
|
||||
"Failed to bring down existing container(s) with docker compose down. Stopping run."
|
||||
));
|
||||
false
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Only for git repo based Stacks.
|
||||
@@ -231,9 +177,9 @@ pub async fn validate_files(
|
||||
|
||||
for (full_path, file) in file_paths {
|
||||
let file_contents =
|
||||
match fs::read_to_string(&full_path).await.with_context(|| {
|
||||
format!("Failed to read file contents at {full_path:?}")
|
||||
}) {
|
||||
match tokio::fs::read_to_string(&full_path).await.with_context(
|
||||
|| format!("Failed to read file contents at {full_path:?}"),
|
||||
) {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
let error = format_serror(&e.into());
|
||||
@@ -61,7 +61,7 @@ impl WriteStackRes for &mut ComposeRunResponse {
|
||||
repo = repo.as_ref().map(|repo| &repo.name),
|
||||
)
|
||||
)]
|
||||
pub async fn stack<'a>(
|
||||
pub async fn write_stack<'a>(
|
||||
stack: &'a Stack,
|
||||
repo: Option<&Repo>,
|
||||
git_token: Option<String>,
|
||||
@@ -168,6 +168,9 @@ pub struct DeploymentConfig {
|
||||
/// Extra args which are interpolated into the
|
||||
/// `docker run` / `docker service create` command,
|
||||
/// and affect the container configuration.
|
||||
///
|
||||
/// - Container ref: https://docs.docker.com/reference/cli/docker/container/run/#options
|
||||
/// - Swarm Service ref: https://docs.docker.com/reference/cli/docker/service/create/#options
|
||||
#[serde(default, deserialize_with = "string_list_deserializer")]
|
||||
#[partial_attr(serde(
|
||||
default,
|
||||
|
||||
@@ -521,7 +521,7 @@ pub struct StackConfig {
|
||||
/// The extra arguments to pass to the deploy command.
|
||||
///
|
||||
/// - For Compose stack, uses `docker compose up -d [EXTRA_ARGS]`.
|
||||
/// - For Swarm mode. `docker stack deploy [OPTIONS] STACK`
|
||||
/// - For Swarm mode. `docker stack deploy [EXTRA_ARGS] STACK_NAME`
|
||||
///
|
||||
/// If empty, no extra arguments will be passed.
|
||||
#[serde(default, deserialize_with = "string_list_deserializer")]
|
||||
|
||||
Reference in New Issue
Block a user