stoobly-agent 1.2.3__py3-none-any.whl → 1.4.0__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 (102) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/application_http_request_handler.py +3 -3
  3. stoobly_agent/app/api/proxy_controller.py +8 -7
  4. stoobly_agent/app/cli/config_cli.py +1 -1
  5. stoobly_agent/app/cli/helpers/certificate_authority.py +7 -6
  6. stoobly_agent/app/cli/helpers/print_service.py +17 -0
  7. stoobly_agent/app/cli/scaffold/app.py +16 -34
  8. stoobly_agent/app/cli/scaffold/app_command.py +4 -7
  9. stoobly_agent/app/cli/scaffold/app_config.py +15 -2
  10. stoobly_agent/app/cli/scaffold/app_create_command.py +18 -2
  11. stoobly_agent/app/cli/scaffold/command.py +1 -1
  12. stoobly_agent/app/cli/scaffold/constants.py +9 -5
  13. stoobly_agent/app/cli/scaffold/docker/app_builder.py +3 -7
  14. stoobly_agent/app/cli/scaffold/docker/constants.py +0 -1
  15. stoobly_agent/app/cli/scaffold/docker/service/builder.py +12 -11
  16. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +14 -31
  17. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +6 -2
  18. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +6 -2
  19. stoobly_agent/app/cli/scaffold/hosts_file_manager.py +112 -0
  20. stoobly_agent/app/cli/scaffold/service.py +1 -2
  21. stoobly_agent/app/cli/scaffold/service_command.py +1 -1
  22. stoobly_agent/app/cli/scaffold/service_config.py +10 -14
  23. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +9 -11
  24. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +2 -4
  25. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +108 -68
  26. stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.base.yml +8 -13
  27. stoobly_agent/app/cli/scaffold/templates/app/Makefile +1 -1
  28. stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +8 -4
  29. stoobly_agent/app/cli/scaffold/templates/app/build/mock/.docker-compose.mock.yml +2 -6
  30. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/.configure +3 -0
  31. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/.init +3 -0
  32. stoobly_agent/app/cli/scaffold/templates/app/build/record/.docker-compose.record.yml +2 -6
  33. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/.configure +3 -0
  34. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/.init +3 -0
  35. stoobly_agent/app/cli/scaffold/templates/app/build/test/.docker-compose.test.yml +2 -6
  36. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/.configure +3 -0
  37. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/.init +3 -0
  38. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +2 -0
  39. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/.docker-compose.mock.yml +2 -8
  40. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/.docker-compose.record.yml +2 -8
  41. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/.docker-compose.test.yml +2 -8
  42. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/.docker-compose.exec.yml +2 -3
  43. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.logs +1 -0
  44. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.services +9 -0
  45. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +1 -2
  46. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +1 -2
  47. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/.configure +3 -0
  48. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/.init +7 -1
  49. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/.configure +3 -0
  50. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/.init +7 -1
  51. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/.configure +3 -0
  52. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/.init +7 -1
  53. stoobly_agent/app/cli/scaffold/validate_command.py +2 -2
  54. stoobly_agent/app/cli/scaffold/workflow.py +5 -4
  55. stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
  56. stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
  57. stoobly_agent/app/cli/scaffold/workflow_run_command.py +78 -45
  58. stoobly_agent/app/cli/scaffold_cli.py +246 -109
  59. stoobly_agent/app/cli/snapshot_cli.py +7 -3
  60. stoobly_agent/app/models/adapters/joined_request_adapter.py +6 -0
  61. stoobly_agent/app/models/factories/resource/local_db/helpers/scenario_snapshot.py +3 -1
  62. stoobly_agent/app/models/helpers/apply.py +34 -17
  63. stoobly_agent/app/models/helpers/create_request_params_service.py +4 -0
  64. stoobly_agent/app/proxy/handle_mock_service.py +2 -0
  65. stoobly_agent/app/proxy/handle_replay_service.py +2 -0
  66. stoobly_agent/app/proxy/mitmproxy/request_facade.py +1 -1
  67. stoobly_agent/app/proxy/mitmproxy/response_body_facade.py +19 -0
  68. stoobly_agent/app/proxy/mitmproxy/response_facade.py +90 -18
  69. stoobly_agent/app/proxy/record/join_request_service.py +1 -1
  70. stoobly_agent/app/proxy/replay/body_parser_service.py +11 -3
  71. stoobly_agent/app/settings/constants/request_component.py +2 -1
  72. stoobly_agent/config/constants/custom_headers.py +13 -13
  73. stoobly_agent/config/constants/headers.py +0 -2
  74. stoobly_agent/config/data_dir.py +2 -1
  75. stoobly_agent/config/schema.yml +2 -2
  76. stoobly_agent/public/18-es2015.583f191cc7ad512ee262.js +1 -0
  77. stoobly_agent/public/18-es5.583f191cc7ad512ee262.js +1 -0
  78. stoobly_agent/public/35-es2015.8f79ff8748d4ff06ab03.js +1 -0
  79. stoobly_agent/public/35-es5.8f79ff8748d4ff06ab03.js +1 -0
  80. stoobly_agent/public/index.html +1 -1
  81. stoobly_agent/public/main-es2015.2cc16523aa3fcaba51e5.js +1 -0
  82. stoobly_agent/public/main-es5.2cc16523aa3fcaba51e5.js +1 -0
  83. stoobly_agent/public/{runtime-es2015.9addf49b79aca951b7e2.js → runtime-es2015.b914470164e4d6e75d96.js} +1 -1
  84. stoobly_agent/public/{runtime-es5.9addf49b79aca951b7e2.js → runtime-es5.b914470164e4d6e75d96.js} +1 -1
  85. stoobly_agent/test/app/cli/scaffold/cli_invoker.py +1 -2
  86. stoobly_agent/test/app/cli/scaffold/{hosts_file_reader_test.py → hosts_file_manager_test.py} +20 -20
  87. stoobly_agent/test/app/cli/snapshot/snapshot_apply_test.py +162 -1
  88. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  89. stoobly_agent/test/mock_data/scaffold/docker-compose-assets-service.yml +1 -3
  90. {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.4.0.dist-info}/METADATA +1 -1
  91. {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.4.0.dist-info}/RECORD +94 -93
  92. stoobly_agent/app/cli/scaffold/hosts_file_reader.py +0 -65
  93. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.proxy +0 -34
  94. stoobly_agent/public/18-es2015.d3b430636a4d6f544d92.js +0 -1
  95. stoobly_agent/public/18-es5.d3b430636a4d6f544d92.js +0 -1
  96. stoobly_agent/public/35-es2015.f741ebce0bfc25f0ec99.js +0 -1
  97. stoobly_agent/public/35-es5.f741ebce0bfc25f0ec99.js +0 -1
  98. stoobly_agent/public/main-es2015.ccd46ac1b6638ddf2066.js +0 -1
  99. stoobly_agent/public/main-es5.ccd46ac1b6638ddf2066.js +0 -1
  100. {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.4.0.dist-info}/LICENSE +0 -0
  101. {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.4.0.dist-info}/WHEEL +0 -0
  102. {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.4.0.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  import click
2
+ import errno
2
3
  import os
3
4
  import pdb
4
5
  import sys
@@ -14,6 +15,7 @@ from stoobly_agent.app.cli.scaffold.constants import (
14
15
  )
15
16
  from stoobly_agent.app.cli.scaffold.docker.service.set_gateway_ports import set_gateway_ports
16
17
  from stoobly_agent.app.cli.scaffold.docker.workflow.decorators_factory import get_workflow_decorators
18
+ from stoobly_agent.app.cli.scaffold.hosts_file_manager import HostsFileManager
17
19
  from stoobly_agent.app.cli.scaffold.service import Service
18
20
  from stoobly_agent.app.cli.scaffold.service_config import ServiceConfig
19
21
  from stoobly_agent.app.cli.scaffold.service_create_command import ServiceCreateCommand
@@ -31,8 +33,13 @@ from stoobly_agent.config.constants import env_vars
31
33
  from stoobly_agent.config.data_dir import DataDir
32
34
  from stoobly_agent.lib.logger import bcolors, DEBUG, ERROR, INFO, Logger, WARNING
33
35
 
36
+ from .helpers.print_service import FORMATS, print_services, select_print_options
37
+
34
38
  LOG_ID = 'Scaffold'
35
39
 
40
+ current_working_dir = os.getcwd()
41
+ data_dir: DataDir = DataDir.instance()
42
+
36
43
  @click.group(
37
44
  epilog="Run 'stoobly-agent project COMMAND --help' for more information on a command.",
38
45
  help="Manage scaffold"
@@ -57,42 +64,53 @@ def app(ctx):
57
64
  def service(ctx):
58
65
  pass
59
66
 
67
+ @click.group(
68
+ epilog="Run 'stoobly-agent request response COMMAND --help' for more information on a command.",
69
+ help="Manage workflow scaffold"
70
+ )
71
+ @click.pass_context
72
+ def workflow(ctx):
73
+ pass
74
+
75
+ @click.group(
76
+ epilog="Run 'stoobly-agent scaffold hostname COMMAND --help' for more information on a command.",
77
+ help="Manage scaffold service hostnames"
78
+ )
79
+ @click.pass_context
80
+ def hostname(ctx):
81
+ pass
82
+
60
83
  @app.command(
61
84
  help="Scaffold application"
62
85
  )
63
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to create the app scaffold.')
86
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to create the app scaffold.')
64
87
  @click.option('--force', is_flag=True, help='Overwrite maintained scaffolded app files.')
88
+ @click.option('--network', help='App default network name. Defaults to app name.')
65
89
  @click.argument('app_name')
66
90
  def create(**kwargs):
67
91
  __validate_app_dir(kwargs['app_dir_path'])
68
92
 
69
93
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
70
94
 
71
- if kwargs['force'] or not os.path.exists(app.namespace_path):
72
- __app_build(app, **kwargs)
95
+ if kwargs['force'] or not os.path.exists(app.scaffold_namespace_path):
96
+ if not kwargs['network']:
97
+ kwargs['network'] = kwargs['app_name']
98
+
99
+ AppCreateCommand(app, **kwargs).build()
73
100
  else:
74
101
  print(f"{kwargs['app_dir_path']} already exists, use option '--force' to continue ")
75
102
 
76
103
  @app.command(
77
104
  help="Scaffold app service certs"
78
105
  )
79
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
80
- @click.option('--ca-certs-dir-path', default=DataDir.instance().mitmproxy_conf_dir_path, help='Path to ca certs directory used to sign SSL certs. Defaults to ~/.mitmproxy')
106
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
107
+ @click.option('--ca-certs-dir-path', default=data_dir.mitmproxy_conf_dir_path, help='Path to ca certs directory used to sign SSL certs. Defaults to ~/.mitmproxy')
81
108
  @click.option('--certs-dir-path', help='Path to certs directory. Defaults to the certs dir of the context.')
82
- @click.option('--context-dir-path', default=DataDir.instance().context_dir_path, help='Path to Stoobly data directory.')
109
+ @click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
83
110
  @click.option('--service', multiple=True, help='Select which services to run. Defaults to all.')
84
111
  def mkcert(**kwargs):
85
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, ca_certs_dir_path=kwargs['ca_certs_dir_path'])
86
-
87
- if kwargs['certs_dir_path']:
88
- app.certs_dir_path = kwargs['certs_dir_path']
89
-
90
- if kwargs['context_dir_path']:
91
- app.context_dir_path = kwargs['context_dir_path']
92
-
93
- if not app.exists:
94
- print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
95
- sys.exit(1)
112
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
113
+ __validate_app(app)
96
114
 
97
115
  services = __get_services(app.services, service=kwargs['service'])
98
116
 
@@ -116,7 +134,7 @@ def mkcert(**kwargs):
116
134
  @service.command(
117
135
  help="Scaffold a service",
118
136
  )
119
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
137
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
120
138
  @click.option('--detached', is_flag=True)
121
139
  @click.option('--env', multiple=True, help='Specify an environment variable.')
122
140
  @click.option('--force', is_flag=True, help='Overwrite maintained scaffolded service files.')
@@ -144,9 +162,34 @@ def create(**kwargs):
144
162
  print(f"{service.dir_path} already exists, use option '--force' to continue")
145
163
 
146
164
  @service.command(
147
- help="Delete a service",
165
+ help="List services",
166
+ name="list"
148
167
  )
149
168
  @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
169
+ @click.option('--format', type=click.Choice(FORMATS), help='Format output.')
170
+ @click.option('--select', multiple=True, help='Select column(s) to display.')
171
+ @click.option('--service', multiple=True, help='Select specific services.')
172
+ @click.option('--without-headers', is_flag=True, default=False, help='Disable printing column headers.')
173
+ @click.option('--workflow', multiple=True, help='Specify workflow(s) to filter the services by.')
174
+ def _list(**kwargs):
175
+ __validate_app_dir(kwargs['app_dir_path'])
176
+
177
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
178
+ __validate_app(app)
179
+
180
+ rows = []
181
+ for service_name in __get_workflow_services(app, **kwargs):
182
+ service = Service(service_name, app)
183
+ __validate_service_dir(service.dir_path)
184
+ service_config = ServiceConfig(service.dir_path)
185
+ rows.append(service_config.to_dict())
186
+
187
+ print_services(rows, **select_print_options(kwargs))
188
+
189
+ @service.command(
190
+ help="Delete a service",
191
+ )
192
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
150
193
  @click.argument('service_name')
151
194
  def delete(**kwargs):
152
195
  __validate_app_dir(kwargs['app_dir_path'])
@@ -164,7 +207,7 @@ def delete(**kwargs):
164
207
  @service.command(
165
208
  help="Update a service config"
166
209
  )
167
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
210
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
168
211
  @click.option('--priority', help='Determines the service run order.')
169
212
  @click.argument('service_name')
170
213
  def update(**kwargs):
@@ -182,18 +225,11 @@ def update(**kwargs):
182
225
 
183
226
  service_config.write()
184
227
 
185
- @click.group(
186
- epilog="Run 'stoobly-agent request response COMMAND --help' for more information on a command.",
187
- help="Manage service scaffold"
188
- )
189
- @click.pass_context
190
- def workflow(ctx):
191
- pass
192
-
193
228
  @workflow.command(
194
229
  help="Create workflow for service(s)"
195
230
  )
196
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
231
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
232
+ @click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
197
233
  @click.option('--env', multiple=True, help='Specify an environment variable.')
198
234
  @click.option('--force', is_flag=True, help='Overwrite maintained scaffolded workflow files.')
199
235
  @click.option('--service', multiple=True, help='Specify the service(s) to create the workflow for.')
@@ -202,7 +238,7 @@ def workflow(ctx):
202
238
  def create(**kwargs):
203
239
  __validate_app_dir(kwargs['app_dir_path'])
204
240
 
205
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
241
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
206
242
 
207
243
  for service_name in kwargs['service']:
208
244
  config = { **kwargs }
@@ -214,19 +250,20 @@ def create(**kwargs):
214
250
 
215
251
  workflow_dir_path = service.workflow_dir_path(kwargs['workflow_name'])
216
252
  if kwargs['force'] or not os.path.exists(workflow_dir_path):
217
- __workflow_build(app, **config)
253
+ __workflow_create(app, **config)
218
254
  else:
219
255
  print(f"{workflow_dir_path} already exists, use option '--force' to continue")
220
256
 
221
257
  @workflow.command(
222
258
  help="Copy a workflow for service(s)",
223
259
  )
224
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
260
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
261
+ @click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
225
262
  @click.option('--service', multiple=True, help='Specify service(s) to add the workflow to.')
226
263
  @click.argument('workflow_name')
227
264
  @click.argument('destination_workflow_name')
228
265
  def copy(**kwargs):
229
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
266
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
230
267
 
231
268
  for service_name in kwargs['service']:
232
269
  config = { **kwargs }
@@ -242,31 +279,30 @@ def copy(**kwargs):
242
279
  command.copy(kwargs['destination_workflow_name'])
243
280
 
244
281
  @workflow.command()
245
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
246
- @click.option('--context-dir-path', default=DataDir.instance().context_dir_path, help='Path to Stoobly data directory.')
282
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
283
+ @click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
247
284
  @click.option('--dry-run', default=False, is_flag=True)
285
+ @click.option('--extra-entrypoint-compose-path', help='Path to extra entrypoint compose file.')
248
286
  @click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
249
287
  Log levels can be "debug", "info", "warning", or "error"
250
288
  ''')
251
289
  @click.option('--namespace', help='Workflow namespace.')
252
- @click.option('--network', help='Workflow network namespace.')
290
+ @click.option('--network', help='Workflow network name.')
253
291
  @click.option('--rmi', is_flag=True, help='Remove images used by containers.')
254
292
  @click.option('--service', multiple=True, help='Select which services to log. Defaults to all.')
293
+ @click.option('--user-id', default=os.getuid(), help='OS user ID of the owner of context dir path.')
255
294
  @click.argument('workflow_name')
256
295
  def down(**kwargs):
257
296
  cwd = os.getcwd()
258
297
 
259
- if not os.getenv(env_vars.LOG_LEVEL):
260
- os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
298
+ os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
261
299
 
262
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, skip_validate_path=True)
300
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
301
+ __validate_app(app)
263
302
 
264
- if kwargs['context_dir_path']:
265
- app.context_dir_path = kwargs['context_dir_path']
266
-
267
- if not app.exists:
268
- print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
269
- sys.exit(1)
303
+ # If namespace is set, default network to namespace
304
+ if kwargs['namespace'] and not kwargs['network']:
305
+ kwargs['network'] = kwargs['namespace']
270
306
 
271
307
  workflow = Workflow(kwargs['workflow_name'], app)
272
308
  services = __get_services(workflow.services, service=kwargs['service'])
@@ -280,10 +316,22 @@ def down(**kwargs):
280
316
  commands.append(command)
281
317
 
282
318
  commands = sorted(commands, key=lambda command: command.service_config.priority)
283
- for command in commands:
319
+ for index, command in enumerate(commands):
284
320
  __print_header(f"SERVICE {command.service_name}")
285
321
 
286
- exec_command = command.down(namespace=kwargs['namespace'], rmi=kwargs['rmi'])
322
+ extra_compose_path = None
323
+
324
+ # By default, the entrypoint service should be last
325
+ # However, this can change if the user has configured a service's priority to be higher
326
+ if index == len(commands) - 1:
327
+ extra_compose_path = kwargs['extra_entrypoint_compose_path']
328
+
329
+ exec_command = command.down(
330
+ extra_compose_path=extra_compose_path,
331
+ namespace=kwargs['namespace'],
332
+ rmi=kwargs['rmi'],
333
+ user_id=kwargs['user_id']
334
+ )
287
335
  if not exec_command:
288
336
  continue
289
337
 
@@ -295,34 +343,41 @@ def down(**kwargs):
295
343
  # After services are stopped, their network needs to be removed
296
344
  if len(commands) > 0:
297
345
  command: WorkflowRunCommand = commands[0]
298
- remove_network_command = command.remove_network()
299
346
 
300
- if not kwargs['dry_run']:
301
- command.write_nameservers()
347
+ if kwargs['rmi']:
348
+ remove_image_command = command.remove_image(kwargs['user_id'])
349
+ if not kwargs['dry_run']:
350
+ exec_stream(remove_image_command)
351
+ else:
352
+ print(remove_image_command)
302
353
 
354
+ remove_network_command = command.remove_network()
355
+ if not kwargs['dry_run']:
303
356
  exec_stream(remove_network_command)
304
357
  else:
305
358
  print(remove_network_command)
306
359
 
307
360
  @workflow.command()
308
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
361
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
309
362
  @click.option(
310
363
  '--container', multiple=True, help=f"Select which containers to log. Defaults to '{WORKFLOW_CONTAINER_PROXY}'"
311
364
  )
312
365
  @click.option('--dry-run', default=False, is_flag=True, help='If set, prints commands.')
313
366
  @click.option('--follow', is_flag=True, help='Follow last container log output.')
367
+ @click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
368
+ Log levels can be "debug", "info", "warning", or "error"
369
+ ''')
314
370
  @click.option('--namespace', help='Workflow namespace.')
