eryph-compute 0.1.1

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 (222) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +47 -0
  3. data/LICENSE +21 -0
  4. data/README.md +144 -0
  5. data/eryph-clientruntime.gemspec +42 -0
  6. data/eryph-compute.gemspec +44 -0
  7. data/lib/eryph/clientruntime/client_credentials_lookup.rb +295 -0
  8. data/lib/eryph/clientruntime/config_store.rb +206 -0
  9. data/lib/eryph/clientruntime/config_stores_reader.rb +211 -0
  10. data/lib/eryph/clientruntime/endpoint_lookup.rb +226 -0
  11. data/lib/eryph/clientruntime/environment.rb +440 -0
  12. data/lib/eryph/clientruntime/local_identity_provider_info.rb +153 -0
  13. data/lib/eryph/clientruntime/token_provider.rb +275 -0
  14. data/lib/eryph/clientruntime/version.rb +6 -0
  15. data/lib/eryph/clientruntime.rb +79 -0
  16. data/lib/eryph/compute/client.rb +442 -0
  17. data/lib/eryph/compute/generated/Gemfile +9 -0
  18. data/lib/eryph/compute/generated/README.md +206 -0
  19. data/lib/eryph/compute/generated/Rakefile +10 -0
  20. data/lib/eryph/compute/generated/compute_client.gemspec +41 -0
  21. data/lib/eryph/compute/generated/docs/ApiVersion.md +20 -0
  22. data/lib/eryph/compute/generated/docs/ApiVersionResponse.md +18 -0
  23. data/lib/eryph/compute/generated/docs/Catlet.md +32 -0
  24. data/lib/eryph/compute/generated/docs/CatletConfigOperationResult.md +18 -0
  25. data/lib/eryph/compute/generated/docs/CatletConfigValidationResult.md +20 -0
  26. data/lib/eryph/compute/generated/docs/CatletConfiguration.md +18 -0
  27. data/lib/eryph/compute/generated/docs/CatletDrive.md +20 -0
  28. data/lib/eryph/compute/generated/docs/CatletDriveType.md +15 -0
  29. data/lib/eryph/compute/generated/docs/CatletList.md +18 -0
  30. data/lib/eryph/compute/generated/docs/CatletNetwork.md +30 -0
  31. data/lib/eryph/compute/generated/docs/CatletNetworkAdapter.md +20 -0
  32. data/lib/eryph/compute/generated/docs/CatletStatus.md +15 -0
  33. data/lib/eryph/compute/generated/docs/CatletStopMode.md +15 -0
  34. data/lib/eryph/compute/generated/docs/CatletsApi.md +863 -0
  35. data/lib/eryph/compute/generated/docs/DiskStatus.md +15 -0
  36. data/lib/eryph/compute/generated/docs/ExpandCatletConfigRequestBody.md +22 -0
  37. data/lib/eryph/compute/generated/docs/ExpandNewCatletConfigRequest.md +22 -0
  38. data/lib/eryph/compute/generated/docs/FloatingNetworkPort.md +26 -0
  39. data/lib/eryph/compute/generated/docs/Gene.md +30 -0
  40. data/lib/eryph/compute/generated/docs/GeneList.md +18 -0
  41. data/lib/eryph/compute/generated/docs/GeneType.md +15 -0
  42. data/lib/eryph/compute/generated/docs/GeneWithUsage.md +34 -0
  43. data/lib/eryph/compute/generated/docs/GenesApi.md +281 -0
  44. data/lib/eryph/compute/generated/docs/NewCatletRequest.md +20 -0
  45. data/lib/eryph/compute/generated/docs/NewProjectMemberBody.md +22 -0
  46. data/lib/eryph/compute/generated/docs/NewProjectRequest.md +20 -0
  47. data/lib/eryph/compute/generated/docs/NewVirtualDiskRequest.md +30 -0
  48. data/lib/eryph/compute/generated/docs/Operation.md +32 -0
  49. data/lib/eryph/compute/generated/docs/OperationList.md +18 -0
  50. data/lib/eryph/compute/generated/docs/OperationLogEntry.md +24 -0
  51. data/lib/eryph/compute/generated/docs/OperationResource.md +22 -0
  52. data/lib/eryph/compute/generated/docs/OperationResult.md +18 -0
  53. data/lib/eryph/compute/generated/docs/OperationStatus.md +15 -0
  54. data/lib/eryph/compute/generated/docs/OperationTask.md +30 -0
  55. data/lib/eryph/compute/generated/docs/OperationTaskReference.md +22 -0
  56. data/lib/eryph/compute/generated/docs/OperationTaskStatus.md +15 -0
  57. data/lib/eryph/compute/generated/docs/OperationsApi.md +157 -0
  58. data/lib/eryph/compute/generated/docs/PopulateCatletConfigVariablesRequest.md +20 -0
  59. data/lib/eryph/compute/generated/docs/ProblemDetails.md +26 -0
  60. data/lib/eryph/compute/generated/docs/Project.md +22 -0
  61. data/lib/eryph/compute/generated/docs/ProjectList.md +18 -0
  62. data/lib/eryph/compute/generated/docs/ProjectMemberRole.md +26 -0
  63. data/lib/eryph/compute/generated/docs/ProjectMemberRoleList.md +18 -0
  64. data/lib/eryph/compute/generated/docs/ProjectMembersApi.md +293 -0
  65. data/lib/eryph/compute/generated/docs/ProjectsApi.md +286 -0
  66. data/lib/eryph/compute/generated/docs/ResourceType.md +15 -0
  67. data/lib/eryph/compute/generated/docs/StopCatletRequestBody.md +18 -0
  68. data/lib/eryph/compute/generated/docs/TaskReferenceType.md +15 -0
  69. data/lib/eryph/compute/generated/docs/UpdateCatletRequestBody.md +20 -0
  70. data/lib/eryph/compute/generated/docs/UpdateProjectNetworksRequestBody.md +20 -0
  71. data/lib/eryph/compute/generated/docs/ValidateConfigRequest.md +18 -0
  72. data/lib/eryph/compute/generated/docs/ValidationIssue.md +20 -0
  73. data/lib/eryph/compute/generated/docs/VersionApi.md +69 -0
  74. data/lib/eryph/compute/generated/docs/VirtualDisk.md +42 -0
  75. data/lib/eryph/compute/generated/docs/VirtualDiskAttachedCatlet.md +20 -0
  76. data/lib/eryph/compute/generated/docs/VirtualDiskGeneInfo.md +22 -0
  77. data/lib/eryph/compute/generated/docs/VirtualDiskList.md +18 -0
  78. data/lib/eryph/compute/generated/docs/VirtualDisksApi.md +291 -0
  79. data/lib/eryph/compute/generated/docs/VirtualNetwork.md +28 -0
  80. data/lib/eryph/compute/generated/docs/VirtualNetworkConfiguration.md +18 -0
  81. data/lib/eryph/compute/generated/docs/VirtualNetworkList.md +18 -0
  82. data/lib/eryph/compute/generated/docs/VirtualNetworksApi.md +291 -0
  83. data/lib/eryph/compute/generated/git_push.sh +57 -0
  84. data/lib/eryph/compute/generated/lib/compute_client/api/catlets_api.rb +812 -0
  85. data/lib/eryph/compute/generated/lib/compute_client/api/genes_api.rb +262 -0
  86. data/lib/eryph/compute/generated/lib/compute_client/api/operations_api.rb +154 -0
  87. data/lib/eryph/compute/generated/lib/compute_client/api/project_members_api.rb +297 -0
  88. data/lib/eryph/compute/generated/lib/compute_client/api/projects_api.rb +269 -0
  89. data/lib/eryph/compute/generated/lib/compute_client/api/version_api.rb +79 -0
  90. data/lib/eryph/compute/generated/lib/compute_client/api/virtual_disks_api.rb +272 -0
  91. data/lib/eryph/compute/generated/lib/compute_client/api/virtual_networks_api.rb +282 -0
  92. data/lib/eryph/compute/generated/lib/compute_client/api_client.rb +437 -0
  93. data/lib/eryph/compute/generated/lib/compute_client/api_error.rb +58 -0
  94. data/lib/eryph/compute/generated/lib/compute_client/configuration.rb +392 -0
  95. data/lib/eryph/compute/generated/lib/compute_client/models/api_version.rb +263 -0
  96. data/lib/eryph/compute/generated/lib/compute_client/models/api_version_response.rb +237 -0
  97. data/lib/eryph/compute/generated/lib/compute_client/models/catlet.rb +400 -0
  98. data/lib/eryph/compute/generated/lib/compute_client/models/catlet_config_operation_result.rb +234 -0
  99. data/lib/eryph/compute/generated/lib/compute_client/models/catlet_config_validation_result.rb +251 -0
  100. data/lib/eryph/compute/generated/lib/compute_client/models/catlet_configuration.rb +223 -0
  101. data/lib/eryph/compute/generated/lib/compute_client/models/catlet_drive.rb +270 -0
  102. data/lib/eryph/compute/generated/lib/compute_client/models/catlet_drive_type.rb +43 -0
  103. data/lib/eryph/compute/generated/lib/compute_client/models/catlet_list.rb +239 -0
  104. data/lib/eryph/compute/generated/lib/compute_client/models/catlet_network.rb +318 -0
  105. data/lib/eryph/compute/generated/lib/compute_client/models/catlet_network_adapter.rb +247 -0
  106. data/lib/eryph/compute/generated/lib/compute_client/models/catlet_status.rb +43 -0
  107. data/lib/eryph/compute/generated/lib/compute_client/models/catlet_stop_mode.rb +41 -0
  108. data/lib/eryph/compute/generated/lib/compute_client/models/disk_status.rb +40 -0
  109. data/lib/eryph/compute/generated/lib/compute_client/models/expand_catlet_config_request_body.rb +243 -0
  110. data/lib/eryph/compute/generated/lib/compute_client/models/expand_new_catlet_config_request.rb +243 -0
  111. data/lib/eryph/compute/generated/lib/compute_client/models/floating_network_port.rb +313 -0
  112. data/lib/eryph/compute/generated/lib/compute_client/models/gene.rb +415 -0
  113. data/lib/eryph/compute/generated/lib/compute_client/models/gene_list.rb +239 -0
  114. data/lib/eryph/compute/generated/lib/compute_client/models/gene_type.rb +41 -0
  115. data/lib/eryph/compute/generated/lib/compute_client/models/gene_with_usage.rb +439 -0
  116. data/lib/eryph/compute/generated/lib/compute_client/models/new_catlet_request.rb +233 -0
  117. data/lib/eryph/compute/generated/lib/compute_client/models/new_project_member_body.rb +273 -0
  118. data/lib/eryph/compute/generated/lib/compute_client/models/new_project_request.rb +247 -0
  119. data/lib/eryph/compute/generated/lib/compute_client/models/new_virtual_disk_request.rb +345 -0
  120. data/lib/eryph/compute/generated/lib/compute_client/models/operation.rb +352 -0
  121. data/lib/eryph/compute/generated/lib/compute_client/models/operation_list.rb +239 -0
  122. data/lib/eryph/compute/generated/lib/compute_client/models/operation_log_entry.rb +299 -0
  123. data/lib/eryph/compute/generated/lib/compute_client/models/operation_resource.rb +311 -0
  124. data/lib/eryph/compute/generated/lib/compute_client/models/operation_result.rb +242 -0
  125. data/lib/eryph/compute/generated/lib/compute_client/models/operation_status.rb +42 -0
  126. data/lib/eryph/compute/generated/lib/compute_client/models/operation_task.rb +366 -0
  127. data/lib/eryph/compute/generated/lib/compute_client/models/operation_task_reference.rb +311 -0
  128. data/lib/eryph/compute/generated/lib/compute_client/models/operation_task_status.rb +42 -0
  129. data/lib/eryph/compute/generated/lib/compute_client/models/populate_catlet_config_variables_request.rb +233 -0
  130. data/lib/eryph/compute/generated/lib/compute_client/models/problem_details.rb +261 -0
  131. data/lib/eryph/compute/generated/lib/compute_client/models/project.rb +289 -0
  132. data/lib/eryph/compute/generated/lib/compute_client/models/project_list.rb +239 -0
  133. data/lib/eryph/compute/generated/lib/compute_client/models/project_member_role.rb +341 -0
  134. data/lib/eryph/compute/generated/lib/compute_client/models/project_member_role_list.rb +239 -0
  135. data/lib/eryph/compute/generated/lib/compute_client/models/resource_type.rb +42 -0
  136. data/lib/eryph/compute/generated/lib/compute_client/models/stop_catlet_request_body.rb +259 -0
  137. data/lib/eryph/compute/generated/lib/compute_client/models/task_reference_type.rb +40 -0
  138. data/lib/eryph/compute/generated/lib/compute_client/models/update_catlet_request_body.rb +233 -0
  139. data/lib/eryph/compute/generated/lib/compute_client/models/update_project_networks_request_body.rb +233 -0
  140. data/lib/eryph/compute/generated/lib/compute_client/models/validate_config_request.rb +223 -0
  141. data/lib/eryph/compute/generated/lib/compute_client/models/validation_issue.rb +249 -0
  142. data/lib/eryph/compute/generated/lib/compute_client/models/virtual_disk.rb +479 -0
  143. data/lib/eryph/compute/generated/lib/compute_client/models/virtual_disk_attached_catlet.rb +285 -0
  144. data/lib/eryph/compute/generated/lib/compute_client/models/virtual_disk_gene_info.rb +289 -0
  145. data/lib/eryph/compute/generated/lib/compute_client/models/virtual_disk_list.rb +239 -0
  146. data/lib/eryph/compute/generated/lib/compute_client/models/virtual_network.rb +351 -0
  147. data/lib/eryph/compute/generated/lib/compute_client/models/virtual_network_configuration.rb +223 -0
  148. data/lib/eryph/compute/generated/lib/compute_client/models/virtual_network_list.rb +239 -0
  149. data/lib/eryph/compute/generated/lib/compute_client/version.rb +15 -0
  150. data/lib/eryph/compute/generated/lib/compute_client.rb +101 -0
  151. data/lib/eryph/compute/generated/spec/api/catlets_api_spec.rb +182 -0
  152. data/lib/eryph/compute/generated/spec/api/genes_api_spec.rb +81 -0
  153. data/lib/eryph/compute/generated/spec/api/operations_api_spec.rb +62 -0
  154. data/lib/eryph/compute/generated/spec/api/project_members_api_spec.rb +86 -0
  155. data/lib/eryph/compute/generated/spec/api/projects_api_spec.rb +82 -0
  156. data/lib/eryph/compute/generated/spec/api/version_api_spec.rb +46 -0
  157. data/lib/eryph/compute/generated/spec/api/virtual_disks_api_spec.rb +83 -0
  158. data/lib/eryph/compute/generated/spec/api/virtual_networks_api_spec.rb +84 -0
  159. data/lib/eryph/compute/generated/spec/models/api_version_response_spec.rb +36 -0
  160. data/lib/eryph/compute/generated/spec/models/api_version_spec.rb +42 -0
  161. data/lib/eryph/compute/generated/spec/models/catlet_config_operation_result_spec.rb +36 -0
  162. data/lib/eryph/compute/generated/spec/models/catlet_config_validation_result_spec.rb +42 -0
  163. data/lib/eryph/compute/generated/spec/models/catlet_configuration_spec.rb +36 -0
  164. data/lib/eryph/compute/generated/spec/models/catlet_drive_spec.rb +42 -0
  165. data/lib/eryph/compute/generated/spec/models/catlet_drive_type_spec.rb +30 -0
  166. data/lib/eryph/compute/generated/spec/models/catlet_list_spec.rb +36 -0
  167. data/lib/eryph/compute/generated/spec/models/catlet_network_adapter_spec.rb +42 -0
  168. data/lib/eryph/compute/generated/spec/models/catlet_network_spec.rb +72 -0
  169. data/lib/eryph/compute/generated/spec/models/catlet_spec.rb +78 -0
  170. data/lib/eryph/compute/generated/spec/models/catlet_status_spec.rb +30 -0
  171. data/lib/eryph/compute/generated/spec/models/catlet_stop_mode_spec.rb +30 -0
  172. data/lib/eryph/compute/generated/spec/models/disk_status_spec.rb +30 -0
  173. data/lib/eryph/compute/generated/spec/models/expand_catlet_config_request_body_spec.rb +48 -0
  174. data/lib/eryph/compute/generated/spec/models/expand_new_catlet_config_request_spec.rb +48 -0
  175. data/lib/eryph/compute/generated/spec/models/floating_network_port_spec.rb +60 -0
  176. data/lib/eryph/compute/generated/spec/models/gene_list_spec.rb +36 -0
  177. data/lib/eryph/compute/generated/spec/models/gene_spec.rb +72 -0
  178. data/lib/eryph/compute/generated/spec/models/gene_type_spec.rb +30 -0
  179. data/lib/eryph/compute/generated/spec/models/gene_with_usage_spec.rb +84 -0
  180. data/lib/eryph/compute/generated/spec/models/new_catlet_request_spec.rb +42 -0
  181. data/lib/eryph/compute/generated/spec/models/new_project_member_body_spec.rb +48 -0
  182. data/lib/eryph/compute/generated/spec/models/new_project_request_spec.rb +42 -0
  183. data/lib/eryph/compute/generated/spec/models/new_virtual_disk_request_spec.rb +72 -0
  184. data/lib/eryph/compute/generated/spec/models/operation_list_spec.rb +36 -0
  185. data/lib/eryph/compute/generated/spec/models/operation_log_entry_spec.rb +54 -0
  186. data/lib/eryph/compute/generated/spec/models/operation_resource_spec.rb +48 -0
  187. data/lib/eryph/compute/generated/spec/models/operation_result_spec.rb +36 -0
  188. data/lib/eryph/compute/generated/spec/models/operation_spec.rb +78 -0
  189. data/lib/eryph/compute/generated/spec/models/operation_status_spec.rb +30 -0
  190. data/lib/eryph/compute/generated/spec/models/operation_task_reference_spec.rb +48 -0
  191. data/lib/eryph/compute/generated/spec/models/operation_task_spec.rb +72 -0
  192. data/lib/eryph/compute/generated/spec/models/operation_task_status_spec.rb +30 -0
  193. data/lib/eryph/compute/generated/spec/models/populate_catlet_config_variables_request_spec.rb +42 -0
  194. data/lib/eryph/compute/generated/spec/models/problem_details_spec.rb +60 -0
  195. data/lib/eryph/compute/generated/spec/models/project_list_spec.rb +36 -0
  196. data/lib/eryph/compute/generated/spec/models/project_member_role_list_spec.rb +36 -0
  197. data/lib/eryph/compute/generated/spec/models/project_member_role_spec.rb +60 -0
  198. data/lib/eryph/compute/generated/spec/models/project_spec.rb +48 -0
  199. data/lib/eryph/compute/generated/spec/models/resource_type_spec.rb +30 -0
  200. data/lib/eryph/compute/generated/spec/models/stop_catlet_request_body_spec.rb +36 -0
  201. data/lib/eryph/compute/generated/spec/models/task_reference_type_spec.rb +30 -0
  202. data/lib/eryph/compute/generated/spec/models/update_catlet_request_body_spec.rb +42 -0
  203. data/lib/eryph/compute/generated/spec/models/update_project_networks_request_body_spec.rb +42 -0
  204. data/lib/eryph/compute/generated/spec/models/validate_config_request_spec.rb +36 -0
  205. data/lib/eryph/compute/generated/spec/models/validation_issue_spec.rb +42 -0
  206. data/lib/eryph/compute/generated/spec/models/virtual_disk_attached_catlet_spec.rb +42 -0
  207. data/lib/eryph/compute/generated/spec/models/virtual_disk_gene_info_spec.rb +48 -0
  208. data/lib/eryph/compute/generated/spec/models/virtual_disk_list_spec.rb +36 -0
  209. data/lib/eryph/compute/generated/spec/models/virtual_disk_spec.rb +108 -0
  210. data/lib/eryph/compute/generated/spec/models/virtual_network_configuration_spec.rb +36 -0
  211. data/lib/eryph/compute/generated/spec/models/virtual_network_list_spec.rb +36 -0
  212. data/lib/eryph/compute/generated/spec/models/virtual_network_spec.rb +66 -0
  213. data/lib/eryph/compute/generated/spec/spec_helper.rb +111 -0
  214. data/lib/eryph/compute/generated.rb +137 -0
  215. data/lib/eryph/compute/operation_result.rb +255 -0
  216. data/lib/eryph/compute/operation_tracker.rb +247 -0
  217. data/lib/eryph/compute/problem_details_error.rb +139 -0
  218. data/lib/eryph/compute/version.rb +6 -0
  219. data/lib/eryph/compute.rb +40 -0
  220. data/lib/eryph/version.rb +4 -0
  221. data/lib/eryph.rb +68 -0
  222. metadata +329 -0
