docker-stack 2.3.0__py3-none-any.whl → 2.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docker_stack/cli.py +178 -6
- docker_stack/helpers.py +8 -4
- docker_stack-2.3.2.dist-info/METADATA +392 -0
- {docker_stack-2.3.0.dist-info → docker_stack-2.3.2.dist-info}/RECORD +7 -7
- docker_stack-2.3.0.dist-info/METADATA +0 -411
- {docker_stack-2.3.0.dist-info → docker_stack-2.3.2.dist-info}/WHEEL +0 -0
- {docker_stack-2.3.0.dist-info → docker_stack-2.3.2.dist-info}/entry_points.txt +0 -0
- {docker_stack-2.3.0.dist-info → docker_stack-2.3.2.dist-info}/top_level.txt +0 -0
docker_stack/cli.py
CHANGED
|
@@ -8,7 +8,7 @@ import shutil
|
|
|
8
8
|
import sys
|
|
9
9
|
import textwrap
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Dict, List, Optional, Tuple
|
|
11
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
12
12
|
import os
|
|
13
13
|
import yaml
|
|
14
14
|
import json
|
|
@@ -703,6 +703,143 @@ def load_env_file(env_file: str, base_env: Optional[Dict[str, str]] = None, max_
|
|
|
703
703
|
return _resolve_env_entries(entries, env_file, base_env=base_env, max_cycles=max_cycles)
|
|
704
704
|
|
|
705
705
|
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
_ENV_REF_PATTERN = re.compile(r"\$\{([^}:\s]+)(?::-(.*?))?\}|\$([a-zA-Z_][a-zA-Z0-9_]*)")
|
|
709
|
+
_ESCAPED_DOLLAR = "$_ESCAPED_DOLLAR_"
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _escape_dollars(text: str) -> str:
|
|
713
|
+
"""Marks text as a literal payload for the manager, which collapses `$$`."""
|
|
714
|
+
return text.replace("$", "$$")
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _substitute_selected_vars(text: str, names: Set[str], replacements: Dict[str, str]) -> str:
|
|
718
|
+
"""Expands only `names`, leaving every other `${VAR}` for the manager."""
|
|
719
|
+
if not names:
|
|
720
|
+
return text
|
|
721
|
+
guarded = text.replace("$$", _ESCAPED_DOLLAR)
|
|
722
|
+
|
|
723
|
+
def replace(match: "re.Match[str]") -> str:
|
|
724
|
+
var = match.group(1) if match.group(1) is not None else match.group(3)
|
|
725
|
+
if var not in names:
|
|
726
|
+
return match.group(0)
|
|
727
|
+
value = os.environ.get(var)
|
|
728
|
+
if value in (None, "") and match.group(2) is not None:
|
|
729
|
+
value = match.group(2)
|
|
730
|
+
if value in (None, ""):
|
|
731
|
+
return match.group(0)
|
|
732
|
+
for old, new in replacements.items():
|
|
733
|
+
value = value.replace(old, new)
|
|
734
|
+
return value
|
|
735
|
+
|
|
736
|
+
return _ENV_REF_PATTERN.sub(replace, guarded).replace(_ESCAPED_DOLLAR, "$$")
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _substitute_selected_in_yaml_tree(value, names: Set[str], replacements: Dict[str, str]):
|
|
740
|
+
if isinstance(value, str):
|
|
741
|
+
return _substitute_selected_vars(value, names, replacements)
|
|
742
|
+
if isinstance(value, dict):
|
|
743
|
+
return {
|
|
744
|
+
_substitute_selected_in_yaml_tree(key, names, replacements): _substitute_selected_in_yaml_tree(item, names, replacements)
|
|
745
|
+
for key, item in value.items()
|
|
746
|
+
}
|
|
747
|
+
if isinstance(value, list):
|
|
748
|
+
return [_substitute_selected_in_yaml_tree(item, names, replacements) for item in value]
|
|
749
|
+
return value
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _mark_inline_secret_content_literal(compose_data) -> None:
|
|
753
|
+
"""Protects inlined secret payloads from the manager's interpolation.
|
|
754
|
+
|
|
755
|
+
`x-content` is a payload, not a template: the file it came from was already
|
|
756
|
+
rendered locally when it was declared as `x-template-file`, and a `file:` or
|
|
757
|
+
`environment:` secret is verbatim by definition. Without this a `$` inside a
|
|
758
|
+
secret is read as a variable reference and the secret is silently corrupted
|
|
759
|
+
or the deploy fails on a variable the payload never meant to name.
|
|
760
|
+
"""
|
|
761
|
+
if not isinstance(compose_data, dict):
|
|
762
|
+
return
|
|
763
|
+
secrets = compose_data.get("secrets")
|
|
764
|
+
if not isinstance(secrets, dict):
|
|
765
|
+
return
|
|
766
|
+
for details in secrets.values():
|
|
767
|
+
if isinstance(details, dict) and isinstance(details.get("x-content"), str):
|
|
768
|
+
details["x-content"] = _escape_dollars(details["x-content"])
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _render_manager_compose(compose_data, secret_env_vars: Set[str], replacements: Dict[str, str]) -> str:
|
|
772
|
+
"""Renders the compose a manager-backed deploy sends.
|
|
773
|
+
|
|
774
|
+
The manager owns interpolation: it receives the authored compose and the
|
|
775
|
+
`.env` holding the values, and it substitutes in the parsed document. So
|
|
776
|
+
variables are left standing here rather than rendered twice, which is what
|
|
777
|
+
the `$` doubling in `replacements` exists to undo. Only the variables the
|
|
778
|
+
manager cannot see are resolved locally - a secret sourced from the
|
|
779
|
+
environment is deliberately kept out of `.env` so its value is not stored
|
|
780
|
+
with the stack.
|
|
781
|
+
|
|
782
|
+
References are still checked here so a missing variable is reported against
|
|
783
|
+
the authored file, with its line and surrounding context, instead of coming
|
|
784
|
+
back as a deploy failure.
|
|
785
|
+
"""
|
|
786
|
+
_mark_inline_secret_content_literal(compose_data)
|
|
787
|
+
missing: List[str] = []
|
|
788
|
+
_substitute_in_yaml_tree(compose_data, replacements, missing)
|
|
789
|
+
if missing:
|
|
790
|
+
_report_missing_variables(compose_data, replacements, missing)
|
|
791
|
+
return yaml.dump(_substitute_selected_in_yaml_tree(compose_data, secret_env_vars, replacements), sort_keys=False)
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _substitute_in_yaml_tree(value, replacements: Dict[str, str], missing: List[str]):
|
|
795
|
+
"""Expands `${VAR}` inside the scalars of a parsed compose document.
|
|
796
|
+
|
|
797
|
+
Substituting in the serialized text instead would let a variable's value
|
|
798
|
+
change the structure of the document. `KEY: ${JSON}` holding `{"a":"b"}`
|
|
799
|
+
reparses as a nested mapping, which the manager rejects as a non-scalar
|
|
800
|
+
environment value, and a value containing `: ` makes the document fail to
|
|
801
|
+
parse at all. Walking the tree keeps every substituted value a scalar,
|
|
802
|
+
whatever characters it contains.
|
|
803
|
+
"""
|
|
804
|
+
if isinstance(value, str):
|
|
805
|
+
try:
|
|
806
|
+
return envsubst(value, replacements=replacements, on_error="throw")
|
|
807
|
+
except SubstitutionError as error:
|
|
808
|
+
missing.extend(result.variable_name for result in error.results if result.has_error)
|
|
809
|
+
return value
|
|
810
|
+
if isinstance(value, dict):
|
|
811
|
+
return {
|
|
812
|
+
_substitute_in_yaml_tree(key, replacements, missing): _substitute_in_yaml_tree(item, replacements, missing)
|
|
813
|
+
for key, item in value.items()
|
|
814
|
+
}
|
|
815
|
+
if isinstance(value, list):
|
|
816
|
+
return [_substitute_in_yaml_tree(item, replacements, missing) for item in value]
|
|
817
|
+
return value
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _render_compose_substitutions(compose_data, replacements: Dict[str, str]) -> str:
|
|
821
|
+
missing: List[str] = []
|
|
822
|
+
substituted = _substitute_in_yaml_tree(compose_data, replacements, missing)
|
|
823
|
+
if missing:
|
|
824
|
+
_report_missing_variables(compose_data, replacements, missing)
|
|
825
|
+
return yaml.dump(substituted, sort_keys=False)
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def _report_missing_variables(compose_data, replacements: Dict[str, str], missing: List[str]) -> None:
|
|
829
|
+
"""Reports unresolved variables against the whole document.
|
|
830
|
+
|
|
831
|
+
Substitution happens scalar by scalar, which has no line numbers to report.
|
|
832
|
+
Re-running over the serialized document restores the line and context in the
|
|
833
|
+
error, and exits the same way it always has.
|
|
834
|
+
"""
|
|
835
|
+
document = yaml.dump(compose_data, sort_keys=False)
|
|
836
|
+
envsubst(document, replacements=replacements)
|
|
837
|
+
raise SubstitutionError(
|
|
838
|
+
[LineCheckResult(line_no=1, line_content="", variable_name=name, start_index=0) for name in missing],
|
|
839
|
+
document,
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
|
|
706
843
|
class Docker:
|
|
707
844
|
def __init__(self, registries: List[str] = []):
|
|
708
845
|
self.stack = DockerStack(self)
|
|
@@ -791,7 +928,27 @@ class DockerStack:
|
|
|
791
928
|
def _relative_source_name(path: Path, base_dir: Path) -> str:
|
|
792
929
|
return os.path.relpath(path, base_dir).replace(os.sep, "/")
|
|
793
930
|
|
|
794
|
-
|
|
931
|
+
@staticmethod
|
|
932
|
+
def _config_environment_vars(compose_data: dict) -> set:
|
|
933
|
+
"""Variables named by `configs.<name>.environment`.
|
|
934
|
+
|
|
935
|
+
These are not `${VAR}` references, so scanning the compose text does not
|
|
936
|
+
find them, yet the manager resolves them from the stack `.env` like any
|
|
937
|
+
other variable. Secrets are deliberately absent: their value is inlined
|
|
938
|
+
locally so it is never stored alongside the stack.
|
|
939
|
+
"""
|
|
940
|
+
config_env_vars = set()
|
|
941
|
+
configs = compose_data.get("configs", {})
|
|
942
|
+
if not isinstance(configs, dict):
|
|
943
|
+
return config_env_vars
|
|
944
|
+
for details in configs.values():
|
|
945
|
+
if isinstance(details, dict) and "environment" in details:
|
|
946
|
+
env_name = str(details["environment"]).strip()
|
|
947
|
+
if env_name:
|
|
948
|
+
config_env_vars.add(env_name)
|
|
949
|
+
return config_env_vars
|
|
950
|
+
|
|
951
|
+
def _build_env_xfile(self, sources: List[str], excluded_names: set, extra_names: Optional[Set[str]] = None) -> Optional[Dict[str, str]]:
|
|
795
952
|
seen = set()
|
|
796
953
|
lines = []
|
|
797
954
|
for source in sources:
|
|
@@ -805,6 +962,14 @@ class DockerStack:
|
|
|
805
962
|
continue
|
|
806
963
|
seen.add(name)
|
|
807
964
|
lines.append(f"{name}={value}")
|
|
965
|
+
for name in sorted(extra_names or ()):
|
|
966
|
+
if name in seen or name in excluded_names:
|
|
967
|
+
continue
|
|
968
|
+
value = os.environ.get(name)
|
|
969
|
+
if value in (None, ""):
|
|
970
|
+
continue
|
|
971
|
+
seen.add(name)
|
|
972
|
+
lines.append(f"{name}={value}")
|
|
808
973
|
if not lines:
|
|
809
974
|
return None
|
|
810
975
|
content = ("\n".join(lines) + "\n").encode("utf-8")
|
|
@@ -912,12 +1077,19 @@ class DockerStack:
|
|
|
912
1077
|
|
|
913
1078
|
# Define the replacements for '$' to '$$' for env variables in compose files
|
|
914
1079
|
replacements_map = {"$": "$$"}
|
|
915
|
-
|
|
1080
|
+
if manager_deploy:
|
|
1081
|
+
clean_content = _render_manager_compose(compose_data, secret_env_vars, replacements_map)
|
|
1082
|
+
else:
|
|
1083
|
+
clean_content = _render_compose_substitutions(compose_data, replacements_map)
|
|
916
1084
|
enriched_data = yaml.safe_load(clean_content) or {}
|
|
917
1085
|
x_files = [
|
|
918
1086
|
{"path": "compose.yml", "content": self._b64_content(template_bytes)},
|
|
919
1087
|
]
|
|
920
|
-
env_xfile = self._build_env_xfile(
|
|
1088
|
+
env_xfile = self._build_env_xfile(
|
|
1089
|
+
env_sources,
|
|
1090
|
+
secret_env_vars,
|
|
1091
|
+
extra_names=self._config_environment_vars(compose_data) if manager_deploy else None,
|
|
1092
|
+
)
|
|
921
1093
|
if env_xfile:
|
|
922
1094
|
x_files.append(env_xfile)
|
|
923
1095
|
x_files.extend(source_x_files)
|
|
@@ -2099,14 +2271,14 @@ def _run(args: List[str] = None):
|
|
|
2099
2271
|
|
|
2100
2272
|
checkout_parser = subparsers.add_parser("checkout", help="Deploy specific version of the stack")
|
|
2101
2273
|
checkout_parser.add_argument("stack_name", help="Name of the stack")
|
|
2102
|
-
checkout_parser.add_argument("version", help="Stack version to
|
|
2274
|
+
checkout_parser.add_argument("version", help="Stack version to deploy")
|
|
2103
2275
|
_add_namespace_argument(checkout_parser)
|
|
2104
2276
|
|
|
2105
2277
|
# version_parser = subparsers.add_parser("version",help="Deploy specific version of the stack")
|
|
2106
2278
|
# version_parser.add_argument("stack_name", help="Name of the stack")
|
|
2107
2279
|
# version_parser.add_argument("version","versions", help="Stack version to cat")
|
|
2108
2280
|
|
|
2109
|
-
version_parser = subparsers.add_parser("version", aliases=["versions"], help="
|
|
2281
|
+
version_parser = subparsers.add_parser("version", aliases=["versions"], help="List recorded versions of the stack")
|
|
2110
2282
|
version_parser.add_argument("stack_name", help="Name of the stack")
|
|
2111
2283
|
_add_namespace_argument(version_parser)
|
|
2112
2284
|
|
docker_stack/helpers.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import secrets as _secrets
|
|
2
2
|
import string
|
|
3
3
|
import sys
|
|
4
4
|
from typing import Callable, Dict, List, Optional, Union
|
|
@@ -23,13 +23,17 @@ def generate_secret(
|
|
|
23
23
|
|
|
24
24
|
Returns:
|
|
25
25
|
A randomly generated secret string.
|
|
26
|
+
|
|
27
|
+
Note:
|
|
28
|
+
Uses the ``secrets`` module rather than ``random``, since these values
|
|
29
|
+
are used as passwords, API keys and tokens.
|
|
26
30
|
"""
|
|
27
31
|
if length is None:
|
|
28
|
-
length =
|
|
32
|
+
length = 12 + _secrets.randbelow(9) # 12..20 inclusive
|
|
29
33
|
|
|
30
34
|
# Always start with a letter (uppercase or lowercase)
|
|
31
35
|
first_char_pool = string.ascii_lowercase + string.ascii_uppercase
|
|
32
|
-
first_char =
|
|
36
|
+
first_char = _secrets.choice(first_char_pool)
|
|
33
37
|
|
|
34
38
|
# Prepare the character pool for the rest of the secret
|
|
35
39
|
characters = string.ascii_lowercase
|
|
@@ -47,7 +51,7 @@ def generate_secret(
|
|
|
47
51
|
raise ValueError("No character types selected for secret generation.")
|
|
48
52
|
|
|
49
53
|
# Generate the rest of the string
|
|
50
|
-
secret = [first_char] + [
|
|
54
|
+
secret = [first_char] + [_secrets.choice(characters) for _ in range(length - 1)]
|
|
51
55
|
|
|
52
56
|
return "".join(secret)
|
|
53
57
|
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: docker-stack
|
|
3
|
+
Version: 2.3.2
|
|
4
|
+
Summary: CLI for deploying and managing Docker stacks.
|
|
5
|
+
Home-page: https://github.com/mesudip/docker-stack
|
|
6
|
+
Author: Sudip Bhattarai
|
|
7
|
+
Author-email: sudip@bhattarai.me
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: PyYAML
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest<9,>=8; extra == "dev"
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: provides-extra
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
Dynamic: summary
|
|
26
|
+
|
|
27
|
+
# Docker Stack CLI Utility
|
|
28
|
+
|
|
29
|
+
A command-line tool for advanced Docker Swarm stack deployments on plain Docker daemons. `docker-stack` extends vanilla `docker stack deploy` with generated secrets, templated configs, versioned stack state, safer rollbacks, and better day-to-day stack workflows.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Install or upgrade `docker-stack` with:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install docker-stack --upgrade --break-system-packages
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### Plain Docker Daemon
|
|
42
|
+
|
|
43
|
+
If you already have a Docker Swarm daemon or Docker context, you can use the advanced stack features directly against it.
|
|
44
|
+
|
|
45
|
+
Typical daemon-only workflow:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
docker-stack deploy my-stack docker-compose.yml
|
|
49
|
+
docker-stack ls
|
|
50
|
+
docker-stack ls -n team-a
|
|
51
|
+
docker-stack ls -A
|
|
52
|
+
docker-stack versions my-stack
|
|
53
|
+
docker-stack cat my-stack
|
|
54
|
+
docker-stack checkout my-stack v2
|
|
55
|
+
docker-stack node ls
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
What this gives you on a raw Docker daemon:
|
|
59
|
+
|
|
60
|
+
- richer secret and config handling in Compose
|
|
61
|
+
- generated secrets without external scripts
|
|
62
|
+
- template expansion from env vars and files
|
|
63
|
+
- versioned stack config history
|
|
64
|
+
- stack version inspection and checkout
|
|
65
|
+
- raw daemon compatibility without extra infrastructure
|
|
66
|
+
|
|
67
|
+
### GitHub Actions
|
|
68
|
+
|
|
69
|
+
#### 1. Normal Docker daemon
|
|
70
|
+
|
|
71
|
+
Use this when the runner already has Docker access through the default Docker context or `DOCKER_HOST`.
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/checkout@v4
|
|
76
|
+
- uses: actions/setup-python@v6
|
|
77
|
+
with:
|
|
78
|
+
python-version: '3.x'
|
|
79
|
+
- run: python3 -m pip install --upgrade docker-stack
|
|
80
|
+
- run: docker-stack deploy my-stack docker-compose.yml
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Use this option when CI can connect directly to the target Docker daemon.
|
|
84
|
+
|
|
85
|
+
#### 2. Docker-Manager
|
|
86
|
+
|
|
87
|
+
Use the bundled action when deploying through
|
|
88
|
+
[Docker-Manager](https://github.com/mesudip/docker-enterprise). The manager
|
|
89
|
+
repository contains the server source, installation instructions, and deployment
|
|
90
|
+
documentation.
|
|
91
|
+
|
|
92
|
+
For a full compose deployment directly from CI, use the action to configure
|
|
93
|
+
Docker-Manager authentication and then run the normal `docker-stack deploy`
|
|
94
|
+
command:
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
permissions:
|
|
98
|
+
contents: read
|
|
99
|
+
id-token: write
|
|
100
|
+
|
|
101
|
+
steps:
|
|
102
|
+
- uses: actions/checkout@v4
|
|
103
|
+
- uses: mesudip/docker-stack@v2
|
|
104
|
+
with:
|
|
105
|
+
manager: https://manager.example.com:2378
|
|
106
|
+
- run: docker-stack deploy --namespace team-a --with-registry-auth my-stack docker-compose.yml
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
You can also deploy the full compose file through action inputs:
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
permissions:
|
|
113
|
+
contents: read
|
|
114
|
+
id-token: write
|
|
115
|
+
|
|
116
|
+
steps:
|
|
117
|
+
- uses: actions/checkout@v4
|
|
118
|
+
- uses: mesudip/docker-stack@v2
|
|
119
|
+
with:
|
|
120
|
+
manager: https://manager.example.com:2378
|
|
121
|
+
stack: my-stack
|
|
122
|
+
compose-file: docker-compose.yml
|
|
123
|
+
namespace: team-a
|
|
124
|
+
with-registry-auth: "true"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
To release new service images without submitting the compose file again:
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
permissions:
|
|
131
|
+
contents: read
|
|
132
|
+
id-token: write
|
|
133
|
+
|
|
134
|
+
steps:
|
|
135
|
+
- uses: actions/checkout@v4
|
|
136
|
+
- uses: mesudip/docker-stack@v2
|
|
137
|
+
with:
|
|
138
|
+
manager: https://manager.example.com:2378
|
|
139
|
+
stack: my-stack
|
|
140
|
+
namespace: team-a
|
|
141
|
+
with-registry-auth: "true"
|
|
142
|
+
images: |
|
|
143
|
+
api=ghcr.io/acme/api:${{ github.sha }}
|
|
144
|
+
worker=ghcr.io/acme/worker:${{ github.sha }}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Use a full compose deployment when the workflow owns the complete stack
|
|
148
|
+
definition. Use an image-only deployment when the stack is already managed and
|
|
149
|
+
the workflow only needs to release new images. Both forms support namespaces;
|
|
150
|
+
the namespace defaults to `default` when omitted.
|
|
151
|
+
|
|
152
|
+
### Authenticated Docker-Manager shell
|
|
153
|
+
|
|
154
|
+
Open an isolated Bash or Zsh session for a manager context:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
docker-stack shell office
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
If `office` does not exist yet, the CLI asks for its Docker-Manager URL, creates
|
|
161
|
+
the context, authenticates, and opens the shell. You can also provide everything
|
|
162
|
+
non-interactively with `docker-stack shell --context office <manager-url>`.
|
|
163
|
+
|
|
164
|
+
Stack commands use the `default` namespace unless `-n/--namespace` is supplied.
|
|
165
|
+
Listings print the selected namespace; `docker-stack ls -A` (or
|
|
166
|
+
`--all-namespaces`) lists every visible namespace.
|
|
167
|
+
|
|
168
|
+
The prompt displays `(docker:office@cluster)`, keeps the selected manager context active,
|
|
169
|
+
and refreshes authentication when needed. The session supports `docker` and
|
|
170
|
+
`docker compose`; legacy `docker-compose` is not supported. If authentication
|
|
171
|
+
expires, run `docker-stack login` again.
|
|
172
|
+
|
|
173
|
+
Container listing is cluster-aware and includes the owning Swarm node. Select a
|
|
174
|
+
node when working with daemon-local resources such as volumes and images:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
docker ps
|
|
178
|
+
docker-stack node current
|
|
179
|
+
docker-stack node use worker-02
|
|
180
|
+
docker volume ls
|
|
181
|
+
docker image ls
|
|
182
|
+
docker-stack node use cluster
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Node selection is stored only in the managed shell's isolated Docker
|
|
186
|
+
configuration and appears in the prompt as `(docker:office@worker-02)`.
|
|
187
|
+
Selected-node requests remain subject to the manager's Docker permissions;
|
|
188
|
+
image management is root-only because the manager does not define delegated
|
|
189
|
+
image permissions.
|
|
190
|
+
|
|
191
|
+
Cluster-aware output is enabled after the manager has observed compatible
|
|
192
|
+
agents on every discovered node. The manager remembers that capability until it
|
|
193
|
+
restarts, so a temporary agent outage does not make the CLI revert to a legacy
|
|
194
|
+
feature decision. A partial cluster listing prints the available containers,
|
|
195
|
+
reports each failed node on stderr with its incident id, and exits non-zero.
|
|
196
|
+
|
|
197
|
+
`docker ps --quiet` and `docker ps --format ...` use Docker's native formatter
|
|
198
|
+
and therefore do not add the `NODE` column. Explicit Docker overrides such as
|
|
199
|
+
`--context`, `--host`/`-H`, and `--config` bypass managed cluster formatting and
|
|
200
|
+
are sent unchanged to the Docker CLI.
|
|
201
|
+
|
|
202
|
+
## What It Adds
|
|
203
|
+
|
|
204
|
+
Beyond `docker stack deploy`, against a plain Docker daemon:
|
|
205
|
+
|
|
206
|
+
- **Generated secrets** — no external scripts; see [Object Versioning and Reuse](#object-versioning-and-reuse) for the once-only semantics.
|
|
207
|
+
- **Inline and templated configs/secrets** — content in the compose file, or expanded from environment variables and files.
|
|
208
|
+
- **Automatic versioning** — configs and secrets are content-hashed and versioned, so edits do not require hand-written `_v2` names.
|
|
209
|
+
- **Version inspection and checkout** — `versions`, `cat`, and `checkout` restore a complete recorded stack version, including its configs.
|
|
210
|
+
- **Cluster-aware inspection** — stack and node output that reports the owning Swarm node.
|
|
211
|
+
|
|
212
|
+
### Concurrent deploys (Docker-Manager)
|
|
213
|
+
|
|
214
|
+
Docker-Manager runs one apply per stack at a time. When `docker-stack deploy`
|
|
215
|
+
or `docker-stack checkout` finds another run in progress (a UI deploy, a CI
|
|
216
|
+
image bump, another operator), it waits and prints who started it:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
[manager] waiting: a deploy started by alice 42s ago is still running (waiting up to 300s, set DOCKER_MANAGER_DEPLOY_WAIT_SECS to change)
|
|
220
|
+
[manager] press Enter twice to force your deploy (aborts that run; changes it already made to the daemon stay), Ctrl+C to quit
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- It retries every 5 seconds until the stack is free or `DOCKER_MANAGER_DEPLOY_WAIT_SECS` runs out (default: the deploy timeout; `0` fails immediately). CI runs wait the same way, without the prompt.
|
|
224
|
+
- At a terminal, pressing Enter twice within 3 seconds asks the manager to abort the running deployment and deploys as soon as the stack is released. This needs stack deploy permission and is offered once per run. The aborted run stops orchestrating, but daemon requests it already made still complete; your deploy then applies over that state.
|
|
225
|
+
- Ctrl+C exits without touching the other run.
|
|
226
|
+
|
|
227
|
+
## Compose Extensions
|
|
228
|
+
|
|
229
|
+
`docker-stack` reads extra keys under top-level `configs:` and `secrets:` and
|
|
230
|
+
resolves them into real Docker objects before calling `docker stack deploy`.
|
|
231
|
+
Standard Compose keys (`file:`, `external:`, `name:`) continue to work.
|
|
232
|
+
|
|
233
|
+
| Key | Configs | Secrets | Content comes from |
|
|
234
|
+
| --- | --- | --- | --- |
|
|
235
|
+
| `file:` | yes | yes | the file, verbatim (standard Compose) |
|
|
236
|
+
| `x-content` | yes | yes | a literal string in the compose file |
|
|
237
|
+
| `x-template` | yes | yes | a literal string, with `${VAR}` expanded |
|
|
238
|
+
| `x-template-file` | yes | yes | a file, with `${VAR}` expanded |
|
|
239
|
+
| `environment` | no | yes | the named environment variable |
|
|
240
|
+
| `x-generate` | no | yes | a value generated by `docker-stack` |
|
|
241
|
+
|
|
242
|
+
Exactly one content key per object.
|
|
243
|
+
|
|
244
|
+
### `x-content` — inline content
|
|
245
|
+
|
|
246
|
+
```yaml
|
|
247
|
+
secrets:
|
|
248
|
+
my_inline_secret:
|
|
249
|
+
x-content: "This is my secret content defined inline."
|
|
250
|
+
|
|
251
|
+
configs:
|
|
252
|
+
my_inline_config:
|
|
253
|
+
x-content: |
|
|
254
|
+
key=value
|
|
255
|
+
another_key=another_value
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### `x-template` and `x-template-file` — environment substitution
|
|
259
|
+
|
|
260
|
+
`${VAR}` references are expanded from the deploying shell's environment.
|
|
261
|
+
|
|
262
|
+
```yaml
|
|
263
|
+
secrets:
|
|
264
|
+
my_templated_secret:
|
|
265
|
+
x-template: "${API_KEY_NAME}:${MY_API_KEY}"
|
|
266
|
+
|
|
267
|
+
configs:
|
|
268
|
+
my_config_from_template_file:
|
|
269
|
+
x-template-file: "./templates/my_config.tpl"
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### `environment` — content from a variable
|
|
273
|
+
|
|
274
|
+
```yaml
|
|
275
|
+
secrets:
|
|
276
|
+
api_token:
|
|
277
|
+
environment: API_TOKEN
|
|
278
|
+
configs:
|
|
279
|
+
app_conf:
|
|
280
|
+
environment: APP_CONF_VALUE
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
If the variable is unset or empty, the deploy fails before any Docker object is
|
|
284
|
+
created.
|
|
285
|
+
|
|
286
|
+
Configs are supported on Docker-Manager deploys, where the value travels in the
|
|
287
|
+
stack `.env` and the manager resolves it. A raw daemon deploy supports secrets
|
|
288
|
+
only. The two are handled differently on purpose: a secret's value is inlined
|
|
289
|
+
locally so it is never written to the `.env` that is stored with the stack.
|
|
290
|
+
|
|
291
|
+
### `x-generate` — generated secrets
|
|
292
|
+
|
|
293
|
+
Secrets only. Configs must use `x-content`, `x-template`, or `x-template-file`.
|
|
294
|
+
|
|
295
|
+
```yaml
|
|
296
|
+
secrets:
|
|
297
|
+
# default options, random length 12-20
|
|
298
|
+
simple:
|
|
299
|
+
x-generate: true
|
|
300
|
+
|
|
301
|
+
# fixed length, default character classes
|
|
302
|
+
fixed_length:
|
|
303
|
+
x-generate: 30
|
|
304
|
+
|
|
305
|
+
# explicit character classes
|
|
306
|
+
api_token:
|
|
307
|
+
x-generate:
|
|
308
|
+
length: 40
|
|
309
|
+
numbers: true # digits 0-9 (default true)
|
|
310
|
+
special: true # punctuation (default true)
|
|
311
|
+
uppercase: true # A-Z (default true)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Lowercase letters are always included and the value always starts with a
|
|
315
|
+
letter.
|
|
316
|
+
|
|
317
|
+
`special: true` leaves out `$`, `'`, `"` and `\`, so generated values are safe
|
|
318
|
+
to paste into a shell or a YAML file. Use `special: false` when the application
|
|
319
|
+
rejects punctuation, or for values that go in URLs or HTTP headers:
|
|
320
|
+
|
|
321
|
+
```yaml
|
|
322
|
+
secrets:
|
|
323
|
+
bearer_token:
|
|
324
|
+
x-generate:
|
|
325
|
+
length: 64
|
|
326
|
+
numbers: true
|
|
327
|
+
special: false
|
|
328
|
+
uppercase: true
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Versioning and Reuse
|
|
332
|
+
|
|
333
|
+
Docker configs and secrets cannot be changed in place. `docker-stack` handles
|
|
334
|
+
that for you: when a config's content changes, it creates a new version and
|
|
335
|
+
points your services at it. You never have to add `_v2` to names yourself.
|
|
336
|
+
|
|
337
|
+
**Generated secrets are created once.** Redeploying does not change them, so
|
|
338
|
+
anything holding a generated value keeps working. The value survives
|
|
339
|
+
redeploys, restarts and unrelated changes to the stack.
|
|
340
|
+
|
|
341
|
+
### Stored source metadata
|
|
342
|
+
|
|
343
|
+
Versioned stack configs include a top-level `x-files` list holding
|
|
344
|
+
base64-encoded source material for recovery and auditing: the original compose
|
|
345
|
+
file as `compose.yml`, a generated `.env` containing referenced non-secret
|
|
346
|
+
environment values, and any config files referenced by `configs.*.file` or
|
|
347
|
+
`configs.*.x-template-file`.
|
|
348
|
+
|
|
349
|
+
Secret source files, and the variables named by `secrets.*.environment`, are
|
|
350
|
+
deliberately **not** stored in `x-files`.
|
|
351
|
+
|
|
352
|
+
### Rotating a generated secret
|
|
353
|
+
|
|
354
|
+
There is no rotate flag. Remove the secret's newest version, then deploy again
|
|
355
|
+
to get a fresh value:
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
docker secret rm <name>_v<N>
|
|
359
|
+
docker-stack deploy --show-generated my-stack docker-compose.yml
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`--show-generated` prints the new value. Update anything still holding the old
|
|
363
|
+
one.
|
|
364
|
+
|
|
365
|
+
### Secrets you created yourself
|
|
366
|
+
|
|
367
|
+
If a secret already exists because you ran `docker secret create`, pointing
|
|
368
|
+
`x-generate` at it does **not** take it over. The deploy generates a new value
|
|
369
|
+
instead, and anything holding the old one stops working.
|
|
370
|
+
|
|
371
|
+
Either leave it as `external: true`, or move to `x-generate` deliberately:
|
|
372
|
+
deploy once with `--show-generated`, then update your clients. After that it
|
|
373
|
+
behaves like any other generated secret.
|
|
374
|
+
|
|
375
|
+
## Known Limitations
|
|
376
|
+
|
|
377
|
+
Docker limits config content to 500 KB. Stack history includes encoded source
|
|
378
|
+
files, so stacks with large compose or config files can exceed that limit.
|
|
379
|
+
|
|
380
|
+
## Development
|
|
381
|
+
|
|
382
|
+
Install runtime and test dependencies with either:
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
python3 -m pip install -r requirements-dev.txt
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
or:
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
python3 -m pip install -e '.[dev]'
|
|
392
|
+
```
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
docker_stack/__init__.py,sha256=qgHdW8ZDBlyk6FzIf8Mld_cJD1SjOfqo2dELg4pK668,880
|
|
2
|
-
docker_stack/cli.py,sha256=
|
|
2
|
+
docker_stack/cli.py,sha256=4l08fHaQurX0A3cRcoNx2oBlx4YFRSeuL_AV9y4SzGk,108003
|
|
3
3
|
docker_stack/command_runner.py,sha256=mNaUAVKtrJbji_5ARVPLKhc92avqllNKrVQj1A6S0dA,1252
|
|
4
4
|
docker_stack/compose.py,sha256=_fAVesjyW5ecid46sYYhcu02yF1vqknDSCFYflJPDOE,534
|
|
5
5
|
docker_stack/conflict_prompt.py,sha256=1pmsyJZyZbzKYAtLJ8kYNVSC0LJb6fsu8Qz2zWvyqBE,4911
|
|
6
6
|
docker_stack/docker_objects.py,sha256=U6szytv5gKrmBYKqThSeMMXdiJ2u4STIQLkeqQ-Fn4Q,12103
|
|
7
7
|
docker_stack/envsubst.py,sha256=gdEUpsjWJeNbbahySk56vUdQh1nns85o3LbU6SUBo3I,8337
|
|
8
8
|
docker_stack/envsubst_merge.py,sha256=TwOu8BducG0rWnSreJGwesNi-VghCetljcf-aWceE8M,4971
|
|
9
|
-
docker_stack/helpers.py,sha256=
|
|
9
|
+
docker_stack/helpers.py,sha256=ppN3VBf-OYGoXhIpJrNzyAFrAJVJR5N-oNQvpeJdg-g,6136
|
|
10
10
|
docker_stack/login.py,sha256=jnrmrDh79Brlzj89YdZ1sGpg-ex16yb3ZN47eFpcW5g,41754
|
|
11
11
|
docker_stack/manager_api.py,sha256=9AnVzsbYDllo1NeQM5vq7zTlQfPXXEYX24vXeuQCj74,44403
|
|
12
12
|
docker_stack/markers.py,sha256=gqStnr0tNWZ1kzqvREc2jVQVQrevheShgJtL5QzSx4g,2388
|
|
@@ -14,8 +14,8 @@ docker_stack/merge_conf.py,sha256=Pmsabcgf3SDyaWvf2OLsBPodzEqsvOIjj9xzn_qWXH0,19
|
|
|
14
14
|
docker_stack/registry.py,sha256=sWC1J9JDIrcYyhei1POYCZHJ0xUCKC2pveisSHMgYsQ,9036
|
|
15
15
|
docker_stack/shell_auth.py,sha256=UUu9y4wMFKfc_j86yGcasvS_TLZ2uV4kv0T4XKTMQBQ,33424
|
|
16
16
|
docker_stack/url_parser.py,sha256=Sk8GQE0nEiwCkijp1OltP2AfgtKEmM2hybcH_rBhfiI,6824
|
|
17
|
-
docker_stack-2.3.
|
|
18
|
-
docker_stack-2.3.
|
|
19
|
-
docker_stack-2.3.
|
|
20
|
-
docker_stack-2.3.
|
|
21
|
-
docker_stack-2.3.
|
|
17
|
+
docker_stack-2.3.2.dist-info/METADATA,sha256=igRt3mHXB_3EXVvAnsjTneG3N37_jTqSvtTsKDYEM5U,13121
|
|
18
|
+
docker_stack-2.3.2.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
|
19
|
+
docker_stack-2.3.2.dist-info/entry_points.txt,sha256=mpe2RwIguARsosXIUBQEN2pKU53URqZCh2S4ATwDFL4,55
|
|
20
|
+
docker_stack-2.3.2.dist-info/top_level.txt,sha256=zT6TPL54cLrt9LO_MNkhEpGGOmsoe2HV6Na5Ohy3_2c,13
|
|
21
|
+
docker_stack-2.3.2.dist-info/RECORD,,
|
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: docker-stack
|
|
3
|
-
Version: 2.3.0
|
|
4
|
-
Summary: CLI for deploying and managing Docker stacks.
|
|
5
|
-
Home-page: https://github.com/mesudip/docker-stack
|
|
6
|
-
Author: Sudip Bhattarai
|
|
7
|
-
Author-email: sudip@bhattarai.me
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.9
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
Requires-Dist: PyYAML
|
|
14
|
-
Provides-Extra: dev
|
|
15
|
-
Requires-Dist: pytest<9,>=8; extra == "dev"
|
|
16
|
-
Dynamic: author
|
|
17
|
-
Dynamic: author-email
|
|
18
|
-
Dynamic: classifier
|
|
19
|
-
Dynamic: description
|
|
20
|
-
Dynamic: description-content-type
|
|
21
|
-
Dynamic: home-page
|
|
22
|
-
Dynamic: provides-extra
|
|
23
|
-
Dynamic: requires-dist
|
|
24
|
-
Dynamic: requires-python
|
|
25
|
-
Dynamic: summary
|
|
26
|
-
|
|
27
|
-
# Docker Stack CLI Utility
|
|
28
|
-
|
|
29
|
-
A command-line tool for advanced Docker Swarm stack deployments on plain Docker daemons. `docker-stack` extends vanilla `docker stack deploy` with generated secrets, templated configs, versioned stack state, safer rollbacks, and better day-to-day stack workflows.
|
|
30
|
-
|
|
31
|
-
## Installation
|
|
32
|
-
|
|
33
|
-
Install or upgrade `docker-stack` with:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
pip install docker-stack --upgrade --break-system-packages
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Quick Start
|
|
40
|
-
|
|
41
|
-
### Plain Docker Daemon
|
|
42
|
-
|
|
43
|
-
If you already have a Docker Swarm daemon or Docker context, you can use the advanced stack features directly against it.
|
|
44
|
-
|
|
45
|
-
Typical daemon-only workflow:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
docker-stack deploy my-stack docker-compose.yml
|
|
49
|
-
docker-stack ls
|
|
50
|
-
docker-stack ls -n team-a
|
|
51
|
-
docker-stack ls -A
|
|
52
|
-
docker-stack versions my-stack
|
|
53
|
-
docker-stack cat my-stack
|
|
54
|
-
docker-stack checkout my-stack v2
|
|
55
|
-
docker-stack node ls
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
What this gives you on a raw Docker daemon:
|
|
59
|
-
|
|
60
|
-
- richer secret and config handling in Compose
|
|
61
|
-
- generated secrets without external scripts
|
|
62
|
-
- template expansion from env vars and files
|
|
63
|
-
- versioned stack config history
|
|
64
|
-
- stack version inspection and checkout
|
|
65
|
-
- raw daemon compatibility without extra infrastructure
|
|
66
|
-
|
|
67
|
-
### GitHub Actions
|
|
68
|
-
|
|
69
|
-
#### 1. Normal Docker daemon
|
|
70
|
-
|
|
71
|
-
Use this when the runner already has Docker access through the default Docker context or `DOCKER_HOST`.
|
|
72
|
-
|
|
73
|
-
```yaml
|
|
74
|
-
steps:
|
|
75
|
-
- uses: actions/checkout@v4
|
|
76
|
-
- uses: actions/setup-python@v6
|
|
77
|
-
with:
|
|
78
|
-
python-version: '3.x'
|
|
79
|
-
- run: python3 -m pip install --upgrade docker-stack
|
|
80
|
-
- run: docker-stack deploy my-stack docker-compose.yml
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Use this option when CI can connect directly to the target Docker daemon.
|
|
84
|
-
|
|
85
|
-
#### 2. Docker-Manager
|
|
86
|
-
|
|
87
|
-
Use the bundled action when deploying through
|
|
88
|
-
[Docker-Manager](https://github.com/mesudip/docker-enterprise). The manager
|
|
89
|
-
repository contains the server source, installation instructions, and deployment
|
|
90
|
-
documentation.
|
|
91
|
-
|
|
92
|
-
For a full compose deployment directly from CI, use the action to configure
|
|
93
|
-
Docker-Manager authentication and then run the normal `docker-stack deploy`
|
|
94
|
-
command:
|
|
95
|
-
|
|
96
|
-
```yaml
|
|
97
|
-
permissions:
|
|
98
|
-
contents: read
|
|
99
|
-
id-token: write
|
|
100
|
-
|
|
101
|
-
steps:
|
|
102
|
-
- uses: actions/checkout@v4
|
|
103
|
-
- uses: mesudip/docker-stack@v2
|
|
104
|
-
with:
|
|
105
|
-
manager: https://manager.example.com:2378
|
|
106
|
-
- run: docker-stack deploy --namespace team-a --with-registry-auth my-stack docker-compose.yml
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
You can also deploy the full compose file through action inputs:
|
|
110
|
-
|
|
111
|
-
```yaml
|
|
112
|
-
permissions:
|
|
113
|
-
contents: read
|
|
114
|
-
id-token: write
|
|
115
|
-
|
|
116
|
-
steps:
|
|
117
|
-
- uses: actions/checkout@v4
|
|
118
|
-
- uses: mesudip/docker-stack@v2
|
|
119
|
-
with:
|
|
120
|
-
manager: https://manager.example.com:2378
|
|
121
|
-
stack: my-stack
|
|
122
|
-
compose-file: docker-compose.yml
|
|
123
|
-
namespace: team-a
|
|
124
|
-
with-registry-auth: "true"
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
To release new service images without submitting the compose file again:
|
|
128
|
-
|
|
129
|
-
```yaml
|
|
130
|
-
permissions:
|
|
131
|
-
contents: read
|
|
132
|
-
id-token: write
|
|
133
|
-
|
|
134
|
-
steps:
|
|
135
|
-
- uses: actions/checkout@v4
|
|
136
|
-
- uses: mesudip/docker-stack@v2
|
|
137
|
-
with:
|
|
138
|
-
manager: https://manager.example.com:2378
|
|
139
|
-
stack: my-stack
|
|
140
|
-
namespace: team-a
|
|
141
|
-
with-registry-auth: "true"
|
|
142
|
-
images: |
|
|
143
|
-
api=ghcr.io/acme/api:${{ github.sha }}
|
|
144
|
-
worker=ghcr.io/acme/worker:${{ github.sha }}
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
Use a full compose deployment when the workflow owns the complete stack
|
|
148
|
-
definition. Use an image-only deployment when the stack is already managed and
|
|
149
|
-
the workflow only needs to release new images. Both forms support namespaces;
|
|
150
|
-
the namespace defaults to `default` when omitted.
|
|
151
|
-
|
|
152
|
-
### Authenticated Docker-Manager shell
|
|
153
|
-
|
|
154
|
-
Open an isolated Bash or Zsh session for a manager context:
|
|
155
|
-
|
|
156
|
-
```bash
|
|
157
|
-
docker-stack shell office
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
If `office` does not exist yet, the CLI asks for its Docker-Manager URL, creates
|
|
161
|
-
the context, authenticates, and opens the shell. You can also provide everything
|
|
162
|
-
non-interactively with `docker-stack shell --context office <manager-url>`.
|
|
163
|
-
|
|
164
|
-
Stack commands use the `default` namespace unless `-n/--namespace` is supplied.
|
|
165
|
-
Listings print the selected namespace; `docker-stack ls -A` (or
|
|
166
|
-
`--all-namespaces`) lists every visible namespace.
|
|
167
|
-
|
|
168
|
-
The prompt displays `(docker:office@cluster)`, keeps the selected manager context active,
|
|
169
|
-
and refreshes authentication when needed. The session supports `docker` and
|
|
170
|
-
`docker compose`; legacy `docker-compose` is not supported. If authentication
|
|
171
|
-
expires, run `docker-stack login` again.
|
|
172
|
-
|
|
173
|
-
Container listing is cluster-aware and includes the owning Swarm node. Select a
|
|
174
|
-
node when working with daemon-local resources such as volumes and images:
|
|
175
|
-
|
|
176
|
-
```bash
|
|
177
|
-
docker ps
|
|
178
|
-
docker-stack node current
|
|
179
|
-
docker-stack node use worker-02
|
|
180
|
-
docker volume ls
|
|
181
|
-
docker image ls
|
|
182
|
-
docker-stack node use cluster
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
Node selection is stored only in the managed shell's isolated Docker
|
|
186
|
-
configuration and appears in the prompt as `(docker:office@worker-02)`.
|
|
187
|
-
Selected-node requests remain subject to the manager's Docker permissions;
|
|
188
|
-
image management is root-only because the manager does not define delegated
|
|
189
|
-
image permissions.
|
|
190
|
-
|
|
191
|
-
Cluster-aware output is enabled after the manager has observed compatible
|
|
192
|
-
agents on every discovered node. The manager remembers that capability until it
|
|
193
|
-
restarts, so a temporary agent outage does not make the CLI revert to a legacy
|
|
194
|
-
feature decision. A partial cluster listing prints the available containers,
|
|
195
|
-
reports each failed node on stderr with its incident id, and exits non-zero.
|
|
196
|
-
|
|
197
|
-
`docker ps --quiet` and `docker ps --format ...` use Docker's native formatter
|
|
198
|
-
and therefore do not add the `NODE` column. Explicit Docker overrides such as
|
|
199
|
-
`--context`, `--host`/`-H`, and `--config` bypass managed cluster formatting and
|
|
200
|
-
are sent unchanged to the Docker CLI.
|
|
201
|
-
|
|
202
|
-
## Core Capabilities
|
|
203
|
-
|
|
204
|
-
- **Advanced Deployments on Plain Docker Daemons:**
|
|
205
|
-
`docker-stack` works directly against a raw Docker daemon and adds capabilities that standard `docker stack deploy` does not provide out of the box:
|
|
206
|
-
- generated secrets
|
|
207
|
-
- inline configs and secrets
|
|
208
|
-
- template rendering from environment variables and files
|
|
209
|
-
- versioned config and secret history
|
|
210
|
-
- version lookup, checkout, and rollback-oriented workflows
|
|
211
|
-
- more ergonomic stack and node inspection output
|
|
212
|
-
|
|
213
|
-
- **Docker Stack Versioning and Config Backup for Rollback:**
|
|
214
|
-
The utility automatically versions your Docker configs and secrets, allowing for easy tracking of changes and seamless rollbacks to previous states. This provides a safety net for your deployments, ensuring you can always revert to a stable configuration.
|
|
215
|
-
|
|
216
|
-
- **Waiting for another deploy of the same stack (Docker-Manager):**
|
|
217
|
-
Docker-Manager runs one apply per stack at a time. When `docker-stack deploy` or `docker-stack checkout` finds another run in progress (a UI deploy, a CI image bump, another operator), it waits and prints who started it:
|
|
218
|
-
|
|
219
|
-
```
|
|
220
|
-
[manager] waiting: a deploy started by alice 42s ago is still running (waiting up to 300s, set DOCKER_MANAGER_DEPLOY_WAIT_SECS to change)
|
|
221
|
-
[manager] press Enter twice to force your deploy (aborts that run; changes it already made to the daemon stay), Ctrl+C to quit
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
- It retries every 5 seconds until the stack is free or `DOCKER_MANAGER_DEPLOY_WAIT_SECS` runs out (default: the deploy timeout; `0` fails immediately). CI runs wait the same way, without the prompt.
|
|
225
|
-
- At a terminal, pressing Enter twice within 3 seconds asks the manager to abort the running deployment and deploys as soon as the stack is released. This needs stack deploy permission and is offered once per run. The aborted run stops orchestrating, but daemon requests it already made still complete; your deploy then applies over that state.
|
|
226
|
-
- Ctrl+C exits without touching the other run.
|
|
227
|
-
|
|
228
|
-
## Why Use It?
|
|
229
|
-
|
|
230
|
-
Vanilla Docker Stack deployments can sometimes lack the flexibility needed for dynamic environments or robust secret management. This utility bridges those gaps by:
|
|
231
|
-
|
|
232
|
-
- **Automating Secret Management:** No more manual secret generation or complex external scripts.
|
|
233
|
-
- **Simplifying Configuration:** Define configs and secrets directly in your compose files or use templates.
|
|
234
|
-
- **Enhancing Security:** Generate strong, random secrets on the fly.
|
|
235
|
-
- **Enabling Rollbacks:** Versioning ensures you can always revert to a known good state.
|
|
236
|
-
- **Improving Raw Daemon Workflows:** Works directly with a plain Docker Swarm daemon.
|
|
237
|
-
|
|
238
|
-
## Advanced Compose Features
|
|
239
|
-
|
|
240
|
-
- **Docker Config and Secret Management with Extended Options:**
|
|
241
|
-
This utility significantly extends Docker's native config and secret management by introducing `x-` prefixed directives in your `docker-compose.yml` files. These directives allow for dynamic content generation, templating, and file inclusion, making your deployments more flexible and secure.
|
|
242
|
-
|
|
243
|
-
### `x-content`: Inline Content for Configs and Secrets
|
|
244
|
-
Allows you to define the content of a Docker config or secret directly within your `docker-compose.yml`.
|
|
245
|
-
|
|
246
|
-
```yaml
|
|
247
|
-
secrets:
|
|
248
|
-
my_inline_secret:
|
|
249
|
-
x-content: "This is my secret content defined inline."
|
|
250
|
-
|
|
251
|
-
configs:
|
|
252
|
-
my_inline_config:
|
|
253
|
-
x-content: |
|
|
254
|
-
key=value
|
|
255
|
-
another_key=another_value
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
### `x-template`: Environment Variable Templating
|
|
259
|
-
Enables the use of environment variables within your config or secret content, which are substituted at deployment time.
|
|
260
|
-
|
|
261
|
-
```yaml
|
|
262
|
-
secrets:
|
|
263
|
-
my_templated_secret:
|
|
264
|
-
x-template: "I can create composite secret with template. ${API_KEY_NAME}:${MY_API_KEY}"
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### `x-template-file`: External Template Files
|
|
268
|
-
Reference an external file whose content will be treated as a template and processed with environment variables.
|
|
269
|
-
|
|
270
|
-
```yaml
|
|
271
|
-
configs:
|
|
272
|
-
my_config_from_template_file:
|
|
273
|
-
x-template-file: "./templates/my_config.tpl"
|
|
274
|
-
```
|
|
275
|
-
*(Content of `./templates/my_config.tpl` might be: `DB_HOST=${DATABASE_HOST}`)*
|
|
276
|
-
|
|
277
|
-
### `environment`: Secret Content from Environment Variables
|
|
278
|
-
Secrets can read their content from an environment variable at deploy time.
|
|
279
|
-
|
|
280
|
-
```yaml
|
|
281
|
-
secrets:
|
|
282
|
-
api_token:
|
|
283
|
-
environment: API_TOKEN
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
If the variable is unset or empty, deployment fails before Docker objects are created.
|
|
287
|
-
|
|
288
|
-
### Stored Source Metadata
|
|
289
|
-
Versioned stack configs include a top-level `x-files` list with base64-encoded source material for recovery and auditing. This includes the original compose file as `compose.yml`, a generated `.env` containing referenced non-secret environment values, and config files referenced by `configs.*.file` or `configs.*.x-template-file`. Secret source files and variables used by `secrets.*.environment` are not stored in `x-files`.
|
|
290
|
-
|
|
291
|
-
### `x-generate`: Dynamic Secret Generation (Secrets Only)
|
|
292
|
-
This powerful feature allows you to automatically generate random secrets based on specified criteria, eliminating the need to manually create and manage them. This is particularly useful for passwords, API keys, and other sensitive data.
|
|
293
|
-
|
|
294
|
-
Supported `x-generate` forms:
|
|
295
|
-
|
|
296
|
-
- `true`
|
|
297
|
-
Generate a secret with default options.
|
|
298
|
-
- integer
|
|
299
|
-
Generate a secret with the requested length.
|
|
300
|
-
- object
|
|
301
|
-
Generate a secret with explicit generation flags.
|
|
302
|
-
|
|
303
|
-
Supported object flags:
|
|
304
|
-
|
|
305
|
-
- `length`
|
|
306
|
-
Exact secret length.
|
|
307
|
-
- `numbers`
|
|
308
|
-
Include digits `0-9`.
|
|
309
|
-
- `special`
|
|
310
|
-
Include special characters.
|
|
311
|
-
- `uppercase`
|
|
312
|
-
Include uppercase letters `A-Z`.
|
|
313
|
-
|
|
314
|
-
Behavior notes:
|
|
315
|
-
|
|
316
|
-
- Generated values are created at deploy time.
|
|
317
|
-
- Generated secrets are versioned like other managed secrets.
|
|
318
|
-
- Newly generated values can be shown after deploy when `--show-generated` is enabled.
|
|
319
|
-
- `x-generate` is for secrets only; configs should use `x-content`, `x-template`, or `x-template-file`.
|
|
320
|
-
|
|
321
|
-
- **Simple Generation (12-20 characters, default options):**
|
|
322
|
-
```yaml
|
|
323
|
-
secrets:
|
|
324
|
-
my_simple_generated_secret:
|
|
325
|
-
x-generate: true
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
- **Specify Length:**
|
|
329
|
-
```yaml
|
|
330
|
-
secrets:
|
|
331
|
-
my_fixed_length_secret:
|
|
332
|
-
x-generate: 30 # Generates a 30-character secret
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
- **Custom Generation Options:**
|
|
336
|
-
You can provide a dictionary to fine-tune the generation process:
|
|
337
|
-
- `length`: (integer, default: 12-20 random) Exact length of the secret.
|
|
338
|
-
- `numbers`: (boolean, default: `true`) Include numbers (0-9).
|
|
339
|
-
- `special`: (boolean, default: `true`) Include special characters (!@#$%^&*...).
|
|
340
|
-
- `uppercase`: (boolean, default: `true`) Include uppercase letters (A-Z).
|
|
341
|
-
|
|
342
|
-
```yaml
|
|
343
|
-
secrets:
|
|
344
|
-
my_complex_generated_secret:
|
|
345
|
-
x-generate:
|
|
346
|
-
length: 25
|
|
347
|
-
numbers: false
|
|
348
|
-
special: true
|
|
349
|
-
uppercase: true
|
|
350
|
-
my_alphanumeric_secret:
|
|
351
|
-
x-generate:
|
|
352
|
-
length: 15
|
|
353
|
-
numbers: true
|
|
354
|
-
special: false
|
|
355
|
-
uppercase: false
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
- **Database Password Style Secret:**
|
|
359
|
-
Generates a strong password with uppercase letters, lowercase letters, numbers, and special characters.
|
|
360
|
-
```yaml
|
|
361
|
-
secrets:
|
|
362
|
-
db_password:
|
|
363
|
-
x-generate:
|
|
364
|
-
length: 32
|
|
365
|
-
numbers: true
|
|
366
|
-
special: false
|
|
367
|
-
uppercase: true
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
- **Application Token Without Special Characters:**
|
|
371
|
-
Useful when the target application rejects punctuation in credentials or tokens.
|
|
372
|
-
```yaml
|
|
373
|
-
secrets:
|
|
374
|
-
app_token:
|
|
375
|
-
x-generate:
|
|
376
|
-
length: 40
|
|
377
|
-
numbers: true
|
|
378
|
-
special: false
|
|
379
|
-
uppercase: true
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
- **Lowercase Alphanumeric Secret:**
|
|
383
|
-
Useful for systems that want URL-safe or copy-friendly generated values.
|
|
384
|
-
```yaml
|
|
385
|
-
secrets:
|
|
386
|
-
compact_secret:
|
|
387
|
-
x-generate:
|
|
388
|
-
length: 24
|
|
389
|
-
numbers: true
|
|
390
|
-
special: false
|
|
391
|
-
uppercase: false
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
## Known Limitations
|
|
395
|
-
|
|
396
|
-
Docker limits config content to 500 KB. Stack history includes encoded source
|
|
397
|
-
files, so stacks with large compose or config files can exceed that limit.
|
|
398
|
-
|
|
399
|
-
## Development
|
|
400
|
-
|
|
401
|
-
Install runtime and test dependencies with either:
|
|
402
|
-
|
|
403
|
-
```bash
|
|
404
|
-
python3 -m pip install -r requirements-dev.txt
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
or:
|
|
408
|
-
|
|
409
|
-
```bash
|
|
410
|
-
python3 -m pip install -e '.[dev]'
|
|
411
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|