stoobly-agent 1.10.1__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 (155) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/__main__.py +10 -0
  3. stoobly_agent/app/cli/ca_cert_cli.py +9 -5
  4. stoobly_agent/app/cli/helpers/replay_facade.py +2 -2
  5. stoobly_agent/app/cli/intercept_cli.py +5 -5
  6. stoobly_agent/app/cli/request_cli.py +2 -2
  7. stoobly_agent/app/cli/scaffold/app.py +14 -5
  8. stoobly_agent/app/cli/scaffold/app_command.py +0 -4
  9. stoobly_agent/app/cli/scaffold/app_config.py +49 -2
  10. stoobly_agent/app/cli/scaffold/app_create_command.py +145 -76
  11. stoobly_agent/app/cli/scaffold/constants.py +8 -1
  12. stoobly_agent/app/cli/scaffold/docker/constants.py +3 -1
  13. stoobly_agent/app/cli/scaffold/docker/service/build_decorator.py +2 -2
  14. stoobly_agent/app/cli/scaffold/docker/service/builder.py +15 -49
  15. stoobly_agent/app/cli/scaffold/docker/service/configure_gateway.py +3 -0
  16. stoobly_agent/app/cli/scaffold/docker/template_files.py +112 -0
  17. stoobly_agent/app/cli/scaffold/docker/workflow/build_decorator.py +1 -1
  18. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +31 -39
  19. stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +1 -1
  20. stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +1 -1
  21. stoobly_agent/app/cli/scaffold/docker/workflow/dns_decorator.py +2 -3
  22. stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +1 -1
  23. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +1 -1
  24. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +1 -1
  25. stoobly_agent/app/cli/scaffold/docker/workflow/run_command.py +423 -0
  26. stoobly_agent/app/cli/scaffold/local/__init__.py +0 -0
  27. stoobly_agent/app/cli/scaffold/local/service/__init__.py +0 -0
  28. stoobly_agent/app/cli/scaffold/local/service/builder.py +72 -0
  29. stoobly_agent/app/cli/scaffold/local/workflow/__init__.py +0 -0
  30. stoobly_agent/app/cli/scaffold/local/workflow/builder.py +35 -0
  31. stoobly_agent/app/cli/scaffold/local/workflow/run_command.py +339 -0
  32. stoobly_agent/app/cli/scaffold/service_command.py +9 -1
  33. stoobly_agent/app/cli/scaffold/service_config.py +8 -0
  34. stoobly_agent/app/cli/scaffold/service_create_command.py +18 -6
  35. stoobly_agent/app/cli/scaffold/service_docker_compose.py +1 -1
  36. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +2 -2
  37. stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +2 -2
  38. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +2 -2
  39. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/run +3 -0
  40. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/run +3 -0
  41. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/run +3 -0
  42. stoobly_agent/app/cli/scaffold/templates/build/services/build/mock/.configure +5 -1
  43. stoobly_agent/app/cli/scaffold/templates/build/services/build/mock/.init +5 -1
  44. stoobly_agent/app/cli/scaffold/templates/build/services/build/mock/.run +14 -0
  45. stoobly_agent/app/cli/scaffold/templates/build/services/build/record/.configure +5 -1
  46. stoobly_agent/app/cli/scaffold/templates/build/services/build/record/.init +5 -1
  47. stoobly_agent/app/cli/scaffold/templates/build/services/build/record/.run +14 -0
  48. stoobly_agent/app/cli/scaffold/templates/build/services/build/test/.configure +5 -1
  49. stoobly_agent/app/cli/scaffold/templates/build/services/build/test/.init +5 -1
  50. stoobly_agent/app/cli/scaffold/templates/build/services/build/test/.run +14 -0
  51. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/mock/.configure +5 -1
  52. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/mock/.init +5 -1
  53. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/mock/.run +19 -0
  54. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/record/.configure +5 -1
  55. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/record/.init +5 -1
  56. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/record/.run +19 -0
  57. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.configure +5 -1
  58. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.init +5 -1
  59. stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.run +19 -0
  60. stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scaffold/.up +0 -1
  61. stoobly_agent/app/cli/scaffold/templates/build/workflows/mock/.configure +5 -1
  62. stoobly_agent/app/cli/scaffold/templates/build/workflows/mock/.init +5 -1
  63. stoobly_agent/app/cli/scaffold/templates/build/workflows/mock/.run +14 -0
  64. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.configure +5 -1
  65. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.init +5 -1
  66. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.run +14 -0
  67. stoobly_agent/app/cli/scaffold/templates/build/workflows/test/.configure +5 -1
  68. stoobly_agent/app/cli/scaffold/templates/build/workflows/test/.init +5 -1
  69. stoobly_agent/app/cli/scaffold/templates/build/workflows/test/.run +14 -0
  70. stoobly_agent/app/cli/scaffold/templates/constants.py +35 -19
  71. stoobly_agent/app/cli/scaffold/templates/factory.py +34 -18
  72. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.run +21 -0
  73. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.run +21 -0
  74. stoobly_agent/app/cli/scaffold/templates/workflow/mock/run +3 -0
  75. stoobly_agent/app/cli/scaffold/templates/workflow/record/run +3 -0
  76. stoobly_agent/app/cli/scaffold/templates/workflow/test/run +3 -0
  77. stoobly_agent/app/cli/scaffold/workflow_command.py +18 -4
  78. stoobly_agent/app/cli/scaffold/workflow_copy_command.py +5 -4
  79. stoobly_agent/app/cli/scaffold/workflow_create_command.py +31 -29
  80. stoobly_agent/app/cli/scaffold/workflow_run_command.py +18 -151
  81. stoobly_agent/app/cli/scaffold_cli.py +115 -161
  82. stoobly_agent/app/cli/scenario_cli.py +2 -2
  83. stoobly_agent/app/cli/types/test.py +2 -2
  84. stoobly_agent/app/cli/types/workflow_run_command.py +52 -3
  85. stoobly_agent/app/proxy/handle_mock_service.py +1 -1
  86. stoobly_agent/app/proxy/intercept_settings.py +5 -25
  87. stoobly_agent/app/proxy/mock/eval_fixtures_service.py +177 -27
  88. stoobly_agent/app/proxy/mock/types/__init__.py +22 -1
  89. stoobly_agent/app/proxy/replay/body_parser_service.py +8 -5
  90. stoobly_agent/app/proxy/replay/multipart.py +15 -13
  91. stoobly_agent/app/proxy/replay/replay_request_service.py +2 -2
  92. stoobly_agent/app/proxy/run.py +3 -0
  93. stoobly_agent/app/proxy/test/context.py +0 -4
  94. stoobly_agent/app/proxy/test/context_abc.py +0 -5
  95. stoobly_agent/cli.py +61 -16
  96. stoobly_agent/config/data_dir.py +0 -8
  97. stoobly_agent/public/12-es2015.618ecfd5f735b801b50f.js +1 -0
  98. stoobly_agent/public/12-es5.618ecfd5f735b801b50f.js +1 -0
  99. stoobly_agent/public/index.html +1 -1
  100. stoobly_agent/public/runtime-es2015.77bcd31efed9e5d5d431.js +1 -0
  101. stoobly_agent/public/runtime-es5.77bcd31efed9e5d5d431.js +1 -0
  102. stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +17 -6
  103. stoobly_agent/test/app/cli/scaffold/docker/cli_invoker.py +177 -0
  104. stoobly_agent/test/app/cli/scaffold/{cli_test.py → docker/cli_test.py} +1 -8
  105. stoobly_agent/test/app/cli/scaffold/{e2e_test.py → docker/e2e_test.py} +31 -16
  106. stoobly_agent/test/app/cli/scaffold/local/__init__.py +0 -0
  107. stoobly_agent/test/app/cli/scaffold/{cli_invoker.py → local/cli_invoker.py} +38 -32
  108. stoobly_agent/test/app/cli/scaffold/local/e2e_test.py +342 -0
  109. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  110. stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +903 -2
  111. stoobly_agent/test/app/proxy/replay/body_parser_service_test.py +95 -3
  112. stoobly_agent/test/config/data_dir_test.py +2 -7
  113. stoobly_agent/test/test_helper.py +16 -5
  114. {stoobly_agent-1.10.1.dist-info → stoobly_agent-1.10.2.dist-info}/METADATA +4 -2
  115. {stoobly_agent-1.10.1.dist-info → stoobly_agent-1.10.2.dist-info}/RECORD +150 -122
  116. {stoobly_agent-1.10.1.dist-info → stoobly_agent-1.10.2.dist-info}/WHEEL +1 -1
  117. stoobly_agent/app/cli/helpers/shell.py +0 -26
  118. stoobly_agent/public/12-es2015.be58ed0ef449008b932e.js +0 -1
  119. stoobly_agent/public/12-es5.be58ed0ef449008b932e.js +0 -1
  120. stoobly_agent/public/runtime-es2015.f8c814b38b27708e91c1.js +0 -1
  121. stoobly_agent/public/runtime-es5.f8c814b38b27708e91c1.js +0 -1
  122. /stoobly_agent/app/cli/scaffold/templates/app/build/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  123. /stoobly_agent/app/cli/scaffold/templates/app/build/mock/{bin/configure → configure} +0 -0
  124. /stoobly_agent/app/cli/scaffold/templates/app/build/mock/{bin/init → init} +0 -0
  125. /stoobly_agent/app/cli/scaffold/templates/app/build/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  126. /stoobly_agent/app/cli/scaffold/templates/app/build/record/{bin/configure → configure} +0 -0
  127. /stoobly_agent/app/cli/scaffold/templates/app/build/record/{bin/init → init} +0 -0
  128. /stoobly_agent/app/cli/scaffold/templates/app/build/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  129. /stoobly_agent/app/cli/scaffold/templates/app/build/test/{bin/configure → configure} +0 -0
  130. /stoobly_agent/app/cli/scaffold/templates/app/build/test/{bin/init → init} +0 -0
  131. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  132. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/{bin/configure → configure} +0 -0
  133. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/{bin/init → init} +0 -0
  134. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  135. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/{bin/configure → configure} +0 -0
  136. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/{bin/init → init} +0 -0
  137. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  138. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/{bin/configure → configure} +0 -0
  139. /stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/{bin/init → init} +0 -0
  140. /stoobly_agent/app/cli/scaffold/templates/app/gateway/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  141. /stoobly_agent/app/cli/scaffold/templates/app/gateway/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  142. /stoobly_agent/app/cli/scaffold/templates/app/gateway/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  143. /stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/{.docker-compose.exec.yml → .docker-compose.yml} +0 -0
  144. /stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/{.docker-compose.mock.yml → .docker-compose.yml} +0 -0
  145. /stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/{.docker-compose.record.yml → .docker-compose.yml} +0 -0
  146. /stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  147. /stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/{.docker-compose.test.yml → .docker-compose.yml} +0 -0
  148. /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{bin/configure → configure} +0 -0
  149. /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{bin/init → init} +0 -0
  150. /stoobly_agent/app/cli/scaffold/templates/workflow/record/{bin/configure → configure} +0 -0
  151. /stoobly_agent/app/cli/scaffold/templates/workflow/record/{bin/init → init} +0 -0
  152. /stoobly_agent/app/cli/scaffold/templates/workflow/test/{bin/configure → configure} +0 -0
  153. /stoobly_agent/app/cli/scaffold/templates/workflow/test/{bin/init → init} +0 -0
  154. {stoobly_agent-1.10.1.dist-info → stoobly_agent-1.10.2.dist-info}/entry_points.txt +0 -0
  155. {stoobly_agent-1.10.1.dist-info → stoobly_agent-1.10.2.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,339 @@
1
+ import os
2
+ import pdb
3
+ import signal
4
+ import subprocess
5
+ import sys
6
+
7
+ from typing import Optional, List
8
+
9
+ from stoobly_agent.app.cli.scaffold.constants import PLUGIN_CYPRESS, PLUGIN_PLAYWRIGHT
10
+ from stoobly_agent.app.cli.scaffold.docker.constants import PLUGIN_CONTAINER_SERVICE_TEMPLATE
11
+ from stoobly_agent.app.cli.scaffold.templates.constants import CORE_BUILD_SERVICE_NAME, CORE_ENTRYPOINT_SERVICE_NAME, CUSTOM_CONFIGURE, CUSTOM_INIT, CUSTOM_RUN, MAINTAINED_CONFIGURE, MAINTAINED_INIT, MAINTAINED_RUN
12
+ from stoobly_agent.app.cli.scaffold.workflow_run_command import WorkflowRunCommand
13
+ from stoobly_agent.app.cli.types.workflow_run_command import WorkflowUpOptions, WorkflowDownOptions, WorkflowLogsOptions
14
+ from stoobly_agent.lib.logger import Logger
15
+
16
+ LOG_ID = 'LocalWorkflowRunCommand'
17
+
18
+ class LocalWorkflowRunCommand(WorkflowRunCommand):
19
+ """Local workflow run command that executes stoobly-agent run directly."""
20
+
21
+ def __init__(self, app, services=None, script=None, **kwargs):
22
+ super().__init__(app, **kwargs)
23
+
24
+ self.services = services or []
25
+ self.script = script
26
+
27
+ self._log_file_path = None
28
+ self._pid_file_path = None
29
+
30
+ @property
31
+ def log_file_path(self):
32
+ """Get the path to the PID file for this workflow."""
33
+ if not self._log_file_path:
34
+ self._log_file_path = os.path.join(self.workflow_namespace.path, f"{self.workflow_name}.log")
35
+ return self._log_file_path
36
+
37
+ @property
38
+ def pid_file_path(self):
39
+ """Get the path to the PID file for this workflow."""
40
+ if not self._pid_file_path:
41
+ self._pid_file_path = os.path.join(self.workflow_namespace.path, f"{self.workflow_name}.pid")
42
+ return self._pid_file_path
43
+
44
+ def _write_pid(self, pid: int):
45
+ """Write the process PID to the PID file."""
46
+ os.makedirs(os.path.dirname(self.pid_file_path), exist_ok=True)
47
+ with open(self.pid_file_path, 'w') as f:
48
+ f.write(str(pid))
49
+
50
+ def _read_pid(self) -> Optional[int]:
51
+ """Read the process PID from the PID file."""
52
+ if not os.path.exists(self.pid_file_path):
53
+ return None
54
+
55
+ try:
56
+ with open(self.pid_file_path, 'r') as f:
57
+ return int(f.read().strip())
58
+ except (ValueError, IOError):
59
+ return None
60
+
61
+ def _kill_process(self, pid: int, signal_type=signal.SIGTERM):
62
+ """Kill a process by PID."""
63
+ try:
64
+ os.kill(pid, signal_type)
65
+ return True
66
+ except (OSError, ProcessLookupError):
67
+ return False
68
+
69
+ def _is_process_running(self, pid: int) -> bool:
70
+ """Check if a process is still running."""
71
+ try:
72
+ os.kill(pid, 0) # Signal 0 doesn't kill the process, just checks if it exists
73
+ return True
74
+ except (OSError, ProcessLookupError):
75
+ return False
76
+
77
+ def exec_service_script(self, service_name: str, step_script_path: str, args: List[str], cwd = None):
78
+ workflow_path = cwd or os.path.join(self.app.scaffold_namespace_path, service_name, self.workflow_name)
79
+
80
+ # Change directory to workflow path
81
+ command = [step_script_path] + args
82
+ if self.script:
83
+ command = [f"cd \"{workflow_path}\";"] + command
84
+
85
+ # Write the command to self.script_path
86
+ if self.script:
87
+ print(' '.join(command), file=self.script)
88
+
89
+ if self.dry_run:
90
+ print(' '.join(command))
91
+ else:
92
+ result = subprocess.run(['sh', '-c', ' '.join(command)], cwd=workflow_path)
93
+ if result.returncode != 0:
94
+ sys.exit(1)
95
+
96
+ def service_up(self, **options: WorkflowUpOptions):
97
+ print_service_header = options.get('print_service_header')
98
+ service_name = self.service_name
99
+ workflow_template = self.workflow_template
100
+
101
+ if print_service_header:
102
+ print_service_header(service_name)
103
+
104
+ self.write_env(**options)
105
+
106
+ # If service is build or entrypoint, use path in templates/build/services/SERVICE_NAME/.init
107
+ if service_name in [CORE_BUILD_SERVICE_NAME, CORE_ENTRYPOINT_SERVICE_NAME]:
108
+ init_script_path = os.path.join(self.service_templates_root_dir, service_name, workflow_template, MAINTAINED_INIT)
109
+ configure_script_path = os.path.join(self.service_templates_root_dir, service_name, workflow_template, MAINTAINED_CONFIGURE)
110
+ run_script_path = os.path.join(self.workflow_path, MAINTAINED_RUN)
111
+
112
+ if not os.path.exists(run_script_path):
113
+ run_script_path = os.path.join(self.service_templates_root_dir, service_name, workflow_template, MAINTAINED_RUN)
114
+ else:
115
+ # Absolute path to workflow .init script
116
+ # e.g. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.init
117
+ init_script_path = os.path.join(self.workflow_templates_build_dir, MAINTAINED_INIT)
118
+
119
+ # Absolute path to workflow .configure script
120
+ # e.g. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.configure
121
+ configure_script_path = os.path.join(self.workflow_templates_build_dir, MAINTAINED_CONFIGURE)
122
+
123
+ run_script_path = os.path.join(self.workflow_templates_build_dir, MAINTAINED_RUN)
124
+
125
+ self.exec_service_script(service_name, init_script_path, [CUSTOM_INIT])
126
+ self.exec_service_script(service_name, configure_script_path, [CUSTOM_CONFIGURE])
127
+ self.exec_service_script(service_name, run_script_path, [CUSTOM_RUN], cwd=os.getcwd())
128
+
129
+ def up(self, **options: WorkflowUpOptions):
130
+ """Start the workflow using local stoobly-agent run."""
131
+ detached = options.get('detached', False)
132
+
133
+ commands = self.workflow_service_commands(**options)
134
+
135
+ # iterate through each service in the workflow
136
+ public_directory_paths = []
137
+ response_fixtures_paths = []
138
+ for command in commands:
139
+ url = command.service_config.url
140
+ if url:
141
+ if os.path.exists(command.public_dir_path):
142
+ public_directory_paths.append('--public-directory-path')
143
+ public_directory_paths.append(f"{command.public_dir_path}:{url}")
144
+
145
+ if os.path.exists(command.response_fixtures_path):
146
+ response_fixtures_paths.append('--response-fixtures-path')
147
+ response_fixtures_paths.append(f"{command.response_fixtures_path}:{url}")
148
+
149
+ # Check if PID file already exists
150
+ if os.path.exists(self.pid_file_path):
151
+ pid = self._read_pid()
152
+ if pid and self._is_process_running(pid):
153
+ Logger.instance(LOG_ID).error(f"Workflow {self.workflow_name} is already running with PID: {pid}")
154
+ Logger.instance(LOG_ID).error(f"Run `stoobly-agent scaffold workflow down {self.workflow_name}` to stop it first")
155
+ sys.exit(1)
156
+ else:
157
+ # PID file exists but process is not running, clean it up
158
+ os.remove(self.pid_file_path)
159
+
160
+ for command in commands:
161
+ command.service_up(**options)
162
+
163
+ # If second from last command, run up_command i.e. right before entrypoint
164
+ if command == commands[-2]:
165
+ self.__up_command(public_directory_paths, response_fixtures_paths, **options)
166
+
167
+ def down(self, **options: WorkflowDownOptions):
168
+ """Stop the workflow by killing the local process."""
169
+
170
+ pid = self._read_pid()
171
+ if not pid:
172
+ Logger.instance(LOG_ID).warning(f"No PID file found for {self.workflow_name}")
173
+ return
174
+
175
+ if not self._is_process_running(pid):
176
+ Logger.instance(LOG_ID).info(f"Process {pid} for {self.workflow_name} is not running")
177
+ # Clean up PID file
178
+ if os.path.exists(self.pid_file_path):
179
+ os.remove(self.pid_file_path)
180
+ return
181
+
182
+ # Kill the process
183
+ if self.script:
184
+ self.__dry_run_down(pid, self.script)
185
+
186
+ if self.dry_run:
187
+ self.__dry_run_down(pid, sys.stdout)
188
+ else:
189
+ try:
190
+ # Try graceful shutdown first with SIGTERM
191
+ Logger.instance(LOG_ID).info(f"Sending SIGTERM to process {pid} for {self.workflow_name}")
192
+ self._kill_process(pid, signal.SIGTERM)
193
+
194
+ # Wait a bit for graceful shutdown
195
+ import time
196
+ time.sleep(2)
197
+
198
+ # Check if process still exists
199
+ if self._is_process_running(pid):
200
+ Logger.instance(LOG_ID).info(f"Process {pid} still running, sending SIGKILL")
201
+ self._kill_process(pid, signal.SIGKILL)
202
+
203
+ # Wait a bit more for SIGKILL to take effect
204
+ time.sleep(1)
205
+
206
+ # Final check
207
+ if self._is_process_running(pid):
208
+ Logger.instance(LOG_ID).warning(f"Process {pid} may still be running after SIGKILL")
209
+ else:
210
+ Logger.instance(LOG_ID).info(f"Successfully stopped process {pid} for {self.workflow_name}")
211
+ else:
212
+ Logger.instance(LOG_ID).info(f"Successfully stopped process {pid} for {self.workflow_name}")
213
+
214
+ # Clean up PID file
215
+ if os.path.exists(self.pid_file_path):
216
+ os.remove(self.pid_file_path)
217
+
218
+ except Exception as e:
219
+ Logger.instance(LOG_ID).error(f"Failed to stop {self.workflow_name}: {e}")
220
+
221
+ def logs(self, **options: WorkflowLogsOptions):
222
+ """Show logs for the local workflow process."""
223
+ follow = options.get('follow', False)
224
+
225
+ pid = self._read_pid()
226
+ if not pid:
227
+ Logger.instance(LOG_ID).warning(f"No PID file found for {self.workflow_name}")
228
+ return
229
+
230
+ # Build log command
231
+ log_file = f"{self.log_file_path}"
232
+ if self.script:
233
+ self.__dry_run_logs(log_file, self.script, follow)
234
+
235
+ if self.dry_run:
236
+ self.__dry_run_logs(log_file, sys.stdout, follow)
237
+ else:
238
+ try:
239
+ if follow:
240
+ subprocess.run(['tail', '-f', log_file])
241
+ else:
242
+ subprocess.run(['cat', log_file])
243
+ except subprocess.CalledProcessError as e:
244
+ Logger.instance(LOG_ID).error(f"Failed to show logs for {self.workflow_name}: {e}")
245
+
246
+ def status(self):
247
+ """Check the status of the local workflow process."""
248
+ pid = self._read_pid()
249
+ if not pid:
250
+ return "not running"
251
+
252
+ if self._is_process_running(pid):
253
+ return f"running (PID: {pid})"
254
+ else:
255
+ return "not running (stale PID file)"
256
+
257
+ def workflow_service_commands(self, **options: WorkflowUpOptions):
258
+ commands = list(map(lambda service_name: LocalWorkflowRunCommand(self.app, service_name=service_name, **options), self.services))
259
+ commands.sort(key=lambda command: command.service_config.priority)
260
+ return commands
261
+
262
+ def __dry_run_down(self, pid: int, output_file: str):
263
+ print(f"# Stop {self.workflow_name} (PID: {pid})", file=output_file)
264
+ print(f"kill {pid} || true", file=output_file)
265
+ print("sleep 1", file=output_file)
266
+ print(f"kill -0 {pid} 2>/dev/null && kill {pid} || true", file=output_file)
267
+ print(f"rm -f {self.pid_file_path}", file=output_file)
268
+
269
+ def __dry_run_logs(self, log_file: str, output_file: str, follow: bool):
270
+ print(f"# Show logs for {self.workflow_name}", file=output_file)
271
+ if follow:
272
+ print(f"tail -f {log_file}", file=output_file)
273
+ else:
274
+ print(f"cat {log_file}", file=output_file)
275
+
276
+ def __up_command(self, public_directory_paths: List[str], response_fixtures_paths: List[str], **options: WorkflowUpOptions):
277
+ # Build the stoobly-agent run command
278
+ command = ['stoobly-agent', 'run']
279
+
280
+ # Add log level if provided
281
+ if options.get('log_level'):
282
+ command.extend(['--log-level', options['log_level']])
283
+
284
+ # Use the PID file path as the detached output file
285
+ command.extend(['--detached', self.log_file_path])
286
+
287
+ command.extend(['--proxy-port', f"{self.app_config.proxy_port}"])
288
+ command.extend(['--ui-port', f"{self.app_config.ui_port}"])
289
+
290
+ if public_directory_paths:
291
+ command.extend(public_directory_paths)
292
+
293
+ if response_fixtures_paths:
294
+ command.extend(response_fixtures_paths)
295
+
296
+ # Convert command to string
297
+ command_str = ' '.join(command)
298
+
299
+ # Run in background using the main run command's --detached option
300
+ if self.script:
301
+ # Write to script for detached execution
302
+ script_lines = [
303
+ f"# Start {self.workflow_name} in background using --detached",
304
+ f"{command_str} > {self.pid_file_path}",
305
+ f"echo \"Started {self.workflow_name} with PID: $(cat {self.pid_file_path})\""
306
+ ]
307
+ for line in script_lines:
308
+ print(line, file=self.script)
309
+
310
+ if self.dry_run:
311
+ print(command_str)
312
+ else:
313
+ # Execute directly
314
+ try:
315
+ # Run the command with --detached option
316
+ result = subprocess.run(
317
+ command,
318
+ capture_output=True,
319
+ text=True,
320
+ check=True
321
+ )
322
+
323
+ if result.returncode != 0:
324
+ Logger.instance(LOG_ID).error(f"Failed to start agent, run `stoobly-agent workflow logs {self.workflow_name}` to see the error")
325
+ sys.exit(1)
326
+
327
+ # The --detached option prints the PID to stdout
328
+ pid = int(result.stdout.strip())
329
+
330
+ # Write PID to file
331
+ self._write_pid(pid)
332
+
333
+ Logger.instance(LOG_ID).info(f"Started {self.workflow_name} with PID: {pid}")
334
+ except subprocess.CalledProcessError as e:
335
+ Logger.instance(LOG_ID).error(f"Failed to start {self.workflow_name}: {e}")
336
+ return None
337
+ except ValueError as e:
338
+ Logger.instance(LOG_ID).error(f"Failed to parse PID from output: {e}")
339
+ return None
@@ -12,10 +12,14 @@ class ServiceCommand(AppCommand):
12
12
  super().__init__(app)
13
13
  self.__service_name = kwargs.get('service_name')
14
14
 
15
- self.__config = ServiceConfig(self.service_path, **kwargs)
15
+ if kwargs.get('service_name'):
16
+ self.__config = ServiceConfig(self.service_path, **kwargs)
16
17
 
17
18
  @property
18
19
  def service_config(self):
20
+ if not self.service_name:
21
+ raise Exception("Service name is required")
22
+
19
23
  return self.__config
20
24
 
21
25
  @service_config.setter
@@ -52,6 +56,10 @@ class ServiceCommand(AppCommand):
52
56
  self.service_name,
53
57
  )
