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.
Files changed (173) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/__main__.py +10 -0
  3. stoobly_agent/app/api/application_http_request_handler.py +5 -2
  4. stoobly_agent/app/cli/ca_cert_cli.py +9 -5
  5. stoobly_agent/app/cli/helpers/replay_facade.py +2 -2
  6. stoobly_agent/app/cli/intercept_cli.py +5 -5
  7. stoobly_agent/app/cli/request_cli.py +2 -2
  8. stoobly_agent/app/cli/scaffold/app.py +14 -5
  9. stoobly_agent/app/cli/scaffold/app_command.py +0 -4
  10. stoobly_agent/app/cli/scaffold/app_config.py +49 -2
  11. stoobly_agent/app/cli/scaffold/app_create_command.py +145 -76
  12. stoobly_agent/app/cli/scaffold/constants.py +9 -4
  13. stoobly_agent/app/cli/scaffold/docker/constants.py +3 -1
  14. stoobly_agent/app/cli/scaffold/docker/service/build_decorator.py +4 -4
  15. stoobly_agent/app/cli/scaffold/docker/service/builder.py +31 -54
  16. stoobly_agent/app/cli/scaffold/docker/service/configure_gateway.py +3 -0
  17. stoobly_agent/app/cli/scaffold/docker/template_files.py +112 -0
  18. stoobly_agent/app/cli/scaffold/docker/workflow/build_decorator.py +1 -1
  19. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +30 -47
  20. stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +3 -2
  21. stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +1 -1
  22. stoobly_agent/app/cli/scaffold/docker/workflow/dns_decorator.py +2 -3
  23. stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +1 -1
  24. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +1 -1
  25. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +1 -1
  26. stoobly_agent/app/cli/scaffold/docker/workflow/run_command.py +423 -0
  27. stoobly_agent/app/cli/scaffold/local/__init__.py +0 -0
  28. stoobly_agent/app/cli/scaffold/local/service/__init__.py +0 -0
  29. stoobly_agent/app/cli/scaffold/local/service/builder.py +72 -0
  30. stoobly_agent/app/cli/scaffold/local/workflow/__init__.py +0 -0
  31. stoobly_agent/app/cli/scaffold/local/workflow/builder.py +35 -0
  32. stoobly_agent/app/cli/scaffold/local/workflow/run_command.py +339 -0
  33. stoobly_agent/app/cli/scaffold/service_command.py +9 -1
  34. stoobly_agent/app/cli/scaffold/service_config.py +9 -25
  35. stoobly_agent/app/cli/scaffold/service_create_command.py +18 -6
  36. stoobly_agent/app/cli/scaffold/service_docker_compose.py +3 -3
  37. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +10 -7
  38. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +2 -2
  39. stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +4 -4
  40. stoobly_agent/app/cli/scaffold/templates/app/build/mock/configure +3 -0
  41. stoobly_agent/app/cli/scaffold/templates/app/build/record/configure +28 -0
  42. stoobly_agent/app/cli/scaffold/templates/app/build/test/configure +3 -0
  43. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +4 -4
  44. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/configure +3 -0
  45. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/run +3 -0
  46. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/configure +3 -0
  47. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/run +3 -0
  48. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/configure +3 -0
  49. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/run +3 -0
  50. stoobly_agent/app/cli/scaffold/templates/build/services/build/mock/.configure +5 -1
  51. stoobly_agent/app/cli/scaffold/templates/build/services/build/mock/.init +5 -1
  52. stoobly_agent/app/cli/scaffold/templates/build/services/build/mock/.run +14 -0
  53. stoobly_agent/app/cli/scaffold/templates/build/services/build/record/.configure +5 -1
  54. stoobly_agent/app/cli/scaffold/templates/build/services/build/record/.init +5 -1
  55. stoobly_agent/app/cli/scaffold/templates/build/services/build/record/.run +14 -0
  56. stoobly_agent/app/cli/scaffold/templates/build/services/build/test/.configure +5 -1
  57. stoobly_agent/app/cli/scaffold/templates/build/services/build/test/.init +5 -1
  58. stoobly_agent/app/cli/scaffold/templates/build/services/build/test/.run +14 -0
  59. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/mock/.configure +5 -1
  60. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/mock/.init +5 -1
  61. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/mock/.run +19 -0
  62. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/record/.configure +5 -1
  63. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/record/.init +5 -1
  64. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/record/.run +19 -0
  65. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.configure +5 -1
  66. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.init +5 -1
  67. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.run +19 -0
  68. stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scaffold/.up +0 -1
  69. stoobly_agent/app/cli/scaffold/templates/build/workflows/mock/.configure +5 -1
  70. stoobly_agent/app/cli/scaffold/templates/build/workflows/mock/.init +5 -1
  71. stoobly_agent/app/cli/scaffold/templates/build/workflows/mock/.run +14 -0
  72. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.configure +25 -1
  73. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.init +5 -1
  74. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.run +14 -0
  75. stoobly_agent/app/cli/scaffold/templates/build/workflows/test/.configure +5 -1
  76. stoobly_agent/app/cli/scaffold/templates/build/workflows/test/.init +5 -1
  77. stoobly_agent/app/cli/scaffold/templates/build/workflows/test/.run +14 -0
  78. stoobly_agent/app/cli/scaffold/templates/constants.py +35 -19
  79. stoobly_agent/app/cli/scaffold/templates/factory.py +34 -18
  80. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.run +21 -0
  81. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.run +21 -0
  82. stoobly_agent/app/cli/scaffold/templates/workflow/mock/configure +5 -0
  83. stoobly_agent/app/cli/scaffold/templates/workflow/mock/run +3 -0
  84. stoobly_agent/app/cli/scaffold/templates/workflow/record/configure +21 -0
  85. stoobly_agent/app/cli/scaffold/templates/workflow/record/run +3 -0
  86. stoobly_agent/app/cli/scaffold/templates/workflow/test/configure +5 -0
  87. stoobly_agent/app/cli/scaffold/templates/workflow/test/run +3 -0
  88. stoobly_agent/app/cli/scaffold/workflow_command.py +18 -4
  89. stoobly_agent/app/cli/scaffold/workflow_copy_command.py +5 -4
  90. stoobly_agent/app/cli/scaffold/workflow_create_command.py +31 -29
  91. stoobly_agent/app/cli/scaffold/workflow_run_command.py +18 -151
  92. stoobly_agent/app/cli/scaffold_cli.py +134 -182
  93. stoobly_agent/app/cli/scenario_cli.py +2 -2
  94. stoobly_agent/app/cli/types/test.py +2 -2
  95. stoobly_agent/app/cli/types/workflow_run_command.py +52 -3
  96. stoobly_agent/app/proxy/handle_mock_service.py +1 -1
  97. stoobly_agent/app/proxy/intercept_settings.py +6 -26
  98. stoobly_agent/app/proxy/mock/eval_fixtures_service.py +177 -27
  99. stoobly_agent/app/proxy/mock/types/__init__.py +22 -1
  100. stoobly_agent/app/proxy/record/upload_request_service.py +3 -6
  101. stoobly_agent/app/proxy/replay/body_parser_service.py +8 -5
  102. stoobly_agent/app/proxy/replay/multipart.py +15 -13
  103. stoobly_agent/app/proxy/replay/replay_request_service.py +2 -2
  104. stoobly_agent/app/proxy/run.py +3 -0
  105. stoobly_agent/app/proxy/test/context.py +0 -4
  106. stoobly_agent/app/proxy/test/context_abc.py +0 -5
  107. stoobly_agent/app/proxy/utils/publish_change_service.py +20 -23
  108. stoobly_agent/app/settings/__init__.py +10 -7
  109. stoobly_agent/cli.py +61 -16
  110. stoobly_agent/config/data_dir.py +1 -8
  111. stoobly_agent/public/12-es2015.618ecfd5f735b801b50f.js +1 -0
  112. stoobly_agent/public/12-es5.618ecfd5f735b801b50f.js +1 -0
  113. stoobly_agent/public/index.html +1 -1
  114. stoobly_agent/public/main-es2015.5a9aa16433404c3f423a.js +1 -0
  115. stoobly_agent/public/main-es5.5a9aa16433404c3f423a.js +1 -0
  116. stoobly_agent/public/runtime-es2015.77bcd31efed9e5d5d431.js +1 -0
  117. stoobly_agent/public/runtime-es5.77bcd31efed9e5d5d431.js +1 -0
  118. stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +17 -6
  119. stoobly_agent/test/app/cli/scaffold/docker/cli_invoker.py +177 -0
  120. stoobly_agent/test/app/cli/scaffold/{cli_test.py → docker/cli_test.py} +4 -11
  121. stoobly_agent/test/app/cli/scaffold/{e2e_test.py → docker/e2e_test.py} +42 -27
  122. stoobly_agent/test/app/cli/scaffold/local/__init__.py +0 -0
  123. stoobly_agent/test/app/cli/scaffold/{cli_invoker.py → local/cli_invoker.py} +38 -32
  124. stoobly_agent/test/app/cli/scaffold/local/e2e_test.py +342 -0
  125. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  126. stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +903 -2
  127. stoobly_agent/test/app/proxy/replay/body_parser_service_test.py +95 -3
  128. stoobly_agent/test/config/data_dir_test.py +2 -7
  129. stoobly_agent/test/test_helper.py +16 -5
  130. {stoobly_agent-1.10.0.dist-info → stoobly_agent-1.10.2.dist-info}/METADATA +4 -2
  131. {stoobly_agent-1.10.0.dist-info → stoobly_agent-1.10.2.dist-info}/RECORD +157 -129
  132. {stoobly_agent-1.10.0.dist-info → stoobly_agent-1.10.2.dist-info}/WHEEL +1 -1
  133. stoobly_agent/app/cli/helpers/shell.py +0 -26
  134. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/configure +0 -3
  135. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/configure +0 -3
  136. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/configure +0 -3
  137. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/bin/configure +0 -3
  138. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/bin/configure +0 -3
  139. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/bin/configure +0 -3
  140. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +0 -13
  141. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/configure +0 -47
  142. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +0 -13
  143. stoobly_agent/public/12-es2015.be58ed0ef449008b932e.js +0 -1
  144. stoobly_agent/public/12-es5.be58ed0ef449008b932e.js +0 -1
  145. stoobly_agent/public/main-es2015.089b46f303768fbe864f.js +0 -1
  146. stoobly_agent/public/main-es5.089b46f303768fbe864f.js +0 -1
  147. stoobly_agent/public/runtime-es2015.f8c814b38b27708e91c1.js +0 -1
  148. stoobly_agent/public/runtime-es5.f8c814b38b27708e91c1.js +0 -1
  149. /stoobly_agent/app/cli/scaffold/templates/app/build/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  150. /stoobly_agent/app/cli/scaffold/templates/app/build/mock/{bin/init → init} +0 -0
  151. /stoobly_agent/app/cli/scaffold/templates/app/build/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  152. /stoobly_agent/app/cli/scaffold/templates/app/build/record/{bin/init → init} +0 -0
  153. /stoobly_agent/app/cli/scaffold/templates/app/build/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  154. /stoobly_agent/app/cli/scaffold/templates/app/build/test/{bin/init → init} +0 -0
  155. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  156. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/{bin/init → init} +0 -0
  157. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  158. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/{bin/init → init} +0 -0
  159. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  160. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/{bin/init → init} +0 -0
  161. /stoobly_agent/app/cli/scaffold/templates/app/gateway/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  162. /stoobly_agent/app/cli/scaffold/templates/app/gateway/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  163. /stoobly_agent/app/cli/scaffold/templates/app/gateway/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  164. /stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/{.docker-compose.exec.yml → .docker-compose.yml} +0 -0
  165. /stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  166. /stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  167. /stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  168. /stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  169. /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{bin/init → init} +0 -0
  170. /stoobly_agent/app/cli/scaffold/templates/workflow/record/{bin/init → init} +0 -0
  171. /stoobly_agent/app/cli/scaffold/templates/workflow/test/{bin/init → init} +0 -0
  172. {stoobly_agent-1.10.0.dist-info → stoobly_agent-1.10.2.dist-info}/entry_points.txt +0 -0
  173. {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
@@ -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
@@ -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