module Jarl class Application attr_reader :group, :name, :hostname, :base_path attr_reader :image, :scale, :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, base_path) @group = group @name = name @base_path = base_path @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 @scale = params['scale'] || 1 unless @scale.is_a?(Integer) && @scale > 0 fail "Application '#{self}' has invalid 'scale' definition, Integer > 0 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 image_is_a_registry_path? image =~ /^.+:\d+\// end def running? instances.any?(&:running?) end def instances running_instances + stopped_instances end def running_containers @running_containers ||= Docker.containers_running.select do |c| c.name =~ /^#{full_name}(\.\d+)?$/ end end def running_instances @running_instances ||= running_containers.map do |c| n = c.name.sub(/^#{full_name}\.?/, '') n = n.empty? ? nil : n.to_i Instance.new(self, n, c) end end def stopped_instances return @stopped_instances if @stopped_instances n_range = scale == 1 ? [nil] : (1..scale).to_a @stopped_instances = n_range.reject do |n| running_instances.map(&:n).include?(n) end @stopped_instances = @stopped_instances.map do |n| Instance.new(self, n, nil) end @stopped_instances end # Start instances of the application # def start 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(command = nil) fail 'Not a running application' unless running? instances.first.ssh(command) end def build Docker::Image.new(image_name, image_full_path).build! end def pull Docker::Image.new(image_name, image_full_path).pull! end def image_name image_is_a_path? ? File.basename(image) : image end def image_path image_is_a_path? ? image : nil end def image_full_path image_is_a_path? ? Pathname.new(base_path).join(image).expand_path : 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, :n, :container # @param application [Application] # @param n [nil,Integer] Instance number, nil if an instance is the primary instance # @param container [nil,Docker::Container] Running container, if present # def initialize(application, n, container) @application = application @n = n @container = container end def running? !@container.nil? end def full_name n ? "#{application.full_name}.#{n}" : application.full_name end def hostname n ? "#{application.hostname}-#{n}" : application.hostname end def stop! return unless running? @container.stop! end def start! return if running? # puts esc_yellow("Starting #{application.name}.#{n}") if n Docker.start( name: full_name, hostname: hostname, image: application.image_name, volumes: application.volumes, ports: application.ports, environment: application.environment, command: application.command ) end def tail_log(since = nil) return unless running? color_code = Console::Colors::SEQUENCE[application.serial % Console::Colors::SEQUENCE.size] begin cmd = "docker logs #{since ? "--since #{since}" : nil} -f #{container.id}" IO.popen(cmd, 'r', external_encoding: Encoding::UTF_8) do |p| str = '<<< STARTED >>>' while str str = p.gets str.split("\n").each do |s| puts "#{esc_color color_code, full_name}: #{s}" end end end rescue Interrupt # end end def ssh(command = nil) fail "Failed to open SSH, no running container for '#{full_name}'" unless running? container.open_ssh_session!(Jarl.config.params, command) end end # class Instance end # class Application end # module Jarl