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
@@ -3,20 +3,16 @@ import os
3
3
  import pdb
4
4
  import sys
5
5
 
6
- from io import TextIOWrapper
7
- from typing import List
8
- from urllib.parse import urlparse
9
6
 
7
+ from stoobly_agent.app.cli.ca_cert_cli import ca_cert_install
10
8
  from stoobly_agent.app.cli.helpers.certificate_authority import CertificateAuthority
11
- from stoobly_agent.app.cli.helpers.shell import exec_stream
12
9
  from stoobly_agent.app.cli.scaffold.app import App
10
+ from stoobly_agent.app.cli.scaffold.app_config import AppConfig
13
11
  from stoobly_agent.app.cli.scaffold.app_create_command import AppCreateCommand
14
12
  from stoobly_agent.app.cli.scaffold.constants import (
15
- SERVICES_NAMESPACE, WORKFLOW_CONTAINER_PROXY, WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
13
+ PLUGIN_CYPRESS, PLUGIN_PLAYWRIGHT, RUN_ON_DOCKER, RUN_ON_OPTIONS, SERVICES_NAMESPACE, WORKFLOW_CONTAINER_PROXY, WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
16
14
  )
17
- from stoobly_agent.app.cli.scaffold.constants import PLUGIN_CYPRESS, PLUGIN_PLAYWRIGHT
18
15
  from stoobly_agent.app.cli.scaffold.containerized_app import ContainerizedApp
19
- from stoobly_agent.app.cli.scaffold.docker.service.configure_gateway import configure_gateway
20
16
  from stoobly_agent.app.cli.scaffold.docker.workflow.decorators_factory import get_workflow_decorators
21
17
  from stoobly_agent.app.cli.scaffold.hosts_file_manager import HostsFileManager
22
18
  from stoobly_agent.app.cli.scaffold.service import Service
@@ -30,16 +26,16 @@ from stoobly_agent.app.cli.scaffold.validate_exceptions import ScaffoldValidateE
30
26
  from stoobly_agent.app.cli.scaffold.workflow import Workflow
31
27
  from stoobly_agent.app.cli.scaffold.workflow_create_command import WorkflowCreateCommand
32
28
  from stoobly_agent.app.cli.scaffold.workflow_copy_command import WorkflowCopyCommand
33
- from stoobly_agent.app.cli.scaffold.workflow_log_command import WorkflowLogCommand
34
29
  from stoobly_agent.app.cli.scaffold.workflow_namesapce import WorkflowNamespace
35
- from stoobly_agent.app.cli.scaffold.workflow_run_command import WorkflowRunCommand
30
+ from stoobly_agent.app.cli.scaffold.docker.workflow.run_command import DockerWorkflowRunCommand
31
+ from stoobly_agent.app.cli.scaffold.local.workflow.run_command import LocalWorkflowRunCommand
36
32
  from stoobly_agent.app.cli.scaffold.workflow_validate_command import WorkflowValidateCommand
37
33
  from stoobly_agent.config.constants import env_vars
38
34
  from stoobly_agent.config.data_dir import DataDir
39
35
  from stoobly_agent.lib.logger import bcolors, DEBUG, ERROR, INFO, Logger, WARNING
40
36
 
41
37
  from .helpers.print_service import FORMATS, print_services, select_print_options
42
- from .validators.scaffold import validate_app_name, validate_hostname, validate_namespace, validate_network, validate_service_name
38
+ from .validators.scaffold import validate_app_name, validate_hostname, validate_namespace, validate_service_name
43
39
 
44
40
  LOG_ID = 'Scaffold'
45
41
 
@@ -92,7 +88,9 @@ def hostname(ctx):
92
88
  @click.option('--app-dir-path', default=current_working_dir, help='Path to create the app scaffold.')
93
89
  @click.option('--docker-socket-path', default='/var/run/docker.sock', type=click.Path(exists=True, file_okay=True, dir_okay=False), help='Path to Docker socket.')
