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.
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require_relative '../util/machine_namer'
5
+ require_relative '../util/orbstack_cli'
6
+ require_relative '../util/ssh_readiness_checker'
7
+
8
+ module VagrantPlugins
9
+ module OrbStack
10
+ module Action
11
+ # Action middleware for creating and starting OrbStack machines
12
+ #
13
+ # This middleware handles the machine creation lifecycle with full
14
+ # idempotency support. It checks the current machine state and:
15
+ # - If running: does nothing (no-op)
16
+ # - If stopped: starts the existing machine
17
+ # - If not created: creates a new machine with unique name
18
+ #
19
+ # After creation, persists machine metadata and invalidates state cache.
20
+ #
21
+ # @example Usage in action builder
22
+ # Vagrant::Action::Builder.new.tap do |b|
23
+ # b.use VagrantPlugins::OrbStack::Action::Create
24
+ # end
25
+ #
26
+ # @api public
27
+ class Create
28
+ # Initialize the middleware.
29
+ #
30
+ # @param app [Object] The next middleware in the chain
31
+ # @param env [Hash] The environment hash containing :machine, :ui, etc.
32
+ # @api public
33
+ def initialize(app, _env)
34
+ @app = app
35
+ end
36
+
37
+ # Execute the middleware.
38
+ #
39
+ # Implements idempotent machine creation:
40
+ # 1. Query current state
41
+ # 2. If running: no-op
42
+ # 3. If stopped: start machine
43
+ # 4. If not created: create new machine
44
+ # 5. Persist metadata and invalidate cache
45
+ # 6. Continue middleware chain
46
+ #
47
+ # @param env [Hash] The environment hash
48
+ # @return [Object] Result from next middleware
49
+ # @raise [MachineNameCollisionError] If name generation fails
50
+ # @raise [OrbStackNotInstalled] If OrbStack CLI not available
51
+ # @raise [CommandTimeoutError] If CLI command times out
52
+ # @raise [CommandExecutionError] If machine creation/start fails
53
+ # @api public
54
+ def call(env)
55
+ handle_machine_state(env)
56
+ @app.call(env)
57
+ end
58
+
59
+ private
60
+
61
+ # Route to appropriate state handler based on current machine state.
62
+ #
63
+ # @param env [Hash] The environment hash
64
+ # @return [void]
65
+ # @api private
66
+ def handle_machine_state(env)
67
+ current_state = env[:machine].provider.state
68
+
69
+ case current_state.id
70
+ when :running then handle_running_machine(env)
71
+ when :stopped then handle_stopped_machine(env)
72
+ else handle_not_created_machine(env)
73
+ end
74
+ end
75
+
76
+ # Handle a machine that is already running (no-op).
77
+ #
78
+ # @param env [Hash] The environment hash
79
+ # @return [void]
80
+ # @api private
81
+ def handle_running_machine(env)
82
+ env[:machine].ui.info('Machine is already running')
83
+ end
84
+
85
+ # Handle a machine that is stopped by starting it.
86
+ #
87
+ # @param env [Hash] The environment hash
88
+ # @return [void]
89
+ # @api private
90
+ def handle_stopped_machine(env)
91
+ machine = env[:machine]
92
+ machine.ui.info('Starting stopped machine...')
93
+ Util::OrbStackCLI.start_machine(machine.id)
94
+ Util::SSHReadinessChecker.wait_for_ready(machine.id, ui: machine.ui)
95
+ machine.provider.invalidate_state_cache
96
+ end
97
+
98
+ # Handle a machine that doesn't exist by creating it.
99
+ #
100
+ # @param env [Hash] The environment hash
101
+ # @return [void]
102
+ # @api private
103
+ def handle_not_created_machine(env)
104
+ create_machine(env)
105
+ end
106
+
107
+ # Create a new OrbStack machine.
108
+ #
109
+ # Generates unique machine name, creates machine via OrbStack CLI,
110
+ # persists metadata, and invalidates state cache.
111
+ #
112
+ # @param env [Hash] The environment hash
113
+ # @return [void]
114
+ # @api private
115
+ def create_machine(env)
116
+ machine = env[:machine]
117
+ ui = machine.ui
118
+
119
+ ui.info('Creating new machine...')
120
+
121
+ machine_name = Util::MachineNamer.generate(machine)
122
+ distro = build_distribution_string(machine.provider_config)
123
+ Util::OrbStackCLI.create_machine(machine_name, distribution: distro)
124
+ Util::SSHReadinessChecker.wait_for_ready(machine_name, ui: ui)
125
+
126
+ persist_metadata(env, machine_name, distro)
127
+
128
+ ui.info("Machine '#{machine_name}' created successfully")
129
+ end
130
+
131
+ # Build distribution string from configuration.
132
+ #
133
+ # Formats the distribution string for OrbStack CLI based on
134
+ # configured distro and version. If version is specified, returns
135
+ # "distro:version", otherwise just "distro".
136
+ #
137
+ # @param config [VagrantPlugins::OrbStack::Config] Provider configuration
138
+ # @return [String] Formatted distribution string
139
+ # @api private
140
+ def build_distribution_string(config)
141
+ return config.distro unless config.version
142
+
143
+ "#{config.distro}:#{config.version}"
144
+ end
145
+
146
+ # Persist machine metadata to Vagrant data directory.
147
+ #
148
+ # Stores machine ID and metadata (name, distribution, timestamp)
149
+ # for future Vagrant operations.
150
+ #
151
+ # @param env [Hash] The environment hash
152
+ # @param machine_name [String] The generated machine name
153
+ # @param distro [String] The distribution string (e.g., "ubuntu:noble")
154
+ # @return [void]
155
+ # @api private
156
+ def persist_metadata(env, machine_name, distro)
157
+ machine = env[:machine]
158
+ provider = machine.provider
159
+
160
+ # Store machine ID (Vagrant core will call machine.id= after this)
161
+ provider.write_machine_id(machine_name)
162
+
163
+ # Store metadata
164
+ metadata = {
165
+ 'machine_name' => machine_name,
166
+ 'distribution' => distro,
167
+ 'created_at' => Time.now.utc.iso8601
168
+ }
169
+ provider.write_metadata(metadata)
170
+
171
+ # Invalidate state cache to force fresh query
172
+ provider.invalidate_state_cache
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../util/orbstack_cli'
4
+ require_relative 'machine_validation'
5
+ require 'fileutils'
6
+
7
+ module VagrantPlugins
8
+ module OrbStack
9
+ module Action
10
+ # Action middleware for destroying (deleting) OrbStack machines
11
+ #
12
+ # This middleware handles permanent removal of OrbStack machines, including
13
+ # deletion from OrbStack and cleanup of local metadata files. The operation
14
+ # is idempotent and uses a best-effort cleanup strategy: if the OrbStack
15
+ # CLI deletion fails (e.g., machine already deleted or daemon offline), local
16
+ # cleanup continues anyway to ensure Vagrant state remains consistent.
17
+ #
18
+ # @example Usage in action builder
19
+ # Vagrant::Action::Builder.new.tap do |b|
20
+ # b.use VagrantPlugins::OrbStack::Action::Destroy
21
+ # end
22
+ #
23
+ # @api public
24
+ class Destroy
25
+ include MachineValidation
26
+
27
+ # Initialize the middleware.
28
+ #
29
+ # @param app [Object] The next middleware in the chain
30
+ # @param env [Hash] The environment hash containing :machine, :ui, etc.
31
+ # @api public
32
+ def initialize(app, _env)
33
+ @app = app
34
+ end
35
+
36
+ # Execute the middleware.
37
+ #
38
+ # Destroys the machine by:
39
+ # 1. Calling OrbStack CLI delete command (best-effort)
40
+ # 2. Removing id and metadata.json files from data directory
41
+ # 3. Invalidating state cache
42
+ # 4. Continuing middleware chain
43
+ #
44
+ # If the OrbStack CLI delete fails, a warning is logged and cleanup
45
+ # continues. This ensures Vagrant can clean up its local state even
46
+ # if the machine was already deleted manually or OrbStack is offline.
47
+ #
48
+ # @param env [Hash] The environment hash
49
+ # @return [Object] Result from next middleware
50
+ # @raise [ArgumentError] If machine ID is empty string (nil is handled gracefully)
51
+ # @api public
52
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
53
+ def call(env)
54
+ machine = env[:machine]
55
+ ui = env[:ui]
56
+
57
+ # Handle already-destroyed machines gracefully (idempotency)
58
+ if machine.id.nil?
59
+ ui.info('Machine is already destroyed or was never created.')
60
+ return @app.call(env)
61
+ end
62
+
63
+ # Validate machine ID exists
64
+ machine_id = validate_machine_id!(machine, 'destroy')
65
+
66
+ ui.info("Destroying machine '#{machine_id}'...")
67
+
68
+ # Call OrbStack CLI to delete the machine (best-effort)
69
+ begin
70
+ Util::OrbStackCLI.delete_machine(machine_id)
71
+ rescue Errors::CommandExecutionError => e
72
+ ui.warn("Error deleting machine from OrbStack: #{e.message}")
73
+ ui.warn('Continuing with local cleanup...')
74
+ end
75
+
76
+ # Clean up metadata files from data_dir (idempotent)
77
+ FileUtils.rm_f(machine.provider.id_file_path)
78
+ FileUtils.rm_f(machine.provider.metadata_file_path)
79
+
80
+ # Invalidate state cache to ensure fresh state on next query
81
+ machine.provider.invalidate_state_cache
82
+
83
+ # Continue middleware chain
84
+ @app.call(env)
85
+ end
86
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../util/orbstack_cli'
4
+ require_relative 'machine_validation'
5
+
6
+ module VagrantPlugins
7
+ module OrbStack
8
+ module Action
9
+ # Action middleware for halting (stopping) OrbStack machines
10
+ #
11
+ # This middleware handles stopping a running OrbStack machine gracefully.
12
+ # It calls the OrbStack CLI to stop the machine and invalidates the
13
+ # provider's state cache to ensure subsequent state queries are fresh.
14
+ #
15
+ # @example Usage in action builder
16
+ # Vagrant::Action::Builder.new.tap do |b|
17
+ # b.use VagrantPlugins::OrbStack::Action::Halt
18
+ # end
19
+ #
20
+ # @api public
21
+ class Halt
22
+ include MachineValidation
23
+
24
+ # Initialize the middleware.
25
+ #
26
+ # @param app [Object] The next middleware in the chain
27
+ # @param env [Hash] The environment hash containing :machine, :ui, etc.
28
+ # @api public
29
+ def initialize(app, _env)
30
+ @app = app
31
+ end
32
+
33
+ # Execute the middleware.
34
+ #
35
+ # Halts the machine by calling OrbStack CLI stop command,
36
+ # then invalidates the state cache to ensure fresh state queries.
37
+ #
38
+ # @param env [Hash] The environment hash
39
+ # @return [Object] Result from next middleware
40
+ # @raise [CommandExecutionError] If stop command fails
41
+ # @raise [CommandTimeoutError] If stop command times out
42
+ # @api public
43
+ def call(env)
44
+ machine = env[:machine]
45
+ ui = env[:ui]
46
+
47
+ # Validate machine ID exists
48
+ machine_id = validate_machine_id!(machine, 'halt')
49
+
50
+ ui.info("Halting machine '#{machine_id}'...")
51
+
52
+ # Call OrbStack CLI to stop the machine
53
+ Util::OrbStackCLI.stop_machine(machine_id)
54
+
55
+ # Invalidate state cache to ensure fresh state on next query
56
+ machine.provider.invalidate_state_cache
57
+
58
+ # Continue middleware chain
59
+ @app.call(env)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VagrantPlugins
4
+ module OrbStack
5
+ module Action
6
+ # Machine validation mixin for action middleware.
7
+ #
8
+ # This module provides machine ID validation for **Direct Action Pattern**
9
+ # actions that interact directly with OrbStack CLI. Include this module
10
+ # when your action needs to validate machine existence before performing
11
+ # CLI operations.
12
+ #
13
+ # **IMPORTANT**: Do NOT include this module in Composition Actions that
14
+ # orchestrate other actions. Composition actions delegate validation to
15
+ # their composed actions to avoid redundant checks.
16
+ #
17
+ # ## Usage
18
+ #
19
+ # ### Direct Actions (Include this module)
20
+ # - Create, Halt, Start, Destroy
21
+ # - Any action that directly calls Util::OrbStackCLI
22
+ # - Actions that manage state cache directly
23
+ #
24
+ # ### Composition Actions (Do NOT include)
25
+ # - Reload (composes Halt + Start, which both validate)
26
+ # - Any action that only orchestrates via Action::Builder
27
+ #
28
+ # See docs/DESIGN.md "Action Patterns" for complete pattern documentation.
29
+ #
30
+ # @example Include in a direct action
31
+ # class Destroy
32
+ # include MachineValidation
33
+ #
34
+ # def call(env)
35
+ # machine_id = validate_machine_id!(env[:machine], 'destroy')
36
+ # # ...
37
+ # end
38
+ # end
39
+ #
40
+ # @api public
41
+ module MachineValidation
42
+ # Validate that a machine has a non-nil, non-empty ID.
43
+ #
44
+ # @param machine [Vagrant::Machine] The machine to validate
45
+ # @param action_name [String] The action being performed (for error messages)
46
+ # @return [String] The machine ID if valid
47
+ # @raise [ArgumentError] If machine ID is nil or empty
48
+ # @api public
49
+ def validate_machine_id!(machine, action_name)
50
+ machine_id = machine.id
51
+ if machine_id.nil? || machine_id.empty?
52
+ raise ArgumentError,
53
+ "Cannot #{action_name} machine: machine ID is nil or empty"
54
+ end
55
+ machine_id
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VagrantPlugins
4
+ module OrbStack
5
+ module Action
6
+ # Action middleware for reloading (restarting) OrbStack machines.
7
+ #
8
+ # This is a **Composition Action** that orchestrates Halt + Start actions
9
+ # rather than directly interacting with OrbStack CLI. Following the
10
+ # composition pattern, this class:
11
+ #
12
+ # - Does NOT include MachineValidation (composed actions handle validation)
13
+ # - Does NOT directly call OrbStackCLI (delegates to Halt/Start)
14
+ # - Does NOT manage state cache (composed actions handle invalidation)
15
+ #
16
+ # The actual work is performed by the Action::Builder chain in the
17
+ # provider's action method, which composes: Halt → Start → (optional) Provision
18
+ #
19
+ # This class exists as a pattern consistency marker and for potential future
20
+ # pre/post reload logic that doesn't fit in the composed actions.
21
+ #
22
+ # See docs/DESIGN.md "Action Patterns" section for pattern documentation.
23
+ #
24
+ # @example Usage in action builder
25
+ # Vagrant::Action::Builder.new.tap do |b|
26
+ # b.use VagrantPlugins::OrbStack::Action::Reload
27
+ # end
28
+ #
29
+ # @api public
30
+ class Reload
31
+ # Initialize the middleware.
32
+ #
33
+ # @param app [Object] The next middleware in the chain
34
+ # @param env [Hash] The environment hash containing :machine, :ui, etc.
35
+ # @api public
36
+ def initialize(app, _env)
37
+ @app = app
38
+ end
39
+
40
+ # Execute the middleware.
41
+ #
42
+ # This is a pass-through middleware - the actual reload logic is
43
+ # composed in the provider's action method. This class exists to
44
+ # maintain consistency with the action middleware pattern.
45
+ #
46
+ # @param env [Hash] The environment hash
47
+ # @return [Object] Result from next middleware
48
+ # @api public
49
+ def call(env)
50
+ # Continue middleware chain
51
+ @app.call(env)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'machine_validation'
4
+
5
+ module VagrantPlugins
6
+ module OrbStack
7
+ module Action
8
+ # Action middleware for validating SSH readiness before command execution
9
+ #
10
+ # This middleware validates that the machine is in a running state before
11
+ # Vagrant executes SSH commands via `vagrant ssh -c "command"`. It does NOT
12
+ # execute the SSH command itself - Vagrant's built-in SSH layer handles that
13
+ # after this validation middleware approves the operation.
14
+ #
15
+ # Unlike other actions, SSHRun is read-only: it only validates state and does
16
+ # not call OrbStack CLI or modify machine state. This makes it safe to call
17
+ # repeatedly without side effects.
18
+ #
19
+ # @example Usage in action builder
20
+ # Vagrant::Action::Builder.new.tap do |b|
21
+ # b.use VagrantPlugins::OrbStack::Action::SSHRun
22
+ # end
23
+ #
24
+ # @api public
25
+ class SSHRun
26
+ include MachineValidation
27
+
28
+ # Initialize the middleware.
29
+ #
30
+ # @param app [Object] The next middleware in the chain
31
+ # @param env [Hash] The environment hash containing :machine, :ui, etc.
32
+ # @api public
33
+ def initialize(app, _env)
34
+ @app = app
35
+ end
36
+
37
+ # Execute the middleware.
38
+ #
39
+ # Validates that the machine has a valid ID and is in running state.
40
+ # If validation passes, continues the middleware chain for Vagrant to
41
+ # execute the SSH command. If validation fails, raises an error and
42
+ # blocks SSH command execution.
43
+ #
44
+ # This is a read-only operation:
45
+ # - Does NOT call OrbStack CLI (only queries cached state)
46
+ # - Does NOT invalidate state cache (no state changes occur)
47
+ # - Does NOT display UI messages (validation only)
48
+ #
49
+ # @param env [Hash] The environment hash
50
+ # @return [Object] Result from next middleware
51
+ # @raise [ArgumentError] If machine ID is nil or empty (from MachineValidation)
52
+ # @raise [Errors::SSHNotReady] If machine is not in running state
53
+ # @api public
54
+ def call(env)
55
+ machine = env[:machine]
56
+
57
+ # Validate machine ID exists (from MachineValidation)
58
+ validate_machine_id!(machine, 'ssh')
59
+
60
+ # Validate machine is running
61
+ state = machine.provider.state
62
+ raise Errors::SSHNotReady, machine_name: machine.id unless state.id == :running
63
+
64
+ # Continue middleware chain (Vagrant handles SSH execution)
65
+ @app.call(env)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../util/orbstack_cli'
4
+ require_relative 'machine_validation'
5
+
6
+ module VagrantPlugins
7
+ module OrbStack
8
+ module Action
9
+ # Action middleware for starting OrbStack machines
10
+ #
11
+ # This middleware handles starting a stopped OrbStack machine.
12
+ # It calls the OrbStack CLI to start the machine and invalidates the
13
+ # provider's state cache to ensure subsequent state queries are fresh.
14
+ #
15
+ # The OrbStack CLI is idempotent - starting an already running machine
16
+ # is safe and will not cause an error.
17
+ #
18
+ # @example Usage in action builder
19
+ # Vagrant::Action::Builder.new.tap do |b|
20
+ # b.use VagrantPlugins::OrbStack::Action::Start
21
+ # end
22
+ #
23
+ # @api public
24
+ class Start
25
+ include MachineValidation
26
+
27
+ # Initialize the middleware.
28
+ #
29
+ # @param app [Object] The next middleware in the chain
30
+ # @param env [Hash] The environment hash containing :machine, :ui, etc.
31
+ # @api public
32
+ def initialize(app, _env)
33
+ @app = app
34
+ end
35
+
36
+ # Execute the middleware.
37
+ #
38
+ # Starts the machine by calling OrbStack CLI start command,
39
+ # then invalidates the state cache to ensure fresh state queries.
40
+ #
41
+ # @param env [Hash] The environment hash
42
+ # @return [Object] Result from next middleware
43
+ # @raise [CommandExecutionError] If start command fails
44
+ # @raise [CommandTimeoutError] If start command times out
45
+ # @raise [OrbStackNotInstalledError] If OrbStack CLI is not available
46
+ # @api public
47
+ def call(env)
48
+ machine = env[:machine]
49
+ ui = env[:ui]
50
+
51
+ # Validate machine ID exists
52
+ machine_id = validate_machine_id!(machine, 'start')
53
+
54
+ ui.info("Starting machine '#{machine_id}'...")
55
+
56
+ # Call OrbStack CLI to start the machine
57
+ Util::OrbStackCLI.start_machine(machine_id)
58
+
59
+ # Invalidate state cache to ensure fresh state on next query
60
+ machine.provider.invalidate_state_cache
61
+
62
+ # Continue middleware chain
63
+ @app.call(env)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VagrantPlugins
4
+ module OrbStack
5
+ # Action middleware namespace for OrbStack provider.
6
+ #
7
+ # Contains action classes that implement Vagrant middleware operations
8
+ # for OrbStack machine lifecycle management (create, halt, destroy, etc).
9
+ #
10
+ # @api public
11
+ module Action
12
+ # Action middleware autoload definitions
13
+ autoload :Create, 'vagrant-orbstack/action/create'
14
+ autoload :Destroy, 'vagrant-orbstack/action/destroy'
15
+ autoload :Halt, 'vagrant-orbstack/action/halt'
16
+ autoload :Reload, 'vagrant-orbstack/action/reload'
17
+ autoload :SSHRun, 'vagrant-orbstack/action/ssh_run'
18
+ autoload :Start, 'vagrant-orbstack/action/start'
19
+ end
20
+ end
21
+ end