@@ -0,0 +1,206 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+
4
+ module Eryph
5
+ module ClientRuntime
6
+ # Represents a configuration store for eryph client settings
7
+ # Manages JSON configuration files and private key storage
8
+ class ConfigStore
9
+ STORE_DIR = '.eryph'.freeze
10
+ PRIVATE_DIR = 'private'.freeze
11
+ CONFIG_EXTENSION = '.config'.freeze
12
+
13
+ # @return [String] base path for this configuration store
14
+ attr_reader :base_path
15
+
16
+ # @return [String] configuration name
17
+ attr_reader :config_name
18
+
19
+ # @return [Environment] environment abstraction
20
+ attr_reader :environment
21
+
22
+ # Initialize a configuration store
23
+ # @param base_path [String] base directory path
24
+ # @param config_name [String] configuration name
25
+ # @param environment [Environment] environment abstraction
26
+ def initialize(base_path, config_name, environment)
27
+ @base_path = base_path
28
+ @config_name = config_name
29
+ @environment = environment
30
+ end
31
+
32
+ # Check if this configuration store exists
33
+ # @return [Boolean] true if the store exists
34
+ def exists?
35
+ @environment.file_exists?(config_file_path)
36
+ end
37
+
38
+ # Get the configuration data
39
+ # @return [Hash] parsed configuration data
40
+ # @raise [ConfigurationError] if configuration cannot be read or parsed
41
+ def configuration
42
+ return {} unless exists?
43
+
44
+ begin
45
+ content = @environment.read_file(config_file_path)
46
+ JSON.parse(content)
47
+ rescue JSON::ParserError => e
48
+ raise ConfigurationError, "Invalid JSON in configuration file #{config_file_path}: #{e.message}"
49
+ rescue IOError => e
50
+ raise ConfigurationError, "Cannot read configuration file #{config_file_path}: #{e.message}"
51
+ end
52
+ end
53
+
54
+ # Get endpoints from this configuration store
55
+ # @return [Hash] endpoint name -> URL mapping
56
+ def endpoints
57
+ config = configuration
58
+ config['endpoints'] || {}
59
+ end
60
+
61
+ # Get clients from this configuration store
62
+ # @return [Array<Hash>] array of client configurations
63
+ def clients
64
+ config = configuration
65
+ config['clients'] || []
66
+ end
67
+
68
+ # Get the default client ID
69
+ # @return [String, nil] default client ID
70
+ def default_client_id
71
+ config = configuration
72
+ config['defaultClient']
73
+ end
74
+
75
+ # Get a client configuration by ID
76
+ # @param client_id [String] client ID to lookup
77
+ # @return [Hash, nil] client configuration or nil if not found
78
+ def get_client(client_id)
79
+ clients.find { |client| client['id'] == client_id }
80
+ end
81
+
82
+ # Get the default client configuration
83
+ # @return [Hash, nil] default client configuration or nil if not found
84
+ def default_client
85
+ config = configuration
86
+ default_client_id = config['defaultClientId']
87
+
88
+ all_clients = clients
89
+ return nil if all_clients.empty?
90
+
91
+ # If explicit defaultClient is set, use that
92
+ return all_clients.find { |client| client['id'] == default_client_id } if default_client_id
93
+
94
+ # Otherwise, return first client
95
+ all_clients.first
96
+ end
97
+
98
+ # Get the private key for a client configuration
99
+ # @param client_config [Hash] client configuration (may include '_store' key)
100
+ # @return [String, nil] private key content or nil if not found
101
+ def get_client_private_key(client_config)
102
+ client_id = client_config['id']
103
+ return nil unless client_id
104
+
105
+ get_private_key(client_id)
106
+ end
107
+
108
+ # Get the private key for a client
109
+ # @param client_id [String] client ID
110
+ # @return [String, nil] private key content or nil if not found
111
+ def get_private_key(client_id)
112
+ key_path = private_key_path(client_id)
113
+ return nil unless @environment.file_exists?(key_path)
114
+
115
+ begin
116
+ @environment.read_file(key_path)
117
+ rescue IOError
118
+ nil
119
+ end
120
+ end
121
+
122
+ # Store a client configuration
123
+ # @param client_config [Hash] client configuration
124
+ # @raise [ConfigurationError] if configuration cannot be saved
125
+ def store_client(client_config)
126
+ config = configuration
127
+ config['clients'] ||= []
128
+
129
+ # Remove existing client with same ID
130
+ config['clients'].reject! { |c| c['id'] == client_config['id'] }
131
+
132
+ # Add new client
133
+ config['clients'] << client_config
134
+
135
+ # Set as default if no default exists
136
+ config['defaultClient'] ||= client_config['id']
137
+
138
+ save_configuration(config)
139
+ end
140
+
141
+ # Store a private key for a client
142
+ # @param client_id [String] client ID
143
+ # @param private_key [String] private key content (PEM format)
144
+ # @raise [ConfigurationError] if private key cannot be saved
145
+ def store_private_key(client_id, private_key)
146
+ key_path = private_key_path(client_id)
147
+
148
+ begin
149
+ @environment.ensure_directory(File.dirname(key_path))
150
+ @environment.write_file(key_path, private_key)
151
+
152
+ # Set restrictive permissions on Unix-like systems
153
+ File.chmod(0o600, key_path) unless @environment.windows?
154
+ rescue IOError => e
155
+ raise ConfigurationError, "Cannot store private key: #{e.message}"
156
+ end
157
+ end
158
+
159
+ # Store an endpoint
160
+ # @param endpoint_name [String] endpoint name
161
+ # @param endpoint_url [String] endpoint URL
162
+ # @raise [ConfigurationError] if endpoint cannot be saved
163
+ def store_endpoint(endpoint_name, endpoint_url)
164
+ config = configuration
165
+ config['endpoints'] ||= {}
166
+ config['endpoints'][endpoint_name] = endpoint_url
167
+ save_configuration(config)
168
+ end
169
+
170
+ # Get the full path to the store directory
171
+ # @return [String] store directory path
172
+ def store_path
173
+ File.join(@base_path, STORE_DIR)
174
+ end
175
+
176
+ # Get the full path to the configuration file
177
+ # @return [String] configuration file path
178
+ def config_file_path
179
+ File.join(store_path, "#{@config_name}#{CONFIG_EXTENSION}")
180
+ end
181
+
182
+ # Get the full path to the private key directory
183
+ # @return [String] private key directory path
184
+ def private_key_directory
185
+ File.join(store_path, PRIVATE_DIR)
186
+ end
187
+
188
+ # Get the full path to a specific private key file
189
+ # @param client_id [String] client ID
190
+ # @return [String] private key file path
191
+ def private_key_path(client_id)
192
+ File.join(private_key_directory, "#{client_id}.key")
193
+ end
194
+
195
+ private
196
+
197
+ def save_configuration(config)
198
+ @environment.ensure_directory(store_path)
199
+ content = JSON.pretty_generate(config)
200
+ @environment.write_file(config_file_path, content)
201
+ rescue IOError => e
202
+ raise ConfigurationError, "Cannot save configuration: #{e.message}"
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,211 @@
1
+ require 'logger'
2
+
3
+ module Eryph
4
+ module ClientRuntime
5
+ # Reads configuration from multiple configuration stores
6
+ # Follows the hierarchy: Current Directory -> User -> System
7
+ class ConfigStoresReader
8
+ # @return [Environment] environment abstraction
9
+ attr_reader :environment
10
+
11
+ # @return [Logger] logger instance
12
+ attr_reader :logger
13
+
14
+ # Initialize the configuration stores reader
15
+ # @param environment [Environment] environment abstraction
16
+ # @param logger [Logger, nil] logger instance
17
+ def initialize(environment, logger: nil)
18
+ @environment = environment
19
+ @logger = logger || Logger.new($stdout).tap { |l| l.level = Logger::WARN }
20
+ end
21
+
22
+ # Get all configuration stores for the specified configuration name
23
+ # @param config_name [String] configuration name
24
+ # @return [Array<ConfigStore>] array of configuration stores in priority order
25
+ def get_stores(config_name)
26
+ locations = [
27
+ Environment::ConfigStoreLocation::CURRENT_DIRECTORY,
28
+ Environment::ConfigStoreLocation::USER,
29
+ Environment::ConfigStoreLocation::SYSTEM,
30
+ ]
31
+
32
+ locations.map do |location|
33
+ base_path = @environment.get_config_path(location)
34
+ ConfigStore.new(base_path, config_name, @environment)
35
+ end
36
+ end
37
+
38
+ # Get all existing configuration stores for the specified configuration name
39
+ # @param config_name [String] configuration name
40
+ # @return [Array<ConfigStore>] array of existing configuration stores in priority order
41
+ def get_existing_stores(config_name)
42
+ stores = get_stores(config_name)
43
+ existing = stores.select(&:exists?)
44
+ @logger.debug("get_existing_stores: config=#{config_name}, found #{existing.length} of #{stores.length} stores")
45
+ existing.each_with_index do |store, idx|
46
+ @logger.debug("get_existing_stores: [#{idx}] #{store.base_path}")
47
+ end
48
+ existing
49
+ end
50
+
51
+ # Get merged configuration from all stores
52
+ # @param config_name [String] configuration name
53
+ # @return [Hash] merged configuration data
54
+ def get_merged_configuration(config_name)
55
+ stores = get_existing_stores(config_name)
56
+ merged_config = {}
57
+
58
+ # Merge configurations in reverse order (system -> user -> current)
59
+ # so that higher priority stores override lower priority ones
60
+ stores.reverse.each do |store|
61
+ config = store.configuration
62
+ merged_config = deep_merge(merged_config, config)
63
+ end
64
+
65
+ merged_config
66
+ end
67
+
68
+ # Get all endpoints from all stores
69
+ # @param config_name [String] configuration name
70
+ # @return [Hash] endpoint name -> URL mapping from all stores
71
+ def get_all_endpoints(config_name)
72
+ merged_config = get_merged_configuration(config_name)
73
+ merged_config['endpoints'] || {}
74
+ end
75
+
76
+ # Get all clients from all stores
77
+ # @param config_name [String] configuration name
78
+ # @return [Array<Hash>] array of all client configurations
79
+ def get_all_clients(config_name)
80
+ @logger.debug("get_all_clients: config=#{config_name}")
81
+
82
+ stores = get_existing_stores(config_name)
83
+ all_clients = []
84
+
85
+ stores.each do |store|
86
+ clients = store.clients
87
+ @logger.debug("get_all_clients: store #{store.base_path} has #{clients ? clients.length : 0} clients")
88
+ next unless clients
89
+
90
+ clients.each do |client|
91
+ # Add store reference to client for private key lookup
92
+ client_with_store = client.dup
93
+ client_with_store['_store'] = store
94
+ all_clients << client_with_store
95
+ end
96
+ end
97
+
98
+ @logger.debug("get_all_clients: total clients before dedup=#{all_clients.length}")
99
+
100
+ # Remove duplicates by ID, keeping the highest priority (first found)
101
+ seen_ids = Set.new
102
+ unique_clients = all_clients.select do |client|
103
+ if seen_ids.include?(client['id'])
104
+ false
105
+ else
106
+ seen_ids.add(client['id'])
107
+ true
108
+ end
109
+ end
110
+
111
+ @logger.debug("get_all_clients: unique clients=#{unique_clients.length}")
112
+ unique_clients
113
+ end
114
+
115
+ # Get the default client configuration
116
+ # @param config_name [String] configuration name
117
+ # @return [Hash, nil] default client configuration or nil if not found
118
+ def get_default_client(config_name)
119
+ @logger.debug("get_default_client: config=#{config_name}")
120
+
121
+ merged_config = get_merged_configuration(config_name)
122
+ default_client_id = merged_config['defaultClientId']
123
+ @logger.debug("get_default_client: defaultClientId=#{default_client_id || 'nil'}")
124
+
125
+ all_clients = get_all_clients(config_name)
126
+ @logger.debug("get_default_client: total_clients=#{all_clients.length}")
127
+
128
+ if all_clients.empty?
129
+ @logger.debug('get_default_client: no clients found')
130
+ return nil
131
+ end
132
+
133
+ # If explicit defaultClient is set, use that
134
+ if default_client_id
135
+ client = all_clients.find { |client| client['id'] == default_client_id }
136
+ if client
137
+ @logger.debug("get_default_client: found by defaultClientId=#{client['id']}")
138
+ return client
139
+ else
140
+ @logger.debug("get_default_client: defaultClientId '#{default_client_id}' not found")
141
+ end
142
+ end
143
+
144
+ # Otherwise, look for client with IsDefault=true (PowerShell -AsDefault flag)
145
+ # Try both case variations since PowerShell uses IsDefault, Ruby might use isDefault
146
+ default_client = all_clients.find { |client| client['IsDefault'] == true || client['isDefault'] == true }
147
+ if default_client
148
+ @logger.debug("get_default_client: found by IsDefault flag=#{default_client['id']}")
149
+ return default_client
150
+ end
151
+
152
+ @logger.debug('get_default_client: no default client found')
153
+ nil
154
+ end
155
+
156
+ # Get a specific client configuration by ID
157
+ # @param config_name [String] configuration name
158
+ # @param client_id [String] client ID to lookup
159
+ # @return [Hash, nil] client configuration or nil if not found
160
+ def get_client(config_name, client_id)
161
+ all_clients = get_all_clients(config_name)
162
+ all_clients.find { |client| client['id'] == client_id }
163
+ end
164
+
165
+ # Get the private key for a client
166
+ # @param client_config [Hash] client configuration (must include '_store' key)
167
+ # @return [String, nil] private key content or nil if not found
168
+ def get_client_private_key(client_config)
169
+ store = client_config['_store']
170
+ return nil unless store
171
+
172
+ store.get_private_key(client_config['id'])
173
+ end
174
+
175
+ # Get the writable store for the current user
176
+ # @param config_name [String] configuration name
177
+ # @return [ConfigStore] user configuration store
178
+ def get_writable_store(config_name)
179
+ base_path = if @environment.admin_user?
180
+ # If running as admin, prefer system store
181
+ @environment.get_config_path(Environment::ConfigStoreLocation::SYSTEM)
182
+ else
183
+ # Otherwise use user store
184
+ @environment.get_config_path(Environment::ConfigStoreLocation::USER)
185
+ end
186
+
187
+ ConfigStore.new(base_path, config_name, @environment)
188
+ end
189
+
190
+ private
191
+
192
+ # Deep merge two hashes
193
+ # @param hash1 [Hash] first hash
194
+ # @param hash2 [Hash] second hash
195
+ # @return [Hash] merged hash
196
+ def deep_merge(hash1, hash2)
197
+ result = hash1.dup
198
+
199
+ hash2.each do |key, value|
200
+ result[key] = if result[key].is_a?(Hash) && value.is_a?(Hash)
201
+ deep_merge(result[key], value)
202
+ else
203
+ value
204
+ end
205
+ end
206
+
207
+ result
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,226 @@
1
+ require 'logger'
2
+
3
+ module Eryph
4
+ module ClientRuntime
5
+ # Looks up endpoint URLs from configuration stores
6
+ # Handles special configurations like 'zero' with local endpoint discovery
7
+ class EndpointLookup
8
+ # @return [ConfigStoresReader] configuration stores reader
9
+ attr_reader :reader
10
+
11
+ # @return [String] configuration name
12
+ attr_reader :config_name
13
+
14
+ # @return [Logger] logger instance
15
+ attr_reader :logger
16
+
17
+ # Initialize endpoint lookup
18
+ # @param reader [ConfigStoresReader] configuration stores reader
19
+ # @param config_name [String] configuration name
20
+ # @param logger [Logger, nil] logger instance
21
+ def initialize(reader, config_name, logger: nil)
22
+ @reader = reader
23
+ @config_name = config_name
24
+ @logger = logger || Logger.new($stdout).tap { |l| l.level = Logger::WARN }
25
+ end
26
+
27
+ # Get an endpoint URL by name
28
+ # @param endpoint_name [String] endpoint name to lookup
29
+ # @return [String, nil] endpoint URL or nil if not found
30
+ def endpoint(endpoint_name)
31
+ @logger.debug("get_endpoint: looking for '#{endpoint_name}' in config '#{@config_name}'")
32
+
33
+ # First try configuration store endpoints
34
+ store_endpoints = @reader.get_all_endpoints(@config_name)
35
+ @logger.debug("get_endpoint: store_endpoints count=#{store_endpoints.length}")
36
+
37
+ endpoint_url = store_endpoints[endpoint_name]
38
+ if endpoint_url
39
+ @logger.debug("get_endpoint: found in config store=#{endpoint_url}")
40
+ return endpoint_url
41
+ end
42
+
43
+ # Then try local endpoints for special configurations
44
+ local_eps = local_endpoints
45
+ @logger.debug("get_endpoint: local_endpoints count=#{local_eps.length}")
46
+
47
+ endpoint_url = local_eps[endpoint_name]
48
+ if endpoint_url
49
+ @logger.debug("get_endpoint: found in local endpoints=#{endpoint_url}")
50
+ else
51
+ @logger.debug('get_endpoint: not found anywhere')
52
+ end
53
+
54
+ endpoint_url
55
+ end
56
+
57
+ # Get all available endpoints
58
+ # @return [Hash] endpoint name -> URL mapping
59
+ def all_endpoints
60
+ store_endpoints = @reader.get_all_endpoints(@config_name)
61
+ local_eps = local_endpoints
62
+
63
+ # Local endpoints have lower priority than store endpoints
64
+ local_eps.merge(store_endpoints)
65
+ end
66
+
67
+ # Check if an endpoint exists
68
+ # @param endpoint_name [String] endpoint name to check
69
+ # @return [Boolean] true if endpoint exists
70
+ def endpoint_exists?(endpoint_name)
71
+ !endpoint(endpoint_name).nil?
72
+ end
73
+
74
+ private
75
+
76
+ # Get local endpoints for special configurations
77
+ # @return [Hash] endpoint name -> URL mapping
78
+ def local_endpoints
79
+ @logger.debug("get_local_endpoints: config=#{@config_name}")
80
+
81
+ case @config_name&.downcase
82
+ when 'zero'
83
+ @logger.debug('get_local_endpoints: checking zero config')
84
+ zero_endpoints
85
+ when 'local'
86
+ @logger.debug('get_local_endpoints: checking local config')
87
+ get_local_endpoints_for_config('local')
88
+ else
89
+ @logger.debug('get_local_endpoints: no special config, returning empty')
90
+ {}
91
+ end
92
+ end
93
+
94
+ # Get endpoints for eryph-zero configuration
95
+ # This discovers running eryph-zero instances from runtime lock files
96
+ # @return [Hash] endpoint name -> URL mapping
97
+ def zero_endpoints
98
+ require_relative 'local_identity_provider_info'
99
+
100
+ # Try to discover running identity provider
101
+ provider_info = LocalIdentityProviderInfo.new(@reader.environment, 'zero', logger: @logger)
102
+
103
+ if provider_info.running?
104
+ endpoints_hash = provider_info.endpoints
105
+
106
+ # Convert URI objects to strings and map to expected names
107
+ result = {}
108
+ endpoints_hash.each do |name, uri|
109
+ case name.downcase
110
+ when 'identity'
111
+ result['identity'] = uri.to_s
112
+ when 'compute'
113
+ result['compute'] = uri.to_s
114
+ else
115
+ # Include other endpoints as-is
116
+ result[name] = uri.to_s
117
+ end
118
+ end
119
+
120
+ # If we have identity but no compute, derive compute endpoint
121
+ if result['identity'] && !result['compute']
122
+ identity_uri = URI.parse(result['identity'])
123
+ result['compute'] = "#{identity_uri.scheme}://#{identity_uri.host}:#{identity_uri.port}/compute"
124
+ end
125
+
126
+ return result
127
+ end
128
+
129
+ # Fallback to common local endpoints if no runtime info found
130
+ fallback_zero_endpoints
131
+ end
132
+
133
+ # Fallback endpoint discovery for eryph-zero when runtime file is not available
134
+ # @return [Hash] endpoint name -> URL mapping
135
+ def fallback_zero_endpoints
136
+ endpoints = {}
137
+
138
+ # Try common local endpoints for eryph-zero
139
+ zero_candidates = [
140
+ 'https://localhost:8080',
141
+ 'https://127.0.0.1:8080',
142
+ 'http://localhost:8080',
143
+ 'http://127.0.0.1:8080',
144
+ ]
145
+
146
+ zero_candidates.each do |candidate_url|
147
+ next unless test_zero_endpoint(candidate_url)
148
+
149
+ endpoints['identity'] = candidate_url
150
+ endpoints['compute'] = "#{candidate_url}/compute"
151
+ break
152
+ end
153
+
154
+ endpoints
155
+ end
156
+
157
+ # Get endpoints for eryph-local configuration
158
+ # This discovers running eryph-local instances from runtime lock files
159
+ # @return [Hash] endpoint name -> URL mapping
160
+ def get_local_endpoints_for_config(config_name)
161
+ require_relative 'local_identity_provider_info'
162
+
163
+ # Try to discover running identity provider
164
+ provider_info = LocalIdentityProviderInfo.new(@reader.environment, config_name, logger: @logger)
165
+
166
+ if provider_info.running?
167
+ endpoints_hash = provider_info.endpoints
168
+
169
+ # Convert URI objects to strings and map to expected names
170
+ result = {}
171
+ endpoints_hash.each do |name, uri|
172
+ case name.downcase
173
+ when 'identity'
174
+ result['identity'] = uri.to_s
175
+ when 'compute'
176
+ result['compute'] = uri.to_s
177
+ else
178
+ # Include other endpoints as-is
179
+ result[name] = uri.to_s
180
+ end
181
+ end
182
+
183
+ # If we have identity but no compute, derive compute endpoint
184
+ if result['identity'] && !result['compute']
185
+ identity_uri = URI.parse(result['identity'])
186
+ result['compute'] = "#{identity_uri.scheme}://#{identity_uri.host}:#{identity_uri.port}/compute"
187
+ end
188
+
189
+ return result
190
+ end
191
+
192
+ # No fallback for local config - if not running, return empty
193
+ {}
194
+ end
195
+
196
+ # Test if a URL hosts an eryph-zero instance
197
+ # @param base_url [String] base URL to test
198
+ # @return [Boolean] true if eryph-zero is detected
199
+ def test_zero_endpoint(base_url)
200
+ # This is a simplified test - in a full implementation you might:
201
+ # 1. Make an HTTP request to check for eryph-zero health endpoint
202
+ # 2. Check for specific response headers or content
203
+ # 3. Verify SSL certificates if applicable
204
+
205
+ # In test environments, don't activate fallback endpoints
206
+ # This prevents tests from accidentally finding "running" services
207
+ # when they're testing the "not running" scenario
208
+ return false if @reader.environment.instance_of?(::TestEnvironment)
209
+
210
+ # For now, we'll just check if the URL format is valid
211
+ # In production, this might make actual HTTP requests
212
+ begin
213
+ uri = URI.parse(base_url)
214
+ # Check if scheme, host, and explicit port are present
215
+ has_scheme = !uri.scheme.nil?
216
+ has_host = !uri.host.nil? && !uri.host.empty?
217
+ has_explicit_port = base_url.include?(':') && base_url.match(/:(\d+)/)
218
+
219
+ !(has_scheme && has_host && has_explicit_port).nil?
220
+ rescue URI::InvalidURIError
221
+ false
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end