94
90
  @click.option('--plugin', multiple=True, type=click.Choice([PLUGIN_CYPRESS, PLUGIN_PLAYWRIGHT]), help='Scaffold integrations.')
91
+ @click.option('--proxy-port', default=8080, type=click.IntRange(1, 65535), help='Proxy service port.')
95
92
  @click.option('--quiet', is_flag=True, help='Disable log output.')
93
+ @click.option('--run-on', multiple=True, type=click.Choice(RUN_ON_OPTIONS), default=[RUN_ON_DOCKER], help='Runtime environments to support (default: docker).')
96
94
  @click.option('--ui-port', default=4200, type=click.IntRange(1, 65535), help='UI service port.')
97
95
  @click.argument('app_name', callback=validate_app_name)
98
96
  def create(**kwargs):
@@ -343,13 +341,10 @@ def copy(**kwargs):
343
341
  @click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
344
342
  @click.option('--containerized', is_flag=True, help='Set if run from within a container.')
345
343
  @click.option('--dry-run', default=False, is_flag=True)
346
- @click.option('--extra-entrypoint-compose-path', help='Path to extra entrypoint compose file.')
347
344
  @click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
348
345
  Log levels can be "debug", "info", "warning", or "error"
349
346
  ''')
350
347
  @click.option('--namespace', callback=validate_namespace, help='Workflow namespace.')
351
- @click.option('--network', callback=validate_network, help='Workflow network name.')
352
- @click.option('--rmi', is_flag=True, help='Remove images used by containers.')
353
348
  @click.option('--script-path', help='Path to intermediate script path.')
354
349
  @click.option('--service', multiple=True, help='Select which services to log. Defaults to all.')
355
350
  @click.option('--user-id', default=os.getuid(), help='OS user ID of the owner of context dir path.')
@@ -364,59 +359,38 @@ def down(**kwargs):
364
359
  __validate_app(app)
365
360
 
366
361
  __with_namespace_defaults(kwargs)
367
- __with_workflow_namespace(app, kwargs['namespace'])
368
362
 
369
363
  services = __get_services(
370
364
  app, service=kwargs['service'], workflow=[kwargs['workflow_name']]
371
365
  )
372
366
 
373
- commands: List[WorkflowRunCommand] = []
374
- for service in services:
375
- config = { **kwargs }
376
- config['service_name'] = service
377
- command = WorkflowRunCommand(app, **config)
378
- command.current_working_dir = current_working_dir
379
- commands.append(command)
380
-
367
+ command_args = { 'print_service_header': lambda service_name: __print_header(f"Step {service_name}") }
381
368
  script = __build_script(app, **kwargs)
382
-
383
- commands = sorted(commands, key=lambda command: command.service_config.priority)
384
- for index, command in enumerate(commands):
385
- __print_header(f"SERVICE {command.service_name}")
386
-
387
- extra_compose_path = None
388
-
389
- # By default, the entrypoint service should be last
390
- # However, this can change if the user has configured a service's priority to be higher
391
- if index == len(commands) - 1:
392
- extra_compose_path = kwargs['extra_entrypoint_compose_path']
393
-
394
- exec_command = command.down(
395
- extra_compose_path=extra_compose_path,
396
- namespace=kwargs['namespace'],
397
- rmi=kwargs['rmi'],
398
- user_id=kwargs['user_id']
369
+
370
+ # Determine which workflow command to use based on app configuration
371
+ app_config = AppConfig(app.scaffold_namespace_path)
372
+ if app_config.run_on_local:
373
+ # Use LocalWorkflowRunCommand for local execution
374
+ workflow_command = LocalWorkflowRunCommand(
375
+ app,
376
+ services=services,
377
+ script=script,
378
+ **kwargs
399
379
  )
400
- if not exec_command:
401
- continue
402
-
403
- print(exec_command, file=script)
404
-
405
- # After services are stopped, their network needs to be removed
406
- if len(commands) > 0:
407
- command: WorkflowRunCommand = commands[0]
408
-
409
- if kwargs['rmi']:
410
- remove_image_command = command.remove_image(kwargs['user_id'])
411
- print(remove_image_command, file=script)
412
-
413
- remove_egress_network_command = command.remove_egress_network()
414
- print(remove_egress_network_command, file=script)
415
-
416
- remove_ingress_network_command = command.remove_ingress_network()
417
- print(remove_ingress_network_command, file=script)
418
-
419
- __run_script(script, kwargs['dry_run'])
380
+ else:
381
+ # Use DockerWorkflowRunCommand for Docker execution
382
+ workflow_command = DockerWorkflowRunCommand(
383
+ app,
384
+ services=services,
385
+ script=script,
386
+ **kwargs
387
+ )
388
+
389
+ # Execute the workflow down
390
+ workflow_command.down(
391
+ **command_args,
392
+ **kwargs
393
+ )
420
394
 
421
395
  # Options are no longer valid
422
396
  if kwargs['containerized'] and os.path.exists(data_dir.mitmproxy_options_json_path):
@@ -451,37 +425,34 @@ def logs(**kwargs):
451
425
  app, service=kwargs['service'], without_core=True, workflow=[kwargs['workflow_name']]
452
426
  )
453
427
 
454
- commands: List[WorkflowLogCommand] = []
455
- for service in services:
456
- if len(kwargs['service']) == 0:
457
- # If no filter is specified, ignore CORE_SERVICES
458
- if service in CORE_SERVICES:
459
- continue
460
- else:
461
- # If a filter is specified, ignore all other services
462
- if service not in kwargs['service']:
463
- continue
464
-
465
- config = { **kwargs }
466
- config['service_name'] = service
467
- command = WorkflowLogCommand(app, **config)
468
- commands.append(command)
469
-
428
+ command_args = { 'print_service_header': lambda service_name: __print_header(f"SERVICE {service_name}") }
470
429
  script = __build_script(app, **kwargs)
471
-
472
- commands = sorted(commands, key=lambda command: command.service_config.priority)
473
- for index, command in enumerate(commands):
474
- __print_header(f"SERVICE {command.service_name}")
475
-
476
- follow = kwargs['follow'] and index == len(commands) - 1
477
- shell_commands = command.build(
478
- containers=kwargs['container'], follow=follow, namespace=kwargs['namespace']
430
+
431
+ # Determine which workflow command to use based on app configuration
432
+ app_config = AppConfig(app.scaffold_namespace_path)
433
+ if app_config.run_on_local:
434
+ # Use LocalWorkflowRunCommand for local execution
435
+ workflow_command = LocalWorkflowRunCommand(
436
+ app,
437
+ services=services,
438
+ script=script,
439
+ **kwargs
479
440
  )
441
+ else:
442
+ # Use DockerWorkflowRunCommand for Docker execution
443
+ workflow_command = DockerWorkflowRunCommand(
444
+ app,
445
+ services=services,
446
+ script=script,
447
+ **kwargs
448
+ )
449
+ command_args['print_service_header'] = lambda service_name: __print_header(f"SERVICE {service_name}")
480
450
 
481
- for shell_command in shell_commands:
482
- print(shell_command, file=script)
483
-
484
- __run_script(script, kwargs['dry_run'])
451
+ # Execute the workflow logs
452
+ workflow_command.logs(
453
+ **command_args,
454
+ **kwargs
455
+ )
485
456
 
486
457
  @workflow.command()
487
458
  @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
@@ -489,28 +460,25 @@ def logs(**kwargs):
489
460
  @click.option('--certs-dir-path', help='Path to certs directory. Defaults to the certs dir of the context.')
490
461
  @click.option('--containerized', is_flag=True, help='Set if run from within a container.')
491
462
  @click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
492
- @click.option('--detached', is_flag=True, help='If set, will not run the highest priority service in the foreground.')
463
+ @click.option('--detached', is_flag=True, help='If set, will run the highest priority service in the background.')
493
464
  @click.option('--dry-run', default=False, is_flag=True, help='If set, prints commands.')
494
- @click.option('--extra-entrypoint-compose-path', help='Path to extra entrypoint compose file.')
495
465
  @click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
496
466
  Log levels can be "debug", "info", "warning", or "error"
497
467
  ''')