54
58
 
59
+ @property
60
+ def service_templates_root_dir(self):
61
+ return os.path.join(self.templates_root_dir, 'build', 'services')
62
+
55
63
  def config(self, _c: dict):
56
64
  _config = self.app_config.read()
57
65
  _config.update(self.service_config.read())
@@ -1,5 +1,6 @@
1
1
  # Wraps the .config.yml file in the service folder
2
2
  import hashlib
3
+ import os
3
4
  import pdb
4
5
  import re
5
6
 
@@ -68,6 +69,10 @@ class ServiceConfig(Config):
68
69
  if 'upstream_scheme' in kwargs:
69
70
  self.__upstream_scheme = kwargs.get('upstream_scheme')
70
71
 
72
+ @property
73
+ def app_config_dir_path(self):
74
+ return os.path.dirname(self.dir)
75
+
71
76
  @property
72
77
  def detached(self) -> bool:
73
78
  return not not self.__detached
@@ -204,6 +209,9 @@ class ServiceConfig(Config):
204
209
 
205
210
  @property
206
211
  def url(self):
212
+ if not self.hostname:
213
+ return ''
214
+
207
215
  _url = f"{self.scheme}://{self.hostname}"
208
216
 
209
217
  if not self.port:
@@ -3,10 +3,12 @@ import pdb
3
3
  import shutil
