docker-dev-env 0.1.0__py3-none-any.whl
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_dev_env/__init__.py +0 -0
- docker_dev_env/bash_scripts/README.md +5 -0
- docker_dev_env/bash_scripts/bash_container.bash +17 -0
- docker_dev_env/bash_scripts/docker_log.bash +36 -0
- docker_dev_env/cli.py +60 -0
- docker_dev_env/docker_utils.py +202 -0
- docker_dev_env/fun.py +18 -0
- docker_dev_env/model/__init__.py +0 -0
- docker_dev_env/model/project.py +73 -0
- docker_dev_env/screens/log_service.py +53 -0
- docker_dev_env/tilix.py +12 -0
- docker_dev_env/ui.py +97 -0
- docker_dev_env/ui.tcss +9 -0
- docker_dev_env-0.1.0.dist-info/METADATA +35 -0
- docker_dev_env-0.1.0.dist-info/RECORD +19 -0
- docker_dev_env-0.1.0.dist-info/WHEEL +5 -0
- docker_dev_env-0.1.0.dist-info/entry_points.txt +2 -0
- docker_dev_env-0.1.0.dist-info/licenses/LICENSE +11 -0
- docker_dev_env-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
SCRIPT_PATH="$(dirname "$0")"
|
|
3
|
+
BASE_PATH=$(realpath "$SCRIPT_PATH/..")
|
|
4
|
+
SERVICE=$1
|
|
5
|
+
|
|
6
|
+
cd "$BASE_PATH" || exit
|
|
7
|
+
|
|
8
|
+
if [ $# -eq 1 ]; then
|
|
9
|
+
ENV_FILE=$(realpath $BASE_PATH/local_vars.env)
|
|
10
|
+
else
|
|
11
|
+
ENV_FILE=$2
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
ENV_FILE=$ENV_FILE docker compose --env-file $ENV_FILE run --rm "$SERVICE" bash
|
|
15
|
+
|
|
16
|
+
echo "Press CTRL+C to close..."
|
|
17
|
+
sleep infinity
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
SCRIPT_PATH="$(dirname "$0")"
|
|
3
|
+
BASE_PATH=$(realpath $SCRIPT_PATH/..)
|
|
4
|
+
DOCKER_PATH=$(realpath $BASE_PATH/$1)
|
|
5
|
+
SERVICE=$2
|
|
6
|
+
|
|
7
|
+
if [ $# -eq 2 ]; then
|
|
8
|
+
ENV_FILE=$(realpath $BASE_PATH/local_vars.env)
|
|
9
|
+
else
|
|
10
|
+
ENV_FILE=$3
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
emoji[0]="😖"
|
|
14
|
+
emoji[1]="😭"
|
|
15
|
+
emoji[2]="😫"
|
|
16
|
+
emoji[3]="😞"
|
|
17
|
+
emoji[4]="😓"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
cd $DOCKER_PATH
|
|
21
|
+
|
|
22
|
+
while :
|
|
23
|
+
do
|
|
24
|
+
ENV_FILE=$ENV_FILE docker compose --env-file $ENV_FILE logs -f --tail=50 $SERVICE
|
|
25
|
+
|
|
26
|
+
size=${#emoji[@]}
|
|
27
|
+
index=$(($RANDOM % $size))
|
|
28
|
+
echo "${emoji[$index]} $SERVICE log ended, press <CTRL+C> to close..."
|
|
29
|
+
sleep 5
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
ENV_FILE=$ENV_FILE docker compose --env-file $ENV_FILE logs -f $SERVICE
|
|
34
|
+
|
|
35
|
+
echo "Press CTRL+C to close..."
|
|
36
|
+
sleep infinity
|
docker_dev_env/cli.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from .docker_utils import DockerUtils
|
|
4
|
+
from .fun import goodbye
|
|
5
|
+
from .model.project import Project
|
|
6
|
+
from .ui import DockerDevEnvUi
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _setup_argparse() -> argparse.ArgumentParser:
|
|
10
|
+
"""Configure argument parser."""
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
description=__doc__,
|
|
13
|
+
formatter_class=argparse.RawTextHelpFormatter)
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
'-c', '--cleanup', action='store_true',
|
|
16
|
+
help='just cleanup')
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
'-n', '--no-cleanup', action='store_true',
|
|
19
|
+
help='no cleanup after finish')
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
'-p', '--project', default='project-config.yml',
|
|
22
|
+
help='project yaml file')
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
'-s', '--stopped', action='store_true',
|
|
25
|
+
help='do not create container')
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
'-r', '--rebuild', action='store_true',
|
|
28
|
+
help='rebuild container before start')
|
|
29
|
+
# we show "-i"/init for --help but as its already handled
|
|
30
|
+
# by the ./run bash script we ignore it.
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
'-i', '--init', action='store_true',
|
|
33
|
+
help='(re)init: deletes the local dev')
|
|
34
|
+
return parser
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
parser = _setup_argparse()
|
|
39
|
+
args = parser.parse_args()
|
|
40
|
+
project = Project.load(args.project)
|
|
41
|
+
_docker_utils = DockerUtils(project)
|
|
42
|
+
if args.cleanup:
|
|
43
|
+
_docker_utils.cleanup()
|
|
44
|
+
return
|
|
45
|
+
if args.rebuild:
|
|
46
|
+
_docker_utils.build_all()
|
|
47
|
+
|
|
48
|
+
if not args.stopped:
|
|
49
|
+
_docker_utils.autostart()
|
|
50
|
+
app = DockerDevEnvUi(_docker_utils)
|
|
51
|
+
app.run()
|
|
52
|
+
|
|
53
|
+
# Cleanup at the end
|
|
54
|
+
if not args.no_cleanup:
|
|
55
|
+
_docker_utils.cleanup()
|
|
56
|
+
goodbye()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == '__main__':
|
|
60
|
+
main()
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import subprocess
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
import docker
|
|
8
|
+
|
|
9
|
+
from .model.project import Project, Service
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DockerUtils:
|
|
13
|
+
"""Wrapper around Popen and docker to manage docker container."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, project: Project):
|
|
16
|
+
self.docker_client = docker.from_env()
|
|
17
|
+
self.project = project
|
|
18
|
+
|
|
19
|
+
def autostart(self):
|
|
20
|
+
if self.project.networks:
|
|
21
|
+
self.create_network(self.project.networks, )
|
|
22
|
+
for service in self.project.services:
|
|
23
|
+
if service.autostart:
|
|
24
|
+
self.compose_start(service, )
|
|
25
|
+
|
|
26
|
+
def run_background(self, cmds):
|
|
27
|
+
def run_docker_command():
|
|
28
|
+
for cmd in cmds:
|
|
29
|
+
# FIXME: use asyncio.create_subprocess_exec instead
|
|
30
|
+
subprocess.run(
|
|
31
|
+
cmd,
|
|
32
|
+
stdout=subprocess.DEVNULL,
|
|
33
|
+
stderr=subprocess.DEVNULL,
|
|
34
|
+
text=True)
|
|
35
|
+
|
|
36
|
+
thread = threading.Thread(target=run_docker_command, daemon=True)
|
|
37
|
+
thread.start()
|
|
38
|
+
|
|
39
|
+
def compose_cmd(self, cmd: List):
|
|
40
|
+
docker_cmd = ['docker', 'compose']
|
|
41
|
+
if self.project.env_file:
|
|
42
|
+
docker_cmd += ['--env-file', self.project.env_file]
|
|
43
|
+
if isinstance(cmd, str):
|
|
44
|
+
docker_cmd.append(cmd)
|
|
45
|
+
else:
|
|
46
|
+
docker_cmd += cmd
|
|
47
|
+
return docker_cmd
|
|
48
|
+
|
|
49
|
+
def logs_cmd(self, service):
|
|
50
|
+
return self.compose_cmd(['logs', '-f', '--tail=50', service.name])
|
|
51
|
+
|
|
52
|
+
def compose_start_cmd(self, service: Service):
|
|
53
|
+
return self.compose_cmd(['up', '-d', service.name])
|
|
54
|
+
|
|
55
|
+
def compose_start(self, service: Service):
|
|
56
|
+
"""Start docker compose command."""
|
|
57
|
+
if hasattr(service, 'description') and service.description:
|
|
58
|
+
print(f'🚀 Starting {service.description}...')
|
|
59
|
+
cmd = self.compose_start_cmd(service)
|
|
60
|
+
subprocess.run(cmd)
|
|
61
|
+
|
|
62
|
+
def compose_kill_cmd(self, service: Service):
|
|
63
|
+
"""Kill container using compose (for faster down)."""
|
|
64
|
+
return self.compose_cmd(['kill', service.name])
|
|
65
|
+
|
|
66
|
+
def compose_kill(self, service: Service):
|
|
67
|
+
"""Kill container using compose (for faster down)."""
|
|
68
|
+
title = service.name
|
|
69
|
+
if hasattr(service, 'description') and service.description:
|
|
70
|
+
title = service.description
|
|
71
|
+
print(f'➜ Kill container {title}')
|
|
72
|
+
cmd = self.compose_kill_cmd(service)
|
|
73
|
+
subprocess.run(cmd)
|
|
74
|
+
|
|
75
|
+
def compose_build_cmd(self, service: Service):
|
|
76
|
+
"""Build image by service name."""
|
|
77
|
+
return self.compose_cmd(['build', service.name])
|
|
78
|
+
|
|
79
|
+
def compose_build(self, service: Service):
|
|
80
|
+
"""Build image by service name."""
|
|
81
|
+
cmd = self.compose_build_cmd(service)
|
|
82
|
+
subprocess.run(cmd)
|
|
83
|
+
|
|
84
|
+
def compose_down(self):
|
|
85
|
+
"""Run docker compose down to clean up."""
|
|
86
|
+
cmd = self.compose_cmd(['down'])
|
|
87
|
+
subprocess.run(cmd)
|
|
88
|
+
|
|
89
|
+
def create_network(self, networks):
|
|
90
|
+
if isinstance(networks, str):
|
|
91
|
+
networks = [networks]
|
|
92
|
+
for network in networks:
|
|
93
|
+
print(f'🌐 Create docker network {network}')
|
|
94
|
+
docker_cmd = ['docker', 'network', 'create', network]
|
|
95
|
+
subprocess.run(docker_cmd)
|
|
96
|
+
|
|
97
|
+
def rm_network(self, networks):
|
|
98
|
+
if isinstance(networks, str):
|
|
99
|
+
networks = [networks]
|
|
100
|
+
for network in networks:
|
|
101
|
+
print(f'➜ Remove network {network}')
|
|
102
|
+
docker_cmd = ['docker', 'network', 'rm', network]
|
|
103
|
+
subprocess.run(docker_cmd)
|
|
104
|
+
|
|
105
|
+
def _not_available(self, service: Service):
|
|
106
|
+
return {
|
|
107
|
+
'service': service.title,
|
|
108
|
+
'container_id': 'N/A',
|
|
109
|
+
'status': 'not running',
|
|
110
|
+
'health': 'unknown',
|
|
111
|
+
'image': 'N/A',
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
def _get_health_status(self, container) -> str:
|
|
115
|
+
"""Get health check status from container."""
|
|
116
|
+
container.reload()
|
|
117
|
+
state = container.attrs.get('State', {})
|
|
118
|
+
|
|
119
|
+
if state.get('Health'):
|
|
120
|
+
health = state['Health'].get('Status', 'unknown')
|
|
121
|
+
if health == 'healthy':
|
|
122
|
+
color = 'green'
|
|
123
|
+
elif health == 'unhealthy':
|
|
124
|
+
color = 'red'
|
|
125
|
+
else:
|
|
126
|
+
color = 'yellow'
|
|
127
|
+
return f'[{color}]{health}[/]'
|
|
128
|
+
return 'no check'
|
|
129
|
+
|
|
130
|
+
def service_status(self, service: Service):
|
|
131
|
+
containers = self.docker_client.containers.list(
|
|
132
|
+
all=True,
|
|
133
|
+
filters={'label': f'com.docker.compose.service={service.name}'}
|
|
134
|
+
)
|
|
135
|
+
if not containers:
|
|
136
|
+
return self._not_available(service)
|
|
137
|
+
container = containers[0]
|
|
138
|
+
health_status = self._get_health_status(container)
|
|
139
|
+
tags = container.image.tags
|
|
140
|
+
return {
|
|
141
|
+
'service': service.title,
|
|
142
|
+
'container_id': container.short_id,
|
|
143
|
+
'status': container.status,
|
|
144
|
+
'health': health_status,
|
|
145
|
+
'image': tags[0] if tags else 'unknown',
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
def service_running(self, service: Service):
|
|
149
|
+
containers = self.docker_client.containers.list(
|
|
150
|
+
all=True,
|
|
151
|
+
filters={'label': f'com.docker.compose.service={service.name}'}
|
|
152
|
+
)
|
|
153
|
+
if not containers:
|
|
154
|
+
return False
|
|
155
|
+
container = containers[0]
|
|
156
|
+
return container.status == 'running'
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def services(self):
|
|
160
|
+
return self.project.services
|
|
161
|
+
|
|
162
|
+
def health_row(self, service: Service):
|
|
163
|
+
service_status = self.service_status(service)
|
|
164
|
+
return [
|
|
165
|
+
service_status['service'],
|
|
166
|
+
service_status['container_id'],
|
|
167
|
+
service_status['status'],
|
|
168
|
+
service_status['health'],
|
|
169
|
+
service_status['image'],
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
def rebuild(self, service: Service):
|
|
173
|
+
cmds = [
|
|
174
|
+
self.compose_kill_cmd(service),
|
|
175
|
+
self.compose_build_cmd(service)
|
|
176
|
+
]
|
|
177
|
+
if service.autostart:
|
|
178
|
+
cmds.append(self.compose_start_cmd(service))
|
|
179
|
+
self.run_background(cmds)
|
|
180
|
+
|
|
181
|
+
def restart(self, service: Service):
|
|
182
|
+
cmds = []
|
|
183
|
+
if self.service_running(service):
|
|
184
|
+
cmds.append(self.compose_kill_cmd(service))
|
|
185
|
+
cmds.append(self.compose_start_cmd(service))
|
|
186
|
+
self.run_background(cmds)
|
|
187
|
+
|
|
188
|
+
def kill(self, service: Service):
|
|
189
|
+
self.run_background([self.compose_kill_cmd(service)])
|
|
190
|
+
|
|
191
|
+
def build_all(self):
|
|
192
|
+
for service in self.services:
|
|
193
|
+
if service.rebuild:
|
|
194
|
+
self.compose_build(service)
|
|
195
|
+
|
|
196
|
+
def cleanup(self):
|
|
197
|
+
for service in self.services:
|
|
198
|
+
self.compose_kill(service)
|
|
199
|
+
self.compose_down()
|
|
200
|
+
if self.project.networks and self.project.cleanup_networks:
|
|
201
|
+
self.rm_network(self.project.networks)
|
|
202
|
+
print(' 🧽 Cleanup complete.')
|
docker_dev_env/fun.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
FAREWELL = [
|
|
4
|
+
'How lucky I am to have something that makes saying goodbye so hard.',
|
|
5
|
+
"Don't cry because it's over, smile because it happened.",
|
|
6
|
+
'The pain of parting is nothing to the joy of meeting again.',
|
|
7
|
+
"Man's feelings are always purest and most glowing in the "
|
|
8
|
+
'hour of meeting and farewell.',
|
|
9
|
+
'So long, and thanks for all the fish.',
|
|
10
|
+
'We only part to meet again.',
|
|
11
|
+
'Every goodbye makes the next hello closer.',
|
|
12
|
+
'Let there be spaces in your togetherness.',
|
|
13
|
+
'My battery is low, and its getting dark.'
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def goodbye():
|
|
18
|
+
print(f'\n 👋 Goodbye: {random.choice(FAREWELL)} ')
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from dataclasses import asdict, dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Service:
|
|
10
|
+
name: str
|
|
11
|
+
description: str | None = None
|
|
12
|
+
rebuild: bool = True
|
|
13
|
+
autostart: bool = True
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def title(self):
|
|
17
|
+
"""Return the title we show to the user."""
|
|
18
|
+
return self.description if self.description else self.name
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ProjectEnv:
|
|
22
|
+
services: List[Service]
|
|
23
|
+
docker_compose_path: str = './compose.yml'
|
|
24
|
+
update_interval: int = 1
|
|
25
|
+
log_tail: int = 100
|
|
26
|
+
build_timeout: float = 3600
|
|
27
|
+
env_file: str | None = None
|
|
28
|
+
networks: List | None = None
|
|
29
|
+
cleanup_networks: bool = True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Project:
|
|
34
|
+
name: str
|
|
35
|
+
# environments: Dict[ProjectEnv]
|
|
36
|
+
services: List[Service]
|
|
37
|
+
docker_compose_path: str = './compose.yml'
|
|
38
|
+
update_interval: int = 1
|
|
39
|
+
log_tail: int = 100
|
|
40
|
+
build_timeout: float = 3600
|
|
41
|
+
env_file: None | str = None
|
|
42
|
+
networks: List | None = None
|
|
43
|
+
cleanup_networks: bool = True
|
|
44
|
+
|
|
45
|
+
def save(self, path: str):
|
|
46
|
+
with open(path, 'w') as f:
|
|
47
|
+
yaml.safe_dump(asdict(self), f)
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def create_new(path: str):
|
|
51
|
+
print('Create new Project config file.')
|
|
52
|
+
project = Project('new_project', [])
|
|
53
|
+
project.save(path)
|
|
54
|
+
return project
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def load(path: str):
|
|
58
|
+
if not Path(path).exists():
|
|
59
|
+
return Project.create_new(path)
|
|
60
|
+
with open(path, 'r') as f:
|
|
61
|
+
data = yaml.safe_load(f)
|
|
62
|
+
if not data:
|
|
63
|
+
return Project.create_new(path)
|
|
64
|
+
return Project(**data)
|
|
65
|
+
|
|
66
|
+
def __post_init__(self):
|
|
67
|
+
services = []
|
|
68
|
+
for _service in self.services:
|
|
69
|
+
if isinstance(_service, Service):
|
|
70
|
+
services.append(_service)
|
|
71
|
+
else:
|
|
72
|
+
services.append(Service(**_service))
|
|
73
|
+
self.services = services
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from textual.app import ComposeResult
|
|
3
|
+
from textual.binding import Binding
|
|
4
|
+
from textual.screen import ModalScreen
|
|
5
|
+
from textual.widgets import Footer, Header, Log
|
|
6
|
+
|
|
7
|
+
from ..model.project import Service
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LogService(ModalScreen):
|
|
11
|
+
CSS_PATH = 'log_service.tcss'
|
|
12
|
+
BINDINGS = [
|
|
13
|
+
Binding('escape', 'close_modal', 'Close', show=True),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
def __init__(self, service: Service):
|
|
17
|
+
super().__init__()
|
|
18
|
+
self.docker_utils = self.app.docker_utils
|
|
19
|
+
self.service = service
|
|
20
|
+
|
|
21
|
+
def compose(self) -> ComposeResult:
|
|
22
|
+
yield Header()
|
|
23
|
+
yield Log(id='docker_logs')
|
|
24
|
+
yield Footer()
|
|
25
|
+
|
|
26
|
+
def action_close_modal(self) -> None:
|
|
27
|
+
"""Close the modal screen."""
|
|
28
|
+
self.app.pop_screen()
|
|
29
|
+
|
|
30
|
+
def on_mount(self) -> None:
|
|
31
|
+
"""Start streaming Docker logs when modal mounts."""
|
|
32
|
+
log_widget = self.query_one('#docker_logs')
|
|
33
|
+
self.run_worker(self.stream_docker_logs(log_widget))
|
|
34
|
+
|
|
35
|
+
async def stream_docker_logs(self, log_widget: Log) -> None:
|
|
36
|
+
try:
|
|
37
|
+
cmd = self.docker_utils.logs_cmd(self.service)
|
|
38
|
+
process = await asyncio.create_subprocess_exec(
|
|
39
|
+
*cmd,
|
|
40
|
+
stdout=asyncio.subprocess.PIPE,
|
|
41
|
+
stderr=asyncio.subprocess.PIPE,
|
|
42
|
+
)
|
|
43
|
+
while True:
|
|
44
|
+
line = await process.stdout.readline()
|
|
45
|
+
if not line:
|
|
46
|
+
break
|
|
47
|
+
# Decode and write to log widget
|
|
48
|
+
log_widget.write(line.decode().rstrip() + '\n')
|
|
49
|
+
except Exception as e:
|
|
50
|
+
log_widget.write(f'Error: {str(e)}')
|
|
51
|
+
finally:
|
|
52
|
+
if process:
|
|
53
|
+
process.terminate()
|
docker_dev_env/tilix.py
ADDED
docker_dev_env/ui.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from textual.app import App, ComposeResult, on
|
|
2
|
+
from textual.binding import Binding
|
|
3
|
+
from textual.containers import Container
|
|
4
|
+
from textual.widgets import \
|
|
5
|
+
DataTable, Footer, Static, Header
|
|
6
|
+
from textual.css.query import NoMatches
|
|
7
|
+
|
|
8
|
+
from .model.project import Service
|
|
9
|
+
from .docker_utils import DockerUtils
|
|
10
|
+
from .screens.log_service import LogService
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ContainerTable(DataTable):
|
|
14
|
+
"""DataTable displaying known Docker services and their health."""
|
|
15
|
+
|
|
16
|
+
def on_mount(self) -> None:
|
|
17
|
+
self.cursor_type = 'row'
|
|
18
|
+
self.add_columns(
|
|
19
|
+
'Service',
|
|
20
|
+
'Container ID',
|
|
21
|
+
'Status',
|
|
22
|
+
'Health',
|
|
23
|
+
'Image',
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DockerStats(Static):
|
|
28
|
+
"""Display Docker daemon connection status."""
|
|
29
|
+
|
|
30
|
+
status = 'Initializing...'
|
|
31
|
+
|
|
32
|
+
def render(self) -> str:
|
|
33
|
+
return f'[bold cyan]Status:[/bold cyan] {self.status}'
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DockerDevEnvUi(App):
|
|
37
|
+
CSS_PATH = 'ui.tcss'
|
|
38
|
+
BINDINGS = [
|
|
39
|
+
Binding('q', 'quit', 'Quit'),
|
|
40
|
+
Binding('r', 'rebuild', 'Rebuild'),
|
|
41
|
+
Binding('s', 'restart', '(Re)Start'),
|
|
42
|
+
Binding('k', 'kill', 'Kill'),
|
|
43
|
+
Binding('enter', 'logs', 'Logs'),
|
|
44
|
+
# Binding('t', 'tiilix', 'Tilix Window')
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
def __init__(self, docker_utils: DockerUtils):
|
|
48
|
+
super().__init__()
|
|
49
|
+
self.docker_utils = docker_utils
|
|
50
|
+
|
|
51
|
+
def current_service(self) -> Service:
|
|
52
|
+
table = self.query_one('#containers')
|
|
53
|
+
return self.docker_utils.services[table.cursor_row]
|
|
54
|
+
|
|
55
|
+
def action_rebuild(self):
|
|
56
|
+
self.docker_utils.rebuild(self.current_service())
|
|
57
|
+
|
|
58
|
+
def action_restart(self):
|
|
59
|
+
self.docker_utils.restart(self.current_service())
|
|
60
|
+
|
|
61
|
+
def action_kill(self):
|
|
62
|
+
self.docker_utils.kill(self.current_service())
|
|
63
|
+
|
|
64
|
+
def compose(self) -> ComposeResult:
|
|
65
|
+
"""Create child widgets."""
|
|
66
|
+
yield Header()
|
|
67
|
+
yield ContainerTable(id='containers')
|
|
68
|
+
# with Container(id='stats_box'):
|
|
69
|
+
# yield DockerStats(id='stats')
|
|
70
|
+
yield Footer()
|
|
71
|
+
|
|
72
|
+
def on_data_table_row_selected(
|
|
73
|
+
self, event: ContainerTable.CellSelected) -> None:
|
|
74
|
+
"""Handle click/enter on a cell."""
|
|
75
|
+
self.push_screen(LogService(self.current_service()))
|
|
76
|
+
|
|
77
|
+
def init_services(self):
|
|
78
|
+
try:
|
|
79
|
+
containers = self.query_one('#containers')
|
|
80
|
+
except NoMatches:
|
|
81
|
+
return
|
|
82
|
+
for service in self.docker_utils.services:
|
|
83
|
+
containers.add_row(*self.docker_utils.health_row(service))
|
|
84
|
+
|
|
85
|
+
def update_services(self):
|
|
86
|
+
try:
|
|
87
|
+
containers = self.query_one('#containers')
|
|
88
|
+
except NoMatches:
|
|
89
|
+
return
|
|
90
|
+
for row, service in enumerate(self.docker_utils.services):
|
|
91
|
+
for col, val in enumerate(self.docker_utils.health_row(service)):
|
|
92
|
+
containers.update_cell_at((row, col), val)
|
|
93
|
+
|
|
94
|
+
def on_mount(self) -> None:
|
|
95
|
+
self.init_services()
|
|
96
|
+
self.update_services()
|
|
97
|
+
self.set_interval(1, self.update_services)
|
docker_dev_env/ui.tcss
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: docker_dev_env
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Docker based development environment
|
|
5
|
+
Author-email: Andreas Bresser <andreas.bresser@dfki.de>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://codeberg.org/brean/docker-dev-env
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: argcomplete
|
|
12
|
+
Requires-Dist: docker
|
|
13
|
+
Requires-Dist: dotenv
|
|
14
|
+
Requires-Dist: pyyaml
|
|
15
|
+
Requires-Dist: textual
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# docker-dev-env
|
|
19
|
+
|
|
20
|
+
Python and Bash scripts around docker to manage a docker compose based development environments.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
just copy the `project-config.yml` and `run.bash`-file in your project root directory and modify the `project-config.yml` to tell it what services to manage.
|
|
24
|
+
|
|
25
|
+
It will ask to install `docker` and `docker compose` if they are not installed.
|
|
26
|
+
|
|
27
|
+
At first run it asks to install as local virutal environment inside your project and adds this to your .gitignore.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
- just one run.bash-script needed to manage the whole project
|
|
31
|
+
- keyboard-shortcuts and args to start, (re)build and log docker container
|
|
32
|
+
- include provided env-file for secrets and deployment
|
|
33
|
+
|
|
34
|
+
## Screenshot
|
|
35
|
+

|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
docker_dev_env/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
docker_dev_env/cli.py,sha256=5d3BXZs879RXkK6QUltnZH--NDQJCKOQ4349I4gZzq4,1702
|
|
3
|
+
docker_dev_env/docker_utils.py,sha256=wf2afvM8CmeRTCYJso9ThRLSracJnbRmk3lMsckZGBg,6716
|
|
4
|
+
docker_dev_env/fun.py,sha256=9tlhqxxcG0w46Qj-7DUjtR5N2BhcZssfxNEqQwIzbIA,637
|
|
5
|
+
docker_dev_env/tilix.py,sha256=iMhu760JuJnkc2HPaVblZbVYhSx1PMS_QLc-RnmOx9g,201
|
|
6
|
+
docker_dev_env/ui.py,sha256=q-PZ3bDOSEVKmFXEff2wyoHCVPFOgC6ozZCqGaQNdXQ,2961
|
|
7
|
+
docker_dev_env/ui.tcss,sha256=FwvZ21KCS8Rk0WC5R62t3_mQoAsPA_cQ2f8e0unZwtE,114
|
|
8
|
+
docker_dev_env/bash_scripts/README.md,sha256=TglpBEqOZgPDoWMuO_TsimtjzzCijhKNLc5zOXqq5BA,259
|
|
9
|
+
docker_dev_env/bash_scripts/bash_container.bash,sha256=PwXNvOznDQloNVS5VwvdRDbVExJ2-ITVKM49hgcn7QM,340
|
|
10
|
+
docker_dev_env/bash_scripts/docker_log.bash,sha256=gha4V-J70JB9Ekd5JuOMf0qjbEjqV4jQxMie1C3C_UE,677
|
|
11
|
+
docker_dev_env/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
docker_dev_env/model/project.py,sha256=HwMMKtjKArWUsW6S7h8HgUYbwF1-yWwJlem3AKOcdKk,1897
|
|
13
|
+
docker_dev_env/screens/log_service.py,sha256=cMRukcS7BRaqv4V_ZJDAwi5YITU80HXJLGempMa9Dc8,1691
|
|
14
|
+
docker_dev_env-0.1.0.dist-info/licenses/LICENSE,sha256=qeAwnsqJbKlClZS--g3HQl_42jGt0vnUyUeyjCOPh0Q,1466
|
|
15
|
+
docker_dev_env-0.1.0.dist-info/METADATA,sha256=h55dP4oN_7hCa3BpQ8FtzxnzGgZxOrcoI-sjSqq5Ce0,1197
|
|
16
|
+
docker_dev_env-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
17
|
+
docker_dev_env-0.1.0.dist-info/entry_points.txt,sha256=wpCncpE8AalYZ7ojdxxKpfY3zhldnMsDIFh8XvT5JRk,59
|
|
18
|
+
docker_dev_env-0.1.0.dist-info/top_level.txt,sha256=-i2tXphymAOjHchvnFDohcv3C9MXmiXVvpeZjU4ADkQ,15
|
|
19
|
+
docker_dev_env-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Copyright (c) 2026 Andreas Bresser.
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
4
|
+
|
|
5
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
6
|
+
|
|
7
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
8
|
+
|
|
9
|
+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
10
|
+
|
|
11
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
docker_dev_env
|