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.
Files changed (21) hide show
  1. {docker-stack-0.2.0 → docker-stack-0.2.2}/PKG-INFO +2 -2
  2. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/cli.py +176 -7
  3. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/helpers.py +12 -8
  4. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack.egg-info/PKG-INFO +2 -2
  5. {docker-stack-0.2.0 → docker-stack-0.2.2}/setup.py +2 -2
  6. {docker-stack-0.2.0 → docker-stack-0.2.2}/README.md +0 -0
  7. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/__init__.py +0 -0
  8. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/compose.py +0 -0
  9. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/docker_objects.py +0 -0
  10. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/envsubst.py +0 -0
  11. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/envsubst_merge.py +0 -0
  12. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/merge_conf.py +0 -0
  13. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/registry.py +0 -0
  14. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack/url_parser.py +0 -0
  15. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack.egg-info/SOURCES.txt +0 -0
  16. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack.egg-info/dependency_links.txt +0 -0
  17. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack.egg-info/entry_points.txt +0 -0
  18. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack.egg-info/requires.txt +0 -0
  19. {docker-stack-0.2.0 → docker-stack-0.2.2}/docker_stack.egg-info/top_level.txt +0 -0
  20. {docker-stack-0.2.0 → docker-stack-0.2.2}/pyproject.toml +0 -0
  21. {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.0
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.dev.np@gmail.com
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
- def deploy(self, stack_name, compose_file, with_registry_auth=False):
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
- _, cmd = self.docker.config.increment(stack_name, rendered_content, [f"mesudip.stack.name={stack_name}"],stack=stack_name)
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
- print("Giving console")
98
- process = subprocess.Popen(self.command, shell=True,cwd=self.cwd)
99
- process.wait()
100
- else:
101
- subprocess.run(self.command)
102
- return run_cli_command(self.command, stdin=self.stdin, log=use_log, shell=False,cwd=self.cwd)
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.0
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.dev.np@gmail.com
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.0",
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.dev.np@gmail.com",
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