498
468
  @click.option('--mkcert', is_flag=True, help='Set to generate SSL certs for HTTPS services.')
499
469
  @click.option('--namespace', callback=validate_namespace, help='Workflow namespace.')
500
- @click.option('--network', callback=validate_network, help='Workflow network name.')
501
- @click.option('--no-build', is_flag=True, help='Do not build images before starting containers.')
502
470
  @click.option('--no-publish', is_flag=True, help='Do not publish all ports.')
503
- @click.option('--pull', is_flag=True, help='Pull image before running.')
504
471
  @click.option('--script-path', help='Path to intermediate script path.')
505
472
  @click.option('--service', multiple=True, help='Select which services to run. Defaults to all.')
506
473
  @click.option('--user-id', default=os.getuid(), help='OS user ID of the owner of context dir path.')
507
474
  @click.option('--verbose', is_flag=True)
508
- @click.option('--without-base', is_flag=True, help='Disable building Stoobly base image.')
475
+ @click.option('-y', '--yes', is_flag=True, help='Auto-confirm CA certificate installation prompt.')
509
476
  @click.argument('workflow_name')
510
477
  def up(**kwargs):
511
478
  os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
512
479
 
513
480
  containerized = kwargs['containerized']
481
+ dry_run = kwargs['dry_run']
514
482
 
