docker-stack 0.2.1__tar.gz → 0.2.3__tar.gz
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-0.2.1 → docker-stack-0.2.3}/PKG-INFO +2 -2
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack/cli.py +184 -8
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack/envsubst.py +4 -0
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack/helpers.py +15 -8
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack.egg-info/PKG-INFO +2 -2
- {docker-stack-0.2.1 → docker-stack-0.2.3}/setup.py +2 -2
- {docker-stack-0.2.1 → docker-stack-0.2.3}/README.md +0 -0
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack/__init__.py +0 -0
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack/compose.py +0 -0
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack/docker_objects.py +0 -0
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack/envsubst_merge.py +0 -0
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack/merge_conf.py +0 -0
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack/registry.py +0 -0
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack/url_parser.py +0 -0
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack.egg-info/SOURCES.txt +0 -0
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack.egg-info/dependency_links.txt +0 -0
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack.egg-info/entry_points.txt +0 -0
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack.egg-info/requires.txt +0 -0
- {docker-stack-0.2.1 → docker-stack-0.2.3}/docker_stack.egg-info/top_level.txt +0 -0
- {docker-stack-0.2.1 → docker-stack-0.2.3}/pyproject.toml +0 -0
- {docker-stack-0.2.1 → docker-stack-0.2.3}/setup.cfg +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: docker-stack
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: CLI for deploying and managing Docker stacks.
|
|
5
5
|
Home-page: https://github.com/mesuidp/docker-stack
|
|
6
6
|
Author: Sudip Bhattarai
|
|
7
|
-
Author-email: sudip
|
|
7
|
+
Author-email: sudip@bhattarai.me
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
import argparse
|
|
3
|
+
import base64
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
3
6
|
import sys
|
|
4
7
|
from pathlib import Path
|
|
5
8
|
from typing import List
|
|
@@ -121,10 +124,150 @@ class DockerStack:
|
|
|
121
124
|
else:
|
|
122
125
|
processed_objects[name] = details
|
|
123
126
|
return processed_objects
|
|
127
|
+
|
|
128
|
+
def ls(self):
|
|
129
|
+
cmd = ["docker", "config", "ls", "--format", "{{.ID}}\t{{.Name}}\t{{.Labels}}"]
|
|
130
|
+
output = subprocess.check_output(cmd, text=True).strip().split("\n")
|
|
131
|
+
stack_versions = {}
|
|
132
|
+
|
|
133
|
+
for line in output:
|
|
134
|
+
parts = line.split("\t")
|
|
135
|
+
if len(parts) == 3 and "mesudip.stack.name" in parts[2]:
|
|
136
|
+
labels = {k: v for k, v in (label.split("=") for label in parts[2].split(",") if "=" in label)}
|
|
137
|
+
stack_name = labels.get("mesudip.stack.name")
|
|
138
|
+
version = labels.get("mesudip.object.version", "unknown")
|
|
139
|
+
|
|
140
|
+
if stack_name:
|
|
141
|
+
if stack_name not in stack_versions:
|
|
142
|
+
stack_versions[stack_name] = []
|
|
143
|
+
stack_versions[stack_name].append(version)
|
|
144
|
+
|
|
145
|
+
# Calculate max stack name width
|
|
146
|
+
max_stack_name_length = max(len(stack) for stack in stack_versions) if stack_versions else 10
|
|
147
|
+
header_stack = "Stack Name".ljust(max_stack_name_length)
|
|
148
|
+
|
|
149
|
+
print(f"{header_stack} | Versions")
|
|
150
|
+
print("-" * (max_stack_name_length + 12))
|
|
151
|
+
|
|
152
|
+
for stack, versions in sorted(stack_versions.items()):
|
|
153
|
+
versions_str = ", ".join(sorted(versions, key=int))
|
|
154
|
+
print(f"{stack.ljust(max_stack_name_length)} | {versions_str}")
|
|
155
|
+
|
|
156
|
+
return stack_versions
|
|
157
|
+
|
|
158
|
+
def cat(self, name:str, version:str):
|
|
159
|
+
if version.startswith('v') or version.startswith('V'):
|
|
160
|
+
version = version[1:]
|
|
161
|
+
if version == '1':
|
|
162
|
+
name = f"{name}"
|
|
163
|
+
else:
|
|
164
|
+
name = f"{name}_v{version}"
|
|
165
|
+
|
|
166
|
+
cmd = ["docker", "config", "inspect", name]
|
|
167
|
+
output = subprocess.check_output(cmd, text=True).strip()
|
|
168
|
+
|
|
169
|
+
# Parse the JSON output
|
|
170
|
+
configs = json.loads(output)
|
|
171
|
+
if not configs:
|
|
172
|
+
print(f"No config found for {name}")
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
# Extract and decode the base64-encoded Spec.Data
|
|
176
|
+
encoded_data = configs[0].get("Spec", {}).get("Data", "")
|
|
177
|
+
if not encoded_data:
|
|
178
|
+
print(f"No data found in config {name}")
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
decoded_data = base64.b64decode(encoded_data).decode("utf-8")
|
|
182
|
+
return decoded_data
|
|
183
|
+
|
|
184
|
+
def versions(self, stack_name):
|
|
185
|
+
cmd = ["docker", "config", "ls", "--format", "{{.Name}}\t{{.Labels}}"]
|
|
186
|
+
output = subprocess.check_output(cmd, text=True).strip().split("\n")
|
|
187
|
+
versions_list = []
|
|
188
|
+
|
|
189
|
+
for line in output:
|
|
190
|
+
parts = line.split("\t")
|
|
191
|
+
if len(parts) == 2 and "mesudip.stack.name" in parts[1]:
|
|
192
|
+
labels = {k: v for k, v in (label.split("=") for label in parts[1].split(",") if "=" in label)}
|
|
193
|
+
stack = labels.get("mesudip.stack.name")
|
|
194
|
+
version = labels.get("mesudip.object.version", "unknown")
|
|
195
|
+
tag = labels.get("mesudip.stack.tag", "")
|
|
196
|
+
|
|
197
|
+
if stack == stack_name:
|
|
198
|
+
versions_list.append((version, tag))
|
|
199
|
+
|
|
200
|
+
# Add headers to list for proper spacing calculation
|
|
201
|
+
versions_list.insert(0, ("Version", "Tag"))
|
|
202
|
+
|
|
203
|
+
# Determine max column widths
|
|
204
|
+
max_version_length = max(len(v[0]) for v in versions_list)
|
|
205
|
+
max_tag_length = max(len(v[1]) for v in versions_list)
|
|
206
|
+
|
|
207
|
+
# Print header
|
|
208
|
+
print(f"{'Version'.ljust(max_version_length)} | {'Tag'.ljust(max_tag_length)}")
|
|
209
|
+
print("-" * (max_version_length + max_tag_length + 3))
|
|
210
|
+
|
|
211
|
+
# Print sorted versions (excluding header)
|
|
212
|
+
for version, tag in sorted(versions_list[1:], key=lambda x: int(x[0]) if x[0].isdigit() else x[0]):
|
|
213
|
+
print(f"{version.ljust(max_version_length)} | {tag.ljust(max_tag_length)}")
|
|
214
|
+
|
|
215
|
+
return versions_list[1:]
|
|
216
|
+
|
|
217
|
+
def checkout(self, stack_name, identifier, with_registry_auth=False):
|
|
218
|
+
"""
|
|
219
|
+
Deploys a stack by version or tag.
|
|
220
|
+
|
|
221
|
+
:param stack_name: Name of the stack.
|
|
222
|
+
:param identifier: Version (e.g., 'v1.2', 'V3') or tag (e.g., 'stable', 'latest').
|
|
223
|
+
:param with_registry_auth: Whether to use registry authentication.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
# Regex to check if the identifier is a version (optional "v" or "V" at the start, followed by digits)
|
|
227
|
+
version_pattern = re.compile(r"^[vV]?(\d+(\.\d+)*)$")
|
|
124
228
|
|
|
125
|
-
|
|
229
|
+
match = version_pattern.match(identifier)
|
|
230
|
+
if match:
|
|
231
|
+
version = match.group(1) # Extract the numeric version part
|
|
232
|
+
tag = None
|
|
233
|
+
else:
|
|
234
|
+
tag = identifier
|
|
235
|
+
versions_list = self.versions(stack_name)
|
|
236
|
+
matching_versions = [v for v, t in versions_list if t == tag]
|
|
237
|
+
if not matching_versions:
|
|
238
|
+
raise ValueError(f"No version found for tag '{tag}' in stack '{stack_name}'")
|
|
239
|
+
version = matching_versions[0] # Use the first matching version
|
|
240
|
+
|
|
241
|
+
compose_content = self.cat(stack_name, version)
|
|
242
|
+
|
|
243
|
+
temp_file = f"/tmp/{stack_name}_v{version}.yml"
|
|
244
|
+
with open(temp_file, "w") as f:
|
|
245
|
+
f.write(compose_content)
|
|
246
|
+
|
|
247
|
+
print(f"Deploying stack {stack_name} with version {version} (tag: {tag})...")
|
|
248
|
+
self._deploy(stack_name, temp_file, compose_content, with_registry_auth=with_registry_auth, tag=tag)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _deploy(self, stack_name, rendered_filename,rendered_content, with_registry_auth=False,tag=None):
|
|
252
|
+
labels = [f"mesudip.stack.name={stack_name}"]
|
|
253
|
+
if tag:
|
|
254
|
+
labels.append(f"mesudip.stack.tag={tag}")
|
|
255
|
+
|
|
256
|
+
_, cmd = self.docker.config.increment(stack_name, rendered_content,labels=labels ,stack=stack_name)
|
|
257
|
+
if not cmd.isNop():
|
|
258
|
+
self.commands.append(cmd)
|
|
259
|
+
cmd = ["docker", "stack", "deploy", "-c", str(rendered_filename), stack_name]
|
|
260
|
+
if with_registry_auth:
|
|
261
|
+
cmd.insert(3, "--with-registry-auth")
|
|
262
|
+
self.commands.append(Command(cmd,give_console=True))
|
|
263
|
+
|
|
264
|
+
def deploy(self, stack_name, compose_file, with_registry_auth=False,tag=None):
|
|
126
265
|
rendered_filename, rendered_content = self.render_compose_file(compose_file,stack=stack_name)
|
|
127
|
-
|
|
266
|
+
labels = [f"mesudip.stack.name={stack_name}"]
|
|
267
|
+
if tag:
|
|
268
|
+
labels.append(f"mesudip.stack.tag={tag}")
|
|
269
|
+
|
|
270
|
+
_, cmd = self.docker.config.increment(stack_name, rendered_content,labels=labels ,stack=stack_name)
|
|
128
271
|
if not cmd.isNop():
|
|
129
272
|
self.commands.append(cmd)
|
|
130
273
|
cmd = ["docker", "stack", "deploy", "-c", str(rendered_filename), stack_name]
|
|
@@ -163,8 +306,14 @@ class DockerStack:
|
|
|
163
306
|
build_command = ["docker", "build", "-t", image]
|
|
164
307
|
|
|
165
308
|
|
|
166
|
-
|
|
167
|
-
|
|
309
|
+
args = build_config.get('args', [])
|
|
310
|
+
|
|
311
|
+
if isinstance(args, dict):
|
|
312
|
+
for key, val in args.items():
|
|
313
|
+
build_command.extend(["--build-arg", f"{envsubst(key)}={envsubst(val)}"])
|
|
314
|
+
elif isinstance(args, list):
|
|
315
|
+
for value in args:
|
|
316
|
+
build_command.extend(["--build-arg", envsubst(value)])
|
|
168
317
|
|
|
169
318
|
build_command.append(os.path.join(base_dir, build_config.get('context', '.')))
|
|
170
319
|
self.commands.append(Command(build_command))
|
|
@@ -203,11 +352,31 @@ def main(args:List[str]=None):
|
|
|
203
352
|
deploy_parser.add_argument("stack_name", help="Name of the stack")
|
|
204
353
|
deploy_parser.add_argument("compose_file", help="Path to the compose file")
|
|
205
354
|
deploy_parser.add_argument("--with-registry-auth", action="store_true", help="Use registry authentication")
|
|
206
|
-
|
|
355
|
+
deploy_parser.add_argument("-t", "--tag", help="Tag the current deployment for later checkout", required=False)
|
|
356
|
+
|
|
207
357
|
# Remove subcommand
|
|
208
358
|
rm_parser = subparsers.add_parser("rm", help="Remove a deployed stack")
|
|
209
359
|
rm_parser.add_argument("stack_name", help="Name of the stack")
|
|
210
360
|
|
|
361
|
+
# Ls command
|
|
362
|
+
subparsers.add_parser("ls",help="List docker-stacks")
|
|
363
|
+
|
|
364
|
+
cat_parser = subparsers.add_parser("cat",help="Print the docker compose of specific version")
|
|
365
|
+
cat_parser.add_argument("stack_name", help="Name of the stack")
|
|
366
|
+
cat_parser.add_argument("version", help="Stack version to cat")
|
|
367
|
+
|
|
368
|
+
checkout_parser = subparsers.add_parser("checkout",help="Deploy specific version of the stack")
|
|
369
|
+
checkout_parser.add_argument("stack_name", help="Name of the stack")
|
|
370
|
+
checkout_parser.add_argument("version", help="Stack version to cat")
|
|
371
|
+
|
|
372
|
+
# version_parser = subparsers.add_parser("version",help="Deploy specific version of the stack")
|
|
373
|
+
# version_parser.add_argument("stack_name", help="Name of the stack")
|
|
374
|
+
# version_parser.add_argument("version","versions", help="Stack version to cat")
|
|
375
|
+
|
|
376
|
+
version_parser = subparsers.add_parser("version",aliases=["versions"], help="Deploy specific version of the stack")
|
|
377
|
+
version_parser.add_argument("stack_name", help="Name of the stack")
|
|
378
|
+
|
|
379
|
+
|
|
211
380
|
parser.add_argument("-u", "--user", help="Registry credentials in format hostname:username:password", action="append", required=False, default=[])
|
|
212
381
|
parser.add_argument("-t", "--tag", help="Tag the current deployment for later checkout", required=False)
|
|
213
382
|
parser.add_argument("-ro",'-r',"--ro","--r", "--dry-run", action="store_true", help="Print commands, don't execute them", required=False)
|
|
@@ -222,11 +391,18 @@ def main(args:List[str]=None):
|
|
|
222
391
|
elif args.command == "push":
|
|
223
392
|
docker.stack.push(args.compose_file)
|
|
224
393
|
elif args.command == "deploy":
|
|
225
|
-
docker.stack.deploy(args.stack_name, args.compose_file, args.with_registry_auth)
|
|
394
|
+
docker.stack.deploy(args.stack_name, args.compose_file, args.with_registry_auth,tag=args.tag)
|
|
395
|
+
elif args.command == "ls":
|
|
396
|
+
docker.stack.ls()
|
|
397
|
+
|
|
226
398
|
elif args.command == "rm":
|
|
227
399
|
docker.stack.rm(args.stack_name)
|
|
228
|
-
|
|
229
|
-
|
|
400
|
+
elif args.command == 'cat':
|
|
401
|
+
print(docker.stack.cat(args.stack_name,args.version))
|
|
402
|
+
elif args.command == 'checkout':
|
|
403
|
+
docker.stack.checkout(args.stack_name,args.version)
|
|
404
|
+
elif args.command == 'versions' or args.command == "version":
|
|
405
|
+
docker.stack.versions(args.stack_name)
|
|
230
406
|
if args.ro:
|
|
231
407
|
print("Following commands were not executed:")
|
|
232
408
|
[print(" >> "+str(x)) for x in docker.stack.commands if x]
|
|
@@ -25,6 +25,7 @@ def envsubst(template_str, env=os.environ):
|
|
|
25
25
|
# Regex for $VARIABLE without default
|
|
26
26
|
pattern_without_default = re.compile(r"\$([a-zA-Z_][a-zA-Z0-9_]*)")
|
|
27
27
|
|
|
28
|
+
template_str = template_str.replace("$$", "__ESCAPED_DOLLAR__")
|
|
28
29
|
def print_error_line(template_str, match_span):
|
|
29
30
|
"""Helper function to print the error context."""
|
|
30
31
|
lines = template_str.splitlines()
|
|
@@ -76,6 +77,8 @@ def envsubst(template_str, env=os.environ):
|
|
|
76
77
|
|
|
77
78
|
# Substitute variables without default values
|
|
78
79
|
template_str = pattern_without_default.sub(replace_without_default, template_str)
|
|
80
|
+
|
|
81
|
+
template_str = template_str.replace("__ESCAPED_DOLLAR__", "$")
|
|
79
82
|
|
|
80
83
|
return template_str
|
|
81
84
|
|
|
@@ -103,3 +106,4 @@ def main():
|
|
|
103
106
|
|
|
104
107
|
if __name__ == "__main__":
|
|
105
108
|
main()
|
|
109
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import subprocess
|
|
2
|
+
import sys
|
|
2
3
|
from typing import List, Optional
|
|
3
4
|
|
|
4
5
|
|
|
@@ -91,15 +92,21 @@ class Command:
|
|
|
91
92
|
return
|
|
92
93
|
# Use the provided log value if available, otherwise use the one from the constructor
|
|
93
94
|
use_log = log if log is not None else self.log
|
|
94
|
-
|
|
95
|
+
if use_log:
|
|
96
|
+
print("> " + " ".join(self.command),flush=True)
|
|
97
|
+
|
|
95
98
|
if not self.stdin:
|
|
96
|
-
if self.give_console:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
# if self.give_console:
|
|
100
|
+
# print("Giving console")
|
|
101
|
+
# process = subprocess.Popen(self.command, shell=True,cwd=self.cwd)
|
|
102
|
+
# process.wait()
|
|
103
|
+
# return process
|
|
104
|
+
# else:
|
|
105
|
+
result= subprocess.run(self.command)
|
|
106
|
+
if result.returncode!= 0:
|
|
107
|
+
sys.exit(result.returncode)
|
|
108
|
+
else:
|
|
109
|
+
return run_cli_command(self.command, stdin=self.stdin, log=False, shell=False,cwd=self.cwd)
|
|
103
110
|
|
|
104
111
|
def __str__(self) -> str:
|
|
105
112
|
"""
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: docker-stack
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: CLI for deploying and managing Docker stacks.
|
|
5
5
|
Home-page: https://github.com/mesuidp/docker-stack
|
|
6
6
|
Author: Sudip Bhattarai
|
|
7
|
-
Author-email: sudip
|
|
7
|
+
Author-email: sudip@bhattarai.me
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
@@ -2,12 +2,12 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="docker-stack",
|
|
5
|
-
version="0.2.
|
|
5
|
+
version="0.2.3",
|
|
6
6
|
description="CLI for deploying and managing Docker stacks.",
|
|
7
7
|
long_description=open("README.md").read(), # You can include a README file to describe your package
|
|
8
8
|
long_description_content_type="text/markdown",
|
|
9
9
|
author="Sudip Bhattarai",
|
|
10
|
-
author_email="sudip
|
|
10
|
+
author_email="sudip@bhattarai.me",
|
|
11
11
|
url="https://github.com/mesuidp/docker-stack", # Replace with your project URL
|
|
12
12
|
packages=find_packages(),
|
|
13
13
|
install_requires=[
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|