stoobly-agent 1.10.3__py3-none-any.whl → 1.10.5__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 CHANGED
@@ -1,2 +1,2 @@
1
1
  COMMAND = 'stoobly-agent'
2
- VERSION = '1.10.3'
2
+ VERSION = '1.10.5'
@@ -12,7 +12,7 @@ from .app import App
12
12
  from .app_command import AppCommand
13
13
  from .constants import PLUGIN_CYPRESS, PLUGIN_PLAYWRIGHT, RUN_ON_DOCKER, RUN_ON_LOCAL
14
14
  from .docker.template_files import plugin_docker_cypress, plugin_docker_playwright, plugin_local_cypress, plugin_local_playwright, remove_app_docker_files, remove_service_docker_files
15
- from .templates.constants import CORE_GATEWAY_SERVICE_NAME, CORE_MOCK_UI_SERVICE_NAME, MAINTAINED_RUN
15
+ from .templates.constants import CORE_GATEWAY_SERVICE_NAME, CORE_MOCK_UI_SERVICE_NAME, CUSTOM_RUN, MAINTAINED_RUN
16
16
 
17
17
  class AppCreateOptions(TypedDict):
18
18
  docker_socket_path: str
@@ -85,6 +85,7 @@ class AppCreateCommand(AppCommand):
85
85
  ignore.append(f"{CORE_MOCK_UI_SERVICE_NAME}/.*")
86
86
 
87
87
  if RUN_ON_DOCKER in self.app_run_on:
88
+ ignore.append(f".*/{CUSTOM_RUN}")
88
89
  ignore.append(f".*/{MAINTAINED_RUN}")
89
90
 
90
91
  # Copy all app templates
@@ -2,12 +2,14 @@ import os
2
2
  import pdb
3
3
  import subprocess
4
4
  import sys
5
+ import time
5
6
 
6
7
  from typing import List
8
+ from types import FunctionType
7
9
 
8
10
  from stoobly_agent.app.cli.scaffold.docker.constants import APP_EGRESS_NETWORK_TEMPLATE, APP_INGRESS_NETWORK_TEMPLATE, DOCKERFILE_CONTEXT
9
11
  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
12
+ from stoobly_agent.app.cli.scaffold.templates.constants import CORE_ENTRYPOINT_SERVICE_NAME, CORE_SERVICES
11
13
  from stoobly_agent.app.cli.scaffold.workflow import Workflow
12
14
  from stoobly_agent.app.cli.scaffold.workflow_run_command import WorkflowRunCommand
13
15
  from stoobly_agent.app.cli.types.workflow_run_command import BuildOptions, DownOptions, UpOptions, WorkflowDownOptions, WorkflowUpOptions, WorkflowLogsOptions
@@ -27,10 +29,14 @@ class DockerWorkflowRunCommand(WorkflowRunCommand):
27
29
  self.services = services or []
28
30
  self.script = script
29
31
 
32
+ @property
33
+ def timestamp_file_extension(self):
34
+ return '.timestamp'
35
+
30
36
  @property
31
37
  def timestamp_file_path(self):
32
38
  """Get the path to the timestamp file for this workflow."""
33
- return os.path.join(self.workflow_namespace.path, f"{self.workflow_name}.timestamp")
39
+ return os.path.join(self.workflow_namespace.path, self.timestamp_file_name(self.workflow_name))
34
40
 
35
41
  def exec_setup(self, containerized=False, user_id=None, verbose=False):
36
42
  """Setup Docker environment including gateway, images, and networks."""
@@ -47,25 +53,22 @@ class DockerWorkflowRunCommand(WorkflowRunCommand):
47
53
 
48
54
  for command in init_commands:
49
55
  self.exec(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
50
-
56
+
57
+ def timestamp_file_name(self, workflow_name: str):
58
+ return f"{workflow_name}{self.timestamp_file_extension}"
59
+
51
60
  def up(self, **options: WorkflowUpOptions):
52
61
  """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
-
62
+
66
63
  no_publish = options.get('no_publish', False)
67
64
  print_service_header = options.get('print_service_header')
68
-
65
+ timestamp_file = None
66
+
67
+ if not self.dry_run:
68
+ self.__iterate_active_workflows(handle_active=self.__handle_up_active)
69
+
70
+ timestamp_file = self.__create_timestamp_file()
71
+
69
72
  try:
70
73
  # Create individual service commands
71
74
  commands: List[DockerWorkflowRunCommand] = []
@@ -117,22 +120,17 @@ class DockerWorkflowRunCommand(WorkflowRunCommand):
117
120
  self.exec(exec_command)
118
121
 
119
122
  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}")
123
+ if timestamp_file:
124
+ # Clean up timestamp file on error
125
+ self.__remove_timestamp_file(timestamp_file)
127
126
  raise e
128
127
 
129
128
  def down(self, **options: WorkflowDownOptions):
130
129
  """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
130
+
131
+ timestamp_file = None
132
+ if not self.dry_run:
133
+ timestamp_file = self.__find_and_verify_timestamp_file()
136
134
 
137
135
  print_service_header = options.get('print_service_header')
138
136
 
@@ -186,25 +184,16 @@ class DockerWorkflowRunCommand(WorkflowRunCommand):
186
184
  remove_ingress_network_command = command.remove_ingress_network()
187
185
  if remove_ingress_network_command:
188
186
  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}")
187
+
188
+ if timestamp_file:
189
+ self.__remove_timestamp_file(timestamp_file)
197
190
 
198
191
  def logs(self, **options: WorkflowLogsOptions):