4
4
 
5
5
  from copy import deepcopy
6
+ from typing import Union
6
7
 
7
8
  from .app import App
8
- from .constants import WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
9
- from .docker.service.builder import ServiceBuilder
9
+ from .constants import RUN_ON_DOCKER, WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
10
+ from .local.service.builder import ServiceBuilder
11
+ from .docker.service.builder import DockerServiceBuilder
10
12
  from .docker.workflow.decorators_factory import get_workflow_decorators
11
13
  from .service_command import ServiceCommand
12
14
  from .workflow_create_command import WorkflowCreateCommand
@@ -32,8 +34,18 @@ class ServiceCreateCommand(ServiceCommand):
32
34
  def workflows(self):
33
35
  return self.__workflows
34
36
 
37
+ @property
38
+ def create_docker_files(self):
39
+ """Determine if Docker files should be created based on app config run-on setting."""
40
+ return RUN_ON_DOCKER in self.app_config.run_on
41
+
35
42
  def build(self):
36
- service_builder = ServiceBuilder(self.service_config)
43
+ # Choose builder based on app run_on configuration
44
+ if RUN_ON_DOCKER in self.app_config.run_on:
45
+ service_builder = DockerServiceBuilder(self.service_config)
46
+ else:
47
+ service_builder = ServiceBuilder(self.service_config)
48
+
37
49
  service_builder.with_env(list(self.env_vars))