315
371
  @click.option('--service', multiple=True, help='Select which services to log. Defaults to all.')
316
372
  @click.argument('workflow_name')
317
373
  def logs(**kwargs):
318
- if len(kwargs['container']) == 0:
319
- kwargs['container'] = [WORKFLOW_CONTAINER_PROXY]
374
+ os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
320
375
 
321
376
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
377
+ __validate_app(app)
322
378
 
323
- if not app.exists:
324
- print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
325
- sys.exit(1)
379
+ if len(kwargs['container']) == 0:
380
+ kwargs['container'] = [WORKFLOW_CONTAINER_PROXY]
326
381
 
327
382
  workflow = Workflow(kwargs['workflow_name'], app)
328
383
  services = __get_services(workflow.services, service=kwargs['service'], without_core=True)
@@ -357,47 +412,38 @@ def logs(**kwargs):
357
412
 
358
413
  if not kwargs['dry_run']:
359
414
  exec_stream(shell_command)
360
-
415
+
361
416
  @workflow.command()
362
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
363
- @click.option('--ca-certs-dir-path', default=DataDir.instance().mitmproxy_conf_dir_path, help='Path to ca certs directory used to sign SSL certs. Defaults to ~/.mitmproxy')
417
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
418
+ @click.option('--build', is_flag=True, help='Build images before starting containers.')
419
+ @click.option('--ca-certs-dir-path', default=data_dir.mitmproxy_conf_dir_path, help='Path to ca certs directory used to sign SSL certs. Defaults to ~/.mitmproxy')
364
420
  @click.option('--certs-dir-path', help='Path to certs directory. Defaults to the certs dir of the context.')
