Class: Kamal::Dev::StateManager
- Inherits:
-
Object
- Object
- Kamal::Dev::StateManager
- Defined in:
- lib/kamal/dev/state_manager.rb
Overview
Manages deployment state file with file locking for concurrency safety
Provides thread-safe read/write operations to .kamal/dev_state.yml using
File.flock with exclusive locks for writes and shared locks for reads.
State file format:
deployments:
container-name-1:
vm_id: "vm-123"
vm_ip: "1.2.3.4"
container_name: "container-name-1"
status: "running"
deployed_at: "2025-11-16T10:00:00Z"
Defined Under Namespace
Classes: LockTimeoutError
Constant Summary collapse
- LOCK_TIMEOUT =
Lock timeout in seconds
10
Instance Attribute Summary collapse
-
#state_file ⇒ Object
readonly
Returns the value of attribute state_file.
Instance Method Summary collapse
-
#add_compose_deployment(vm_name, vm_id, vm_ip, containers) ⇒ Object
Add a compose stack deployment to state (multiple containers per VM).
-
#add_deployment(deployment) ⇒ Object
Add a new deployment to state (single container).
-
#compose_deployment?(deployment_name) ⇒ Boolean
Check if deployment is a compose stack.
-
#get_containers(deployment_name) ⇒ Array<Hash>
Get containers for a deployment.
-
#initialize(state_file_path) ⇒ StateManager
constructor
Initialize state manager with state file path.
-
#list_deployments ⇒ Hash
List all deployments.
-
#read_state ⇒ Hash
Read state with shared lock (multiple readers allowed).
-
#remove_deployment(name) ⇒ Object
Remove deployment from state.
-
#update_deployment_status(name, new_status) ⇒ Object
Update deployment status.
-
#update_state {|state| ... } ⇒ Object
Update state (read-modify-write pattern with exclusive lock).
-
#write_state(state) ⇒ Object
Write state with exclusive lock (single writer).
Constructor Details
#initialize(state_file_path) ⇒ StateManager
Initialize state manager with state file path
41 42 43 |
# File 'lib/kamal/dev/state_manager.rb', line 41 def initialize(state_file_path) @state_file = state_file_path end |
Instance Attribute Details
#state_file ⇒ Object (readonly)
Returns the value of attribute state_file.
33 34 35 |
# File 'lib/kamal/dev/state_manager.rb', line 33 def state_file @state_file end |
Instance Method Details
#add_compose_deployment(vm_name, vm_id, vm_ip, containers) ⇒ Object
Add a compose stack deployment to state (multiple containers per VM)
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/kamal/dev/state_manager.rb', line 119 def add_compose_deployment(vm_name, vm_id, vm_ip, containers) update_state do |state| state["deployments"] ||= {} state["deployments"][vm_name] = { "vm_id" => vm_id, "vm_ip" => vm_ip, "deployed_at" => Time.now.utc.iso8601, "type" => "compose", "containers" => containers.map do |container| { "name" => container[:name], "service" => container[:service], "image" => container[:image], "status" => container[:status] } end } state end end |
#add_deployment(deployment) ⇒ Object
Add a new deployment to state (single container)
95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/kamal/dev/state_manager.rb', line 95 def add_deployment(deployment) update_state do |state| state["deployments"] ||= {} state["deployments"][deployment[:name]] = { "vm_id" => deployment[:vm_id], "vm_ip" => deployment[:vm_ip], "container_name" => deployment[:container_name], "status" => deployment[:status], "deployed_at" => deployment[:deployed_at] } state end end |
#compose_deployment?(deployment_name) ⇒ Boolean
Check if deployment is a compose stack
189 190 191 192 193 194 195 |
# File 'lib/kamal/dev/state_manager.rb', line 189 def compose_deployment?(deployment_name) state = read_state deployment = state.dig("deployments", deployment_name) return false unless deployment deployment["type"] == "compose" || deployment.key?("containers") end |
#get_containers(deployment_name) ⇒ Array<Hash>
Get containers for a deployment
For single container deployments, returns array with one item. For compose deployments, returns all containers in the stack.
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/kamal/dev/state_manager.rb', line 204 def get_containers(deployment_name) state = read_state deployment = state.dig("deployments", deployment_name) return [] unless deployment if deployment.key?("containers") # Compose deployment - return container array deployment["containers"] else # Single container deployment - wrap in array [{ "name" => deployment["container_name"], "service" => "app", "image" => "unknown", "status" => deployment["status"] }] end end |
#list_deployments ⇒ Hash
List all deployments
Returns deployments in a normalized format, handling both single container and compose (multi-container) deployments.
180 181 182 183 |
# File 'lib/kamal/dev/state_manager.rb', line 180 def list_deployments state = read_state state["deployments"] || {} end |
#read_state ⇒ Hash
Read state with shared lock (multiple readers allowed)
48 49 50 51 52 53 54 55 56 |
# File 'lib/kamal/dev/state_manager.rb', line 48 def read_state with_lock(:shared) do |file| content = file.read return {} if content.empty? YAML.safe_load(content, permitted_classes: [Symbol, Time], aliases: true, symbolize_names: false) || {} end rescue Errno::ENOENT {} # File doesn't exist yet end |
#remove_deployment(name) ⇒ Object
Remove deployment from state
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/kamal/dev/state_manager.rb', line 156 def remove_deployment(name) should_delete_file = false update_state do |state| state["deployments"]&.delete(name) # Mark for deletion if no deployments remain if state["deployments"].nil? || state["deployments"].empty? should_delete_file = true end state end # Delete state file outside of lock File.delete(@state_file) if should_delete_file && File.exist?(@state_file) end |
#update_deployment_status(name, new_status) ⇒ Object
Update deployment status
144 145 146 147 148 149 150 151 |
# File 'lib/kamal/dev/state_manager.rb', line 144 def update_deployment_status(name, new_status) update_state do |state| if state.dig("deployments", name) state["deployments"][name]["status"] = new_status end state end end |
#update_state {|state| ... } ⇒ Object
Update state (read-modify-write pattern with exclusive lock)
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/kamal/dev/state_manager.rb', line 70 def update_state with_lock(:exclusive) do |file| file.rewind content = file.read current_state = if content && !content.empty? YAML.safe_load(content, permitted_classes: [Symbol, Time], aliases: true, symbolize_names: false) || {} else {} end new_state = yield(current_state) atomic_write(new_state) end end |
#write_state(state) ⇒ Object
Write state with exclusive lock (single writer)
61 62 63 |
# File 'lib/kamal/dev/state_manager.rb', line 61 def write_state(state) atomic_write(state) end |