38
50
  service_decorators = []
39
51
 
@@ -67,19 +79,19 @@ class ServiceCreateCommand(ServiceCommand):
67
79
  if os.path.exists(dest):
68
80
  shutil.rmtree(dest)
69
81
 
70
- def __build_with_mock_workflow(self, service_builder: ServiceBuilder, **kwargs):
82
+ def __build_with_mock_workflow(self, service_builder: Union[ServiceBuilder, DockerServiceBuilder], **kwargs):
71
83
  mock_workflow = WorkflowCreateCommand(self.app, **{ **kwargs, **{ 'workflow_name': WORKFLOW_MOCK_TYPE }})
72
84
 
73
85
  workflow_decorators = get_workflow_decorators(WORKFLOW_MOCK_TYPE, self.service_config)
74
86
  mock_workflow.build(service_builder=service_builder, workflow_decorators=workflow_decorators)
75
87
 
76
- def __build_with_record_workflow(self, service_builder: ServiceBuilder, **kwargs):
88
+ def __build_with_record_workflow(self, service_builder: Union[ServiceBuilder, DockerServiceBuilder], **kwargs):
77
89
  record_workflow = WorkflowCreateCommand(self.app, **{ **kwargs, **{ 'workflow_name': WORKFLOW_RECORD_TYPE }})
