docker-stack 0.2.0__tar.gz → 0.2.2__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.0 → docker-stack-0.2.2}/PKG-INFO +2 -2
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/cli.py +176 -7
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/helpers.py +12 -8
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack.egg-info/PKG-INFO +2 -2
- {docker-stack-0.2.0 → docker-stack-0.2.2}/setup.py +2 -2
- {docker-stack-0.2.0 → docker-stack-0.2.2}/README.md +0 -0
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/__init__.py +0 -0
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/compose.py +0 -0
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/docker_objects.py +0 -0
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/envsubst.py +0 -0
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/envsubst_merge.py +0 -0
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/merge_conf.py +0 -0
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/registry.py +0 -0
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/url_parser.py +0 -0
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack.egg-info/SOURCES.txt +0 -0
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack.egg-info/dependency_links.txt +0 -0
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack.egg-info/entry_points.txt +0 -0
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack.egg-info/requires.txt +0 -0
- {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack.egg-info/top_level.txt +0 -0
- {docker-stack-0.2.0 → docker-stack-0.2.2}/pyproject.toml +0 -0
- {docker-stack-0.2.0 → docker-stack-0.2.2}/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.2
|
|
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]
|
|
@@ -203,11 +346,31 @@ def main(args:List[str]=None):
|
|
|
203
346
|
deploy_parser.add_argument("stack_name", help="Name of the stack")
|
|
204
347
|
deploy_parser.add_argument("compose_file", help="Path to the compose file")
|
|
205
348
|
deploy_parser.add_argument("--with-registry-auth", action="store_true", help="Use registry authentication")
|
|
206
|
-
|
|
349
|
+
deploy_parser.add_argument("-t", "--tag", help="Tag the current deployment for later checkout", required=False)
|
|
350
|
+
|
|
207
351
|
# Remove subcommand
|
|
208
352
|
rm_parser = subparsers.add_parser("rm", help="Remove a deployed stack")
|
|
209
353
|
rm_parser.add_argument("stack_name", help="Name of the stack")
|
|
210
354
|
|
|
355
|
+
# Ls command
|
|
356
|
+
subparsers.add_parser("ls",help="List docker-stacks")
|
|
357
|
+
|
|
358
|
+
cat_parser = subparsers.add_parser("cat",help="Print the docker compose of specific version")
|
|
359
|
+
cat_parser.add_argument("stack_name", help="Name of the stack")
|
|
360
|
+
cat_parser.add_argument("version", help="Stack version to cat")
|
|
361
|
+
|
|
362
|
+
checkout_parser = subparsers.add_parser("checkout",help="Deploy specific version of the stack")
|
|
363
|
+
checkout_parser.add_argument("stack_name", help="Name of the stack")
|
|
364
|
+
checkout_parser.add_argument("version", help="Stack version to cat")
|
|
365
|
+
|
|
366
|
+
# version_parser = subparsers.add_parser("version",help="Deploy specific version of the stack")
|
|
367
|
+
# version_parser.add_argument("stack_name", help="Name of the stack")
|
|
368
|
+
# version_parser.add_argument("version","versions", help="Stack version to cat")
|
|
369
|
+
|
|
370
|
+
version_parser = subparsers.add_parser("version",aliases=["versions"], help="Deploy specific version of the stack")
|
|
371
|
+
version_parser.add_argument("stack_name", help="Name of the stack")
|
|
372
|
+
|
|
373
|
+
|
|
211
374
|
parser.add_argument("-u", "--user", help="Registry credentials in format hostname:username:password", action="append", required=False, default=[])
|
|
212
375
|
parser.add_argument("-t", "--tag", help="Tag the current deployment for later checkout", required=False)
|
|
213
376
|
parser.add_argument("-ro",'-r',"--ro","--r", "--dry-run", action="store_true", help="Print commands, don't execute them", required=False)
|
|
@@ -216,18 +379,24 @@ def main(args:List[str]=None):
|
|
|
216
379
|
|
|
217
380
|
docker = Docker(registries=args.user)
|
|
218
381
|
docker.load_env()
|
|
219
|
-
docker.check_env()
|
|
220
382
|
|
|
221
383
|
if args.command == "build":
|
|
222
384
|
docker.stack.build_and_push(args.compose_file,push=args.push)
|
|
223
385
|
elif args.command == "push":
|
|
224
386
|
docker.stack.push(args.compose_file)
|
|
225
387
|
elif args.command == "deploy":
|
|
226
|
-
docker.stack.deploy(args.stack_name, args.compose_file, args.with_registry_auth)
|
|
388
|
+
docker.stack.deploy(args.stack_name, args.compose_file, args.with_registry_auth,tag=args.tag)
|
|
389
|
+
elif args.command == "ls":
|
|
390
|
+
docker.stack.ls()
|
|
391
|
+
|
|
227
392
|
elif args.command == "rm":
|
|
228
393
|
docker.stack.rm(args.stack_name)
|
|
229
|
-
|
|
230
|
-
|
|
394
|
+
elif args.command == 'cat':
|
|
395
|
+
print(docker.stack.cat(args.stack_name,args.version))
|
|
396
|
+
elif args.command == 'checkout':
|
|
397
|
+
docker.stack.checkout(args.stack_name,args.version)
|
|
398
|
+
elif args.command == 'versions' or args.command == "version":
|
|
399
|
+
docker.stack.versions(args.stack_name)
|
|
231
400
|
if args.ro:
|
|
232
401
|
print("Following commands were not executed:")
|
|
233
402
|
[print(" >> "+str(x)) for x in docker.stack.commands if x]
|
|
@@ -91,15 +91,19 @@ class Command:
|
|
|
91
91
|
return
|
|
92
92
|
# Use the provided log value if available, otherwise use the one from the constructor
|
|
93
93
|
use_log = log if log is not None else self.log
|
|
94
|
-
|
|
94
|
+
if use_log:
|
|
95
|
+
print("> " + " ".join(self.command),flush=True)
|
|
96
|
+
|
|
95
97
|
if not self.stdin:
|
|
96
|
-
if self.give_console:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
# if self.give_console:
|
|
99
|
+
# print("Giving console")
|
|
100
|
+
# process = subprocess.Popen(self.command, shell=True,cwd=self.cwd)
|
|
101
|
+
# process.wait()
|
|
102
|
+
# return process
|
|
103
|
+
# else:
|
|
104
|
+
return subprocess.run(self.command)
|
|
105
|
+
else:
|
|
106
|
+
return run_cli_command(self.command, stdin=self.stdin, log=False, shell=False,cwd=self.cwd)
|
|
103
107
|
|
|
104
108
|
def __str__(self) -> str:
|
|
105
109
|
"""
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: docker-stack
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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.2",
|
|
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
|
|
File without changes
|