docker-stack 0.2.3__tar.gz → 0.2.5__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 (22) hide show
  1. {docker-stack-0.2.3 → docker-stack-0.2.5}/PKG-INFO +2 -2
  2. {docker-stack-0.2.3 → docker-stack-0.2.5}/docker_stack/cli.py +26 -12
  3. docker-stack-0.2.5/docker_stack/envsubst.py +206 -0
  4. {docker-stack-0.2.3 → docker-stack-0.2.5}/docker_stack/envsubst_merge.py +17 -4
  5. {docker-stack-0.2.3 → docker-stack-0.2.5}/docker_stack.egg-info/PKG-INFO +2 -2
  6. {docker-stack-0.2.3 → docker-stack-0.2.5}/pyproject.toml +3 -0
  7. {docker-stack-0.2.3 → docker-stack-0.2.5}/setup.py +2 -2
  8. docker-stack-0.2.3/docker_stack/envsubst.py +0 -109
  9. {docker-stack-0.2.3 → docker-stack-0.2.5}/README.md +0 -0
  10. {docker-stack-0.2.3 → docker-stack-0.2.5}/docker_stack/__init__.py +0 -0
  11. {docker-stack-0.2.3 → docker-stack-0.2.5}/docker_stack/compose.py +0 -0
  12. {docker-stack-0.2.3 → docker-stack-0.2.5}/docker_stack/docker_objects.py +0 -0
  13. {docker-stack-0.2.3 → docker-stack-0.2.5}/docker_stack/helpers.py +0 -0
  14. {docker-stack-0.2.3 → docker-stack-0.2.5}/docker_stack/merge_conf.py +0 -0
  15. {docker-stack-0.2.3 → docker-stack-0.2.5}/docker_stack/registry.py +0 -0
  16. {docker-stack-0.2.3 → docker-stack-0.2.5}/docker_stack/url_parser.py +0 -0
  17. {docker-stack-0.2.3 → docker-stack-0.2.5}/docker_stack.egg-info/SOURCES.txt +0 -0
  18. {docker-stack-0.2.3 → docker-stack-0.2.5}/docker_stack.egg-info/dependency_links.txt +0 -0
  19. {docker-stack-0.2.3 → docker-stack-0.2.5}/docker_stack.egg-info/entry_points.txt +0 -0
  20. {docker-stack-0.2.3 → docker-stack-0.2.5}/docker_stack.egg-info/requires.txt +0 -0
  21. {docker-stack-0.2.3 → docker-stack-0.2.5}/docker_stack.egg-info/top_level.txt +0 -0
  22. {docker-stack-0.2.3 → docker-stack-0.2.5}/setup.cfg +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: docker-stack
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: CLI for deploying and managing Docker stacks.
5
- Home-page: https://github.com/mesuidp/docker-stack
5
+ Home-page: https://github.com/mesudip/docker-stack
6
6
  Author: Sudip Bhattarai
7
7
  Author-email: sudip@bhattarai.me
8
8
  Classifier: Programming Language :: Python :: 3
@@ -61,38 +61,42 @@ class DockerStack:
61
61
  with open(compose_file) as f:
62
62
  return self.decode_yaml(f.read())
63
63
 
64
- def rendered_compose_file(self,compose_file,stack=None)->str:
64
+ def rendered_compose_file(self,compose_file,stack=None,include_build=True)->str:
65
65
  with open(compose_file) as f:
66
66
  template_content = f.read()
67
67
  # Parse the YAML content
68
68
  compose_data = self.decode_yaml(template_content)
69
+ if not include_build:
70
+ services: dict=compose_data.get('services',{})
71
+ for (k,v) in services.items():
72
+ if 'build' in v:
73
+ del v['build']
69
74
  if stack:
70
75
  base_dir = os.path.dirname(os.path.abspath(compose_file))
71
76
  if "configs" in compose_data:
72
77
  compose_data["configs"] = self._process_x_content(compose_data["configs"], self.docker.config,base_dir=base_dir,stack=stack)
