bin/dory
#!/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