legate 0.1.0

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 (317) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +345 -0
  4. data/bin/legate +13 -0
  5. data/examples/00_quickstart.rb +51 -0
  6. data/examples/01_simple_agent.rb +105 -0
  7. data/examples/02_multi_tool_agent.rb +140 -0
  8. data/examples/03_custom_tool.rb +93 -0
  9. data/examples/04_agent_instructions.rb +84 -0
  10. data/examples/05_state_and_sessions.rb +91 -0
  11. data/examples/06_callbacks.rb +186 -0
  12. data/examples/07_async_jobs.rb +112 -0
  13. data/examples/08_loop_agent.rb +197 -0
  14. data/examples/09_sequential_workflow.rb +40 -0
  15. data/examples/10_parallel_workflow.rb +34 -0
  16. data/examples/11_agent_delegation.rb +24 -0
  17. data/examples/12_http_client_tool.rb +156 -0
  18. data/examples/13_authentication.rb +220 -0
  19. data/examples/14_mcp_client.rb +154 -0
  20. data/examples/15_mcp_server.rb +79 -0
  21. data/examples/16_webhooks.rb +91 -0
  22. data/examples/README_sequential_agents.md +164 -0
  23. data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
  24. data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
  25. data/examples/advanced/auth/excon_middleware.rb +317 -0
  26. data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
  27. data/examples/advanced/auth/fiber_auth_example.rb +281 -0
  28. data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
  29. data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
  30. data/examples/advanced/auth/oauth2_auth.rb +419 -0
  31. data/examples/advanced/auth/oidc_auth.rb +514 -0
  32. data/examples/advanced/auth/openweather_api.rb +251 -0
  33. data/examples/advanced/auth/openweather_tool.rb +153 -0
  34. data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
  35. data/examples/advanced/auth/service_account.rb +135 -0
  36. data/examples/advanced/auth/test_with_httpbin.rb +202 -0
  37. data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
  38. data/examples/advanced/callback_monitoring.rb +679 -0
  39. data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
  40. data/examples/advanced/mas/loop_workflow.rb +28 -0
  41. data/examples/advanced/mas/mock_planner.rb +77 -0
  42. data/examples/advanced/mas/proper_delegation_example.rb +276 -0
  43. data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
  44. data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
  45. data/examples/advanced/mcp/mcp_server_async.rb +76 -0
  46. data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
  47. data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
  48. data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
  49. data/examples/advanced/random_calculator.rb +104 -0
  50. data/examples/advanced/sleep_agent.rb +153 -0
  51. data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
  52. data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
  53. data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
  54. data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
  55. data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
  56. data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
  57. data/examples/tools/oauth2_example.rb +136 -0
  58. data/examples/tools/sleepy_tool.rb +42 -0
  59. data/lib/legate/activity_log.rb +71 -0
  60. data/lib/legate/agent.rb +959 -0
  61. data/lib/legate/agent_code_generator.rb +185 -0
  62. data/lib/legate/agent_definition.rb +812 -0
  63. data/lib/legate/agentic/decision.rb +49 -0
  64. data/lib/legate/agentic/loop.rb +134 -0
  65. data/lib/legate/agentic.rb +5 -0
  66. data/lib/legate/agents/loop_agent.rb +248 -0
  67. data/lib/legate/agents/parallel_agent.rb +163 -0
  68. data/lib/legate/agents/sequential_agent.rb +190 -0
  69. data/lib/legate/agents.rb +14 -0
  70. data/lib/legate/auth/config.rb +148 -0
  71. data/lib/legate/auth/coordinator.rb +218 -0
  72. data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
  73. data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
  74. data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
  75. data/lib/legate/auth/credential.rb +157 -0
  76. data/lib/legate/auth/encryption.rb +108 -0
  77. data/lib/legate/auth/error.rb +94 -0
  78. data/lib/legate/auth/exchanged_credential.rb +180 -0
  79. data/lib/legate/auth/excon_middleware.rb +285 -0
  80. data/lib/legate/auth/http_client_utils.rb +364 -0
  81. data/lib/legate/auth/manager.rb +531 -0
  82. data/lib/legate/auth/manager_store.rb +394 -0
  83. data/lib/legate/auth/middleware_factory.rb +290 -0
  84. data/lib/legate/auth/runner.rb +279 -0
  85. data/lib/legate/auth/scheme.rb +125 -0
  86. data/lib/legate/auth/schemes/api_key.rb +212 -0
  87. data/lib/legate/auth/schemes/google_service_account.rb +108 -0
  88. data/lib/legate/auth/schemes/http_bearer.rb +98 -0
  89. data/lib/legate/auth/schemes/oauth2.rb +396 -0
  90. data/lib/legate/auth/schemes/openid_connect.rb +346 -0
  91. data/lib/legate/auth/schemes/service_account.rb +388 -0
  92. data/lib/legate/auth/schemes.rb +40 -0
  93. data/lib/legate/auth/token_manager.rb +362 -0
  94. data/lib/legate/auth/token_store.rb +86 -0
  95. data/lib/legate/auth/tool_context_extension.rb +97 -0
  96. data/lib/legate/auth/tool_integration.rb +188 -0
  97. data/lib/legate/auth/url_guard.rb +81 -0
  98. data/lib/legate/auth.rb +453 -0
  99. data/lib/legate/callbacks/callback_context.rb +71 -0
  100. data/lib/legate/cli/agent_commands.rb +950 -0
  101. data/lib/legate/cli/auth_commands.rb +520 -0
  102. data/lib/legate/cli/base_command.rb +24 -0
  103. data/lib/legate/cli/deployment_commands.rb +934 -0
  104. data/lib/legate/cli/output_helper.rb +108 -0
  105. data/lib/legate/cli/session_commands.rb +138 -0
  106. data/lib/legate/cli/skaffold_commands.rb +223 -0
  107. data/lib/legate/cli/tool_commands.rb +261 -0
  108. data/lib/legate/cli/web_commands.rb +182 -0
  109. data/lib/legate/cli.rb +40 -0
  110. data/lib/legate/configuration/webhooks.rb +113 -0
  111. data/lib/legate/configuration.rb +39 -0
  112. data/lib/legate/definition_store.rb +23 -0
  113. data/lib/legate/errors.rb +118 -0
  114. data/lib/legate/event.rb +161 -0
  115. data/lib/legate/gemini_ai_beta_patch.rb +39 -0
  116. data/lib/legate/generators/agent_generator.rb +412 -0
  117. data/lib/legate/generators/code_validator.rb +48 -0
  118. data/lib/legate/generators/legate/install_generator.rb +35 -0
  119. data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
  120. data/lib/legate/generators/legate/templates/initializer.rb +18 -0
  121. data/lib/legate/generators/runtime_tool_loader.rb +76 -0
  122. data/lib/legate/generators/tool_generator.rb +408 -0
  123. data/lib/legate/generators.rb +11 -0
  124. data/lib/legate/global_definition_registry.rb +506 -0
  125. data/lib/legate/global_tool_manager.rb +135 -0
  126. data/lib/legate/llm/adapter.rb +69 -0
  127. data/lib/legate/llm/gemini.rb +172 -0
  128. data/lib/legate/llm/ollama.rb +80 -0
  129. data/lib/legate/llm.rb +34 -0
  130. data/lib/legate/mcp/client.rb +320 -0
  131. data/lib/legate/mcp/connection/sse.rb +292 -0
  132. data/lib/legate/mcp/connection/stdio.rb +273 -0
  133. data/lib/legate/mcp/connection_manager.rb +103 -0
  134. data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
  135. data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
  136. data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
  137. data/lib/legate/mcp/tool_wrapper.rb +138 -0
  138. data/lib/legate/mcp/util/schema_converter.rb +134 -0
  139. data/lib/legate/mcp.rb +23 -0
  140. data/lib/legate/plan_executor.rb +375 -0
  141. data/lib/legate/planner.rb +839 -0
  142. data/lib/legate/rails/railtie.rb +43 -0
  143. data/lib/legate/rails.rb +9 -0
  144. data/lib/legate/redaction.rb +32 -0
  145. data/lib/legate/session.rb +299 -0
  146. data/lib/legate/session_service/active_record.rb +300 -0
  147. data/lib/legate/session_service/base.rb +68 -0
  148. data/lib/legate/session_service/event_broadcast.rb +74 -0
  149. data/lib/legate/session_service/in_memory.rb +188 -0
  150. data/lib/legate/tool/metadata_dsl.rb +122 -0
  151. data/lib/legate/tool.rb +276 -0
  152. data/lib/legate/tool_code_generator.rb +103 -0
  153. data/lib/legate/tool_context.rb +350 -0
  154. data/lib/legate/tool_loader.rb +39 -0
  155. data/lib/legate/tool_registry.rb +73 -0
  156. data/lib/legate/tool_result.rb +61 -0
  157. data/lib/legate/tools/agent_tool.rb +187 -0
  158. data/lib/legate/tools/base/http_client.rb +319 -0
  159. data/lib/legate/tools/base/safe_url.rb +56 -0
  160. data/lib/legate/tools/base_async_job_tool.rb +91 -0
  161. data/lib/legate/tools/calculator.rb +89 -0
  162. data/lib/legate/tools/cat_facts.rb +81 -0
  163. data/lib/legate/tools/check_job_status_tool.rb +48 -0
  164. data/lib/legate/tools/current_time_tool.rb +64 -0
  165. data/lib/legate/tools/echo.rb +43 -0
  166. data/lib/legate/tools/http_request_tool.rb +105 -0
  167. data/lib/legate/tools/random_number_tool.rb +64 -0
  168. data/lib/legate/tools/read_webpage_tool.rb +92 -0
  169. data/lib/legate/tools/sleepy_tool.rb +74 -0
  170. data/lib/legate/tools/webhook_tool.rb +146 -0
  171. data/lib/legate/version.rb +5 -0
  172. data/lib/legate/web/app.rb +984 -0
  173. data/lib/legate/web/public/css/main.css +4980 -0
  174. data/lib/legate/web/public/images/favicon-256.png +0 -0
  175. data/lib/legate/web/public/images/favicon-32.png +0 -0
  176. data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
  177. data/lib/legate/web/public/images/legate-logo-light.png +0 -0
  178. data/lib/legate/web/public/js/legate.js +616 -0
  179. data/lib/legate/web/public/styles/main.scss +4402 -0
  180. data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
  181. data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
  182. data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
  183. data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
  184. data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
  185. data/lib/legate/web/routes/api_routes.rb +56 -0
  186. data/lib/legate/web/routes/authentication_routes.rb +1541 -0
  187. data/lib/legate/web/routes/core_routes.rb +111 -0
  188. data/lib/legate/web/routes/documentation_routes.rb +220 -0
  189. data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
  190. data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
  191. data/lib/legate/web/sass_compiler.rb +73 -0
  192. data/lib/legate/web/views/_active_session_info.slim +25 -0
  193. data/lib/legate/web/views/_activity_list.slim +55 -0
  194. data/lib/legate/web/views/_agent_card.slim +56 -0
  195. data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
  196. data/lib/legate/web/views/_agent_status_controls.slim +71 -0
  197. data/lib/legate/web/views/_agent_tool_table.slim +74 -0
  198. data/lib/legate/web/views/_chat_message.slim +95 -0
  199. data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
  200. data/lib/legate/web/views/_display_agent_description.slim +11 -0
  201. data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
  202. data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
  203. data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
  204. data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
  205. data/lib/legate/web/views/_display_agent_model.slim +17 -0
  206. data/lib/legate/web/views/_display_agent_name.slim +42 -0
  207. data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
  208. data/lib/legate/web/views/_display_agent_type.slim +65 -0
  209. data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
  210. data/lib/legate/web/views/_edit_agent_description.slim +16 -0
  211. data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
  212. data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
  213. data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
  214. data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
  215. data/lib/legate/web/views/_edit_agent_model.slim +23 -0
  216. data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
  217. data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
  218. data/lib/legate/web/views/_edit_agent_type.slim +67 -0
  219. data/lib/legate/web/views/_session_error.slim +4 -0
  220. data/lib/legate/web/views/_skeleton.slim +69 -0
  221. data/lib/legate/web/views/_tool_card.slim +9 -0
  222. data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
  223. data/lib/legate/web/views/agent.slim +436 -0
  224. data/lib/legate/web/views/agent_auth.slim +562 -0
  225. data/lib/legate/web/views/agents.slim +369 -0
  226. data/lib/legate/web/views/auth.slim +112 -0
  227. data/lib/legate/web/views/auth_credential_detail.slim +327 -0
  228. data/lib/legate/web/views/auth_credentials.slim +261 -0
  229. data/lib/legate/web/views/auth_debug.slim +94 -0
  230. data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
  231. data/lib/legate/web/views/auth_mapping_new.slim +123 -0
  232. data/lib/legate/web/views/auth_mappings.slim +120 -0
  233. data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
  234. data/lib/legate/web/views/auth_schemes.slim +259 -0
  235. data/lib/legate/web/views/auth_test.slim +418 -0
  236. data/lib/legate/web/views/chat.slim +192 -0
  237. data/lib/legate/web/views/docs_index.slim +105 -0
  238. data/lib/legate/web/views/docs_show.slim +105 -0
  239. data/lib/legate/web/views/error_404.slim +5 -0
  240. data/lib/legate/web/views/index.slim +148 -0
  241. data/lib/legate/web/views/layout.slim +144 -0
  242. data/lib/legate/web/views/tool_detail.slim +87 -0
  243. data/lib/legate/web/views/tools.slim +50 -0
  244. data/lib/legate/web/webhook_listener.rb +367 -0
  245. data/lib/legate/web.rb +9 -0
  246. data/lib/legate.rb +220 -0
  247. data/public/docs/advanced/callbacks.md +828 -0
  248. data/public/docs/advanced/mcp_schema_conversion.md +59 -0
  249. data/public/docs/authentication/api_reference/config.md +210 -0
  250. data/public/docs/authentication/api_reference/credential.md +246 -0
  251. data/public/docs/authentication/api_reference/encryption.md +218 -0
  252. data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
  253. data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
  254. data/public/docs/authentication/api_reference/index.md +30 -0
  255. data/public/docs/authentication/api_reference/scheme.md +250 -0
  256. data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
  257. data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
  258. data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
  259. data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
  260. data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
  261. data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
  262. data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
  263. data/public/docs/authentication/api_reference/token_manager.md +221 -0
  264. data/public/docs/authentication/api_reference/token_store.md +146 -0
  265. data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
  266. data/public/docs/authentication/guides/api_key.md +190 -0
  267. data/public/docs/authentication/guides/bearer.md +172 -0
  268. data/public/docs/authentication/guides/configuration.md +255 -0
  269. data/public/docs/authentication/guides/custom_flow.md +523 -0
  270. data/public/docs/authentication/guides/index.md +24 -0
  271. data/public/docs/authentication/guides/migration.md +435 -0
  272. data/public/docs/authentication/guides/oauth2.md +252 -0
  273. data/public/docs/authentication/guides/oidc.md +241 -0
  274. data/public/docs/authentication/guides/overview.md +155 -0
  275. data/public/docs/authentication/guides/secure_storage.md +301 -0
  276. data/public/docs/authentication/guides/service_account.md +228 -0
  277. data/public/docs/authentication/guides/token_lifecycle.md +295 -0
  278. data/public/docs/authentication/guides/web_ui_integration.md +504 -0
  279. data/public/docs/authentication/index.md +58 -0
  280. data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
  281. data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
  282. data/public/docs/authentication/troubleshooting/index.md +11 -0
  283. data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
  284. data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
  285. data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
  286. data/public/docs/cli/legate_cli_usage.md +363 -0
  287. data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
  288. data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
  289. data/public/docs/core_concepts/legate_configuration.md +116 -0
  290. data/public/docs/core_concepts/legate_definition_store.md +102 -0
  291. data/public/docs/core_concepts/legate_planner.md +94 -0
  292. data/public/docs/core_concepts/legate_session_service.md +104 -0
  293. data/public/docs/error_handling/legate_error_handling.md +122 -0
  294. data/public/docs/examples.md +199 -0
  295. data/public/docs/getting_started.md +111 -0
  296. data/public/docs/guides/agentic_agents.md +137 -0
  297. data/public/docs/guides/ai_code_generators.md +437 -0
  298. data/public/docs/guides/auto_loading.md +326 -0
  299. data/public/docs/guides/configuring_agent_webhooks.md +219 -0
  300. data/public/docs/guides/http_client_usage.md +264 -0
  301. data/public/docs/guides/llm_providers.md +137 -0
  302. data/public/docs/guides/mcp_client_integration.md +232 -0
  303. data/public/docs/guides/mcp_server_exposure.md +206 -0
  304. data/public/docs/guides/rails_integration.md +128 -0
  305. data/public/docs/guides/sending_outbound_webhooks.md +227 -0
  306. data/public/docs/guides/streaming.md +112 -0
  307. data/public/docs/guides/webhooks.md +288 -0
  308. data/public/docs/introduction.md +51 -0
  309. data/public/docs/multi_agent_systems/advanced_features.md +57 -0
  310. data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
  311. data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
  312. data/public/docs/multi_agent_systems/state_management.md +47 -0
  313. data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
  314. data/public/docs/tools/legate_built_in_tools.md +332 -0
  315. data/public/docs/tools/legate_tools_and_registry.md +263 -0
  316. data/public/docs/web_ui/legate_web_ui.md +137 -0
  317. metadata +823 -0
