docker-stack 0.1.1__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.1.1/PKG-INFO +19 -0
- docker-stack-0.1.1/README.md +6 -0
- docker-stack-0.1.1/docker_stack/__init__.py +21 -0
- docker-stack-0.1.1/docker_stack/cli.py +169 -0
- docker-stack-0.1.1/docker_stack/compose.py +17 -0
- docker-stack-0.1.1/docker_stack/docker_objects.py +221 -0
- docker-stack-0.1.1/docker_stack/envsubst.py +72 -0
- docker-stack-0.1.1/docker_stack/envsubst_merge.py +125 -0
- docker-stack-0.1.1/docker_stack/helpers.py +107 -0
- docker-stack-0.1.1/docker_stack/merge_conf.py +52 -0
- docker-stack-0.1.1/docker_stack/registry.py +150 -0
- docker-stack-0.1.1/docker_stack.egg-info/PKG-INFO +19 -0
- docker-stack-0.1.1/docker_stack.egg-info/SOURCES.txt +18 -0
- docker-stack-0.1.1/docker_stack.egg-info/dependency_links.txt +1 -0
- docker-stack-0.1.1/docker_stack.egg-info/entry_points.txt +2 -0
- docker-stack-0.1.1/docker_stack.egg-info/requires.txt +1 -0
- docker-stack-0.1.1/docker_stack.egg-info/top_level.txt +1 -0
- docker-stack-0.1.1/pyproject.toml +5 -0
- docker-stack-0.1.1/setup.cfg +4 -0
- docker-stack-0.1.1/setup.py +27 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: docker-stack
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: CLI for deploying and managing Docker stacks.
|
|
5
|
+
Home-page: https://github.com/mesuidp/docker-stack
|
|
6
|
+
Author: Sudip Bhattarai
|
|
7
|
+
Author-email: sudip.dev.np@gmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
Docker Stack
|
|
15
|
+
==============
|
|
16
|
+
cli utility for stack deployment in docker-swarm.
|
|
17
|
+
#### Features
|
|
18
|
+
- docker config and secret creation and versioning
|
|
19
|
+
- docker stack versioning and config backup for rollback
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .envsubst import envsubst
|
|
2
|
+
from .compose import read_compose_file
|
|
3
|
+
from .docker_objects import DockerConfig, DockerSecret
|
|
4
|
+
from .cli import main
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Functions:
|
|
8
|
+
|
|
9
|
+
1. read_compose_file(compose_file_path):
|
|
10
|
+
- Reads a Docker Compose YAML file and returns its contents as a dictionary.
|
|
11
|
+
|
|
12
|
+
2. create_config(config_name, config_content):
|
|
13
|
+
- Creates a Docker config from the given string content. Adds a label with the SHA256 hash of the config content.
|
|
14
|
+
|
|
15
|
+
3. check_config(config_name):
|
|
16
|
+
- Checks if a Docker config with the given name already exists.
|
|
17
|
+
|
|
18
|
+
4. update_config(config_name, config_content):
|
|
19
|
+
- Updates an existing Docker config or creates a new one if it doesn't exist.
|
|
20
|
+
If the config exists, checks the SHA256 hash and recreates the config if the hash doesn't match.
|
|
21
|
+
"""
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List
|
|
6
|
+
from docker_stack import DockerConfig, DockerSecret
|
|
7
|
+
import os
|
|
8
|
+
import yaml
|
|
9
|
+
import json
|
|
10
|
+
from docker_stack.docker_objects import DockerObjectManager
|
|
11
|
+
from docker_stack.helpers import Command
|
|
12
|
+
from docker_stack.registry import DockerRegistry
|
|
13
|
+
from .envsubst import envsubst
|
|
14
|
+
|
|
15
|
+
class Docker:
|
|
16
|
+
def __init__(self,resgistry_url='https://docker.io',userpass=''):
|
|
17
|
+
self.stack = DockerStack(self)
|
|
18
|
+
self.config = DockerConfig()
|
|
19
|
+
self.secret = DockerSecret()
|
|
20
|
+
self.registry = DockerRegistry(resgistry_url, userpass)
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def load_env(env_file=".env"):
|
|
24
|
+
if Path(env_file).is_file():
|
|
25
|
+
with open(env_file) as f:
|
|
26
|
+
for line in f:
|
|
27
|
+
line = line.strip()
|
|
28
|
+
if line and not line.startswith("#"):
|
|
29
|
+
key, _, value = line.partition("=")
|
|
30
|
+
os.environ[key.strip()] = value.strip()
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def check_env(example_file=".env.example"):
|
|
34
|
+
if not Path(example_file).is_file():
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
unset_keys = []
|
|
38
|
+
with open(example_file) as f:
|
|
39
|
+
for line in f:
|
|
40
|
+
line = line.strip()
|
|
41
|
+
if line and not line.startswith("#"):
|
|
42
|
+
key = line.split("=")[0].strip()
|
|
43
|
+
if not os.environ.get(key):
|
|
44
|
+
unset_keys.append(key)
|
|
45
|
+
|
|
46
|
+
if unset_keys:
|
|
47
|
+
print("The following keys are not set in the environment:")
|
|
48
|
+
for key in unset_keys:
|
|
49
|
+
print(f"- {key}")
|
|
50
|
+
print("Exiting due to missing environment variables.")
|
|
51
|
+
sys.exit(2)
|
|
52
|
+
|
|
53
|
+
class DockerStack:
|
|
54
|
+
def __init__(self, docker: Docker):
|
|
55
|
+
self.docker = docker
|
|
56
|
+
self.commands: List[Command] = []
|
|
57
|
+
|
|
58
|
+
def render_compose_file(self, compose_file):
|
|
59
|
+
"""
|
|
60
|
+
Render the Docker Compose file with environment variables and create Docker configs/secrets.
|
|
61
|
+
"""
|
|
62
|
+
with open(compose_file) as f:
|
|
63
|
+
template_content = f.read()
|
|
64
|
+
rendered_content = envsubst(template_content)
|
|
65
|
+
|
|
66
|
+
# Parse the YAML content
|
|
67
|
+
compose_data = yaml.safe_load(rendered_content)
|
|
68
|
+
|
|
69
|
+
# Process configs and secrets with x-content
|
|
70
|
+
if "configs" in compose_data:
|
|
71
|
+
compose_data["configs"] = self._process_x_content(compose_data["configs"], self.docker.config)
|
|
72
|
+
if "secrets" in compose_data:
|
|
73
|
+
compose_data["secrets"] = self._process_x_content(compose_data["secrets"], self.docker.secret)
|
|
74
|
+
|
|
75
|
+
# Convert the modified data back to YAML
|
|
76
|
+
|
|
77
|
+
rendered_content = yaml.dump(compose_data)
|
|
78
|
+
|
|
79
|
+
# Write the rendered file
|
|
80
|
+
rendered_filename = Path(compose_file).with_name(
|
|
81
|
+
f"{Path(compose_file).stem}-rendered{Path(compose_file).suffix}"
|
|
82
|
+
)
|
|
83
|
+
with open(rendered_filename, "w") as f:
|
|
84
|
+
f.write(rendered_content)
|
|
85
|
+
with open(rendered_filename.as_posix()+".json","w") as f:
|
|
86
|
+
f.write(json.dumps(compose_data,indent=2))
|
|
87
|
+
return (rendered_filename,rendered_content)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _process_x_content(self, objects, manager:DockerObjectManager):
|
|
92
|
+
"""
|
|
93
|
+
Process configs or secrets with x-content keys.
|
|
94
|
+
Returns a tuple: (processed_objects, commands)
|
|
95
|
+
"""
|
|
96
|
+
processed_objects = {}
|
|
97
|
+
for name, details in objects.items():
|
|
98
|
+
if isinstance(details, dict) and "x-content" in details:
|
|
99
|
+
# Create the Docker object (config or secret)
|
|
100
|
+
(object_name,command)=manager.create(name, details['x-content'])
|
|
101
|
+
if not command.isNop():
|
|
102
|
+
self.commands.append(command)
|
|
103
|
+
# Replace x-content with the name of the created object
|
|
104
|
+
processed_objects[name] = {"name": object_name,"external": True}
|
|
105
|
+
else:
|
|
106
|
+
processed_objects[name] = details
|
|
107
|
+
return processed_objects
|
|
108
|
+
|
|
109
|
+
def deploy(self, stack_name, compose_file, with_registry_auth=False):
|
|
110
|
+
rendered_filename, rendered_content = self.render_compose_file(compose_file)
|
|
111
|
+
_, cmd = self.docker.config.increment(stack_name, rendered_content, [f"mesudip.stack.name={stack_name}"])
|
|
112
|
+
if not cmd.isNop():
|
|
113
|
+
self.commands.append(cmd)
|
|
114
|
+
cmd = ["docker", "stack", "deploy", "-c", str(rendered_filename), stack_name]
|
|
115
|
+
if with_registry_auth:
|
|
116
|
+
cmd.insert(3, "--with-registry-auth")
|
|
117
|
+
self.commands.append(Command(cmd))
|
|
118
|
+
|
|
119
|
+
def push(self, compose_file, credentials):
|
|
120
|
+
with open(compose_file) as f:
|
|
121
|
+
compose_data = yaml.safe_load(f)
|
|
122
|
+
for service_name, service_data in compose_data.get("services", {}).items():
|
|
123
|
+
if "build" in service_data:
|
|
124
|
+
build_path = service_data["build"]
|
|
125
|
+
print(f"++ docker build -t {service_data['image']} {build_path}")
|
|
126
|
+
build_command = ["docker", "build", "-t", service_data['image'], build_path.get('context', '.')]
|
|
127
|
+
self.commands.append(Command(build_command))
|
|
128
|
+
push_result = self.check_and_push_pull_image(service_data['image'], 'push')
|
|
129
|
+
if push_result:
|
|
130
|
+
self.commands.append(push_result)
|
|
131
|
+
else:
|
|
132
|
+
print("No need to push: Already exists")
|
|
133
|
+
|
|
134
|
+
def check_and_push_pull_image(self, image_name: str, action: str):
|
|
135
|
+
if self.docker.registry.check_image(image_name):
|
|
136
|
+
print(f"Image {image_name} already in the registry.")
|
|
137
|
+
return None
|
|
138
|
+
if action == 'push':
|
|
139
|
+
print(f"Pushing image {image_name} to the registry...")
|
|
140
|
+
cmd = self.docker.registry.push(image_name)
|
|
141
|
+
if cmd:
|
|
142
|
+
self.commands.append(cmd)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main():
|
|
146
|
+
parser = argparse.ArgumentParser(description="Deploy and manage Docker stacks.")
|
|
147
|
+
parser.add_argument("command", choices=["deploy", "push"], help="Command to execute")
|
|
148
|
+
parser.add_argument("stack_name", help="Name of the stack", nargs="?")
|
|
149
|
+
parser.add_argument("compose_file", help="Path to the compose file")
|
|
150
|
+
parser.add_argument("--with-registry-auth", action="store_true", help="Use registry authentication")
|
|
151
|
+
parser.add_argument("-u", "--user", help="Registry credentials in format username:password", required=False)
|
|
152
|
+
|
|
153
|
+
args = parser.parse_args()
|
|
154
|
+
docker = Docker(resgistry_url="https://registry.sireto.io",userpass='admin:69a017f5de7509e5e7ab0e89a5687dbda58f4fa70762bee17d2e454704bd7a4f')
|
|
155
|
+
docker.load_env()
|
|
156
|
+
docker.check_env()
|
|
157
|
+
docker.registry.login()
|
|
158
|
+
|
|
159
|
+
if args.command == "push":
|
|
160
|
+
docker.stack.push(args.compose_file, args.user)
|
|
161
|
+
else:
|
|
162
|
+
docker.stack.deploy(args.stack_name, args.compose_file, args.with_registry_auth)
|
|
163
|
+
|
|
164
|
+
print("Commands to Execute")
|
|
165
|
+
[print(" >", x) for x in docker.stack.commands] if docker.stack.commands else print("-- empty --")
|
|
166
|
+
[x.execute() for x in docker.stack.commands]
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
main()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def read_compose_file(compose_file_path):
|
|
5
|
+
"""
|
|
6
|
+
Reads a Docker Compose YAML file and returns its contents as a dictionary.
|
|
7
|
+
|
|
8
|
+
:param compose_file_path: Path to the Docker Compose file.
|
|
9
|
+
:return: Dictionary representation of the YAML contents.
|
|
10
|
+
"""
|
|
11
|
+
if not os.path.exists(compose_file_path):
|
|
12
|
+
raise FileNotFoundError(f"Compose file {compose_file_path} does not exist.")
|
|
13
|
+
|
|
14
|
+
with open(compose_file_path, "r") as file:
|
|
15
|
+
compose_data = yaml.safe_load(file)
|
|
16
|
+
|
|
17
|
+
return compose_data
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import re
|
|
3
|
+
import json
|
|
4
|
+
from typing import List, Tuple
|
|
5
|
+
from docker_stack.helpers import Command, run_cli_command # Import the helper function
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DockerObjectManager:
|
|
9
|
+
def __init__(self, object_type, log=False):
|
|
10
|
+
self.object_type = object_type
|
|
11
|
+
self.log = log
|
|
12
|
+
|
|
13
|
+
def _create(
|
|
14
|
+
self, object_name, object_content, labels: List[str] = [], sha_hash=None
|
|
15
|
+
):
|
|
16
|
+
sha_hash = (
|
|
17
|
+
sha_hash if sha_hash else self.calculate_hash(object_name, object_content)
|
|
18
|
+
)
|
|
19
|
+
command = ["docker", self.object_type, "create"]
|
|
20
|
+
command.append("--label")
|
|
21
|
+
command.append(f"sha256={sha_hash}")
|
|
22
|
+
|
|
23
|
+
for label in labels:
|
|
24
|
+
command.append("--label")
|
|
25
|
+
command.append(label.strip())
|
|
26
|
+
|
|
27
|
+
command.append(object_name)
|
|
28
|
+
command.append("-")
|
|
29
|
+
|
|
30
|
+
# Return a Command object with the object_content as stdin
|
|
31
|
+
return Command(command, stdin=object_content, id=sha_hash)
|
|
32
|
+
|
|
33
|
+
def calculate_hash(self, object_name, object_content):
|
|
34
|
+
hash_input = f"{self.object_type}{object_name}{object_content}"
|
|
35
|
+
return hashlib.sha256(hash_input.encode("utf-8")).hexdigest()
|
|
36
|
+
|
|
37
|
+
def check(self, object_name):
|
|
38
|
+
command = ["docker", self.object_type, "inspect", object_name]
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
# Use run_cli_command instead of subprocess.run
|
|
42
|
+
run_cli_command(command, raise_error=True, log=self.log)
|
|
43
|
+
return True
|
|
44
|
+
except Exception:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
def create(
|
|
48
|
+
self, object_name, object_content, labels: List[str] = []
|
|
49
|
+
) -> Tuple[str, Command]:
|
|
50
|
+
sha_hash = self.calculate_hash(object_name, object_content)
|
|
51
|
+
|
|
52
|
+
# Check if any version of the object already exists by its label
|
|
53
|
+
command = [
|
|
54
|
+
"docker",
|
|
55
|
+
self.object_type,
|
|
56
|
+
"ls",
|
|
57
|
+
"--filter",
|
|
58
|
+
f"label=mesudip.object.name={object_name}",
|
|
59
|
+
"--format",
|
|
60
|
+
"{{json .}}",
|
|
61
|
+
]
|
|
62
|
+
output = run_cli_command(command, raise_error=True, log=self.log)
|
|
63
|
+
|
|
64
|
+
# Parse existing versions
|
|
65
|
+
existing_versions = {}
|
|
66
|
+
max_version = 0
|
|
67
|
+
|
|
68
|
+
for line in output.splitlines():
|
|
69
|
+
object_info = json.loads(line)
|
|
70
|
+
object_name_in_docker = object_info["Name"]
|
|
71
|
+
if object_name_in_docker == object_name:
|
|
72
|
+
max_version = max(max_version, 1)
|
|
73
|
+
existing_versions[1] = object_info
|
|
74
|
+
else:
|
|
75
|
+
match = re.search(r"_(v\d+)$", object_name_in_docker)
|
|
76
|
+
if match:
|
|
77
|
+
version = int(match.group(1)[1:])
|
|
78
|
+
max_version = max(version, max_version)
|
|
79
|
+
existing_versions[version] = object_info
|
|
80
|
+
|
|
81
|
+
# Determine the next version number
|
|
82
|
+
new_version_suffix = ""
|
|
83
|
+
if len(existing_versions) > 0:
|
|
84
|
+
new_version = max_version + 1
|
|
85
|
+
new_version_suffix = f"_v{new_version}"
|
|
86
|
+
else:
|
|
87
|
+
new_version = 1
|
|
88
|
+
|
|
89
|
+
new_object_name = f"{object_name}{new_version_suffix}"
|
|
90
|
+
|
|
91
|
+
# Check if the SHA hash for the new content already exists in any version
|
|
92
|
+
existing_sha_hash = None
|
|
93
|
+
matching_object = None
|
|
94
|
+
|
|
95
|
+
for object_info in existing_versions.values():
|
|
96
|
+
object_name_in_docker = object_info["Name"]
|
|
97
|
+
parsed_labels = parse_labels(object_info["Labels"])
|
|
98
|
+
object_sha_hash = parsed_labels.get("sha256")
|
|
99
|
+
|
|
100
|
+
if object_sha_hash == sha_hash:
|
|
101
|
+
existing_sha_hash = sha_hash
|
|
102
|
+
matching_object = object_info
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
if existing_sha_hash == sha_hash:
|
|
106
|
+
existing_name = matching_object["Name"]
|
|
107
|
+
if self.log:
|
|
108
|
+
print(
|
|
109
|
+
f"{self.object_type.capitalize()} {existing_name} already exists with the same SHA hash. No update needed."
|
|
110
|
+
)
|
|
111
|
+
return existing_name, Command.nop
|
|
112
|
+
|
|
113
|
+
if self.log:
|
|
114
|
+
print(f"SHA mismatch. Creating a new version: {new_object_name}")
|
|
115
|
+
labels = [
|
|
116
|
+
f"mesudip.object.version={new_version:01d}",
|
|
117
|
+
f"mesudip.object.name={object_name}",
|
|
118
|
+
] + labels
|
|
119
|
+
return new_object_name, self._create(
|
|
120
|
+
new_object_name, object_content, labels, sha_hash=sha_hash
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def increment(
|
|
124
|
+
self, object_name, object_content, labels: List[str] = []
|
|
125
|
+
) -> Tuple[str, Command]:
|
|
126
|
+
sha_hash = self.calculate_hash(object_name, object_content)
|
|
127
|
+
|
|
128
|
+
# Check if any version of the object already exists by its label
|
|
129
|
+
command = [
|
|
130
|
+
"docker",
|
|
131
|
+
self.object_type,
|
|
132
|
+
"ls",
|
|
133
|
+
"--filter",
|
|
134
|
+
f"label=mesudip.object.name={object_name}",
|
|
135
|
+
"--format",
|
|
136
|
+
"{{json .}}",
|
|
137
|
+
]
|
|
138
|
+
output = run_cli_command(command, raise_error=True, log=self.log)
|
|
139
|
+
|
|
140
|
+
# Parse existing versions
|
|
141
|
+
existing_versions = {}
|
|
142
|
+
max_version = 0
|
|
143
|
+
|
|
144
|
+
for line in output.splitlines():
|
|
145
|
+
object_info = json.loads(line)
|
|
146
|
+
object_name_in_docker = object_info["Name"]
|
|
147
|
+
if object_name_in_docker == object_name:
|
|
148
|
+
max_version = max(max_version, 1)
|
|
149
|
+
existing_versions[1] = object_info
|
|
150
|
+
else:
|
|
151
|
+
match = re.search(r"_(v\d+)$", object_name_in_docker)
|
|
152
|
+
if match:
|
|
153
|
+
version = int(match.group(1)[1:])
|
|
154
|
+
max_version = max(version, max_version)
|
|
155
|
+
existing_versions[version] = object_info
|
|
156
|
+
|
|
157
|
+
# Determine the next version number
|
|
158
|
+
new_version_suffix = ""
|
|
159
|
+
if len(existing_versions) > 0:
|
|
160
|
+
new_version = max_version + 1
|
|
161
|
+
new_version_suffix = f"_v{new_version}"
|
|
162
|
+
else:
|
|
163
|
+
new_version = 1
|
|
164
|
+
|
|
165
|
+
new_object_name = f"{object_name}{new_version_suffix}"
|
|
166
|
+
last_sha = None
|
|
167
|
+
if max_version > 0:
|
|
168
|
+
last_object = existing_versions[max_version]
|
|
169
|
+
command = [
|
|
170
|
+
"docker",
|
|
171
|
+
self.object_type,
|
|
172
|
+
"inspect",
|
|
173
|
+
last_object["Name"],
|
|
174
|
+
"--format",
|
|
175
|
+
"{{json .Spec.Labels}}",
|
|
176
|
+
]
|
|
177
|
+
inspect_result = run_cli_command(command, raise_error=True, log=self.log)
|
|
178
|
+
labels_info = json.loads(inspect_result)
|
|
179
|
+
last_sha = labels_info.get("sha256")
|
|
180
|
+
|
|
181
|
+
if sha_hash == last_sha:
|
|
182
|
+
return (last_object["Name"], Command.nop)
|
|
183
|
+
else:
|
|
184
|
+
labels = [
|
|
185
|
+
f"mesudip.object.version={new_version:01d}",
|
|
186
|
+
f"mesudip.object.name={object_name}",
|
|
187
|
+
] + labels
|
|
188
|
+
return new_object_name, self._create(
|
|
189
|
+
new_object_name, object_content, labels, sha_hash=sha_hash
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class DockerConfig(DockerObjectManager):
|
|
194
|
+
def __init__(self, log=False):
|
|
195
|
+
super().__init__("config", log)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class DockerSecret(DockerObjectManager):
|
|
199
|
+
def __init__(self, log=False):
|
|
200
|
+
super().__init__("secret", log)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def parse_labels(label_string):
|
|
204
|
+
"""
|
|
205
|
+
Parse a comma-separated string of key-value pairs into a dictionary.
|
|
206
|
+
Handles cases where values may contain commas.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
label_string (str): A string containing key-value pairs separated by commas.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
dict: A dictionary of labels and their corresponding values.
|
|
213
|
+
"""
|
|
214
|
+
labels = {}
|
|
215
|
+
# Use a regex to split on commas that are not part of a value
|
|
216
|
+
pattern = re.compile(r",(?![^=,]*(?:,|$))")
|
|
217
|
+
for pair in pattern.split(label_string):
|
|
218
|
+
if "=" in pair:
|
|
219
|
+
key, value = pair.split("=", 1) # Split on the first '=' only
|
|
220
|
+
labels[key.strip()] = value.strip()
|
|
221
|
+
return labels
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
def replace_with_default(match):
|
|
29
|
+
var = match.group(1)
|
|
30
|
+
default_value = match.group(2) if match.group(2) is not None else None
|
|
31
|
+
result = env.get(var, default_value)
|
|
32
|
+
if result is None:
|
|
33
|
+
print(f"Missing template variable with default: {var}", file=sys.stderr)
|
|
34
|
+
exit(1)
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
def replace_without_default(match):
|
|
38
|
+
var = match.group(1)
|
|
39
|
+
result = env.get(var, None)
|
|
40
|
+
if result is None:
|
|
41
|
+
print(f"Missing template variable: {var}", file=sys.stderr)
|
|
42
|
+
exit(1)
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
# Substitute variables with default values
|
|
46
|
+
template_str = pattern_with_default.sub(replace_with_default, template_str)
|
|
47
|
+
|
|
48
|
+
# Substitute variables without default values
|
|
49
|
+
template_str = pattern_without_default.sub(replace_without_default, template_str)
|
|
50
|
+
|
|
51
|
+
return template_str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
if len(sys.argv) > 2:
|
|
56
|
+
print("Usage: python envsubst.py [template_file]")
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
if len(sys.argv) == 2:
|
|
60
|
+
template_file = sys.argv[1]
|
|
61
|
+
with open(template_file, "r") as file:
|
|
62
|
+
template_str = file.read()
|
|
63
|
+
else:
|
|
64
|
+
template_str = sys.stdin.read()
|
|
65
|
+
|
|
66
|
+
result = envsubst(template_str)
|
|
67
|
+
|
|
68
|
+
print(result)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
main()
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
NAME
|
|
4
|
+
envsubst_merge.py - merges files and substitutes environment variables in the content
|
|
5
|
+
|
|
6
|
+
DESCRIPTION
|
|
7
|
+
envsubst_merge.py combines the functionality of merging files and substituting environment
|
|
8
|
+
variables in bash format strings. An optional file extension can be provided to filter the files to be merged.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def envsubst(template_str, env=os.environ):
|
|
17
|
+
"""Substitute environment variables in the template string, supporting default values."""
|
|
18
|
+
|
|
19
|
+
# Regex for ${VARIABLE} with optional default
|
|
20
|
+
pattern_with_default = re.compile(r"\$\{([^}:\s]+)(?::-([^}]*))?\}")
|
|
21
|
+
|
|
22
|
+
# Regex for $VARIABLE without default
|
|
23
|
+
pattern_without_default = re.compile(r"\$([a-zA-Z_][a-zA-Z0-9_]*)")
|
|
24
|
+
|
|
25
|
+
def replace_with_default(match):
|
|
26
|
+
var = match.group(1)
|
|
27
|
+
default_value = match.group(2) if match.group(2) is not None else None
|
|
28
|
+
result = env.get(var, default_value)
|
|
29
|
+
if result is None:
|
|
30
|
+
print(f"Missing template variable with default: {var}", file=sys.stderr)
|
|
31
|
+
exit(1)
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
def replace_without_default(match):
|
|
35
|
+
var = match.group(1)
|
|
36
|
+
result = env.get(var, None)
|
|
37
|
+
if result is None:
|
|
38
|
+
print(f"Missing template variable: {var}", file=sys.stderr)
|
|
39
|
+
exit(1)
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
# Substitute variables with default values
|
|
43
|
+
template_str = pattern_with_default.sub(replace_with_default, template_str)
|
|
44
|
+
|
|
45
|
+
# Substitute variables without default values
|
|
46
|
+
template_str = pattern_without_default.sub(replace_without_default, template_str)
|
|
47
|
+
|
|
48
|
+
return template_str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def merge_files_from_directories(directories, file_extension=None):
|
|
52
|
+
merged_content = []
|
|
53
|
+
|
|
54
|
+
for path in directories:
|
|
55
|
+
if os.path.isdir(path):
|
|
56
|
+
# If the path is a directory, read files with the specified extension
|
|
57
|
+
for filename in os.listdir(path):
|
|
58
|
+
if not file_extension or filename.endswith(file_extension):
|
|
59
|
+
filepath = os.path.join(path, filename)
|
|
60
|
+
with open(filepath, "r") as file:
|
|
61
|
+
content = (
|
|
62
|
+
file.read().strip()
|
|
63
|
+
) # Strip leading/trailing whitespace
|
|
64
|
+
if content: # Add only non-empty content
|
|
65
|
+
# Add directory and filename as a comment at the start of the content
|
|
66
|
+
merged_content.append(f"# {path}/{filename}\n{content}")
|
|
67
|
+
elif os.path.isfile(path) and (
|
|
68
|
+
not file_extension or path.endswith(file_extension)
|
|
69
|
+
):
|
|
70
|
+
# If the path is a file with the specified extension, read its content
|
|
71
|
+
with open(path, "r") as file:
|
|
72
|
+
content = file.read().strip() # Strip leading/trailing whitespace
|
|
73
|
+
if content: # Add only non-empty content
|
|
74
|
+
# Add the file name as a comment at the start of the content
|
|
75
|
+
merged_content.append(f"# {path}\n{content}")
|
|
76
|
+
else:
|
|
77
|
+
print(
|
|
78
|
+
f"Warning: '{path}' is not a valid directory or file. Skipping.",
|
|
79
|
+
file=sys.stderr,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Join the content with a single newline between entries
|
|
83
|
+
result = "\n\n".join(merged_content)
|
|
84
|
+
|
|
85
|
+
# Strip extra empty lines from the beginning and end
|
|
86
|
+
result = result.strip()
|
|
87
|
+
|
|
88
|
+
# Perform environment variable substitution on the final result
|
|
89
|
+
return envsubst(result)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main():
|
|
93
|
+
# Take directories or files from command-line arguments
|
|
94
|
+
if len(sys.argv) < 2:
|
|
95
|
+
print(
|
|
96
|
+
"Usage: python envsubst_merge.py <directory1|file1> <directory2|file2> ... [--ext <file_extension>] [--ext=<file_extension>]",
|
|
97
|
+
file=sys.stderr,
|
|
98
|
+
)
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
paths_to_read = []
|
|
102
|
+
file_extension = None
|
|
103
|
+
|
|
104
|
+
# Parse arguments
|
|
105
|
+
args = iter(sys.argv[1:])
|
|
106
|
+
for arg in args:
|
|
107
|
+
if arg.startswith("--ext="):
|
|
108
|
+
file_extension = arg.split("=", 1)[1]
|
|
109
|
+
elif arg == "--ext":
|
|
110
|
+
try:
|
|
111
|
+
file_extension = next(args)
|
|
112
|
+
except StopIteration:
|
|
113
|
+
print("Error: Missing file extension after --ext.", file=sys.stderr)
|
|
114
|
+
sys.exit(1)
|
|
115
|
+
else:
|
|
116
|
+
paths_to_read.append(arg)
|
|
117
|
+
|
|
118
|
+
result = merge_files_from_directories(paths_to_read, file_extension)
|
|
119
|
+
|
|
120
|
+
# Print the output to stdout
|
|
121
|
+
print(result)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
main()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def run_cli_command(
|
|
6
|
+
command: List[str],
|
|
7
|
+
stdin: Optional[str] = None,
|
|
8
|
+
raise_error: bool = True,
|
|
9
|
+
log: bool = True,
|
|
10
|
+
shell: bool = False,
|
|
11
|
+
interactive: bool = False,
|
|
12
|
+
) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Run a CLI command and return its output.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
command: The command to run as a list of strings.
|
|
18
|
+
stdin: Input to pass to the command via stdin.
|
|
19
|
+
raise_error: If True, raise an exception if the command fails.
|
|
20
|
+
log: If True, log the command before executing it.
|
|
21
|
+
shell: If True, run the command in a shell.
|
|
22
|
+
interactive: If True, run the command with everything sent to the current console.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The stdout of the command as a string, or None if interactive mode is enabled.
|
|
26
|
+
"""
|
|
27
|
+
if log:
|
|
28
|
+
print("> " + " ".join(command))
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
if interactive:
|
|
32
|
+
result = subprocess.run(command, shell=shell)
|
|
33
|
+
return None
|
|
34
|
+
else:
|
|
35
|
+
result = subprocess.run(
|
|
36
|
+
command,
|
|
37
|
+
input=stdin,
|
|
38
|
+
text=True,
|
|
39
|
+
capture_output=True,
|
|
40
|
+
check=raise_error,
|
|
41
|
+
shell=shell,
|
|
42
|
+
)
|
|
43
|
+
return result.stdout.strip()
|
|
44
|
+
except subprocess.CalledProcessError as e:
|
|
45
|
+
# Log the error and re-raise the exception
|
|
46
|
+
print(f"Error running command: {e}")
|
|
47
|
+
print(f"stdout: {e.stdout}")
|
|
48
|
+
print(f"stderr: {e.stderr}")
|
|
49
|
+
raise e
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Command:
|
|
53
|
+
nop: "Command" = None
|
|
54
|
+
|
|
55
|
+
def isNop(self):
|
|
56
|
+
return self == Command.nop
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self, command: List[str], stdin: Optional[str] = None, log: bool = True, id=None
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Initialize a Command object.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
command: The command to run as a list of strings.
|
|
66
|
+
stdin: Input to pass to the command via stdin.
|
|
67
|
+
log: If True, log the command before executing it.
|
|
68
|
+
"""
|
|
69
|
+
self.command = command
|
|
70
|
+
self.stdin = stdin
|
|
71
|
+
self.log = log
|
|
72
|
+
self.id = id
|
|
73
|
+
|
|
74
|
+
def execute(self, log: Optional[bool] = None) -> str:
|
|
75
|
+
"""
|
|
76
|
+
Execute the command and return its output.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
log: If provided, overrides the log setting from the constructor.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The stdout of the command as a string.
|
|
83
|
+
"""
|
|
84
|
+
if not self.command:
|
|
85
|
+
return
|
|
86
|
+
# Use the provided log value if available, otherwise use the one from the constructor
|
|
87
|
+
use_log = log if log is not None else self.log
|
|
88
|
+
if not self.stdin:
|
|
89
|
+
subprocess.run(self.command)
|
|
90
|
+
return run_cli_command(self.command, stdin=self.stdin, log=use_log, shell=False)
|
|
91
|
+
|
|
92
|
+
def __str__(self) -> str:
|
|
93
|
+
"""
|
|
94
|
+
Return a string representation of the command.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
A string representation of the command, including stdin if applicable.
|
|
98
|
+
"""
|
|
99
|
+
if self.stdin:
|
|
100
|
+
return f"echo '{self.stdin}' | {' '.join(self.command)}"
|
|
101
|
+
elif self.command:
|
|
102
|
+
return " ".join(self.command)
|
|
103
|
+
else:
|
|
104
|
+
return "NOP"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
Command.nop = Command([])
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def merge_files_from_directories(directories):
|
|
6
|
+
merged_content = []
|
|
7
|
+
|
|
8
|
+
for path in directories:
|
|
9
|
+
if os.path.isdir(path):
|
|
10
|
+
# If the path is a directory, read .conf files from it
|
|
11
|
+
for filename in os.listdir(path):
|
|
12
|
+
if filename.endswith(".conf"): # Only consider .conf files
|
|
13
|
+
filepath = os.path.join(path, filename)
|
|
14
|
+
with open(filepath, "r") as file:
|
|
15
|
+
content = (
|
|
16
|
+
file.read().strip()
|
|
17
|
+
) # Strip leading/trailing whitespace
|
|
18
|
+
if content: # Add only non-empty content
|
|
19
|
+
# Add directory and filename as a comment at the start of the content
|
|
20
|
+
merged_content.append(f"# {path}/{filename}\n{content}")
|
|
21
|
+
elif os.path.isfile(path) and path.endswith(".conf"):
|
|
22
|
+
# If the path is a .conf file, read its content
|
|
23
|
+
with open(path, "r") as file:
|
|
24
|
+
content = file.read().strip() # Strip leading/trailing whitespace
|
|
25
|
+
if content: # Add only non-empty content
|
|
26
|
+
# Add the file name as a comment at the start of the content
|
|
27
|
+
merged_content.append(f"# {path}\n{content}")
|
|
28
|
+
else:
|
|
29
|
+
print(
|
|
30
|
+
f"Warning: '{path}' is not a valid directory or .conf file. Skipping."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Join the content with a single newline between entries
|
|
34
|
+
result = "\n\n".join(merged_content)
|
|
35
|
+
|
|
36
|
+
# Strip extra empty lines from the beginning and end
|
|
37
|
+
result = result.strip()
|
|
38
|
+
|
|
39
|
+
# Print the output to stdout
|
|
40
|
+
print(result)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
# Take directories or .conf files from command-line arguments
|
|
45
|
+
if len(sys.argv) < 2:
|
|
46
|
+
print(
|
|
47
|
+
"Usage: python merge_files.py <directory1|file1.conf> <directory2|file2.conf> ..."
|
|
48
|
+
)
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
paths_to_read = sys.argv[1:]
|
|
52
|
+
merge_files_from_directories(paths_to_read)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import http.client
|
|
3
|
+
import select
|
|
4
|
+
import subprocess
|
|
5
|
+
from base64 import b64encode
|
|
6
|
+
|
|
7
|
+
from docker_stack.helpers import Command
|
|
8
|
+
|
|
9
|
+
class DockerRegistry:
|
|
10
|
+
def __init__(self, registry_url, userpass:str=None):
|
|
11
|
+
"""
|
|
12
|
+
Initializes the DockerRegistry class with the registry URL, and optional
|
|
13
|
+
username and password for authentication.
|
|
14
|
+
|
|
15
|
+
:param registry_url: URL of the Docker registry (e.g., 'registry.hub.docker.com')
|
|
16
|
+
:param username: Optional username for authentication
|
|
17
|
+
:param password: Optional password for authentication
|
|
18
|
+
"""
|
|
19
|
+
self.registry_url = registry_url
|
|
20
|
+
self.username = None
|
|
21
|
+
self.password = None
|
|
22
|
+
if userpass:
|
|
23
|
+
splitted=userpass.split(':')
|
|
24
|
+
|
|
25
|
+
self.username=splitted[0]
|
|
26
|
+
self.password=splitted[1]
|
|
27
|
+
|
|
28
|
+
# Parse registry host and port
|
|
29
|
+
self.host = self._get_host_from_url(registry_url)
|
|
30
|
+
self.port = 443 if self.registry_url.startswith("https") else 80
|
|
31
|
+
|
|
32
|
+
def _get_host_from_url(self, url):
|
|
33
|
+
"""Extracts the host from the URL."""
|
|
34
|
+
# Remove protocol part (http:// or https://)
|
|
35
|
+
return url.split('://')[1].split('/')[0]
|
|
36
|
+
|
|
37
|
+
def _send_request(self, method, endpoint, auth=None):
|
|
38
|
+
"""Send a generic HTTP request to the Docker registry."""
|
|
39
|
+
connection = http.client.HTTPSConnection(self.host, self.port)
|
|
40
|
+
|
|
41
|
+
# Add Authorization header if needed
|
|
42
|
+
headers = {}
|
|
43
|
+
if auth:
|
|
44
|
+
headers['Authorization'] = f"Basic {b64encode(auth.encode()).decode()}"
|
|
45
|
+
|
|
46
|
+
connection.request(method, endpoint, headers=headers)
|
|
47
|
+
response = connection.getresponse()
|
|
48
|
+
return response
|
|
49
|
+
|
|
50
|
+
def check_auth(self):
|
|
51
|
+
"""
|
|
52
|
+
Check if the authentication credentials (if provided) are valid for the Docker registry.
|
|
53
|
+
|
|
54
|
+
:return: Boolean indicating whether authentication is successful
|
|
55
|
+
"""
|
|
56
|
+
url = "/v2/"
|
|
57
|
+
if self.username and self.password:
|
|
58
|
+
auth = f"{self.username}:{self.password}"
|
|
59
|
+
response = self._send_request('GET', url, auth)
|
|
60
|
+
else:
|
|
61
|
+
response = self._send_request('GET', url)
|
|
62
|
+
|
|
63
|
+
# Check if the status code is 200
|
|
64
|
+
return response.status == 200
|
|
65
|
+
|
|
66
|
+
def check_image(self, image_name):
|
|
67
|
+
"""
|
|
68
|
+
Check if an image exists in the Docker registry.
|
|
69
|
+
|
|
70
|
+
:param image_name: Name of the image (e.g., 'ubuntu' or 'python')
|
|
71
|
+
:return: Boolean indicating whether the image exists in the registry
|
|
72
|
+
"""
|
|
73
|
+
url = f"/v2/{image_name}/tags/list"
|
|
74
|
+
if self.username and self.password:
|
|
75
|
+
auth = f"{self.username}:{self.password}"
|
|
76
|
+
response = self._send_request('GET', url, auth)
|
|
77
|
+
else:
|
|
78
|
+
response = self._send_request('GET', url)
|
|
79
|
+
|
|
80
|
+
# Check if the status code is 200
|
|
81
|
+
return response.status == 200
|
|
82
|
+
|
|
83
|
+
def _run_docker_command(self, command):
|
|
84
|
+
"""
|
|
85
|
+
Run a Docker command using the subprocess module and stream the output to the terminal in real-time.
|
|
86
|
+
|
|
87
|
+
:param command: A list of strings representing the Docker command to run
|
|
88
|
+
:return: None
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
|
|
92
|
+
|
|
93
|
+
# Use select to handle both stdout and stderr without blocking
|
|
94
|
+
while process.poll() is None:
|
|
95
|
+
readable, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1)
|
|
96
|
+
for stream in readable:
|
|
97
|
+
line = stream.readline()
|
|
98
|
+
if line:
|
|
99
|
+
print(line, end="", flush=True)
|
|
100
|
+
|
|
101
|
+
# Ensure remaining output is printed
|
|
102
|
+
for stream in (process.stdout, process.stderr):
|
|
103
|
+
for line in iter(stream.readline, ""):
|
|
104
|
+
print(line, end="", flush=True)
|
|
105
|
+
|
|
106
|
+
process.stdout.close()
|
|
107
|
+
process.stderr.close()
|
|
108
|
+
process.wait()
|
|
109
|
+
|
|
110
|
+
if process.returncode != 0:
|
|
111
|
+
print(f"Command failed with return code {process.returncode}")
|
|
112
|
+
|
|
113
|
+
except FileNotFoundError:
|
|
114
|
+
print("Docker command not found. Please ensure Docker is installed and accessible.")
|
|
115
|
+
|
|
116
|
+
def _run_docker_command_(self, command):
|
|
117
|
+
"""
|
|
118
|
+
Run a Docker command using the subprocess module.
|
|
119
|
+
|
|
120
|
+
:param command: A list of strings representing the Docker command to run
|
|
121
|
+
:return: Tuple of (stdout, stderr)
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
125
|
+
return result.stdout, result.stderr
|
|
126
|
+
except FileNotFoundError:
|
|
127
|
+
return '', 'Docker not found. Please install Docker.'
|
|
128
|
+
|
|
129
|
+
def push(self, image_name)->Command:
|
|
130
|
+
"""
|
|
131
|
+
Push an image to the Docker registry.
|
|
132
|
+
|
|
133
|
+
:param image_name: Name of the image to push (e.g., 'myrepo/myimage:tag')
|
|
134
|
+
:return: Tuple of (stdout, stderr)
|
|
135
|
+
"""
|
|
136
|
+
return Command(['docker', 'push', image_name])
|
|
137
|
+
|
|
138
|
+
def pull(self, image_name):
|
|
139
|
+
"""
|
|
140
|
+
Pull an image from the Docker registry.
|
|
141
|
+
|
|
142
|
+
:param image_name: Name of the image to pull (e.g., 'myrepo/myimage:tag')
|
|
143
|
+
:return: Tuple of (stdout, stderr)
|
|
144
|
+
"""
|
|
145
|
+
command = ['docker', 'pull', image_name]
|
|
146
|
+
return self._run_docker_command(command)
|
|
147
|
+
|
|
148
|
+
def login(self):
|
|
149
|
+
print("> " ,['docker','login','-u',self.username,'-p',self.password])
|
|
150
|
+
subprocess.run(['docker','login','-u',self.username,'-p',self.password,self.host])
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: docker-stack
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: CLI for deploying and managing Docker stacks.
|
|
5
|
+
Home-page: https://github.com/mesuidp/docker-stack
|
|
6
|
+
Author: Sudip Bhattarai
|
|
7
|
+
Author-email: sudip.dev.np@gmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
Docker Stack
|
|
15
|
+
==============
|
|
16
|
+
cli utility for stack deployment in docker-swarm.
|
|
17
|
+
#### Features
|
|
18
|
+
- docker config and secret creation and versioning
|
|
19
|
+
- docker stack versioning and config backup for rollback
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
docker_stack/__init__.py
|
|
5
|
+
docker_stack/cli.py
|
|
6
|
+
docker_stack/compose.py
|
|
7
|
+
docker_stack/docker_objects.py
|
|
8
|
+
docker_stack/envsubst.py
|
|
9
|
+
docker_stack/envsubst_merge.py
|
|
10
|
+
docker_stack/helpers.py
|
|
11
|
+
docker_stack/merge_conf.py
|
|
12
|
+
docker_stack/registry.py
|
|
13
|
+
docker_stack.egg-info/PKG-INFO
|
|
14
|
+
docker_stack.egg-info/SOURCES.txt
|
|
15
|
+
docker_stack.egg-info/dependency_links.txt
|
|
16
|
+
docker_stack.egg-info/entry_points.txt
|
|
17
|
+
docker_stack.egg-info/requires.txt
|
|
18
|
+
docker_stack.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PyYAML
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
docker_stack
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="docker-stack",
|
|
5
|
+
version="0.1.1",
|
|
6
|
+
description="CLI for deploying and managing Docker stacks.",
|
|
7
|
+
long_description=open("README.md").read(), # You can include a README file to describe your package
|
|
8
|
+
long_description_content_type="text/markdown",
|
|
9
|
+
author="Sudip Bhattarai",
|
|
10
|
+
author_email="sudip.dev.np@gmail.com",
|
|
11
|
+
url="https://github.com/mesuidp/docker-stack", # Replace with your project URL
|
|
12
|
+
packages=find_packages(),
|
|
13
|
+
install_requires=[
|
|
14
|
+
"PyYAML"
|
|
15
|
+
],
|
|
16
|
+
entry_points={
|
|
17
|
+
"console_scripts": [
|
|
18
|
+
"docker-stack=docker_stack.cli:main", # The function main() inside docker_stack.cli module
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
classifiers=[
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
],
|
|
26
|
+
python_requires=">=3.6",
|
|
27
|
+
)
|