module Jarl class Application attr_reader :group, :name, :hostname attr_reader :image, :volumes, :ports, :environment, :entrypoint, :command attr_reader :serial # Create Application object from application definition: # # app_name: # image: blabla # volumes: # - /host/path:/container/path # ports: # - 1234:5678 # command: blabla # def initialize(group, name, params) @group = group @name = name @image = params['image'] || fail("Application '#{self}' has no 'image' defined") unless @image.is_a?(String) fail "Application '#{self}' has invalid 'image' definition, String is expected" end @volumes = params['volumes'] || [] unless @volumes.is_a?(Array) fail "Application '#{self}' has invalid 'volumes' definition, Array is expected" end @ports = params['ports'] || [] unless @ports.is_a?(Array) fail "Application '#{self}' has invalid 'ports' definition, Array is expected" end @environment = params['environment'] || {} unless @environment.is_a?(Hash) fail "Application '#{self}' has invalid 'environment' definition, Hash is expected" end @entrypoint = params['entrypoint'] if @entrypoint && !@entrypoint.is_a?(String) fail "Application '#{self}' has invalid 'entrypoint' definition, String is expected" end @command = params['command'] if @command && !@command.is_a?(String) fail "Application '#{self}' has invalid 'command' definition, String is expected" end @serial = self.class.next_serial @hostname = @name end def full_name "#{group}.#{name}" end def image_is_a_path? image =~ /^[~\.\/]/ end def running? instances.size > 0 end def instances Docker.containers_running.select { |c| c.name =~ /^#{full_name}(\.\d+)?$/ }.map do |c| Instance.new(self, c) end end # Start +scale+ instances of the application # def start(scale = 1) instances.map(&:stop!) if running? if scale > 1 scale.times { |n| Instance.start(self, n + 1) } else Instance.start(self) end end def execute(execute_command, args) Docker.execute( name: full_name, hostname: hostname, image: image_name, volumes: volumes, ports: ports, environment: environment, command: (execute_command || command || '') + ' ' + args.join(' ') ) end def ssh fail 'Not a running application' unless running? instances.first.ssh end def build fail 'Not a file based image' unless image_is_a_path? Docker::Image.new(image_name, image_path).build! end def image_name image_is_a_path? ? File.basename(image) : image end def image_path image_is_a_path? ? image : nil end def to_s full_name end def self.next_serial @current_serial ||= 0 @current_serial += 1 @current_serial - 1 end # Application::Instance represents a single running instance of the application # class Instance attr_reader :application, :container def initialize(application, container) @application = application @container = container end def stop! @container.stop! end def name @container.name end def n suffix = @container.name.split('.').last suffix =~ /^\d+$/ ? suffix : nil end def tail_log(*_args) color_code = Console::Colors::SEQUENCE[application.serial % Console::Colors::SEQUENCE.size] begin instance_no = n ? ".#{n}" : '' IO.popen "docker logs -f #{container.id}", 'r' do |p| str = '<<< STARTED >>>' while str str = p.gets str.split("\n").each do |s| puts "#{esc_color color_code, application.full_name}#{instance_no}: #{s}" end end end rescue Interrupt # end end def ssh container.open_ssh_session!(Jarl.config.params) end def self.start(application, n = nil) Docker.start( name: (n ? "#{application.full_name}.#{n}" : application.full_name), hostname: (n ? "#{application.hostname}-#{n}" : application.hostname), image: application.image_name, volumes: application.volumes, ports: application.ports, environment: application.environment, command: application.command ) end end # class Instance end # class Application end # module Jarl