Class: Spoon::Client
- Inherits:
-
Object
- Object
- Spoon::Client
- Defined in:
- lib/spoon.rb
Instance Attribute Summary collapse
-
#options ⇒ Object
Returns the value of attribute options.
Class Method Summary collapse
- .apply_prefix(name) ⇒ Object
-
.combine_config ⇒ Object
This combines our default configs with our command line and config file configurations in the desired precedence.
- .confirm_delete?(name) ⇒ Boolean
- .container_config(name) ⇒ Object
- .D(message) ⇒ Object
- .docker_url ⇒ Object
- .get_all_containers ⇒ Object
- .get_container(name) ⇒ Object
- .get_port(port, container) ⇒ Object
- .get_port_forwards(forwards = "") ⇒ Object
- .get_running_containers ⇒ Object
- .get_spoon_containers ⇒ Object
- .host_available?(hostname, port) ⇒ Boolean
- .image_build ⇒ Object
- .image_list ⇒ Object
- .image_name(container) ⇒ Object
- .instance_connect(name, command = '') ⇒ Object
- .instance_copy_authorized_keys(name, keyfile) ⇒ Object
- .instance_copy_files(name) ⇒ Object
- .instance_create(name) ⇒ Object
- .instance_destroy(name) ⇒ Object
- .instance_exists?(name) ⇒ Boolean
- .instance_kill(name) ⇒ Object
- .instance_list ⇒ Object
- .instance_network(name) ⇒ Object
- .instance_restart(name) ⇒ Object
- .instance_run_actions(name) ⇒ Object
- .instance_ssh(name, command = '', exec = true) ⇒ Object
- .instance_start(container) ⇒ Object
- .is_running?(container) ⇒ Boolean
- .main ⇒ Object
- .parse(args) ⇒ Object
- .print_docker_response(json) ⇒ Object
- .print_parsed_response(response) ⇒ Object
- .remove_prefix(name) ⇒ Object
- .strip_slash(name) ⇒ Object
Instance Attribute Details
#options ⇒ Object
Returns the value of attribute options.
11 12 13 |
# File 'lib/spoon.rb', line 11 def @options end |
Class Method Details
.apply_prefix(name) ⇒ Object
196 197 198 |
# File 'lib/spoon.rb', line 196 def self.apply_prefix(name) "#{@options[:prefix]}#{name}" end |
.combine_config ⇒ Object
This combines our default configs with our command line and config file configurations in the desired precedence
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/spoon.rb', line 16 def self.combine_config config = self.parse(ARGV) # init config only options @options["pre-build-commands"] = [] @options[:copy_on_create] = [] @options[:run_on_create] = [] @options[:add_authorized_keys] = false @options[:command] = '' # init command line options @options[:builddir] = '.' @options[:url] = ::Docker.url @options[:image] = 'spoon-pairing' @options[:prefix] = 'spoon-' @options[:privileged] ||= false # Eval config file D "Config file is: #{config[:config]}" = {} if File.exists?(config[:config]) eval(File.read(config[:config])) else puts "File #{config[:config]} does not exist" exit(1) end # Read in config file values .each do |k, v| @options[k] = v end # Read in command line values config.each do |k, v| @options[k] = v end @options end |
.confirm_delete?(name) ⇒ Boolean
186 187 188 189 190 191 192 193 194 |
# File 'lib/spoon.rb', line 186 def self.confirm_delete?(name) if @options[:force] return true else print "Are you sure you want to delete #{name}? (y/n) " answer = $stdin.gets.chomp.downcase return answer == "y" end end |
.container_config(name) ⇒ Object
482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 |
# File 'lib/spoon.rb', line 482 def self.container_config(name) data = { :Cmd => 'runit', :Image => @options[:image], :AttachStdout => true, :AttachStderr => true, :Privileged => @options[:privileged], :PublishAllPorts => true, :Tty => true } # Yes, this key must be a string data['name'] = name data[:CpuShares] = @options[:cpu] if @options[:cpu] data[:Dns] = @options[:dns] if @options[:dns] data[:Hostname] = remove_prefix(name) data[:Memory] = @options[:memory] if @options[:memory] ports = ['22'] + Array(@options[:ports]).map { |mapping| mapping.to_s } ports.compact! data[:PortSpecs] = ports data[:PortBindings] = ports.inject({}) do |bindings, mapping| guest_port, host_port = mapping.split(':').reverse bindings["#{guest_port}/tcp"] = [{ :HostIp => '', :HostPort => host_port || '' }] bindings end data[:Volumes] = Hash[Array(@options[:volume]).map { |volume| [volume, {}] }] data end |
.D(message) ⇒ Object
540 541 542 543 544 |
# File 'lib/spoon.rb', line 540 def self.D() if @options[:debug] puts "D: #{}" end end |
.docker_url ⇒ Object
532 533 534 |
# File 'lib/spoon.rb', line 532 def self.docker_url ::Docker.url = @options[:url] end |
.get_all_containers ⇒ Object
435 436 437 |
# File 'lib/spoon.rb', line 435 def self.get_all_containers ::Docker::Container.all(:all => true) end |
.get_container(name) ⇒ Object
469 470 471 472 473 474 475 476 477 478 479 480 |
# File 'lib/spoon.rb', line 469 def self.get_container(name) docker_url container_list = get_all_containers l_name = strip_slash(name) container_list.each do |container| if container.info["Names"].first.to_s == "/#{l_name}" return container end end return nil end |
.get_port(port, container) ⇒ Object
536 537 538 |
# File 'lib/spoon.rb', line 536 def self.get_port(port, container) container.json['NetworkSettings']['Ports']["#{port}/tcp"].first['HostPort'] end |
.get_port_forwards(forwards = "") ⇒ Object
364 365 366 367 368 369 370 371 372 |
# File 'lib/spoon.rb', line 364 def self.get_port_forwards(forwards = "") if @options[:portforwards] @options[:portforwards].each do |port| (lport,rport) = port.split(':') forwards << "-L #{lport}:127.0.0.1:#{rport || lport} " end end return forwards end |
.get_running_containers ⇒ Object
448 449 450 |
# File 'lib/spoon.rb', line 448 def self.get_running_containers ::Docker::Container.all end |
.get_spoon_containers ⇒ Object
439 440 441 442 443 444 445 446 |
# File 'lib/spoon.rb', line 439 def self.get_spoon_containers container_list = get_all_containers.select { |c| c.info["Names"].first.to_s.start_with? "/#{@options[:prefix]}" } unless container_list.empty? return container_list.sort { |c1, c2| c1.info["Names"].first.to_s <=> c2.info["Names"].first.to_s } else return container_list end end |
.host_available?(hostname, port) ⇒ Boolean
519 520 521 522 523 524 525 526 527 528 529 530 |
# File 'lib/spoon.rb', line 519 def self.host_available?(hostname, port) socket = TCPSocket.new(hostname, port) IO.select([socket], nil, nil, 5) rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError sleep(0.25) false rescue Errno::EPERM, Errno::ETIMEDOUT false ensure socket && socket.close end |
.image_build ⇒ Object
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
# File 'lib/spoon.rb', line 204 def self.image_build # Run pre-build commands @options["pre-build-commands"].each do |command| sh command end unless @options["pre-build-commands"].nil? D "pre-build commands complete, building Docker image" docker_url build_opts = { 't' => @options[:image], 'rm' => true } docker_connection = ::Docker::Connection.new(@options[:url], :read_timeout => 3000) # Quick sanity check for Dockerfile unless File.exist?("#{@options[:builddir]}/Dockerfile") puts "Directory `#{@options[:builddir]}` must contain a Dockerfile... cannot continue" exit(1) end ::Docker::Image.build_from_dir(@options[:builddir], build_opts, docker_connection) do |chunk| print_docker_response(chunk) end end |
.image_list ⇒ Object
226 227 228 229 230 231 232 |
# File 'lib/spoon.rb', line 226 def self.image_list docker_url ::Docker::Image.all.each do |image| next if image.info["RepoTags"] == ["<none>:<none>"] puts "Image: #{image.info["RepoTags"]}" end end |
.image_name(container) ⇒ Object
292 293 294 295 |
# File 'lib/spoon.rb', line 292 def self.image_name(container) env = Hash[container.json['Config']['Env'].collect { |v| v.split('=') }] return env['IMAGE_NAME'] || container.json['Config']['Image'] end |
.instance_connect(name, command = '') ⇒ Object
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/spoon.rb', line 256 def self.instance_connect(name, command='') docker_url if not instance_exists? name puts "The '#{name}' container doesn't exist, creating..." instance_create(name) (name, @options[:add_authorized_keys]) instance_copy_files(name) instance_run_actions(name) end container = get_container(name) unless is_running?(container) instance_start(container) end puts "Connecting to `#{name}`" instance_ssh(name, command) end |
.instance_copy_authorized_keys(name, keyfile) ⇒ Object
399 400 401 402 403 404 405 406 407 408 409 410 |
# File 'lib/spoon.rb', line 399 def self.(name, keyfile) D "Setting up authorized_keys file" # We sleep this once to cope w/ slow starting ssh daemon on create sleep 1 if keyfile full_keyfile = "#{ENV['HOME']}/.ssh/#{keyfile}" key = File.read(full_keyfile).chop D "Read keyfile `#{full_keyfile}` with contents:\n#{key}" cmd = "mkdir -p .ssh ; chmod 700 .ssh ; echo '#{key}' >> .ssh/authorized_keys" instance_ssh(name, cmd, false) end end |
.instance_copy_files(name) ⇒ Object
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 |
# File 'lib/spoon.rb', line 412 def self.instance_copy_files(name) @options[:copy_on_create].each do |file| D "Copying file #{file}" container = get_container(name) host = URI.parse(@options[:url]).host if container ssh_port = get_port('22', container) puts "Waiting for #{name}:#{ssh_port}..." until host_available?(host, ssh_port) return if @options[:nologin] system("scp -o StrictHostKeyChecking=no -P #{ssh_port} #{ENV['HOME']}/#{file} pairing@#{host}:#{file}") else puts "No container named: #{container.inspect}" end end end |
.instance_create(name) ⇒ Object
513 514 515 516 517 |
# File 'lib/spoon.rb', line 513 def self.instance_create(name) docker_url container = ::Docker::Container.create(container_config(name)) container = container.start(container_config(name)) end |
.instance_destroy(name) ⇒ Object
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 |
# File 'lib/spoon.rb', line 331 def self.instance_destroy(name) docker_url container = get_container(name) if container if confirm_delete?(name) puts "Destroying #{name}" begin container.kill rescue puts "Failed to kill container #{container.id}" end container.wait(10) begin container.delete(:force => true) rescue puts "Failed to remove container #{container.id}" end puts "Done!" else puts "Delete aborted.. #{name} lives to pair another day." end else puts "No container named: #{name}" end end |
.instance_exists?(name) ⇒ Boolean
360 361 362 |
# File 'lib/spoon.rb', line 360 def self.instance_exists?(name) get_container(name) end |
.instance_kill(name) ⇒ Object
463 464 465 466 467 |
# File 'lib/spoon.rb', line 463 def self.instance_kill(name) container = get_container(name) container.kill puts "Container #{name} killed" end |
.instance_list ⇒ Object
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/spoon.rb', line 275 def self.instance_list docker_url puts "List of available spoon containers:" container_list = get_spoon_containers if container_list.empty? puts "No spoon containers running at #{@options[:url]}" exit end max_width_container_name = remove_prefix(container_list.max_by {|c| c.info["Names"].first.to_s.length }.info["Names"].first.to_s) max_name_width = max_width_container_name.length container_list.each do |container| name = container.info["Names"].first.to_s running = is_running?(container) ? Rainbow("Running").green : Rainbow("Stopped").red puts "#{remove_prefix(name)} [ #{running} ]".rjust(max_name_width + 22) + " " + Rainbow(image_name(container)).yellow end end |
.instance_network(name) ⇒ Object
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 |
# File 'lib/spoon.rb', line 313 def self.instance_network(name) docker_url container = get_container(name) if is_running?(container) host = URI.parse(@options[:url]).host puts "Host: #{host}" ports = container.json['NetworkSettings']['Ports'] ports.each do |p_name, p_port| tcp_name = p_name.split('/')[0] puts "#{tcp_name} -> #{p_port.first['HostPort']}" end else puts "Container is not running, cannot show ports" end end |
.instance_restart(name) ⇒ Object
456 457 458 459 460 461 |
# File 'lib/spoon.rb', line 456 def self.instance_restart(name) container = get_container(name) container.kill container.start! puts "Container #{name} restarted" end |
.instance_run_actions(name) ⇒ Object
428 429 430 431 432 433 |
# File 'lib/spoon.rb', line 428 def self.instance_run_actions(name) @options[:run_on_create].each do |action| puts "Running command: #{action}" instance_ssh(name, action, false) end end |
.instance_ssh(name, command = '', exec = true) ⇒ Object
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 |
# File 'lib/spoon.rb', line 374 def self.instance_ssh(name, command='', exec=true) container = get_container(name) forwards = get_port_forwards D "Got forwards: #{forwards}" host = URI.parse(@options[:url]).host if container ssh_command = "\"#{command}\"" if not command.empty? ssh_port = get_port('22', container) puts "Waiting for #{name}:#{ssh_port}..." until host_available?(host, ssh_port) = "-t -o StrictHostKeyChecking=no -p #{ssh_port} #{forwards} " << "-v " if @options[:debugssh] ssh_cmd = "ssh #{} pairing@#{host} #{ssh_command}" puts "SSH Forwards: #{forwards}" unless forwards.empty? D "SSH CMD: #{ssh_cmd}" return if @options[:nologin] if exec exec(ssh_cmd) else system(ssh_cmd) end else puts "No container named: #{container.inspect}" end end |
.instance_start(container) ⇒ Object
452 453 454 |
# File 'lib/spoon.rb', line 452 def self.instance_start(container) container.start! end |
.is_running?(container) ⇒ Boolean
305 306 307 308 309 310 311 |
# File 'lib/spoon.rb', line 305 def self.is_running?(container) if /^Up.+/ =~ container.info["Status"] return $~ else return false end end |
.main ⇒ Object
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/spoon.rb', line 56 def self.main instance = false @options = combine_config instance = ARGV[0] D @options.inspect if @options[:list] instance_list elsif @options["list-images"] image_list elsif @options[:build] image_build elsif @options[:destroy] instance_destroy(apply_prefix(@options[:destroy])) elsif @options[:kill] instance_kill(apply_prefix(@options[:kill])) elsif @options[:restart] instance_restart(apply_prefix(@options[:restart])) elsif @options[:network] instance_network(apply_prefix(@options[:network])) elsif instance instance_connect(apply_prefix(instance), @options[:command]) else puts("You either need to provide an action or an instance to connect to") exit end end |
.parse(args) ⇒ Object
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/spoon.rb', line 84 def self.parse(args) config = {} optparser = OptionParser.new do |opts| opts. = "Usage: spoon [options] [instance name]\n\n" opts. += "Create & Connect to pairing environments in Docker\n\n" opts.program_name = "spoon" opts.on("-l", "--list", "List available spoon instances") do config[:list] = true end opts.on("-d", "--destroy NAME", "Destroy spoon instance with NAME") do |destroy| config[:destroy] = destroy end opts.on("-b", "--build", "Build image from Dockerfile using name passed to --image") do config[:build] = true end opts.on("-n", "--network NAME", "Display exposed ports using name passed to NAME") do |name| config[:network] = name end opts.on("--restart NAME", "Restart the specified spoon instance") do |name| config[:restart] = name end opts.on("--kill NAME", "Kill the specified spoon instance") do |name| config[:kill] = name end config[:config] = "#{ENV['HOME']}/.spoonrc" opts.on("-c", "--config FILE", "Config file to use for spoon @options") do |c| config[:config] = c end opts.on("--builddir DIR", "Directory containing Dockerfile") do |b| config[:builddir] = b end opts.on("--url URL", "Docker url to connect to") do |url| config[:url] = url end opts.on("--list-images", "List available spoon images") do config["list-images"] = true end opts.on("--image NAME", "Use image for spoon instance") do |image| config[:image] = image end opts.on("--prefix PREFIX", "Prefix for container names") do |prefix| config[:prefix] = prefix end opts.on("--privileged", "Enable privileged mode for new containers") do |privileged| config[:privileged] = true end opts.on("--force", "Skip any confirmations") do config[:force] = true end opts.on("--nologin", "Do not ssh to contianer, just create") do config[:nologin] = true end opts.on("--debug", "Enable debug") do config[:debug] = true end opts.on("--debugssh", "Enable SSH debugging") do config[:debugssh] = true end opts.on("--ports PORT", Array, "Expose additional docker ports") do |ports| config[:ports] = ports end opts.on("--portforwards PORT", Array, "Forward PORT over ssh (must be > 1023)") do |portforwards| config[:portforwards] = portforwards end opts.on("--version", "Show version") do puts Spoon::VERSION exit end opts.on("-h", "--help", "Show help") do puts opts exit end end begin optparser.parse!(ARGV) rescue OptionParser::MissingArgument, OptionParser::InvalidOption puts $!.to_s puts optparser exit(1) end config end |
.print_docker_response(json) ⇒ Object
252 253 254 |
# File 'lib/spoon.rb', line 252 def self.print_docker_response(json) print_parsed_response(JSON.parse(json)) end |
.print_parsed_response(response) ⇒ Object
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 |
# File 'lib/spoon.rb', line 234 def self.print_parsed_response(response) case response when Hash response.each do |key, value| case key when 'stream' puts value else puts "#{key}: #{value}" end end when Array response.each do |hash| print_parsed_response(hash) end end end |
.remove_prefix(name) ⇒ Object
200 201 202 |
# File 'lib/spoon.rb', line 200 def self.remove_prefix(name) name.sub(/\/?#{@options[:prefix]}/, '') end |
.strip_slash(name) ⇒ Object
297 298 299 300 301 302 303 |
# File 'lib/spoon.rb', line 297 def self.strip_slash(name) if name.start_with? "/" name[1..-1] else name end end |