makit 0.0.144 → 0.0.145

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 (165) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -41
  3. data/exe/makit +5 -5
  4. data/lib/makit/apache.rb +28 -28
  5. data/lib/makit/auto.rb +48 -48
  6. data/lib/makit/cli/base.rb +17 -0
  7. data/lib/makit/cli/build_commands.rb +500 -500
  8. data/lib/makit/cli/generators/base_generator.rb +74 -74
  9. data/lib/makit/cli/generators/dotnet_generator.rb +50 -50
  10. data/lib/makit/cli/generators/generator_factory.rb +49 -49
  11. data/lib/makit/cli/generators/node_generator.rb +50 -50
  12. data/lib/makit/cli/generators/ruby_generator.rb +77 -77
  13. data/lib/makit/cli/generators/rust_generator.rb +50 -50
  14. data/lib/makit/cli/generators/templates/dotnet_templates.rb +167 -167
  15. data/lib/makit/cli/generators/templates/node_templates.rb +161 -161
  16. data/lib/makit/cli/generators/templates/ruby/gemfile.rb +26 -26
  17. data/lib/makit/cli/generators/templates/ruby/gemspec.rb +41 -40
  18. data/lib/makit/cli/generators/templates/ruby/main_lib.rb +33 -33
  19. data/lib/makit/cli/generators/templates/ruby/rakefile.rb +35 -35
  20. data/lib/makit/cli/generators/templates/ruby/readme.rb +63 -63
  21. data/lib/makit/cli/generators/templates/ruby/test.rb +39 -39
  22. data/lib/makit/cli/generators/templates/ruby/test_helper.rb +29 -29
  23. data/lib/makit/cli/generators/templates/ruby/version.rb +29 -29
  24. data/lib/makit/cli/generators/templates/rust_templates.rb +128 -128
  25. data/lib/makit/cli/main.rb +78 -69
  26. data/lib/makit/cli/pipeline_commands.rb +311 -0
  27. data/lib/makit/cli/project_commands.rb +868 -868
  28. data/lib/makit/cli/repository_commands.rb +661 -661
  29. data/lib/makit/cli/strategy_commands.rb +207 -212
  30. data/lib/makit/cli/utility_commands.rb +521 -521
  31. data/lib/makit/commands/factory.rb +359 -359
  32. data/lib/makit/commands/middleware/base.rb +73 -73
  33. data/lib/makit/commands/middleware/cache.rb +248 -248
  34. data/lib/makit/commands/middleware/command_logger.rb +312 -312
  35. data/lib/makit/commands/middleware/validator.rb +269 -269
  36. data/lib/makit/commands/request.rb +316 -316
  37. data/lib/makit/commands/result.rb +323 -323
  38. data/lib/makit/commands/runner.rb +386 -386
  39. data/lib/makit/commands/strategies/base.rb +171 -171
  40. data/lib/makit/commands/strategies/child_process.rb +162 -162
  41. data/lib/makit/commands/strategies/factory.rb +136 -136
  42. data/lib/makit/commands/strategies/synchronous.rb +139 -139
  43. data/lib/makit/commands.rb +50 -50
  44. data/lib/makit/configuration/dotnet_project.rb +48 -48
  45. data/lib/makit/configuration/gitlab_helper.rb +61 -58
  46. data/lib/makit/configuration/project.rb +446 -168
  47. data/lib/makit/configuration/rakefile_helper.rb +43 -43
  48. data/lib/makit/configuration/step.rb +34 -34
  49. data/lib/makit/configuration/timeout.rb +74 -74
  50. data/lib/makit/configuration.rb +21 -16
  51. data/lib/makit/content/default_gitignore.rb +7 -7
  52. data/lib/makit/content/default_gitignore.txt +225 -225
  53. data/lib/makit/content/default_rakefile.rb +13 -13
  54. data/lib/makit/content/gem_rakefile.rb +16 -16
  55. data/lib/makit/context.rb +1 -1
  56. data/lib/makit/data.rb +49 -49
  57. data/lib/makit/directories.rb +140 -140
  58. data/lib/makit/directory.rb +262 -262
  59. data/lib/makit/docs/files.rb +89 -89
  60. data/lib/makit/docs/rake.rb +102 -102
  61. data/lib/makit/dotnet/cli.rb +69 -69
  62. data/lib/makit/dotnet/project.rb +217 -217
  63. data/lib/makit/dotnet/solution.rb +38 -38
  64. data/lib/makit/dotnet/solution_classlib.rb +239 -239
  65. data/lib/makit/dotnet/solution_console.rb +264 -264
  66. data/lib/makit/dotnet/solution_maui.rb +354 -354
  67. data/lib/makit/dotnet/solution_wasm.rb +275 -275
  68. data/lib/makit/dotnet/solution_wpf.rb +304 -304
  69. data/lib/makit/dotnet.rb +102 -102
  70. data/lib/makit/email.rb +90 -90
  71. data/lib/makit/environment.rb +142 -142
  72. data/lib/makit/examples/runner.rb +370 -370
  73. data/lib/makit/exceptions.rb +45 -45
  74. data/lib/makit/fileinfo.rb +32 -24
  75. data/lib/makit/files.rb +43 -43
  76. data/lib/makit/gems.rb +40 -40
  77. data/lib/makit/git/cli.rb +54 -54
  78. data/lib/makit/git/repository.rb +266 -90
  79. data/lib/makit/git.rb +104 -98
  80. data/lib/makit/gitlab/pipeline.rb +857 -0
  81. data/lib/makit/gitlab/pipeline_service_impl.rb +1536 -0
  82. data/lib/makit/gitlab_runner.rb +59 -59
  83. data/lib/makit/humanize.rb +218 -137
  84. data/lib/makit/indexer.rb +47 -47
  85. data/lib/makit/io/filesystem.rb +111 -0
  86. data/lib/makit/io/filesystem_service_impl.rb +337 -0
  87. data/lib/makit/logging/configuration.rb +308 -308
  88. data/lib/makit/logging/format_registry.rb +84 -84
  89. data/lib/makit/logging/formatters/base.rb +39 -39
  90. data/lib/makit/logging/formatters/console_formatter.rb +140 -140
  91. data/lib/makit/logging/formatters/json_formatter.rb +65 -65
  92. data/lib/makit/logging/formatters/plain_text_formatter.rb +71 -71
  93. data/lib/makit/logging/formatters/text_formatter.rb +64 -64
  94. data/lib/makit/logging/log_request.rb +119 -119
  95. data/lib/makit/logging/logger.rb +199 -199
  96. data/lib/makit/logging/sinks/base.rb +91 -91
  97. data/lib/makit/logging/sinks/console.rb +72 -72
  98. data/lib/makit/logging/sinks/file_sink.rb +92 -92
  99. data/lib/makit/logging/sinks/structured.rb +123 -123
  100. data/lib/makit/logging/sinks/unified_file_sink.rb +296 -296
  101. data/lib/makit/logging.rb +565 -565
  102. data/lib/makit/markdown.rb +75 -75
  103. data/lib/makit/mp/basic_object_mp.rb +17 -17
  104. data/lib/makit/mp/command_mp.rb +13 -13
  105. data/lib/makit/mp/command_request.mp.rb +17 -17
  106. data/lib/makit/mp/project_mp.rb +199 -199
  107. data/lib/makit/mp/string_mp.rb +205 -199
  108. data/lib/makit/nuget.rb +74 -74
  109. data/lib/makit/podman/podman.rb +458 -0
  110. data/lib/makit/podman/podman_service_impl.rb +1081 -0
  111. data/lib/makit/port.rb +32 -32
  112. data/lib/makit/process.rb +377 -377
  113. data/lib/makit/protoc.rb +112 -107
  114. data/lib/makit/rake/cli.rb +196 -196
  115. data/lib/makit/rake/trace_controller.rb +174 -174
  116. data/lib/makit/rake.rb +81 -81
  117. data/lib/makit/ruby/cli.rb +185 -185
  118. data/lib/makit/ruby.rb +25 -25
  119. data/lib/makit/secrets.rb +51 -51
  120. data/lib/makit/serializer.rb +130 -130
  121. data/lib/makit/services/builder.rb +186 -186
  122. data/lib/makit/services/error_handler.rb +226 -226
  123. data/lib/makit/services/repository_manager.rb +367 -231
  124. data/lib/makit/services/validator.rb +112 -112
  125. data/lib/makit/setup/classlib.rb +101 -101
  126. data/lib/makit/setup/gem.rb +268 -268
  127. data/lib/makit/setup/pages.rb +11 -11
  128. data/lib/makit/setup/razorclasslib.rb +101 -101
  129. data/lib/makit/setup/runner.rb +54 -54
  130. data/lib/makit/setup.rb +5 -5
  131. data/lib/makit/show.rb +110 -110
  132. data/lib/makit/storage.rb +126 -126
  133. data/lib/makit/symbols.rb +175 -170
  134. data/lib/makit/task_info.rb +130 -130
  135. data/lib/makit/tasks/at_exit.rb +15 -15
  136. data/lib/makit/tasks/build.rb +22 -22
  137. data/lib/makit/tasks/clean.rb +13 -13
  138. data/lib/makit/tasks/configure.rb +10 -10
  139. data/lib/makit/tasks/format.rb +10 -10
  140. data/lib/makit/tasks/hook_manager.rb +443 -443
  141. data/lib/makit/tasks/init.rb +49 -49
  142. data/lib/makit/tasks/integrate.rb +29 -29
  143. data/lib/makit/tasks/pull_incoming.rb +13 -13
  144. data/lib/makit/tasks/setup.rb +16 -16
  145. data/lib/makit/tasks/sync.rb +17 -17
  146. data/lib/makit/tasks/tag.rb +16 -16
  147. data/lib/makit/tasks/task_monkey_patch.rb +81 -81
  148. data/lib/makit/tasks/test.rb +22 -22
  149. data/lib/makit/tasks/update.rb +18 -18
  150. data/lib/makit/tasks.rb +20 -20
  151. data/lib/makit/test_cache.rb +239 -239
  152. data/lib/makit/tree.rb +37 -37
  153. data/lib/makit/v1/configuration/project_service_impl.rb +371 -0
  154. data/lib/makit/v1/git/git_repository_service_impl.rb +295 -0
  155. data/lib/makit/v1/makit.v1_pb.rb +35 -35
  156. data/lib/makit/v1/makit.v1_services_pb.rb +27 -27
  157. data/lib/makit/v1/services/repository_manager_service_impl.rb +572 -0
  158. data/lib/makit/version.rb +100 -100
  159. data/lib/makit/version_util.rb +21 -21
  160. data/lib/makit/wix.rb +95 -95
  161. data/lib/makit/yaml.rb +29 -29
  162. data/lib/makit/zip.rb +17 -17
  163. data/lib/makit copy.rb +44 -44
  164. data/lib/makit.rb +111 -43
  165. metadata +61 -36