365
- @click.option('--context-dir-path', default=DataDir.instance().context_dir_path, help='Path to Stoobly data directory.')
421
+ @click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
366
422
  @click.option('--detached', is_flag=True, help='If set, will not run the highest priority service in the foreground.')
367
423
  @click.option('--dry-run', default=False, is_flag=True, help='If set, prints commands.')
368
- @click.option('--extra-compose-path', help='Path to extra compose configuration files.')
424
+ @click.option('--extra-entrypoint-compose-path', help='Path to extra entrypoint compose file.')
425
+ @click.option('--from-make', is_flag=True, help='Set if run from scaffolded Makefile.')
369
426
  @click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
370
427
  Log levels can be "debug", "info", "warning", or "error"
371
428
  ''')
372
429
  @click.option('--namespace', help='Workflow namespace.')
373
- @click.option('--network', help='Workflow network namespace.')
374
- @click.option('--no-cache', is_flag=True, help='Do not use cache when building images.')
430
+ @click.option('--network', help='Workflow network name.')
431
+ @click.option('--pull', is_flag=True, help='Pull image before running.')
375
432
  @click.option('--service', multiple=True, help='Select which services to run. Defaults to all.')
433
+ @click.option('--user-id', default=os.getuid(), help='OS user ID of the owner of context dir path.')
376
434
  @click.option('--verbose', is_flag=True)
377
435
  @click.argument('workflow_name')
378
436
  def up(**kwargs):
379
437
  cwd = os.getcwd()
380
438
 
381
- # Create the certs_dir_path if it doesn't exist
382
- DataDir.instance().certs_dir_path
439
+ os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
383
440
 
384
- if not os.getenv(env_vars.LOG_LEVEL):
385
- os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
441
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
442
+ __validate_app(app)
386
443
 
387
- app = App(
388
- kwargs['app_dir_path'], DOCKER_NAMESPACE,
389
- ca_certs_dir_path=kwargs['ca_certs_dir_path'], skip_validate_path=kwargs['dry_run']
390
- )
391
-
392
- if kwargs['certs_dir_path']:
393
- app.certs_dir_path = kwargs['certs_dir_path']
394
-
395
- if kwargs['context_dir_path']:
396
- app.context_dir_path = kwargs['context_dir_path']
397
-
398
- if not app.exists:
399
- print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
400
- sys.exit(1)
444
+ # If namespace is set, default network to namespace
445
+ if kwargs['namespace'] and not kwargs['network']:
446
+ kwargs['network'] = kwargs['namespace']
401
447
 
402
448
  workflow = Workflow(kwargs['workflow_name'], app)
403
449
  services = __get_services(workflow.services, service=kwargs['service'])
@@ -413,27 +459,44 @@ def up(**kwargs):
413
459
  command.current_working_dir = cwd
414
460
  commands.append(command)
415
461
 
416
- # Before services can be started, their network needs to be created
462
+ # Before services can be started, their image and network needs to be created
417
463
  if len(commands) > 0:
418
464
  command: WorkflowRunCommand = commands[0]
419
- create_network_command = command.create_network()
465
+
466
+ init_commands = []
467
+ if not kwargs['from_make']:
468
+ create_image_command = command.create_image(user_id=kwargs['user_id'], verbose=kwargs['verbose'])
469
+ init_commands.append(create_image_command)
470
+
471
+ init_commands.append(command.create_network())
472
+ joined_command = ' && '.join(init_commands)
420
473
 
421
474
  if not kwargs['dry_run']:
422
475
  command.write_nameservers()
423
-
424
- exec_stream(create_network_command)
476
+ exec_stream(joined_command)
425
477
  else:
426
- print(create_network_command)
478
+ print(joined_command)
427
479
 
428
480
  commands = sorted(commands, key=lambda command: command.service_config.priority)
429
481
  for index, command in enumerate(commands):
430
482
  __print_header(f"SERVICE {command.service_name}")
431
483
 
484
+ attached = False
485
+ extra_compose_path = None
486
+
432
487
  # By default, the entrypoint service should be last
433
488
  # However, this can change if the user has configured a service's priority to be higher
434
- attached = not kwargs['detached'] and index == len(commands) - 1
489
+ if index == len(commands) - 1:
490
+ attached = not kwargs['detached']
491
+ extra_compose_path = kwargs['extra_entrypoint_compose_path']
492
+
435
493
  exec_command = command.up(
436
- attached=attached, namespace=kwargs['namespace'], no_cache=kwargs['no_cache'], verbose=kwargs['verbose']
494
+ attached=attached,
495
+ build=kwargs['build'],
496
+ extra_compose_path=extra_compose_path,
497
+ namespace=kwargs['namespace'],
498
+ pull=kwargs['pull'],
499
+ user_id=kwargs['user_id']
437
500
  )
438
501
  if not exec_command:
439
502
  continue
@@ -446,10 +509,12 @@ def up(**kwargs):
446
509
  @workflow.command(
447
510
  help="Validate a scaffold workflow"
448
511
  )
449
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to validate the app scaffold.')
512
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to validate the app scaffold.')
450
513
  @click.argument('workflow_name')
451
514
  def validate(**kwargs):
452
515
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
516
+ __validate_app(app)
517
+
453
518
  workflow = Workflow(kwargs['workflow_name'], app)
454
519
 
455
520
  config = { **kwargs }
@@ -472,35 +537,103 @@ def validate(**kwargs):
472
537
  print(f"\nFatal Scaffold Validation Exception: {sve}", file=sys.stderr)
473
538
  sys.exit(1)
474
539
 
540
+ @hostname.command(
541
+ help="Update the system hosts file for all scaffold service hostnames"
542
+ )
543
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
544
+ @click.option('--service', multiple=True, help='Select specific services.')
545
+ @click.option('--workflow', multiple=True, help='Specify services by workflow(s).')
546
+ def install(**kwargs):
547
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
548
+ __validate_app(app)
549
+
550
+ services = __get_workflow_services(app, **kwargs)
551
+
552
+ hostnames = []
553
+ for service_name in services:
554
+ service = Service(service_name, app)
555
+ __validate_service_dir(service.dir_path)
556
+
557
+ service_config = ServiceConfig(service.dir_path)
558
+ if service_config.hostname:
559
+ hostnames.append(service_config.hostname)
560
+
561
+ __elevate_sudo()
562
+
563
+ try:
564
+ hosts_file_manager = HostsFileManager()
565
+ hosts_file_manager.install_hostnames(hostnames)
566
+ except PermissionError:
567
+ print("Permission denied. Please run this command with sudo.", file=sys.stderr)
568
+
569
+ @hostname.command(
570
+ help="Delete from the system hosts file all scaffold service hostnames"
571
+ )
572
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
573
+ def uninstall(**kwargs):
574
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
575
+ __validate_app(app)
576
+
577
+ __elevate_sudo()
578
+
579
+ try:
580
+ hosts_file_manager = HostsFileManager()
581
+ hosts_file_manager.uninstall_hostnames()
582
+ except OSError as e:
583
+ if e.errno == errno.EACCES or e.errno == errno.EPERM:
584
+ print("Permission denied. Please run this command with sudo.", file=sys.stderr)
585
+ else:
586
+ print(f"An unexpected error occurred: {e}", file=sys.stderr)
587
+
475
588
  scaffold.add_command(app)
476
589
  scaffold.add_command(service)
477
590
  scaffold.add_command(workflow)
591
+ scaffold.add_command(hostname)
592
+
593
+ def __elevate_sudo():
594
+ import subprocess
595
+
596
+ if os.geteuid() != 0:
597
+ subprocess.run(["sudo", sys.executable] + sys.argv)
598
+ sys.exit(0)
599
+
600
+ def __get_services(services: List[str], **kwargs) -> List[str]:
601
+ selected_services = list(kwargs['service'])
602
+
603
+ # If service is specified, run only those services
604
+ if selected_services:
605
+ missing_services = [service for service in selected_services if service not in services]
606
+
607
+ # Remove services that don't exist
608
+ if missing_services:
609
+ Logger.instance(LOG_ID).warn(f"Service(s) {','.join(missing_services)} are not found")
610
+ selected_services = list(set(selected_services) - set(missing_services))
611
+
612
+ services = selected_services
478
613
 
479
- def __get_services(services: List[str], **kwargs):
480
- # Log services that don't exist
481
- missing_services = [service for service in kwargs['service'] if service not in services]
482
- if missing_services:
483
- Logger.instance(LOG_ID).warn(f"Service(s) {','.join(missing_services)} are not found")
614
+ services += CORE_SERVICES
484
615
 
485
- if kwargs['service']:
486
- # If service is specified, run only those services
487
- services = list(kwargs['service'])
616
+ services_index = {}
617
+ for service in services:
618
+ if kwargs.get('without_core') and service in CORE_SERVICES:
619
+ continue
620
+ services_index[service] = True
621
+
622
+ return services_index.keys()
488
623
 
489
- if not kwargs.get('without_core'):
490
- services += CORE_SERVICES
624
+ def __get_workflow_services(app: App, **kwargs):
625
+ selected_services = []
626
+ if not kwargs['workflow']:
627
+ selected_services += __get_services(app.services, service=kwargs['service'], without_core=True)
491
628
  else:
492
- # If set, filter out core services
493
- if kwargs.get('without_core'):
494
- services = list(filter(lambda service: service not in CORE_SERVICES, services))
495
-
496
- return list(set(services))
629
+ for workflow_name in kwargs['workflow']:
630
+ workflow = Workflow(workflow_name, app)
631
+ selected_services += __get_services(workflow.services, service=kwargs['service'], without_core=True)
632
+ return set(selected_services)
497
633
 
498
634
  def __print_header(text: str):
499
635
  Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}{text}{bcolors.ENDC}")
500
636
 
501
- def __app_build(app, **kwargs):
502
- AppCreateCommand(app, **kwargs).build()
503
-
504
637
  def __scaffold_build(app, **kwargs):
505
638
  command = ServiceCreateCommand(app, **kwargs)
506
639
 
@@ -511,6 +644,11 @@ def __scaffold_delete(app, **kwargs):
511
644
 
512
645
  command.delete()
513
646
 
647
+ def __validate_app(app: App):
648
+ if not app.valid:
649
+ print(f"Error: {app.dir_path} is not a valid scaffold app", file=sys.stderr)
650
+ sys.exit(1)
651
+
514
652
  def __validate_app_dir(app_dir_path):
515
653
  if not os.path.exists(app_dir_path):
516
654
  print(f"Error: {app_dir_path} does not exist", file=sys.stderr)
@@ -521,13 +659,12 @@ def __validate_service_dir(service_dir_path):
521
659
  print(f"Error: {service_dir_path} does not exist, please scaffold this service", file=sys.stderr)
522
660
  sys.exit(1)
523
661
 
524
- def __workflow_build(app, **kwargs):
662
+ def __workflow_create(app, **kwargs):
525
663
  command = WorkflowCreateCommand(app, **kwargs)
526
664
 
527
665
  service_config = command.service_config
528
666
  workflow_decorators = get_workflow_decorators(kwargs['template'], service_config)
529
667
  command.build(
530
- headless=kwargs['headless'],
531
668
  template=kwargs['template'],
532
669
  workflow_decorators=workflow_decorators
533
670
  )
@@ -36,11 +36,15 @@ def snapshot(ctx):
36
36
  @click.argument('uuid', required=False)
37
37
  def apply(**kwargs):
38
38
  apply = Apply(force=kwargs['force']).with_logger(print)
39
+ completed = True
39
40
 
40
41
  if kwargs.get('uuid'):
41
- apply.single(kwargs['uuid'])
42
+ completed = apply.single(kwargs['uuid'])
42
43
  else:
43
- apply.all()
44
+ completed = apply.all()
45
+
46
+ if not completed:
47
+ sys.exit(1)
44
48
 
45
49
  @snapshot.command(
46
50
  help="Copy snapshots to a different data directory."
@@ -96,7 +100,7 @@ def prune(**kwargs):
96
100
  log.prune(kwargs['dry_run'])
97
101
 
98
102
  @snapshot.command(
99
- help="Update snapshot.",
103
+ help="Update a snapshot.",
100
104
  )
101
105
  @click.option('--format', type=click.Choice(FORMATS), help='Format output.')
102
106
  @click.option('--select', multiple=True, help='Select column(s) to display.')
@@ -18,6 +18,12 @@ class JoinedRequestAdapter():
18
18
  payloads_delimitter = payloads_delimitter.encode()
19
19
 
20
20
  self.__split_joined_request_string = joined_request_string.split(payloads_delimitter)
21
+ if len(self.__split_joined_request_string) != 2:
22
+ self.__split_joined_request_string = joined_request_string.split(payloads_delimitter.replace(b"\n", b"\r\n"))
23
+
24
+ if len(self.__split_joined_request_string) != 2:
25
+ raise ValueError(f"Could not split by {payloads_delimitter}")
26
+
21
27
  self.__raw_request_string = None
22
28
  self.__raw_response_string = None
23
29
 
@@ -34,7 +34,9 @@ class ScenarioSnapshot(Snapshot):
34
34
  try:
35
35
  return json.loads(fp.read())
36
36
  except Exception:
37
- return {}
37
+ return {
38
+ 'name': self.uuid
39
+ }
38
40
 
39
41
  @property
40
42
  def metadata_backup(self):