stoobly-agent 1.10.0__py3-none-any.whl → 1.10.2__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.
- stoobly_agent/__init__.py +1 -1
- stoobly_agent/__main__.py +10 -0
- stoobly_agent/app/api/application_http_request_handler.py +5 -2
- stoobly_agent/app/cli/ca_cert_cli.py +9 -5
- stoobly_agent/app/cli/helpers/replay_facade.py +2 -2
- stoobly_agent/app/cli/intercept_cli.py +5 -5
- stoobly_agent/app/cli/request_cli.py +2 -2
- stoobly_agent/app/cli/scaffold/app.py +14 -5
- stoobly_agent/app/cli/scaffold/app_command.py +0 -4
- stoobly_agent/app/cli/scaffold/app_config.py +49 -2
- stoobly_agent/app/cli/scaffold/app_create_command.py +145 -76
- stoobly_agent/app/cli/scaffold/constants.py +9 -4
- stoobly_agent/app/cli/scaffold/docker/constants.py +3 -1
- stoobly_agent/app/cli/scaffold/docker/service/build_decorator.py +4 -4
- stoobly_agent/app/cli/scaffold/docker/service/builder.py +31 -54
- stoobly_agent/app/cli/scaffold/docker/service/configure_gateway.py +3 -0
- stoobly_agent/app/cli/scaffold/docker/template_files.py +112 -0
- stoobly_agent/app/cli/scaffold/docker/workflow/build_decorator.py +1 -1
- stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +30 -47
- stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +3 -2
- stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +1 -1
- stoobly_agent/app/cli/scaffold/docker/workflow/dns_decorator.py +2 -3
- stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +1 -1
- stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +1 -1
- stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +1 -1
- stoobly_agent/app/cli/scaffold/docker/workflow/run_command.py +423 -0
- stoobly_agent/app/cli/scaffold/local/__init__.py +0 -0
- stoobly_agent/app/cli/scaffold/local/service/__init__.py +0 -0
- stoobly_agent/app/cli/scaffold/local/service/builder.py +72 -0
- stoobly_agent/app/cli/scaffold/local/workflow/__init__.py +0 -0
- stoobly_agent/app/cli/scaffold/local/workflow/builder.py +35 -0
- stoobly_agent/app/cli/scaffold/local/workflow/run_command.py +339 -0
- stoobly_agent/app/cli/scaffold/service_command.py +9 -1
- stoobly_agent/app/cli/scaffold/service_config.py +9 -25
- stoobly_agent/app/cli/scaffold/service_create_command.py +18 -6
- stoobly_agent/app/cli/scaffold/service_docker_compose.py +3 -3
- stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +10 -7
- stoobly_agent/app/cli/scaffold/templates/app/.Makefile +2 -2
- stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +4 -4
- stoobly_agent/app/cli/scaffold/templates/app/build/mock/configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/build/record/configure +28 -0
- stoobly_agent/app/cli/scaffold/templates/app/build/test/configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +4 -4
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/run +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/run +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/run +3 -0
- stoobly_agent/app/cli/scaffold/templates/build/services/build/mock/.configure +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/services/build/mock/.init +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/services/build/mock/.run +14 -0
- stoobly_agent/app/cli/scaffold/templates/build/services/build/record/.configure +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/services/build/record/.init +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/services/build/record/.run +14 -0
- stoobly_agent/app/cli/scaffold/templates/build/services/build/test/.configure +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/services/build/test/.init +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/services/build/test/.run +14 -0
- stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/mock/.configure +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/mock/.init +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/mock/.run +19 -0
- stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/record/.configure +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/record/.init +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/record/.run +19 -0
- stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.configure +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.init +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.run +19 -0
- stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scaffold/.up +0 -1
- stoobly_agent/app/cli/scaffold/templates/build/workflows/mock/.configure +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/workflows/mock/.init +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/workflows/mock/.run +14 -0
- stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.configure +25 -1
- stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.init +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.run +14 -0
- stoobly_agent/app/cli/scaffold/templates/build/workflows/test/.configure +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/workflows/test/.init +5 -1
- stoobly_agent/app/cli/scaffold/templates/build/workflows/test/.run +14 -0
- stoobly_agent/app/cli/scaffold/templates/constants.py +35 -19
- stoobly_agent/app/cli/scaffold/templates/factory.py +34 -18
- stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.run +21 -0
- stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.run +21 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/configure +5 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/run +3 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/record/configure +21 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/record/run +3 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/test/configure +5 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/test/run +3 -0
- stoobly_agent/app/cli/scaffold/workflow_command.py +18 -4
- stoobly_agent/app/cli/scaffold/workflow_copy_command.py +5 -4
- stoobly_agent/app/cli/scaffold/workflow_create_command.py +31 -29
- stoobly_agent/app/cli/scaffold/workflow_run_command.py +18 -151
- stoobly_agent/app/cli/scaffold_cli.py +134 -182
- stoobly_agent/app/cli/scenario_cli.py +2 -2
- stoobly_agent/app/cli/types/test.py +2 -2
- stoobly_agent/app/cli/types/workflow_run_command.py +52 -3
- stoobly_agent/app/proxy/handle_mock_service.py +1 -1
- stoobly_agent/app/proxy/intercept_settings.py +6 -26
- stoobly_agent/app/proxy/mock/eval_fixtures_service.py +177 -27
- stoobly_agent/app/proxy/mock/types/__init__.py +22 -1
- stoobly_agent/app/proxy/record/upload_request_service.py +3 -6
- stoobly_agent/app/proxy/replay/body_parser_service.py +8 -5
- stoobly_agent/app/proxy/replay/multipart.py +15 -13
- stoobly_agent/app/proxy/replay/replay_request_service.py +2 -2
- stoobly_agent/app/proxy/run.py +3 -0
- stoobly_agent/app/proxy/test/context.py +0 -4
- stoobly_agent/app/proxy/test/context_abc.py +0 -5
- stoobly_agent/app/proxy/utils/publish_change_service.py +20 -23
- stoobly_agent/app/settings/__init__.py +10 -7
- stoobly_agent/cli.py +61 -16
- stoobly_agent/config/data_dir.py +1 -8
- stoobly_agent/public/12-es2015.618ecfd5f735b801b50f.js +1 -0
- stoobly_agent/public/12-es5.618ecfd5f735b801b50f.js +1 -0
- stoobly_agent/public/index.html +1 -1
- stoobly_agent/public/main-es2015.5a9aa16433404c3f423a.js +1 -0
- stoobly_agent/public/main-es5.5a9aa16433404c3f423a.js +1 -0
- stoobly_agent/public/runtime-es2015.77bcd31efed9e5d5d431.js +1 -0
- stoobly_agent/public/runtime-es5.77bcd31efed9e5d5d431.js +1 -0
- stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +17 -6
- stoobly_agent/test/app/cli/scaffold/docker/cli_invoker.py +177 -0
- stoobly_agent/test/app/cli/scaffold/{cli_test.py → docker/cli_test.py} +4 -11
- stoobly_agent/test/app/cli/scaffold/{e2e_test.py → docker/e2e_test.py} +42 -27
- stoobly_agent/test/app/cli/scaffold/local/__init__.py +0 -0
- stoobly_agent/test/app/cli/scaffold/{cli_invoker.py → local/cli_invoker.py} +38 -32
- stoobly_agent/test/app/cli/scaffold/local/e2e_test.py +342 -0
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +903 -2
- stoobly_agent/test/app/proxy/replay/body_parser_service_test.py +95 -3
- stoobly_agent/test/config/data_dir_test.py +2 -7
- stoobly_agent/test/test_helper.py +16 -5
- {stoobly_agent-1.10.0.dist-info → stoobly_agent-1.10.2.dist-info}/METADATA +4 -2
- {stoobly_agent-1.10.0.dist-info → stoobly_agent-1.10.2.dist-info}/RECORD +157 -129
- {stoobly_agent-1.10.0.dist-info → stoobly_agent-1.10.2.dist-info}/WHEEL +1 -1
- stoobly_agent/app/cli/helpers/shell.py +0 -26
- stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/configure +0 -3
- stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/configure +0 -3
- stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/configure +0 -3
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/bin/configure +0 -3
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/bin/configure +0 -3
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/bin/configure +0 -3
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +0 -13
- stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/configure +0 -47
- stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +0 -13
- stoobly_agent/public/12-es2015.be58ed0ef449008b932e.js +0 -1
- stoobly_agent/public/12-es5.be58ed0ef449008b932e.js +0 -1
- stoobly_agent/public/main-es2015.089b46f303768fbe864f.js +0 -1
- stoobly_agent/public/main-es5.089b46f303768fbe864f.js +0 -1
- stoobly_agent/public/runtime-es2015.f8c814b38b27708e91c1.js +0 -1
- stoobly_agent/public/runtime-es5.f8c814b38b27708e91c1.js +0 -1
- /stoobly_agent/app/cli/scaffold/templates/app/build/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/build/mock/{bin/init → init} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/build/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/build/record/{bin/init → init} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/build/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/build/test/{bin/init → init} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/{bin/init → init} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/{bin/init → init} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/{bin/init → init} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/gateway/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/gateway/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/gateway/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/{.docker-compose.exec.yml → .docker-compose.yml} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{bin/init → init} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/workflow/record/{bin/init → init} +0 -0
- /stoobly_agent/app/cli/scaffold/templates/workflow/test/{bin/init → init} +0 -0
- {stoobly_agent-1.10.0.dist-info → stoobly_agent-1.10.2.dist-info}/entry_points.txt +0 -0
- {stoobly_agent-1.10.0.dist-info → stoobly_agent-1.10.2.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,423 @@
|
|
1
|
+
import os
|
2
|
+
import pdb
|
3
|
+
import subprocess
|
4
|
+
import sys
|
5
|
+
|
6
|
+
from typing import List
|
7
|
+
|
8
|
+
from stoobly_agent.app.cli.scaffold.docker.constants import APP_EGRESS_NETWORK_TEMPLATE, APP_INGRESS_NETWORK_TEMPLATE, DOCKERFILE_CONTEXT
|
9
|
+
from stoobly_agent.app.cli.scaffold.docker.service.configure_gateway import configure_gateway
|
10
|
+
from stoobly_agent.app.cli.scaffold.templates.constants import CORE_ENTRYPOINT_SERVICE_NAME
|
11
|
+
from stoobly_agent.app.cli.scaffold.workflow import Workflow
|
12
|
+
from stoobly_agent.app.cli.scaffold.workflow_run_command import WorkflowRunCommand
|
13
|
+
from stoobly_agent.app.cli.types.workflow_run_command import BuildOptions, DownOptions, UpOptions, WorkflowDownOptions, WorkflowUpOptions, WorkflowLogsOptions
|
14
|
+
from stoobly_agent.lib.logger import Logger
|
15
|
+
|
16
|
+
LOG_ID = 'DockerWorkflowRunCommand'
|
17
|
+
|
18
|
+
class DockerWorkflowRunCommand(WorkflowRunCommand):
|
19
|
+
"""Docker-specific workflow run command that handles Docker Compose operations."""
|
20
|
+
|
21
|
+
def __init__(self, app, services=None, script=None, **kwargs):
|
22
|
+
if not kwargs.get('service_name'):
|
23
|
+
kwargs['service_name'] = CORE_ENTRYPOINT_SERVICE_NAME
|
24
|
+
|
25
|
+
super().__init__(app, **kwargs)
|
26
|
+
|
27
|
+
self.services = services or []
|
28
|
+
self.script = script
|
29
|
+
|
30
|
+
@property
|
31
|
+
def timestamp_file_path(self):
|
32
|
+
"""Get the path to the timestamp file for this workflow."""
|
33
|
+
return os.path.join(self.workflow_namespace.path, f"{self.workflow_name}.timestamp")
|
34
|
+
|
35
|
+
def exec_setup(self, containerized=False, user_id=None, verbose=False):
|
36
|
+
"""Setup Docker environment including gateway, images, and networks."""
|
37
|
+
init_commands = []
|
38
|
+
|
39
|
+
# Create base image if needed
|
40
|
+
if not containerized:
|
41
|
+
create_image_command = self.create_image(user_id=user_id, verbose=verbose)
|
42
|
+
init_commands.append(create_image_command)
|
43
|
+
|
44
|
+
# Create networks
|
45
|
+
init_commands.append(self.create_egress_network())
|
46
|
+
init_commands.append(self.create_ingress_network())
|
47
|
+
|
48
|
+
for command in init_commands:
|
49
|
+
self.exec(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
50
|
+
|
51
|
+
def up(self, **options: WorkflowUpOptions):
|
52
|
+
"""Execute the complete Docker workflow up process."""
|
53
|
+
# Define timestamp file path
|
54
|
+
timestamp_file = self.timestamp_file_path
|
55
|
+
|
56
|
+
# Create timestamp file to indicate workflow is starting
|
57
|
+
try:
|
58
|
+
with open(timestamp_file, 'w') as f:
|
59
|
+
import time
|
60
|
+
f.write(str(time.time()))
|
61
|
+
Logger.instance(LOG_ID).info(f"Created timestamp file: {timestamp_file}")
|
62
|
+
except Exception as e:
|
63
|
+
Logger.instance(LOG_ID).error(f"Failed to create timestamp file: {e}")
|
64
|
+
sys.exit(1)
|
65
|
+
|
66
|
+
no_publish = options.get('no_publish', False)
|
67
|
+
print_service_header = options.get('print_service_header')
|
68
|
+
|
69
|
+
try:
|
70
|
+
# Create individual service commands
|
71
|
+
commands: List[DockerWorkflowRunCommand] = []
|
72
|
+
for service in self.services:
|
73
|
+
config = { **options }
|
74
|
+
config['service_name'] = service
|
75
|
+
command = DockerWorkflowRunCommand(self.app, **config)
|
76
|
+
commands.append(command)
|
77
|
+
|
78
|
+
if not commands:
|
79
|
+
return
|
80
|
+
|
81
|
+
# Configure gateway ports dynamically based on workflow run
|
82
|
+
workflow = Workflow(self.workflow_name, self.app)
|
83
|
+
configure_gateway(self.workflow_namespace, workflow.service_paths_from_services(self.services), no_publish)
|
84
|
+
|
85
|
+
# Write nameservers if not running in container
|
86
|
+
if not options.get('containerized'):
|
87
|
+
self.write_nameservers()
|
88
|
+
|
89
|
+
# Setup Docker environment
|
90
|
+
self.exec_setup(
|
91
|
+
containerized=options.get('containerized', False),
|
92
|
+
user_id=options.get('user_id'),
|
93
|
+
verbose=options.get('verbose', False)
|
94
|
+
)
|
95
|
+
|
96
|
+
# Sort commands by priority and execute
|
97
|
+
commands = sorted(commands, key=lambda command: command.service_config.priority)
|
98
|
+
for index, command in enumerate(commands):
|
99
|
+
if print_service_header:
|
100
|
+
print_service_header(command.service_name)
|
101
|
+
|
102
|
+
attached = False
|
103
|
+
|
104
|
+
# By default, the entrypoint service should be last
|
105
|
+
# However, this can change if the user has configured a service's priority to be higher
|
106
|
+
if index == len(commands) - 1:
|
107
|
+
attached = not options.get('detached', False)
|
108
|
+
|
109
|
+
exec_command = command.service_up(
|
110
|
+
attached=attached,
|
111
|
+
namespace=options.get('namespace'),
|
112
|
+
pull=options.get('pull', False),
|
113
|
+
user_id=options.get('user_id')
|
114
|
+
)
|
115
|
+
|
116
|
+
if exec_command and self.script:
|
117
|
+
self.exec(exec_command)
|
118
|
+
|
119
|
+
except Exception as e:
|
120
|
+
# Clean up timestamp file on error
|
121
|
+
if os.path.exists(timestamp_file):
|
122
|
+
try:
|
123
|
+
os.remove(timestamp_file)
|
124
|
+
Logger.instance(LOG_ID).info(f"Removed timestamp file due to error: {timestamp_file}")
|
125
|
+
except Exception as cleanup_error:
|
126
|
+
Logger.instance(LOG_ID).warning(f"Failed to remove timestamp file after error: {cleanup_error}")
|
127
|
+
raise e
|
128
|
+
|
129
|
+
def down(self, **options: WorkflowDownOptions):
|
130
|
+
"""Execute the complete Docker workflow down process."""
|
131
|
+
# Check if workflow is running (timestamp file exists)
|
132
|
+
timestamp_file = self.timestamp_file_path
|
133
|
+
if not os.path.exists(timestamp_file):
|
134
|
+
Logger.instance(LOG_ID).info(f"Workflow '{self.workflow_name}' is not running. No timestamp file found: {timestamp_file}")
|
135
|
+
return
|
136
|
+
|
137
|
+
print_service_header = options.get('print_service_header')
|
138
|
+
|
139
|
+
# Create individual service commands
|
140
|
+
commands: List[DockerWorkflowRunCommand] = []
|
141
|
+
for service in self.services:
|
142
|
+
config = { **options }
|
143
|
+
config['service_name'] = service
|
144
|
+
command = DockerWorkflowRunCommand(self.app, **config)
|
145
|
+
commands.append(command)
|
146
|
+
|
147
|
+
if not commands:
|
148
|
+
return
|
149
|
+
|
150
|
+
# Sort commands by priority and execute
|
151
|
+
commands = sorted(commands, key=lambda command: command.service_config.priority)
|
152
|
+
for index, command in enumerate(commands):
|
153
|
+
if print_service_header:
|
154
|
+
print_service_header(command.service_name)
|
155
|
+
|
156
|
+
extra_compose_path = None
|
157
|
+
|
158
|
+
# By default, the entrypoint service should be last
|
159
|
+
# However, this can change if the user has configured a service's priority to be higher
|
160
|
+
if index == len(commands) - 1:
|
161
|
+
extra_compose_path = options.get('extra_entrypoint_compose_path')
|
162
|
+
|
163
|
+
exec_command = command.service_down(
|
164
|
+
extra_compose_path=extra_compose_path,
|
165
|
+
namespace=options.get('namespace'),
|
166
|
+
rmi=options.get('rmi', False),
|
167
|
+
user_id=options.get('user_id')
|
168
|
+
)
|
169
|
+
|
170
|
+
if exec_command and self.script:
|
171
|
+
self.exec(exec_command)
|
172
|
+
|
173
|
+
# After services are stopped, their network needs to be removed
|
174
|
+
if commands:
|
175
|
+
command = commands[0]
|
176
|
+
|
177
|
+
if options.get('rmi'):
|
178
|
+
remove_image_command = command.remove_image(options.get('user_id'))
|
179
|
+
if remove_image_command:
|
180
|
+
self.exec(remove_image_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
181
|
+
|
182
|
+
remove_egress_network_command = command.remove_egress_network()
|
183
|
+
if remove_egress_network_command:
|
184
|
+
self.exec(remove_egress_network_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
185
|
+
|
186
|
+
remove_ingress_network_command = command.remove_ingress_network()
|
187
|
+
if remove_ingress_network_command:
|
188
|
+
self.exec(remove_ingress_network_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
189
|
+
|
190
|
+
# Clean up timestamp file
|
191
|
+
timestamp_file = os.path.join(self.workflow_namespace.path, f"{self.workflow_name}.timestamp")
|
192
|
+
if os.path.exists(timestamp_file):
|
193
|
+
try:
|
194
|
+
os.remove(timestamp_file)
|
195
|
+
except Exception as e:
|
196
|
+
Logger.instance(LOG_ID).warning(f"Failed to remove timestamp file: {e}")
|
197
|
+
|
198
|
+
def logs(self, **options: WorkflowLogsOptions):
|
199
|
+
"""Execute the complete Docker workflow logs process."""
|
200
|
+
# Check if workflow is running (timestamp file exists)
|
201
|
+
timestamp_file = self.timestamp_file_path
|
202
|
+
if not os.path.exists(timestamp_file):
|
203
|
+
Logger.instance(LOG_ID).info(f"Workflow '{self.workflow_name}' is not running. No timestamp file found: {timestamp_file}")
|
204
|
+
return
|
205
|
+
|
206
|
+
from ...templates.constants import CORE_SERVICES
|
207
|
+
|
208
|
+
print_service_header = options.get('print_service_header')
|
209
|
+
|
210
|
+
# Filter services based on options
|
211
|
+
filtered_services = []
|
212
|
+
for service in self.services:
|
213
|
+
if len(options.get('service', [])) == 0:
|
214
|
+
# If no filter is specified, ignore CORE_SERVICES
|
215
|
+
if service in CORE_SERVICES:
|
216
|
+
continue
|
217
|
+
else:
|
218
|
+
# If a filter is specified, ignore all other services
|
219
|
+
if service not in options.get('service', []):
|
220
|
+
continue
|
221
|
+
filtered_services.append(service)
|
222
|
+
|
223
|
+
# Create individual service commands and get their log commands
|
224
|
+
commands = []
|
225
|
+
for service in filtered_services:
|
226
|
+
config = dict(options)
|
227
|
+
config['service_name'] = service
|
228
|
+
command = DockerWorkflowRunCommand(self.app, **config)
|
229
|
+
commands.append((service, command))
|
230
|
+
|
231
|
+
# Sort commands by priority and execute
|
232
|
+
commands = sorted(commands, key=lambda x: x[1].service_config.priority)
|
233
|
+
for index, (service, command) in enumerate(commands):
|
234
|
+
if print_service_header:
|
235
|
+
print_service_header(service)
|
236
|
+
|
237
|
+
follow = options.get('follow', False) and index == len(commands) - 1
|
238
|
+
shell_commands = self._build_log_commands(
|
239
|
+
command,
|
240
|
+
containers=options.get('container', []),
|
241
|
+
follow=follow,
|
242
|
+
namespace=options.get('namespace')
|
243
|
+
)
|
244
|
+
|
245
|
+
for shell_command in shell_commands:
|
246
|
+
if self.script:
|
247
|
+
self.exec(shell_command)
|
248
|
+
|
249
|
+
def _build_log_commands(self, command, containers=None, follow=False, namespace=None):
|
250
|
+
"""Build Docker log commands for a service."""
|
251
|
+
from ...constants import WORKFLOW_CONTAINER_TEMPLATE
|
252
|
+
|
253
|
+
log_commands = []
|
254
|
+
available_containers = command.containers
|
255
|
+
allowed_containers = list(
|
256
|
+
map(
|
257
|
+
lambda container: WORKFLOW_CONTAINER_TEMPLATE.format(
|
258
|
+
container=container, service_name=command.service_name
|
259
|
+
), containers or []
|
260
|
+
)
|
261
|
+
)
|
262
|
+
|
263
|
+
for index, container in enumerate(available_containers):
|
264
|
+
if container not in allowed_containers:
|
265
|
+
continue
|
266
|
+
|
267
|
+
container_name = self._container_name(container, namespace or command.workflow_name)
|
268
|
+
log_commands.append(f"echo \"=== Logging {container_name}\"")
|
269
|
+
|
270
|
+
if follow and index == len(available_containers) - 1:
|
271
|
+
docker_command = ['docker', 'logs', '--follow', container_name]
|
272
|
+
else:
|
273
|
+
docker_command = ['docker', 'logs', container_name]
|
274
|
+
|
275
|
+
log_commands.append(' '.join(docker_command))
|
276
|
+
|
277
|
+
return log_commands
|
278
|
+
|
279
|
+
def _container_name(self, container, namespace):
|
280
|
+
"""Generate container name based on namespace and container."""
|
281
|
+
return f"{namespace}-{container}-1"
|
282
|
+
|
283
|
+
def create_image(self, **options: BuildOptions):
|
284
|
+
"""Build Docker image for the workflow."""
|
285
|
+
relative_namespace_path = os.path.relpath(self.scaffold_namespace_path, self.current_working_dir)
|
286
|
+
dockerfile_path = os.path.join(relative_namespace_path, DOCKERFILE_CONTEXT)
|
287
|
+
user_id = options['user_id'] or os.getuid()
|
288
|
+
|
289
|
+
command = ['docker', 'build']
|
290
|
+
command.append(f"-f {dockerfile_path}")
|
291
|
+
command.append(f"-t stoobly.{user_id}")
|
292
|
+
command.append(f"--build-arg USER_ID={user_id}")
|
293
|
+
|
294
|
+
if not os.environ.get('STOOBLY_IMAGE_USE_LOCAL'):
|
295
|
+
command.append('--pull')
|
296
|
+
|
297
|
+
if not options.get('verbose'):
|
298
|
+
command.append('--quiet')
|
299
|
+
|
300
|
+
# To avoid large context transfer times, should be a folder with relatively low number of files
|
301
|
+
command.append(relative_namespace_path)
|
302
|
+
|
303
|
+
return ' '.join(command)
|
304
|
+
|
305
|
+
def remove_image(self, user_id: str = None):
|
306
|
+
"""Remove Docker image for the workflow."""
|
307
|
+
user_id = user_id or os.getuid()
|
308
|
+
command = ['docker', 'rmi', f"stoobly.{user_id}", '&>', '/dev/null']
|
309
|
+
command.append('|| true')
|
310
|
+
return ' '.join(command)
|
311
|
+
|
312
|
+
def create_egress_network(self):
|
313
|
+
"""Create Docker egress network."""
|
314
|
+
return f"docker network create {APP_EGRESS_NETWORK_TEMPLATE.format(network=self.network)} &> /dev/null"
|
315
|
+
|
316
|
+
def create_ingress_network(self):
|
317
|
+
"""Create Docker ingress network."""
|
318
|
+
return f"docker network create {APP_INGRESS_NETWORK_TEMPLATE.format(network=self.network)} &> /dev/null"
|
319
|
+
|
320
|
+
def remove_egress_network(self):
|
321
|
+
"""Remove Docker egress network."""
|
322
|
+
return f"docker network rm {APP_EGRESS_NETWORK_TEMPLATE.format(network=self.network)} &> /dev/null || true"
|
323
|
+
|
324
|
+
def remove_ingress_network(self):
|
325
|
+
"""Remove Docker ingress network."""
|
326
|
+
return f"docker network rm {APP_INGRESS_NETWORK_TEMPLATE.format(network=self.network)} &> /dev/null || true"
|
327
|
+
|
328
|
+
def service_up(self, **options: UpOptions):
|
329
|
+
"""Start the workflow using Docker Compose."""
|
330
|
+
if not os.path.exists(self.compose_path):
|
331
|
+
return ''
|
332
|
+
|
333
|
+
command = ['COMPOSE_IGNORE_ORPHANS=true', 'docker', 'compose']
|
334
|
+
command_options = []
|
335
|
+
|
336
|
+
# Add docker compose file
|
337
|
+
command_options.append(f"-f {os.path.relpath(self.compose_path, self.current_working_dir)}")
|
338
|
+
|
339
|
+
# Add docker compose networks file
|
340
|
+
command_options.append(f"-f {os.path.relpath(self.networks_compose_path, os.getcwd())}")
|
341
|
+
|
342
|
+
# Add custom docker compose file
|
343
|
+
custom_services = self.custom_services
|
344
|
+
if custom_services:
|
345
|
+
uses_profile = False
|
346
|
+
for service_name in custom_services:
|
347
|
+
service = custom_services[service_name]
|
348
|
+
profiles = service.get('profiles')
|
349
|
+
if isinstance(profiles, list):
|
350
|
+
if self.workflow_name in profiles:
|
351
|
+
uses_profile = True
|
352
|
+
break
|
353
|
+
if not uses_profile:
|
354
|
+
# TODO: looking into why warning does not print in docker
|
355
|
+
Logger.instance(LOG_ID).error(f"Missing {self.workflow_name} profile in custom compose file")
|
356
|
+
|
357
|
+
compose_file_path = os.path.relpath(self.custom_compose_path, self.current_working_dir)
|
358
|
+
command_options.append(f"-f {compose_file_path}")
|
359
|
+
|
360
|
+
command_options.append(f"--profile {self.workflow_name}")
|
361
|
+
|
362
|
+
if not options.get('namespace'):
|
363
|
+
options['namespace'] = self.workflow_name
|
364
|
+
command_options.append(f"-p {options['namespace']}")
|
365
|
+
|
366
|
+
command += command_options
|
367
|
+
command.append('up')
|
368
|
+
|
369
|
+
if not options.get('attached'):
|
370
|
+
command.append('-d')
|
371
|
+
|
372
|
+
# Add all remaining arguments
|
373
|
+
command.append('"$@"')
|
374
|
+
|
375
|
+
self.write_env(**options)
|
376
|
+
|
377
|
+
return ' '.join(command)
|
378
|
+
|
379
|
+
def service_down(self, **options: DownOptions):
|
380
|
+
"""Stop the workflow using Docker Compose."""
|
381
|
+
if not os.path.exists(self.compose_path):
|
382
|
+
return ''
|
383
|
+
|
384
|
+
command = ['docker', 'compose']
|
385
|
+
|
386
|
+
# Add docker compose file
|
387
|
+
command.append(f"-f {os.path.relpath(self.compose_path, os.getcwd())}")
|
388
|
+
|
389
|
+
# Add docker compose networks file
|
390
|
+
command.append(f"-f {os.path.relpath(self.networks_compose_path, os.getcwd())}")
|
391
|
+
|
392
|
+
# Add custom docker compose file
|
393
|
+
if self.custom_services:
|
394
|
+
command.append(f"-f {os.path.relpath(self.custom_compose_path, self.current_working_dir)}")
|
395
|
+
|
396
|
+
command.append(f"--profile {self.workflow_name}")
|
397
|
+
|
398
|
+
if not options.get('namespace'):
|
399
|
+
options['namespace'] = self.workflow_name
|
400
|
+
command.append(f"-p {options['namespace']}")
|
401
|
+
|
402
|
+
command.append('down')
|
403
|
+
command.append('--volumes')
|
404
|
+
command.append('--rmi local')
|
405
|
+
|
406
|
+
# Add all remaining arguments
|
407
|
+
command.append('"$@"')
|
408
|
+
|
409
|
+
self.write_env(**options)
|
410
|
+
|
411
|
+
return ' '.join(command)
|
412
|
+
|
413
|
+
def exec(self, command: List[str], **options):
|
414
|
+
if self.script:
|
415
|
+
print(command, file=self.script)
|
416
|
+
|
417
|
+
if self.dry_run:
|
418
|
+
print(command)
|
419
|
+
else:
|
420
|
+
result = subprocess.run(command, shell=True, **options)
|
421
|
+
if result.returncode != 0:
|
422
|
+
Logger.instance(LOG_ID).error(command)
|
423
|
+
sys.exit(1)
|
File without changes
|
File without changes
|
@@ -0,0 +1,72 @@
|
|
1
|
+
import os
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
from stoobly_agent.config.data_dir import DATA_DIR_NAME
|
5
|
+
|
6
|
+
from ...app_config import AppConfig
|
7
|
+
from ...constants import (
|
8
|
+
APP_DIR, SERVICES_NAMESPACE,
|
9
|
+
SERVICE_HOSTNAME, SERVICE_HOSTNAME_ENV,
|
10
|
+
SERVICE_NAME, SERVICE_NAME_ENV,
|
11
|
+
SERVICE_ID,
|
12
|
+
SERVICE_PORT, SERVICE_PORT_ENV,
|
13
|
+
SERVICE_SCHEME, SERVICE_SCHEME_ENV,
|
14
|
+
SERVICE_UPSTREAM_HOSTNAME, SERVICE_UPSTREAM_HOSTNAME_ENV, SERVICE_UPSTREAM_PORT, SERVICE_UPSTREAM_PORT_ENV, SERVICE_UPSTREAM_SCHEME, SERVICE_UPSTREAM_SCHEME_ENV,
|
15
|
+
STOOBLY_HOME_DIR,
|
16
|
+
WORKFLOW_NAME, WORKFLOW_NAME_ENV, WORKFLOW_SCRIPTS, WORKFLOW_TEMPLATE
|
17
|
+
)
|
18
|
+
from ...service_config import ServiceConfig
|
19
|
+
|
20
|
+
class ServiceBuilder():
|
21
|
+
|
22
|
+
def __init__(self, config: ServiceConfig):
|
23
|
+
self.__config = config
|
24
|
+
self.__dir_path = config.dir
|
25
|
+
self.__upstream_port = None
|
26
|
+
self.__env = [SERVICE_NAME_ENV, WORKFLOW_NAME_ENV]
|
27
|
+
self.__service_name = os.path.basename(config.dir)
|
28
|
+
self.__working_dir = os.path.join(
|
29
|
+
STOOBLY_HOME_DIR, DATA_DIR_NAME, SERVICES_NAMESPACE, SERVICE_NAME, WORKFLOW_NAME
|
30
|
+
)
|
31
|
+
|
32
|
+
@property
|
33
|
+
def config(self):
|
34
|
+
return self.__config
|
35
|
+
|
36
|
+
@property
|
37
|
+
def dir_path(self):
|
38
|
+
return self.__dir_path
|
39
|
+
|
40
|
+
@property
|
41
|
+
def upstream_port(self) -> int:
|
42
|
+
return self.__upstream_port
|
43
|
+
|
44
|
+
@property
|
45
|
+
def service_name(self):
|
46
|
+
return self.__service_name
|
47
|
+
|
48
|
+
@property
|
49
|
+
def working_dir(self):
|
50
|
+
return self.__working_dir
|
51
|
+
|
52
|
+
def env_dict(self):
|
53
|
+
env = {}
|
54
|
+
for e in self.__env:
|
55
|
+
env[e] = '${' + e + '}'
|
56
|
+
return env
|
57
|
+
|
58
|
+
def with_upstream_port(self, v: int):
|
59
|
+
if not isinstance(v, int):
|
60
|
+
return self
|
61
|
+
self.__upstream_port = v
|
62
|
+
return self
|
63
|
+
|
64
|
+
def with_env(self, v: List[str]):
|
65
|
+
if not isinstance(v, list):
|
66
|
+
return self
|
67
|
+
self.__env += v
|
68
|
+
return self
|
69
|
+
|
70
|
+
def write(self):
|
71
|
+
"""Base write method - to be implemented by subclasses"""
|
72
|
+
pass
|
File without changes
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
from typing import Union
|
4
|
+
|
5
|
+
from ...constants import SERVICE_NAME_ENV, WORKFLOW_NAME_ENV
|
6
|
+
from ..service.builder import ServiceBuilder
|
7
|
+
|
8
|
+
class WorkflowBuilder():
|
9
|
+
|
10
|
+
def __init__(self, workflow_path: str, service_builder: ServiceBuilder):
|
11
|
+
self._env = [SERVICE_NAME_ENV, WORKFLOW_NAME_ENV]
|
12
|
+
self._service_builder = service_builder
|
13
|
+
self._workflow_name = os.path.basename(workflow_path)
|
14
|
+
|
15
|
+
@property
|
16
|
+
def config(self):
|
17
|
+
return self._service_builder.config
|
18
|
+
|
19
|
+
@property
|
20
|
+
def service_builder(self):
|
21
|
+
return self._service_builder
|
22
|
+
|
23
|
+
@property
|
24
|
+
def service_path(self):
|
25
|
+
return self._service_builder.dir_path
|
26
|
+
|
27
|
+
@property
|
28
|
+
def workflow_name(self):
|
29
|
+
return self._workflow_name
|
30
|
+
|
31
|
+
def env_dict(self):
|
32
|
+
env = {}
|
33
|
+
for e in self._env:
|
34
|
+
env[e] = '${' + e + '}'
|
35
|
+
return env
|