515
483
  # Because we are running a docker-compose command which depends on APP_DIR env var
516
484
  # when we are running this command within a container, the host's app_dir_path will likely differ
@@ -519,76 +487,72 @@ def up(**kwargs):
519
487
  __validate_app(app)
520
488
 
521
489
  __with_namespace_defaults(kwargs)
522
- workflow_namespace = __with_workflow_namespace(app, kwargs['namespace'])
490
+
491
+ # First time if folder does not exist or is empty
492
+ first_time = not os.path.exists(app.ca_certs_dir_path) or not os.listdir(app.ca_certs_dir_path)
493
+ 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)
498
+ else:
499
+ 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)
523
505
 
524
506
  services = __get_services(
525
507
  app, service=kwargs['service'], workflow=[kwargs['workflow_name']]
526
508
  )
527
509
 
528
- if kwargs['mkcert']:
529
- _app = ContainerizedApp(app_dir_path, SERVICES_NAMESPACE) if containerized else app
530
- __services_mkcert(_app, services)
531
-
532
- # Gateway ports are dynamically set depending on the workflow run
533
- workflow = Workflow(kwargs['workflow_name'], app)
534
- configure_gateway(workflow_namespace, workflow.service_paths_from_services(services), kwargs['no_publish'])
535
-
536
- commands: List[WorkflowRunCommand] = []
537
- for service in services:
538
- config = { **kwargs }
539
- config['service_name'] = service
540
- command = WorkflowRunCommand(app, **config)
541
- command.current_working_dir = current_working_dir
542
- commands.append(command)
543
-
510
+ command_args = { 'print_service_header': lambda service_name: __print_header(f"SERVICE {service_name}") }
544
511
  script = __build_script(app, **kwargs)
545
512
 