73
78
  if "secrets" in compose_data:
74
79
  compose_data["secrets"] = self._process_x_content(compose_data["secrets"], self.docker.secret,base_dir=base_dir,stack=stack)
75
- return envsubst(yaml.dump(compose_data))
80
+
81
+ # Define the replacements for '$' to '$$' for env variables in compose files
82
+ replacements_map = {"$": "$$"}
83
+ return envsubst(yaml.dump(compose_data), replacements=replacements_map)
76
84
 
77
85
  def decode_yaml(self,data:str)->dict:
78
86
  return yaml.safe_load(data)
79
87
 
80
- def render_compose_file(self, compose_file,stack=None):
88
+ def render_compose_file(self, compose_file,stack=None,include_build=True):
81
89
  """
82
90
  Render the Docker Compose file with environment variables and create Docker configs/secrets.
83
91
  """
84
92
 
85
93
  # Convert the modified data back to YAML
86
- rendered_content = self.rendered_compose_file(compose_file,stack)
94
+ rendered_content = self.rendered_compose_file(compose_file,stack,include_build=include_build)
87
95
 
88
96
  # Write the rendered file
89
97
  rendered_filename = Path(compose_file).with_name(
90
98
  f"{Path(compose_file).stem}-rendered{Path(compose_file).suffix}"
91
99
  )
92
- with open(rendered_filename, "w") as f:
93
- f.write(rendered_content)
94
- with open(rendered_filename.as_posix()+".json","w") as f:
95
- f.write(json.dumps(rendered_content,indent=2))
96
100
  return (rendered_filename,rendered_content)
97
101
 
98
102
 
@@ -262,7 +266,7 @@ class DockerStack:
262
266
  self.commands.append(Command(cmd,give_console=True))
263
267
 
264
268
  def deploy(self, stack_name, compose_file, with_registry_auth=False,tag=None):
265
- rendered_filename, rendered_content = self.render_compose_file(compose_file,stack=stack_name)
269
+ rendered_filename, rendered_content = self.render_compose_file(compose_file,stack=stack_name,include_build=False)
266
270
  labels = [f"mesudip.stack.name={stack_name}"]
267
271
  if tag:
268
272
  labels.append(f"mesudip.stack.tag={tag}")
@@ -361,9 +365,9 @@ def main(args:List[str]=None):
361
365
  # Ls command
362
366
  subparsers.add_parser("ls",help="List docker-stacks")
363
367
 
364
- cat_parser = subparsers.add_parser("cat",help="Print the docker compose of specific version")
368
+ cat_parser = subparsers.add_parser("cat",help="Print the docker compose of specific version. Defaults to latest version if not specified.")
365
369
  cat_parser.add_argument("stack_name", help="Name of the stack")
366
- cat_parser.add_argument("version", help="Stack version to cat")
370
+ cat_parser.add_argument("version", nargs='?', help="Stack version to cat. Defaults to latest if omitted.")
367
371
 
368
372
  checkout_parser = subparsers.add_parser("checkout",help="Deploy specific version of the stack")
369
373
  checkout_parser.add_argument("stack_name", help="Name of the stack")
@@ -398,7 +402,17 @@ def main(args:List[str]=None):
398
402
  elif args.command == "rm":
399
403
  docker.stack.rm(args.stack_name)
400
404
  elif args.command == 'cat':
401
- print(docker.stack.cat(args.stack_name,args.version))
405
+ version_to_cat = args.version
406
+ if version_to_cat is None:
407
+ versions_list = docker.stack.versions(args.stack_name)
408
+ if versions_list:
409
+ # Assuming versions are integers, find the maximum
410
+ latest_version = max(int(v[0]) for v in versions_list if v[0].isdigit())
411
+ version_to_cat = str(latest_version)
412
+ else:
413
+ print(f"No versions found for stack '{args.stack_name}'.")
414
+ sys.exit(1)
415
+ print(docker.stack.cat(args.stack_name, version_to_cat))
402
416
  elif args.command == 'checkout':
