vagrant-orbstack 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +155 -0
- data/LICENSE +21 -0
- data/README.md +622 -0
- data/lib/vagrant-orbstack/action/create.rb +177 -0
- data/lib/vagrant-orbstack/action/destroy.rb +90 -0
- data/lib/vagrant-orbstack/action/halt.rb +64 -0
- data/lib/vagrant-orbstack/action/machine_validation.rb +60 -0
- data/lib/vagrant-orbstack/action/reload.rb +56 -0
- data/lib/vagrant-orbstack/action/ssh_run.rb +70 -0
- data/lib/vagrant-orbstack/action/start.rb +68 -0
- data/lib/vagrant-orbstack/action.rb +21 -0
- data/lib/vagrant-orbstack/config.rb +161 -0
- data/lib/vagrant-orbstack/errors.rb +64 -0
- data/lib/vagrant-orbstack/plugin.rb +45 -0
- data/lib/vagrant-orbstack/provider.rb +387 -0
- data/lib/vagrant-orbstack/util/machine_namer.rb +130 -0
- data/lib/vagrant-orbstack/util/orbstack_cli.rb +283 -0
- data/lib/vagrant-orbstack/util/ssh_readiness_checker.rb +115 -0
- data/lib/vagrant-orbstack/util/state_cache.rb +106 -0
- data/lib/vagrant-orbstack/version.rb +7 -0
- data/lib/vagrant-orbstack.rb +36 -0
- data/locales/en.yml +49 -0
- metadata +69 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require_relative 'orbstack_cli'
|
|
5
|
+
require_relative '../errors'
|
|
6
|
+
|
|
7
|
+
module VagrantPlugins
|
|
8
|
+
module OrbStack
|
|
9
|
+
module Util
|
|
10
|
+
# Utility class for generating unique machine names with collision avoidance
|
|
11
|
+
#
|
|
12
|
+
# Generates machine names in the format: vagrant-<sanitized-name>-<short-id>
|
|
13
|
+
# where short-id is a 6-character random hex string. Implements collision
|
|
14
|
+
# detection and automatic retry with new IDs.
|
|
15
|
+
#
|
|
16
|
+
# @example Generate a unique machine name
|
|
17
|
+
# machine = double('machine', name: 'web_server')
|
|
18
|
+
# name = MachineNamer.generate(machine)
|
|
19
|
+
# # => "vagrant-web-server-a3b2c1"
|
|
20
|
+
#
|
|
21
|
+
# @example Collision handling
|
|
22
|
+
# # If "vagrant-default-a3b2c1" exists, generates new ID automatically
|
|
23
|
+
# name = MachineNamer.generate(machine)
|
|
24
|
+
# # => "vagrant-default-d4e5f6" (different ID)
|
|
25
|
+
#
|
|
26
|
+
# @api public
|
|
27
|
+
class MachineNamer
|
|
28
|
+
# Maximum number of retry attempts for collision avoidance
|
|
29
|
+
MAX_RETRIES = 3
|
|
30
|
+
|
|
31
|
+
# Maximum machine name length (DNS hostname limit)
|
|
32
|
+
MAX_NAME_LENGTH = 63
|
|
33
|
+
|
|
34
|
+
# Generate a unique machine name with collision avoidance.
|
|
35
|
+
#
|
|
36
|
+
# Creates a machine name in the format vagrant-<name>-<id> where:
|
|
37
|
+
# - name is sanitized from machine.name (lowercase, hyphens, alphanumeric)
|
|
38
|
+
# - id is a 6-character random hex string
|
|
39
|
+
#
|
|
40
|
+
# If a collision is detected (name already exists in OrbStack), retries
|
|
41
|
+
# with a new random ID up to MAX_RETRIES times.
|
|
42
|
+
#
|
|
43
|
+
# @param machine [Vagrant::Machine] The machine object with name attribute
|
|
44
|
+
# @return [String] A unique machine name
|
|
45
|
+
# @raise [MachineNameCollisionError] If all retry attempts are exhausted
|
|
46
|
+
# @raise [OrbStackNotInstalled] If OrbStack CLI is not available
|
|
47
|
+
# @raise [CommandTimeoutError] If OrbStack CLI times out
|
|
48
|
+
# @api public
|
|
49
|
+
def self.generate(machine)
|
|
50
|
+
machine_name = machine.name.to_s
|
|
51
|
+
sanitized = sanitize_name(machine_name)
|
|
52
|
+
|
|
53
|
+
MAX_RETRIES.times do
|
|
54
|
+
short_id = SecureRandom.hex(3)
|
|
55
|
+
candidate = "vagrant-#{sanitized}-#{short_id}"
|
|
56
|
+
|
|
57
|
+
return candidate unless check_collision?(candidate)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# All retries exhausted - raise error
|
|
61
|
+
raise MachineNameCollisionError,
|
|
62
|
+
"Failed to generate unique machine name after #{MAX_RETRIES} attempts (machine: #{machine_name})"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Sanitize a machine name for use in hostname.
|
|
66
|
+
#
|
|
67
|
+
# Applies the following transformations:
|
|
68
|
+
# - Convert to lowercase
|
|
69
|
+
# - Replace underscores with hyphens
|
|
70
|
+
# - Remove all characters except alphanumeric and hyphens
|
|
71
|
+
# - Strip leading/trailing whitespace
|
|
72
|
+
# - Collapse consecutive hyphens to single hyphen
|
|
73
|
+
# - Truncate to fit within MAX_NAME_LENGTH minus prefix/suffix space
|
|
74
|
+
# - Default to "default" if result is empty
|
|
75
|
+
#
|
|
76
|
+
# @param name [String, nil] The name to sanitize
|
|
77
|
+
# @return [String] The sanitized name
|
|
78
|
+
# @api private
|
|
79
|
+
class << self
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Method length acceptable: Each transformation step is clear and well-documented.
|
|
83
|
+
# Combining steps would reduce readability.
|
|
84
|
+
# rubocop:disable Metrics/MethodLength
|
|
85
|
+
def sanitize_name(name)
|
|
86
|
+
# Handle nil or empty input → default
|
|
87
|
+
return 'default' if name.nil? || name.strip.empty?
|
|
88
|
+
|
|
89
|
+
# Apply sanitization rules:
|
|
90
|
+
# 1. Strip whitespace
|
|
91
|
+
# 2. Convert to lowercase
|
|
92
|
+
# 3. Replace underscores with hyphens (DNS-safe)
|
|
93
|
+
# 4. Remove non-alphanumeric (except hyphens)
|
|
94
|
+
# 5. Collapse consecutive hyphens
|
|
95
|
+
# 6. Remove leading/trailing hyphens
|
|
96
|
+
sanitized = name.to_s
|
|
97
|
+
.strip # Rule 1
|
|
98
|
+
.downcase # Rule 2
|
|
99
|
+
.gsub('_', '-') # Rule 3
|
|
100
|
+
.gsub(/[^a-z0-9-]/, '') # Rule 4
|
|
101
|
+
.gsub(/-+/, '-') # Rule 5
|
|
102
|
+
.gsub(/^-|-$/, '') # Rule 6
|
|
103
|
+
|
|
104
|
+
return 'default' if sanitized.empty?
|
|
105
|
+
|
|
106
|
+
# DNS limit (63) - "vagrant-" (8) - "-XXXXXX" (7) = 48 max
|
|
107
|
+
max_length = MAX_NAME_LENGTH - 15
|
|
108
|
+
sanitized[0...max_length]
|
|
109
|
+
end
|
|
110
|
+
# rubocop:enable Metrics/MethodLength
|
|
111
|
+
|
|
112
|
+
# Check if a machine name already exists in OrbStack.
|
|
113
|
+
#
|
|
114
|
+
# Queries OrbStack CLI for list of existing machines and checks
|
|
115
|
+
# if the candidate name is already in use.
|
|
116
|
+
#
|
|
117
|
+
# @param name [String] The candidate name to check
|
|
118
|
+
# @return [Boolean] true if collision detected, false otherwise
|
|
119
|
+
# @raise [OrbStackNotInstalled] If OrbStack CLI is not available
|
|
120
|
+
# @raise [CommandTimeoutError] If OrbStack CLI times out
|
|
121
|
+
# @api private
|
|
122
|
+
def check_collision?(name)
|
|
123
|
+
machines = OrbStackCLI.list_machines
|
|
124
|
+
machines.any? { |m| m[:name] == name }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'vagrant-orbstack/errors'
|
|
7
|
+
|
|
8
|
+
module VagrantPlugins
|
|
9
|
+
module OrbStack
|
|
10
|
+
module Util
|
|
11
|
+
# Utility class for detecting and interacting with OrbStack CLI
|
|
12
|
+
#
|
|
13
|
+
# This class provides a Ruby interface to the OrbStack command-line tool (`orb`).
|
|
14
|
+
# All methods execute shell commands and return parsed results. The class handles
|
|
15
|
+
# timeouts, error detection, and logging automatically.
|
|
16
|
+
#
|
|
17
|
+
# @example Check if OrbStack is available
|
|
18
|
+
# if OrbStackCLI.available?
|
|
19
|
+
# puts "OrbStack version: #{OrbStackCLI.version}"
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @example Create and manage a machine
|
|
23
|
+
# OrbStackCLI.create_machine('ubuntu', 'my-dev-vm')
|
|
24
|
+
# OrbStackCLI.start_machine('my-dev-vm')
|
|
25
|
+
# info = OrbStackCLI.machine_info('my-dev-vm')
|
|
26
|
+
# OrbStackCLI.stop_machine('my-dev-vm')
|
|
27
|
+
#
|
|
28
|
+
# @example List all machines
|
|
29
|
+
# machines = OrbStackCLI.list_machines
|
|
30
|
+
# machines.each do |m|
|
|
31
|
+
# puts "#{m[:name]} - #{m[:status]}"
|
|
32
|
+
# end
|
|
33
|
+
class OrbStackCLI
|
|
34
|
+
# Default timeout for non-mutating query operations (list, info).
|
|
35
|
+
# Use this for fast read operations.
|
|
36
|
+
# @return [Integer] Timeout in seconds
|
|
37
|
+
QUERY_TIMEOUT = 30
|
|
38
|
+
|
|
39
|
+
# Default timeout for state-changing operations (start, stop, delete).
|
|
40
|
+
# Use this for operations that modify machine state.
|
|
41
|
+
# @return [Integer] Timeout in seconds
|
|
42
|
+
MUTATE_TIMEOUT = 60
|
|
43
|
+
|
|
44
|
+
# Default timeout for machine creation operations.
|
|
45
|
+
# Longer timeout accounts for distribution image downloads which
|
|
46
|
+
# can take 30-120 seconds on first use.
|
|
47
|
+
# @return [Integer] Timeout in seconds
|
|
48
|
+
CREATE_TIMEOUT = 120
|
|
49
|
+
|
|
50
|
+
@logger = Log4r::Logger.new('vagrant_orbstack::util')
|
|
51
|
+
|
|
52
|
+
class << self
|
|
53
|
+
# Check if orb command is available in PATH
|
|
54
|
+
# @return [Boolean] true if orb command exists, false otherwise
|
|
55
|
+
def available?
|
|
56
|
+
stdout, _stderr, success = execute_command('which orb')
|
|
57
|
+
!stdout.empty? && success
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get OrbStack version
|
|
61
|
+
# @return [String, nil] version string or nil if not available
|
|
62
|
+
def version
|
|
63
|
+
stdout, _stderr, success = execute_command('orb --version')
|
|
64
|
+
return nil unless success
|
|
65
|
+
|
|
66
|
+
# Parse version from output like "orb version 1.2.3" or just "1.2.3"
|
|
67
|
+
match = stdout.match(/(\d+\.\d+\.\d+)/)
|
|
68
|
+
match ? match[1] : nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Check if OrbStack is currently running
|
|
72
|
+
# @return [Boolean] true if running, false otherwise
|
|
73
|
+
def running?
|
|
74
|
+
stdout, _stderr, success = execute_command('orb status')
|
|
75
|
+
success && stdout.match?(/running/i)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# List all OrbStack machines
|
|
79
|
+
#
|
|
80
|
+
# Executes `orb list` and parses the output into an array of machine hashes.
|
|
81
|
+
# Each hash contains the machine name and current status.
|
|
82
|
+
#
|
|
83
|
+
# @return [Array<Hash>] Array of machine hashes, each with:
|
|
84
|
+
# - :name [String] The machine name
|
|
85
|
+
# - :status [String] The machine status (e.g., 'running', 'stopped')
|
|
86
|
+
# @return [Array] Empty array on failure or if no machines exist
|
|
87
|
+
# @raise [CommandTimeoutError] If command times out
|
|
88
|
+
#
|
|
89
|
+
# @example List all machines
|
|
90
|
+
# machines = OrbStackCLI.list_machines
|
|
91
|
+
# # => [{name: 'ubuntu-dev', status: 'running'}, {name: 'debian-test', status: 'stopped'}]
|
|
92
|
+
# rubocop:disable Metrics/MethodLength
|
|
93
|
+
def list_machines
|
|
94
|
+
stdout, _stderr, success = execute_command('orb list', timeout: QUERY_TIMEOUT)
|
|
95
|
+
return [] unless success
|
|
96
|
+
return [] if stdout.empty?
|
|
97
|
+
|
|
98
|
+
# Parse machine list output (format: NAME STATUS DISTRO IP)
|
|
99
|
+
machines = []
|
|
100
|
+
stdout.each_line do |line|
|
|
101
|
+
parts = line.strip.split(/\s+/)
|
|
102
|
+
next if parts.empty?
|
|
103
|
+
|
|
104
|
+
machines << {
|
|
105
|
+
name: parts[0],
|
|
106
|
+
status: parts[1]
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
machines
|
|
111
|
+
end
|
|
112
|
+
# rubocop:enable Metrics/MethodLength
|
|
113
|
+
|
|
114
|
+
# Get detailed information about a specific machine
|
|
115
|
+
#
|
|
116
|
+
# Executes `orb info <name>` and parses the JSON output. Returns nil if
|
|
117
|
+
# the machine doesn't exist or if JSON parsing fails.
|
|
118
|
+
#
|
|
119
|
+
# @param name [String] The machine name
|
|
120
|
+
# @return [Hash, nil] Parsed machine information hash with OrbStack-specific
|
|
121
|
+
# fields, or nil if machine not found or parsing fails
|
|
122
|
+
# @raise [CommandTimeoutError] If command times out
|
|
123
|
+
#
|
|
124
|
+
# @example Get machine info
|
|
125
|
+
# info = OrbStackCLI.machine_info('my-dev-vm')
|
|
126
|
+
# puts info['distro'] if info
|
|
127
|
+
def machine_info(name)
|
|
128
|
+
stdout, _stderr, success = execute_command("orb info --format json #{name}", timeout: QUERY_TIMEOUT)
|
|
129
|
+
return nil unless success
|
|
130
|
+
|
|
131
|
+
JSON.parse(stdout)
|
|
132
|
+
rescue JSON::ParserError => e
|
|
133
|
+
@logger.warn("Failed to parse machine info JSON: #{e.message}")
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Create a new OrbStack machine
|
|
138
|
+
#
|
|
139
|
+
# Executes `orb create <distro> <name>`. This operation can take 30-120 seconds
|
|
140
|
+
# on first use if the distribution image needs to be downloaded. Subsequent
|
|
141
|
+
# creations of the same distribution are much faster.
|
|
142
|
+
#
|
|
143
|
+
# @param name [String] The machine name
|
|
144
|
+
# @param distribution [String] The distribution to use (e.g., 'ubuntu:noble', 'debian')
|
|
145
|
+
# @param timeout [Integer] Command timeout in seconds (default: CREATE_TIMEOUT)
|
|
146
|
+
# @return [Hash] Machine info hash with :id and :status keys
|
|
147
|
+
# @raise [CommandExecutionError] If creation fails
|
|
148
|
+
# @raise [CommandTimeoutError] If command times out
|
|
149
|
+
#
|
|
150
|
+
# @example Create an Ubuntu machine
|
|
151
|
+
# OrbStackCLI.create_machine('my-dev-vm', distribution: 'ubuntu:noble')
|
|
152
|
+
#
|
|
153
|
+
# @example Create with custom timeout for slow networks
|
|
154
|
+
# OrbStackCLI.create_machine('my-vm', distribution: 'ubuntu', timeout: 180)
|
|
155
|
+
# rubocop:disable Naming/PredicateMethod
|
|
156
|
+
def create_machine(name, distribution:, timeout: CREATE_TIMEOUT)
|
|
157
|
+
_, stderr, success = execute_command("orb create #{distribution} #{name}", timeout: timeout)
|
|
158
|
+
raise_unless_successful!('create', stderr, success)
|
|
159
|
+
{ id: name, status: 'running' }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Delete an OrbStack machine
|
|
163
|
+
#
|
|
164
|
+
# Executes `orb delete <name>`. This permanently removes the machine and
|
|
165
|
+
# all its data. The operation cannot be undone.
|
|
166
|
+
#
|
|
167
|
+
# @param name [String] The machine name
|
|
168
|
+
# @return [Boolean] true on success
|
|
169
|
+
# @raise [CommandExecutionError] If deletion fails
|
|
170
|
+
# @raise [CommandTimeoutError] If command times out
|
|
171
|
+
#
|
|
172
|
+
# @example Delete a machine
|
|
173
|
+
# OrbStackCLI.delete_machine('my-dev-vm')
|
|
174
|
+
def delete_machine(name)
|
|
175
|
+
_, stderr, success = execute_command("orb delete #{name}", timeout: MUTATE_TIMEOUT)
|
|
176
|
+
raise_unless_successful!('delete', stderr, success)
|
|
177
|
+
true
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Start an OrbStack machine
|
|
181
|
+
#
|
|
182
|
+
# Executes `orb start <name>`. Starts a stopped machine. If the machine
|
|
183
|
+
# is already running, this is a no-op.
|
|
184
|
+
#
|
|
185
|
+
# @param name [String] The machine name
|
|
186
|
+
# @param timeout [Integer] Command timeout in seconds (default: MUTATE_TIMEOUT)
|
|
187
|
+
# @return [Hash] Machine info hash with :id and :status keys
|
|
188
|
+
# @raise [CommandExecutionError] If start fails
|
|
189
|
+
# @raise [CommandTimeoutError] If command times out
|
|
190
|
+
#
|
|
191
|
+
# @example Start a machine
|
|
192
|
+
# OrbStackCLI.start_machine('my-dev-vm')
|
|
193
|
+
def start_machine(name, timeout: MUTATE_TIMEOUT)
|
|
194
|
+
_, stderr, success = execute_command("orb start #{name}", timeout: timeout)
|
|
195
|
+
raise_unless_successful!('start', stderr, success)
|
|
196
|
+
{ id: name, status: 'running' }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Stop an OrbStack machine
|
|
200
|
+
#
|
|
201
|
+
# Executes `orb stop <name>`. Stops a running machine gracefully. If the
|
|
202
|
+
# machine is already stopped, this is a no-op.
|
|
203
|
+
#
|
|
204
|
+
# @param name [String] The machine name
|
|
205
|
+
# @return [Boolean] true on success
|
|
206
|
+
# @raise [CommandExecutionError] If stop fails
|
|
207
|
+
# @raise [CommandTimeoutError] If command times out
|
|
208
|
+
#
|
|
209
|
+
# @example Stop a machine
|
|
210
|
+
# OrbStackCLI.stop_machine('my-dev-vm')
|
|
211
|
+
def stop_machine(name)
|
|
212
|
+
_, stderr, success = execute_command("orb stop #{name}", timeout: MUTATE_TIMEOUT)
|
|
213
|
+
raise_unless_successful!('stop', stderr, success)
|
|
214
|
+
true
|
|
215
|
+
end
|
|
216
|
+
# rubocop:enable Naming/PredicateMethod
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
# Raises CommandExecutionError unless the operation succeeded
|
|
221
|
+
#
|
|
222
|
+
# Helper method to enforce error handling for mutating operations.
|
|
223
|
+
# Only raises if the operation failed.
|
|
224
|
+
#
|
|
225
|
+
# @param action [String] The action being performed (e.g., 'create', 'delete')
|
|
226
|
+
# @param stderr [String] Error output from CLI
|
|
227
|
+
# @param success [Boolean] Whether the operation succeeded
|
|
228
|
+
# @return [void]
|
|
229
|
+
# @raise [CommandExecutionError] If success is false
|
|
230
|
+
def raise_unless_successful!(action, stderr, success)
|
|
231
|
+
return if success
|
|
232
|
+
|
|
233
|
+
raise CommandExecutionError, command: "orb #{action}", details: stderr
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Execute an OrbStack CLI command.
|
|
237
|
+
#
|
|
238
|
+
# Executes a shell command using Open3.capture3 and captures stdout,
|
|
239
|
+
# stderr, and exit status. Supports optional timeout for long-running
|
|
240
|
+
# operations. Logs command execution at DEBUG level on success and
|
|
241
|
+
# ERROR level on failure.
|
|
242
|
+
#
|
|
243
|
+
# On any StandardError (other than timeout), returns empty strings and
|
|
244
|
+
# false success flag rather than propagating the exception.
|
|
245
|
+
#
|
|
246
|
+
# @param command [String] The command to execute
|
|
247
|
+
# @param timeout [Integer] Timeout in seconds (default: QUERY_TIMEOUT)
|
|
248
|
+
# @return [Array<String, String, Boolean>] Tuple of [stdout, stderr, success].
|
|
249
|
+
# All string values are stripped of leading/trailing whitespace.
|
|
250
|
+
# @raise [CommandTimeoutError] If command exceeds timeout
|
|
251
|
+
# @api private
|
|
252
|
+
# rubocop:disable Metrics/MethodLength
|
|
253
|
+
def execute_command(command, timeout: QUERY_TIMEOUT)
|
|
254
|
+
stdout, stderr, status = if timeout
|
|
255
|
+
Timeout.timeout(timeout) do
|
|
256
|
+
Open3.capture3(command)
|
|
257
|
+
end
|
|
258
|
+
else
|
|
259
|
+
Open3.capture3(command)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
success = status.success?
|
|
263
|
+
|
|
264
|
+
if success
|
|
265
|
+
@logger.debug("Command succeeded: #{command}")
|
|
266
|
+
else
|
|
267
|
+
@logger.error("Command failed: #{command}, stderr: #{stderr}")
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
[stdout.strip, stderr.strip, success]
|
|
271
|
+
rescue Timeout::Error
|
|
272
|
+
@logger.error("Command timed out after #{timeout}s: #{command}")
|
|
273
|
+
raise CommandTimeoutError, "Command timed out: #{command}"
|
|
274
|
+
rescue StandardError => e
|
|
275
|
+
@logger.error("Command error: #{e.message}")
|
|
276
|
+
['', '', false]
|
|
277
|
+
end
|
|
278
|
+
# rubocop:enable Metrics/MethodLength
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'orbstack_cli'
|
|
4
|
+
require_relative '../errors'
|
|
5
|
+
|
|
6
|
+
module VagrantPlugins
|
|
7
|
+
module OrbStack
|
|
8
|
+
module Util
|
|
9
|
+
# Utility module for waiting until SSH becomes available on an OrbStack machine
|
|
10
|
+
#
|
|
11
|
+
# This module provides a polling mechanism to check if an OrbStack machine
|
|
12
|
+
# is ready for SSH connections. It repeatedly queries the machine status
|
|
13
|
+
# until the machine reports as 'running', indicating SSH is available.
|
|
14
|
+
#
|
|
15
|
+
# @example Wait for SSH on a newly created machine
|
|
16
|
+
# SSHReadinessChecker.wait_for_ready('my-machine', ui: ui)
|
|
17
|
+
#
|
|
18
|
+
# @example Handle timeout gracefully
|
|
19
|
+
# begin
|
|
20
|
+
# SSHReadinessChecker.wait_for_ready('my-machine', ui: ui)
|
|
21
|
+
# rescue VagrantPlugins::OrbStack::SSHNotReady => e
|
|
22
|
+
# ui.error("SSH not ready: #{e.message}")
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# @api public
|
|
26
|
+
module SSHReadinessChecker
|
|
27
|
+
# Maximum time to wait for SSH to become ready (in seconds)
|
|
28
|
+
# @return [Integer] Timeout in seconds
|
|
29
|
+
MAX_WAIT_TIME = 120
|
|
30
|
+
|
|
31
|
+
# Interval between status polls (in seconds)
|
|
32
|
+
# @return [Integer] Poll interval in seconds
|
|
33
|
+
POLL_INTERVAL = 2
|
|
34
|
+
|
|
35
|
+
# Wait for SSH to become available on a machine.
|
|
36
|
+
#
|
|
37
|
+
# Polls the machine status every POLL_INTERVAL seconds until the machine
|
|
38
|
+
# reports 'running' status, or until MAX_WAIT_TIME is exceeded. Displays
|
|
39
|
+
# progress messages via the provided UI object.
|
|
40
|
+
#
|
|
41
|
+
# @param machine_name [String] The name of the machine to check
|
|
42
|
+
# @param ui [Vagrant::UI::Interface] UI object for displaying progress messages
|
|
43
|
+
# @return [Boolean] true when SSH is ready
|
|
44
|
+
# @raise [SSHNotReady] If machine doesn't become ready within MAX_WAIT_TIME
|
|
45
|
+
# @raise [CommandExecutionError] If OrbStack CLI command fails
|
|
46
|
+
# @raise [CommandTimeoutError] If OrbStack CLI command times out
|
|
47
|
+
# @raise [OrbStackNotInstalled] If OrbStack CLI is not available
|
|
48
|
+
#
|
|
49
|
+
# @example Wait for SSH
|
|
50
|
+
# SSHReadinessChecker.wait_for_ready('ubuntu-dev', ui: machine.ui)
|
|
51
|
+
# # => true
|
|
52
|
+
#
|
|
53
|
+
# @api public
|
|
54
|
+
# rubocop:disable Naming/MethodParameterName
|
|
55
|
+
def self.wait_for_ready(machine_name, ui:)
|
|
56
|
+
# rubocop:enable Naming/MethodParameterName
|
|
57
|
+
ui.info("Waiting for SSH to become available on #{machine_name}...")
|
|
58
|
+
|
|
59
|
+
poll_until_ready(machine_name, ui) ||
|
|
60
|
+
raise_timeout_error(machine_name)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Poll machine status until ready or timeout.
|
|
64
|
+
#
|
|
65
|
+
# @param machine_name [String] The name of the machine to check
|
|
66
|
+
# @param ui [Vagrant::UI::Interface] UI object for progress messages
|
|
67
|
+
# @return [Boolean, nil] true if ready, nil if timeout
|
|
68
|
+
# @api private
|
|
69
|
+
# rubocop:disable Naming/MethodParameterName, Metrics/MethodLength
|
|
70
|
+
def self.poll_until_ready(machine_name, ui)
|
|
71
|
+
# rubocop:enable Naming/MethodParameterName, Metrics/MethodLength
|
|
72
|
+
start_time = Time.now
|
|
73
|
+
elapsed = 0
|
|
74
|
+
|
|
75
|
+
while elapsed < MAX_WAIT_TIME
|
|
76
|
+
if machine_running?(machine_name)
|
|
77
|
+
ui.info('SSH is ready!')
|
|
78
|
+
return true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
sleep(POLL_INTERVAL)
|
|
82
|
+
elapsed = Time.now - start_time
|
|
83
|
+
ui.info(" Still waiting... (#{elapsed.to_i} seconds elapsed)")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Raise timeout error with machine_name parameter for I18n.
|
|
90
|
+
#
|
|
91
|
+
# @param machine_name [String] The name of the machine
|
|
92
|
+
# @raise [SSHNotReady] Always raises with machine_name parameter
|
|
93
|
+
# @api private
|
|
94
|
+
def self.raise_timeout_error(machine_name)
|
|
95
|
+
raise SSHNotReady, machine_name: machine_name
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Check if machine is in running state.
|
|
99
|
+
#
|
|
100
|
+
# Queries OrbStack CLI for machine status and checks if it's 'running'.
|
|
101
|
+
# Handles nil responses and missing 'status' keys gracefully.
|
|
102
|
+
#
|
|
103
|
+
# @param machine_name [String] The name of the machine to check
|
|
104
|
+
# @return [Boolean] true if machine status is 'running', false otherwise
|
|
105
|
+
# @api private
|
|
106
|
+
def self.machine_running?(machine_name)
|
|
107
|
+
info = OrbStackCLI.machine_info(machine_name)
|
|
108
|
+
info && info.dig('record', 'state') == 'running'
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private_class_method :poll_until_ready, :raise_timeout_error, :machine_running?
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VagrantPlugins
|
|
4
|
+
module OrbStack
|
|
5
|
+
module Util
|
|
6
|
+
# TTL-based cache utility for machine state queries.
|
|
7
|
+
#
|
|
8
|
+
# This class provides a simple time-to-live (TTL) cache to reduce redundant
|
|
9
|
+
# CLI calls when querying machine state. State queries are cached with a
|
|
10
|
+
# configurable TTL (default 5 seconds), and automatically expire when the
|
|
11
|
+
# TTL is exceeded.
|
|
12
|
+
#
|
|
13
|
+
# The cache is designed for Vagrant's single-threaded environment and does
|
|
14
|
+
# not implement thread-safety mechanisms.
|
|
15
|
+
#
|
|
16
|
+
# @example Basic usage with default TTL
|
|
17
|
+
# cache = StateCache.new
|
|
18
|
+
# cache.set('machine-id', state)
|
|
19
|
+
# cached_state = cache.get('machine-id') # Returns state
|
|
20
|
+
#
|
|
21
|
+
# @example Custom TTL
|
|
22
|
+
# cache = StateCache.new(ttl: 10) # 10-second cache
|
|
23
|
+
# cache.set('key', 'value')
|
|
24
|
+
# # 11 seconds later...
|
|
25
|
+
# cache.get('key') # Returns nil (expired)
|
|
26
|
+
#
|
|
27
|
+
# @example Cache invalidation
|
|
28
|
+
# cache.invalidate('key') # Remove specific key
|
|
29
|
+
# cache.invalidate_all # Clear entire cache
|
|
30
|
+
class StateCache
|
|
31
|
+
# Default TTL for cached entries (in seconds)
|
|
32
|
+
DEFAULT_TTL = 5
|
|
33
|
+
|
|
34
|
+
# Initialize a new state cache.
|
|
35
|
+
#
|
|
36
|
+
# @param ttl [Integer] Time-to-live for cached entries in seconds (default: 5)
|
|
37
|
+
# @api public
|
|
38
|
+
def initialize(ttl: DEFAULT_TTL)
|
|
39
|
+
@ttl = ttl
|
|
40
|
+
@cache = {}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Retrieve a cached value.
|
|
44
|
+
#
|
|
45
|
+
# Returns the cached value if it exists and has not expired. Returns nil
|
|
46
|
+
# if the key doesn't exist or if the cached entry has exceeded its TTL.
|
|
47
|
+
#
|
|
48
|
+
# @param key [String] The cache key
|
|
49
|
+
# @return [Object, nil] The cached value if valid, nil if missing or expired
|
|
50
|
+
# @api public
|
|
51
|
+
def get(key)
|
|
52
|
+
entry = @cache[key]
|
|
53
|
+
return nil unless entry
|
|
54
|
+
|
|
55
|
+
# Check if entry has expired
|
|
56
|
+
if Time.now - entry[:timestamp] > @ttl
|
|
57
|
+
# Entry expired, remove it and return nil
|
|
58
|
+
@cache.delete(key)
|
|
59
|
+
return nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
entry[:value]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Store a value in the cache.
|
|
66
|
+
#
|
|
67
|
+
# Stores the value with the current timestamp. If the key already exists,
|
|
68
|
+
# its value and timestamp are overwritten.
|
|
69
|
+
#
|
|
70
|
+
# @param key [String] The cache key
|
|
71
|
+
# @param value [Object] The value to cache (can be any object, including nil)
|
|
72
|
+
# @return [void]
|
|
73
|
+
# @api public
|
|
74
|
+
def set(key, value)
|
|
75
|
+
@cache[key] = {
|
|
76
|
+
value: value,
|
|
77
|
+
timestamp: Time.now
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Invalidate a specific cache entry.
|
|
82
|
+
#
|
|
83
|
+
# Removes the specified key from the cache. This is a no-op if the key
|
|
84
|
+
# doesn't exist.
|
|
85
|
+
#
|
|
86
|
+
# @param key [String] The cache key to remove
|
|
87
|
+
# @return [void]
|
|
88
|
+
# @api public
|
|
89
|
+
def invalidate(key)
|
|
90
|
+
@cache.delete(key)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Clear all cache entries.
|
|
94
|
+
#
|
|
95
|
+
# Removes all cached entries. This is useful when you need to ensure
|
|
96
|
+
# fresh data is retrieved on the next query.
|
|
97
|
+
#
|
|
98
|
+
# @return [void]
|
|
99
|
+
# @api public
|
|
100
|
+
def invalidate_all
|
|
101
|
+
@cache.clear
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'vagrant-orbstack/plugin'
|
|
5
|
+
|
|
6
|
+
module VagrantPlugins
|
|
7
|
+
# OrbStack provider for Vagrant.
|
|
8
|
+
#
|
|
9
|
+
# This module contains the implementation of the OrbStack provider plugin,
|
|
10
|
+
# enabling Vagrant to use OrbStack as a backend for managing Linux
|
|
11
|
+
# development environments on macOS.
|
|
12
|
+
#
|
|
13
|
+
# @see Plugin
|
|
14
|
+
# @see Provider
|
|
15
|
+
# @see Config
|
|
16
|
+
module OrbStack
|
|
17
|
+
lib_path = Pathname.new(File.join(__dir__, 'vagrant-orbstack'))
|
|
18
|
+
autoload :Action, lib_path.join('action')
|
|
19
|
+
autoload :Errors, lib_path.join('errors')
|
|
20
|
+
|
|
21
|
+
# Sentinel value to distinguish "not set" from nil.
|
|
22
|
+
# Used in configuration to differentiate between explicitly set to nil
|
|
23
|
+
# versus never configured (should use default).
|
|
24
|
+
#
|
|
25
|
+
# @api private
|
|
26
|
+
UNSET_VALUE = ::Object.new.freeze
|
|
27
|
+
|
|
28
|
+
# Returns the path to the source of this plugin.
|
|
29
|
+
#
|
|
30
|
+
# @return [Pathname] Root directory of the plugin source
|
|
31
|
+
# @api private
|
|
32
|
+
def self.source_root
|
|
33
|
+
@source_root ||= Pathname.new(File.join(__dir__, '..'))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|