78
90
 
79
91
  workflow_decorators = get_workflow_decorators(WORKFLOW_RECORD_TYPE, self.service_config)
80
92
  record_workflow.build(service_builder=service_builder, workflow_decorators=workflow_decorators)
81
93
 
82
- def __build_with_test_workflow(self, service_builder: ServiceBuilder, **kwargs):
94
+ def __build_with_test_workflow(self, service_builder: Union[ServiceBuilder, DockerServiceBuilder], **kwargs):
83
95
  mock_workflow = WorkflowCreateCommand(self.app, **{ **kwargs, **{ 'workflow_name': WORKFLOW_TEST_TYPE }})
84
96
 
85
97
  workflow_decorators = get_workflow_decorators(WORKFLOW_TEST_TYPE, self.service_config)
@@ -12,4 +12,4 @@ class ServiceDockerCompose():
12
12
 
13
13
  data_dir_path = DataDir.instance(app_dir_path).path
14
14
  self.docker_compose_path = f"{data_dir_path}/{SERVICES_NAMESPACE}/{service_name}/{target_workflow_name}/docker-compose.yml"
15
- self.init_script_path = f"{data_dir_path}/{SERVICES_NAMESPACE}/{service_name}/{target_workflow_name}/bin/init"
15
+ self.init_script_path = f"{data_dir_path}/{SERVICES_NAMESPACE}/{service_name}/{target_workflow_name}/init"
@@ -33,10 +33,10 @@ workflow=record
33
33
  workflow_service_options=$(shell echo $$STOOBLY_WORKFLOW_SERVICE_OPTIONS)