@@ -0,0 +1,1081 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+ require "securerandom"
6
+
7
+ module Makit
8
+ module Podman
9
+ class PodmanServiceImpl
10
+ def initialize(podman_executable: "podman")
11
+ @podman_executable = find_executable(podman_executable)
12
+ raise "Podman executable not found" unless @podman_executable
13
+ end
14
+
15
+ # Check if gRPC service is available
16
+ def self.grpc_available?
17
+ defined?(Podman::Service::PodmanService)
18
+ end
19
+
20
+ # Image operations
21
+ def pull_image(request, _unused_call = nil)
22
+ cmd = [@podman_executable, "pull"]
23
+ cmd << "--force" if request.respond_to?(:force) ? request.force : request[:force]
24
+ cmd << "#{request.respond_to?(:image_name) ? request.image_name : request[:image_name]}:#{request.respond_to?(:tag) ? request.tag : request[:tag]}"
25
+
26
+ result = execute_shell_command(cmd.join(" "))
27
+
28
+ if result[:success]
29
+ # Extract image ID from output
30
+ image_id = extract_image_id(result[:stdout])
31
+ if grpc_available?
32
+ Podman::Service::PullImageResponse.new(
33
+ success: true,
34
+ image_id: image_id,
35
+ layers: extract_layers(result[:stdout]),
36
+ pulled_at: Time.now
37
+ )
38
+ else
39
+ {
40
+ success: true,
41
+ image_id: image_id,
42
+ layers: extract_layers(result[:stdout]),
43
+ pulled_at: Time.now
44
+ }
45
+ end
46
+ else
47
+ if grpc_available?
48
+ Podman::Service::PullImageResponse.new(
49
+ success: false,
50
+ error_message: result[:stderr]
51
+ )
52
+ else
53
+ {
54
+ success: false,
55
+ error_message: result[:stderr]
56
+ }
57
+ end
58
+ end
59
+ end
60
+
61
+ def list_images(request, _unused_call = nil)
62
+ cmd = [@podman_executable, "images", "--format", "json"]
63
+ cmd << "--all" if request.respond_to?(:all) ? request.all : request[:all]
64
+
65
+ result = execute_shell_command(cmd.join(" "))
66
+
67
+ if result[:success]
68
+ images_data = JSON.parse(result[:stdout])
69
+ images = images_data.map do |img|
70
+ # Extract repository and tag from Names array
71
+ names = img["Names"] || []
72
+ repository = "unknown"
73
+ tag = "latest"
74
+
75
+ if names.any?
76
+ name_parts = names.first.split(":")
77
+ repository = name_parts[0]
78
+ tag = name_parts[1] || "latest"
79
+ end
80
+
81
+ created_time = if img["Created"].is_a?(String)
82
+ Time.parse(img["Created"])
83
+ else
84
+ Time.at(img["Created"])
85
+ end
86
+
87
+ if grpc_available?
88
+ Podman::Service::Image.new(
89
+ id: img["Id"],
90
+ repository: repository,
91
+ tag: tag,
92
+ digest: img["Digest"],
93
+ created: created_time,
94
+ size: img["Size"].to_i,
95
+ labels: img["Labels"] || {}
96
+ )
97
+ else
98
+ {
99
+ id: img["Id"],
100
+ repository: repository,
101
+ tag: tag,
102
+ digest: img["Digest"],
103
+ created: created_time,
104
+ size: img["Size"].to_i,
105
+ labels: img["Labels"] || {}
106
+ }
107
+ end
108
+ end
109
+
110
+ if grpc_available?
111
+ Podman::Service::ListImagesResponse.new(
112
+ images: images,
113
+ total_count: images.length
114
+ )
115
+ else
116
+ {
117
+ images: images,
118
+ total_count: images.length
119
+ }
120
+ end
121
+ else
122
+ if grpc_available?
123
+ Podman::Service::ListImagesResponse.new(
124
+ images: [],
125
+ total_count: 0
126
+ )
127
+ else
128
+ {
129
+ images: [],
130
+ total_count: 0
131
+ }
132
+ end
133
+ end
134
+ end
135
+
136
+ def remove_image(request, _unused_call = nil)
137
+ cmd = [@podman_executable, "rmi"]
138
+ cmd << "--force" if request.respond_to?(:force) ? request.force : request[:force]
139
+ cmd << (request.respond_to?(:image_id) ? request.image_id : request[:image_id])
140
+
141
+ result = execute_shell_command(cmd.join(" "))
142
+
143
+ if grpc_available?
144
+ Podman::Service::RemoveImageResponse.new(
145
+ success: result[:success],
146
+ error_message: result[:success] ? "" : result[:stderr]
147
+ )
148
+ else
149
+ {
150
+ success: result[:success],
151
+ error_message: result[:success] ? "" : result[:stderr]
152
+ }
153
+ end
154
+ end
155
+
156
+ def inspect_image(request, _unused_call = nil)
157
+ cmd = [@podman_executable, "inspect", request.respond_to?(:image_id) ? request.image_id : request[:image_id]]
158
+ result = execute_shell_command(cmd.join(" "))
159
+
160
+ if grpc_available?
161
+ Podman::Service::InspectImageResponse.new(
162
+ success: result[:success],
163
+ image_info: result[:success] ? result[:stdout] : "",
164
+ error_message: result[:success] ? "" : result[:stderr]
165
+ )
166
+ else
167
+ {
168
+ success: result[:success],
169
+ image_info: result[:success] ? result[:stdout] : "",
170
+ error_message: result[:success] ? "" : result[:stderr]
171
+ }
172
+ end
173
+ end
174
+
175
+ # Container operations
176
+ def create_container(request, _unused_call = nil)
177
+ cmd = [@podman_executable, "create"]
178
+ name = request.respond_to?(:name) ? request.name : request[:name]
179
+ working_directory = request.respond_to?(:working_directory) ? request.working_directory : request[:working_directory]
180
+ user = request.respond_to?(:user) ? request.user : request[:user]
181
+ privileged = request.respond_to?(:privileged) ? request.privileged : request[:privileged]
182
+ auto_remove = request.respond_to?(:auto_remove) ? request.auto_remove : request[:auto_remove]
183
+ environment = request.respond_to?(:environment) ? request.environment : request[:environment]
184
+ volume_mounts = request.respond_to?(:volume_mounts) ? request.volume_mounts : request[:volume_mounts]
185
+ port_mappings = request.respond_to?(:port_mappings) ? request.port_mappings : request[:port_mappings]
186
+ labels = request.respond_to?(:labels) ? request.labels : request[:labels]
187
+ image = request.respond_to?(:image) ? request.image : request[:image]
188
+ command = request.respond_to?(:command) ? request.command : request[:command]
189
+ args = request.respond_to?(:args) ? request.args : request[:args]
190
+
191
+ cmd << "--name" << name if name
192
+ cmd << "--workdir" << working_directory if working_directory
193
+ cmd << "--user" << user if user
194
+ cmd << "--privileged" if privileged
195
+ cmd << "--rm" if auto_remove
196
+
197
+ # Add environment variables
198
+ (environment || {}).each do |key, value|
199
+ cmd << "--env" << "#{key}=#{value}"
200
+ end
201
+
202
+ # Add volume mounts
203
+ (volume_mounts || []).each do |mount|
204
+ host_path = mount.respond_to?(:host_path) ? mount.host_path : mount[:host_path]
205
+ container_path = mount.respond_to?(:container_path) ? mount.container_path : mount[:container_path]
206
+ mode = mount.respond_to?(:mode) ? mount.mode : mount[:mode]
207
+
208
+ # Convert Windows paths to WSL paths on Windows
209
+ if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
210
+ host_path = convert_to_wsl_path(host_path)
211
+ end
212
+
213
+ cmd << "--volume" << "#{host_path}:#{container_path}:#{mode}"
214
+ end
215
+
216
+ # Add port mappings
217
+ (port_mappings || []).each do |port|
218
+ host_port = port.respond_to?(:host_port) ? port.host_port : port[:host_port]
219
+ container_port = port.respond_to?(:container_port) ? port.container_port : port[:container_port]
220
+ protocol = port.respond_to?(:protocol) ? port.protocol : port[:protocol]
221
+ cmd << "--publish" << "#{host_port}:#{container_port}/#{protocol}"
222
+ end
223
+
224
+ # Add labels
225
+ (labels || {}).each do |key, value|
226
+ cmd << "--label" << "#{key}=#{value}"
227
+ end
228
+
229
+ cmd << image
230
+
231
+ # Add command and args
232
+ if command && command.any?
233
+ cmd.concat(command)
234
+ end
235
+ if args && args.any?
236
+ cmd.concat(args)
237
+ end
238
+
239
+ result = execute_shell_command(cmd.join(" "))
240
+
241
+ if result[:success]
242
+ container_id = result[:stdout].strip
243
+ if grpc_available?
244
+ Podman::Service::CreateContainerResponse.new(
245
+ success: true,
246
+ container_id: container_id,
247
+ container_name: request.respond_to?(:name) ? request.name : request[:name]
248
+ )
249
+ else
250
+ {
251
+ success: true,
252
+ container_id: container_id,
253
+ container_name: request.respond_to?(:name) ? request.name : request[:name]
254
+ }
255
+ end
256
+ else
257
+ if grpc_available?
258
+ Podman::Service::CreateContainerResponse.new(
259
+ success: false,
260
+ error_message: result[:stderr]
261
+ )
262
+ else
263
+ {
264
+ success: false,
265
+ error_message: result[:stderr]
266
+ }
267
+ end
268
+ end
269
+ end
270
+
271
+ def start_container(request, _unused_call = nil)
272
+ cmd = [@podman_executable, "start", request.respond_to?(:container_id) ? request.container_id : request[:container_id]]
273
+ result = execute_shell_command(cmd.join(" "))
274
+
275
+ if grpc_available?
276
+ Podman::Service::StartContainerResponse.new(
277
+ success: result[:success],
278
+ error_message: result[:success] ? "" : result[:stderr]
279
+ )
280
+ else
281
+ {
282
+ success: result[:success],
283
+ error_message: result[:success] ? "" : result[:stderr]
284
+ }
285
+ end
286
+ end
287
+
288
+ def stop_container(request, _unused_call = nil)
289
+ cmd = [@podman_executable, "stop"]
290
+ timeout_seconds = request.respond_to?(:timeout_seconds) ? request.timeout_seconds : request[:timeout_seconds]
291
+ cmd << "--time" << timeout_seconds.to_s if timeout_seconds && timeout_seconds > 0
292
+ cmd << (request.respond_to?(:container_id) ? request.container_id : request[:container_id])
293
+
294
+ result = execute_shell_command(cmd.join(" "))
295
+
296
+ if grpc_available?
297
+ Podman::Service::StopContainerResponse.new(
298
+ success: result[:success],
299
+ error_message: result[:success] ? "" : result[:stderr]
300
+ )
301
+ else
302
+ {
303
+ success: result[:success],
304
+ error_message: result[:success] ? "" : result[:stderr]
305
+ }
306
+ end
307
+ end
308
+
309
+ def remove_container(request, _unused_call = nil)
310
+ cmd = [@podman_executable, "rm"]
311
+ cmd << "--force" if request.respond_to?(:force) ? request.force : request[:force]
312
+ cmd << (request.respond_to?(:container_id) ? request.container_id : request[:container_id])
313
+
314
+ result = execute_shell_command(cmd.join(" "))
315
+
316
+ if grpc_available?
317
+ Podman::Service::RemoveContainerResponse.new(
318
+ success: result[:success],
319
+ error_message: result[:success] ? "" : result[:stderr]
320
+ )
321
+ else
322
+ {
323
+ success: result[:success],
324
+ error_message: result[:success] ? "" : result[:stderr]
325
+ }
326
+ end
327
+ end
328
+
329
+ def list_containers(request, _unused_call = nil)
330
+ cmd = [@podman_executable, "ps", "--format", "json"]
331
+ cmd << "--all" if request.respond_to?(:all) ? request.all : request[:all]
332
+
333
+ result = execute_shell_command(cmd.join(" "))
334
+
335
+ if result[:success]
336
+ containers_data = JSON.parse(result[:stdout])
337
+ containers = containers_data.map do |container|
338
+ if grpc_available?
339
+ Podman::Service::Container.new(
340
+ id: container["Id"],
341
+ name: container["Names"][0],
342
+ image: container["Image"],
343
+ status: map_container_status(container["State"]),
344
+ created: parse_time(container["CreatedAt"]),
345
+ started: container["StartedAt"] ? parse_time(container["StartedAt"]) : nil,
346
+ finished: container["FinishedAt"] ? parse_time(container["FinishedAt"]) : nil,
347
+ command: container["Command"] ? [container["Command"]] : [],
348
+ environment: container["Env"] || {},
349
+ labels: container["Labels"] || {}
350
+ )
351
+ else
352
+ {
353
+ id: container["Id"],
354
+ name: container["Names"][0],
355
+ image: container["Image"],
356
+ status: map_container_status(container["State"]),
357
+ created: parse_time(container["CreatedAt"]),
358
+ started: container["StartedAt"] ? parse_time(container["StartedAt"]) : nil,
359
+ finished: container["FinishedAt"] ? parse_time(container["FinishedAt"]) : nil,
360
+ command: container["Command"] ? [container["Command"]] : [],
361
+ environment: container["Env"] || {},
362
+ labels: container["Labels"] || {}
363
+ }
364
+ end
365
+ end
366
+
367
+ if grpc_available?
368
+ Podman::Service::ListContainersResponse.new(
369
+ containers: containers,
370
+ total_count: containers.length
371
+ )
372
+ else
373
+ {
374
+ containers: containers,
375
+ total_count: containers.length
376
+ }
377
+ end
378
+ else
379
+ if grpc_available?
380
+ Podman::Service::ListContainersResponse.new(
381
+ containers: [],
382
+ total_count: 0
383
+ )
384
+ else
385
+ {
386
+ containers: [],
387
+ total_count: 0
388
+ }
389
+ end
390
+ end
391
+ end
392
+
393
+ def inspect_container(request, _unused_call = nil)
394
+ cmd = [@podman_executable, "inspect", request.respond_to?(:container_id) ? request.container_id : request[:container_id]]
395
+ result = execute_shell_command(cmd.join(" "))
396
+
397
+ if grpc_available?
398
+ Podman::Service::InspectContainerResponse.new(
399
+ success: result[:success],
400
+ container_info: result[:success] ? result[:stdout] : "",
401
+ error_message: result[:success] ? "" : result[:stderr]
402
+ )
403
+ else
404
+ {
405
+ success: result[:success],
406
+ container_info: result[:success] ? result[:stdout] : "",
407
+ error_message: result[:success] ? "" : result[:stderr]
408
+ }
409
+ end
410
+ end
411
+
412
+ # Execution operations
413
+ def execute_script(request, _unused_call = nil)
414
+ # Create temporary script file
415
+ script_file = "/tmp/#{SecureRandom.uuid}.sh"
416
+
417
+ begin
418
+ # Ensure /tmp directory exists
419
+ mkdir_cmd = [@podman_executable, "exec", request.respond_to?(:container_id) ? request.container_id : request[:container_id], "mkdir", "-p", "/tmp"]
420
+ execute_shell_command(mkdir_cmd.join(" "))
421
+
422
+ # Write script to a temporary file on host and mount it
423
+ require 'tempfile'
424
+ script_content = request.respond_to?(:script_content) ? request.script_content : request[:script_content]
425
+ # Normalize line endings to Unix format for container execution
426
+ script_content_normalized = script_content.gsub(/\r\n/, "\n").gsub(/\r/, "\n")
427
+ temp_file = Tempfile.new(['script', '.sh'], binmode: true)
428
+ temp_file.write(script_content_normalized)
429
+ temp_file.close
430
+
431
+ # Copy the script to the container
432
+ copy_cmd = [@podman_executable, "cp", temp_file.path, "#{request.respond_to?(:container_id) ? request.container_id : request[:container_id]}:#{script_file}"]
433
+ write_result = execute_shell_command(copy_cmd.join(" "))
434
+
435
+ # Clean up temp file
436
+ temp_file.unlink
437
+ return create_error_response("Failed to write script") unless write_result[:success]
438
+
439
+ # Make script executable
440
+ chmod_cmd = [@podman_executable, "exec", request.respond_to?(:container_id) ? request.container_id : request[:container_id], "chmod", "+x", script_file]
441
+ execute_shell_command(chmod_cmd.join(" "))
442
+
443
+ # Execute script
444
+ exec_cmd = [@podman_executable, "exec"]
445
+ exec_cmd << "--workdir" << (request.respond_to?(:working_directory) ? request.working_directory : request[:working_directory]) if request.respond_to?(:working_directory) ? request.working_directory : request[:working_directory]
446
+ exec_cmd << "--user" << (request.respond_to?(:user) ? request.user : request[:user]) if request.respond_to?(:user) ? request.user : request[:user]
447
+
448
+ (request.respond_to?(:environment) ? request.environment : request[:environment]).each do |key, value|
449
+ exec_cmd << "--env" << "#{key}=#{value}"
450
+ end
451
+
452
+ exec_cmd << (request.respond_to?(:container_id) ? request.container_id : request[:container_id]) << "sh" << script_file
453
+
454
+ result = execute_command_with_timeout(exec_cmd.join(" "), request.respond_to?(:timeout_seconds) ? request.timeout_seconds : request[:timeout_seconds])
455
+
456
+ if grpc_available?
457
+ Podman::Service::ExecuteScriptResponse.new(
458
+ success: result[:success],
459
+ exit_code: result[:exit_code],
460
+ stdout: (request.respond_to?(:capture_output) ? request.capture_output : request[:capture_output]) ? result[:stdout] : "",
461
+ stderr: (request.respond_to?(:capture_errors) ? request.capture_errors : request[:capture_errors]) ? result[:stderr] : "",
462
+ started_at: result[:started_at],
463
+ finished_at: result[:finished_at],
464
+ duration_seconds: result[:duration_seconds],
465
+ error_message: result[:error_message]
466
+ )
467
+ else
468
+ {
469
+ success: result[:success],
470
+ exit_code: result[:exit_code],
471
+ stdout: (request.respond_to?(:capture_output) ? request.capture_output : request[:capture_output]) ? result[:stdout] : "",
472
+ stderr: (request.respond_to?(:capture_errors) ? request.capture_errors : request[:capture_errors]) ? result[:stderr] : "",
473
+ started_at: result[:started_at],
474
+ finished_at: result[:finished_at],
475
+ duration_seconds: result[:duration_seconds],
476
+ error_message: result[:error_message]
477
+ }
478
+ end
479
+
480
+ ensure
481
+ # Cleanup script file
482
+ cleanup_cmd = [@podman_executable, "exec", request.respond_to?(:container_id) ? request.container_id : request[:container_id], "rm", "-f", script_file]
483
+ execute_shell_command(cleanup_cmd.join(" "))
484
+ end
485
+ end
486
+
487
+ def execute_command(request, _unused_call = nil)
488
+ cmd = [@podman_executable, "exec"]
489
+ working_directory = request.respond_to?(:working_directory) ? request.working_directory : request[:working_directory]
490
+ user = request.respond_to?(:user) ? request.user : request[:user]
491
+ environment = request.respond_to?(:environment) ? request.environment : request[:environment]
492
+ container_id = request.respond_to?(:container_id) ? request.container_id : request[:container_id]
493
+ command = request.respond_to?(:command) ? request.command : request[:command]
494
+ timeout_seconds = request.respond_to?(:timeout_seconds) ? request.timeout_seconds : request[:timeout_seconds]
495
+ capture_output = request.respond_to?(:capture_output) ? request.capture_output : request[:capture_output]
496
+ capture_errors = request.respond_to?(:capture_errors) ? request.capture_errors : request[:capture_errors]
497
+
498
+ cmd << "--workdir" << working_directory if working_directory
499
+ cmd << "--user" << user if user
500
+
501
+ # Add environment variables
502
+ (environment || {}).each do |key, value|
503
+ cmd << "--env" << "#{key}=#{value}"
504
+ end
505
+
506
+ cmd << container_id
507
+ cmd.concat(command)
508
+
509
+ result = execute_command_with_timeout(cmd.join(" "), timeout_seconds)
510
+
511
+ if grpc_available?
512
+ Podman::Service::ExecuteCommandResponse.new(
513
+ success: result[:success],
514
+ exit_code: result[:exit_code],
515
+ stdout: capture_output ? result[:stdout] : "",
516
+ stderr: capture_errors ? result[:stderr] : "",
517
+ started_at: result[:started_at],
518
+ finished_at: result[:finished_at],
519
+ duration_seconds: result[:duration_seconds],
520
+ error_message: result[:error_message]
521
+ )
522
+ else
523
+ {
524
+ success: result[:success],
525
+ exit_code: result[:exit_code],
526
+ stdout: capture_output ? result[:stdout] : "",
527
+ stderr: capture_errors ? result[:stderr] : "",
528
+ started_at: result[:started_at],
529
+ finished_at: result[:finished_at],
530
+ duration_seconds: result[:duration_seconds],
531
+ error_message: result[:error_message]
532
+ }
533
+ end
534
+ end
535
+
536
+ def run_container(request, _unused_call = nil)
537
+ cmd = [@podman_executable, "run"]
538
+ auto_remove = request.respond_to?(:auto_remove) ? request.auto_remove : request[:auto_remove]
539
+ interactive = request.respond_to?(:interactive) ? request.interactive : request[:interactive]
540
+ tty = request.respond_to?(:tty) ? request.tty : request[:tty]
541
+ environment = request.respond_to?(:environment) ? request.environment : request[:environment]
542
+ volume_mounts = request.respond_to?(:volume_mounts) ? request.volume_mounts : request[:volume_mounts]
543
+ port_mappings = request.respond_to?(:port_mappings) ? request.port_mappings : request[:port_mappings]
544
+ labels = request.respond_to?(:labels) ? request.labels : request[:labels]
545
+ image = request.respond_to?(:image) ? request.image : request[:image]
546
+ command = request.respond_to?(:command) ? request.command : request[:command]
547
+ args = request.respond_to?(:args) ? request.args : request[:args]
548
+
549
+ cmd << "--rm" if auto_remove
550
+ cmd << "--interactive" if interactive
551
+ cmd << "--tty" if tty
552
+
553
+ # Add environment variables
554
+ (environment || {}).each do |key, value|
555
+ cmd << "--env" << "#{key}=#{value}"
556
+ end
557
+
558
+ # Add volume mounts
559
+ (volume_mounts || []).each do |mount|
560
+ host_path = mount.respond_to?(:host_path) ? mount.host_path : mount[:host_path]
561
+ container_path = mount.respond_to?(:container_path) ? mount.container_path : mount[:container_path]
562
+ mode = mount.respond_to?(:mode) ? mount.mode : mount[:mode]
563
+
564
+ # Convert Windows paths to WSL paths on Windows
565
+ if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
566
+ host_path = convert_to_wsl_path(host_path)
567
+ end
568
+
569
+ cmd << "--volume" << "#{host_path}:#{container_path}:#{mode}"
570
+ end
571
+
572
+ # Add port mappings
573
+ (port_mappings || []).each do |port|
574
+ host_port = port.respond_to?(:host_port) ? port.host_port : port[:host_port]
575
+ container_port = port.respond_to?(:container_port) ? port.container_port : port[:container_port]
576
+ protocol = port.respond_to?(:protocol) ? port.protocol : port[:protocol]
577
+ cmd << "--publish" << "#{host_port}:#{container_port}/#{protocol}"
578
+ end
579
+
580
+ # Add labels
581
+ (labels || {}).each do |key, value|
582
+ cmd << "--label" << "#{key}=#{value}"
583
+ end
584
+
585
+ cmd << image
586
+
587
+ # Add command and args
588
+ if command && (command.is_a?(Array) ? command.any? : !command.empty?)
589
+ if command.is_a?(String)
590
+ # If command is a string, treat it as a shell command
591
+ cmd << "sh" << "-c" << command
592
+ elsif command.length == 3 && command[0] == "sh" && command[1] == "-c"
593
+ # If the command contains shell commands with spaces, we need to quote them properly
594
+ # Escape any double quotes in the command
595
+ escaped_command = command[2].gsub('"', '\\"')
596
+ cmd << command[0] << command[1] << "\"#{escaped_command}\""
597
+ else
598
+ cmd.concat(command)
599
+ end
600
+ end
601
+ if args && args.any?
602
+ cmd.concat(args)
603
+ end
604
+
605
+ result = execute_command_with_timeout(cmd.join(" "), request.respond_to?(:timeout_seconds) ? request.timeout_seconds : request[:timeout_seconds])
606
+
607
+ if grpc_available?
608
+ Podman::Service::RunContainerResponse.new(
609
+ success: result[:success],
610
+ container_id: extract_container_id(result[:stdout]),
611
+ exit_code: result[:exit_code],
612
+ stdout: (request.respond_to?(:capture_output) ? request.capture_output : request[:capture_output]) ? result[:stdout] : "",
613
+ stderr: (request.respond_to?(:capture_errors) ? request.capture_errors : request[:capture_errors]) ? result[:stderr] : "",
614
+ started_at: result[:started_at],
615
+ finished_at: result[:finished_at],
616
+ duration_seconds: result[:duration_seconds],
617
+ error_message: result[:error_message]
618
+ )
619
+ else
620
+ {
621
+ success: result[:success],
622
+ container_id: extract_container_id(result[:stdout]),
623
+ exit_code: result[:exit_code],
624
+ stdout: (request.respond_to?(:capture_output) ? request.capture_output : request[:capture_output]) ? result[:stdout] : "",
625
+ stderr: (request.respond_to?(:capture_errors) ? request.capture_errors : request[:capture_errors]) ? result[:stderr] : "",
626
+ started_at: result[:started_at],
627
+ finished_at: result[:finished_at],
628
+ duration_seconds: result[:duration_seconds],
629
+ error_message: result[:error_message]
630
+ }
631
+ end
632
+ end
633
+
634
+ # Volume operations
635
+ def create_volume(request, _unused_call = nil)
636
+ cmd = [@podman_executable, "volume", "create"]
637
+ driver = request.respond_to?(:driver) ? request.driver : request[:driver]
638
+ labels = request.respond_to?(:labels) ? request.labels : request[:labels]
639
+ options = request.respond_to?(:options) ? request.options : request[:options]
640
+ name = request.respond_to?(:name) ? request.name : request[:name]
641
+
642
+ cmd << "--driver" << driver if driver
643
+
644
+ (labels || {}).each do |key, value|
645
+ cmd << "--label" << "#{key}=#{value}"
646
+ end
647
+
648
+ (options || {}).each do |key, value|
649
+ cmd << "--opt" << "#{key}=#{value}"
650
+ end
651
+
652
+ cmd << name
653
+
654
+ result = execute_shell_command(cmd.join(" "))
655
+
656
+ if result[:success]
657
+ # Get volume info
658
+ inspect_cmd = [@podman_executable, "volume", "inspect", request.respond_to?(:name) ? request.name : request[:name]]
659
+ inspect_result = execute_shell_command(inspect_cmd.join(" "))
660
+
661
+ volume_info = if inspect_result[:success]
662
+ JSON.parse(inspect_result[:stdout])[0]
663
+ else
664
+ {}
665
+ end
666
+
667
+ volume = if grpc_available?
668
+ Podman::Service::Volume.new(
669
+ name: volume_info["Name"] || (request.respond_to?(:name) ? request.name : request[:name]),
670
+ driver: volume_info["Driver"] || "local",
671
+ mountpoint: volume_info["Mountpoint"] || "",
672
+ created: volume_info["CreatedAt"] ? Time.parse(volume_info["CreatedAt"]) : Time.now,
673
+ labels: volume_info["Labels"] || {},
674
+ options: volume_info["Options"] || {}
675
+ )
676
+ else
677
+ {
678
+ name: volume_info["Name"] || (request.respond_to?(:name) ? request.name : request[:name]),
679
+ driver: volume_info["Driver"] || "local",
680
+ mountpoint: volume_info["Mountpoint"] || "",
681
+ created: volume_info["CreatedAt"] ? Time.parse(volume_info["CreatedAt"]) : Time.now,
682
+ labels: volume_info["Labels"] || {},
683
+ options: volume_info["Options"] || {}
684
+ }
685
+ end
686
+
687
+ if grpc_available?
688
+ Podman::Service::CreateVolumeResponse.new(
689
+ success: true,
690
+ volume: volume
691
+ )
692
+ else
693
+ {
694
+ success: true,
695
+ volume: volume
696
+ }
697
+ end
698
+ else
699
+ if grpc_available?
700
+ Podman::Service::CreateVolumeResponse.new(
701
+ success: false,
702
+ error_message: result[:stderr]
703
+ )
704
+ else
705
+ {
706
+ success: false,
707
+ error_message: result[:stderr]
708
+ }
709
+ end
710
+ end
711
+ end
712
+
713
+ def list_volumes(request, _unused_call = nil)
714
+ cmd = [@podman_executable, "volume", "ls", "--format", "json"]
715
+
716
+ result = execute_shell_command(cmd.join(" "))
717
+
718
+ if result[:success]
719
+ volumes_data = JSON.parse(result[:stdout])
720
+ volumes = volumes_data.map do |vol|
721
+ if grpc_available?
722
+ Podman::Service::Volume.new(
723
+ name: vol["Name"],
724
+ driver: vol["Driver"],
725
+ mountpoint: vol["Mountpoint"],
726
+ created: vol["CreatedAt"] ? Time.parse(vol["CreatedAt"]) : Time.now,
727
+ labels: vol["Labels"] || {},
728
+ options: vol["Options"] || {}
729
+ )
730
+ else
731
+ {
732
+ name: vol["Name"],
733
+ driver: vol["Driver"],
734
+ mountpoint: vol["Mountpoint"],
735
+ created: vol["CreatedAt"] ? Time.parse(vol["CreatedAt"]) : Time.now,
736
+ labels: vol["Labels"] || {},
737
+ options: vol["Options"] || {}
738
+ }
739
+ end
740
+ end
741
+
742
+ if grpc_available?
743
+ Podman::Service::ListVolumesResponse.new(
744
+ volumes: volumes,
745
+ total_count: volumes.length
746
+ )
747
+ else
748
+ {
749
+ volumes: volumes,
750
+ total_count: volumes.length
751
+ }
752
+ end
753
+ else
754
+ if grpc_available?
755
+ Podman::Service::ListVolumesResponse.new(
756
+ volumes: [],
757
+ total_count: 0
758
+ )
759
+ else
760
+ {
761
+ volumes: [],
762
+ total_count: 0
763
+ }
764
+ end
765
+ end
766
+ end
767
+
768
+ def remove_volume(request, _unused_call = nil)
769
+ cmd = [@podman_executable, "volume", "rm"]
770
+ cmd << "--force" if request.respond_to?(:force) ? request.force : request[:force]
771
+ cmd << (request.respond_to?(:volume_name) ? request.volume_name : request[:volume_name])
772
+
773
+ result = execute_shell_command(cmd.join(" "))
774
+
775
+ if grpc_available?
776
+ Podman::Service::RemoveVolumeResponse.new(
777
+ success: result[:success],
778
+ error_message: result[:success] ? "" : result[:stderr]
779
+ )
780
+ else
781
+ {
782
+ success: result[:success],
783
+ error_message: result[:success] ? "" : result[:stderr]
784
+ }
785
+ end
786
+ end
787
+
788
+ # System operations
789
+ def get_version(_request, _unused_call = nil)
790
+ result = execute_shell_command("#{@podman_executable} version --format json")
791
+
792
+ if result[:success]
793
+ version_info = JSON.parse(result[:stdout])
794
+ if grpc_available?
795
+ Podman::Service::VersionResponse.new(
796
+ version: version_info["Client"]["Version"],
797
+ api_version: version_info["Client"]["APIVersion"],
798
+ go_version: version_info["Client"]["GoVersion"],
799
+ git_commit: version_info["Client"]["GitCommit"],
800
+ build_time: version_info["Client"]["BuildTime"],
801
+ os_arch: version_info["Client"]["OsArch"]
802
+ )
803
+ else
804
+ {
805
+ version: version_info["Client"]["Version"],
806
+ api_version: version_info["Client"]["APIVersion"],
807
+ go_version: version_info["Client"]["GoVersion"],
808
+ git_commit: version_info["Client"]["GitCommit"],
809
+ build_time: version_info["Client"]["BuildTime"],
810
+ os_arch: version_info["Client"]["OsArch"]
811
+ }
812
+ end
813
+ else
814
+ if grpc_available?
815
+ Podman::Service::VersionResponse.new(
816
+ version: "unknown",
817
+ error_message: result[:stderr]
818
+ )
819
+ else
820
+ {
821
+ version: "unknown",
822
+ error_message: result[:stderr]
823
+ }
824
+ end
825
+ end
826
+ end
827
+
828
+ def get_system_info(_request, _unused_call = nil)
829
+ result = execute_shell_command("#{@podman_executable} system info --format json")
830
+
831
+ if result[:success]
832
+ system_info = JSON.parse(result[:stdout])
833
+ if grpc_available?
834
+ Podman::Service::SystemInfoResponse.new(
835
+ hostname: system_info["hostname"] || "",
836
+ kernel: system_info["kernel"] || "",
837
+ os: system_info["os"] || "",
838
+ arch: system_info["arch"] || "",
839
+ total_memory: system_info["memTotal"] || 0,
840
+ available_memory: system_info["memAvailable"] || 0,
841
+ cpu_count: system_info["cpus"] || 0,
842
+ storage_drivers: system_info["store"]["graphDriverName"] ? [system_info["store"]["graphDriverName"]] : [],
843
+ log_drivers: system_info["logDriver"] ? [system_info["logDriver"]] : []
844
+ )
845
+ else
846
+ {
847
+ hostname: system_info["hostname"] || "",
848
+ kernel: system_info["kernel"] || "",
849
+ os: system_info["os"] || "",
850
+ arch: system_info["arch"] || "",
851
+ total_memory: system_info["memTotal"] || 0,
852
+ available_memory: system_info["memAvailable"] || 0,
853
+ cpu_count: system_info["cpus"] || 0,
854
+ storage_drivers: system_info["store"]["graphDriverName"] ? [system_info["store"]["graphDriverName"]] : [],
855
+ log_drivers: system_info["logDriver"] ? [system_info["logDriver"]] : []
856
+ }
857
+ end
858
+ else
859
+ if grpc_available?
860
+ Podman::Service::SystemInfoResponse.new(
861
+ hostname: "",
862
+ kernel: "",
863
+ os: "",
864
+ arch: "",
865
+ total_memory: 0,
866
+ available_memory: 0,
867
+ cpu_count: 0,
868
+ storage_drivers: [],
869
+ log_drivers: []
870
+ )
871
+ else
872
+ {
873
+ hostname: "",
874
+ kernel: "",
875
+ os: "",
876
+ arch: "",
877
+ total_memory: 0,
878
+ available_memory: 0,
879
+ cpu_count: 0,
880
+ storage_drivers: [],
881
+ log_drivers: []
882
+ }
883
+ end
884
+ end
885
+ end
886
+
887
+ def health_check(_request, _unused_call = nil)
888
+ result = execute_shell_command("#{@podman_executable} version")
889
+
890
+ if grpc_available?
891
+ Podman::Service::HealthCheckResponse.new(
892
+ healthy: result[:success],
893
+ status: result[:success] ? "healthy" : "unhealthy",
894
+ error_message: result[:success] ? "" : result[:stderr],
895
+ checked_at: Time.now
896
+ )
897
+ else
898
+ {
899
+ healthy: result[:success],
900
+ status: result[:success] ? "healthy" : "unhealthy",
901
+ error_message: result[:success] ? "" : result[:stderr],
902
+ checked_at: Time.now
903
+ }
904
+ end
905
+ end
906
+
907
+ def execute_shell_command(command)
908
+ start_time = Time.now
909
+ output = `#{command} 2>&1`
910
+ exit_code = $?.exitstatus
911
+ end_time = Time.now
912
+
913
+ {
914
+ success: exit_code == 0,
915
+ exit_code: exit_code,
916
+ stdout: output,
917
+ stderr: exit_code != 0 ? output : "",
918
+ started_at: start_time,
919
+ finished_at: end_time,
920
+ duration_seconds: (end_time - start_time).to_i,
921
+ error_message: exit_code != 0 ? output : ""
922
+ }
923
+ end
924
+
925
+ private
926
+
927
+ def grpc_available?
928
+ self.class.grpc_available?
929
+ end
930
+
931
+ def execute_command_with_input(command, input)
932
+ start_time = Time.now
933
+
934
+ # Use Ruby's Open3 for proper input handling
935
+ require 'open3'
936
+
937
+ begin
938
+ stdout, stderr, status = Open3.capture3(command, stdin_data: input)
939
+ exit_code = status.exitstatus
940
+ end_time = Time.now
941
+
942
+ {
943
+ success: exit_code == 0,
944
+ exit_code: exit_code,
945
+ stdout: stdout,
946
+ stderr: stderr,
947
+ started_at: start_time,
948
+ finished_at: end_time,
949
+ duration_seconds: (end_time - start_time).to_i,
950
+ error_message: exit_code != 0 ? stderr : ""
951
+ }
952
+ rescue => e
953
+ end_time = Time.now
954
+ {
955
+ success: false,
956
+ exit_code: 1,
957
+ stdout: "",
958
+ stderr: e.message,
959
+ started_at: start_time,
960
+ finished_at: end_time,
961
+ duration_seconds: (end_time - start_time).to_i,
962
+ error_message: e.message
963
+ }
964
+ end
965
+ end
966
+
967
+ def execute_command_with_timeout(command, timeout_seconds)
968
+ # For now, just execute normally
969
+ # In a production implementation, you'd use proper timeout handling
970
+ execute_shell_command(command)
971
+ end
972
+
973
+ def find_executable(executable_name)
974
+ # Cross-platform executable finding logic
975
+ if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
976
+ # Windows logic
977
+ # If it's already a full path, check if it exists and is executable
978
+ if executable_name.include?("\\") || executable_name.include?("/")
979
+ return executable_name if File.executable?(executable_name)
980
+ end
981
+
982
+ # Otherwise, try common extensions
983
+ ["#{executable_name}.exe", "#{executable_name}.bat", "#{executable_name}.cmd"].each do |exe|
984
+ return exe if system("#{exe} --version", out: File::NULL, err: File::NULL)
985
+ end
986
+ nil
987
+ else
988
+ # Unix logic
989
+ # If it's already a full path, check if it exists and is executable
990
+ if executable_name.include?("/")
991
+ return executable_name if File.executable?(executable_name)
992
+ end
993
+
994
+ # Otherwise, use which
995
+ `which #{executable_name}`.strip
996
+ end
997
+ end
998
+
999
+ def extract_image_id(output)
1000
+ # Extract image ID from podman pull output
1001
+ output.lines.last&.strip || ""
1002
+ end
1003
+
1004
+ def extract_layers(output)
1005
+ # Extract layer information from podman pull output
1006
+ output.lines.select { |line| line.include?("Pulling") }.map(&:strip)
1007
+ end
1008
+
1009
+ def extract_container_id(output)
1010
+ # Extract container ID from podman run output
1011
+ output.strip
1012
+ end
1013
+
1014
+ def map_container_status(status)
1015
+ case status.downcase
1016
+ when "created"
1017
+ grpc_available? ? Podman::Service::ContainerStatus::CREATED : :created
1018
+ when "running"
1019
+ grpc_available? ? Podman::Service::ContainerStatus::RUNNING : :running
1020
+ when "paused"
1021
+ grpc_available? ? Podman::Service::ContainerStatus::PAUSED : :paused
1022
+ when "restarting"
1023
+ grpc_available? ? Podman::Service::ContainerStatus::RESTARTING : :restarting
1024
+ when "removing"
1025
+ grpc_available? ? Podman::Service::ContainerStatus::REMOVING : :removing
1026
+ when "exited"
1027
+ grpc_available? ? Podman::Service::ContainerStatus::EXITED : :exited
1028
+ when "dead"
1029
+ grpc_available? ? Podman::Service::ContainerStatus::DEAD : :dead
1030
+ else
1031
+ grpc_available? ? Podman::Service::ContainerStatus::UNKNOWN : :unknown
1032
+ end
1033
+ end
1034
+
1035
+ def parse_time(time_string)
1036
+ return Time.now if time_string.nil?
1037
+
1038
+ # Handle integers (Unix timestamps)
1039
+ if time_string.is_a?(Integer)
1040
+ return Time.at(time_string)
1041
+ end
1042
+
1043
+ # Handle empty strings
1044
+ return Time.now if time_string.empty?
1045
+
1046
+ # Handle relative time strings like "Less than a second ago"
1047
+ if time_string.include?("ago")
1048
+ return Time.now
1049
+ end
1050
+
1051
+ begin
1052
+ Time.parse(time_string)
1053
+ rescue ArgumentError
1054
+ Time.now
1055
+ end
1056
+ end
1057
+
1058
+ def create_error_response(message)
1059
+ if grpc_available?
1060
+ Podman::Service::ExecuteScriptResponse.new(
1061
+ success: false,
1062
+ error_message: message,
1063
+ exit_code: 1
1064
+ )
1065
+ else
1066
+ {
1067
+ success: false,
1068
+ error_message: message,
1069
+ exit_code: 1
1070
+ }
1071
+ end
1072
+ end
1073
+
1074
+ def convert_to_wsl_path(windows_path)
1075
+ # On Windows, Podman handles Windows paths natively
1076
+ # Just convert forward slashes to backslashes if needed
1077
+ windows_path.tr('/', '\\')
1078
+ end
1079
+ end
1080
+ end
1081
+ end