546
- # Before services can be started, their image and network needs to be created
547
- if len(commands) > 0:
548
- command: WorkflowRunCommand = commands[0]
549
-
550
- init_commands = []
551
- if not kwargs['without_base']:
552
- create_image_command = command.create_image(user_id=kwargs['user_id'], verbose=kwargs['verbose'])
553
- init_commands.append(create_image_command)
554
-
555
- init_commands.append(command.create_egress_network())
556
- init_commands.append(command.create_ingress_network())
557
- joined_command = ' && '.join(init_commands)
558
-
559
- if not containerized:
560
- command.write_nameservers()
561
-
562
- print(joined_command, file=script)
563
-
564
- commands = sorted(commands, key=lambda command: command.service_config.priority)
565
- for index, command in enumerate(commands):
566
- __print_header(f"SERVICE {command.service_name}")
513
+ # Determine which workflow command to use based on app configuration
514
+ app_config = AppConfig(app.scaffold_namespace_path)
515
+ if app_config.run_on_local:
516
+ # Use LocalWorkflowRunCommand for local execution
517
+ workflow_command = LocalWorkflowRunCommand(
518
+ app,
519
+ services=services,
520
+ script=script,
521
+ **kwargs
522
+ )
523
+ else:
524
+ if kwargs['mkcert']:
525
+ _app = ContainerizedApp(app_dir_path, SERVICES_NAMESPACE) if containerized else app
526
+ __services_mkcert(_app, services)
527
+
528
+ # Use DockerWorkflowRunCommand for Docker execution
529
+ workflow_command = DockerWorkflowRunCommand(
530
+ app,
531
+ services=services,
532
+ script=script,
533
+ **kwargs
534
+ )
567
535
 
568
- attached = False
569
- extra_compose_path = None
536
+ if first_time and not containerized and not dry_run:
537
+ options = {}
570
538
 
571
- # By default, the entrypoint service should be last
572
- # However, this can change if the user has configured a service's priority to be higher
573
- if index == len(commands) - 1:
574
- attached = not kwargs['detached']
575
- extra_compose_path = kwargs['extra_entrypoint_compose_path']
539
+ if os.getcwd() != app_dir_path:
540
+ options['app_dir_path'] = app_dir_path
576
541
 
577
- exec_command = command.up(
578
- attached=attached,
579
- extra_compose_path=extra_compose_path,
580
- namespace=kwargs['namespace'],
581
- no_build=kwargs['no_build'],
582
- pull=kwargs['pull'],
583
- user_id=kwargs['user_id']
584
- )
585
- if not exec_command:
586
- continue
542
+ if kwargs['namespace'] != kwargs['workflow_name']:
543
+ options['namespace'] = kwargs['namespace']
587
544
 
588
- print(exec_command, file=script)
589
-
590
- __run_script(script, kwargs['dry_run'])
545
+ options_str = ' '.join([f"--{key} {value}" for key, value in options.items()])
546
+ if options_str:
547
+ options_str = f" {options_str}"
591
548
 
549
+ Logger.instance(LOG_ID).info(f"To view logs, run `stoobly-agent workflow logs{options_str} {kwargs['workflow_name']}`")
550
+
551
+ # Execute the workflow
552
+ workflow_command.up(
553
+ **command_args,
554
+ **kwargs
555
+ )
592
556
 
593
557
  @workflow.command(
594
558
  help="Validate a scaffold workflow"
@@ -755,16 +719,6 @@ def __get_services(app: App, **kwargs):
755
719
  def __print_header(text: str):
756
720
  Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}{text}{bcolors.ENDC}")
757
721
 
758
- def __run_script(script: TextIOWrapper, dry_run = False):
759
- script.close()
760
-
761
- with open(script.name, 'r') as fp:
762
- for line in fp:
763
- if not dry_run:
764
- exec_stream(line.strip())
765
- else:
766
- print(line.strip())
767
-
768
722
  def __scaffold_build(app, **kwargs):
769
723
  command = ServiceCreateCommand(app, **kwargs)
770
724
 
@@ -158,10 +158,10 @@ if is_local:
158
158
  Configure which tests to print. Defaults to {test_output_level.PASSED}.