@@ -0,0 +1,934 @@
1
+ # File: lib/legate/cli/deployment_commands.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'thor'
5
+ require_relative 'base_command'
6
+ require 'fileutils'
7
+ require 'json'
8
+ require 'yaml'
9
+ require 'logger' # Needed for sample entrypoint
10
+ require 'securerandom' # Needed for suggested project ID
11
+ require 'shellwords'
12
+
13
+ module Legate
14
+ module CLI
15
+ # CLI commands for generating deployment assets
16
+ class DeploymentCommands < BaseCommand
17
+ # Default Ruby image if not specified
18
+ DEFAULT_RUBY_IMAGE = 'ruby:3.2-slim'
19
+ # Default output directory name
20
+ DEFAULT_DEPLOYMENT_DIR_NAME = 'deployment'
21
+ # Default sample entrypoint path
22
+ DEFAULT_SAMPLE_ENTRYPOINT_PATH = 'bin/legate_web_entrypoint.rb'
23
+
24
+ # --- Generic Options ---
25
+ desc 'generate', 'Generate deployment assets (Dockerfile, .dockerignore, cloud-specific configs)'
26
+ method_option :cloud, type: :string, aliases: '-c', default: 'none', required: true,
27
+ enum: %w[gcp aws azure none], desc: 'Target cloud provider (gcp, aws, azure, none)'
28
+ method_option :entry_point, type: :string, aliases: '-e', required: false,
29
+ desc: 'Entry point script for the main application/web process (e.g., bin/web). Required unless --generate-sample-entrypoint is used.'
30
+ method_option :agent_entry_points, type: :array, aliases: '-a',
31
+ desc: 'Entry points for user agents (comma separated)'
32
+ method_option :name, type: :string, aliases: '-n', default: DEFAULT_DEPLOYMENT_DIR_NAME,
33
+ desc: 'Base name for the output directory and potentially generated resources'
34
+ method_option :base_image, type: :string, default: DEFAULT_RUBY_IMAGE, desc: 'Base Ruby Docker image to use'
35
+ method_option :generate_sample_entrypoint, type: :boolean, default: false,
36
+ desc: "Generate a sample web entrypoint script (#{DEFAULT_SAMPLE_ENTRYPOINT_PATH}) with a /healthz check."
37
+
38
+ # --- GCP Specific Options (Only relevant if --cloud gcp) ---
39
+ class_option :gcp_project_id, type: :string, group: 'GCP', desc: 'GCP Project ID (required for GCP deployment)'
40
+ class_option :gcp_region, type: :string, default: 'us-central1', group: 'GCP', desc: 'GCP Region'
41
+ class_option :gcp_service_name, type: :string, default: 'legate-agent-service', group: 'GCP',
42
+ desc: 'GCP Cloud Run service name for the main process'
43
+ class_option :gcp_memory, type: :string, default: '512Mi', group: 'GCP',
44
+ desc: 'GCP Cloud Run memory allocation (e.g., 512Mi, 1Gi)'
45
+ class_option :gcp_cpu, type: :string, default: '1', group: 'GCP', desc: 'GCP Cloud Run CPU allocation'
46
+ # We might add options for agent service names, memory, cpu later.
47
+
48
+ def generate(_directory = '.')
49
+ # Determine the effective entry point
50
+ effective_entry_point = if options[:generate_sample_entrypoint]
51
+ options[:entry_point] || DEFAULT_SAMPLE_ENTRYPOINT_PATH
52
+ else
53
+ options[:entry_point]
54
+ end
55
+
56
+ # Validate entry_point is provided if sample isn't generated
57
+ unless effective_entry_point
58
+ say 'Error: --entry-point is required unless --generate-sample-entrypoint is used.', :red
59
+ exit 1
60
+ end
61
+
62
+ deployment_dir = File.expand_path(options[:name])
63
+ deployment_dir_basename = File.basename(deployment_dir)
64
+ gcp_config_name = nil # Store generated config name for final message
65
+ FileUtils.mkdir_p(deployment_dir)
66
+
67
+ say "Generating deployment assets in #{deployment_dir}...", :green
68
+
69
+ # 0. Generate sample entrypoint if requested (BEFORE generating Dockerfiles)
70
+ generate_sample_entrypoint_script(effective_entry_point) if options[:generate_sample_entrypoint]
71
+
72
+ # 1. Generate Generic Assets (Dockerfile(s), .dockerignore, config.ru)
73
+ generate_dockerfiles(deployment_dir, effective_entry_point, deployment_dir_basename)
74
+ generate_dockerignore(deployment_dir)
75
+ generate_config_ru(deployment_dir, effective_entry_point)
76
+
77
+ # 2. Generate Cloud-Specific Assets
78
+ case options[:cloud]
79
+ when 'gcp'
80
+ gcp_config_name = generate_gcp_assets(deployment_dir)
81
+ when 'aws'
82
+ generate_aws_assets(deployment_dir)
83
+ when 'azure'
84
+ generate_azure_assets(deployment_dir)
85
+ when 'none'
86
+ say 'Generated generic Docker assets only.', :yellow
87
+ else
88
+ # Should not happen due to Thor's enum check, but good practice
89
+ say "Unsupported cloud provider: #{options[:cloud]}", :red
90
+ exit 1
91
+ end
92
+
93
+ say 'Deployment asset generation complete!', :green
94
+ say "NOTE: Sample entrypoint generated at '#{effective_entry_point}'.", :yellow if options[:generate_sample_entrypoint]
95
+ if gcp_config_name
96
+ say "NOTE: A gcloud configuration named '#{gcp_config_name}' was created/updated.", :yellow
97
+ say ' Activate it using:', :yellow
98
+ say " gcloud config configurations activate #{gcp_config_name}", :cyan
99
+ say ' Before running the deployment script.', :yellow
100
+ end
101
+ return unless options[:cloud] == 'gcp'
102
+
103
+ say "Review the generated files in #{deployment_dir} and the deployment guide:"
104
+ say " #{File.join(deployment_dir, 'README-GCP-DEPLOYMENT.md')}", :cyan
105
+ end
106
+
107
+ private
108
+
109
+ def generate_dockerfiles(directory, main_entry_point, deployment_dir_basename)
110
+ # Main Dockerfile
111
+ main_dockerfile_path = File.join(directory, 'Dockerfile')
112
+ generate_dockerfile_content(main_dockerfile_path, main_entry_point, options[:base_image],
113
+ deployment_dir_basename)
114
+ say "Created main Dockerfile at #{main_dockerfile_path}", :cyan
115
+
116
+ # Agent Dockerfiles (if specified)
117
+ options[:agent_entry_points]&.each_with_index do |agent_entry, index|
118
+ agent_name = File.basename(agent_entry, '.rb').gsub(/[^0-9a-z_.-]/i, '_')
119
+ agent_dockerfile_path = File.join(directory, "Dockerfile.agent.#{agent_name}.#{index}")
120
+ generate_dockerfile_content(agent_dockerfile_path, agent_entry, options[:base_image], '')
121
+ say "Created agent Dockerfile for '#{agent_entry}' at #{agent_dockerfile_path}", :cyan
122
+ end
123
+ end
124
+
125
+ def generate_dockerfile_content(path, entry_point, base_image, deployment_dir_basename)
126
+ # Basic validation for entry point format (crude check)
127
+ say "Warning: Entry point '#{entry_point}' does not look like a path. Ensure it's correct.", :yellow unless entry_point&.include?('/') || entry_point&.start_with?('bin/')
128
+
129
+ # Determine the path to config.ru relative to the build context (project root)
130
+ # config.ru is generated inside the deployment directory
131
+ config_ru_build_context_path = File.join(deployment_dir_basename, 'config.ru')
132
+
133
+ # Apply changes based on user provided diff
134
+ content = <<~DOCKERFILE
135
+ # syntax=docker/dockerfile:1
136
+
137
+ # Dockerfile generated by Legate CLI
138
+ ARG RUBY_VERSION=#{base_image.split(':').last || '3.2-slim'} # Extract tag if possible
139
+ FROM #{base_image}
140
+
141
+ WORKDIR /app
142
+
143
+ # Install essential dependencies
144
+ # You may need to add more depending on your Gemfile (e.g., libpq-dev for pg gem)
145
+ RUN apt-get update -qq && \
146
+ apt-get install -y --no-install-recommends \
147
+ build-essential \
148
+ git \
149
+ libcurl4 \
150
+ && apt-get clean && \
151
+ rm -rf /var/lib/apt/lists/*
152
+
153
+ # Install Bundler
154
+ RUN gem install bundler --no-document
155
+
156
+ # Copy dependency definition files
157
+ COPY Gemfile Gemfile.lock ./
158
+
159
+ # Install Legate gem if present locally, then bundle install
160
+ COPY legate-*.gem ./
161
+ # Use wildcard and ignore errors if no gem file exists
162
+ RUN gem install legate-*.gem || echo "No local legate gem found, assuming it is in Gemfile."
163
+ RUN bundle config set without 'development test'
164
+ RUN bundle install --jobs $(nproc) --retry 3
165
+
166
+ # Copy the rest of the application code
167
+ # Ensure .dockerignore is properly configured
168
+ COPY . .
169
+ # Copy the generated config.ru from the deployment dir in the build context
170
+ COPY #{config_ru_build_context_path} ./
171
+
172
+ # --- Runtime Environment Variables ---
173
+ # Set sensible defaults, overrideable at runtime (e.g., via Cloud Run)
174
+ ENV RACK_ENV="production"
175
+
176
+ # Port (Required by Cloud Run)
177
+ ENV PORT="8080"
178
+
179
+ # Log Level
180
+ ENV LEGATE_LOG_LEVEL="INFO"
181
+
182
+ # Required by Legate for Gemini access, override with secret injection
183
+ ENV GOOGLE_API_KEY=""
184
+
185
+ # Expose the port the application listens on
186
+ EXPOSE ${PORT}
187
+
188
+ # --- Entry Point ---
189
+ # Runs the specified application or agent script using rackup
190
+ # Assumes a config.ru file exists in the root directory
191
+ # The config.ru should load the entrypoint script (e.g., bin/legate_web_entrypoint.rb)
192
+ # and run the defined Rack application (e.g., LegateWebApp).
193
+ CMD ["bundle", "exec", "rackup", "-p", "${PORT}", "-o", "0.0.0.0"]
194
+ DOCKERFILE
195
+
196
+ File.write(path, content)
197
+ end
198
+
199
+ def generate_dockerignore(directory)
200
+ dockerignore_path = File.join(directory, '.dockerignore')
201
+ # Avoid overwriting if it exists, maybe merge or warn later?
202
+ if File.exist?(dockerignore_path)
203
+ say "Skipping .dockerignore generation, file already exists: #{dockerignore_path}", :yellow
204
+ return
205
+ end
206
+
207
+ content = <<~IGNORE
208
+ # Dockerignore generated by Legate CLI
209
+ # Add files/directories here that are not needed in the final image
210
+
211
+ # Git files
212
+ .git
213
+ .gitignore
214
+
215
+ # Docker artifacts
216
+ .dockerignore
217
+ Dockerfile*
218
+
219
+ # Legate / Ruby specific
220
+ *.gem
221
+ .bundle/
222
+ vendor/bundle/
223
+ coverage/
224
+ spec/
225
+ tmp/
226
+ logs/
227
+ *.log
228
+
229
+ # Local config / secrets
230
+ .env*
231
+
232
+ # IDE / Editor specific
233
+ .vscode/
234
+ .idea/
235
+ .ruby-mine/
236
+ .project
237
+ *~ # Backup files
238
+
239
+ # OS specific
240
+ .DS_Store
241
+ Thumbs.db
242
+
243
+ # Deployment directory itself (if it's inside the project)
244
+ #{File.basename(directory)}/
245
+
246
+ IGNORE
247
+
248
+ File.write(dockerignore_path, content)
249
+ say "Created .dockerignore at #{dockerignore_path}", :cyan
250
+ end
251
+
252
+ # --- Generate config.ru (Generic) ---
253
+ def generate_config_ru(directory, entry_point_script)
254
+ config_ru_path = File.join(directory, 'config.ru')
255
+
256
+ if File.exist?(config_ru_path)
257
+ say "Skipping config.ru generation, file already exists: #{config_ru_path}", :yellow
258
+ return
259
+ end
260
+
261
+ # Determine the relative path from config.ru (in deployment dir) to the entry_point
262
+ # This assumes entry_point_script is relative to the project root.
263
+ # We need the path *inside* the container (relative to /app)
264
+ relative_entry_point = entry_point_script # Use the path as provided (e.g., 'bin/legate_web_entrypoint.rb')
265
+
266
+ # Basic validation
267
+ unless relative_entry_point&.include?('/')
268
+ say "Warning: Entry point '#{relative_entry_point}' for config.ru doesn't look like a relative path. Ensure it's correct.",
269
+ :yellow
270
+ end
271
+
272
+ content = <<~RACKUP
273
+ # File: config.ru (Generated by Legate CLI)
274
+ # This file is used by 'rackup' to start the web application.
275
+
276
+ # Load the environment and application defined in the entrypoint script.
277
+ # Ensure the path is correct relative to the application root inside the container.
278
+ require_relative '#{relative_entry_point}'
279
+
280
+ # Tell rackup which Rack application class to run.
281
+ # This should match the class name defined in your entrypoint script (e.g., LegateWebApp).
282
+ run LegateWebApp
283
+
284
+ RACKUP
285
+
286
+ File.write(config_ru_path, content)
287
+ say "Created config.ru at #{config_ru_path}", :cyan
288
+ say "Ensure the entrypoint path in config.ru ('#{relative_entry_point}') is correct for your project structure.",
289
+ :yellow
290
+ end
291
+
292
+ # --- Sample Entrypoint Generation (Optional, generic) ---
293
+ def generate_sample_entrypoint_script(sample_path)
294
+ sample_path = File.expand_path(sample_path) # Ensure absolute path
295
+ sample_dir = File.dirname(sample_path)
296
+
297
+ unless Dir.exist?(sample_dir)
298
+ say "Creating directory: #{sample_dir}", :green
299
+ FileUtils.mkdir_p(sample_dir)
300
+ end
301
+
302
+ if File.exist?(sample_path)
303
+ say "Sample entrypoint already exists, skipping: #{sample_path}", :yellow
304
+ return
305
+ end
306
+
307
+ say "Generating sample entrypoint script at #{sample_path}", :cyan
308
+
309
+ content = <<-'RUBYCONTENT'
310
+ #!/usr/bin/env ruby
311
+ # frozen_string_literal: true
312
+
313
+ # --- Generated Sample Legate Web Entrypoint ---
314
+ # This script provides a basic starting point for running Legate with a web server
315
+ # and includes a /healthz endpoint suitable for Cloud Run health checks.
316
+
317
+ require 'sinatra/base'
318
+ require 'sinatra/json'
319
+ require 'legate'
320
+ require 'legate/agent'
321
+ require 'legate/session_service/base'
322
+ require 'legate/tools/echo'
323
+
324
+ # --- Configuration ---
325
+ # Legate components will often rely on environment variables for configuration
326
+ # (e.g., GOOGLE_API_KEY, PORT).
327
+ # Ensure these are set correctly in your deployment environment (e.g., Cloud Run).
328
+
329
+ # Configure Legate settings if needed
330
+ # Example: Set the default model
331
+ # config.default_model_name = 'gemini-1.5-pro'
332
+
333
+ # Example: Configure webhooks if you plan to use them
334
+ # config.webhooks.listener_enabled = true
335
+ # config.webhooks.listen_address = '0.0.0.0' # Important for Cloud Run
336
+ # config.webhooks.listen_port = ENV.fetch('PORT', 8080).to_i
337
+ # config.webhooks.base_path = '/webhooks'
338
+ # config.webhooks.global_secret = ENV['WEBHOOK_SECRET'] # Load from env
339
+
340
+ Legate.configure do |config|
341
+ # Configure Legate settings if needed
342
+ # Example: Set the default model
343
+ # config.default_model_name = 'gemini-1.5-pro'
344
+
345
+ # Example: Configure webhooks if you plan to use them
346
+ # config.webhooks.listener_enabled = true
347
+ # config.webhooks.listen_address = '0.0.0.0' # Important for Cloud Run
348
+ # config.webhooks.listen_port = ENV.fetch('PORT', 8080).to_i
349
+ # config.webhooks.base_path = '/webhooks'
350
+ # config.webhooks.global_secret = ENV['WEBHOOK_SECRET'] # Load from env
351
+
352
+ # Session service uses in-memory storage
353
+ config.session_service = Legate::SessionService::InMemory.new
354
+
355
+ # --- IMPORTANT ---
356
+ # The Legate framework initializes its own logger.
357
+ # You generally don't need to set it here unless you have specific needs.
358
+ # If you DO need to customize logging, refer to the Legate documentation.
359
+ end
360
+
361
+ Legate.logger.info("Sample Legate Web Entrypoint environment configured.")
362
+
363
+ # --- Legate Agent/Application Logic Integration ---
364
+ # You might load agent definitions or start background tasks here.
365
+ # Example:
366
+ # Dir[File.expand_path('../../../app/agents/**/*.rb', __FILE__)].each { |file| require file }
367
+ # puts "INFO: Loaded agent definitions."
368
+
369
+ # --- Define Rack Application(s) ---
370
+ # Define your main application logic within a Rack-compatible class (like Sinatra).
371
+ # The actual server (Puma, Unicorn, etc.) will be started via rackup/config.ru
372
+ # based on the Dockerfile\'s CMD.
373
+
374
+ class LegateWebApp < Sinatra::Base
375
+ configure do
376
+ # Use the central Legate logger
377
+ set :logger, Legate.logger
378
+ # You might want to disable Sinatra\'s default logging if it\'s noisy
379
+ # disable :logging
380
+ end
381
+
382
+ # --- Health Check Endpoint ---
383
+ # Cloud Run uses this to check if the container is ready to serve requests.
384
+ get '/healthz' do
385
+ # Check essential dependencies (e.g., database connection, Legate services)
386
+ # Return 503 if not ready.
387
+ begin
388
+ # Example: Check Legate session service (adjust based on your config)
389
+ # raise "Session service not available" unless Legate.config.session_service&.check_connection
390
+ status 200
391
+ headers 'Content-Type' => 'text/plain'
392
+ body 'OK'
393
+ rescue => e
394
+ logger.error("Health check failed: #{e.message}")
395
+ status 503
396
+ headers 'Content-Type' => 'text/plain'
397
+ body "Service Unavailable: #{e.message}"
398
+ end
399
+ end
400
+
401
+ # --- Echo Agent Endpoint ---
402
+ post '/echo' do
403
+ content_type :json
404
+
405
+ begin
406
+ # 1. Get input from request body (expecting JSON: { "message": "..." })
407
+ request.body.rewind
408
+ request_payload = JSON.parse(request.body.read)
409
+ user_message = request_payload['message']
410
+
411
+ unless user_message
412
+ halt 400, json({ status: :error, error_message: "Missing 'message' key in JSON request body." })
413
+ end
414
+
415
+ # 2. Get configured session service
416
+ session_service = Legate.config.session_service
417
+ unless session_service
418
+ logger.error("/echo: Legate session service is not configured!")
419
+ halt 500, json({ status: :error, error_message: "Internal Server Error: Session service not configured." })
420
+ end
421
+
422
+ # 3. Instantiate an ephemeral Echo agent
423
+ # Create an ephemeral definition for the echo agent.
424
+ echo_agent_definition = Legate::AgentDefinition.new
425
+ echo_agent_definition.define do |def_proxy|
426
+ def_proxy.name :ephemeral_echo_sample # Ensure a unique name if needed, or just :ephemeral_echo
427
+ def_proxy.description 'Temporary Echo Agent for sample endpoint'
428
+ def_proxy.instruction 'You are an echo agent. You will use the echo tool to repeat the input.'
429
+ def_proxy.use_tool :echo # Assumes EchoTool is globally registered
430
+ # Model, fallback_mode, etc., will use defaults from AgentDefinition
431
+ end
432
+
433
+ # Legate::GlobalToolManager.register(Legate::Tools::Echo) unless Legate::GlobalToolManager.find_class(:echo)
434
+ # Ensure EchoTool is globally available if not already (though Legate typically handles this for built-ins)
435
+
436
+ echo_agent = Legate::Agent.new(definition: echo_agent_definition)
437
+ # The agent will use the session_service from Legate.config for its internal @session_service,
438
+ # but run_task below will use the specific session_service instance.
439
+
440
+ # 4. Create a temporary session for this request
441
+ temp_session = session_service.create_session(app_name: :echo_service, user_id: "web_#{SecureRandom.hex(4)}")
442
+ session_id = temp_session.id
443
+
444
+ logger.info("/echo: Running echo task in session #{session_id} for message: \"#{user_message}\"")
445
+
446
+ # 5. Run the task
447
+ # The Echo tool doesn't require planning, agent.run_task handles it.
448
+ final_event_or_error = echo_agent.run_task(
449
+ session_id: session_id,
450
+ user_input: user_message, # The agent/planner uses this
451
+ session_service: session_service
452
+ # We don't *need* to explicitly tell it to use the echo tool;
453
+ # the agent should figure it out or the Echo tool might be a fallback.
454
+ # If direct tool execution was needed: agent.execute_tool(:echo, {message: user_message}, session_id, session_service)
455
+ )
456
+
457
+ # 6. Process the result
458
+ if final_event_or_error.is_a?(Legate::Event)
459
+ result_content = final_event_or_error.content
460
+ # Successfully echoed
461
+ json({ status: :success, echoed_message: result_content[:result] })
462
+ elsif final_event_or_error.is_a?(Hash) && final_event_or_error[:status] == :error
463
+ # Handle errors reported by run_task
464
+ logger.error("/echo: Agent execution failed: #{final_event_or_error[:error_message]}")
465
+ status 500
466
+ json(final_event_or_error) # Return the error hash
467
+ else
468
+ # Unexpected result
469
+ logger.error("/echo: Unexpected result from agent execution: #{final_event_or_error.inspect}")
470
+ halt 500, json({ status: :error, error_message: "Internal Server Error: Unexpected agent result." })
471
+ end
472
+
473
+ rescue JSON::ParserError => e
474
+ logger.error("/echo: Invalid JSON input: #{e.message}")
475
+ halt 400, json({ status: :error, error_message: "Invalid JSON format: #{e.message}" })
476
+ rescue => e
477
+ logger.error("/echo: Unhandled error: #{e.class} - #{e.message}\n#{e.backtrace.join("\n")}")
478
+ halt 500, json({ status: :error, error_message: "Internal Server Error: #{e.message}" })
479
+ ensure
480
+ # Clean up temporary session if created
481
+ session_service.delete_session(session_id: session_id) if session_service && session_id
482
+ end
483
+ end
484
+
485
+ # --- Add Your Application Routes Here ---
486
+ # Example:
487
+ # get \'/\' do
488
+ # \'Hello from Legate Web App!\'
489
+ # end
490
+ end
491
+
492
+ # --- NOTE ---
493
+ # This script NO LONGER starts the web server directly.
494
+ # The Docker container\'s CMD should use \'rackup\' (referencing config.ru)
495
+ # to start a web server (like Puma) which will load this environment
496
+ # and run the LegateWebApp.
497
+
498
+ # Example config.ru content:
499
+ # require_relative \'./bin/legate_web_entrypoint\' # Load this script\'s environment
500
+ # run LegateWebApp # Tell rackup to run your Sinatra app
501
+
502
+ RUBYCONTENT
503
+
504
+ File.write(sample_path, content)
505
+ # Make the script executable
506
+ FileUtils.chmod(0o755, sample_path)
507
+ end
508
+
509
+ # --- GCP Asset Generation (Only called if --cloud gcp) ---
510
+ def generate_gcp_assets(directory)
511
+ say 'Generating GCP specific assets...', :magenta
512
+ gcp_config_name = nil # Initialize
513
+
514
+ # Validate required GCP options
515
+ project_id = options[:gcp_project_id]
516
+
517
+ unless project_id
518
+ # Project ID is missing, generate a suggestion and exit
519
+ random_hex = SecureRandom.hex(3) # Generate 6 hex characters
520
+ # Ensure name is valid for project ID (lowercase, digits, hyphens, 6-30 chars)
521
+ sanitized_base_name = options[:name].downcase.gsub(/[^a-z0-9-]/, '-').gsub(/^-+|-+$/, '')
522
+ suggested_project_id = "legate-deploy-#{sanitized_base_name}-#{random_hex}".slice(0, 30).gsub(/-+$/, '') # Ensure max 30 chars, no trailing hyphen
523
+ # Ensure it starts with a letter (though our pattern should ensure this)
524
+ suggested_project_id = "a#{suggested_project_id}" unless suggested_project_id[/^[a-z]/]
525
+ suggested_project_id = suggested_project_id.slice(0, 30) # Re-slice if prepended 'a' pushed length
526
+ say 'Error: --gcp-project-id is required for GCP deployment.', :red
527
+ say 'You must provide an existing GCP project ID where you have appropriate permissions.', :yellow
528
+ say 'If you need to create a new project first, you could use a command like this ', :yellow
529
+ say 'After ensuring billing is configured for your account):', :yellow
530
+ say " gcloud projects create #{suggested_project_id}", :cyan
531
+ say 'Then, re-run this command adding the flag:', :yellow
532
+ say " --gcp-project-id #{suggested_project_id}", :cyan
533
+ exit 1 # Stop execution, user needs to provide a valid project ID
534
+ end
535
+
536
+ # --- Project ID is present, proceed ---
537
+ region = options[:gcp_region] # Use the class_option value
538
+
539
+ # 1. Attempt to create gcloud configuration
540
+ # gcp_config_name = create_gcloud_config(options[:name], project_id, region)
541
+
542
+ # 2. Generate GCP specific config files (optional for now, script preferred)
543
+ # generate_gcp_cloud_run_config(directory)
544
+
545
+ # 3. Generate GCP deploy script
546
+ generate_gcp_deploy_script(directory)
547
+
548
+ # 4. Generate Cloud Build Config
549
+ generate_gcp_cloudbuild_yaml(directory)
550
+
551
+ # 5. Generate/Copy GCP docs
552
+ generate_gcp_deployment_docs(directory)
553
+
554
+ gcp_config_name # Return the generated name for the final message
555
+ end
556
+
557
+ # Helper to execute shell commands and check status
558
+ def run_gcloud_command(command, error_message)
559
+ say "Executing: gcloud #{command}"
560
+ output = `gcloud #{command} 2>&1` # Capture stderr too
561
+ unless $?.success?
562
+ say "Error: #{error_message}", :red
563
+ say "gcloud output:\n#{output}", :red
564
+ # Decide if we should exit or just warn
565
+ # For config commands, maybe warn and continue?
566
+ # For critical commands in deploy script, exit is better.
567
+ # Let's warn for config issues but allow script generation.
568
+ say 'Warning: Failed to automatically configure gcloud. Please ensure configuration is correct manually.',
569
+ :yellow
570
+ return false # Indicate failure
571
+ end
572
+ true # Indicate success
573
+ end
574
+
575
+ def create_gcloud_config(base_name, project_id, region)
576
+ # Sanitize base_name for config name
577
+ config_name = "legate-deploy-#{base_name.gsub(/[^0-9a-zA-Z_-]/, '-')}"
578
+ say "Attempting to create/update gcloud configuration: #{config_name}"
579
+
580
+ # Check if gcloud command exists first
581
+ unless system('command -v gcloud > /dev/null 2>&1')
582
+ say "Error: 'gcloud' command not found in PATH. Cannot create gcloud configuration.", :red
583
+ say 'Please install the Google Cloud SDK.', :yellow
584
+ return nil # Cannot proceed
585
+ end
586
+
587
+ # 1. Create or check configuration
588
+ # Use describe to check existence non-destructively
589
+ `gcloud config configurations describe #{config_name} > /dev/null 2>&1`
590
+ if $?.success?
591
+ say "Configuration '#{config_name}' already exists. Settings will be updated.", :yellow
592
+ else
593
+ # Try to create (use --no-activate)
594
+ unless run_gcloud_command("config configurations create #{config_name} --no-activate",
595
+ "Failed to create gcloud configuration '#{config_name}'.")
596
+ return nil # Failed, can't set properties
597
+ end
598
+
599
+ say "Created gcloud configuration: #{config_name}"
600
+ end
601
+
602
+ # 2. Set properties
603
+ run_gcloud_command("config set project #{project_id} --configuration=#{config_name}",
604
+ 'Failed to set project in gcloud config.')
605
+ run_gcloud_command("config set compute/region #{region} --configuration=#{config_name}",
606
+ 'Failed to set region in gcloud config.')
607
+ # Add other relevant defaults? e.g., run/region?
608
+ # run_gcloud_command("config set run/region #{region} --configuration=#{config_name}", "Failed to set run/region in gcloud config.")
609
+
610
+ config_name # Return the name used
611
+ end
612
+
613
+ # --- GCP Specific Helper Methods ---
614
+ def generate_gcp_cloud_run_config(_directory)
615
+ # NOTE: Generating a static YAML is less flexible than the deploy script.
616
+ # The script can dynamically fetch Redis IP etc. Keeping this commented out
617
+ # as generating the script is generally preferred.
618
+ say 'Skipping generation of static cloud-run-service.yaml, deploy script is preferred.', :yellow
619
+ end
620
+
621
+ def generate_gcp_deploy_script(directory)
622
+ deploy_script_path = File.join(directory, 'deploy-gcp.sh')
623
+
624
+ # Extract GCP options with defaults from class_options
625
+ # Escape inputs for Bash safety
626
+ project_id = Shellwords.escape(options[:gcp_project_id])
627
+ region = Shellwords.escape(options[:gcp_region])
628
+ main_service_name = Shellwords.escape(options[:gcp_service_name])
629
+ main_memory = Shellwords.escape(options[:gcp_memory])
630
+ main_cpu = Shellwords.escape(options[:gcp_cpu])
631
+ base_name = options[:name] # Used for image naming
632
+ deployment_dir_basename = File.basename(directory) # Get basename
633
+
634
+ # Image names
635
+ main_image_name = Shellwords.escape("#{base_name}-web")
636
+ main_image_tag = Shellwords.escape('latest')
637
+
638
+ # Artifact Registry setup
639
+ ar_location = region # Already escaped
640
+ ar_repo_name = Shellwords.escape('legate-images')
641
+
642
+ # Cloud Build config file path (relative to project root)
643
+ cloudbuild_config_file = Shellwords.escape(File.join(deployment_dir_basename, 'cloudbuild.yaml'))
644
+ # Dockerfile path relative to deployment dir
645
+ main_dockerfile_path_relative = Shellwords.escape('Dockerfile')
646
+
647
+ # --- Script Content ---
648
+ # This script is more comprehensive than before, includes setup
649
+ script_content = <<~BASH
650
+ #!/bin/bash
651
+ # Generated by Legate CLI for GCP Cloud Run deployment
652
+ set -euo pipefail # Enable strict mode
653
+
654
+ # --- Configuration (Edit these if needed) ---
655
+ PROJECT_ID=#{project_id}
656
+ REGION=#{region}
657
+ MAIN_SERVICE_NAME=#{main_service_name}
658
+ MAIN_DOCKERFILE=#{main_dockerfile_path_relative} # Relative path within deployment dir
659
+ MAIN_IMAGE_NAME=#{main_image_name}
660
+ MAIN_IMAGE_TAG=#{main_image_tag}
661
+ MAIN_MEMORY=#{main_memory}
662
+ MAIN_CPU=#{main_cpu}
663
+
664
+ # Secrets Configuration
665
+ SECRET_NAME="google-api-key" # Name of the secret in Secret Manager
666
+ SECRET_ENV_VAR="GOOGLE_API_KEY" # Env var name in Cloud Run
667
+
668
+ # Artifact Registry Repository
669
+ AR_REPO_NAME=#{ar_repo_name} # Artifact Registry repo name
670
+ AR_LOCATION=#{ar_location} # Often same as REGION, but can differ
671
+
672
+ # VPC Access Connector (optional, for private networking)
673
+ CONNECTOR_NAME="legate-vpc-connector" # Name for the VPC Access Connector
674
+ # Important: Ensure this range does not overlap with other subnets!
675
+ CONNECTOR_IP_RANGE="10.8.0.0/28"
676
+ VPC_NETWORK_NAME="default" # Use 'default' or your specific VPC network
677
+
678
+ # Service Account for Cloud Run (Recommended: Create a dedicated one)
679
+ # Leave empty to use the default Compute Engine service account (less secure)
680
+ RUN_SERVICE_ACCOUNT=""
681
+ # Example: RUN_SERVICE_ACCOUNT="legate-runner@${PROJECT_ID}.iam.gserviceaccount.com"
682
+
683
+ # --- Helper Functions ---
684
+ info() {
685
+ echo "[INFO] $1"
686
+ }
687
+
688
+ error() {
689
+ echo "[ERROR] $1 >&2
690
+ exit 1
691
+ }
692
+
693
+ check_command() {
694
+ command -v "$1" >/dev/null 2>&1 || error "$1' command not found. Please install it."
695
+ }
696
+
697
+ # --- Prerequisites Check ---
698
+ info "Checking prerequisites..."
699
+ check_command gcloud
700
+ #check_command docker # Often not needed if using Cloud Build
701
+
702
+ # --- Set GCP Project ---
703
+ info "Setting GCP project to ${PROJECT_ID}"
704
+ gcloud config set project "${PROJECT_ID}"
705
+
706
+ # --- Enable Required APIs ---
707
+ info "Enabling necessary GCP APIs..."
708
+ gcloud services enable \
709
+ run.googleapis.com \
710
+ artifactregistry.googleapis.com \
711
+ secretmanager.googleapis.com \
712
+ cloudbuild.googleapis.com \
713
+ vpcaccess.googleapis.com \
714
+ compute.googleapis.com || error "Failed to enable APIs"
715
+
716
+ # --- Configure Docker for Artifact Registry ---
717
+ #info "Configuring Docker authentication for ${AR_LOCATION}..."
718
+ #gcloud auth configure-docker "${AR_LOCATION}-docker.pkg.dev" || error "Docker auth configuration failed"
719
+
720
+ # --- Create Artifact Registry Repository (if it doesn't exist) ---
721
+ info "Ensuring Artifact Registry repository '${AR_REPO_NAME}' exists in ${AR_LOCATION}..."
722
+ if ! gcloud artifacts repositories describe "${AR_REPO_NAME}" --location="${AR_LOCATION}" --project="${PROJECT_ID}" &>/dev/null; then
723
+ info "Creating Artifact Registry repository '${AR_REPO_NAME}'..."
724
+ gcloud artifacts repositories create "${AR_REPO_NAME}" \
725
+ --repository-format=docker \
726
+ --location="${AR_LOCATION}" \
727
+ --description="Legate Application Images" \
728
+ --project="${PROJECT_ID}" || error "Failed to create Artifact Registry repository"
729
+ else
730
+ info "Artifact Registry repository '${AR_REPO_NAME}' already exists."
731
+ fi
732
+ MAIN_IMAGE_URI="${AR_LOCATION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO_NAME}/${MAIN_IMAGE_NAME}:${MAIN_IMAGE_TAG}"
733
+
734
+ # --- Create VPC Access Connector (if it doesn't exist) ---
735
+ info "Ensuring Serverless VPC Access connector '${CONNECTOR_NAME}' exists in ${REGION}..."
736
+ if ! gcloud compute networks vpc-access connectors describe "${CONNECTOR_NAME}" --region="${REGION}" --project="${PROJECT_ID}" &>/dev/null; then
737
+ info "Creating Serverless VPC Access connector '${CONNECTOR_NAME}'..."
738
+ gcloud compute networks vpc-access connectors create "${CONNECTOR_NAME}" \
739
+ --region="${REGION}" \
740
+ --range="${CONNECTOR_IP_RANGE}" \
741
+ --network="${VPC_NETWORK_NAME}" \
742
+ --project="${PROJECT_ID}" || error "Failed to create VPC Access connector"
743
+ else
744
+ info "Serverless VPC Access connector '${CONNECTOR_NAME}' already exists."
745
+ fi
746
+ VPC_CONNECTOR_FULL_NAME="projects/${PROJECT_ID}/locations/${REGION}/connectors/${CONNECTOR_NAME}"
747
+
748
+ # --- Create Secret for API Key (if it doesn't exist) ---
749
+ info "Ensuring Secret Manager secret '${SECRET_NAME}' exists..."
750
+ if ! gcloud secrets describe "${SECRET_NAME}" --project="${PROJECT_ID}" &>/dev/null; then
751
+ info "Secret '${SECRET_NAME}' not found. Please create it manually or enter the value now."
752
+ read -sp "Enter value for ${SECRET_NAME}: " SECRET_VALUE
753
+ echo # Newline after password prompt
754
+ if [[ -z "$SECRET_VALUE" ]]; then
755
+ error "Secret value cannot be empty."
756
+ fi
757
+ echo -n "$SECRET_VALUE" | gcloud secrets create "${SECRET_NAME}" --data-file=- \
758
+ --replication-policy="automatic" \
759
+ --project="${PROJECT_ID}" || error "Failed to create secret '${SECRET_NAME}'"
760
+ # Grant default compute service account access (adjust if using dedicated SA)
761
+ info "Granting default compute SA access to secret..."
762
+ PROJECT_NUMBER=$(gcloud projects describe ${PROJECT_ID} --format='value(projectNumber)')
763
+ DEFAULT_SA="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
764
+ gcloud secrets add-iam-policy-binding ${SECRET_NAME} \
765
+ --member="serviceAccount:${DEFAULT_SA}" \
766
+ --role="roles/secretmanager.secretAccessor" \
767
+ --project="${PROJECT_ID}" || echo "Warning: Failed to grant default SA access to secret. Ensure the running service account has access."
768
+ else
769
+ info "Secret '${SECRET_NAME}' already exists."
770
+ fi
771
+ # Use name:version format for Cloud Run secret injection
772
+ SECRET_RESOURCE_FOR_RUN="${SECRET_NAME}:latest"
773
+
774
+ # --- Grant Service Account Access to Secret ---
775
+ info "Ensuring Cloud Run service account can access secret '${SECRET_NAME}'..."
776
+ TARGET_SERVICE_ACCOUNT="${RUN_SERVICE_ACCOUNT}"
777
+ if [[ -z "${TARGET_SERVICE_ACCOUNT}" ]]; then
778
+ info "Using default Compute Engine service account."
779
+ PROJECT_NUMBER=$(gcloud projects describe "${PROJECT_ID}" --format='value(projectNumber)') || error "Failed to get project number."
780
+ TARGET_SERVICE_ACCOUNT="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
781
+ else
782
+ info "Using specified service account: ${TARGET_SERVICE_ACCOUNT}"
783
+ fi
784
+
785
+ # Attempt to grant the role. Might fail if runner lacks permissions.
786
+ gcloud secrets add-iam-policy-binding "${SECRET_NAME}" \
787
+ --member="serviceAccount:${TARGET_SERVICE_ACCOUNT}" \
788
+ --role="roles/secretmanager.secretAccessor" \
789
+ --project="${PROJECT_ID}" \
790
+ --condition=None \
791
+ >/dev/null || echo "[WARNING] Failed to automatically grant Secret Accessor role to ${TARGET_SERVICE_ACCOUNT}. Please ensure it has permission manually."
792
+
793
+ # --- Build and Push Main Docker Image ---
794
+ info "Building main application image: ${MAIN_IMAGE_URI}..."
795
+ # Using Cloud Build with an explicit config file
796
+ CONFIG_FILE=#{cloudbuild_config_file}
797
+ gcloud builds submit --config "${CONFIG_FILE}" --project="${PROJECT_ID}" --substitutions=_IMAGE_URI="${MAIN_IMAGE_URI}" .
798
+ if [[ $? -ne 0 ]]; then
799
+ error "Failed to build main image using ${CONFIG_FILE}"
800
+ fi
801
+ # Alternatively, build locally (Requires Docker):
802
+ # info "Configuring Docker authentication for ${AR_LOCATION}..."
803
+ # gcloud auth configure-docker "${AR_LOCATION}-docker.pkg.dev" || error "Docker auth configuration failed"
804
+ # docker build -t "${MAIN_IMAGE_URI}" -f "#{File.join(deployment_dir_basename, main_dockerfile_path_relative)}" . || error "Failed to build main image locally"
805
+ # docker push "${MAIN_IMAGE_URI}" || error "Failed to push main image"
806
+
807
+ # --- Build and Push Agent Docker Images (if configured) ---
808
+ # <<< Add logic here to loop through agent entry points, build and push their images >>>
809
+ # Example for one agent:
810
+ # AGENT_SERVICE_NAME="legate-agent-processor"
811
+ # AGENT_DOCKERFILE="Dockerfile.agent.processor"
812
+ # AGENT_IMAGE_NAME="legate-agent-processor"
813
+ # AGENT_IMAGE_URI="${AR_LOCATION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO_NAME}/${AGENT_IMAGE_NAME}:${MAIN_IMAGE_TAG}"
814
+ # AGENT_CLOUDBUILD_CONFIG="path/to/agent/cloudbuild.yaml" # Example path
815
+ # info "Building agent image: ${AGENT_IMAGE_URI}..."
816
+ # gcloud builds submit --config "${AGENT_CLOUDBUILD_CONFIG}" --project="${PROJECT_ID}" --substitutions=_IMAGE_URI="${AGENT_IMAGE_URI}" . || error "Failed to build agent image"
817
+
818
+ # --- Deploy Main Service to Cloud Run ---
819
+ info "Deploying main service '${MAIN_SERVICE_NAME}' to Cloud Run in ${REGION}..."
820
+
821
+ # Base command arguments
822
+ CMD_ARGS=(
823
+ gcloud run deploy "${MAIN_SERVICE_NAME}"
824
+ --project="${PROJECT_ID}"
825
+ --region="${REGION}"
826
+ --image="${MAIN_IMAGE_URI}"
827
+ --platform="managed"
828
+ --memory="${MAIN_MEMORY}"
829
+ --cpu="${MAIN_CPU}"
830
+ --port="8080"
831
+ --set-env-vars="RACK_ENV=production"
832
+ # Use name:version format for secrets
833
+ --set-secrets="${SECRET_ENV_VAR}=${SECRET_RESOURCE_FOR_RUN}"
834
+ --vpc-connector="${VPC_CONNECTOR_FULL_NAME}"
835
+ --vpc-egress="all-traffic"
836
+ --allow-unauthenticated # Remove if service should not be public
837
+ )
838
+
839
+ # Conditionally add service account
840
+ if [[ -n "${RUN_SERVICE_ACCOUNT}" ]]; then
841
+ info "Using service account: ${RUN_SERVICE_ACCOUNT}"
842
+ # Ensure this SA has roles/secretmanager.secretAccessor for the secret!
843
+ CMD_ARGS+=(--service-account="${RUN_SERVICE_ACCOUNT}")
844
+ else
845
+ info "Using default Compute Engine service account. Ensure it has Secret Accessor role."
846
+ fi
847
+
848
+ # Debug: Print the command arguments
849
+ echo "DEBUG: Executing: ${CMD_ARGS[@]}"
850
+
851
+ # Execute the command
852
+ "${CMD_ARGS[@]}"
853
+ if [[ $? -ne 0 ]]; then
854
+ error "Failed to deploy main service '${MAIN_SERVICE_NAME}'"
855
+ fi
856
+
857
+ # --- Deploy Agent Services to Cloud Run (if configured) ---
858
+ # <<< Add logic here to loop through agents and deploy them >>>
859
+ # Example for one agent:
860
+ # info "Deploying agent service '${AGENT_SERVICE_NAME}'..."
861
+ # AGENT_DEPLOY_ARGS=( ...) # Construct agent deployment args similarly
862
+ # gcloud run deploy "${AGENT_DEPLOY_ARGS[@]}" || error "Failed to deploy agent service '${AGENT_SERVICE_NAME}'"
863
+
864
+ # --- Deployment Complete ---
865
+ info "Deployment successful!"
866
+ MAIN_SERVICE_URL=$(gcloud run services describe "${MAIN_SERVICE_NAME}" --region="${REGION}" --project="${PROJECT_ID}" --platform="managed" --format="value(status.url)")
867
+ if [[ -n "${MAIN_SERVICE_URL}" ]]; then
868
+ info "Main service '${MAIN_SERVICE_NAME}' URL: ${MAIN_SERVICE_URL}"
869
+ else
870
+ info "Main service '${MAIN_SERVICE_NAME}' deployed, but URL not available yet."
871
+ fi
872
+
873
+ BASH
874
+
875
+ File.write(deploy_script_path, script_content)
876
+ FileUtils.chmod(0o755, deploy_script_path)
877
+ say "Created GCP deployment script at #{deploy_script_path}", :cyan
878
+ say 'Please review and customize the script, especially the Configuration section, before running.', :yellow
879
+ end
880
+
881
+ def generate_gcp_deployment_docs(directory)
882
+ # Instead of hardcoding, copy the canonical doc we maintain
883
+ # Use __dir__ to get the directory of the current file (deployment_commands.rb)
884
+ source_doc_path = File.expand_path('../../../../docs/go-to-gcp-production-gemini.md', __dir__)
885
+ target_doc_path = File.join(directory, 'README-GCP-DEPLOYMENT.md')
886
+
887
+ if File.exist?(source_doc_path)
888
+ FileUtils.cp(source_doc_path, target_doc_path)
889
+ say "Copied GCP deployment guide to #{target_doc_path}", :cyan
890
+ else
891
+ say "Warning: Source deployment document not found at #{source_doc_path}", :yellow
892
+ # Optionally generate a placeholder
893
+ File.write(target_doc_path, "# GCP Deployment Guide\n\nSee online documentation for deployment steps.\n")
894
+ end
895
+ end
896
+
897
+ # New method to generate cloudbuild.yaml
898
+ def generate_gcp_cloudbuild_yaml(directory)
899
+ cloudbuild_path = File.join(directory, 'cloudbuild.yaml')
900
+ deployment_dir_basename = File.basename(directory)
901
+ main_dockerfile_path_relative = File.join(deployment_dir_basename, 'Dockerfile')
902
+
903
+ content = <<~YAML
904
+ steps:
905
+ # Build the container image
906
+ - name: 'gcr.io/cloud-builders/docker'
907
+ args: ['build', '-t', '${_IMAGE_URI}', '-f', '#{main_dockerfile_path_relative}', '.']
908
+
909
+ # Push the container image to Artifact Registry
910
+ images: ['${_IMAGE_URI}']
911
+
912
+ # Define substitutions that can be passed in via --substitutions flag
913
+ substitutions:
914
+ _IMAGE_URI: 'gcr.io/cloud-build/image' # Default value, will be overridden
915
+ YAML
916
+
917
+ File.write(cloudbuild_path, content)
918
+ say "Created GCP Cloud Build config at #{cloudbuild_path}", :cyan
919
+ end
920
+
921
+ # --- AWS Asset Generation (Placeholder) ---
922
+ def generate_aws_assets(_directory)
923
+ say 'AWS deployment asset generation is not yet implemented.', :yellow
924
+ # Placeholder for future: generate CloudFormation/CDK/Terraform, deploy scripts etc.
925
+ end
926
+
927
+ # --- Azure Asset Generation (Placeholder) ---
928
+ def generate_azure_assets(_directory)
929
+ say 'Azure deployment asset generation is not yet implemented.', :yellow
930
+ # Placeholder for future: generate ARM templates/Bicep, deploy scripts etc.
931
+ end
932
+ end
933
+ end
934
+ end