FreedomBen/dory

View on GitHub
bin/dory

Summary

Maintainability
Test Coverage
#!/usr/bin/env ruby

require 'thor'
require 'yaml'
require 'json'

require 'dory'

class DoryBin < Thor
  class_option :verbose, type: :boolean, aliases: 'v', default: false

  desc 'upgrade', 'Upgrade dory to the latest version'
  long_desc <<-LONGDESC
    Upgrades dory to the latest version.  Old versions are cleaned up

    If dory was installed with sudo, this may not work.  You will
    have to do it manually:

      sudo gem install dory   # install the latest versions
      sudo gem cleanup dory   # cleanup old versions

    > $ dory upgrade
  LONGDESC
  def upgrade
    exec_upgrade(options)
  end

  desc 'up', 'Bring up dory services (nginx-proxy, dnsmasq, resolv)'
  long_desc <<-LONGDESC
    Bring up dory services (nginx-proxy, dnsmasq, resolv)

    When run, the docker container for the nginx proxy is started,
    along with a local dnsmasq instance to resolve DNS requests
    for your custom domain to the nginx proxy.  The local resolver
    will also be configured to use the dnsmasq instance as a nameserver

    Optionally, you can pass a list of service names as arguments
    to have only one or two services started.

    > $ dory up [proxy] [dns] [resolv]
  LONGDESC
  def up(*services)
    exec_up(options, services)
  end

  desc 'down', 'Stop all dory services'
  long_desc <<-LONGDESC
    Stops all dory services.  Can optionally pass [-d|--destroy]
    to destroy the containers when they stop.

    Optionally, you can pass a list of service names as arguments
    to have only one or two services started.

    > $ dory down [-d|--destroy] [proxy] [dns] [resolv]
  LONGDESC
  option :destroy, type: :boolean, aliases: 'd', default: true
  def down(*services)
    exec_down(options, services)
  end

  desc 'version', 'Check current installed version of dory'
  def version
    puts "Dory - Version: #{Dory.version}"
  end

  desc 'restart', 'Stop and restart all dory services'
  long_desc <<-LONGDESC
    Stop and restart dory services (nginx-proxy, dnsmasq, resolv)

    > $ dory restart [-d|--destroy]
  LONGDESC
  option :destroy, type: :boolean, aliases: 'd', default: true
  def restart(*services)
    exec_down(options, services)
    exec_up(options, services)
  end

  desc 'status', 'Report status of the dory services'
  long_desc <<-LONGDESC
    Checks the current status of the services managed by dory.
    This includes nginx-proxy, dnsmasq, and resolv

    > $ dory status
  LONGDESC
  def status
    exec_status(options)
  end

  desc 'config-file', 'Write a default config file'
  long_desc <<-LONGDESC
    Writes a dory config file to #{Dory::Config.filename}
    containing the default settings.  This can then be configured
    as preferred.

    > $ dory config-file [--upgrade] [--force]
  LONGDESC
  option :upgrade, type: :boolean, aliases: 'u', default: false
  option :force, type: :boolean, aliases: 'f', default: false
  def config_file
    exec_config_file(options)
  end

  desc 'attach', 'Attach to the output of a docker service container'
  long_desc <<-LONGDESC
    For the nginx proxy and dnsmasq containers, this command
    provides a pass-through to the underlying docker attach command.
    It is simply more convenient than looking up the docker
    container name and using the equivalent 'docker attach'

    > $ dory attach [proxy] [dns]
  LONGDESC
  def attach(service = nil)
    exec_attach(service, options)
  end

  desc 'pull', 'Pull down the docker images that dory uses'
  long_desc <<-LONGDESC
    This will pull down the docker images that dory uses.
    You don't need to run this command as dory will automatically
    pull down the images when needed if they aren't there already.
    However, you may wish to pull down in advance, and for that
    purpose this command is here for you.

    > $ dory pull [proxy] [dns]
  LONGDESC
  def pull(*services)
    exec_pull(services, options)
  end

  desc 'ip', 'Grab the IPv4 address of a running dory service'
  long_desc <<-LONGDESC
    For the nginx proxy and dnsmasq containers, this command
    grabs the IP address of the container.  You could get this
    info from `docker inspect` but this is more convenient.

    > $ dory ip [proxy] [dns]
  LONGDESC
  def ip(service = nil)
    exec_ip(service, options)
  end

  private

  def self.exit_on_failure?
    true
  end

  def exec_pull(services, _options)
    servs = services.empty? ? %w[proxy dns] : services
    servs = sanitize_services(servs)
    return unless servs

    unless Dory::DockerService.docker_installed?
      puts "Docker does not appear to be installed /o\\".red
      puts "Docker is required for DNS and Nginx proxy.  These can be " \
        "disabled in the config file if you don't need them.\n".yellow
      puts "Please install docker and try again".red
      return
    end

    servs.each do |service|
      imgname = if service == 'proxy'
                  Dory::Proxy.dory_http_proxy_image_name
                elsif service == 'dns'
                  Dory::Dnsmasq.dnsmasq_image_name
                else
                  nil
                end
      if imgname
        puts "Pulling image '#{imgname}'...".green
        if Dory::Sh.run_command("docker pull #{imgname}").success?
          puts "Successfully pulled image '#{imgname}'".green
        else
          puts "Error pulling docker image '#{imgname}'".red
        end
      else
        puts "Can only pull 'proxy' or 'dns', you tried to pull '#{service}'".red
      end
    end
  end

  def exec_ip(service, _options)
    s = sanitize_service(service)
    mod = if s == 'proxy'
            Dory::Proxy
          elsif s == 'dns'
            Dory::Dnsmasq
          else
            nil
          end
    if mod
      unless mod.running?
        puts "Service '#{service}' is not running.  Starting...".green
        if mod.start
          puts "Service '#{service}' successfully started.".green
        else
          puts "Error starting service '#{service}'".red
        end
      end
      if mod.running?
        inspect = Dory::Sh.run_command("docker inspect #{mod.container_name}")
        if inspect.success?
          begin
            puts JSON.parse(inspect.stdout).first['NetworkSettings']['IPAddress']
          rescue StandardError => e
            puts "Error parsing JSON from container #{mod.container_name}:".red
            puts e.message.red
          end
        else
          puts "Error inspecting docker container #{mod.container_name}!".red
        end
      else
        puts "Service '#{service}' is not running.  Cannot get IP address".red
      end
    else
      puts "Can only get IP address for 'proxy' or 'dns'".red
    end
  end

  def exec_attach(service, _options)
    s = sanitize_service(service)
    mod = if s == 'proxy'
            Dory::Proxy
          elsif s == 'dns'
            Dory::Dnsmasq
          else
            nil
          end
    if mod
      unless mod.running?
        puts "Service '#{service}' is not running.  Starting...".green
        if mod.start
          puts "Service '#{service}' successfully started.".green
        else
          puts "Error starting service '#{service}'".red
        end
      end
      if mod.running?
        puts "Note that if you Ctrl + C after attaching,".yellow
        puts "the service will need to be restarted.".yellow
        puts "Service '#{service}' is running.".green
        puts "Attaching to docker container '#{mod.container_name}'...".green
        exec("docker attach #{mod.container_name}")
      else
        puts "Service '#{service}' is not running.  Cannot attach".red
      end
    else
      puts "Can only attach to 'proxy' or 'dns'".red
    end
  end

  def exec_upgrade(_options)
    puts "Checking if dory has updates available...".green
    new_version = Dory::Upgrade.new_version
    if new_version
      if Dory::Upgrade.outdated?(new_version)
        puts "New version #{new_version} is available.  You currently have #{Dory.version}.".yellow
        print "Would you like to install the update? (Y/N): ".yellow
        if STDIN.gets.chomp =~ /y/i
          puts "Upgrading dory...".green
          if Dory::Upgrade.install.success?
            if Dory::Upgrade.cleanup.success?
              puts "New version installed successfully!\n" \
                   "You may want to upgrade your config file with:\n\n" \
                   "    dory config-file --upgrade".green
            else
              puts "Failure cleaning up old versions of dory.  You may want " \
                   "to run 'gem cleanup dory' manually.".red
            end
          else
            puts "Failure installing new version of dory.  If you are " \
                 "installing into a system ruby, this could be because " \
                 "you need to use sudo.  Please try 'gem install dory' " \
                 "manually, and then 'gem cleanup dory' to remove old " \
                 "versions.".red
          end
        else
          puts "Not upgrading.  User declined.".red
        end
      else
        puts "Dory is up to date!  Nothing to do".green
      end
    else
      puts "Encountered an error checking the latest version from Rubygems".red
    end
  end

  def config_file_action(options)
    if options[:upgrade]
      'u'
    elsif options[:force]
      'o'
    else
      print "A config file already exists at #{Dory::Config.filename}.  [U]pgrade, [O]verwrite with default settings, or do [N]othing? (U/O/N): ".yellow
      STDIN.gets.chomp
    end
  end

  def take_action(action)
    if action =~ /u/i
      puts "Upgrading config file at #{Dory::Config.filename}".green
      Dory::Config.upgrade_settings_file
    elsif action =~ /o/i
      puts "Overwriting config file with new version at #{Dory::Config.filename}".green
      Dory::Config.write_default_settings_file
    else
      puts "User declined.  Not writing config file".red
      return
    end
  end

  def exec_config_file(options)
    if File.exist?(Dory::Config.filename)
      take_action(config_file_action(options))
    else
      puts "Writing new config file to #{Dory::Config.filename}".green
      Dory::Config.write_default_settings_file
    end
  end

  def exec_up(options, services)
    services = sanitize_services(services)
    return unless services

    puts "Reading settings file at '#{Dory::Config.filename}'".green if options[:verbose]
    settings = Dory::Config.settings
    if services.include?('proxy')
      if nginx_proxy_enabled?(settings)
        puts "nginx_proxy enabled in config file".green if options[:verbose]
        if Dory::Proxy.start
          puts "Successfully started nginx proxy".green
        else
          puts "Error starting nginx proxy".red
        end
      else
        puts "nginx_proxy disabled in config file".yellow
      end
    end

    if services.include?('dns')
      if dnsmasq_enabled?(settings)
        puts "dnsmasq enabled in config file".green if options[:verbose]
        if Dory::Dnsmasq.start
          puts "Successfully started dnsmasq".green
        else
          puts "Error starting dnsmasq".red
        end
      else
        puts "dnsmasq disabled in config file".yellow
      end
    end

    if services.include?('resolv')
      if resolv_enabled?(settings)
        if Dory::Resolv.configure
          puts "Successfully configured local resolver".green
        else
          puts "Error configuring local resolver".red
        end
        puts "resolv enabled in config file".green if options[:verbose]
      else
        puts "resolv disabled in config file".yellow
      end
    end
  end

  def exec_status(_options)
    puts "Reading settings file at '#{Dory::Config.filename}'".green if options[:verbose]
    settings = Dory::Config.settings

    if Dory::Proxy.running?
      puts "[*] Nginx proxy:  Running as docker container #{Dory::Proxy.container_name}".green
    elsif !nginx_proxy_enabled?(settings)
      puts "[*] Nginx proxy is disabled in config file".yellow
    else
      puts "[*] Nginx proxy is not running".red
    end

    if Dory::Dnsmasq.running?
      puts "[*] Dnsmasq:  Running as docker container #{Dory::Dnsmasq.container_name}".green
    elsif !dnsmasq_enabled?(settings)
      puts "[*] Dnsmasq is disabled in config file".yellow
    else
      puts "[*] Dnsmasq is not running".red
    end

    if Dory::Resolv.has_our_nameserver?
      puts "[*] Resolv:  configured with #{Dory::Resolv.file_nameserver_line}".green
    elsif !resolv_enabled?(settings)
      puts "[*] Resolv is disabled in config file".yellow
    else
      puts "[*] Resolv is not configured".red
    end
  end

  def exec_down(options, services)
    services = sanitize_services(services)
    return unless services

    puts "Reading settings file at '#{Dory::Config.filename}'".green if options[:verbose]
    settings = Dory::Config.settings

    if services.include?('resolv')
      if Dory::Resolv.clean
        if resolv_enabled?(settings)
          puts "nameserver removed from resolv file".green
        else
          puts "Resolv disabled in config file".yellow
        end
      else
        puts "Unable to remove nameserver from resolv file".red
      end
    end

    if services.include?('dns')
      if Dory::Dnsmasq.stop
        if dnsmasq_enabled?(settings)
          puts "Dnsmasq container stopped".green
          if options[:destroy]
            if Dory::Dnsmasq.delete
              puts "Dnsmasq container successfully deleted".green
            else
              puts "Dnsmasq container failed to delete".red
            end
          end
        else
          puts "dnsmasq disabled in config file".yellow
        end
      else
        puts "Dnsmasq container failed to stop".red
      end
    end

    if services.include?('proxy')
      if Dory::Proxy.stop
        if nginx_proxy_enabled?(settings)
          puts "Nginx proxy stopped".green
          if options[:destroy]
            if Dory::Proxy.delete
              puts "Nginx proxy container successfully deleted".green
            else
              puts "Nginx proxy container failed to delete".red
            end
          end
        else
          puts "Nginx proxy disabled in config file".yellow
        end
      else
        puts "Nginx proxy failed to stop".red
      end
    end
  end

  def nginx_proxy_enabled?(settings)
    settings[:dory][:nginx_proxy][:enabled]
  end

  def nginx_proxy_disabled?(settings)
    !nginx_proxy_enabled?(settings)
  end

  def dnsmasq_enabled?(settings)
    settings[:dory][:dnsmasq][:enabled]
  end

  def dnsmasq_disabled?(settings)
    !dnsmasq_enabled?(settings)
  end

  def resolv_enabled?(settings)
    settings[:dory][:resolv][:enabled]
  end

  def resolv_disabled?(settings)
    !resolv_enabled?(settings)
  end

  def valid_services
    %w[proxy dns resolv]
  end

  def canonical_service(service)
    {
      'proxy' => 'proxy',
      'nginx' => 'proxy',
      'nginx_proxy' => 'proxy',
      'nginx-proxy' => 'proxy',
      'dns' => 'dns',
      'dnsmasq' => 'dns',
      'resolv' => 'resolv',
      'resolve' => 'resolv'
    }[service]
  end

  def valid_service?(service)
    valid_services.include?(canonical_service(service))
  end

  def valid_services?(services)
    services.all? do |service|
      if valid_service?(service)
        true
      else
        puts "'#{service}' is not valid.  Must be one or more of these: #{valid_services.join(', ')}".red
        false
      end
    end
  end

  def sanitize_service(service)
    return false if service.nil? || service.empty?
    return false unless valid_service?(service)
    canonical_service(service)
  end

  def sanitize_services(services)
    return valid_services if !services || services.empty?
    return false unless valid_services?(services)
    services.map{|s| canonical_service(s) }
  end
end

aliases = {
  'start'  => 'up',
  'stop'   => 'down',
  'update' => 'upgrade'
}

if !ARGV.empty? && %w[-v --version].include?(ARGV.first)
  puts "Dory - Version: #{Dory.version}"
else
  DoryBin.start(ARGV.map { |a| aliases.keys.include?(a) ? aliases[a] : a })
end