34
34
 
35
35
  app_data_dir=$(app_dir)/.stoobly
36
- app_namespace_dir=$(app_data_dir)/docker
36
+ app_namespace_dir=$(app_data_dir)/services
37
37
  app_tmp_dir=$(app_data_dir)/tmp
38
38
  dockerfile_path=$(app_namespace_dir)/.Dockerfile.context
39
- exec_docker_compose_file_path=$(app_namespace_dir)/stoobly-ui/exec/.docker-compose.exec.yml
39
+ exec_docker_compose_file_path=$(app_namespace_dir)/stoobly-ui/exec/.docker-compose.yml
40
40
  workflow_namespace=$(if $(namespace),$(namespace),$(workflow))
41
41
  workflow_namespace_dir=$(app_tmp_dir)/$(workflow_namespace)
42
42
  workflow_script=.stoobly/tmp/$(workflow_namespace)/run.sh
@@ -2,7 +2,7 @@ services:
2
2
  build.configure_base:
3
3
  command:
4
4
  - ${SERVICE_SCRIPTS}/${SERVICE_NAME}/${WORKFLOW_TEMPLATE}/.configure
5
- - bin/configure
5
+ - configure
6
6
  extends:
7
7
  file: ../.docker-compose.base.yml
8
8
  service: context_base
