portmgr 0.1.0__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.
portmgr-0.1.0/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 theascone
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
portmgr-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: portmgr
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Home-page: https://github.com/Craeckie/portmgr
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: humanfriendly~=10.0
10
+ Requires-Dist: jsonschema~=3.2.0
11
+ Requires-Dist: pyyaml~=6.0.2
12
+ Requires-Dist: tabulate~=0.8.9
13
+ Dynamic: home-page
14
+ Dynamic: license-file
15
+
16
+ # portmgr
17
+ portmgr is a wrapper around [docker-compose](https://docs.docker.com/compose/) that allows running typical docker-compose commands recursively. Additionally, it shortens commands to a single letter.
18
+
19
+ Let's say you have organized your compose files like this, you just add a `dckrsub.yml` in each parent folder:
20
+ <pre>
21
+ docker/
22
+ ├── <b>dckrsub.yml</b>
23
+ ├── reverse-proxy/
24
+ │ └── docker-compose.yml
25
+ ├── storage
26
+ │ ├── <b>dckrsub.yml</b>
27
+ │ ├── nextcloud/
28
+ │ │ └── docker-compose.yml
29
+ │ └── immich/
30
+ │ └── docker-compose.yml
31
+ └── scripts
32
+ </pre>
33
+
34
+ Each `dckrsub.yml` has a list of subdirectories, which portmgr should decend into.
35
+ For example, the `dckrsub.yml` in `docker/` might look like this:
36
+ ```yaml
37
+ - reverse-proxy
38
+ - storage
39
+ ```
40
+
41
+ And the `dckrsub.yml` in `docker/storage/` like this:
42
+ ```yaml
43
+ - nextcloud
44
+ - immich
45
+ ```
46
+
47
+ Now, if you run `portmgr u` in `docker/` it will run `docker compose up -d` in `reverse-proxy/`, `storage/nextcloud/` and `storage/immich/`.
48
+
49
+ portmgr starts from the current directory, so when running it in `docker/storage/`, it will run `docker compose` only in `nextcloud/` and `immich/`. You can also use it in a directory with a `docker-compose.yml` as a shortener for docker-compose commands.
50
+
51
+ ### Commands
52
+ The following commands are available. The respective docker-compose commands are in brackets.
53
+
54
+ ```
55
+ u Create and start containers (up)
56
+ p Pull images (pull)
57
+ s Stop services (stop)
58
+ d Stop and remove containers (down)
59
+ l Show container logs (logs)
60
+ a Run shell in container (exec -it <service> sh)
61
+ b Build images (build)
62
+ c List containers (ps)
63
+ t List processes in containers (top)
64
+ r Build and push to registry (build, push)
65
+ v Scan container images for vulnerabilities
66
+ ```
67
+
68
+ You combine multiple commands. For example `portmgr dul`, runs docker compose with `down`, `up` and `logs`, thus stopping, removing and starting all containers and then showing the logs.
69
+
70
+ ### Installation
71
+ ```
72
+ sudo pip install portmgr
73
+ ```
74
+
75
+ Or build it from source (here using the latest commit on master branch)
76
+ ```
77
+ sudo pip install https://github.com/Craeckie/portmgr.git
78
+ ```
79
+
80
+ ### Tipps
81
+ If you use portmgr a lot like me, you might want to shorten it to one letter. For bash, you can add `alias p='portmgr'` to `~/.bashrc`. For fish-shell you can add `abbr p portmgr` to `~/.config/fish/config.fish`.
@@ -0,0 +1,66 @@
1
+ # portmgr
2
+ portmgr is a wrapper around [docker-compose](https://docs.docker.com/compose/) that allows running typical docker-compose commands recursively. Additionally, it shortens commands to a single letter.
3
+
4
+ Let's say you have organized your compose files like this, you just add a `dckrsub.yml` in each parent folder:
5
+ <pre>
6
+ docker/
7
+ ├── <b>dckrsub.yml</b>
8
+ ├── reverse-proxy/
9
+ │ └── docker-compose.yml
10
+ ├── storage
11
+ │ ├── <b>dckrsub.yml</b>
12
+ │ ├── nextcloud/
13
+ │ │ └── docker-compose.yml
14
+ │ └── immich/
15
+ │ └── docker-compose.yml
16
+ └── scripts
17
+ </pre>
18
+
19
+ Each `dckrsub.yml` has a list of subdirectories, which portmgr should decend into.
20
+ For example, the `dckrsub.yml` in `docker/` might look like this:
21
+ ```yaml
22
+ - reverse-proxy
23
+ - storage
24
+ ```
25
+
26
+ And the `dckrsub.yml` in `docker/storage/` like this:
27
+ ```yaml
28
+ - nextcloud
29
+ - immich
30
+ ```
31
+
32
+ Now, if you run `portmgr u` in `docker/` it will run `docker compose up -d` in `reverse-proxy/`, `storage/nextcloud/` and `storage/immich/`.
33
+
34
+ portmgr starts from the current directory, so when running it in `docker/storage/`, it will run `docker compose` only in `nextcloud/` and `immich/`. You can also use it in a directory with a `docker-compose.yml` as a shortener for docker-compose commands.
35
+
36
+ ### Commands
37
+ The following commands are available. The respective docker-compose commands are in brackets.
38
+
39
+ ```
40
+ u Create and start containers (up)
41
+ p Pull images (pull)
42
+ s Stop services (stop)
43
+ d Stop and remove containers (down)
44
+ l Show container logs (logs)
45
+ a Run shell in container (exec -it <service> sh)
46
+ b Build images (build)
47
+ c List containers (ps)
48
+ t List processes in containers (top)
49
+ r Build and push to registry (build, push)
50
+ v Scan container images for vulnerabilities
51
+ ```
52
+
53
+ You combine multiple commands. For example `portmgr dul`, runs docker compose with `down`, `up` and `logs`, thus stopping, removing and starting all containers and then showing the logs.
54
+
55
+ ### Installation
56
+ ```
57
+ sudo pip install portmgr
58
+ ```
59
+
60
+ Or build it from source (here using the latest commit on master branch)
61
+ ```
62
+ sudo pip install https://github.com/Craeckie/portmgr.git
63
+ ```
64
+
65
+ ### Tipps
66
+ If you use portmgr a lot like me, you might want to shorten it to one letter. For bash, you can add `alias p='portmgr'` to `~/.bashrc`. For fish-shell you can add `abbr p portmgr` to `~/.config/fish/config.fish`.
@@ -0,0 +1,2 @@
1
+ from .portmgr import main, command_list, bcolors
2
+ from .wrapper import runCompose, runBuildx
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import unicode_literals
3
+
4
+ import sys
5
+
6
+ if __package__ is None and not hasattr(sys, 'frozen'):
7
+ # direct call of __main__.py
8
+ import os.path
9
+ path = os.path.realpath(os.path.abspath(__file__))
10
+ sys.path.insert(0, os.path.dirname(os.path.dirname(path)))
11
+
12
+ import portmgr
13
+
14
+ if __name__ == '__main__':
15
+ portmgr.main()
File without changes
@@ -0,0 +1,51 @@
1
+ from portmgr import command_list, bcolors
2
+ import subprocess
3
+
4
+ from portmgr.wrapper import getServicesRunning
5
+
6
+
7
+ def func(action):
8
+ directory = action['directory']
9
+ relative = action['relative']
10
+
11
+ names = getServicesRunning()
12
+
13
+ index = 0
14
+ cont_count = len(names)
15
+ if cont_count == 0:
16
+ print("No containers found!")
17
+ return 1
18
+ elif cont_count > 1:
19
+ i = 0
20
+ for cont in names:
21
+ print("(" + str(i) + ") " + cont)
22
+ i += 1
23
+
24
+ while True:
25
+ choice = input("Choose container: ")
26
+ if choice:
27
+ try:
28
+ index = int(choice)
29
+ if index >= 0 and index < cont_count:
30
+ break
31
+ except ValueError:
32
+ print("Please enter a number!")
33
+ pass
34
+ print("Please enter a number between 0 and " + cont_count - 1 + "!")
35
+
36
+ container_id = names[index]
37
+ print("Attaching to " + container_id)
38
+ subprocess.call(["docker", "exec", "-it", container_id, "sh"])
39
+
40
+ # res = subprocess.call(["docker-compose", "logs", "--follow", "--tail=200"])
41
+
42
+ # if res != 0:
43
+ # print("Error showing logs for " + relative + "!\n")
44
+
45
+ return 0
46
+
47
+ command_list['a'] = {
48
+ 'hlp': 'Attach to process',
49
+ 'ord': 'nrm',
50
+ 'fnc': func
51
+ }
@@ -0,0 +1,25 @@
1
+ from portmgr import command_list, bcolors, runCompose
2
+
3
+
4
+ def func(action):
5
+ directory = action['directory']
6
+ relative = action['relative']
7
+
8
+ res = runCompose(
9
+ ['build', '--pull'],
10
+ env={
11
+ 'COMPOSE_DOCKER_CLI_BUILD': '1',
12
+ 'DOCKER_BUILDKIT': '1'
13
+ },
14
+ )
15
+
16
+ if res != 0:
17
+ print("Error building " + relative + "!")
18
+
19
+ return res
20
+
21
+ command_list['b'] = {
22
+ 'hlp': 'Build local image',
23
+ 'ord': 'nrm',
24
+ 'fnc': func
25
+ }
@@ -0,0 +1,27 @@
1
+ from portmgr import command_list, bcolors, runCompose
2
+
3
+
4
+ def func(action):
5
+ directory = action['directory']
6
+ relative = action['relative']
7
+
8
+ res = runCompose(["down"])
9
+ # p = subprocess.Popen(["docker-compose", "down"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
10
+
11
+ # out, err = p.communicate()
12
+
13
+ # if out != "":
14
+ # print(out.decode("UTF-8"))
15
+
16
+ if not res == 0:
17
+ print("Error removing " + relative + "!")
18
+
19
+ # print(bcolors.FAIL + err.decode("UTF-8") + bcolors.ENDC)
20
+
21
+ return 0
22
+
23
+ command_list['d'] = {
24
+ 'hlp': 'Stop and remove container',
25
+ 'ord': 'rev',
26
+ 'fnc': func
27
+ }
@@ -0,0 +1,19 @@
1
+ from portmgr import command_list, bcolors, runCompose
2
+
3
+
4
+ def func(action):
5
+ directory = action['directory']
6
+ relative = action['relative']
7
+
8
+ res = runCompose(["logs", "--follow", "--tail=200"])
9
+
10
+ if res != 0:
11
+ print("Error showing logs for " + relative + "!\n")
12
+
13
+ return 0
14
+
15
+ command_list['l'] = {
16
+ 'hlp': 'Show logs',
17
+ 'ord': 'nrm',
18
+ 'fnc': func
19
+ }
@@ -0,0 +1,19 @@
1
+ from portmgr import command_list, bcolors, runCompose
2
+
3
+
4
+ def func(action):
5
+ directory = action['directory']
6
+ relative = action['relative']
7
+
8
+ res = runCompose(["ps"])
9
+
10
+ if res != 0:
11
+ print("Error listing containers for " + relative + "!\n")
12
+
13
+ return 0
14
+
15
+ command_list['c'] = {
16
+ 'hlp': 'List containers',
17
+ 'ord': 'nrm',
18
+ 'fnc': func
19
+ }
@@ -0,0 +1,19 @@
1
+ from portmgr import command_list, bcolors, runCompose
2
+
3
+
4
+ def func(action):
5
+ directory = action['directory']
6
+ relative = action['relative']
7
+
8
+ res = runCompose(["pull"])
9
+
10
+ if res != 0:
11
+ print("Error pulling " + relative + "!")
12
+
13
+ return 0
14
+
15
+ command_list['p'] = {
16
+ 'hlp': 'Pull image from repository',
17
+ 'ord': 'nrm',
18
+ 'fnc': func
19
+ }
@@ -0,0 +1,76 @@
1
+ from portmgr import command_list, bcolors, runCompose, runBuildx
2
+ import subprocess
3
+ import os
4
+
5
+ from portmgr.wrapper import getServices
6
+
7
+
8
+ def func(action):
9
+ directory = action['directory']
10
+ relative = action['relative']
11
+
12
+ services = getServices(includeOnlyBuildable=True)
13
+
14
+ print('Services to build: ' + ', '.join(services))
15
+
16
+ res = 0
17
+ for service in services.keys():
18
+ print(f"\nBuilding {service}")
19
+
20
+ new_res = 2
21
+ if multi_platform := os.environ.get("PORTMGR_MULTI_PLATFORM", "").lower():
22
+ try:
23
+ new_res = runBuildx(
24
+ ['bake',
25
+ '--pull',
26
+ '--push',
27
+ '--set', f'*.platform={multi_platform}',
28
+ service
29
+ ], timeout=1200 #kill after 20mins
30
+ )
31
+ if new_res != 0:
32
+ res = new_res
33
+ print(f"Error building {service}!")
34
+ except TimeoutExpired:
35
+ print(f"Error building {service}! Build timed out.")
36
+ else:
37
+ try:
38
+ new_res = runCompose(
39
+ ['build',
40
+ '--pull',
41
+ '--force-rm',
42
+ '--compress',
43
+ service
44
+ ], timeout=1200
45
+ )
46
+ if new_res != 0:
47
+ res = new_res
48
+ print(f"Error building {service}!")
49
+ else:
50
+ new_res = runCompose(
51
+ ['push',
52
+ '--ignore-push-failures',
53
+ service
54
+ ]
55
+ )
56
+ print(f"Error pushing {service}!")
57
+ except TimeoutExpired:
58
+ print(f"Error building {service}! Build timed out.")
59
+ if new_res != 1:
60
+ res = new_res
61
+ if os.environ.get("PORTMGR_CLEAN_AFTER_PUSH", "").lower() == "true":
62
+ subprocess.call(['docker', 'system', 'prune', '--all', '--force'])
63
+ subprocess.call(['docker', 'buildx', 'prune', '--all', '--force'])
64
+
65
+ if res != 0:
66
+ print("Error building&pushing " + relative + "!")
67
+ return res
68
+
69
+ return res
70
+
71
+
72
+ command_list['r'] = {
73
+ 'hlp': 'build, push to registry & remove image',
74
+ 'ord': 'nrm',
75
+ 'fnc': func
76
+ }
@@ -0,0 +1,43 @@
1
+ from portmgr import command_list
2
+ import subprocess
3
+
4
+ from portmgr.wrapper import getImages
5
+
6
+
7
+ def func(action):
8
+ directory = action['directory']
9
+ relative = action['relative']
10
+
11
+ images = getImages()
12
+
13
+ res = 0
14
+
15
+ for image in images:
16
+ image_name = image["Name"]
17
+ print(f'Scanning {image_name} of {image["ContainerName"]}')
18
+ scan_res = subprocess.run(['trivy', '-q', 'image', '-s', 'CRITICAL', '--exit-code', '1', image_name], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
19
+ if scan_res.returncode != 0:
20
+ print('Found vulnerabilites:')
21
+ print(scan_res.stdout)
22
+ res = scan_res.returncode
23
+
24
+ #print("Name: %s" % container.name)
25
+ #names.append(container.name)
26
+ #config_str = subprocess.check_output(["docker", "inspect", "-f", '{{json .}}', container.id], stdout=subprocess.PIPE)
27
+ #json.loads(config_str)
28
+ #print("IP: %s" % ip)
29
+ #print(container.inspect)
30
+
31
+
32
+ #ID = subprocess.run(["docker-compose", "ps", '-q'], stdout=subprocess.PIPE).stdout
33
+
34
+ #if res != 0:
35
+ # print("Error listing containers for " + relative + "!\n")
36
+
37
+ return res
38
+
39
+ command_list['v'] = {
40
+ 'hlp': 'Scan container images for vulnerabilities',
41
+ 'ord': 'nrm',
42
+ 'fnc': func
43
+ }
@@ -0,0 +1,52 @@
1
+ from functools import cmp_to_key
2
+
3
+ from portmgr import command_list
4
+
5
+ from tabulate import tabulate
6
+ from humanfriendly import format_size, parse_size
7
+
8
+ from portmgr.wrapper import getStats
9
+
10
+
11
+ def func(action):
12
+ directory = action['directory']
13
+ relative = action['relative']
14
+
15
+ stats_list = getStats()
16
+
17
+ values = []
18
+ has_network_stats = False
19
+ for stats in stats_list:
20
+ name = stats['Name'] if 'Name' in stats else stats['Container']
21
+ #memory = stats["memory_stats"]
22
+ #usage = format_size(memory['usage'])
23
+ #limit = format_size(memory['limit'])
24
+ memory_string = stats["MemUsage"]
25
+ usage, limit = [p.strip() for p in memory_string.split('/')]
26
+ if 'NetIO' in stats:
27
+ network = stats["NetIO"]
28
+ received, sent = [p.strip() for p in memory_string.split('/')]
29
+ #received = format_size(sum(stats['rx_bytes'] for iface, stats in network.items()))
30
+ #sent = format_size(sum(stats['tx_bytes'] for iface, stats in network.items()))
31
+ columns = (stats['Name'], usage, limit, received, sent)
32
+ has_network_stats = True
33
+ else:
34
+ columns = (stats['Name'], usage, limit)
35
+ values.append(columns)
36
+ if values:
37
+ # sort by memory usage
38
+ values = sorted(values, key=cmp_to_key(lambda s1, s2: parse_size(s2[1]) - parse_size(s1[1])))
39
+ print(tabulate(values,
40
+ headers=['Service', 'Mem Usage', 'Mem Limit', 'Net Recv', 'Net Sent'],
41
+ colalign=['left', 'right', 'right', 'right', 'right'] if has_network_stats
42
+ else ['left', 'right', 'right']))
43
+ print('')
44
+
45
+ return 0
46
+
47
+
48
+ command_list['o'] = {
49
+ 'hlp': 'Show container stats',
50
+ 'ord': 'nrm',
51
+ 'fnc': func
52
+ }
@@ -0,0 +1,26 @@
1
+ from portmgr import command_list, bcolors, runCompose
2
+
3
+ def func(action):
4
+ directory = action['directory']
5
+ relative = action['relative']
6
+
7
+ res = runCompose(["stop"])
8
+ # p = subprocess.Popen(["docker-compose", "stop"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
9
+
10
+ #out, err = p.communicate()
11
+
12
+ #if out != "":
13
+ # print(out.decode("UTF-8"))
14
+
15
+ if res != 0:
16
+ print("Error stopping " + relative + "!")
17
+
18
+ # print(bcolors.FAIL + err.decode("UTF-8") + bcolors.ENDC)
19
+
20
+ return 0
21
+
22
+ command_list['s'] = {
23
+ 'hlp': 'Stop container',
24
+ 'ord': 'rev',
25
+ 'fnc': func
26
+ }
@@ -0,0 +1,18 @@
1
+ from portmgr import command_list, runCompose
2
+
3
+ def func(action):
4
+ relative = action['relative']
5
+
6
+ res = runCompose(["top"])
7
+
8
+ if res != 0:
9
+ print("Error listing processes of containers in " + relative + "!\n")
10
+
11
+ return 0
12
+
13
+
14
+ command_list['t'] = {
15
+ 'hlp': 'List processes in containers',
16
+ 'ord': 'nrm',
17
+ 'fnc': func
18
+ }
@@ -0,0 +1,26 @@
1
+ from portmgr import command_list, bcolors, runCompose
2
+
3
+ def func(action):
4
+ directory = action['directory']
5
+ relative = action['relative']
6
+
7
+ res = runCompose(["up", "-d"])
8
+ # p = subprocess.Popen(["docker-compose", "up", "-d"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
9
+
10
+ # out, err = p.communicate()
11
+
12
+ # if out != "":
13
+ # print(out.decode("UTF-8"))
14
+
15
+ if res != 0:
16
+ print("Error creating " + relative + "!")
17
+
18
+ # print(bcolors.FAIL + err.decode("UTF-8") + bcolors.ENDC)
19
+
20
+ return 0
21
+
22
+ command_list['u'] = {
23
+ 'hlp': 'Create container',
24
+ 'ord': 'nrm',
25
+ 'fnc': func
26
+ }
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/python3
2
+ import os, sys
3
+ import argparse
4
+ import importlib
5
+ import re
6
+
7
+ import yaml
8
+
9
+
10
+ class MyParser(argparse.ArgumentParser):
11
+ def error(self, message):
12
+ sys.stderr.write('Error: %s\n' % message)
13
+ self.print_help()
14
+ sys.exit(2)
15
+
16
+
17
+ sub_names = [x.strip() for x in re.split('[ ,;]+', os.environ.get('PORTMGR_SUB_NAME', 'dckrsub.yml'))]
18
+ compose_names = [x.strip() for x in
19
+ os.environ.get('PORTMGR_COMPOSE_NAME', 'docker-compose.yml, docker-compose.yaml').split(',')]
20
+
21
+ # sub_scheme_name = 'dckrsub.schema.yml'
22
+
23
+ src_path = os.path.dirname(os.path.abspath(__file__))
24
+
25
+
26
+ # conf_scheme_path = os.path.join(src_path, sub_scheme_name)
27
+ # sub_scheme_path = os.path.join(src_path, sub_scheme_name)
28
+
29
+ # conf_scheme = dckrjsn.read_json(conf_scheme_path)
30
+ # sub_scheme = dckrjsn.read_json(sub_scheme_path)
31
+
32
+ class bcolors:
33
+ HEADER = '\033[95m'
34
+ OKBLUE = '\033[94m'
35
+ OKGREEN = '\033[92m'
36
+ WARNING = '\033[93m'
37
+ FAIL = '\033[91m'
38
+ ENDC = '\033[0m'
39
+ BOLD = '\033[1m'
40
+ UNDERLINE = '\033[4m'
41
+
42
+
43
+ command_list = {}
44
+ action_list = []
45
+
46
+
47
+ def addCommand(cur_directory):
48
+ if not any(action['directory'] == cur_directory for action in action_list):
49
+ relative_dir = os.path.relpath(cur_directory, base_directory)
50
+ if relative_dir == '.':
51
+ relative_dir = os.path.basename(os.path.normpath(cur_directory))
52
+ action_list.append({
53
+ 'directory': cur_directory,
54
+ 'relative': relative_dir
55
+ })
56
+
57
+
58
+ def read_yaml(path):
59
+ with open(path, 'r') as stream:
60
+ try:
61
+ return yaml.load(stream, Loader=yaml.SafeLoader)
62
+ except yaml.YAMLError as exc:
63
+ print(exc)
64
+
65
+
66
+ def traverse(cur_directory):
67
+ # print("Traversing in " + cur_directory)
68
+ for sub_name in sub_names:
69
+ sub_path = os.path.join(cur_directory, sub_name)
70
+ compose_paths = [os.path.join(cur_directory, name) for name in compose_names]
71
+
72
+ # print("Checking file at " + sub_path)
73
+ if os.path.isfile(sub_path): # has sub folders
74
+ # print("Has sub folders!")
75
+ # sub_folders = dckrjsn.read_json(sub_path, sch = sub_scheme)
76
+ sub_folders = read_yaml(sub_path)
77
+ for sub_folder in sub_folders:
78
+ # print("Checking out " + sub_folder)
79
+ next_directory = os.path.join(cur_directory, sub_folder)
80
+ traverse(next_directory)
81
+ elif any(os.path.isfile(path) for path in compose_paths): # has a docker-compose file
82
+ addCommand(cur_directory)
83
+
84
+
85
+ def main():
86
+ # global cli
87
+ global base_directory
88
+
89
+ # cli = docker.Client('unix://var/run/docker.sock')
90
+
91
+ # Include external source files for commands
92
+ # These fill the m_cmd list
93
+ for file in os.listdir(os.path.join(src_path, 'commands')):
94
+ ext_file = os.path.splitext(file)
95
+
96
+ if ext_file[1] == '.py' and not ext_file[0] == '__init__':
97
+ importlib.import_module('portmgr.commands.' + ext_file[0])
98
+
99
+ parser = MyParser()
100
+ parser.add_argument('-D',
101
+ dest='base_directory',
102
+ action='store',
103
+ default='',
104
+ help='Set working directory')
105
+ # parser.add_argument('-R',
106
+ # dest='recursive',
107
+ # action='store_true',
108
+ # help='Use dckrsub.json files to recursively apply operations')
109
+
110
+ for cmd in command_list.items():
111
+ parser.add_argument('-' + cmd[0],
112
+ dest='a_cmd',
113
+ action='append_const',
114
+ const=cmd[0],
115
+ help=cmd[1]['hlp'])
116
+
117
+ argv = sys.argv[1:]
118
+ if len(argv) == 1 and not argv[0].startswith('-'):
119
+ argv[0] = '-' + argv[0]
120
+ args = parser.parse_args(argv)
121
+
122
+ if len(sys.argv) == 1:
123
+ parser.print_help()
124
+ sys.exit(1)
125
+
126
+ base_directory = os.path.join(os.getcwd(), args.base_directory)
127
+
128
+ # if args.recursive:
129
+ traverse(base_directory)
130
+ # else:
131
+ # addCommand(base_directory)
132
+
133
+ last_cmd = ''
134
+ for cmd in args.a_cmd: # loop over all passed arguments (t, r, u)
135
+ cur_cmd = command_list[cmd]
136
+ cmd_function = cur_cmd['fnc']
137
+ cmd_order = cur_cmd['ord'];
138
+
139
+ # if last_cmd == 'r' and cur_cmd == 'u':
140
+ # print("Waiting 3 seconds.. ")
141
+ # sleep(3)
142
+
143
+ if cmd_order == 'nrm':
144
+ action_list_sorted = action_list
145
+ elif cmd_order == 'rev':
146
+ action_list_sorted = reversed(action_list)
147
+ else:
148
+ exit(1)
149
+
150
+ failed_list = []
151
+
152
+ for action in action_list_sorted:
153
+ origWD = os.getcwd()
154
+ newWD = action['directory']
155
+ os.chdir(newWD)
156
+ print('-> ' + action["relative"])
157
+ if cmd_function(action) != 0: # execute the function through reflection
158
+ failed_list.append(action)
159
+ os.chdir(origWD)
160
+ if failed_list:
161
+ print('Failed containers:')
162
+ for action in failed_list:
163
+ print('- ' + action['relative'])
164
+ print("")
165
+
166
+ exit(0)
167
+
168
+
169
+ if __name__ == '__main__':
170
+ main()
@@ -0,0 +1,67 @@
1
+ import json
2
+ import os
3
+ from subprocess import run, check_output
4
+
5
+
6
+ def getServices(includeOnlyBuildable=False):
7
+ data = check_output(['docker', 'compose', 'config', '--format', 'json'])
8
+ config = json.loads(data)
9
+ services = config['services']
10
+ if includeOnlyBuildable:
11
+ services = {name: values for name, values in services.items() if 'build' in values.keys()}
12
+ return services
13
+
14
+
15
+ def getServicesRunning():
16
+ data = check_output(['docker', 'compose', 'ps', '--format', 'json'])
17
+ try:
18
+ lines = data.decode().splitlines()
19
+ if len(lines) > 1:
20
+ container_list = []
21
+ for l in lines:
22
+ #print(f'l: {l}')
23
+ container_list.append(json.loads(l.strip()))
24
+ else:
25
+ container_list = json.loads(data)
26
+ except Exception as e:
27
+ print(e)
28
+ print(data.decode())
29
+ if isinstance(container_list, dict):
30
+ container_names = [container_list['Name']]
31
+ else:
32
+ container_names = [s['Name'] for s in container_list]
33
+ return container_names
34
+
35
+
36
+ def getImages():
37
+ data = check_output(['docker', 'compose', 'images', '--format', 'json'])
38
+ image_list = json.loads(data)
39
+ images = [
40
+ {'ID': image['ID'],
41
+ 'Name': image['Repository'],
42
+ 'ContainerName': image['ContainerName'],
43
+ 'Tag': image['Tag']}
44
+ for image in image_list
45
+ ]
46
+ return images
47
+
48
+
49
+ def getStats():
50
+ containers = getServicesRunning()
51
+ data = check_output(['docker', 'stats', '--format', 'json', '--no-stream'] + containers, text=True).strip()
52
+ data_lines = data.split('\n')
53
+ stats = [json.loads(line) for line in data_lines]
54
+ return stats
55
+
56
+
57
+ def runCompose(args, **kwargs):
58
+ command = ['docker', 'compose']
59
+ if os.environ.get("PORTMGR_IN_SCRIPT", "").lower() == "true":
60
+ command += ["--ansi", "never"]
61
+ command += list(args)
62
+ return run(command, **kwargs).returncode
63
+
64
+ def runBuildx(args, **kwargs):
65
+ command = ['docker', 'buildx']
66
+ command += list(args)
67
+ return run(command, **kwargs).returncode
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: portmgr
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Home-page: https://github.com/Craeckie/portmgr
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: humanfriendly~=10.0
10
+ Requires-Dist: jsonschema~=3.2.0
11
+ Requires-Dist: pyyaml~=6.0.2
12
+ Requires-Dist: tabulate~=0.8.9
13
+ Dynamic: home-page
14
+ Dynamic: license-file
15
+
16
+ # portmgr
17
+ portmgr is a wrapper around [docker-compose](https://docs.docker.com/compose/) that allows running typical docker-compose commands recursively. Additionally, it shortens commands to a single letter.
18
+
19
+ Let's say you have organized your compose files like this, you just add a `dckrsub.yml` in each parent folder:
20
+ <pre>
21
+ docker/
22
+ ├── <b>dckrsub.yml</b>
23
+ ├── reverse-proxy/
24
+ │ └── docker-compose.yml
25
+ ├── storage
26
+ │ ├── <b>dckrsub.yml</b>
27
+ │ ├── nextcloud/
28
+ │ │ └── docker-compose.yml
29
+ │ └── immich/
30
+ │ └── docker-compose.yml
31
+ └── scripts
32
+ </pre>
33
+
34
+ Each `dckrsub.yml` has a list of subdirectories, which portmgr should decend into.
35
+ For example, the `dckrsub.yml` in `docker/` might look like this:
36
+ ```yaml
37
+ - reverse-proxy
38
+ - storage
39
+ ```
40
+
41
+ And the `dckrsub.yml` in `docker/storage/` like this:
42
+ ```yaml
43
+ - nextcloud
44
+ - immich
45
+ ```
46
+
47
+ Now, if you run `portmgr u` in `docker/` it will run `docker compose up -d` in `reverse-proxy/`, `storage/nextcloud/` and `storage/immich/`.
48
+
49
+ portmgr starts from the current directory, so when running it in `docker/storage/`, it will run `docker compose` only in `nextcloud/` and `immich/`. You can also use it in a directory with a `docker-compose.yml` as a shortener for docker-compose commands.
50
+
51
+ ### Commands
52
+ The following commands are available. The respective docker-compose commands are in brackets.
53
+
54
+ ```
55
+ u Create and start containers (up)
56
+ p Pull images (pull)
57
+ s Stop services (stop)
58
+ d Stop and remove containers (down)
59
+ l Show container logs (logs)
60
+ a Run shell in container (exec -it <service> sh)
61
+ b Build images (build)
62
+ c List containers (ps)
63
+ t List processes in containers (top)
64
+ r Build and push to registry (build, push)
65
+ v Scan container images for vulnerabilities
66
+ ```
67
+
68
+ You combine multiple commands. For example `portmgr dul`, runs docker compose with `down`, `up` and `logs`, thus stopping, removing and starting all containers and then showing the logs.
69
+
70
+ ### Installation
71
+ ```
72
+ sudo pip install portmgr
73
+ ```
74
+
75
+ Or build it from source (here using the latest commit on master branch)
76
+ ```
77
+ sudo pip install https://github.com/Craeckie/portmgr.git
78
+ ```
79
+
80
+ ### Tipps
81
+ If you use portmgr a lot like me, you might want to shorten it to one letter. For bash, you can add `alias p='portmgr'` to `~/.bashrc`. For fish-shell you can add `abbr p portmgr` to `~/.config/fish/config.fish`.
@@ -0,0 +1,27 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ portmgr/__init__.py
6
+ portmgr/__main__.py
7
+ portmgr/portmgr.py
8
+ portmgr/wrapper.py
9
+ portmgr.egg-info/PKG-INFO
10
+ portmgr.egg-info/SOURCES.txt
11
+ portmgr.egg-info/dependency_links.txt
12
+ portmgr.egg-info/not-zip-safe
13
+ portmgr.egg-info/requires.txt
14
+ portmgr.egg-info/top_level.txt
15
+ portmgr/commands/__init__.py
16
+ portmgr/commands/attach.py
17
+ portmgr/commands/build.py
18
+ portmgr/commands/down.py
19
+ portmgr/commands/logs.py
20
+ portmgr/commands/ps.py
21
+ portmgr/commands/pull.py
22
+ portmgr/commands/push.py
23
+ portmgr/commands/scan.py
24
+ portmgr/commands/stats.py
25
+ portmgr/commands/stop.py
26
+ portmgr/commands/top.py
27
+ portmgr/commands/up.py
@@ -0,0 +1,4 @@
1
+ humanfriendly~=10.0
2
+ jsonschema~=3.2.0
3
+ pyyaml~=6.0.2
4
+ tabulate~=0.8.9
@@ -0,0 +1 @@
1
+ portmgr
@@ -0,0 +1,12 @@
1
+ [project]
2
+ name = "portmgr"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "humanfriendly~=10.0",
9
+ "jsonschema~=3.2.0",
10
+ "pyyaml~=6.0.2",
11
+ "tabulate~=0.8.9",
12
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
portmgr-0.1.0/setup.py ADDED
@@ -0,0 +1,26 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name='portmgr',
5
+ version='1.8.0dev0',
6
+ url="https://github.com/Craeckie/portmgr",
7
+ description="Simple command interface to manage multiple Docker container",
8
+ packages=find_packages(), #['portmgr'],
9
+ license='Creative Commons Attribution-Noncommercial-Share Alike license',
10
+ long_description=open('README.md').read(),
11
+ long_description_content_type="text/markdown",
12
+ zip_safe=False,
13
+
14
+ install_requires=[
15
+ 'jsonschema',
16
+ 'tabulate',
17
+ 'PyYAML',
18
+ 'jsonschema',
19
+ 'humanfriendly'
20
+ ],
21
+ entry_points = {
22
+ 'console_scripts': [
23
+ 'portmgr = portmgr:main'
24
+ ]
25
+ }
26
+ )