159
159
  '''
160
160
  )
161
- @click.option('--public-directory-path', help='Path to public files. Used for mocking requests.')
161
+ @click.option('--public-directory-path', multiple=True, help='Path to public files. Used for mocking requests. Can take the form <FOLDER-PATH>[:<ORIGIN>].')
162
162
  @ConditionalDecorator(lambda f: click.option('--remote-project-key', help='Use remote project for endpoint definitions.')(f), is_remote)
163
163
  @ConditionalDecorator(lambda f: click.option('--report-key', help='Save results to report.')(f), is_remote)
164
- @click.option('--response-fixtures-path', help='Path to response fixtures yaml. Used for mocking requests.')
164
+ @click.option('--response-fixtures-path', multiple=True, help='Path to response fixtures yaml. Used for mocking requests. Can take the form <FILE-PATH>[:<ORIGIN>].')
165
165
  @ConditionalDecorator(lambda f: click.option('--save', is_flag=True, default=False, help='Save results.')(f), is_remote)
166
166
  @click.option('--scheme', help='Rewrite request scheme.')
167
167
  @click.option(
@@ -15,10 +15,10 @@ class TestOptions(TypedDict):
15
15
  lifecycle_hooks_path: str
16
16
  log_level: logger.LogLevel
17
17
  output_level: test_output_level.TestOutputLevel
18
- public_directory_path: str
18
+ public_directory_path: str # Comma-separated list of paths, optionally with origin suffix (e.g., "/path/to/files:example.com,/other/path")
19
19
  remote_project_key: str
20
20
  report_key: str
21
- response_fixtures_path: str
21
+ response_fixtures_path: str # Comma-separated list of paths, optionally with origin suffix (e.g., "/path/to/fixtures.yml:example.com,/other/fixtures.yml")
22
22
  save: str
23
23
  scheme: str
24
24
  strategy: test_strategy.TestStrategy
@@ -1,4 +1,4 @@
1
- from typing import TypedDict
1
+ from typing import TypedDict, List, Callable, Optional
2
2
 
3
3
  class ComposeOptions(TypedDict):
4
4
  namespace: str
@@ -14,5 +14,54 @@ class DownOptions(ComposeOptions):
14
14
 
15
15
  class UpOptions(ComposeOptions):
16
16
  attached: bool
17
- extra_compose_path: str
18
- pull: bool
17
+ pull: bool
18
+
19
+ # Workflow-specific option types
20
+ class WorkflowDownOptions(TypedDict, total=False):
21
+ print_service_header: Optional[Callable[[str], None]]
22
+ extra_entrypoint_compose_path: Optional[str]
23
+ namespace: Optional[str]
24
+ rmi: bool
25
+ user_id: Optional[int]
26
+ # CLI-specific options that get passed through
27
+ containerized: bool
28
+ dry_run: bool
29
+ log_level: str
30
+ script_path: Optional[str]
31
+ service: List[str]
32
+ app_dir_path: Optional[str]
33
+ context_dir_path: Optional[str]
34
+
35
+ class WorkflowUpOptions(TypedDict, total=False):
36
+ print_service_header: Optional[Callable[[str], None]]
37
+ extra_entrypoint_compose_path: Optional[str]
38
+ namespace: Optional[str]
39
+ pull: bool
40
+ user_id: Optional[int]
41
+ detached: bool
42
+ # CLI-specific options that get passed through
43
+ containerized: bool
44
+ dry_run: bool
45
+ log_level: str
46
+ script_path: Optional[str]
47
+ service: List[str]
48
+ no_publish: bool
49
+ verbose: bool
50
+ mkcert: bool
51
+ app_dir_path: Optional[str]
52
+ ca_certs_dir_path: Optional[str]
53
+ certs_dir_path: Optional[str]
54
+ context_dir_path: Optional[str]
55
+
56
+ class WorkflowLogsOptions(TypedDict, total=False):
57
+ print_service_header: Optional[Callable[[str], None]]
58
+ container: List[str]
59
+ follow: bool
60
+ namespace: Optional[str]
61
+ # CLI-specific options that get passed through
62
+ containerized: bool
63
+ dry_run: bool
64
+ log_level: str
65
+ script_path: Optional[str]
66
+ service: List[str]
67
+ app_dir_path: Optional[str]
@@ -127,7 +127,7 @@ def eval_request_with_retry(context: MockContext, eval_request, **options: MockO
127
127
  fixture = eval_fixtures(
128
128
  request,
129
129
  public_directory_path=intercept_settings.public_directory_path,
130
- response_fixtures=intercept_settings.response_fixtures
130
+ response_fixtures_path=intercept_settings.response_fixtures_path
131
131
  )
132
132
  if fixture:
133
133
  res = fixture
@@ -38,9 +38,6 @@ class InterceptSettings:
38
38
  self.__lifecycle_hooks = None
39
39
  self.__initialize_lifecycle_hooks()
40
40
 
41
- self.__response_fixtures = None
42
- self.__initialize_response_fixtures()
43
-
44
41
  self._mock_rewrite_rules = None
45
42
  self._record_rewrite_rules = None
46
43
  self._replay_rewrite_rules = None
@@ -110,11 +107,12 @@ class InterceptSettings:
110
107
 
111
108
  @property
112
109
  def public_directory_path(self):
110
+ """Get raw public directory paths string from environment or headers."""
113
111
  if self.__headers and custom_headers.PUBLIC_DIRECTORY_PATH in self.__headers:
114
112
  return self.__headers[custom_headers.PUBLIC_DIRECTORY_PATH]
115
-
116
- if os.environ.get(env_vars.AGENT_PUBLIC_DIRECTORY_PATH):
117
- return os.environ[env_vars.AGENT_PUBLIC_DIRECTORY_PATH]
113
+ elif os.environ.get(env_vars.AGENT_PUBLIC_DIRECTORY_PATH):
114
+ return os.environ[env_vars.AGENT_PUBLIC_DIRECTORY_PATH]
115
+ return None
118
116
 
119
117
  @property
120
118
  def remote_project_key(self):
@@ -130,16 +128,13 @@ class InterceptSettings:
130
128
 
131
129
  @property
132
130
  def response_fixtures_path(self):
131
+ """Returns comma-separated list of response fixtures paths, optionally with origin prefix."""
133
132
  if self.__headers and custom_headers.RESPONSE_FIXTURES_PATH in self.__headers:
134
133
  return self.__headers[custom_headers.RESPONSE_FIXTURES_PATH]
135
134
 
136
135
  if os.environ.get(env_vars.AGENT_RESPONSE_FIXTURES_PATH):
137
136
  return os.environ[env_vars.AGENT_RESPONSE_FIXTURES_PATH]
138
137
 
139
- @property
140
- def response_fixtures(self):
141
- return self.__response_fixtures or {}
142
-
143
138
  @property
144
139
  def parsed_remote_project_key(self):
145
140
  try:
@@ -333,21 +328,6 @@ class InterceptSettings:
333
328
  except Exception as e:
334
329
  return Logger.instance().error(e)
335
330
 
336
- def __initialize_response_fixtures(self):
337
- fixtures_path = self.response_fixtures_path
338
-
339
- if not fixtures_path:
340
- return
341
-
342
- if not os.path.exists(fixtures_path):
343
- return Logger.instance().error(f"Response fixtures {fixtures_path} does not exist")
344
-
345
- with open(fixtures_path, 'r') as stream:
346
- try:
347
- self.__response_fixtures = yaml.safe_load(stream)
348
- except yaml.YAMLError as exc:
349
- Logger.instance().error(exc)
350
-
351
331
  def __order(self, mode):
352
332
  if mode == intercept_mode.RECORD:
353
333
  return self.__data_rules.record_order