403
417
  docker.stack.checkout(args.stack_name,args.version)
404
418
  elif args.command == 'versions' or args.command == "version":
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ NAME
4
+ envsubst.py - substitutes environment variables in bash format strings
5
+
6
+ DESCRIPTION
7
+ envsubst.py is an upgrade of the POSIX command `envsubst`
8
+
9
+ supported syntax:
10
+ normal - ${VARIABLE1} or $VARIABLE1
11
+ with default - ${VARIABLE1:-somevalue}
12
+ """
13
+
14
+ from dataclasses import dataclass
15
+ import os
16
+ import re
17
+ import sys
18
+ from typing import Dict, List, Literal, Optional
19
+
20
+
21
+ @dataclass
22
+ class LineCheckResult:
23
+ line_no: int
24
+ line_content: str
25
+ variable_name: Optional[str] = None # None means no error
26
+ start_index: Optional[int] = None # Start index of the variable in the line_content
27
+
28
+ @property
29
+ def has_error(self) -> bool:
30
+ return self.variable_name is not None
31
+
32
+ def __str__(self):
33
+ if self.has_error:
34
+ return (f"ERROR :: Missing variable: '{self.variable_name}' "
35
+ f"on line {self.line_no}: '{self.line_content}'")
36
+ return f"OK :: Line {self.line_no}: '{self.line_content}'"
37
+
38
+
39
+ class SubstitutionError(Exception):
40
+ """Custom exception to collect multiple substitution errors with detailed information."""
41
+ def __init__(self, results: List[LineCheckResult], template_str: str, underline_char: str = '\u0333'):
42
+ self.results = results
43
+ self.template_lines = template_str.splitlines(keepends=False)
44
+ self.underline_char = underline_char
45
+ super().__init__(self._format_messages(results))
46
+
47
+ def _format_messages(self, results: List[LineCheckResult]) -> str:
48
+ formatted_messages = []
49
+
50
+ # Group results by line number
51
+ errors_by_line = {}
52
+ for result in results:
53
+ if result.has_error:
54
+ if result.line_no not in errors_by_line:
55
+ errors_by_line[result.line_no] = {
56
+ 'line_content': result.line_content,
57
+ 'variables': []
58
+ }
59
+ errors_by_line[result.line_no]['variables'].append({
60
+ 'name': result.variable_name,
61
+ 'start_index': result.start_index
62
+ })
63
+
64
+ # Sort line numbers
65
+ sorted_line_nos = sorted(errors_by_line.keys())
66
+
67
+ last_printed_line = 0
68
+
69
+ for line_no in sorted_line_nos:
70
+ # Add separator if there's a gap from the last context block
71
+ if last_printed_line > 0 and (line_no - 2) > (last_printed_line + 1):
72
+ formatted_messages.append("")
73
+
74
+ # Determine context lines to display
75
+ start_context_line = max(last_printed_line + 1, line_no - 2)
76
+ end_context_line = min(len(self.template_lines), line_no + 2)
77
+
78
+ for current_ln in range(start_context_line, end_context_line + 1):
79
+ line_text = self.template_lines[current_ln - 1]
80
+
81
+ # If this is an error line, apply the underlining
82
+ if current_ln in errors_by_line:
83
+ current_line_errors = errors_by_line[current_ln]['variables']
84
+
85
+ # Sort errors by start_index in reverse to avoid index shifting issues
86
+ current_line_errors.sort(key=lambda x: x['start_index'], reverse=True)
87
+
88
+ modified_line_text_chars = list(line_text) # Convert to list for mutability
89
+
90
+ for var_info in current_line_errors:
91
+ var_name = var_info['name']
92
+ start_idx = var_info['start_index']
93
+
94
+ # Insert underline_char after each character of the variable name
95
+ for k in range(len(var_name) - 1, -1, -1):
96
+ insert_pos = start_idx + k + 1 # Position after the character
97
+ modified_line_text_chars.insert(insert_pos, self.underline_char)
98
+
99
+ line_text = "".join(modified_line_text_chars)
100
+
101
+ formatted_messages.append(f"{current_ln:3d} {line_text}")
102
+
103
+ last_printed_line = end_context_line
104
+
105
+ return "\n".join(formatted_messages)
106
+
107
+
108
+
109
+ def envsubst(template_str, env=os.environ, replacements: Dict[str, str] = None, on_error: Literal['exit','throw'] = 'exit'):
110
+ """Substitute environment variables in the template string, supporting default values."""
111
+
112
+ # Combined regex for ${VAR:-default} and $VAR, and also $$
113
+ pattern = re.compile(r"\$\_ESCAPED_DOLLAR_|\$\{([^}:\s]+)(?::-(.*?))?\}|\$([a-zA-Z_][a-zA-Z0-9_]*)")
114
+
115
+ # Handle escaped dollars
116
+ template_str = template_str.replace("$$", "$_ESCAPED_DOLLAR_")
117
+
118
+ lines = template_str.splitlines(True) # keepends=True
119
+ processed_lines = []
120
+ error_results: List[LineCheckResult] = []
121
+
122
+ for i, original_line in enumerate(lines):
123
+ line_no = i + 1
124
+
125
+ line_errors_raw = [] # Store (var_name, start_index) tuples
126
+
127
+ def replacer(match: re.Match[str]):
128
+ if match.group(0) == '$_ESCAPED_DOLLAR_':
129
+ return '$$'
130
+ # Group 1, 2 for ${VAR:-default}
131
+ if match.group(1) is not None:
132
+ var = match.group(1)
133
+ default_value = match.group(2) if match.group(2) is not None else None
134
+ result = env.get(var, default_value)
135
+ if result is None:
136
+ line_errors_raw.append((var, match.start(1))) # Use match.start(1) for ${VAR}
137
+ return match.group(0) # Keep original if variable not found
138
+ # Group 3 for $VAR
139
+ else:
140
+ var = match.group(3)
141
+ result = env.get(var, None)
142
+ if result is None:
143
+ line_errors_raw.append((var, match.start(3))) # Use match.start(3) for $VAR
144
+ return match.group(0) # Keep original if variable not found
145
+
146
+ if replacements:
147
+ for old, new in replacements.items():
148
+ result = result.replace(old, new)
149
+ return result
150
+
151
+ processed_line = pattern.sub(replacer, original_line)
152
+
153
+ processed_lines.append(processed_line)
154
+
155
+ if line_errors_raw:
156
+ # The original line content for error reporting should not have the escaped dollar placeholder.
157
+ # It should also retain its leading/trailing whitespace for accurate caret positioning.
158
+ error_line_content_for_report = original_line.replace("$_ESCAPED_DOLLAR_", "$$").rstrip('\n') # Remove only trailing newline
159
+
160
+ # Use a set of tuples to store unique (var_name, start_index) pairs for this line
161
+ unique_errors_on_line = set()
162
+ for var_name, start_index in line_errors_raw:
163
+ unique_errors_on_line.add((var_name, start_index))
164
+
165
+ for var_name, start_index in sorted(list(unique_errors_on_line), key=lambda x: x[1]): # Sort by start_index
166
+ error_results.append(LineCheckResult(line_no=line_no, line_content=error_line_content_for_report, variable_name=var_name, start_index=start_index))
167
+
168
+ if error_results:
169
+ # Sort errors by line number, then by start_index
170
+ error_results.sort(key=lambda x: (x.line_no, x.start_index))
171
+ if on_error == 'exit':
172
+ error_output = SubstitutionError(error_results, template_str)._format_messages(error_results) # Pass template_str
173
+ print(error_output, file=sys.stderr)
174
+ exit(1)
175
+ elif on_error == 'throw':
176
+ raise SubstitutionError(error_results, template_str) # Pass template_str
177
+
178
+ result_str = "".join(processed_lines)
179
+ # Restore escaped dollars
180
+ result_str = result_str.replace("$_ESCAPED_DOLLAR_", "$$")
181
+
182
+ return result_str
183
+
184
+
185
+ def envsubst_load_file(template_file, env=os.environ, replacements: Dict[str, str] = None, on_error: str = 'exit'):
186
+ with open(template_file) as file:
187
+ return envsubst(file.read(), env, replacements, on_error)
188
+
189
+ def main():
190
+ if len(sys.argv) > 2:
191
+ print("Usage: python envsubst.py [template_file]")
192
+ sys.exit(1)
193
+
194
+ if len(sys.argv) == 2:
195
+ template_file = sys.argv[1]
196
+ with open(template_file, "r") as file:
197
+ template_str = file.read()
198
+ else:
199
+ template_str = sys.stdin.read()
200
+
201
+ result = envsubst(template_str)
202
+ print(result)
203
+
204
+
205
+ if __name__ == "__main__":
206
+ main()
@@ -11,9 +11,11 @@ DESCRIPTION
11
11
  import os
12
12
  import re
13
13
  import sys
14
+ from typing import Dict
15
+ from .envsubst import SubstitutionError, envsubst as base_envsubst
14
16
 
15
17
 
16
- def envsubst(template_str, env=os.environ):
18
+ def envsubst(template_str, env=os.environ, replacements: Dict[str, str] = None, on_error: str = 'exit'):
17
19
  """Substitute environment variables in the template string, supporting default values."""
18
20
 
19
21
  # Regex for ${VARIABLE} with optional default
@@ -29,6 +31,10 @@ def envsubst(template_str, env=os.environ):
29
31
  if result is None:
30
32
  print(f"Missing template variable with default: {var}", file=sys.stderr)
31
33
  exit(1)
34
+
35
+ if replacements:
36
+ for old, new in replacements.items():
37
+ result = result.replace(old, new)
32
38
  return result
33
39
 
34
40
  def replace_without_default(match):
@@ -37,6 +43,10 @@ def envsubst(template_str, env=os.environ):
37
43
  if result is None:
38
44
  print(f"Missing template variable: {var}", file=sys.stderr)
39
45
  exit(1)
46
+
47
+ if replacements:
48
+ for old, new in replacements.items():
49
+ result = result.replace(old, new)
40
50
  return result
41
51
 
42
52
  # Substitute variables with default values
@@ -48,7 +58,7 @@ def envsubst(template_str, env=os.environ):
48
58
  return template_str
49
59
 
50
60
 
51
- def merge_files_from_directories(directories, file_extension=None):
61
+ def merge_files_from_directories(directories, file_extension=None, on_error: str = 'exit'):
52
62
  merged_content = []
53
63
 
54
64
  for path in directories:
@@ -85,8 +95,11 @@ def merge_files_from_directories(directories, file_extension=None):
85
95
  # Strip extra empty lines from the beginning and end
86
96
  result = result.strip()
87
97
 
88
- # Perform environment variable substitution on the final result
89
- return envsubst(result)
98
+ # Define the replacements for '$' to '$$'
99
+ replacements_map = {"$": "$$"}
100
+
101
+ # Perform environment variable substitution on the final result with replacements
102
+ return base_envsubst(result, replacements=replacements_map, on_error=on_error)
90
103
 
91
104
 
92
105
  def main():
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: docker-stack
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: CLI for deploying and managing Docker stacks.
5
- Home-page: https://github.com/mesuidp/docker-stack
5
+ Home-page: https://github.com/mesudip/docker-stack
6
6
  Author: Sudip Bhattarai
7
7
  Author-email: sudip@bhattarai.me
8
8
  Classifier: Programming Language :: Python :: 3
@@ -3,3 +3,6 @@ testpaths = [
3
3
  "tests",
4
4
  ]
5
5
  pythonpath = ["docker_stack", "."]
6
+
7
+ [tool.black]
8
+ line-length = 140
@@ -2,13 +2,13 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="docker-stack",
5
- version="0.2.3",
5
+ version="0.2.5",
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
10
  author_email="sudip@bhattarai.me",
11
- url="https://github.com/mesuidp/docker-stack", # Replace with your project URL
11
+ url="https://github.com/mesudip/docker-stack", # Replace with your project URL
12
12
  packages=find_packages(),
13
13
  install_requires=[
14
14
  "PyYAML"
@@ -1,109 +0,0 @@
1
- #!/usr/bin/python3
2
- """
3
- NAME
4
- envsubst.py - substitutes environment variables in bash format strings
5
-
6
- DESCRIPTION
7
- envsubst.py is an upgrade of the POSIX command `envsubst`
8
-
9
- supported syntax:
10
- normal - ${VARIABLE1} or $VARIABLE1
11
- with default - ${VARIABLE1:-somevalue}
12
- """
13
-
14
- import os
15
- import re
16
- import sys
17
-
18
-
19
- def envsubst(template_str, env=os.environ):
20
- """Substitute environment variables in the template string, supporting default values."""
21
-
22
- # Regex for ${VARIABLE} with optional default
23
- pattern_with_default = re.compile(r"\$\{([^}:\s]+)(?::-(.*?))?\}")
24
-
25
- # Regex for $VARIABLE without default
26
- pattern_without_default = re.compile(r"\$([a-zA-Z_][a-zA-Z0-9_]*)")
27
-
28
- template_str = template_str.replace("$$", "__ESCAPED_DOLLAR__")
29
- def print_error_line(template_str, match_span):
30
- """Helper function to print the error context."""
31
- lines = template_str.splitlines()
32
-
33
- # Determine the start position and line
34
- start_pos = match_span[0]
35
- end_pos = match_span[1]
36
-
37
- # Calculate line numbers based on character positions
38
- char_count = 0
39
- start_line = end_line = None
40
- for i, line in enumerate(lines):
41
- char_count += len(line) + 1 # +1 for the newline character
42
- if start_line is None and char_count > start_pos:
43
- start_line = i
44
- if char_count >= end_pos:
45
- end_line = i
46
- break
47
-
48
- # Display lines before, the error line, and after (with line numbers)
49
- start = max(start_line - 1, 0)
50
- end = min(end_line + 1, len(lines) - 1)
51
-
52
- for i in range(start, end + 1):
53
- print(f"{i + 1}: {lines[i]}",file=sys.stderr)
54
-
55
- def replace_with_default(match: re.Match[str]):
56
- var = match.group(1)
57
- default_value = match.group(2) if match.group(2) is not None else None
58
- result = env.get(var, default_value)
59
- if result is None:
60
- print_error_line(template_str, match.span())
61
- print(f"ERROR :: Missing template variable with default: {var}", file=sys.stderr)
62
-
63
- exit(1)
64
- return result
65
-
66
- def replace_without_default(match: re.Match[str]):
67
- var = match.group(1)
68
- result = env.get(var, None)
69
- if result is None:
70
- print_error_line(template_str, match.span())
71
- print(f"ERROR :: Missing template variable: {var}", file=sys.stderr)
72
- exit(1)
73
- return result
74
-
75
- # Substitute variables with default values
76
- template_str = pattern_with_default.sub(replace_with_default, template_str)
77
-
78
- # Substitute variables without default values
79
- template_str = pattern_without_default.sub(replace_without_default, template_str)
80
-
81
- template_str = template_str.replace("__ESCAPED_DOLLAR__", "$")
82
-
83
- return template_str
84
-
85
-
86
- def envsubst_load_file(template_file,env=os.environ):
87
- with open(template_file) as file:
88
- return envsubst(file.read(),env)
89
-
90
- def main():
91
- if len(sys.argv) > 2:
92
- print("Usage: python envsubst.py [template_file]")
93
- sys.exit(1)
94
-
95
- if len(sys.argv) == 2:
96
- template_file = sys.argv[1]
97
- with open(template_file, "r") as file:
98
- template_str = file.read()
99
- else:
100
- template_str = sys.stdin.read()
101
-
102
- result = envsubst(template_str)
103
-
104
- print(result)
105
-
106
-
107
- if __name__ == "__main__":
108
- main()
109
-
File without changes
File without changes