@@ -10,7 +10,7 @@ services:
10
10
  build.init_base:
11
11
  command:
12
12
  - ${SERVICE_SCRIPTS}/${SERVICE_NAME}/${WORKFLOW_TEMPLATE}/.init
13
- - bin/init
13
+ - init
14
14
  extends:
15
15
  file: ../.docker-compose.base.yml
16
16
  service: context_base
@@ -2,7 +2,7 @@ services:
2
2
  entrypoint.configure_base:
3
3
  command:
4
4
  - ${SERVICE_SCRIPTS}/${SERVICE_NAME}/${WORKFLOW_TEMPLATE}/.configure
5
- - bin/configure
5
+ - configure
6
6
  extends:
7
7
  file: ../.docker-compose.base.yml
8
8
  service: context_base
@@ -10,7 +10,7 @@ services:
10
10
  entrypoint.init_base:
11
11
  command:
12
12
  - ${SERVICE_SCRIPTS}/${SERVICE_NAME}/${WORKFLOW_TEMPLATE}/.init
13
- - bin/init
13
+ - init
14
14
  extends:
15
15
  file: ../.docker-compose.base.yml
16
16
  service: context_base
@@ -0,0 +1,3 @@
1
+ #! /bin/bash
2
+
3
+ # Add custom run logic here
@@ -0,0 +1,3 @@
1
+ #! /bin/bash
2
+
3
+ # Add custom run logic here
@@ -0,0 +1,3 @@
1
+ #! /bin/bash
2
+
3
+ # Add custom run logic here
@@ -3,6 +3,10 @@
3
3
  # This file was automatically generated. DO NOT EDIT.
4
4
  # Any changes made to this file will be overwritten.
5
5
 
