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,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'vagrant-orbstack'
|
|
4
|
+
|
|
5
|
+
module VagrantPlugins
|
|
6
|
+
module OrbStack
|
|
7
|
+
# Configuration class for OrbStack provider.
|
|
8
|
+
#
|
|
9
|
+
# This class defines configuration options available in Vagrantfiles
|
|
10
|
+
# for customizing OrbStack machine creation and behavior.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic configuration
|
|
13
|
+
# Vagrant.configure("2") do |config|
|
|
14
|
+
# config.vm.provider :orbstack do |os|
|
|
15
|
+
# os.distro = "ubuntu"
|
|
16
|
+
# os.version = "22.04"
|
|
17
|
+
# os.machine_name = "my-dev-env"
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @api public
|
|
22
|
+
class Config < Vagrant.plugin('2', :config)
|
|
23
|
+
# @!attribute [rw] distro
|
|
24
|
+
# Linux distribution to use for the machine
|
|
25
|
+
# @return [String, nil] Distribution name (e.g., "ubuntu", "debian")
|
|
26
|
+
# @api public
|
|
27
|
+
#
|
|
28
|
+
# @!attribute [rw] version
|
|
29
|
+
# Distribution version to use
|
|
30
|
+
# @return [String, nil] Version string (e.g., "22.04")
|
|
31
|
+
# @api public
|
|
32
|
+
#
|
|
33
|
+
# @!attribute [rw] machine_name
|
|
34
|
+
# Custom name for the OrbStack machine
|
|
35
|
+
# @return [String, nil] Machine name
|
|
36
|
+
# @api public
|
|
37
|
+
#
|
|
38
|
+
# @!attribute [rw] ssh_username
|
|
39
|
+
# Custom SSH username for connecting to the machine
|
|
40
|
+
# @return [String, nil] SSH username (defaults to OrbStack's default if nil)
|
|
41
|
+
# @api public
|
|
42
|
+
#
|
|
43
|
+
# @!attribute [rw] forward_agent
|
|
44
|
+
# Enable SSH agent forwarding
|
|
45
|
+
# @return [Boolean, nil] Whether to forward SSH agent (defaults to false)
|
|
46
|
+
# @api public
|
|
47
|
+
attr_accessor :distro
|
|
48
|
+
attr_accessor :version, :machine_name, :ssh_username, :forward_agent
|
|
49
|
+
|
|
50
|
+
# Error message constants for validation
|
|
51
|
+
DISTRO_EMPTY_ERROR = 'distro cannot be empty'
|
|
52
|
+
MACHINE_NAME_FORMAT_ERROR = 'machine_name must contain only alphanumeric characters and hyphens'
|
|
53
|
+
SSH_USERNAME_EMPTY_ERROR = 'ssh_username cannot be empty'
|
|
54
|
+
FORWARD_AGENT_BOOLEAN_ERROR = 'forward_agent must be a boolean (true or false)'
|
|
55
|
+
|
|
56
|
+
# Regular expression pattern for valid machine_name format.
|
|
57
|
+
#
|
|
58
|
+
# Valid machine names must:
|
|
59
|
+
# - Start with an alphanumeric character (a-z, A-Z, 0-9)
|
|
60
|
+
# - End with an alphanumeric character
|
|
61
|
+
# - May contain hyphens between alphanumeric segments
|
|
62
|
+
# - No consecutive hyphens allowed
|
|
63
|
+
MACHINE_NAME_PATTERN = /^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$/
|
|
64
|
+
|
|
65
|
+
# Initialize configuration with unset values.
|
|
66
|
+
#
|
|
67
|
+
# @api public
|
|
68
|
+
def initialize
|
|
69
|
+
super
|
|
70
|
+
@distro = VagrantPlugins::OrbStack::UNSET_VALUE
|
|
71
|
+
@version = VagrantPlugins::OrbStack::UNSET_VALUE
|
|
72
|
+
@machine_name = VagrantPlugins::OrbStack::UNSET_VALUE
|
|
73
|
+
@ssh_username = VagrantPlugins::OrbStack::UNSET_VALUE
|
|
74
|
+
@forward_agent = VagrantPlugins::OrbStack::UNSET_VALUE
|
|
75
|
+
@logger = Log4r::Logger.new('vagrant_orbstack::config')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Finalize configuration by setting defaults.
|
|
79
|
+
#
|
|
80
|
+
# Called by Vagrant after Vagrantfile is loaded to set default values
|
|
81
|
+
# for any unset configuration options.
|
|
82
|
+
#
|
|
83
|
+
# @api public
|
|
84
|
+
def finalize!
|
|
85
|
+
@distro = 'ubuntu' if @distro == VagrantPlugins::OrbStack::UNSET_VALUE
|
|
86
|
+
@version = nil if @version == VagrantPlugins::OrbStack::UNSET_VALUE
|
|
87
|
+
@machine_name = nil if @machine_name == VagrantPlugins::OrbStack::UNSET_VALUE
|
|
88
|
+
@ssh_username = nil if @ssh_username == VagrantPlugins::OrbStack::UNSET_VALUE
|
|
89
|
+
@forward_agent = false if @forward_agent == VagrantPlugins::OrbStack::UNSET_VALUE
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Validate configuration values.
|
|
93
|
+
#
|
|
94
|
+
# @param [Vagrant::Machine] _machine The machine to validate for
|
|
95
|
+
# @return [Hash<String, Array<String>>] Validation errors by namespace
|
|
96
|
+
# @api public
|
|
97
|
+
def validate(_machine)
|
|
98
|
+
errors = _detected_errors
|
|
99
|
+
validate_distro(errors)
|
|
100
|
+
validate_machine_name(errors)
|
|
101
|
+
validate_ssh_username(errors)
|
|
102
|
+
validate_forward_agent(errors)
|
|
103
|
+
{ 'OrbStack Provider' => errors }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# Validation methods use defensive type coercion (.to_s) to prevent
|
|
109
|
+
# NoMethodError crashes if attributes are accidentally set to non-String types.
|
|
110
|
+
# This approach prioritizes robustness over strict type enforcement.
|
|
111
|
+
#
|
|
112
|
+
# Pattern: Always check nil first, then coerce to string for validation.
|
|
113
|
+
#
|
|
114
|
+
# Example:
|
|
115
|
+
# return if @attribute.nil?
|
|
116
|
+
# errors << ERROR_CONSTANT if @attribute.to_s.strip.empty?
|
|
117
|
+
|
|
118
|
+
# Validate that distro attribute is not empty.
|
|
119
|
+
#
|
|
120
|
+
# @param errors [Array<String>] Error accumulator array
|
|
121
|
+
# @return [void]
|
|
122
|
+
def validate_distro(errors)
|
|
123
|
+
errors << DISTRO_EMPTY_ERROR if @distro.nil? || @distro.to_s.strip.empty?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Validate machine_name format if set.
|
|
127
|
+
#
|
|
128
|
+
# @param errors [Array<String>] Error accumulator array
|
|
129
|
+
# @return [void]
|
|
130
|
+
def validate_machine_name(errors)
|
|
131
|
+
return if @machine_name.nil?
|
|
132
|
+
|
|
133
|
+
# Convert to string for regex matching (defensive programming)
|
|
134
|
+
machine_name_str = @machine_name.to_s
|
|
135
|
+
errors << MACHINE_NAME_FORMAT_ERROR unless machine_name_str.match?(MACHINE_NAME_PATTERN)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Validate that ssh_username attribute is not empty if set.
|
|
139
|
+
#
|
|
140
|
+
# @param errors [Array<String>] Error accumulator array
|
|
141
|
+
# @return [void]
|
|
142
|
+
def validate_ssh_username(errors)
|
|
143
|
+
return if @ssh_username.nil?
|
|
144
|
+
return unless @ssh_username.to_s.strip.empty?
|
|
145
|
+
|
|
146
|
+
errors << SSH_USERNAME_EMPTY_ERROR
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Validate that forward_agent is a boolean if set.
|
|
150
|
+
#
|
|
151
|
+
# @param errors [Array<String>] Error accumulator array
|
|
152
|
+
# @return [void]
|
|
153
|
+
def validate_forward_agent(errors)
|
|
154
|
+
return if @forward_agent.nil?
|
|
155
|
+
return if [true, false].include?(@forward_agent)
|
|
156
|
+
|
|
157
|
+
errors << FORWARD_AGENT_BOOLEAN_ERROR
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VagrantPlugins
|
|
4
|
+
module OrbStack
|
|
5
|
+
# Base error class for OrbStack provider errors.
|
|
6
|
+
# Inherits from Vagrant's VagrantError to integrate with Vagrant's error handling.
|
|
7
|
+
class Errors < Vagrant::Errors::VagrantError
|
|
8
|
+
error_namespace('vagrant_orbstack.errors')
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Error raised when OrbStack is not installed.
|
|
12
|
+
class OrbStackNotInstalled < Errors
|
|
13
|
+
error_key(:orbstack_not_installed)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Error raised when OrbStack is not running.
|
|
17
|
+
class OrbStackNotRunning < Errors
|
|
18
|
+
error_key(:orbstack_not_running)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Error raised when an OrbStack CLI command fails.
|
|
22
|
+
class CommandExecutionError < Errors
|
|
23
|
+
error_key(:command_execution_error)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Error raised when an OrbStack CLI command times out.
|
|
27
|
+
class CommandTimeoutError < Errors
|
|
28
|
+
error_key(:command_timeout_error)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Error raised when machine name collision cannot be resolved after retries.
|
|
32
|
+
class MachineNameCollisionError < Errors
|
|
33
|
+
error_key(:machine_name_collision)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Error raised when SSH is not ready on a machine.
|
|
37
|
+
class SSHNotReady < Errors
|
|
38
|
+
error_key(:ssh_not_ready)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Error raised when SSH connection fails.
|
|
42
|
+
class SSHConnectionFailed < Errors
|
|
43
|
+
error_key(:ssh_connection_failed)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Reopen Errors class to add nested constants for namespaced access
|
|
47
|
+
# This allows both VagrantPlugins::OrbStack::CommandTimeoutError
|
|
48
|
+
# and VagrantPlugins::OrbStack::Errors::CommandTimeoutError to work
|
|
49
|
+
class Errors
|
|
50
|
+
# Alias error classes inside Errors namespace for tests that expect them there
|
|
51
|
+
OrbStackNotInstalled = ::VagrantPlugins::OrbStack::OrbStackNotInstalled
|
|
52
|
+
OrbStackNotInstalledError = OrbStackNotInstalled
|
|
53
|
+
OrbStackNotRunning = ::VagrantPlugins::OrbStack::OrbStackNotRunning
|
|
54
|
+
CommandExecutionError = ::VagrantPlugins::OrbStack::CommandExecutionError
|
|
55
|
+
CommandTimeoutError = ::VagrantPlugins::OrbStack::CommandTimeoutError
|
|
56
|
+
MachineNameCollisionError = ::VagrantPlugins::OrbStack::MachineNameCollisionError
|
|
57
|
+
SSHNotReady = ::VagrantPlugins::OrbStack::SSHNotReady
|
|
58
|
+
SSHConnectionFailed = ::VagrantPlugins::OrbStack::SSHConnectionFailed
|
|
59
|
+
|
|
60
|
+
# Alias for CLI errors
|
|
61
|
+
OrbStackCLIError = CommandExecutionError
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'vagrant-orbstack/version'
|
|
4
|
+
|
|
5
|
+
module VagrantPlugins
|
|
6
|
+
# OrbStack provider plugin namespace
|
|
7
|
+
module OrbStack
|
|
8
|
+
# OrbStack provider plugin for Vagrant.
|
|
9
|
+
#
|
|
10
|
+
# This plugin enables OrbStack as a provider backend for Vagrant,
|
|
11
|
+
# allowing users to create and manage Linux development environments
|
|
12
|
+
# on macOS using OrbStack's high-performance virtualization.
|
|
13
|
+
#
|
|
14
|
+
# @api public
|
|
15
|
+
class Plugin < Vagrant.plugin('2')
|
|
16
|
+
name 'vagrant-orbstack'
|
|
17
|
+
description 'Enables OrbStack as a Vagrant provider for macOS development'
|
|
18
|
+
|
|
19
|
+
# Register OrbStack provider with Vagrant.
|
|
20
|
+
#
|
|
21
|
+
# @api private
|
|
22
|
+
provider(:orbstack, priority: 5) do
|
|
23
|
+
require_relative 'provider'
|
|
24
|
+
Provider
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Register configuration class for OrbStack provider.
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
config(:orbstack, :provider) do
|
|
31
|
+
require_relative 'config'
|
|
32
|
+
Config
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Setup I18n locale files for error messages
|
|
36
|
+
def self.setup!
|
|
37
|
+
locale_path = Pathname.new(File.expand_path('../locales', __dir__))
|
|
38
|
+
I18n.load_path << locale_path.join('en.yml') if locale_path.exist?
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Initialize I18n on plugin load
|
|
43
|
+
Plugin.setup!
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'vagrant-orbstack/errors'
|
|
6
|
+
require 'vagrant-orbstack/util/state_cache'
|
|
7
|
+
require 'vagrant-orbstack/util/orbstack_cli'
|
|
8
|
+
require 'vagrant-orbstack/action'
|
|
9
|
+
|
|
10
|
+
module VagrantPlugins
|
|
11
|
+
module OrbStack
|
|
12
|
+
# Vagrant provider implementation for OrbStack.
|
|
13
|
+
#
|
|
14
|
+
# This class implements the Vagrant provider interface, delegating
|
|
15
|
+
# machine lifecycle operations to OrbStack via CLI commands.
|
|
16
|
+
#
|
|
17
|
+
# @api public
|
|
18
|
+
class Provider < Vagrant.plugin('2', :provider)
|
|
19
|
+
# Initialize the provider with a machine instance.
|
|
20
|
+
#
|
|
21
|
+
# @param [Vagrant::Machine] machine The machine this provider is for
|
|
22
|
+
# @api public
|
|
23
|
+
def initialize(machine)
|
|
24
|
+
@machine = machine
|
|
25
|
+
@logger = Log4r::Logger.new('vagrant_orbstack::provider')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Return action middleware for requested operation.
|
|
29
|
+
#
|
|
30
|
+
# Creates and returns an Action::Builder containing the appropriate
|
|
31
|
+
# middleware stack for the requested operation. Currently only :up
|
|
32
|
+
# is implemented; other actions return nil.
|
|
33
|
+
#
|
|
34
|
+
# @param [Symbol] name The action name (:up, :halt, :destroy, etc.)
|
|
35
|
+
# @return [Vagrant::Action::Builder, nil] Action middleware builder or nil
|
|
36
|
+
# @api public
|
|
37
|
+
# rubocop:disable Metrics/MethodLength
|
|
38
|
+
def action(name)
|
|
39
|
+
case name
|
|
40
|
+
when :up
|
|
41
|
+
Vagrant::Action::Builder.new.tap do |b|
|
|
42
|
+
b.use Action::Create
|
|
43
|
+
end
|
|
44
|
+
when :halt
|
|
45
|
+
Vagrant::Action::Builder.new.tap do |b|
|
|
46
|
+
b.use Action::Halt
|
|
47
|
+
end
|
|
48
|
+
when :reload
|
|
49
|
+
build_reload_action
|
|
50
|
+
when :ssh_run
|
|
51
|
+
Vagrant::Action::Builder.new.tap do |b|
|
|
52
|
+
b.use Action::SSHRun
|
|
53
|
+
end
|
|
54
|
+
when :destroy
|
|
55
|
+
Vagrant::Action::Builder.new.tap do |b|
|
|
56
|
+
b.use Action::Destroy
|
|
57
|
+
end
|
|
58
|
+
# Return nil for unsupported actions (future stories, etc.)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
# rubocop:enable Metrics/MethodLength
|
|
62
|
+
|
|
63
|
+
# Provide SSH connection information for the machine.
|
|
64
|
+
#
|
|
65
|
+
# Returns SSH connection parameters for Vagrant to connect to the machine
|
|
66
|
+
# using OrbStack's SSH proxy architecture.
|
|
67
|
+
#
|
|
68
|
+
# CRITICAL: OrbStack uses SSH proxy at localhost:32222, NOT direct SSH to VM IP.
|
|
69
|
+
#
|
|
70
|
+
# Returns nil if the machine is not running.
|
|
71
|
+
#
|
|
72
|
+
# @return [Hash, nil] SSH connection parameters with keys:
|
|
73
|
+
# - :host - Always '127.0.0.1' (OrbStack SSH proxy, NOT VM IP)
|
|
74
|
+
# - :port - Always 32222 (OrbStack SSH proxy port, NOT 22)
|
|
75
|
+
# - :username - Machine ID for proxy routing
|
|
76
|
+
# - :private_key_path - OrbStack's auto-generated ED25519 key
|
|
77
|
+
# - :forward_agent - Whether to forward SSH agent (from config)
|
|
78
|
+
# @api public
|
|
79
|
+
def ssh_info
|
|
80
|
+
# Return nil if machine is not running
|
|
81
|
+
current_state = state
|
|
82
|
+
return nil if %i[not_created stopped].include?(current_state.id)
|
|
83
|
+
|
|
84
|
+
# Return OrbStack SSH proxy configuration
|
|
85
|
+
{
|
|
86
|
+
host: '127.0.0.1',
|
|
87
|
+
port: 32_222,
|
|
88
|
+
username: @machine.id,
|
|
89
|
+
private_key_path: File.expand_path('~/.orbstack/ssh/id_ed25519'),
|
|
90
|
+
proxy_command: orbstack_proxy_command,
|
|
91
|
+
forward_agent: @machine.provider_config.forward_agent
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Return current machine state.
|
|
96
|
+
#
|
|
97
|
+
# Queries OrbStack CLI to determine the current state of the machine.
|
|
98
|
+
# Results are cached with a 5-second TTL to reduce redundant CLI calls.
|
|
99
|
+
# State is automatically invalidated when state-changing actions occur.
|
|
100
|
+
#
|
|
101
|
+
# @return [Vagrant::MachineState] Current state of the machine
|
|
102
|
+
# @api public
|
|
103
|
+
def state
|
|
104
|
+
# Return early if machine ID is nil
|
|
105
|
+
return not_created_state('The machine has not been created') if @machine.id.nil?
|
|
106
|
+
|
|
107
|
+
# Check cache first
|
|
108
|
+
cached_state = state_cache.get(@machine.id)
|
|
109
|
+
return cached_state if cached_state
|
|
110
|
+
|
|
111
|
+
# Cache miss: Query OrbStack CLI
|
|
112
|
+
query_and_cache_state
|
|
113
|
+
rescue StandardError => e
|
|
114
|
+
# Handle query errors gracefully
|
|
115
|
+
handle_state_query_error(e)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Invalidate the state cache.
|
|
119
|
+
#
|
|
120
|
+
# Clears all cached state entries, forcing the next state query to
|
|
121
|
+
# fetch fresh data from OrbStack CLI. This is typically called by
|
|
122
|
+
# action middleware after state-changing operations (create, start, stop).
|
|
123
|
+
#
|
|
124
|
+
# @return [void]
|
|
125
|
+
# @api public
|
|
126
|
+
def invalidate_state_cache
|
|
127
|
+
state_cache.invalidate_all
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Human-readable provider description.
|
|
131
|
+
#
|
|
132
|
+
# @return [String] Provider name
|
|
133
|
+
# @api public
|
|
134
|
+
def to_s
|
|
135
|
+
'OrbStack'
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Callback invoked when the machine ID changes.
|
|
139
|
+
#
|
|
140
|
+
# Persists the new machine ID to the data directory for retrieval
|
|
141
|
+
# in future Vagrant sessions. This is called by Vagrant core when
|
|
142
|
+
# a machine is created or its ID is updated.
|
|
143
|
+
#
|
|
144
|
+
# The guard clause ensures we only persist when the machine has a valid ID,
|
|
145
|
+
# as some test scenarios may not have @machine.id available.
|
|
146
|
+
#
|
|
147
|
+
# @return [void]
|
|
148
|
+
# @api public
|
|
149
|
+
def machine_id_changed
|
|
150
|
+
# Guard clause: Only persist if machine has an ID
|
|
151
|
+
# Some test scenarios may not have @machine.id available
|
|
152
|
+
return unless @machine.respond_to?(:id) && !@machine.id.nil?
|
|
153
|
+
|
|
154
|
+
write_machine_id(@machine.id)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Read the machine ID from persistent storage.
|
|
158
|
+
#
|
|
159
|
+
# Reads the machine ID from the id file in the data directory.
|
|
160
|
+
# Returns nil if the file doesn't exist or cannot be read.
|
|
161
|
+
#
|
|
162
|
+
# @return [String, nil] The machine ID if found, nil otherwise
|
|
163
|
+
# @raise [Errno::EACCES] If permission denied (logged and returns nil)
|
|
164
|
+
# @raise [Errno::ENOENT] If file not found (logged and returns nil)
|
|
165
|
+
# @raise [Encoding::InvalidByteSequenceError] If file contains invalid data (logged and returns nil)
|
|
166
|
+
# @api public
|
|
167
|
+
def read_machine_id
|
|
168
|
+
return nil unless File.exist?(id_file_path)
|
|
169
|
+
|
|
170
|
+
File.read(id_file_path).strip
|
|
171
|
+
rescue Errno::EACCES, Errno::ENOENT, Encoding::InvalidByteSequenceError => e
|
|
172
|
+
# Log error and return nil - graceful degradation for non-critical errors
|
|
173
|
+
@machine.ui&.warn("OrbStack: Could not read machine ID: #{e.message}")
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Write the machine ID to persistent storage.
|
|
178
|
+
#
|
|
179
|
+
# Writes the machine ID to the id file in the data directory.
|
|
180
|
+
# Creates the directory if it doesn't exist.
|
|
181
|
+
#
|
|
182
|
+
# @param [String] machine_id The machine ID to persist
|
|
183
|
+
# @return [void]
|
|
184
|
+
# @raise [Errno::EACCES] If permission denied
|
|
185
|
+
# @raise [Errno::ENOSPC] If disk is full
|
|
186
|
+
# @raise [Errno::EROFS] If filesystem is read-only
|
|
187
|
+
# @api public
|
|
188
|
+
def write_machine_id(machine_id)
|
|
189
|
+
ensure_data_dir_exists
|
|
190
|
+
File.write(id_file_path, machine_id)
|
|
191
|
+
rescue Errno::EACCES, Errno::ENOSPC, Errno::EROFS => e
|
|
192
|
+
# Log error and re-raise - critical errors that cannot be ignored
|
|
193
|
+
@machine.ui&.error("OrbStack: Could not write machine ID: #{e.message}")
|
|
194
|
+
raise
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Read machine metadata from persistent storage.
|
|
198
|
+
#
|
|
199
|
+
# Reads metadata from the metadata.json file in the data directory.
|
|
200
|
+
# Returns an empty hash if the file doesn't exist or contains invalid JSON.
|
|
201
|
+
#
|
|
202
|
+
# @return [Hash] The metadata hash, or empty hash if not found
|
|
203
|
+
# @raise [JSON::ParserError] If JSON is invalid (logged and returns {})
|
|
204
|
+
# @raise [Errno::EACCES] If permission denied (logged and returns {})
|
|
205
|
+
# @raise [Errno::ENOENT] If file not found (logged and returns {})
|
|
206
|
+
# @raise [Encoding::InvalidByteSequenceError] If file contains invalid data (logged and returns {})
|
|
207
|
+
# @api public
|
|
208
|
+
def read_metadata
|
|
209
|
+
return {} unless File.exist?(metadata_file_path)
|
|
210
|
+
|
|
211
|
+
JSON.parse(File.read(metadata_file_path))
|
|
212
|
+
rescue JSON::ParserError, Errno::EACCES, Errno::ENOENT, Encoding::InvalidByteSequenceError => e
|
|
213
|
+
# Log error and return empty hash - graceful degradation for non-critical errors
|
|
214
|
+
@machine.ui&.warn("OrbStack: Could not read metadata: #{e.message}")
|
|
215
|
+
{}
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Write machine metadata to persistent storage.
|
|
219
|
+
#
|
|
220
|
+
# Writes metadata to the metadata.json file in the data directory.
|
|
221
|
+
# Creates the directory if it doesn't exist. Formats JSON for readability.
|
|
222
|
+
#
|
|
223
|
+
# @param [Hash] metadata The metadata hash to persist
|
|
224
|
+
# @return [void]
|
|
225
|
+
# @raise [JSON::ParserError] If JSON generation fails
|
|
226
|
+
# @raise [Errno::EACCES] If permission denied
|
|
227
|
+
# @raise [Errno::ENOSPC] If disk is full
|
|
228
|
+
# @raise [Errno::EROFS] If filesystem is read-only
|
|
229
|
+
# @api public
|
|
230
|
+
def write_metadata(metadata)
|
|
231
|
+
ensure_data_dir_exists
|
|
232
|
+
File.write(metadata_file_path, JSON.pretty_generate(metadata))
|
|
233
|
+
rescue JSON::ParserError, Errno::EACCES, Errno::ENOSPC, Errno::EROFS => e
|
|
234
|
+
# Log error and re-raise - critical errors that cannot be ignored
|
|
235
|
+
@machine.ui&.error("OrbStack: Could not write metadata: #{e.message}")
|
|
236
|
+
raise
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Path to the machine ID file.
|
|
240
|
+
#
|
|
241
|
+
# @return [Pathname] Path to the ID file
|
|
242
|
+
# @api public
|
|
243
|
+
def id_file_path
|
|
244
|
+
@machine.data_dir.join('id')
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Path to the metadata JSON file.
|
|
248
|
+
#
|
|
249
|
+
# @return [Pathname] Path to the metadata file
|
|
250
|
+
# @api public
|
|
251
|
+
def metadata_file_path
|
|
252
|
+
@machine.data_dir.join('metadata.json')
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
private
|
|
256
|
+
|
|
257
|
+
# Get the state cache instance.
|
|
258
|
+
#
|
|
259
|
+
# Lazy-initializes a StateCache instance with 5-second TTL.
|
|
260
|
+
# The cache is shared across all state queries for this provider instance.
|
|
261
|
+
#
|
|
262
|
+
# @return [Util::StateCache] The state cache instance
|
|
263
|
+
# @api private
|
|
264
|
+
def state_cache
|
|
265
|
+
@state_cache ||= Util::StateCache.new(ttl: 5)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Generate OrbStack SSH ProxyCommand.
|
|
269
|
+
#
|
|
270
|
+
# OrbStack routes all SSH connections through the OrbStack Helper app
|
|
271
|
+
# using a ProxyCommand. This returns the correctly formatted command
|
|
272
|
+
# that Vagrant's SSH layer will use.
|
|
273
|
+
#
|
|
274
|
+
# @return [String] ProxyCommand string for SSH config
|
|
275
|
+
# @api private
|
|
276
|
+
def orbstack_proxy_command
|
|
277
|
+
helper_path = '/Applications/OrbStack.app/Contents/Frameworks/' \
|
|
278
|
+
'OrbStack Helper.app/Contents/MacOS/OrbStack Helper'
|
|
279
|
+
"'#{helper_path}' ssh-proxy-fdpass #{Process.uid}"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Map OrbStack machine info to Vagrant state tuple.
|
|
283
|
+
#
|
|
284
|
+
# Converts OrbStack machine status to Vagrant state representation.
|
|
285
|
+
# Returns a tuple of [state_id, short_description, long_description].
|
|
286
|
+
#
|
|
287
|
+
# @param machine_info [Hash, nil] Machine info from OrbStack CLI with :name and :status,
|
|
288
|
+
# or nil if machine was not found
|
|
289
|
+
# @return [Array<Symbol, String, String>] Tuple of [state_id, short_desc, long_desc]
|
|
290
|
+
# @api private
|
|
291
|
+
def map_orbstack_state_to_vagrant(machine_info)
|
|
292
|
+
if machine_info.nil?
|
|
293
|
+
[:not_created, 'not created', 'Machine does not exist in OrbStack']
|
|
294
|
+
elsif machine_info[:status] == 'running'
|
|
295
|
+
[:running, 'running', 'Machine is running in OrbStack']
|
|
296
|
+
elsif machine_info[:status] == 'stopped'
|
|
297
|
+
[:stopped, 'stopped', 'Machine is stopped']
|
|
298
|
+
else
|
|
299
|
+
# Unknown state - treat as not created
|
|
300
|
+
[:not_created, 'unknown', 'Machine state is unknown']
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Create a :not_created MachineState with appropriate description.
|
|
305
|
+
#
|
|
306
|
+
# @param reason [String] The reason the machine is not created
|
|
307
|
+
# @return [Vagrant::MachineState] A not_created state object
|
|
308
|
+
# @api private
|
|
309
|
+
def not_created_state(reason)
|
|
310
|
+
Vagrant::MachineState.new(:not_created, 'not created', reason)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Query OrbStack CLI for current machine state and cache result.
|
|
314
|
+
#
|
|
315
|
+
# @return [Vagrant::MachineState] The queried machine state
|
|
316
|
+
# @api private
|
|
317
|
+
def query_and_cache_state
|
|
318
|
+
machines = Util::OrbStackCLI.list_machines
|
|
319
|
+
our_machine = machines.find { |m| m[:name] == @machine.id }
|
|
320
|
+
|
|
321
|
+
# Map OrbStack state to Vagrant state
|
|
322
|
+
state_id, short_desc, long_desc = map_orbstack_state_to_vagrant(our_machine)
|
|
323
|
+
|
|
324
|
+
# Create MachineState and cache it
|
|
325
|
+
machine_state = Vagrant::MachineState.new(state_id, short_desc, long_desc)
|
|
326
|
+
state_cache.set(@machine.id, machine_state)
|
|
327
|
+
|
|
328
|
+
machine_state
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Handle error during state query and return not_created state.
|
|
332
|
+
#
|
|
333
|
+
# @param error [Exception] The error that occurred
|
|
334
|
+
# @return [Vagrant::MachineState] A not_created state object
|
|
335
|
+
# @api private
|
|
336
|
+
def handle_state_query_error(error)
|
|
337
|
+
if error.is_a?(CommandTimeoutError)
|
|
338
|
+
@machine.ui.warn('OrbStack: Command timeout querying machine state')
|
|
339
|
+
@logger.warn("State query timed out: #{error.message}")
|
|
340
|
+
not_created_state('Timeout querying machine state')
|
|
341
|
+
else
|
|
342
|
+
@machine.ui.warn("OrbStack: Error querying machine state: #{error.message}")
|
|
343
|
+
@logger.error("Failed to query machine state: #{error.message}")
|
|
344
|
+
not_created_state('Error querying machine state')
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Ensure the data directory exists.
|
|
349
|
+
#
|
|
350
|
+
# Creates the data directory if it doesn't exist.
|
|
351
|
+
#
|
|
352
|
+
# @return [void]
|
|
353
|
+
# @api private
|
|
354
|
+
def ensure_data_dir_exists
|
|
355
|
+
dir = @machine.data_dir
|
|
356
|
+
FileUtils.mkdir_p(dir)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Build reload action: Halt → Start → (optional) Provision
|
|
360
|
+
#
|
|
361
|
+
# @param env [Hash] Environment hash (unused, for consistency)
|
|
362
|
+
# @return [Vagrant::Action::Builder] Configured action builder
|
|
363
|
+
# @api private
|
|
364
|
+
def build_reload_action(_env = nil)
|
|
365
|
+
Vagrant::Action::Builder.new.tap do |b|
|
|
366
|
+
b.use Action::Halt
|
|
367
|
+
b.use Action::Start
|
|
368
|
+
include_provisioning(b)
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Include provisioning middleware if available.
|
|
373
|
+
#
|
|
374
|
+
# Conditionally includes Vagrant's built-in Provision middleware.
|
|
375
|
+
# Defensive check ensures compatibility if middleware isn't available.
|
|
376
|
+
#
|
|
377
|
+
# @param builder [Vagrant::Action::Builder] Builder to modify
|
|
378
|
+
# @return [void]
|
|
379
|
+
# @api private
|
|
380
|
+
def include_provisioning(builder)
|
|
381
|
+
return unless defined?(Vagrant::Action::Builtin::Provision)
|
|
382
|
+
|
|
383
|
+
builder.use Vagrant::Action::Builtin::Provision
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|