199
192
  """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
-
193
+
194
+ if not self.dry_run:
195
+ self.__find_and_verify_timestamp_file()
196
+
208
197
  print_service_header = options.get('print_service_header')
209
198
 
210
199
  # Filter services based on options
@@ -414,10 +403,92 @@ class DockerWorkflowRunCommand(WorkflowRunCommand):
414
403
  if self.script:
415
404
  print(command, file=self.script)
416
405
 
417
- if self.dry_run:
406
+ if self.dry_run or self.containerized:
418
407
  print(command)
419
408
  else:
420
409
  result = subprocess.run(command, shell=True, **options)
421
410
  if result.returncode != 0:
422
411
  Logger.instance(LOG_ID).error(command)
423
- sys.exit(1)
412
+ sys.exit(1)
413
+
414
+ def __find_and_verify_timestamp_file(self):
415
+ # Check if workflow is running (timestamp file exists)
416
+
417
+ timestamp_file = self.timestamp_file_path
418
+
419
+ if not os.path.exists(timestamp_file):
420
+ Logger.instance(LOG_ID).error(f"Workflow '{self.workflow_name}' is not running.")
421
+
422
+ if self.workflow_name != self.workflow_namespace.namespace:
423
+ Logger.instance(LOG_ID).error(f"Run `stoobly-agent scaffold workflow up {self.workflow_name} --namespace {self.workflow_namespace.namespace}` to start it first.")
424
+ else:
425
+ Logger.instance(LOG_ID).error(f"Run `stoobly-agent scaffold workflow up {self.workflow_name}` to start it first.")
426
+ sys.exit(2)
427
+
428
+ return timestamp_file
429
+
430
+ def __handle_up_active(self, folder: str, timestamp_file_path: str):
431
+ file_name = os.path.basename(timestamp_file_path)
432
+
433
+ # In the case of a namespace, the workflow name is the name of the file without the timestamp extension
434
+ workflow_name = self.workflow_name
435
+ if folder != self.workflow_name:
436
+ workflow_name = file_name.split(self.timestamp_file_extension)[0]
437
+
438
+ Logger.instance(LOG_ID).error(f"Workflow '{workflow_name}' is running, please stop it first.")
439
+
440
+ if folder != workflow_name:
441
+ Logger.instance(LOG_ID).error(f"Run `stoobly-agent scaffold workflow down {workflow_name} --namespace {folder}` to stop it first.")
442
+ else:
443
+ Logger.instance(LOG_ID).error(f"Run `stoobly-agent scaffold workflow down {workflow_name}` to stop it first.")
444
+
445
+ sys.exit(1)
446
+
447
+ def __iterate_active_workflows(self, **kwargs):
448
+ handle_active: FunctionType = kwargs.get('handle_active')
449
+ tmp_dir_path = self.app.data_dir.tmp_dir_path
450
+
451
+ # For each folder in self.app.data_dir.tmp_dir_path
452
+ for folder in os.listdir(tmp_dir_path):
453
+ folder_path = os.path.join(tmp_dir_path, folder)
454
+
455
+ # If the folder is not a directory, skip
456
+ if not os.path.isdir(folder_path):
457
+ continue
458
+
459
+ # For each file in folder_path that ends with .timestamp
460
+ for file in os.listdir(folder_path):
461
+ if not file.endswith(self.timestamp_file_extension):
462
+ continue
463
+
464
+ # If the folder contains a .timestamp file, then another workflow is running
465
+ timestamp_file_path = os.path.join(folder_path, file)
466
+
467
+ # Allow re-running the same workflow
468
+ if timestamp_file_path == self.timestamp_file_path:
469
+ continue
470
+
471
+ if handle_active:
472
+ handle_active(folder, timestamp_file_path)
473
+
474
+ def __create_timestamp_file(self):
475
+ # Create timestamp file to indicate workflow is starting
476
+ timestamp_file = self.timestamp_file_path
477
+
478
+ try:
479
+ with open(timestamp_file, 'w') as f:
480
+ f.write(str(time.time()))
481
+ Logger.instance(LOG_ID).debug(f"Created timestamp file: {timestamp_file}")
482
+ except Exception as e:
483
+ Logger.instance(LOG_ID).error(f"Failed to create timestamp file: {e}")
484
+ sys.exit(1)
485
+
486
+ return timestamp_file
487
+
488
+ def __remove_timestamp_file(self, timestamp_file: str):
489
+ # Clean up timestamp file
490
+ if os.path.exists(timestamp_file):
491
+ try:
492
+ os.remove(timestamp_file)
493
+ except Exception as e:
494
+ Logger.instance(LOG_ID).warning(f"Failed to remove timestamp file: {e}")
@@ -3,7 +3,9 @@ import pdb
3
3
  import signal
4
4
  import subprocess
5
5
  import sys
6
+ import time
6
7
 
8
+ from types import FunctionType
7
9
  from typing import Optional, List
8
10
 
9
11
  from stoobly_agent.app.cli.scaffold.constants import PLUGIN_CYPRESS, PLUGIN_PLAYWRIGHT
@@ -34,26 +36,28 @@ class LocalWorkflowRunCommand(WorkflowRunCommand):
34
36
  self._log_file_path = os.path.join(self.workflow_namespace.path, f"{self.workflow_name}.log")
35
37
  return self._log_file_path
36
38
 
39
+ @property
40
+ def pid_file_extension(self):
41
+ return '.pid'
42
+
37
43
  @property
38
44
  def pid_file_path(self):
39
45
  """Get the path to the PID file for this workflow."""
40
46
  if not self._pid_file_path:
41
- self._pid_file_path = os.path.join(self.workflow_namespace.path, f"{self.workflow_name}.pid")
47
+ self._pid_file_path = os.path.join(self.workflow_namespace.path, self.pid_file_name(self.workflow_name))
42
48
  return self._pid_file_path
43
49
 
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))
50
+ def pid_file_name(self, workflow_name: str):
51
+ return f"{workflow_name}{self.pid_file_extension}"
49
52
 
50
- def _read_pid(self) -> Optional[int]:
53
+ def _read_pid(self, file_path = None) -> Optional[int]:
51
54
  """Read the process PID from the PID file."""
52
- if not os.path.exists(self.pid_file_path):
55
+ file_path = file_path or self.pid_file_path
56
+ if not os.path.exists(file_path):
53
57
  return None
54
58
 
55
59
  try:
56
- with open(self.pid_file_path, 'r') as f:
60
+ with open(file_path, 'r') as f:
57
61
  return int(f.read().strip())
58
62
  except (ValueError, IOError):
59
63
  return None
@@ -130,9 +134,12 @@ class LocalWorkflowRunCommand(WorkflowRunCommand):
130
134
  """Start the workflow using local stoobly-agent run."""
131
135
  detached = options.get('detached', False)
132
136
 
133
- commands = self.workflow_service_commands(**options)
137
+ if not self.dry_run:
138
+ self.__iterate_active_workflows(handle_active=self.__handle_up_active, handle_stale=self.__handle_up_stale)
134
139
 
135
140
  # iterate through each service in the workflow
141
+ commands = self.workflow_service_commands(**options)
142
+
136
143
  public_directory_paths = []
137
144
  response_fixtures_paths = []
138
145
  for command in commands:
@@ -145,18 +152,7 @@ class LocalWorkflowRunCommand(WorkflowRunCommand):
145
152
  if os.path.exists(command.response_fixtures_path):
146
153
  response_fixtures_paths.append('--response-fixtures-path')
147
154
  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
-
155
+
160
156
  for command in commands:
161
157
  command.service_up(**options)
162
158
 
@@ -166,17 +162,10 @@ class LocalWorkflowRunCommand(WorkflowRunCommand):
166
162
 
167
163
  def down(self, **options: WorkflowDownOptions):
168
164
  """Stop the workflow by killing the local process."""
169
-
170
- pid = self._read_pid()
165
+
166
+ # Intentially run this during dry run, we need the PID to be returned
167
+ pid = self.__find_and_verify_workflow_pid()
171
168
  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
169
  return
181
170
 
182
171
  # Kill the process
@@ -212,8 +201,7 @@ class LocalWorkflowRunCommand(WorkflowRunCommand):
212
201
  Logger.instance(LOG_ID).info(f"Successfully stopped process {pid} for {self.workflow_name}")
213
202
 
214
203
  # Clean up PID file
215
- if os.path.exists(self.pid_file_path):
216
- os.remove(self.pid_file_path)
204
+ self.__remove_pid_file()
217
205
 
218
206
  except Exception as e:
219
207
  Logger.instance(LOG_ID).error(f"Failed to stop {self.workflow_name}: {e}")
@@ -221,11 +209,9 @@ class LocalWorkflowRunCommand(WorkflowRunCommand):
221
209
  def logs(self, **options: WorkflowLogsOptions):
222
210
  """Show logs for the local workflow process."""
223
211
  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
212
+
213
+ if not self.dry_run:
214
+ self.__find_and_verify_workflow_pid()
229
215
 
230
216
  # Build log command
231
217
  log_file = f"{self.log_file_path}"
@@ -243,22 +229,17 @@ class LocalWorkflowRunCommand(WorkflowRunCommand):
243
229
  except subprocess.CalledProcessError as e:
244
230
  Logger.instance(LOG_ID).error(f"Failed to show logs for {self.workflow_name}: {e}")
245
231
 
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
232
  def workflow_service_commands(self, **options: WorkflowUpOptions):
258
233
  commands = list(map(lambda service_name: LocalWorkflowRunCommand(self.app, service_name=service_name, **options), self.services))
259
234
  commands.sort(key=lambda command: command.service_config.priority)
260
235
  return commands
261
236
 
237
+ def __create_pid_file(self, pid: int):
238
+ """Write the process PID to the PID file."""
239
+ os.makedirs(os.path.dirname(self.pid_file_path), exist_ok=True)
240
+ with open(self.pid_file_path, 'w') as f:
241
+ f.write(str(pid))
242
+
262
243
  def __dry_run_down(self, pid: int, output_file: str):
263
244
  print(f"# Stop {self.workflow_name} (PID: {pid})", file=output_file)
264
245
  print(f"kill {pid} || true", file=output_file)
@@ -273,6 +254,83 @@ class LocalWorkflowRunCommand(WorkflowRunCommand):
273
254
  else:
274
255
  print(f"cat {log_file}", file=output_file)
275
256
 
257
+ def __find_and_verify_workflow_pid(self):
258
+ # Find and verify the workflow PID
259
+ pid = self._read_pid()
260
+
261
+ if not pid:
262
+ Logger.instance(LOG_ID).error(f"Workflow {self.workflow_name} is not running.")
263
+
264
+ # If the workflow name does not match the workflow namespace, then recommend with --namespace option
265
+ if self.workflow_name != self.workflow_namespace.namespace:
266
+ Logger.instance(LOG_ID).error(f"Run `stoobly-agent scaffold workflow up {self.workflow_name} --namespace {self.workflow_namespace.namespace}` to start it first.")
267
+ else:
268
+ Logger.instance(LOG_ID).error(f"Run `stoobly-agent scaffold workflow up {self.workflow_name}` to start it first.")
269
+
270
+ sys.exit(1)
271
+
272
+ if not self._is_process_running(pid):
273
+ Logger.instance(LOG_ID).info(f"Process {pid} for {self.workflow_name} is not running")
274
+ # Clean up PID file
275
+ return self.__remove_pid_file()
276
+
277
+ return pid
278
+
279
+ def __handle_up_active(self, folder: str, pid: str, pid_file_path: str):
280
+ # Allow re-running the same workflow, bring workflow down first
281
+ if pid_file_path == self.pid_file_path and os.path.exists(pid_file_path):
282
+ self.down()
283
+ else:
284
+ file_name = os.path.basename(pid_file_path)
285
+ workflow_name = self.workflow_name
286
+ if folder != self.workflow_name:
287
+ workflow_name = file_name.split(self.pid_file_extension)[0]
288
+
289
+ Logger.instance(LOG_ID).error(f"Workflow {workflow_name} is already running with PID {pid}")
290
+
291
+ if folder != workflow_name:
292
+ Logger.instance(LOG_ID).error(f"Run `stoobly-agent scaffold workflow down {workflow_name} --namespace {folder}` to stop it first.")
293
+ else:
294
+ Logger.instance(LOG_ID).error(f"Run `stoobly-agent scaffold workflow down {workflow_name}` to stop it first.")
295
+
296
+ sys.exit(1)
297
+
298
+ def __handle_up_stale(self, folder: str, pid: str, pid_file_path: str):
299
+ # PID file exists but process is not running, clean it up
300
+ os.remove(pid_file_path)
301
+
302
+ def __iterate_active_workflows(self, **kwargs):
303
+ handle_active: FunctionType = kwargs.get('handle_active')
304
+ handle_stale: FunctionType = kwargs.get('handle_stale')
305
+ tmp_dir_path = self.app.data_dir.tmp_dir_path
306
+
307
+ # For each folder in self.app.data_dir.tmp_dir_path
308
+ for folder in os.listdir(tmp_dir_path):
309
+ folder_path = os.path.join(tmp_dir_path, folder)
310
+
311
+ # If the folder is not a directory, skip
312
+ if not os.path.isdir(folder_path):
313
+ continue
314
+
315
+ # For each file in folder_path that ends with .pid
316
+ for file in os.listdir(folder_path):
317
+ if not file.endswith(self.pid_file_extension):
318
+ continue
319
+
320
+ # If the folder contains a .pid file, then another workflow is running
321
+ pid_file_path = os.path.join(folder_path, file)
322
+ pid = self._read_pid(pid_file_path)
323
+ if pid and self._is_process_running(pid):
324
+ if handle_active:
325
+ handle_active(folder, pid, pid_file_path)
326
+ else:
327
+ if handle_stale:
328
+ handle_stale(folder, pid, pid_file_path)
329
+
330
+ def __remove_pid_file(self):
331
+ if os.path.exists(self.pid_file_path):
332
+ os.remove(self.pid_file_path)
333
+
276
334
  def __up_command(self, public_directory_paths: List[str], response_fixtures_paths: List[str], **options: WorkflowUpOptions):
277
335
  # Build the stoobly-agent run command
278
336
  command = ['stoobly-agent', 'run']
@@ -307,7 +365,7 @@ class LocalWorkflowRunCommand(WorkflowRunCommand):
307
365
  for line in script_lines:
308
366
  print(line, file=self.script)
309
367
 
310
- if self.dry_run:
368
+ if self.dry_run or self.containerized:
311
369
  print(command_str)
312
370
  else:
313
371
  # Execute directly
@@ -320,20 +378,31 @@ class LocalWorkflowRunCommand(WorkflowRunCommand):
320
378
  check=True
321
379
  )
322
380
 
381
+ time.sleep(1) # Wait for the process to start
382
+
323
383
  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)
384
+ self.__handle_up_error()
326
385
 
327
386
  # The --detached option prints the PID to stdout
328
387
  pid = int(result.stdout.strip())
388
+
389
+ if not self._is_process_running(pid):
390
+ self.__handle_up_error()
329
391
 
330
392
  # Write PID to file
331
- self._write_pid(pid)
393
+ self.__create_pid_file(pid)
332
394
 
333
395
  Logger.instance(LOG_ID).info(f"Started {self.workflow_name} with PID: {pid}")
334
396
  except subprocess.CalledProcessError as e:
335
- Logger.instance(LOG_ID).error(f"Failed to start {self.workflow_name}: {e}")
336
- return None
397
+ self.__handle_up_error()
337
398
  except ValueError as e:
338
399
  Logger.instance(LOG_ID).error(f"Failed to parse PID from output: {e}")
339
- return None
400
+ return None
401
+
402
+ def __handle_up_error(self):
403
+ log_file = f"{self.log_file_path}"
404
+ # Read log file it exists and print to stderr
405
+ if os.path.exists(log_file):
406
+ with open(log_file, 'r') as f:
407
+ print(f.read(), file=sys.stderr)
408
+ sys.exit(1)
@@ -201,7 +201,12 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
201
201
  # Test workflow won't expose services that are detached and have a hostname to the host such as assets.
202
202
  # Need to test connection from inside the Docker network
203
203
  if self.service_config.hostname and self.workflow_name == WORKFLOW_TEST_TYPE:
204
- self.validate_internal_hostname(url)
204
+ try:
205
+ self.validate_internal_hostname(url)
206
+ except ScaffoldValidateException:
207
+ time.sleep(1)
208
+ # Retry once
209
+ self.validate_internal_hostname(url)
205
210
 
206
211
  self.validate_init_containers(self.service_docker_compose.init_container_name, self.service_docker_compose.configure_container_name)
207
212
 
@@ -2,9 +2,11 @@
2
2
  #
3
3
  # STOOBLY_APP_DIR: path to the application source code directory, defaults to $(pwd)
4
4
  # STOOBLY_CA_CERTS_DIR: path to folder where ca certs are stored, defaults to $(pwd)/.stoobly/ca_certs
5
+ # STOOBLY_CA_CERTS_INSTALL_CONFIRM: confirm answer to CA certificate installation prompt
5
6
  # STOOBLY_CERTS_DIR: path to a folder to store certs, defaults to $(pwd)/.stoobly/certs
6
7
  # STOOBLY_CONTEXT_DIR: path to the folder containing the .stoobly folder, defaults to $(pwd)
7
8
  # STOOBLY_DOTENV_FILE: path to dotenv file, defaults to $(pwd)/.env
9
+ # STOOBLY_HOSTNAME_INSTALL_CONFIRM: confirm answer to hostname installation prompt
8
10
  # STOOBLY_WORKFLOW_SERVICE_OPTIONS: extra --service options to pass 'stoobly-agent scaffold workflow' commands
9
11
 
10
12
  # Overridable Options
@@ -68,16 +70,17 @@ stoobly_exec_env=$(exec_env) CONTEXT_DIR="$(context_dir)"
68
70
  stoobly_exec_run=$(stoobly_exec_build) && $(stoobly_exec_run_env) $(exec_up)
69
71
  stoobly_exec_run_env=$(exec_env) CONTEXT_DIR="$(app_dir)"
70
72
 
71
- # Workflow run
72
- workflow_run=bash "$(app_dir)/$(workflow_script)"
73
-
74
73
  action/install:
75
74
  $(eval action=install)
76
75
  action/uninstall:
77
76
  $(eval action=uninstall)
78
77
  ca-cert/install: stoobly/install
79
78
  @if [ -z "$$(ls $(ca_certs_dir) 2> /dev/null)" ]; then \
80
- read -p "Installing CA certificate is required for $(workflow)ing requests, continue? (y/N) " confirm && \
79
+ if [ -n "$$STOOBLY_CA_CERTS_INSTALL_CONFIRM" ]; then \
80
+ confirm="$$STOOBLY_CA_CERTS_INSTALL_CONFIRM"; \
81
+ else \
82
+ read -p "Installing CA certificate is required for $(workflow)ing requests, continue? (y/N) " confirm; \
83
+ fi && \
81
84
  if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
82
85
  echo "Running stoobly-agent ca-cert install..."; \
83
86
  stoobly-agent ca-cert install --ca-certs-dir-path $(ca_certs_dir); \
@@ -107,10 +110,10 @@ intercept/disable:
107
110
  intercept/enable:
108
111
  @export EXEC_COMMAND=intercept/.enable EXEC_OPTIONS="" EXEC_ARGS=$(scenario_key) && \
109
112
  $(stoobly_exec)
110
- mock: workflow/mock ca-cert/install workflow/hostname/install nameservers workflow/up
113
+ mock: workflow/mock ca-cert/install workflow/up nameservers workflow/hostname/install workflow/run
111
114
  mock/services: workflow/mock workflow/services
112
- mock/logs: workflow/mock workflow/logs
113
- mock/down: workflow/mock workflow/down workflow/hostname/uninstall
115
+ mock/logs: workflow/mock workflow/logs workflow/run
116
+ mock/down: workflow/mock workflow/down workflow/run workflow/hostname/uninstall
114
117
  pipx/install:
115
118
  @if ! command -v pipx >/dev/null 2>&1; then \
116
119
  echo "pipx is not installed. Installing pipx..."; \
@@ -121,10 +124,10 @@ python/validate:
121
124
  echo "Error: Python 3.10, 3.11, or 3.12 is required."; \
122
125
  exit 1; \
123
126
  fi
124
- record: workflow/record ca-cert/install workflow/hostname/install nameservers workflow/up
125
- record/down: workflow/record workflow/down workflow/hostname/uninstall
127
+ record: workflow/record ca-cert/install workflow/up nameservers workflow/hostname/install workflow/run
128
+ record/down: workflow/record workflow/down workflow/run workflow/hostname/uninstall
126
129
  record/services: workflow/record workflow/services
127
- record/logs: workflow/record workflow/logs
130
+ record/logs: workflow/record workflow/logs workflow/run
128
131
  scenario/create:
129
132
  # Create a scenario
130
133
  @export EXEC_COMMAND=scenario/.create EXEC_OPTIONS="$(options)" EXEC_ARGS="$(name)" && \
@@ -154,18 +157,21 @@ stoobly/install: python/validate pipx/install
154
157
  echo "stoobly-agent not found. Installing..."; \
155
158
  pipx install stoobly-agent || { echo "Failed to install stoobly-agent"; exit 1; }; \
156
159
  fi
157
- test: workflow/test workflow/up
160
+ test: workflow/test workflow/up workflow/run
158
161
  test/services: workflow/test workflow/services
159
- test/logs: workflow/test workflow/logs
160
- test/down: workflow/test workflow/down
162
+ test/logs: workflow/test workflow/logs workflow/run
163
+ test/down: workflow/test workflow/down workflow/run
161
164
  tmpdir:
162
165
  @mkdir -p $(app_tmp_dir)
163
166
  workflow/down: dotenv
164
167
  @export EXEC_COMMAND=scaffold/.down EXEC_OPTIONS="$(workflow_down_options) $(workflow_run_options) $(options)" EXEC_ARGS="$(workflow)" && \
165
- $(stoobly_exec_run) && \
166
- $(workflow_run)
168
+ $(stoobly_exec_run)
167
169
  workflow/hostname: stoobly/install
168
- @read -p "Do you want to $(action) hostname(s) in /etc/hosts? (y/N) " confirm && \
170
+ @if [ -n "$$STOOBLY_HOSTNAME_INSTALL_CONFIRM" ]; then \
171
+ confirm="$$STOOBLY_HOSTNAME_INSTALL_CONFIRM"; \
172
+ else \
173
+ read -p "Do you want to $(action) hostname(s) in /etc/hosts? (y/N) " confirm; \
174
+ fi && \
169
175
  if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
170
176
  CURRENT_VERSION=$$(stoobly-agent --version); \
171
177
  REQUIRED_VERSION="1.4.0"; \
@@ -180,14 +186,15 @@ workflow/hostname/install: action/install workflow/hostname
180
186
  workflow/hostname/uninstall: action/uninstall workflow/hostname
181
187
  workflow/logs:
182
188
  @export EXEC_COMMAND=scaffold/.logs EXEC_OPTIONS="$(workflow_log_options) $(workflow_run_options) $(options)" EXEC_ARGS="$(workflow)" && \
183
- $(stoobly_exec_run) && \
184
- $(workflow_run)
189
+ $(stoobly_exec_run)
185
190
  workflow/mock:
186
191
  $(eval workflow=mock)
187
192
  workflow/namespace: tmpdir
188
193
  @mkdir -p $(workflow_namespace_dir)
189
194
  workflow/record:
190
195
  $(eval workflow=record)
196
+ workflow/run:
197
+ @bash "$(app_dir)/$(workflow_script)"
191
198
  workflow/services:
192
199
  @export EXEC_COMMAND=scaffold/.services EXEC_OPTIONS="$(workflow_service_options) $(options)" EXEC_ARGS="$(workflow)" && \
193
200
  $(stoobly_exec_run)
@@ -195,5 +202,4 @@ workflow/test:
195
202
  $(eval workflow=test) $(eval workflow_up_extra_options=$(workflow_up_extra_options) --no-publish)
196
203
  workflow/up: dotenv
197
204
  @export EXEC_COMMAND=scaffold/.up EXEC_OPTIONS="$(workflow_up_options) $(workflow_run_options) $(options)" EXEC_ARGS="$(workflow)" && \
198
- $(stoobly_exec_run) && \
199
- $(workflow_run)
205
+ $(stoobly_exec_run)
@@ -7,6 +7,5 @@ mkdir -p .stoobly/tmp
7
7
  stoobly-agent scaffold workflow down \
8
8
  --app-dir-path "$(pwd)" \
9
9
  --containerized \
10
- --dry-run \
11
10
  --log-level error \
12
11
  $extra_options $1 > /dev/null
@@ -4,6 +4,6 @@ extra_options=$EXEC_OPTIONS
4
4
 
5
5
  stoobly-agent scaffold workflow logs \
6
6
  --app-dir-path "$(pwd)" \
7
- --dry-run \
7
+ --containerized \
8
8
  --log-level warning \
9
9
  $extra_options $1 > /dev/null
@@ -5,7 +5,6 @@ extra_options=$EXEC_OPTIONS
5
5
  stoobly-agent scaffold workflow up \
6
6
  --app-dir-path "$(pwd)" \
7
7
  --containerized \
8
- --dry-run \
9
8
  --log-level warning \
10
9
  --mkcert \
11
10
  $extra_options $1 > /dev/null
@@ -3,6 +3,8 @@
3
3
  # This file was automatically generated. DO NOT EDIT.
4
4
  # Any changes made to this file will be overwritten.
5
5
 
6
+ npx cypress run --project .
7
+
6
8
  if [ -f .env ]; then
7
9
  set -a; . ./.env; set +a;
8
10
  fi
@@ -12,8 +14,6 @@ export http_proxy=http://localhost:$APP_PROXY_PORT
12
14
  export HTTPS_PROXY=http://localhost:$APP_PROXY_PORT
13
15
  export https_proxy=http://localhost:$APP_PROXY_PORT
14
16
 
15
- npx cypress run --project .
16
-
17
17
  entrypoint=$1
18
18
 
19
19
  if [ -e "$entrypoint" ]; then
@@ -3,6 +3,8 @@
3
3
  # This file was automatically generated. DO NOT EDIT.
4
4
  # Any changes made to this file will be overwritten.
5
5
 
6
+ npx playwright test --reporter dot
7
+
6
8
  if [ -f .env ]; then
7
9
  set -a; . ./.env; set +a;
8
10
  fi
@@ -12,8 +14,6 @@ export http_proxy=http://localhost:$APP_PROXY_PORT
12
14
  export HTTPS_PROXY=http://localhost:$APP_PROXY_PORT
13
15
  export https_proxy=http://localhost:$APP_PROXY_PORT
14
16
 
15
- npx playwright test --reporter dot
16
-
17
17
  entrypoint=$1
18
18
 
19
19
  if [ -e "$entrypoint" ]; then
@@ -28,6 +28,7 @@ class WorkflowRunCommand(WorkflowCommand):
28
28
  self.__current_working_dir = os.getcwd()
29
29
  self.__ca_certs_dir_path = kwargs.get('ca_certs_dir_path') or app.ca_certs_dir_path
30
30
  self.__certs_dir_path = kwargs.get('certs_dir_path') or app.certs_dir_path
31
+ self.__containerized = kwargs.get('containerized') or False
31
32
  self.__context_dir_path = kwargs.get('context_dir_path') or app.context_dir_path
32
33
  self.__dry_run = kwargs.get('dry_run', False)
33
34
  self.__namespace = kwargs.get('namespace') or self.workflow_name
@@ -48,6 +49,10 @@ class WorkflowRunCommand(WorkflowCommand):
48
49
  def certs_dir_path(self):
49
50
  return self.__certs_dir_path
50
51
 
52
+ @property
53
+ def containerized(self):
54
+ return self.__containerized
55
+
51
56
  @property
52
57
  def context_dir_path(self):
53
58
  if not self.__context_dir_path:
@@ -341,6 +341,7 @@ def copy(**kwargs):
341
341
  @click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
342
342
  @click.option('--containerized', is_flag=True, help='Set if run from within a container.')
343
343
  @click.option('--dry-run', default=False, is_flag=True)
344
+ @click.option('--hostname-uninstall-confirm', default=None, type=click.Choice(['y', 'Y', 'n', 'N']), help='Confirm answer to hostname uninstall prompt.')
344
345
  @click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
345
346
  Log levels can be "debug", "info", "warning", or "error"
346
347
  ''')
@@ -385,7 +386,20 @@ def down(**kwargs):
385
386
  script=script,
386
387
  **kwargs
387
388
  )
388
-
389
+
390
+ # Because test workflow is completely containerized, we don't need to prompt to install hostnames in /etc/hosts
391
+ # Entrypoint container will be within the container network
392
+ if workflow_command.workflow_template != WORKFLOW_TEST_TYPE:
393
+ if not containerized and not kwargs['dry_run']:
394
+ # Prompt confirm to install hostnames
395
+ if kwargs.get('hostname_uninstall_confirm'):
396
+ confirm = kwargs['hostname_uninstall_confirm']
397
+ else:
398
+ confirm = input(f"Do you want to uninstall hostnames for {kwargs['workflow_name']}? (y/N) ")
399
+
400
+ if confirm == "y" or confirm == "Y":
401
+ __hostname_uninstall(app_dir_path=kwargs['app_dir_path'], service=kwargs['service'], workflow=[kwargs['workflow_name']])
402
+
389
403
  # Execute the workflow down
390
404
  workflow_command.down(
391
405
  **command_args,
@@ -401,6 +415,7 @@ def down(**kwargs):
401
415
  @click.option(
402
416
  '--container', multiple=True, help=f"Select which containers to log. Defaults to '{WORKFLOW_CONTAINER_PROXY}'"
403
417
  )
418
+ @click.option('--containerized', is_flag=True, help='Set if run from within a container.')
404
419
  @click.option('--dry-run', default=False, is_flag=True, help='If set, prints commands.')
405
420
  @click.option('--follow', is_flag=True, help='Follow last container log output.')
406
421
  @click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
@@ -457,11 +472,13 @@ def logs(**kwargs):
457
472
  @workflow.command()
458
473
  @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
459
474
  @click.option('--ca-certs-dir-path', default=data_dir.ca_certs_dir_path, help='Path to ca certs directory used to sign SSL certs.')
475
+ @click.option('--ca-certs-install-confirm', default=None, type=click.Choice(['y', 'Y', 'n', 'N']), help='Confirm answer to CA certificate installation prompt.')
460
476
  @click.option('--certs-dir-path', help='Path to certs directory. Defaults to the certs dir of the context.')
461
477
  @click.option('--containerized', is_flag=True, help='Set if run from within a container.')
462
478
  @click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
463
479
  @click.option('--detached', is_flag=True, help='If set, will run the highest priority service in the background.')
464
480
  @click.option('--dry-run', default=False, is_flag=True, help='If set, prints commands.')
481
+ @click.option('--hostname-install-confirm', default=None, type=click.Choice(['y', 'Y', 'n', 'N']), help='Confirm answer to hostname installation prompt.')
465
482
  @click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
466
483
  Log levels can be "debug", "info", "warning", or "error"
467
484
  ''')
@@ -472,7 +489,6 @@ def logs(**kwargs):
472
489
  @click.option('--service', multiple=True, help='Select which services to run. Defaults to all.')
473
490
  @click.option('--user-id', default=os.getuid(), help='OS user ID of the owner of context dir path.')
474
491
  @click.option('--verbose', is_flag=True)
475
- @click.option('-y', '--yes', is_flag=True, help='Auto-confirm CA certificate installation prompt.')
476
492
  @click.argument('workflow_name')
477
493
  def up(**kwargs):
478
494
  os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
@@ -491,17 +507,15 @@ def up(**kwargs):
491
507
  # First time if folder does not exist or is empty
492
508
  first_time = not os.path.exists(app.ca_certs_dir_path) or not os.listdir(app.ca_certs_dir_path)
493
509
  if first_time and not containerized and not dry_run:
494
- # If ca certs dir path does not exist, run ca-cert install
495
- if kwargs.get('yes'):
496
- # Auto-confirm if -y/--yes option is provided
497
- ca_cert_install(app.ca_certs_dir_path)
510
+ if kwargs.get('ca_certs_install_confirm'):
511
+ confirm = kwargs['ca_certs_install_confirm']
498
512
  else:
499
513
  confirm = input(f"Installing CA certificate is required for {kwargs['workflow_name']}ing requests, continue? (y/N) ")
500
- if confirm == "y" or confirm == "Y":
501
- ca_cert_install(app.ca_certs_dir_path)
502
- else:
503
- print("You can install the CA certificate later by running: stoobly-agent ca-cert install")
504
- sys.exit(1)
514
+
515
+ if confirm == "y" or confirm == "Y":
516
+ ca_cert_install(app.ca_certs_dir_path)
517
+ else:
518
+ print("You can install the CA certificate later by running: stoobly-agent ca-cert install")
505
519
 
506
520
  services = __get_services(
507
521
  app, service=kwargs['service'], workflow=[kwargs['workflow_name']]
@@ -533,6 +547,19 @@ def up(**kwargs):
533
547
  **kwargs
534
548
  )
535
549
 
550
+ # Because test workflow is complete containerized, we don't need to prompt to install hostnames in /etc/hosts
551
+ # Entrypoint container will be within the container network
552
+ if workflow_command.workflow_template != WORKFLOW_TEST_TYPE:
553
+ if not containerized and not dry_run:
554
+ # Prompt confirm to install hostnames
555
+ if kwargs.get('hostname_install_confirm'):
556
+ confirm = kwargs['hostname_install_confirm']
557
+ else:
558
+ confirm = input(f"Do you want to install hostnames for {kwargs['workflow_name']}? (y/N) ")
559
+
560
+ if confirm == "y" or confirm == "Y":
561
+ __hostname_install(app_dir_path=kwargs['app_dir_path'], service=kwargs['service'], workflow=[kwargs['workflow_name']])
562
+
536
563
  if first_time and not containerized and not dry_run:
537
564
  options = {}
538
565
 
@@ -596,30 +623,7 @@ def validate(**kwargs):
596
623
  @click.option('--service', multiple=True, help='Select specific services. Defaults to all.')
597
624
  @click.option('--workflow', multiple=True, help='Specify services by workflow(s). Defaults to all.')
598
625
  def install(**kwargs):
599
- app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE)
600
- __validate_app(app)
601
-
602
- services = __get_services(
603
- app, service=kwargs['service'], without_core=True, workflow=kwargs['workflow']
604
- )
605
-
606
- hostnames = []
607
- for service_name in services:
608
- service = Service(service_name, app)
609
- __validate_service_dir(service.dir_path)
610
-
611
- service_config = ServiceConfig(service.dir_path)
612
- if service_config.hostname:
613
- hostnames.append(service_config.hostname)
614
-
615
- __elevate_sudo()
616
-
617
- try:
618
- hosts_file_manager = HostsFileManager()
619
- hosts_file_manager.install_hostnames(hostnames)
620
- except PermissionError:
621
- print("Permission denied. Please run this command with sudo.", file=sys.stderr)
622
- sys.exit(1)
626
+ __hostname_install(**kwargs)
623
627
 
624
628
  @hostname.command(
625
629
  help="Delete from the system hosts file all scaffold service hostnames"
@@ -628,30 +632,7 @@ def install(**kwargs):
628
632
  @click.option('--service', multiple=True, help='Select specific services. Defaults to all.')
629
633
  @click.option('--workflow', multiple=True, help='Specify services by workflow(s). Defaults to all.')
630
634
  def uninstall(**kwargs):
631
- app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE)
632
- __validate_app(app)
633
-
634
- services = __get_services(
635
- app, service=kwargs['service'], without_core=True, workflow=kwargs['workflow']
636
- )
637
-
638
- hostnames = []
639
- for service_name in services:
640
- service = Service(service_name, app)
641
- __validate_service_dir(service.dir_path)
642
-
643
- service_config = ServiceConfig(service.dir_path)
644
- if service_config.hostname:
645
- hostnames.append(service_config.hostname)
646
-
647
- __elevate_sudo()
648
-
649
- try:
650
- hosts_file_manager = HostsFileManager()
651
- hosts_file_manager.uninstall_hostnames(hostnames)
652
- except PermissionError:
653
- print("Permission denied. Please run this command with sudo.", file=sys.stderr)
654
- sys.exit(1)
635
+ __hostname_uninstall(**kwargs)
655
636
 
656
637
  scaffold.add_command(app)
657
638
  scaffold.add_command(service)
@@ -716,6 +697,64 @@ def __get_services(app: App, **kwargs):
716
697
 
717
698
  return services
718
699
 
700
+ def __hostname_install(**kwargs):
701
+ app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE)
702
+ __validate_app(app)
703
+
704
+ services = __get_services(
705
+ app, service=kwargs['service'], without_core=True, workflow=kwargs['workflow']
706
+ )
707
+
708
+ hostnames = []
709
+ for service_name in services:
710
+ service = Service(service_name, app)
711
+ __validate_service_dir(service.dir_path)
712
+
713
+ service_config = ServiceConfig(service.dir_path)
714
+ if service_config.hostname:
715
+ hostnames.append(service_config.hostname)
716
+
717
+ if not hostnames:
718
+ return
719
+
720
+ __elevate_sudo()
721
+
722
+ try:
723
+ hosts_file_manager = HostsFileManager()
724
+ hosts_file_manager.install_hostnames(hostnames)
725
+ except PermissionError:
726
+ print("Permission denied. Please run this command with sudo.", file=sys.stderr)
727
+ sys.exit(1)
728
+
729
+ def __hostname_uninstall(**kwargs):
730
+ app = App(kwargs['app_dir_path'], SERVICES_NAMESPACE)
731
+ __validate_app(app)
732
+
733
+ services = __get_services(
734
+ app, service=kwargs['service'], without_core=True, workflow=kwargs['workflow']
735
+ )
736
+
737
+ hostnames = []
738
+ for service_name in services:
739
+ service = Service(service_name, app)
740
+ __validate_service_dir(service.dir_path)
741
+
742
+ service_config = ServiceConfig(service.dir_path)
743
+ if service_config.hostname:
744
+ hostnames.append(service_config.hostname)
745
+
746
+ if not hostnames:
747
+ return
748
+
749
+ __elevate_sudo()
750
+
751
+ try:
752
+ hosts_file_manager = HostsFileManager()
753
+ hosts_file_manager.uninstall_hostnames(hostnames)
754
+ except PermissionError:
755
+ print("Permission denied. Please run this command with sudo.", file=sys.stderr)
756
+ sys.exit(1)
757
+
719
758
  def __print_header(text: str):
720
759
  Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}{text}{bcolors.ENDC}")
721
760
 
@@ -117,7 +117,7 @@ class ScaffoldCliInvoker():
117
117
  command = ['workflow', 'up',
118
118
  '--app-dir-path', app_dir_path,
119
119
  '--context-dir-path', app_dir_path,
120
- '--yes',
120
+ '--hostname-install-confirm', 'n',
121
121
  target_workflow_name,
122
122
  ]
123
123
  result = runner.invoke(scaffold, command)
@@ -134,6 +134,7 @@ class ScaffoldCliInvoker():
134
134
  command = ['workflow', 'down',
135
135
  '--app-dir-path', app_dir_path,
136
136
  '--context-dir-path', app_dir_path,
137
+ '--hostname-uninstall-confirm', 'n',
137
138
  target_workflow_name,
138
139
  ]
139
140
  result = runner.invoke(scaffold, command)
@@ -92,8 +92,9 @@ class LocalScaffoldCliInvoker():
92
92
  def cli_workflow_up(runner: CliRunner, app_dir_path: str, target_workflow_name: str):
93
93
  command = ['workflow', 'up',
94
94
  '--app-dir-path', app_dir_path,
95
+ '--ca-certs-install-confirm', 'y',
95
96
  '--context-dir-path', app_dir_path,
96
- '--yes',
97
+ '--hostname-install-confirm', 'y',
97
98
  target_workflow_name,
98
99
  ]
99
100
  result = runner.invoke(scaffold, command)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stoobly-agent
3
- Version: 1.10.3
3
+ Version: 1.10.5
4
4
  Summary: Record, mock, and test HTTP(s) requests. CLI agent for Stoobly
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -1,4 +1,4 @@
1
- stoobly_agent/__init__.py,sha256=G6RHX8ESNEXsGSpOUuCoZooxoXtbFgnLwqa8NHC683M,45
1
+ stoobly_agent/__init__.py,sha256=sxrNGBqNJn6xpatZ-k8nV1eIVOpFpk1_oBfoMJlWMWA,45
2
2
  stoobly_agent/__main__.py,sha256=tefOkFZeCFU4l3C-Y4R_lR9Yt-FETISiXGUnbh6Os54,146
3
3
  stoobly_agent/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  stoobly_agent/app/api/__init__.py,sha256=NIgcbX7iiWrApsCITXlmhr4SYbWS0fwb01x-F3jTFdo,666
@@ -70,7 +70,7 @@ stoobly_agent/app/cli/scaffold/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
70
70
  stoobly_agent/app/cli/scaffold/app.py,sha256=7d5nHnQwyBxEwSC2Jrvbk2U3dj9OnhaHrE2zmC37oOs,3922
71
71
  stoobly_agent/app/cli/scaffold/app_command.py,sha256=x--ejtVSBL0Jz8OiakBtxfI2IZFAWJWCwuSJo7TEU9Y,2386
72
72
  stoobly_agent/app/cli/scaffold/app_config.py,sha256=oG06y9yuAo05ROpOhDC_LFPJ0KKNrhfvYLOgHx5nE4Y,2788
73
- stoobly_agent/app/cli/scaffold/app_create_command.py,sha256=G4C-XSp4hN0FvaSKLpGGvTp8iXHqKYdqoxxRPHgO0a0,8253
73
+ stoobly_agent/app/cli/scaffold/app_create_command.py,sha256=6wGEuBkIkQkpyFv41C8CVkT2y1RCzD5OCEI9fZXku0s,8311
74
74
  stoobly_agent/app/cli/scaffold/command.py,sha256=G4Zp647cuviaEXUdcl7Rbx_qQAr0Z_DS7-Y3MWDC1Qc,281
75
75
  stoobly_agent/app/cli/scaffold/config.py,sha256=HZU5tkvr3dkPr4JMXZtrJlu2wxxO-134Em6jReFFcq0,688
76
76
  stoobly_agent/app/cli/scaffold/constants.py,sha256=aI--kf5t9D10iBZKujMXVB5IjYML1mX8bhchT9d560k,3324
@@ -95,7 +95,7 @@ stoobly_agent/app/cli/scaffold/docker/workflow/dns_decorator.py,sha256=DGaSlbOvA
95
95
  stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py,sha256=xp1TmP8drOyl9Zhm5B1ci6NqPqRFDr2yxipmvSljgiE,717
96
96
  stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py,sha256=QO1TbAj6QthIyHvy7itV_d9NteNcjClYaan1GX-0kLc,1201
97
97
  stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py,sha256=tSPnscsBZusBaSt_NlB4exrZ2MnWMRAUJgw_NaEdHiw,1199
98
- stoobly_agent/app/cli/scaffold/docker/workflow/run_command.py,sha256=az_asRqxfch_M6gRdtoZI6_QgYbOSEKutrf8q_asp-8,15753
98
+ stoobly_agent/app/cli/scaffold/docker/workflow/run_command.py,sha256=aqRVHTbCooan8zTMfKjxEjssf255YTEdLzoPUXna29o,18010
99
99
  stoobly_agent/app/cli/scaffold/env.py,sha256=dT33tHoQaUxfsFCYm8kfaAv-qPVrUPmNFQmLnFQhZeQ,1107
100
100
  stoobly_agent/app/cli/scaffold/hosts_file_manager.py,sha256=zNX5wh6zXQ4J2BA0YYdD7_CPqDz02b_ghXsY3oTjjB4,4999
101
101
  stoobly_agent/app/cli/scaffold/local/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -103,7 +103,7 @@ stoobly_agent/app/cli/scaffold/local/service/__init__.py,sha256=47DEQpj8HBSa-_TI
103
103
  stoobly_agent/app/cli/scaffold/local/service/builder.py,sha256=uZNPIQWo4UcLy3bcE6Wvntle6ONPpWjS5oAq3g0Punk,1852
104
104
  stoobly_agent/app/cli/scaffold/local/workflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
105
105
  stoobly_agent/app/cli/scaffold/local/workflow/builder.py,sha256=O8Iwyd231-ciQWkiGB5HAiFJXMDUtXIoyl0S_Jrj3lQ,806
106
- stoobly_agent/app/cli/scaffold/local/workflow/run_command.py,sha256=gBVLi7I42B9_HCpzBoM3XtxzmvjDc_3FnquxH8YqsfA,13030
106
+ stoobly_agent/app/cli/scaffold/local/workflow/run_command.py,sha256=BJrG5g-c4kf_cgQ1hE41KUKQsvtwVVKzaTgQ7eCaPUk,15625
107
107
  stoobly_agent/app/cli/scaffold/managed_services_docker_compose.py,sha256=-wLBXUi7DCWsfm5KzZzd_kdJKOTl1NT924XR7dyjbSY,574
108
108
  stoobly_agent/app/cli/scaffold/service.py,sha256=74JwjTRRkk6lo-k9hre1iGztbKa9zDqjPVx3Qgpze-s,699
109
109
  stoobly_agent/app/cli/scaffold/service_command.py,sha256=j-lkG5Zth_CBHa6Z9Kv3dJwxX9gylFBZMZbW691R8ZU,1480
@@ -114,10 +114,10 @@ stoobly_agent/app/cli/scaffold/service_dependency.py,sha256=olr_s_cfn51Pz5FlIihl
114
114
  stoobly_agent/app/cli/scaffold/service_docker_compose.py,sha256=fVUZ-oo-bn5GVZp8JgGq7AkiQQ6-JkxwK_OMlinS9WM,915
115
115
  stoobly_agent/app/cli/scaffold/service_update_command.py,sha256=oWusBKfvjt4RnK03_V3CJYWrfsCI4_LcR7W12eLXMR4,2579
116
116
  stoobly_agent/app/cli/scaffold/service_workflow.py,sha256=sQ_Edy_wGHKMXpD0DmhnOWkGEKz7gSgEGNI8f7aXOdg,444
117
- stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py,sha256=80pX8vTKWCP3Or7zW7lZpn2LS8ScnwMZsjItKizHIkQ,11627
117
+ stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py,sha256=DPYLBAwUsh4C8ho-4SwjBe32xuBSUpWlWgRF9JIbKRc,11768
118
118
  stoobly_agent/app/cli/scaffold/templates/__init__.py,sha256=x8C_a0VoO_vUbosp4_6IC1U7Ge9NnUdVKDPpVMtMkeY,171
119
119
  stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context,sha256=9DQK-OXnRKjjKWsUSIRAio6dkR4eGxD1vizPT7Q5sp8,159
120
- stoobly_agent/app/cli/scaffold/templates/app/.Makefile,sha256=OnY_3D9nxl3HxfUxvCgk7PY3XntEbgO9j1myx_ETK7w,9161
120
+ stoobly_agent/app/cli/scaffold/templates/app/.Makefile,sha256=9E46Dzs3xCKy2fP6L6Q4DxourYxrwKCcoUhEf3Y7DEs,9619
121
121
  stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.base.yml,sha256=SABod6_InBFyOm-uulK7r27SR0sWRUiL0h69g_jvNJA,249
122
122
  stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.networks.yml,sha256=I4PbJpQjFHb5IbAUWNvYM6okDEtmwtKFDQg-yog05WM,141
123
123
  stoobly_agent/app/cli/scaffold/templates/app/Makefile,sha256=TEmPG7Bf0KZOnmfsgdzza3UdwcVMmM5Lj1YdLc4cgjA,79
@@ -182,11 +182,11 @@ stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.init,sh
182
182
  stoobly_agent/app/cli/scaffold/templates/build/services/entrypoint/test/.run,sha256=LQs_bJiN3LaC0eJnaF_Fnrr9ZIlxkaM8tnWMxjCovXM,446
183
183
  stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/intercept/.disable,sha256=xVf4Pk1RLvJm7Ff0rbGoWhYHPv0ME5e93fxS2yFqLnE,45
184
184
  stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/intercept/.enable,sha256=sfUSPG4uHdXX95BLgivXQYLbsLBP2DjJIiSTXRtvXuY,188
185
- stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scaffold/.down,sha256=WtEjm2hJ01-1GGxRmmcUqZq7VjEe5JfZZKvm8TNhNEs,219
186
- stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scaffold/.logs,sha256=4CJVEVLn9VTQN9LAjOJpijwd_DZ5IuIXL1aFCYkelxk,178
185
+ stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scaffold/.down,sha256=Eb-DY3NYLBny2Akx4R3ckqHqi_5gzYKNp9tJgFbeFOQ,205
186
+ stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scaffold/.logs,sha256=ebsSW7N3RbT4asqDVpdKWzztC3FZxXLpjnxsiblMSGQ,184
187
187
  stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scaffold/.mkcert,sha256=vyHaXmvy-7oL2RD8rIxwT-fdJS5kXmB0yHK5fRMo_cM,46
188
188
  stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scaffold/.services,sha256=sAbaKzBxNDXimyUNinB_a2Kt47vUCa0Kd3Z1QAp2GrU,161
189
- stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scaffold/.up,sha256=_0ZrBktSJRJm4Tubin8nYmjebui5Wv6r3tPQ_FN8nxo,209
189
+ stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scaffold/.up,sha256=woSsPc0S9CasNItleUD1nz6Rz-8TThamy-3_DGLMiSk,195
190
190
  stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scenario/.create,sha256=EZe84bLAKB-mrB9PGMx-amyMjp6x-ESUZKC8wxrWdLE,245
191
191
  stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scenario/.delete,sha256=RspRDQ7WT5jpN2o-6qXOlH-A2VpN4981pD4ZJljk9Rw,260
192
192
  stoobly_agent/app/cli/scaffold/templates/build/workflows/exec/scenario/.list,sha256=BdX3ARC-Piuwk0vFzBrcxEwtHjdFncpORzqP-hP4IwE,84
@@ -206,11 +206,11 @@ stoobly_agent/app/cli/scaffold/templates/constants.py,sha256=o6SbbG0AJoIYI0cQ5qD
206
206
  stoobly_agent/app/cli/scaffold/templates/factory.py,sha256=WuAKRGq4qMGQTxgG8Hx0Q8xzMoe87IRPfxrwcCeCP2E,2537
207
207
  stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.Dockerfile.cypress,sha256=iMEONOnfsVTKpoVxv-tHDfUTCiemEMCt0LZ9QcdOyJQ,680
208
208
  stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.docker-compose.yml,sha256=fPjbDnGQQj-TLGQbctHBwgXnC8zz2m9H-9uYK0I8mwk,484
209
- stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.run,sha256=YzvRZqTqIy4P5sM7SBsUjVgI6un6KbjwkbuPgF5FWB8,475
209
+ stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.run,sha256=nwTqzhR5a7OgfQj7aPiq0_3_smV0qTLw1WRcPFNQEtM,475
210
210
  stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.Dockerfile.playwright,sha256=LGibNX2Ar2r_yDbb4AcdCm6zzArtOHDk_cvRxszKExw,1221
211
211
  stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.docker-compose.yml,sha256=1eyJ-wYLXXhiPaT9dYhORaa9IGnhSaqnMuLSwxrAPiQ,503
212
212
  stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.entrypoint.sh,sha256=vIffcTuYdoOLH3GDKzoppExDhP0ls92wyPziqpxZrAc,286
213
- stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.run,sha256=a7UxsdfxNssbu5jLnNiHZKMmY5KD0fixk8L6dnKHLCE,482
213
+ stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.run,sha256=6AS9JZtoFw5eA4Gmh6wRbJq8BDujNLoifyLbRK1BkrI,482
214
214
  stoobly_agent/app/cli/scaffold/templates/workflow/mock/configure,sha256=ktsd7J4r3YQbeUj7Uex7iakdcb6JAouwvlRqs-LI35c,171
215
215
  stoobly_agent/app/cli/scaffold/templates/workflow/mock/docker-compose.yml,sha256=x6tofP1N9rFnlMdWKtyjzpWnX7-_h6eYihI_iDxGr90,479
216
216
  stoobly_agent/app/cli/scaffold/templates/workflow/mock/fixtures.yml,sha256=CJlZ_kugygZpmyqIauBjNZxqk7XyLaa3yl3AWj8KV28,259
@@ -240,9 +240,9 @@ stoobly_agent/app/cli/scaffold/workflow_create_command.py,sha256=5gSkxNxrXLeobLU
240
240
  stoobly_agent/app/cli/scaffold/workflow_env.py,sha256=shPjoX1SWe7K6pGpZvw2fPVHWd6j_azTe58jvOjGUns,607
241
241
  stoobly_agent/app/cli/scaffold/workflow_log_command.py,sha256=Bke4lMOMxuDUFuAx9nlXHbKgYMO4KAg9ASHvjz4aVWc,1372
242
242
  stoobly_agent/app/cli/scaffold/workflow_namesapce.py,sha256=VNaZrcqMMeqrzpPGhD9-oaZems1k0ebRc6wR74EvA8c,1170
243
- stoobly_agent/app/cli/scaffold/workflow_run_command.py,sha256=842VEl4hxpgxJdqZQErY7gb2hhcykErApOw9rfexSdY,6049
243
+ stoobly_agent/app/cli/scaffold/workflow_run_command.py,sha256=35wxoc9qH3ZGuPcKgFGQLEMvGwDVWbjpXqve4iz2wBY,6185
244
244
  stoobly_agent/app/cli/scaffold/workflow_validate_command.py,sha256=Uo_yo6rVR1ZR7xpvsQvlH48AyMBVLRupd4G-bRjzm_Q,5584
245
- stoobly_agent/app/cli/scaffold_cli.py,sha256=us5QCcuseawNJh2Y3gvOHUa9h2GFqlX_c-grar5Ntuo,31653
245
+ stoobly_agent/app/cli/scaffold_cli.py,sha256=YnRKrC_XQ5OdQ1tgA-oL4rmamw70DlMm17SqIAQ9sfU,33677
246
246
  stoobly_agent/app/cli/scenario_cli.py,sha256=lA9a4UNnLzrbJX5JJE07KGb5i9pBd2c2vdFUW6_7k0E,8345
247
247
  stoobly_agent/app/cli/snapshot_cli.py,sha256=1Dw5JgDlmG6vctrawIRO7CdB73vAQk_wRBnPG2lVOrQ,11929
248
248
  stoobly_agent/app/cli/trace_cli.py,sha256=K7E-vx3JUcqEDSWOdIOi_AieKNQz7dBfmRrVvKDkzFI,4605
@@ -721,12 +721,12 @@ stoobly_agent/test/app/cli/request/request_reset_test.py,sha256=5My6Z452eideAOUu
721
721
  stoobly_agent/test/app/cli/request/request_response_test.py,sha256=Fu-A8tIn016DKme4WIaPzo3YeFY-CPtTOpaSFigUVVM,1263
722
722
  stoobly_agent/test/app/cli/request/request_snapshot_test.py,sha256=3kMmv0CuvnMXLgDQA-_u9S1DIiNOdL63L-IptVuOpf8,6308
723
723
  stoobly_agent/test/app/cli/request/request_test_test.py,sha256=-cJNXKjgryVVfVt-7IN5fIhBwe3NjFoPmeavDH8lAjU,5527
724
- stoobly_agent/test/app/cli/scaffold/docker/cli_invoker.py,sha256=jVTiFytPJhmrz8nTDb9xZYbRCFugOb4s7FglE_kiR3c,5322
724
+ stoobly_agent/test/app/cli/scaffold/docker/cli_invoker.py,sha256=W4ZZgrgUU0hBm_l0x04eR_9eGVyfbLSMbDVGqhpi2qE,5391
725
725
  stoobly_agent/test/app/cli/scaffold/docker/cli_test.py,sha256=wkYnRQo8OBDds-pGmdc2cYs5yxgO_2GiDxzI1imGntc,4301
726
726
  stoobly_agent/test/app/cli/scaffold/docker/e2e_test.py,sha256=RxHvIMyVhpXy0fkfCJPFLZ2DtyC3dieVGETWOq-rQos,13655
727
727
  stoobly_agent/test/app/cli/scaffold/hosts_file_manager_test.py,sha256=ztcPh1x0ZCW1FWA5YL4ulEVjfbW9TOPgk1bnSDPNmCw,2287
728
728
  stoobly_agent/test/app/cli/scaffold/local/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
729
- stoobly_agent/test/app/cli/scaffold/local/cli_invoker.py,sha256=BL5OVbibeDr3xsiUZQNvAmZmTdmmGh0hQVRl6AEFhXI,4071
729
+ stoobly_agent/test/app/cli/scaffold/local/cli_invoker.py,sha256=YLQuz5JjvzfqFKk-BFT-Bl9EZgj37ZDZtwNsvqNRDHA,4138
730
730
  stoobly_agent/test/app/cli/scaffold/local/e2e_test.py,sha256=10cFuX43dv6-L00zWHT9MV5c0idJhiLDhEw0FOOsKK8,15513
731
731
  stoobly_agent/test/app/cli/scenario/scenario_create_test.py,sha256=fGqcjO1_1OvdpUMQfGRVkSyFe61u8WIcp_ndLFrf33A,3962
732
732
  stoobly_agent/test/app/cli/scenario/scenario_replay_integration_test.py,sha256=NbGJzmvPsNLBR0ac65yt_cOTfpnsST1IG7i3F0euwAk,7031
@@ -797,8 +797,8 @@ stoobly_agent/test/mock_data/scaffold/docker-compose-local-service.yml,sha256=1W
797
797
  stoobly_agent/test/mock_data/scaffold/index.html,sha256=qJwuYajKZ4ihWZrJQ3BNObV5kf1VGnnm_vqlPJzdqLE,258
798
798
  stoobly_agent/test/mock_data/uspto.yaml,sha256=6U5se7C3o-86J4m9xpOk9Npias399f5CbfWzR87WKwE,7835
799
799
  stoobly_agent/test/test_helper.py,sha256=6v4AHeqYPw7vtRoxET_ubmRWPJoSmTR_DVHay3FxNbQ,1299
800
- stoobly_agent-1.10.3.dist-info/METADATA,sha256=WOtXuRZ-PH8eZ38X2Z63_zddgLbLMbtLJ6Mbq3-vo6I,3203
801
- stoobly_agent-1.10.3.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
802
- stoobly_agent-1.10.3.dist-info/entry_points.txt,sha256=aq5wix5oC8MDQtmyPGU0xaFrsjJg7WH28NmXh2sc3Z8,56
803
- stoobly_agent-1.10.3.dist-info/licenses/LICENSE,sha256=o93sj12cdoEOsTCjPaPFsw3Xq0SXs3pPcY-9reE2sEw,548
804
- stoobly_agent-1.10.3.dist-info/RECORD,,
800
+ stoobly_agent-1.10.5.dist-info/METADATA,sha256=7QB_Saw0H6Pz_qkMRM9HKiXta5p-noQJIMFN5lGVm4g,3203
801
+ stoobly_agent-1.10.5.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
802
+ stoobly_agent-1.10.5.dist-info/entry_points.txt,sha256=aq5wix5oC8MDQtmyPGU0xaFrsjJg7WH28NmXh2sc3Z8,56
803
+ stoobly_agent-1.10.5.dist-info/licenses/LICENSE,sha256=o93sj12cdoEOsTCjPaPFsw3Xq0SXs3pPcY-9reE2sEw,548
804
+ stoobly_agent-1.10.5.dist-info/RECORD,,