6
+ if [ -f .env ]; then
7
+ set -a; . ./.env; set +a;
8
+ fi
9
+
6
10
  echo "Configuring intercept..."
7
11
  stoobly-agent intercept configure --mode mock --policy all
8
12
 
@@ -12,5 +16,5 @@ stoobly-agent intercept enable
12
16
  entrypoint=$1
13
17
 
14
18
  if [ -e "$entrypoint" ]; then
15
- "$entrypoint"
19
+ ./"$entrypoint"
16
20
  fi
@@ -3,6 +3,10 @@
3
3
  # This file was automatically generated. DO NOT EDIT.
4
4
  # Any changes made to this file will be overwritten.
5
5
 
6
+ if [ -f .env ]; then
7
+ set -a; . ./.env; set +a;
8
+ fi
9
+
6
10
  stoobly-agent config reset
7
11
 
8
12
  stoobly-agent snapshot apply
@@ -10,5 +14,5 @@ stoobly-agent snapshot apply
10
14
  entrypoint=$1
11
15
 
12
16
  if [ -e "$entrypoint" ]; then
13
- "$entrypoint" /app
17
+ ./"$entrypoint" /app
14
18
  fi
@@ -0,0 +1,14 @@
1
+ #!/bin/bash
2
+
3
+ # This file was automatically generated. DO NOT EDIT.
4
+ # Any changes made to this file will be overwritten.
5
+
6
+ if [ -f .env ]; then
7
+ set -a; . ./.env; set +a;
8
+ fi
9
+
10
+ entrypoint=$1
11
+
12
+ if [ -e "$entrypoint" ]; then
13
+ ./"$entrypoint"
14
+ fi
@@ -3,11 +3,15 @@
3
3
  # This file was automatically generated. DO NOT EDIT.
4
4
  # Any changes made to this file will be overwritten.
5
5
 
6
+ if [ -f .env ]; then
7
+ set -a; . ./.env; set +a;
8
+ fi
9
+
6
10
  echo "Configuring intercept..."
7
11
  stoobly-agent intercept configure --mode record --policy all
8
12
 
9
13
  entrypoint=$1
10
14
 
11
15
  if [ -e "$entrypoint" ]; then
12
- "$entrypoint"
16
+ ./"$entrypoint"
13
17
  fi
@@ -3,6 +3,10 @@
3
3
  # This file was automatically generated. DO NOT EDIT.
4
4
  # Any changes made to this file will be overwritten.
5
5
 
6
+ if [ -f .env ]; then
7
+ set -a; . ./.env; set +a;
8
+ fi
9
+
6
10
  stoobly-agent config reset
7
11
 
8
12
  stoobly-agent snapshot apply
@@ -10,5 +14,5 @@ stoobly-agent snapshot apply
10
14
  entrypoint=$1
11
15
 
12
16
  if [ -e "$entrypoint" ]; then
13
- "$entrypoint" /app
17
+ ./"$entrypoint" /app
14
18
  fi
@@ -0,0 +1,14 @@
1
+ #!/bin/bash
2
+
3
+ # This file was automatically generated. DO NOT EDIT.
4
+ # Any changes made to this file will be overwritten.
5
+
6
+ if [ -f .env ]; then
7
+ set -a; . ./.env; set +a;
8
+ fi
9
+
10
+ entrypoint=$1
11
+
12
+ if [ -e "$entrypoint" ]; then
13
+ ./"$entrypoint"
14
+ fi
@@ -3,6 +3,10 @@
3
3
  # This file was automatically generated. DO NOT EDIT.
4
4
  # Any changes made to this file will be overwritten.
5
5
 
6
+ if [ -f .env ]; then
7
+ set -a; . ./.env; set +a;
8
+ fi
9
+
6
10
  echo "Configuring intercept..."
7
11
  stoobly-agent intercept configure --mode mock --policy all
8
12
 
@@ -12,5 +16,5 @@ stoobly-agent intercept enable
12
16
  entrypoint=$1
13
17
 
14
18
  if [ -e "$entrypoint" ]; then
15
- "$entrypoint"
19
+ ./"$entrypoint"
16
20
  fi
@@ -3,6 +3,10 @@
3
3
  # This file was automatically generated. DO NOT EDIT.
4
4
  # Any changes made to this file will be overwritten.
5
5
 
6
+ if [ -f .env ]; then
7
+ set -a; . ./.env; set +a;
8
+ fi
9
+
6
10
  stoobly-agent config reset
7
11
 
8
12
  stoobly-agent snapshot apply
@@ -10,5 +14,5 @@ stoobly-agent snapshot apply
10
14
  entrypoint=$1
11
15
 
12
16
  if [ -e "$entrypoint" ]; then
13
- "$entrypoint" /app
17
+ ./"$entrypoint" /app
14
18
  fi
@@ -0,0 +1,14 @@
1
+ #!/bin/bash
2
+
3
+ # This file was automatically generated. DO NOT EDIT.
4
+ # Any changes made to this file will be overwritten.
5
+
6
+ if [ -f .env ]; then
7
+ set -a; . ./.env; set +a;
8
+ fi
9
+
10
+ entrypoint=$1
11
+
12
+ if [ -e "$entrypoint" ]; then
13
+ ./"$entrypoint"
14
+ fi